diff options
author | 2023-01-13 13:59:27 +0100 | |
---|---|---|
committer | 2023-01-13 14:02:44 +0100 | |
commit | f52f7a845c34ce7da57b11c17d261733be89554f (patch) | |
tree | 6fd78f5d5bfd6bebe4a26367381baef45c7d7a22 /src/tools/jwt-parser/jwt-parser.service.ts | |
parent | acc7f0a586c64500c5f720e70cdbccf9bffe76d9 (diff) | |
download | it-tools-f52f7a845c34ce7da57b11c17d261733be89554f.tar.gz it-tools-f52f7a845c34ce7da57b11c17d261733be89554f.tar.zst it-tools-f52f7a845c34ce7da57b11c17d261733be89554f.zip |
refactor(jwt-parser): simplified code
Diffstat (limited to 'src/tools/jwt-parser/jwt-parser.service.ts')
-rw-r--r-- | src/tools/jwt-parser/jwt-parser.service.ts | 457 |
1 files changed, 37 insertions, 420 deletions
diff --git a/src/tools/jwt-parser/jwt-parser.service.ts b/src/tools/jwt-parser/jwt-parser.service.ts index 37b6ecc..9bc994c 100644 --- a/src/tools/jwt-parser/jwt-parser.service.ts +++ b/src/tools/jwt-parser/jwt-parser.service.ts @@ -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 })); -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; - } - } + return { + header, + payload, + }; } -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 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 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 }; - } +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); } -// 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 dateFormatter = (value: unknown) => { + if (_.isNil(value)) return undefined; -// 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]', - }, -]; + const date = new Date(Number(value) * 1000); + return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; +}; |