@auth0/auth0-spa-js
Version:
Auth0 SDK for Single Page Applications using Authorization Code Grant Flow with PKCE
226 lines (204 loc) • 6.45 kB
text/typescript
import { urlDecodeB64 } from './utils';
import { IdToken, JWTVerifyOptions } from './global';
const isNumber = (n: any) => typeof n === 'number';
const idTokendecoded = [
'iss',
'aud',
'exp',
'nbf',
'iat',
'jti',
'azp',
'nonce',
'auth_time',
'at_hash',
'c_hash',
'acr',
'amr',
'sub_jwk',
'cnf',
'sip_from_tag',
'sip_date',
'sip_callid',
'sip_cseq_num',
'sip_via_branch',
'orig',
'dest',
'mky',
'events',
'toe',
'txn',
'rph',
'sid',
'vot',
'vtm'
];
export const decode = (token: string) => {
const parts = token.split('.');
const [header, payload, signature] = parts;
if (parts.length !== 3 || !header || !payload || !signature) {
throw new Error('ID token could not be decoded');
}
const payloadJSON = JSON.parse(urlDecodeB64(payload));
const claims: IdToken = { __raw: token };
const user: any = {};
Object.keys(payloadJSON).forEach(k => {
claims[k] = payloadJSON[k];
if (!idTokendecoded.includes(k)) {
user[k] = payloadJSON[k];
}
});
return {
encoded: { header, payload, signature },
header: JSON.parse(urlDecodeB64(header)),
claims,
user
};
};
export const verify = (options: JWTVerifyOptions) => {
if (!options.id_token) {
throw new Error('ID token is required but missing');
}
const decoded = decode(options.id_token);
if (!decoded.claims.iss) {
throw new Error(
'Issuer (iss) claim must be a string present in the ID token'
);
}
if (decoded.claims.iss !== options.iss) {
throw new Error(
`Issuer (iss) claim mismatch in the ID token; expected "${options.iss}", found "${decoded.claims.iss}"`
);
}
if (!decoded.user.sub) {
throw new Error(
'Subject (sub) claim must be a string present in the ID token'
);
}
if (decoded.header.alg !== 'RS256') {
throw new Error(
`Signature algorithm of "${decoded.header.alg}" is not supported. Expected the ID token to be signed with "RS256".`
);
}
if (
!decoded.claims.aud ||
!(
typeof decoded.claims.aud === 'string' ||
Array.isArray(decoded.claims.aud)
)
) {
throw new Error(
'Audience (aud) claim must be a string or array of strings present in the ID token'
);
}
if (Array.isArray(decoded.claims.aud)) {
if (!decoded.claims.aud.includes(options.aud)) {
throw new Error(
`Audience (aud) claim mismatch in the ID token; expected "${
options.aud
}" but was not one of "${decoded.claims.aud.join(', ')}"`
);
}
if (decoded.claims.aud.length > 1) {
if (!decoded.claims.azp) {
throw new Error(
'Authorized Party (azp) claim must be a string present in the ID token when Audience (aud) claim has multiple values'
);
}
if (decoded.claims.azp !== options.aud) {
throw new Error(
`Authorized Party (azp) claim mismatch in the ID token; expected "${options.aud}", found "${decoded.claims.azp}"`
);
}
}
} else if (decoded.claims.aud !== options.aud) {
throw new Error(
`Audience (aud) claim mismatch in the ID token; expected "${options.aud}" but found "${decoded.claims.aud}"`
);
}
if (options.nonce) {
if (!decoded.claims.nonce) {
throw new Error(
'Nonce (nonce) claim must be a string present in the ID token'
);
}
if (decoded.claims.nonce !== options.nonce) {
throw new Error(
`Nonce (nonce) claim mismatch in the ID token; expected "${options.nonce}", found "${decoded.claims.nonce}"`
);
}
}
if (options.max_age && !isNumber(decoded.claims.auth_time)) {
throw new Error(
'Authentication Time (auth_time) claim must be a number present in the ID token when Max Age (max_age) is specified'
);
}
/* c8 ignore next 5 */
if (decoded.claims.exp == null || !isNumber(decoded.claims.exp)) {
throw new Error(
'Expiration Time (exp) claim must be a number present in the ID token'
);
}
if (!isNumber(decoded.claims.iat)) {
throw new Error(
'Issued At (iat) claim must be a number present in the ID token'
);
}
const leeway = options.leeway || 60;
const now = new Date(options.now || Date.now());
const expDate = new Date(0);
expDate.setUTCSeconds(decoded.claims.exp + leeway);
if (now > expDate) {
throw new Error(
`Expiration Time (exp) claim error in the ID token; current time (${now}) is after expiration time (${expDate})`
);
}
if (decoded.claims.nbf != null && isNumber(decoded.claims.nbf)) {
const nbfDate = new Date(0);
nbfDate.setUTCSeconds(decoded.claims.nbf - leeway);
if (now < nbfDate) {
throw new Error(
`Not Before time (nbf) claim in the ID token indicates that this token can't be used just yet. Current time (${now}) is before ${nbfDate}`
);
}
}
if (decoded.claims.auth_time != null && isNumber(decoded.claims.auth_time)) {
const authTimeDate = new Date(0);
authTimeDate.setUTCSeconds(
parseInt(decoded.claims.auth_time) + (options.max_age as number) + leeway
);
if (now > authTimeDate) {
throw new Error(
`Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication. Current time (${now}) is after last auth at ${authTimeDate}`
);
}
}
if (options.organization) {
const org = options.organization.trim();
if (org.startsWith('org_')) {
const orgId = org;
if (!decoded.claims.org_id) {
throw new Error(
'Organization ID (org_id) claim must be a string present in the ID token'
);
} else if (orgId !== decoded.claims.org_id) {
throw new Error(
`Organization ID (org_id) claim mismatch in the ID token; expected "${orgId}", found "${decoded.claims.org_id}"`
);
}
} else {
const orgName = org.toLowerCase();
// TODO should we verify if there is an `org_id` claim?
if (!decoded.claims.org_name) {
throw new Error(
'Organization Name (org_name) claim must be a string present in the ID token'
);
} else if (orgName !== decoded.claims.org_name) {
throw new Error(
`Organization Name (org_name) claim mismatch in the ID token; expected "${orgName}", found "${decoded.claims.org_name}"`
);
}
}
}
return decoded;
};