mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-04-20 06:55:06 -04:00
refactor(jwt-parser): simplified code
This commit is contained in:
parent
acc7f0a586
commit
f52f7a845c
8 changed files with 203 additions and 558 deletions
|
@ -1,429 +1,46 @@
|
|||
import jwt_decode, { InvalidTokenError } from 'jwt-decode';
|
||||
import jwtDecode, { type JwtHeader, type JwtPayload } from 'jwt-decode';
|
||||
import _ from 'lodash';
|
||||
import { match } from 'ts-pattern';
|
||||
import { ALGORITHM_DESCRIPTIONS, CLAIM_DESCRIPTIONS } from './jwt-parser.constants';
|
||||
|
||||
interface JWT {
|
||||
header: Map<string, unknown>;
|
||||
payload: Map<string, unknown>;
|
||||
export { decodeJwt };
|
||||
|
||||
function decodeJwt({ jwt }: { jwt: string }) {
|
||||
const rawHeader = jwtDecode<JwtHeader>(jwt, { header: true });
|
||||
const rawPayload = jwtDecode<JwtPayload>(jwt);
|
||||
|
||||
const header = _.map(rawHeader, (value, claim) => parseClaims({ claim, value }));
|
||||
const payload = _.map(rawPayload, (value, claim) => parseClaims({ claim, value }));
|
||||
|
||||
return {
|
||||
header,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
export function safeJwtDecode(rawJwt: string): JWT {
|
||||
try {
|
||||
const header = jwt_decode(rawJwt, { header: true }) as Map<string, unknown>;
|
||||
const payload = jwt_decode(rawJwt) as Map<string, unknown>;
|
||||
return { header, payload };
|
||||
} catch (e) {
|
||||
if (e instanceof InvalidTokenError) {
|
||||
return { header: new Map<string, unknown>(), payload: new Map<string, unknown>() };
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
function parseClaims({ claim, value }: { claim: string; value: unknown }) {
|
||||
const claimDescription = CLAIM_DESCRIPTIONS[claim];
|
||||
const formattedValue = _.toString(value);
|
||||
const friendlyValue = getFriendlyValue({ claim, value });
|
||||
|
||||
return {
|
||||
value: formattedValue,
|
||||
friendlyValue,
|
||||
claim,
|
||||
claimDescription,
|
||||
};
|
||||
}
|
||||
|
||||
export function getClaimLabel(claim: string): { label: string; ref: string } {
|
||||
const infos = STANDARD_CLAIMS.find((info) => info.name === claim);
|
||||
if (infos) {
|
||||
return { label: infos.long_name, ref: infos.ref };
|
||||
}
|
||||
switch (claim) {
|
||||
case 'typ':
|
||||
return { label: 'Type', ref: '' };
|
||||
case 'alg':
|
||||
return { label: 'Algorithm', ref: '' };
|
||||
}
|
||||
return { label: claim, ref: '' };
|
||||
function getFriendlyValue({ claim, value }: { claim: string; value: unknown }) {
|
||||
return match(claim)
|
||||
.with('exp', 'nbf', 'iat', () => dateFormatter(value))
|
||||
.with('alg', () => (_.isString(value) ? ALGORITHM_DESCRIPTIONS[value] : undefined))
|
||||
.otherwise(() => undefined);
|
||||
}
|
||||
|
||||
export function parseClaimValue(claim: string, value: unknown): { value: unknown; extension?: unknown } {
|
||||
switch (claim) {
|
||||
case 'exp':
|
||||
case 'nbf':
|
||||
case 'iat': {
|
||||
// Convert to milliseconds, JWT specs says it should be in seconds, JS
|
||||
// works with milliseconds
|
||||
value = typeof value === 'string' ? parseInt(value) : value;
|
||||
const date = new Date((value as number) * 1000);
|
||||
return { value: `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`, extension: value };
|
||||
}
|
||||
case 'alg':
|
||||
return { value: AlgorithmKeyDescriptionMapping[value as string], extension: value };
|
||||
default:
|
||||
if (typeof value === 'boolean') {
|
||||
// Perhaps there's a better way to do this?
|
||||
return { value: value ? 'true' : 'false' };
|
||||
}
|
||||
return { value: value };
|
||||
}
|
||||
}
|
||||
const dateFormatter = (value: unknown) => {
|
||||
if (_.isNil(value)) return undefined;
|
||||
|
||||
// From https://datatracker.ietf.org/doc/html/rfc7518#section-3.1
|
||||
const AlgorithmKeyDescriptionMapping: { [k: string]: string } = {
|
||||
HS256: 'HMAC using SHA-256',
|
||||
HS384: 'HMAC using SHA-384',
|
||||
HS512: 'HMAC using SHA-512',
|
||||
RS256: 'RSASSA-PKCS1-v1_5 using SHA-256',
|
||||
RS384: 'RSASSA-PKCS1-v1_5 using SHA-384',
|
||||
RS512: 'RSASSA-PKCS1-v1_5 using SHA-512',
|
||||
ES256: 'ECDSA using P-256 and SHA-256',
|
||||
ES384: 'ECDSA using P-384 and SHA-384',
|
||||
ES512: 'ECDSA using P-521 and SHA-512',
|
||||
PS256: 'RSASSA-PSS using SHA-256 and MGF1 with SHA-256',
|
||||
PS384: 'RSASSA-PSS using SHA-384 and MGF1 with SHA-384',
|
||||
PS512: 'RSASSA-PSS using SHA-512 and MGF1 with SHA-512',
|
||||
none: 'No digital signature or MAC performed',
|
||||
const date = new Date(Number(value) * 1000);
|
||||
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
|
||||
};
|
||||
|
||||
// List extracted from IANA: https://www.iana.org/assignments/jwt/jwt.xhtml
|
||||
const STANDARD_CLAIMS = [
|
||||
{
|
||||
name: 'iss',
|
||||
long_name: 'Issuer',
|
||||
ref: '[RFC7519 - Section 4.1.1]',
|
||||
},
|
||||
{
|
||||
name: 'sub',
|
||||
long_name: 'Subject',
|
||||
ref: '[RFC7519 - Section 4.1.2]',
|
||||
},
|
||||
{
|
||||
name: 'aud',
|
||||
long_name: 'Audience',
|
||||
ref: '[RFC7519 - Section 4.1.3]',
|
||||
},
|
||||
{
|
||||
name: 'exp',
|
||||
long_name: 'Expiration Time',
|
||||
ref: '[RFC7519 - Section 4.1.4]',
|
||||
},
|
||||
{
|
||||
name: 'nbf',
|
||||
long_name: 'Not Before',
|
||||
ref: '[RFC7519 - Section 4.1.5]',
|
||||
},
|
||||
{
|
||||
name: 'iat',
|
||||
long_name: 'Issued At',
|
||||
ref: '[RFC7519 - Section 4.1.6]',
|
||||
},
|
||||
{
|
||||
name: 'jti',
|
||||
long_name: 'JWT ID',
|
||||
ref: '[RFC7519 - Section 4.1.7]',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
long_name: 'Full name',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
|
||||
},
|
||||
{
|
||||
name: 'given_name',
|
||||
long_name: 'Given name(s) or first name(s)',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
|
||||
},
|
||||
{
|
||||
name: 'family_name',
|
||||
long_name: 'Surname(s) or last name(s)',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
|
||||
},
|
||||
{
|
||||
name: 'middle_name',
|
||||
long_name: 'Middle name(s)',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
|
||||
},
|
||||
{
|
||||
name: 'nickname',
|
||||
long_name: 'Casual name',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
|
||||
},
|
||||
{
|
||||
name: 'preferred_username',
|
||||
long_name: 'Shorthand name by which the End-User wishes to be referred to',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
|
||||
},
|
||||
{
|
||||
name: 'profile',
|
||||
long_name: 'Profile page URL',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
|
||||
},
|
||||
{
|
||||
name: 'picture',
|
||||
long_name: 'Profile picture URL',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
|
||||
},
|
||||
{
|
||||
name: 'website',
|
||||
long_name: 'Web page or blog URL',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
long_name: 'Preferred e-mail address',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
|
||||
},
|
||||
{
|
||||
name: 'email_verified',
|
||||
long_name: 'True if the e-mail address has been verified; otherwise false',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
|
||||
},
|
||||
{
|
||||
name: 'gender',
|
||||
long_name: 'Gender',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
|
||||
},
|
||||
{
|
||||
name: 'birthdate',
|
||||
long_name: 'Birthday',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
|
||||
},
|
||||
{
|
||||
name: 'zoneinfo',
|
||||
long_name: 'Time zone',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
|
||||
},
|
||||
{
|
||||
name: 'locale',
|
||||
long_name: 'Locale',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
|
||||
},
|
||||
{
|
||||
name: 'phone_number',
|
||||
long_name: 'Preferred telephone number',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
|
||||
},
|
||||
{
|
||||
name: 'phone_number_verified',
|
||||
long_name: 'True if the phone number has been verified; otherwise false',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
|
||||
},
|
||||
{
|
||||
name: 'address',
|
||||
long_name: 'Preferred postal address',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
|
||||
},
|
||||
{
|
||||
name: 'updated_at',
|
||||
long_name: 'Time the information was last updated',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
|
||||
},
|
||||
{
|
||||
name: 'azp',
|
||||
long_name: 'Authorized party - the party to which the ID Token was issued',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 2]',
|
||||
},
|
||||
{
|
||||
name: 'nonce',
|
||||
long_name: 'Value used to associate a Client session with an ID Token',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 2]',
|
||||
},
|
||||
{
|
||||
name: 'auth_time',
|
||||
long_name: 'Time when the authentication occurred',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 2]',
|
||||
},
|
||||
{
|
||||
name: 'at_hash',
|
||||
long_name: 'Access Token hash value',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 2]',
|
||||
},
|
||||
{
|
||||
name: 'c_hash',
|
||||
long_name: 'Code hash value',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 3.3.2.11]',
|
||||
},
|
||||
{
|
||||
name: 'acr',
|
||||
long_name: 'Authentication Context Class Reference',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 2]',
|
||||
},
|
||||
{
|
||||
name: 'amr',
|
||||
long_name: 'Authentication Methods References',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 2]',
|
||||
},
|
||||
{
|
||||
name: 'sub_jwk',
|
||||
long_name: 'Public key used to check the signature of an ID Token',
|
||||
ref: '[OpenID Connect Core 1.0 - Section 7.4]',
|
||||
},
|
||||
{
|
||||
name: 'cnf',
|
||||
long_name: 'Confirmation',
|
||||
ref: '[RFC7800 - Section 3.1]',
|
||||
},
|
||||
{
|
||||
name: 'sip_from_tag',
|
||||
long_name: 'SIP From tag header field parameter value',
|
||||
ref: '[RFC8055][RFC3261]',
|
||||
},
|
||||
{
|
||||
name: 'sip_date',
|
||||
long_name: 'SIP Date header field value',
|
||||
ref: '[RFC8055][RFC3261]',
|
||||
},
|
||||
{
|
||||
name: 'sip_callid',
|
||||
long_name: 'SIP Call-Id header field value',
|
||||
ref: '[RFC8055][RFC3261]',
|
||||
},
|
||||
{
|
||||
name: 'sip_cseq_num',
|
||||
long_name: 'SIP CSeq numeric header field parameter value',
|
||||
ref: '[RFC8055][RFC3261]',
|
||||
},
|
||||
{
|
||||
name: 'sip_via_branch',
|
||||
long_name: 'SIP Via branch header field parameter value',
|
||||
ref: '[RFC8055][RFC3261]',
|
||||
},
|
||||
{
|
||||
name: 'orig',
|
||||
long_name: 'Originating Identity String',
|
||||
ref: '[RFC8225 - Section 5.2.1]',
|
||||
},
|
||||
{
|
||||
name: 'dest',
|
||||
long_name: 'Destination Identity String',
|
||||
ref: '[RFC8225 - Section 5.2.1]',
|
||||
},
|
||||
{
|
||||
name: 'mky',
|
||||
long_name: 'Media Key Fingerprint String',
|
||||
ref: '[RFC8225 - Section 5.2.2]',
|
||||
},
|
||||
{
|
||||
name: 'events',
|
||||
long_name: 'Security Events',
|
||||
ref: '[RFC8417 - Section 2.2]',
|
||||
},
|
||||
{
|
||||
name: 'toe',
|
||||
long_name: 'Time of Event',
|
||||
ref: '[RFC8417 - Section 2.2]',
|
||||
},
|
||||
{
|
||||
name: 'txn',
|
||||
long_name: 'Transaction Identifier',
|
||||
ref: '[RFC8417 - Section 2.2]',
|
||||
},
|
||||
{
|
||||
name: 'rph',
|
||||
long_name: 'Resource Priority Header Authorization',
|
||||
ref: '[RFC8443 - Section 3]',
|
||||
},
|
||||
{
|
||||
name: 'sid',
|
||||
long_name: 'Session ID',
|
||||
ref: '[OpenID Connect Front-Channel Logout 1.0 - Section 3]',
|
||||
},
|
||||
{
|
||||
name: 'vot',
|
||||
long_name: 'Vector of Trust value',
|
||||
ref: '[RFC8485]',
|
||||
},
|
||||
{
|
||||
name: 'vtm',
|
||||
long_name: 'Vector of Trust trustmark URL',
|
||||
ref: '[RFC8485]',
|
||||
},
|
||||
{
|
||||
name: 'attest',
|
||||
long_name: 'Attestation level as defined in SHAKEN framework',
|
||||
ref: '[RFC8588]',
|
||||
},
|
||||
{
|
||||
name: 'origid',
|
||||
long_name: 'Originating Identifier as defined in SHAKEN framework',
|
||||
ref: '[RFC8588]',
|
||||
},
|
||||
{
|
||||
name: 'act',
|
||||
long_name: 'Actor',
|
||||
ref: '[RFC8693 - Section 4.1]',
|
||||
},
|
||||
{
|
||||
name: 'scope',
|
||||
long_name: 'Scope Values',
|
||||
ref: '[RFC8693 - Section 4.2]',
|
||||
},
|
||||
{
|
||||
name: 'client_id',
|
||||
long_name: 'Client Identifier',
|
||||
ref: '[RFC8693 - Section 4.3]',
|
||||
},
|
||||
{
|
||||
name: 'may_act',
|
||||
long_name: 'Authorized Actor - the party that is authorized to become the actor',
|
||||
ref: '[RFC8693 - Section 4.4]',
|
||||
},
|
||||
{
|
||||
name: 'jcard',
|
||||
long_name: 'jCard data',
|
||||
ref: '[RFC8688][RFC7095]',
|
||||
},
|
||||
{
|
||||
name: 'at_use_nbr',
|
||||
long_name: 'Number of API requests for which the access token can be used',
|
||||
ref: '[ETSI GS NFV-SEC 022 V2.7.1]',
|
||||
},
|
||||
{
|
||||
name: 'div',
|
||||
long_name: 'Diverted Target of a Call',
|
||||
ref: '[RFC8946]',
|
||||
},
|
||||
{
|
||||
name: 'opt',
|
||||
long_name: 'Original PASSporT (in Full Form)',
|
||||
ref: '[RFC8946]',
|
||||
},
|
||||
{
|
||||
name: 'vc',
|
||||
long_name: 'Verifiable Credential as specified in the W3C Recommendation',
|
||||
ref: '[W3C Recommendation Verifiable Credentials Data Model 1.0 - Expressing verifiable information on the Web (19 November 2019) - Section 6.3.1]',
|
||||
},
|
||||
{
|
||||
name: 'vp',
|
||||
long_name: 'Verifiable Presentation as specified in the W3C Recommendation',
|
||||
ref: '[W3C Recommendation Verifiable Credentials Data Model 1.0 - Expressing verifiable information on the Web (19 November 2019) - Section 6.3.1]',
|
||||
},
|
||||
{
|
||||
name: 'sph',
|
||||
long_name: 'SIP Priority header field',
|
||||
ref: '[RFC9027]',
|
||||
},
|
||||
{
|
||||
name: 'ace_profile',
|
||||
long_name: 'The ACE profile a token is supposed to be used with.',
|
||||
ref: '[RFC-ietf-ace-oauth-authz-46 - Section 5.10]',
|
||||
},
|
||||
{
|
||||
name: 'cnonce',
|
||||
long_name:
|
||||
'client-nonce. A nonce previously provided to the AS by the RS via the client. Used to verify token freshness when the RS cannot synchronize its clock with the AS.',
|
||||
ref: '[RFC-ietf-ace-oauth-authz-46 - Section 5.10]',
|
||||
},
|
||||
{
|
||||
name: 'exi',
|
||||
long_name:
|
||||
'Expires in. Lifetime of the token in seconds from the time the RS first sees it. Used to implement a weaker from of token expiration for devices that cannot synchronize their internal clocks.',
|
||||
ref: '[RFC-ietf-ace-oauth-authz-46 - Section 5.10.3]',
|
||||
},
|
||||
{
|
||||
name: 'roles',
|
||||
long_name: 'Roles',
|
||||
ref: '[RFC7643 - Section 4.1.2][RFC9068 - Section 2.2.3.1]',
|
||||
},
|
||||
{
|
||||
name: 'groups',
|
||||
long_name: 'Groups',
|
||||
ref: '[RFC7643 - Section 4.1.2][RFC9068 - Section 2.2.3.1]',
|
||||
},
|
||||
{
|
||||
name: 'entitlements',
|
||||
long_name: 'Entitlements',
|
||||
ref: '[RFC7643 - Section 4.1.2][RFC9068 - Section 2.2.3.1]',
|
||||
},
|
||||
{
|
||||
name: 'token_introspection',
|
||||
long_name: 'Token introspection response',
|
||||
ref: '[RFC-ietf-oauth-jwt-introspection-response-12 - Section 5]',
|
||||
},
|
||||
];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue