The JSON Web Token (JWT) is a JSON object that is defined in the open standard RFC 7519. It is considered one of the secure ways to transfer information between two parties. To create it, you need to define a header with general information about the token, payloads such as user id, role, etc., and a signature.
In simple words, JWT is just a string in the following format header.payload.signature.
abw343erty5re6r23rwer48cd.e32f54ter645egh.ijkgft54gt52a3sww23er4qr2w1l
Suppose we want to log in to the site using a Google account. In our case there are three participants - the user user, the application server and the Google authentication server. The authentication server will provide the user with a token with which he can later interact with the application.
The application uses JWT to verify user authentication as follows:
- First, the user accesses the Google OAuth server with an authentication token (Google key), which is a URL to that server, and some url parameters that define the user and the application. When user try to login, it is necessary to create state (which include creation time, time to live, and remember-me parameter value) end encrypt it to protect data. How to encrypt described in HowTo_encrypt. Check global variable values.
- After entering the credentials, the authentication server redirects the user back to the server, only the URL will use
/callback
instead of/login
. All params passed into server when login will be included in url. Check the status settings to make sure that malicious attackers have not tampered with the link. - Get JWT by making POST request. Pass OAuth
code
param value to get access.
function randomString(length) {
const iv = crypto.getRandomValues(new Uint8Array(length));
return Array.from(iv).map(b => ('00' + b.toString(16)).slice(-2)).join('');
}
function redirectUrl(path, params) {
return path + '?' + Object.entries(params).map(([k, v]) => k + '=' + encodeURIComponent(v)).join('&');
}
async function login(stateSecret) { //[1.4]
return redirectUrl(GOOGLE_OAUTH_LINK, {
state: stateSecret,
nonce: randomString(12),
client_id: GOOGLE_CLIENTID,
redirect_uri: GOOGLE_REDIRECT,
scope: 'openid email',
response_type: 'code',
});
}
async function handleRequest(request) {
try {
const url = new URL(request.url); //[1.1]
const [ignore, action, provider] = url.pathname.split('/'); //[1.2]
if (action === 'login') {
const stateSecret = await encryptData(JSON.stringify({ //[1.3]
iat: Date.now(),
ttl: STATE_PARAM_TTL,
rm: url.searchParams.get('remember-me')
}), SECRET);
return Response.redirect(await login(stateSecret)); //[1.4]
}
if (action === 'callback') { //[2.1]
//1. decrypt and verify state secret
const stateSecret = url.searchParams.get('state');
const state = JSON.parse(await decryptData(stateSecret, SECRET)); //[2.2]
checkTTL(state.iat, state.ttl); //[2.3]
if (state.provider !== provider)
throw 'BAD: valid stateSecret but unknown provider?';
const code = url.searchParams.get('code');
const [providerId, username] = await googleProcessTokenPackage(code); //[3]
return new Response(JSON.stringify({id: providerId, name: username}), {"content-type": "application/json"});
}
}catch(err){
return new Response(JSON.stringify({"hello": err.toString()}), {headers}); //handle error
}
}
- Getting the URL values.
- Getting URL params which defines browser behaviour.
- Make state.
- Redirect to openid provider using state secret, using
login()
. PassGOOGLE_OAUTH_LINK
variable value as a base path, and an object which property and values will be used to make url parmeters. It contains next properties:state
include the value of the anti-forgery unique session token, as well as any other information needed to recover the context when the user returns to your application, e.g., the starting URL.nonce
random value generated by your app that enables replay protection when present.client_id
which you obtain from the API Console Credentials page .redirect_uri
should be the HTTP endpoint on your server that will receive the response from Google. The value must exactly match one of the authorized redirect URIs for the OAuth 2.0 client, which you configured in the API Console Credentials page.scope
basic request should be "openid email".response_type
which in a basic authorization code flow request must equal "code".
- After successful authentication, the user will be redirected back to the web application with
/callback
action value. - Decrypt state param. How to decrypt described here
- Check time to live is not stitched.
function checkTTL(iat, ttl) { const now = Date.now(); if (iat > now) throw 'BAD: iat issued in the future'; if (now > iat + ttl) throw 'timed out'; }
To get Google auth JWT googleProcessTokenPackage()
is used.
async function googleProcessTokenPackage(code) {
const tokenPackage = await fetchAccessToken( //[1]
GOOGLE_CODE_LINK, {
code,
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
redirect_uri: GOOGLE_REDIRECT,
grant_type: 'authorization_code'
}
);
const jwt = await tokenPackage.json(); //[2]
const [header, payloadB64url, signature] = jwt.id_token.split('.'); //[3]
const payloadText = atob(fromBase64url(payloadB64url)); //[4]
const payload = JSON.parse(payloadText); //[5]
return ['go' + payload.sub, payload.email]; //[6]
}
- Use
fetchAccessToken()
to make POST request used to get JWT.
async function fetchAccessToken(path, data) {
return await fetch(path, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: Object.entries(data).map(([k, v]) => k + '=' + encodeURIComponent(v)).join('&')
});
}
The request must include the following parameters in the POST body:
* code
the authorization code that is returned from the initial request.
* client_id
the client ID that you obtain from the API Console Credentials page.
* client_secret
the client secret that you obtain from the API Console Credentials page.
* redirect_uri
an authorized redirect URI for the given client_id specified in the API Console Credentials page.
* grant_type
this field must contain a string value of "authorization_code".
2. The json()
takes a response and reads it to completion. The result of execution is JWT.
3. Getting a header, payload u signature with JWT.
4. Decoding a string from base64url to base64.
5. Conversion from base64 into object.
6. Return some user properties.