Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions src/modules/token/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { ecRecover } from '../../utils/ec-recover';
import { parseDIDToken } from '../../utils/parse-didt';
import { parsePublicAddressFromIssuer } from '../../utils/issuer';
import { parseJWT } from '../../utils/parse-jwt';

export class TokenModule extends BaseModule {
public validate(DIDToken: string, attachment = 'none') {
Expand Down Expand Up @@ -81,4 +82,72 @@ export class TokenModule extends BaseModule {
public getIssuer(DIDToken: string): string {
return this.decode(DIDToken)[1].iss;
}

/**
* Validates a JWT token.
* Similar to validate() but for JWT format tokens.
*
* A JWT has three parts: header.payload.signature
* The signature is automatically extracted, but verifying it cryptographically
* requires checking it against the header+payload using the signing key.
*
* @param jwtToken - The JWT token string to validate (format: header.payload.signature)
* @param options - Optional configuration
* @param options.verifySignature - Optional function to verify the JWT signature.
* The signature is already extracted from the JWT,
* but this function performs the cryptographic verification.
* Function receives: (header, payload, signature) => boolean
* If not provided, only claim validation is performed.
*/
public validateJWT(
jwtToken: string,
options?: {
verifySignature?: (header: any, payload: any, signature: string) => boolean;
},
) {
let parsedJWT;

try {
// Parse JWT into header, payload, and signature (signature is the 3rd part)
parsedJWT = parseJWT(jwtToken);
} catch {
throw createMalformedTokenError();
}

const { payload, header, signature } = parsedJWT;

// Verify signature cryptographically if verifier function is provided
// The signature is already extracted above, but we need to verify it's valid
if (options?.verifySignature) {
try {
const isValid = options.verifySignature(header, payload, signature);
if (!isValid) {
throw createFailedRecoveringProofError();
}
} catch {
throw createFailedRecoveringProofError();
}
}

const timeSecs = Math.floor(Date.now() / 1000);
const nbfLeeway = 300; // 5 min grace period

// Assert the token is not expired
if (payload.exp && payload.exp < timeSecs) {
throw createTokenExpiredError();
}

// Assert the token is not used before allowed.
if (payload.nbf && payload.nbf - nbfLeeway > timeSecs) {
throw createTokenCannotBeUsedYetError();
}

// Assert the audience matches the client ID.
if (this.sdk.clientId && payload.aud) {
const audience = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
if (!audience.includes(this.sdk.clientId)) {
throw createAudienceMismatchError();
}
}
}
}
90 changes: 90 additions & 0 deletions src/utils/parse-jwt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { createMalformedTokenError } from '../core/sdk-exceptions';

export interface JWTPayload {
iss?: string; // Issuer
sub?: string; // Subject
aud?: string | string[]; // Audience
exp?: number; // Expiration time
nbf?: number; // Not before
iat?: number; // Issued at
jti?: string; // JWT ID
[key: string]: any; // Allow additional claims
}

export interface JWTHeader {
alg: string; // Algorithm
typ?: string; // Type
kid?: string; // Key ID
[key: string]: any; // Allow additional header fields
}

export interface ParsedJWT {
header: JWTHeader;
payload: JWTPayload;
signature: string;
raw: {
header: string;
payload: string;
signature: string;
};
}

/**
* Base64URL decode helper
*/
function base64UrlDecode(str: string): string {
// Add padding if needed
let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
while (base64.length % 4) {
base64 += '=';
}

try {
if (typeof Buffer !== 'undefined') {
return Buffer.from(base64, 'base64').toString('utf-8');
}
// Browser fallback
return decodeURIComponent(
atob(base64)
.split('')
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
} catch {
throw createMalformedTokenError();
}
}

/**
* Parses a JWT token into its components.
*/
export function parseJWT(jwt: string): ParsedJWT {
try {
const parts = jwt.split('.');
if (parts.length !== 3) {
throw new Error('Invalid JWT format');
}

const [headerB64, payloadB64, signature] = parts;

const headerStr = base64UrlDecode(headerB64);
const payloadStr = base64UrlDecode(payloadB64);

const header = JSON.parse(headerStr) as JWTHeader;
const payload = JSON.parse(payloadStr) as JWTPayload;

return {
header,
payload,
signature,
raw: {
header: headerB64,
payload: payloadB64,
signature,
},
};
} catch {
throw createMalformedTokenError();
}
}

Loading