var urlDecodeB64 = function(data) { return Buffer.from(data, 'base64').toString('utf8'); } /** * Decodes a string token into the 3 parts, throws if the format is invalid * @param token */ var decode = function(token) { var parts = token.split('.'); if (parts.length !== 3) { throw new Error('ID token could not be decoded') } return { _raw: token, header: JSON.parse(urlDecodeB64(parts[0])), payload: JSON.parse(urlDecodeB64(parts[1])), signature: parts[2] }; } var DEFAULT_LEEWAY = 60; //default clock-skew, in seconds /** * Verifier for ID Tokens following OIDC spec. * @param token the string token to verify * @param options the options required to run this verification * @returns A promise containing the decoded token payload, or throws an exception if validation failed */ var verify = (token, options) => { if (!token) { throw new Error('ID token is required but missing'); } var decodedToken = decode(token); // Check algorithm var header = decodedToken.header; if (header.alg !== 'RS256' && header.alg !== 'HS256') { throw new Error( `Signature algorithm of "${header.alg}" is not supported. Expected the ID token to be signed with "RS256" or "HS256".` ); } var payload = decodedToken.payload; // Issuer if (!payload.iss || typeof payload.iss !== 'string') { throw new Error('Issuer (iss) claim must be a string present in the ID token'); } if (payload.iss !== options.iss) { throw new Error(`Issuer (iss) claim mismatch in the ID token; expected "${options.iss}", found "${payload.iss}"`); } // Subject if (!payload.sub || typeof payload.sub !== 'string') { throw new Error('Subject (sub) claim must be a string present in the ID token'); } // Audience if (!payload.aud || !(typeof payload.aud === 'string' || Array.isArray(payload.aud))) { throw new Error('Audience (aud) claim must be a string or array of strings present in the ID token'); } if (Array.isArray(payload.aud) && !payload.aud.includes(options.aud)) { throw new Error(`Audience (aud) claim mismatch in the ID token; expected "${options.aud}" but was not one of "${payload.aud.join(', ')}"`); } else if (typeof payload.aud === 'string' && payload.aud !== options.aud) { throw new Error(`Audience (aud) claim mismatch in the ID token; expected "${options.aud}" but found "${payload.aud}"`); } // --Time validation (epoch)-- var now = Math.floor(Date.now() / 1000); var leeway = options.leeway || DEFAULT_LEEWAY; // Expires at if (!payload.exp || typeof payload.exp !== 'number') { throw new Error('Expiration Time (exp) claim must be a number present in the ID token'); } var expTime = payload.exp + leeway; if (now > expTime) { throw new Error(`Expiration Time (exp) claim error in the ID token; current time (${now}) is after expiration time (${expTime})`); } // Issued at if (!payload.iat || typeof payload.iat !== 'number') { throw new Error('Issued At (iat) claim must be a number present in the ID token'); } // Nonce if (options.nonce) { if (!payload.nonce || typeof payload.nonce !== 'string') { throw new Error('Nonce (nonce) claim must be a string present in the ID token'); } if (payload.nonce !== options.nonce) { throw new Error(`Nonce (nonce) claim mismatch in the ID token; expected "${options.nonce}", found "${payload.nonce}"`); } } // Authorized party if (Array.isArray(payload.aud) && payload.aud.length > 1) { if (!payload.azp || typeof payload.azp !== 'string') { throw new Error('Authorized Party (azp) claim must be a string present in the ID token when Audience (aud) claim has multiple values'); } if (payload.azp !== options.aud) { throw new Error(`Authorized Party (azp) claim mismatch in the ID token; expected "${options.aud}", found "${payload.azp}"`); } } // Authentication time if (options.maxAge) { if (!payload.auth_time || typeof payload.auth_time !== 'number') { throw new Error('Authentication Time (auth_time) claim must be a number present in the ID token when Max Age (max_age) is specified'); } var authValidUntil = payload.auth_time + options.maxAge + leeway; if (now > authValidUntil) { 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. Currrent time (${now}) is after last auth at ${authValidUntil}` ); } } return decodedToken; }; module.exports = { decode: decode, verify: verify };