108 lines
3.4 KiB
JavaScript
108 lines
3.4 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
// @flow
|
|
|
|
// This file provides simple utils to deal with JWT tokens.
|
|
// It was copied from https://github.com/firefox-devtools/profiler/blob/main/src/utils/jwt.js
|
|
// and transformed to use typescript jsdocs instead flow types (even though it's
|
|
// not TS-checked at the moment).
|
|
|
|
/**
|
|
* @param {string} jwtToken
|
|
* @returns {unknown}
|
|
*/
|
|
export function extractAndDecodePayload(jwtToken) {
|
|
if (!isValidJwtToken(jwtToken)) {
|
|
console.error("The token is an invalid JWT token.");
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const payload = jwtToken.split(".")[1];
|
|
const decodedPayload = decodeJwtBase64Url(payload);
|
|
const jsonPayload = JSON.parse(decodedPayload);
|
|
|
|
return jsonPayload;
|
|
} catch (e) {
|
|
console.error(
|
|
`We got an unexpected error when trying to decode the JWT token '${jwtToken}':`,
|
|
e
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// A JWT token is composed of 3 parts, separated by a period.
|
|
// These parts all use the base64url characters, that is base64 characters where
|
|
// "+" is replaced by "-", and "/" is replaced by "_". Moreover the padding
|
|
// character "=" isn't used with JWT.
|
|
// Here is an example:
|
|
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiP34_fj9-In0.KIumXQmDxL1bJ0RGNV2-mm-8h0LEQATKbtHUsCHMGcg
|
|
// ╰ header ╰ payload ╰ signature
|
|
const JWT_TOKEN_RE =
|
|
/^(?:[a-zA-Z0-9_-])+\.(?:[a-zA-Z0-9_-])+\.(?:[a-zA-Z0-9_-])+$/;
|
|
|
|
/**
|
|
* @param {string} jwtToken
|
|
* @returns {boolean}
|
|
*/
|
|
export function isValidJwtToken(jwtToken) {
|
|
return JWT_TOKEN_RE.test(jwtToken);
|
|
}
|
|
|
|
/**
|
|
* @param {string} base64UrlEncodedValue
|
|
* @returns {string}
|
|
*/
|
|
export function decodeJwtBase64Url(base64UrlEncodedValue) {
|
|
// In the base64url variant used in JWT, the padding "=" character is removed.
|
|
// But atob doesn't mind, so we don't need to recover the missing padding like
|
|
// most implementations do.
|
|
|
|
// We do need to convert the string to a "normal" base64 encoding though.
|
|
const base64EncodedValue = base64UrlEncodedValue.replace(/[-_]/g, match => {
|
|
// prettier-ignore
|
|
switch (match) {
|
|
case "-": return "+";
|
|
case "_": return "/";
|
|
default: throw new Error(`Unexpected match value ${match}`);
|
|
}
|
|
});
|
|
|
|
return atob(base64EncodedValue);
|
|
}
|
|
|
|
/**
|
|
* This function returns a profile token from a JWT token, if the passed string
|
|
* looks like a JWT token. Otherwise it just returns the passed string because
|
|
* this would be the hash directly, as returned by a previous version of the
|
|
* server.
|
|
* In the future when the server will be migrated we'll be able to remove this
|
|
* fallback.
|
|
*
|
|
* @param {string} hashOrToken
|
|
* @returns {string}
|
|
*/
|
|
export function extractProfileTokenFromJwt(hashOrToken) {
|
|
if (isValidJwtToken(hashOrToken)) {
|
|
// This is a JWT token, let's extract the hash out of it.
|
|
const jwtPayload = extractAndDecodePayload(hashOrToken);
|
|
if (!jwtPayload) {
|
|
throw new Error(
|
|
`The JWT token that's been returned by the server is incorrect.`
|
|
);
|
|
}
|
|
|
|
const { profileToken } = jwtPayload;
|
|
if (!profileToken) {
|
|
throw new Error(
|
|
`The JWT token returned by the server doesn't contain a profile token.`
|
|
);
|
|
}
|
|
return profileToken;
|
|
}
|
|
|
|
// Then this is a good old hash.
|
|
return hashOrToken;
|
|
}
|