Skip to content

Commit

Permalink
refactor(client): decodeIdToken (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
IceHe authored Jan 7, 2022
1 parent 515a0c6 commit d858085
Show file tree
Hide file tree
Showing 6 changed files with 32 additions and 26 deletions.
2 changes: 1 addition & 1 deletion packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { createJWKS, verifyIdToken } from './utils/id-token';
import { parseRedirectCallback } from './utils/parser';
import { createRequester, Requester } from './utils/requester';

export type { IDToken } from './utils/id-token';
export type { IdTokenClaims } from './utils/id-token';

export * from './modules/storage';

Expand Down
6 changes: 3 additions & 3 deletions packages/client/src/modules/token-set.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TokenResponse } from '../api';
import { nowRoundToSec } from '../utils';
import { decodeToken, IDToken } from '../utils/id-token';
import { decodeIdToken, IdTokenClaims } from '../utils/id-token';

export default class TokenSet {
public accessToken: string;
Expand All @@ -26,11 +26,11 @@ export default class TokenSet {
return this.expiresIn === 0;
}

public claims(): IDToken {
public claims(): IdTokenClaims {
if (!this.idToken) {
throw new TypeError('id_token not present in TokenSet');
}

return decodeToken(this.idToken);
return decodeIdToken(this.idToken);
}
}
18 changes: 9 additions & 9 deletions packages/client/src/utils/id-token.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { SignJWT, generateKeyPair } from 'jose';
import nock from 'nock';
import { StructError } from 'superstruct';

import { decodeToken, createJWKS, verifyIdToken } from './id-token';
import { decodeIdToken, createJWKS, verifyIdToken } from './id-token';

describe('verifyIdToken', () => {
test('valid idToken', async () => {
test('valid ID Token', async () => {
const { privateKey, publicKey } = await generateKeyPair('RS256');

if (!(publicKey instanceof KeyObject)) {
Expand Down Expand Up @@ -107,8 +107,8 @@ describe('verifyIdToken', () => {
});
});

describe('decodeToken', () => {
test('decode token and get claims', async () => {
describe('decodeIdToken', () => {
test('decode ID Token and get claims', async () => {
const { privateKey } = await generateKeyPair('RS256');
const JWT = await new SignJWT({})
.setProtectedHeader({ alg: 'RS256' })
Expand All @@ -118,15 +118,15 @@ describe('decodeToken', () => {
.setIssuedAt()
.setExpirationTime('2h')
.sign(privateKey);
const payload = decodeToken(JWT);
expect(payload.sub).toEqual('foz');
const idTokenClaims = decodeIdToken(JWT);
expect(idTokenClaims.sub).toEqual('foz');
});

test('throw on invalid JWT string', async () => {
expect(() => decodeToken('invalid-JWT')).toThrow();
expect(() => decodeIdToken('invalid-JWT')).toThrow('invalid token');
});

test('throw ZodError when iss is missing', async () => {
test('throw StructError when iss is missing', async () => {
const { privateKey } = await generateKeyPair('RS256');
const JWT = await new SignJWT({})
.setProtectedHeader({ alg: 'RS256' })
Expand All @@ -135,6 +135,6 @@ describe('decodeToken', () => {
.setIssuedAt()
.setExpirationTime('2h')
.sign(privateKey);
expect(() => decodeToken(JWT)).toThrowError(StructError);
expect(() => decodeIdToken(JWT)).toThrowError(StructError);
});
});
22 changes: 14 additions & 8 deletions packages/client/src/utils/id-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,31 @@ const fullfillBase64 = (input: string) => {
return input;
};

const IDTokenSchema = s.type({
const IdTokenClaimsSchema = s.type({
iss: s.string(),
sub: s.string(),
aud: s.string(),
exp: s.number(),
iat: s.number(),
auth_time: s.optional(s.number()),
nonce: s.optional(s.string()),
acr: s.optional(s.string()),
amr: s.optional(s.array(s.string())),
azp: s.optional(s.string()),
at_hash: s.optional(s.string()),
c_hash: s.optional(s.string()),
});

export type IDToken = s.Infer<typeof IDTokenSchema>;
export type IdTokenClaims = s.Infer<typeof IdTokenClaimsSchema>;

/**
* Decode IDToken from JWT, without verifying.
* Decode ID Token from JWT, without verifying.
* Verifying JWT requires fetching public key first, this can not
* be done in a sync function, in some cases, verifying is not necessary.
* @param token JWT string.
* @returns IDToken combined with JWT Claims.
*/
export const decodeToken = (token: string): IDToken => {
export const decodeIdToken = (token: string): IdTokenClaims => {
const payloadPart = token.split('.')[1];

if (!payloadPart) {
Expand All @@ -47,9 +53,9 @@ export const decodeToken = (token: string): IDToken => {

try {
// Using SuperStruct to validate the json type
const data = JSON.parse(json) as IDToken;
s.assert(data, IDTokenSchema);
return data;
const idToken = JSON.parse(json) as IdTokenClaims;
s.assert(idToken, IdTokenClaimsSchema);
return idToken;
} catch (error: unknown) {
if (error instanceof s.StructError) {
throw error;
Expand All @@ -70,7 +76,7 @@ export const createJWKS = (JWKSUri: string): JWTVerifyGetKey => {
};

/**
* Verify IDToken
* Verify ID Token
* @param {Function} JWKS
* @param {String} idToken
* @param {String} audience
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/auth-state.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { IDToken } from '@logto/client';
import { IdTokenClaims } from '@logto/client';

export interface AuthState {
isLoading: boolean;
isInitialized: boolean;
isAuthenticated: boolean;
error?: Error;
claims?: IDToken;
claims?: IdTokenClaims;
}

export const defaultAuthState: AuthState = {
Expand Down
6 changes: 3 additions & 3 deletions packages/react/src/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { IDToken } from '@logto/client';
import { IdTokenClaims } from '@logto/client';

import { AuthState } from './auth-state';

type Action =
| {
type: 'INITIALIZE';
payload: { isAuthenticated: boolean; isLoading: boolean; claims?: IDToken };
payload: { isAuthenticated: boolean; isLoading: boolean; claims?: IdTokenClaims };
}
| { type: 'LOGIN_WITH_REDIRECT' }
| { type: 'HANDLE_CALLBACK_REQUEST' }
| { type: 'HANDLE_CALLBACK_SUCCESS'; payload: { claims: IDToken } }
| { type: 'HANDLE_CALLBACK_SUCCESS'; payload: { claims: IdTokenClaims } }
| { type: 'LOGOUT' }
| { type: 'ERROR'; payload: { error: unknown } };

Expand Down

0 comments on commit d858085

Please sign in to comment.