JWT - is a JSON-based access token standard.
It is typically used to transfer data for authentication in client-server applications.
Tokens are created by the server, signed with a secret key, and sent to the client, who then uses this token to verify his identity.
Client part:
- on login request:
- sends credentials
- receives tokens
- on each request:
- uses an access token
- on refresh request:
- sends refresh
- receives tokens
Server part:
- on login request:
- signs the access key
- stores the refresh token
- return pair of tokens
- on each request:
- validates the access token
- identifies user
- on refresh request:
- validates the current refresh
- deletes the refresh token
- returns new pair of tokens
Since the "secret-key" is only revealed to the server, only it can issue new tokens with an authentic signature.
Users can't forge tokens and create a valid signature, as the token demands knowledge of the "secret-key".
These are generally passed in the "authorization" header at the time a user submits a request.
- Simplicity - easy to understand, use and implement
- Compatibility - can be used with different languages, platforms, libraries, frameworks and tools
- Cross-domain - can be used in different domains
- Scalability - can be used with load balancers
- Security - can be used with different encryption algorithms
- Mobile - can be used in mobile applications
- Stateless - no need to store tokens on the server?
- Extensibility - can be used with different claims
- Size - the token is larger than the session id
- Storage - the server must store the token?
- Transfer - the token is sent with each request
- Revocation - the token cannot be revoked
- Rotation - the token cannot be rotated
Token string is made up of three components divided by a dot:
- header that specifies the algorithm used to encrypt the contents of the token;
- payload that contains "claims" (information the token securely transmits);
- signature that can be used to verify the authenticity of the information.
The first two blocks are in base64 encoded JSON format.
The JWT standard defines several reserved names (iss, aud, exp, and others).
The signature can be generated using both symmetric and asymmetric encryption algorithms.
Example:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjp7ImlkIjo1LCJuYW1lIjoiUnVzbGFuIFBvcGVseXNoeW4iLCJlbWFpbCI6InNvbWVAbWFpbC5jb20ifSwianRpIjoiYWNjZXNzX3Rva2VuIiwiYXVkIjoiR2l0SHViIEdpc3QiLCJpc3MiOiJwb3BlbHlzaHluLnBwLnVhIiwiZXhwIjoxNjkxNzcyMjI2LjMwNDg3OH0.fYe5aCcrygBHywCvdV2dsLMRIatc8Fd6Ubjs4K6cxrc
Header:
{
"typ": "JWT",
"alg": "HS256"
}
Payload:
{
"user": {
"id": 5,
"name": "Ruslan Popelyshyn",
"email": "some@mail.com"
},
"jti": "access_token",
"aud": "GitHub Gist",
"iss": "popelyshyn.pp.ua",
"exp": 1691772226.304878
}
Signature: fYe5aCcrygBHywCvdV2dsLMRIatc8Fd6Ubjs4K6cxrc
Property | Access | Refresh |
---|---|---|
Purpose | Access to resources | Access to refresh |
Usage | Used for requests | Used for refresh |
Lifetime | Short, usually 15 minutes | Long, usually 30 days |
Storage | In state of app | Cookie's or local storage |
Security | Not encrypted, but signed | Encrypted and signed |
Validation | Validated by the server | Validated by the server |
Revocation | Not revoked | Revoked after refresh |
Rotation | Not rotated | Rotated after refresh |
Transfer | Sent with each request | Sent only on refresh |
Payload | Contains user data | Contains device info |
During each login, a record is created with IP/Fingerprint and other meta information, the so-called refresh session.
- Client sends a request to an api endpoint:
api/auth/login
- Server checks credentials
- Server creates a pair of tokens
- Access token
- Stores refresh session
- Sends a response to the client
- Access token
- Refresh token UUID
- Client stores tokens in storage
You should limit the number (usually 5) of refresh sessions per user.
This will allow you to limit the number of devices that can be logged in at the same time.
The easiest way is just to delete old sessions when creating a new one.
- Before the request or by timeout, the client checks whether the lifetime of the access token has expired
- If the time has expired, a request is sent to an api endpoint:
api/auth/refresh
- Server retrieves the "refresh session" by the UUID of the refresh token
- Removes session from storage
- Checks the current session for its lifetime
- Compares fingerprints
- On failure throws an exception
- On success stores a new "refresh session"
- Sends access and refresh token uuid to the client
- Client sends a request to an api endpoint:
api/auth/logout
- Server removes "refresh session" by the UUID of the refresh token
- Client removes tokens from storage
- Sends a response to the client
access_token
dies at the end of its lifetime.
There is no need to ban, delete, or store the access token manually, this violates the entire essence of the access token.
For web applications, it is recommended to use cookies to store refresh tokens.
After login/refresh requests, the server sends a cookie with the UUID of the refresh token.
With parameters:
HttpOnly
- the cookie is not available to JavaScriptSecure
- the cookie is only transmitted over HTTPSSameSite
- the cookie is not sent in cross-site requestsDomain
- the cookie is only sent to the specified domainPath
- the cookie is only sent to the specified pathExpires
- the cookie is only sent until the specified dateMax-Age
- the cookie is only sent for the specified number of seconds
If we use cookies, then we do not need to store the refresh token UUID in the client storage.
It is not an easy task to steal all the authorization data.
IP/Fingerprint - the server checks the IP/Fingerprint of the device
Old Refresh - if we receive a request with an old refresh token, we can logout all devices
The following are scenarios in which JWT may be helpful:
Authentication – This is the most prevalent scenario.
After the user has logged in, every following request will feature the JWT, letting the user access services, routes and resources allowed with the token.
Single Sign On (SSO) commonly uses JWT today, because of its minimal overhead and its ability to be smoothly employed across various domains.
Information Exchange – JWT is one of the most effective ways of securely exchanging data between parties.
For instance, a JWT may be signed using private/public key pairs to confirm the senders identity.
Furthermore, as the signature is attained using the payload and the header, it is possible to verify that the content has not been compromised.
If you do not store the refresh token in the database, there is a high probability that the tokens will be uncontrolled in the hands of attackers. To track them, we will have to create a blacklist and periodically clean it of overdue ones. Instead, we keep a limited list of white tokens for each user separately.
For creating a fingerprint, you can use some sort of device information.
For example, you can use the following data:
- User-Agent
- Accept-Language
- Screen resolution
- Timezone
- Platform
- ...
You can sent client fingerprint in the request header.
Hash it with other data (ip) and store it in the refresh session.
*Be careful, on using ip address, because it can be changed, and you will have to re-login. Like on different wifi networks.