The standard way to do exactly what you're attempting is a JWT (JSON Web Token), which is a structured data blob containing a cryptographic signature / MAC that can be generated by an authentication server and then verified by any of your servers (for example, only the auth server has the private key needed to create the signature, but all servers have the public key needed to verify it). JWTs are typically either stored and transmitted in a cookie, or stored in JS local (session) storage and transmitted via JS-initiated requests (think Angular, etc. using XHR or
fetch) as a Bearer token in the HTTP
JWTs contain identity and expiration information, and usually also role / access information and potentially other data. They are not typically encrypted (just base64-encoded), so don't put any info in there you don't want the user to see. However, attackers can neither tamper with existing JWTs nor generate valid ones from scratch, as they would have no way to (re-)create a valid signature.
JWTs are a widely-used format, so there are tons of libraries that implement them. Not all of these libraries are equal in quality, though. JWTs also provide a way to authenticate and even authorize a user without consulting any server-side state (like a database); this makes them ideal for widely-distributed or heavily-scaling services.
JWTs have two major weaknesses: they are hard to revoke and they have a single point of failure for the whole system (the signing/HMAC key).
- The revocation problem is typically handled by having the JWT lifetime be very short (a matter of minutes) and - if longer sessions are needed - also issuing the user a "refresh token" at login; this token is simply a cryptographically random string that is stored in the database server-side. When the JWT expires (or is about to expire), the client exchanges the refresh token for a new JWT; this does cause an auth database hit, but most requests are still able to verify auth statelessly. Actual revocation can be implemented by creating a cache of invalid JWTs that is pushed to all servers and that every incoming JWT is checked against, but this adds complexity and costs performance and is rarely done in practice.
- The single point of failure is that if an attacker ever figures out the signing/HMAC key, then that attacker can forge completely arbitrary JWTs (any user, any role, won't expire until 2700, etc.) and, if done carefully, this is hard to detect (detection after-the-fact would usually require logging auth requests and user actions and noting that a user took actions without completing the auth flow, but that kind of log cross-checking usually can't be done in real-time). Compared with traditional session tokens (which are just random strings, same as a refresh token) and cannot be forged unless the attacker is able to write a value into the database or the server's auth cache, this is a somewhat greater risk and requires that the keys be very well protected, and also that it be possible to rotate the keys (which invalidates all JWTs signed with them) on-demand.