Authentication is a lot more about authorization than you probably think.
Let’s say you’ve got a user trying to access a protected resource. Your API needs to know who they are (authentication) and what they’re allowed to do (authorization). Tokens are the bridge.
Here’s a typical flow:
- User Login: A user provides credentials (username/password, OAuth flow, etc.) to your authentication service.
- Token Issuance: If credentials are valid, the authentication service generates a token and sends it back to the user’s client.
- Subsequent Requests: The client includes this token in the
Authorizationheader of subsequent requests to your API. - Token Validation: Your API endpoint receives the request, extracts the token, and validates it. If valid, it checks the token’s contents (claims) to determine if the user has permission to perform the requested action.
// Example of a JWT (JSON Web Token) payload
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622,
"roles": ["admin", "editor"]
}
The sub (subject) identifies the user, name is a common claim, iat is the issued-at time, exp is the expiration time, and roles are example permissions.
The problem API security often solves is preventing unauthorized access. Without proper token management, anyone could impersonate another user or access sensitive data.
A common pattern is using JSON Web Tokens (JWTs). JWTs are compact, URL-safe means of representing claims to be transferred between two parties. They are signed, so the recipient can verify the sender’s identity and ensure the message hasn’t been altered.
Here’s how a JWT works under the hood:
A JWT consists of three parts separated by dots (.): a Header, a Payload, and a Signature.
-
Header: Contains metadata about the token, such as the algorithm used for signing (e.g.,
HS256orRS256) and the token type (JWT).{ "alg": "HS256", "typ": "JWT" }This JSON is Base64Url encoded.
-
Payload: Contains the claims. Claims are statements about an entity (typically, the user) and additional data.
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }This JSON is also Base64Url encoded.
-
Signature: Created by taking the encoded header, the encoded payload, a secret (for symmetric algorithms like HS256) or a private key (for asymmetric algorithms like RS256), and signing them with the algorithm specified in the header.
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
The client sends the token in the Authorization header, typically as:
Authorization: Bearer <token>
Your API server receives this, splits it into its three parts, decodes the header and payload, and then verifies the signature using the same secret or public key. If the signature is valid and the token hasn’t expired, the claims in the payload are trusted.
The most surprising true thing about JWTs is that the payload itself is not encrypted by default. It’s only Base64Url encoded, meaning anyone can decode it and see the claims. This is why you should never put sensitive information directly into the JWT payload. Think of it as a signed receipt, not a locked box.
Proper token management involves several key practices:
- Short Expiration Times: Set
expclaims to be short-lived, typically minutes or hours, to minimize the window of opportunity if a token is compromised. For instance,exp: 1678886400(which is March 15, 2023, 12:00:00 PM UTC). - Refresh Tokens: Use longer-lived refresh tokens to obtain new access tokens without requiring the user to re-authenticate frequently. Store refresh tokens securely, often in an HTTP-only cookie.
- Token Revocation: Implement a mechanism to revoke tokens before they expire, such as a denylist. This is crucial for scenarios like a user changing their password or logging out on all devices. A common approach is to store revoked token IDs (e.g.,
jticlaim) in a fast-access data store like Redis. - HTTPS Everywhere: Always transmit tokens over HTTPS to prevent man-in-the-middle attacks.
- Secure Secret/Key Management: Protect your signing secrets or private keys vigilantly. For symmetric keys (like HS256), never hardcode them. Use environment variables or a secrets manager. For asymmetric keys (like RS256), protect the private key and distribute the public key widely for verification.
- Audience (
aud) and Issuer (iss) Claims: Use theaudclaim to specify the intended recipient of the token (your API) and theissclaim to identify the entity that issued the token. Verify these claims on your API server. For example, checkingtoken.payload.aud === 'my-api-service'andtoken.payload.iss === 'auth.example.com'. - Unique Token IDs (
jti): Include a unique identifier for each token. Thisjticlaim is invaluable for revocation lists.
The next step in securing your API will likely involve exploring different authorization models beyond simple role-based access control.