Skip to content
This repository was archived by the owner on Mar 18, 2025. It is now read-only.

Commit a144e14

Browse files
authored
Feature/jwt validation (#2)
* add validation for tokens * improve jwt through tests * improve jwt and CryptrUser type to match required fields
1 parent 0560b79 commit a144e14

File tree

5 files changed

+468
-9
lines changed

5 files changed

+468
-9
lines changed

src/__tests__/utils/jwt.test.tsx

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import jwtDecode from 'jwt-decode';
2+
import type {
3+
JwtHeaderType,
4+
PreparedCryptrConfig,
5+
} from '../../utils/interfaces';
6+
import Jwt, {
7+
validatesAudience,
8+
validatesClient,
9+
validatesFieldsExist,
10+
validatesHeader,
11+
validatesIssuer,
12+
validatesTimestamps,
13+
} from '../../utils/jwt';
14+
15+
let config: PreparedCryptrConfig = {
16+
tenant_domain: 'shark-academy',
17+
audience: 'cryptr://auth-natif',
18+
default_redirect_uri: 'cryptr://auth-natif',
19+
client_id: 'e2629eb9-3f56-4397-b19d-b85747cecd6b',
20+
cryptr_base_url: 'http://localhost:4000',
21+
dedicated_server: false,
22+
};
23+
24+
const validExpiredAccess =
25+
'eyJhbGciOiJSUzI1NiIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMC90L3NoYXJrLWFjYWRlbXkiLCJraWQiOiI5ZjhlNTE1MC1lNWIxLTQ4MWEtOTAyNS1mYzc2YmQ1Y2JlYmUiLCJ0eXAiOiJKV1QifQ.eyJhcHBsaWNhdGlvbl9tZXRhZGF0YSI6e30sImF1ZCI6ImNyeXB0cjovL2F1dGgtbmF0aWYiLCJjaWQiOiJlMjYyOWViOS0zZjU2LTQzOTctYjE5ZC1iODU3NDdjZWNkNmIiLCJkYnMiOiJzYW5kYm94IiwiZW1haWwiOiJqb2huLmRvZUBjcnlwdHIuY28iLCJleHAiOjE2NTQ3NDQ5MzUsImlhdCI6MTY1NDcwODkzNSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo0MDAwL3Qvc2hhcmstYWNhZGVteSIsImp0aSI6Ijg3Y2Y4YzRmLWMwODItNDVmZC05OTY2LWViMDNkYWM1MzFkOCIsImp0dCI6ImFjY2VzcyIsInNjcCI6WyJvcGVuaWQiLCJlbWFpbCIsInByb2ZpbGUiXSwic3ViIjoiMTkzZmViMGYtMTdkYy00NTE4LTg4N2YtNTAxNDVmN2RmNzg2IiwidG50Ijoic2hhcmstYWNhZGVteSIsInZlciI6MX0.hm6scm5s2gpZFBqGH_DxiIdyZDvW9KFNN1sTYdDYjO88uQtRcXJstbRxzUGp-xhLkevcUoy_NhZH91Jtkk3L-3ZEiaSD-AQ_l0GkRaUi8Ft3KjQ6wy6_H71ESVYykivY7kiB3sgQ0LNbVYu8lBcWAYuR6mMAoPW46aQ4vE-m2PoSNR2ULvp3JFpSk72ojBeebxXT_AxgJphf5vBr8Y1AqGkFxcbWhqP-x-CIfGvOnXFO_MsufkgJJz1pcqBspuiGBz4C_0FcFjJI9zS6OYHUXGbYy6OtxY7BhQoq9qphalVLfNjzJ9pcMRKAfDXX99eEeBFwCuquJtEjlRYpgee577Nkl9o_tDKWMGhkw0YdigXNueO6YhfoaWWQL8jCt0hhcAQPlvcEtCPznQ2dhvprWqbk5TREWMRTN2bGfadaPtGXeWPPt7YLSBMii02WfkDOL6Rg0oO9KRwkgGEUmZQ9KEb9YJAQef5oLDY7R8bCoFalKPfYCHO1s736Qm2MJitUEu2yNIErmNCYUYlXDnseFs9dfY7Y_cyrZeqIqvp9ma2SnOrzagdXV7V7LwLMI1zeB_EkGbAFnx8Ekvwu2z372gg_XQMSF1SpYajPb_0fu_NfmAWro_BOfD7AAfC8GQ1_PAxqLqRr9eoCM3iUJ6GLQ_eKBS49scujwsA_qDGgvWI';
26+
27+
const validAccess =
28+
'eyJhbGciOiJSUzI1NiIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMC90L3NoYXJrLWFjYWRlbXkiLCJraWQiOiI5ZjhlNTE1MC1lNWIxLTQ4MWEtOTAyNS1mYzc2YmQ1Y2JlYmUiLCJ0eXAiOiJKV1QifQ.eyJhcHBsaWNhdGlvbl9tZXRhZGF0YSI6e30sImF1ZCI6ImNyeXB0cjovL2F1dGgtbmF0aWYiLCJjaWQiOiJlMjYyOWViOS0zZjU2LTQzOTctYjE5ZC1iODU3NDdjZWNkNmIiLCJkYnMiOiJzYW5kYm94IiwiZW1haWwiOiJqb2huLmRvZUBjcnlwdHIuY28iLCJleHAiOjE3MTc5MjE4ODEsImlhdCI6MTY1NDg0OTg4MSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo0MDAwL3Qvc2hhcmstYWNhZGVteSIsImp0aSI6IjAyZmExZjJmLThlNzktNDUzZi04OTAwLTViMmM0MDVjZDY0NiIsImp0dCI6ImFjY2VzcyIsInNjcCI6WyJvcGVuaWQiLCJlbWFpbCIsInByb2ZpbGUiXSwic3ViIjoiMTkzZmViMGYtMTdkYy00NTE4LTg4N2YtNTAxNDVmN2RmNzg2IiwidG50Ijoic2hhcmstYWNhZGVteSIsInZlciI6MX0.ZaWKKuSGJpMhEaCdfp_DmPxGE-8-wHKy0_8rjPZZNXYy7HC-djxITq69cj_-jJ36-MyeM2KWd5Blig8feH2MQsHq-K2rlpgf6dGsEvXdQyqOn0O-DrfMaMhLVGbBYaOoxJUD-23L50SRUiIyfkSsDkJPqW6Js2Y4dcvH6mCh6sVPtlBJop2SCe2e1WATVGYukvGna-ViSaa1Bg0rCCQgiv7E0OCq9Gbv74pkJoLJbZF6nAnyb6tUB42ybxZUCnDLwMgRohrRJXI-I06UiEofld5baP0oPJZIz-AaiR5-M8kZmtGSQyQCr4Cjl4GDgcYaSv6tpoO_USVgkFEv76kbg_wOXfIah2HGRDXtgvQLVs0q8FlB3LBphsqCVCBSz4qI08uOjsk0O3Q4mTfyM8BqO3gfFrBakV6YeQBpgcSCWa203xLWd_xYTWKYntjyJhliGlX2ZmG94Vrc7VzPfwXvWeP5NzRtIuM1G2pdX-XELmLu1aUJsVG-JhqRrw-IqOYemfgxjqwDKNcGvVe4Ub4fKyADwgEdBKIbpQyRy0CQdkF32aMPg8Iz8uxzasSD4vlNJG-sNtrpkhyRwjkbJo5t9NNEEEst86mpKjcFQkHYtgVHXrqQaGQ9aA8J0bzbJkKUsfmfpmgoEjlUZz5-dRrY4LheG3b8ARPwjEsHYMp8hc8';
29+
30+
const validIdToken =
31+
'eyJhbGciOiJSUzI1NiIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMC90L3NoYXJrLWFjYWRlbXkiLCJraWQiOiI5ZjhlNTE1MC1lNWIxLTQ4MWEtOTAyNS1mYzc2YmQ1Y2JlYmUiLCJ0eXAiOiJKV1QifQ.eyJhcHBsaWNhdGlvbl9tZXRhZGF0YSI6e30sImF0X2hhc2giOiJUaEJ3YU8yYXBfc21TTjc3bEVTdHBBIiwiYXVkIjoiY3J5cHRyOi8vYXV0aC1uYXRpZiIsImNfaGFzaCI6IkNMVF9QY0psWXVta0dqYTFoMzVzekEiLCJjaWQiOiJlMjYyOWViOS0zZjU2LTQzOTctYjE5ZC1iODU3NDdjZWNkNmIiLCJkYnMiOiJzYW5kYm94IiwiZW1haWwiOiJqb2huLmRvZUBjcnlwdHIuY28iLCJleHAiOjE3MTc5MjE4ODEsImZhbWlseV9uYW1lIjoiRG9lIiwiZ2l2ZW5fbmFtZSI6IkpvaG4iLCJpYXQiOjE2NTQ4NDk4ODEsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMC90L3NoYXJrLWFjYWRlbXkiLCJqdGkiOiIyNjIzM2Y4ZC01MmM3LTQ2ZmYtYTM3NS04NWNmM2M0NjVlNTciLCJqdHQiOiJvcGVuaWQiLCJub25jZSI6IjY2MjdiNDA0LTM4MmQtNDJjNC04NGYyLTRkMDAxYmRjZjdiYSIsInJlc291cmNlX293bmVyX21ldGFkYXRhIjp7ImNpdHkiOiJTYW4gRnJhbmNpc2NvIiwiZW1haWwiOiJqb2huLmRvZUBjcnlwdHIuY28iLCJmaXJzdF9uYW1lIjoiSm9obiIsImxhc3RfbmFtZSI6IkRvZSIsIm1vYmlsZV9waG9uZSI6Iis0MTExMjQzNTk4ODciLCJwcm9maWxlX3VybCI6Imh0dHBzOi8vcmFuZG9tdXNlci5tZS9hcGkvcG9ydHJhaXRzL21lbi80Mi5qcGciLCJzYW1sX25hbWVpZCI6ImpvaG4uZG9lQGNyeXB0ci5jbyIsInppcF9jb2RlIjoiMTIzNDUifSwic19oYXNoIjoiQ0xUX1BjSmxZdW1rR2phMWgzNXN6QSIsInNjcCI6WyJvcGVuaWQiLCJlbWFpbCIsInByb2ZpbGUiXSwic3ViIjoiMTkzZmViMGYtMTdkYy00NTE4LTg4N2YtNTAxNDVmN2RmNzg2IiwidG50Ijoic2hhcmstYWNhZGVteSIsInZlciI6MX0.P6xUWrngIcLdh19_1IRUH_tGNeXW15QCjc43kUwt73vUITTWgkM6Z3DSO7Ar6aoe49FWbs0HND7gjLYrjkl_Ta4j08r5wcg2nZbPHSmS-C5sAKdMXmKy1JcNZaiM9EHeF-P2neiZUcL1P7h2wUAWt4mMgkQrmE0vZ7OVizOrG86kkf0BhAWv20bzofoO878mJv64ITcHGo2Rpcf8blQbLe_9v--pwkpR7Yh7Hm45tfYqNp5NBZqTRsMsBDn_JMkjjmsi7gga1wz42-_mEOOauIB4zAVzyomnYsbd4Inaw7mbaxb_d_0GrQiu2hiJ1cP4yUmNUz5AqNzfqAfS4p5P4VHIWydDSxNJBZDW_rqRZrGR_jyY2FBy-7QEpoj7pxtgL-4X9x7n4y9RNlyJ67pW2Bjpl_toy216rn0L2xWgeC1DVdctwdErq5AqU1Yg51M1G0eOs4X_iuywUpM69g1XZGsfhHZoFzeIf3Y9DVYULdhDlb4NT1mrNgLZ1wRiPDdNlieNQLHsUSTkwh1DKyamaugqJbl-8bmWSfocIU5MguMmRdxUxJJwQZX26ppTipVJXFVBOLAo5yO1MckmwcgrVjExsGmvtOov2TpThrspuNKnblBcsw1JvrYelTGt2VUr0Nk_qtv62hDjXyVhwoQ4h6rWKBQ0NTMB8yhB-ab2glA';
32+
33+
describe('Jwt.body/1', () => {
34+
it('should returns proper body ', () => {
35+
expect(Jwt.body(validAccess)).toEqual({
36+
application_metadata: {},
37+
aud: 'cryptr://auth-natif',
38+
dbs: 'sandbox',
39+
cid: 'e2629eb9-3f56-4397-b19d-b85747cecd6b',
40+
email: 'john.doe@cryptr.co',
41+
exp: 1717921881,
42+
iat: 1654849881,
43+
iss: 'http://localhost:4000/t/shark-academy',
44+
jti: '02fa1f2f-8e79-453f-8900-5b2c405cd646',
45+
jtt: 'access',
46+
scp: ['openid', 'email', 'profile'],
47+
sub: '193feb0f-17dc-4518-887f-50145f7df786',
48+
tnt: 'shark-academy',
49+
ver: 1,
50+
});
51+
});
52+
53+
it('should returns proper body even if expired', () => {
54+
expect(Jwt.body(validExpiredAccess)).toEqual({
55+
application_metadata: {},
56+
aud: 'cryptr://auth-natif',
57+
dbs: 'sandbox',
58+
cid: 'e2629eb9-3f56-4397-b19d-b85747cecd6b',
59+
email: 'john.doe@cryptr.co',
60+
exp: 1654744935,
61+
iat: 1654708935,
62+
iss: 'http://localhost:4000/t/shark-academy',
63+
jti: '87cf8c4f-c082-45fd-9966-eb03dac531d8',
64+
jtt: 'access',
65+
scp: ['openid', 'email', 'profile'],
66+
sub: '193feb0f-17dc-4518-887f-50145f7df786',
67+
tnt: 'shark-academy',
68+
ver: 1,
69+
});
70+
});
71+
72+
it('should fail if empty string', () => {
73+
expect(() => Jwt.body('')).toThrow(
74+
"Invalid token specified: Cannot read property 'replace' of undefined"
75+
);
76+
});
77+
78+
it('should fail if not jwt string', () => {
79+
expect(() => Jwt.body('azerty.azerty')).toThrowError();
80+
});
81+
});
82+
83+
describe('Jwt.validatesAccessToken/2', () => {
84+
it('should return success if accessToken compatible with config and not epxired', () => {
85+
expect(Jwt.validatesAccessToken(validAccess, config)).toBeTruthy();
86+
});
87+
88+
it('should throw error if accessToken expired', () => {
89+
expect(() =>
90+
Jwt.validatesAccessToken(validExpiredAccess, config)
91+
).toThrowError(
92+
'Expiration (exp) is invalid, (1654744935000) must be in the future'
93+
);
94+
});
95+
});
96+
97+
describe('validatesIdToken/2', () => {
98+
it('should return success if id token compatible with config and not epxired', () => {
99+
expect(Jwt.validatesIdToken(validIdToken, config)).toBeTruthy();
100+
});
101+
});
102+
103+
describe('validatesFieldsExists/2', () => {
104+
it('should return true if valid', () => {
105+
expect(validatesFieldsExist(Jwt.body(validAccess), ['sub'])).toBeTruthy();
106+
});
107+
108+
it('should throw error if field not present', () => {
109+
expect(() => validatesFieldsExist(Jwt.body(validAccess), ['user'])).toThrow(
110+
'user is missing'
111+
);
112+
});
113+
});
114+
115+
describe('validatesHeader/1', () => {
116+
const header: JwtHeaderType = jwtDecode(validAccess, { header: true });
117+
it('should return true if valid header', () => {
118+
expect(validatesHeader(header)).toBeTruthy();
119+
});
120+
121+
it('should throw error if wrong type', () => {
122+
expect(() => validatesHeader({ ...header, typ: 'TOK' })).toThrow(
123+
'The token must be a JWT'
124+
);
125+
});
126+
127+
it('should throw error if wrong alg', () => {
128+
expect(() => validatesHeader({ ...header, alg: 'RAW' })).toThrow(
129+
'The token must be sign in RSA 256'
130+
);
131+
});
132+
133+
it('should throw error if no kid', () => {
134+
expect(() =>
135+
validatesHeader({
136+
alg: 'RS256',
137+
typ: 'JWT',
138+
})
139+
).toThrow('The token need a kid (key identifier) in header');
140+
});
141+
});
142+
143+
describe('validatesTimestamps', () => {
144+
it('should returns true if exp and iat are number', () => {
145+
expect(validatesTimestamps(Jwt.body(validAccess))).toBeTruthy();
146+
});
147+
it('should throw error if exp no a number', () => {
148+
expect(() =>
149+
validatesTimestamps({ ...Jwt.body(validAccess), exp: '12' })
150+
).toThrowError('Expiration Time (exp) claim must be a present number');
151+
});
152+
153+
it('should throw iat error if exp no a number', () => {
154+
expect(() =>
155+
validatesTimestamps({ ...Jwt.body(validAccess), iat: '12' })
156+
).toThrowError('Issued at (iat) claim must be a present number');
157+
});
158+
});
159+
160+
describe('validatesAudience', () => {
161+
it('should return true if audience compatible with config', () => {
162+
expect(validatesAudience(Jwt.body(validAccess), config)).toBeTruthy();
163+
});
164+
165+
it('should throw error if audience incompatible with config', () => {
166+
expect(() =>
167+
validatesAudience(Jwt.body(validAccess), {
168+
...config,
169+
audience: 'http://app.example.com',
170+
})
171+
).toThrow(
172+
'Audience (aud) cryptr://auth-natif claim is not compliant with http://app.example.com from config'
173+
);
174+
});
175+
});
176+
177+
describe('validatesClient', () => {
178+
it('should return true if client compatible with config', () => {
179+
expect(validatesClient(Jwt.body(validAccess), config)).toBeTruthy();
180+
});
181+
182+
it('should throw error if client incompatible with config', () => {
183+
expect(() =>
184+
validatesClient(Jwt.body(validAccess), {
185+
...config,
186+
client_id: '02fa1f2f-8e79-453f-8900-5b2c405cd646',
187+
})
188+
).toThrow(
189+
'Client id (cid) e2629eb9-3f56-4397-b19d-b85747cecd6b claim is not compliant with 02fa1f2f-8e79-453f-8900-5b2c405cd646 from config'
190+
);
191+
});
192+
});
193+
194+
describe('validatesIssuer/2', () => {
195+
it('should return true if client compatible with config', () => {
196+
expect(validatesIssuer(Jwt.body(validAccess), config)).toBeTruthy();
197+
});
198+
199+
it('should throw error if client incompatible with config', () => {
200+
expect(() =>
201+
validatesIssuer(Jwt.body(validAccess), {
202+
...config,
203+
cryptr_base_url: 'http://app.server.com',
204+
})
205+
).toThrow(
206+
'Issuer (iss) http://localhost:4000/t/shark-academy is not compliant with http://app.server.com/t/shark-academy'
207+
);
208+
});
209+
});
210+
211+
describe('validatesIssuer/3', () => {
212+
it('should return true if organization domain same as token but different as tenant_domain', () => {
213+
expect(
214+
validatesIssuer(
215+
{ ...Jwt.body(validAccess), iss: 'http://localhost:4000/t/my-tenant' },
216+
config,
217+
'my-tenant'
218+
)
219+
).toBeTruthy();
220+
});
221+
222+
it('should throw error if organization not matching either token or config', () => {
223+
expect(() =>
224+
validatesIssuer(Jwt.body(validAccess), config, 'some-tenant')
225+
).toThrow(
226+
'Issuer (iss) http://localhost:4000/t/shark-academy is not compliant with http://localhost:4000/t/some-tenant'
227+
);
228+
});
229+
});

src/models/CryptrProvider.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React, { useEffect, useReducer, useState } from 'react';
2-
import jwtDecode from 'jwt-decode';
32
import CryptrContext from './CryptrContext';
43
import CryptrReducer from './CryptrReducer';
54
import initialCryptrState from './initialCryptrState';
@@ -26,11 +25,13 @@ import Cryptr from './Cryptr';
2625
import {
2726
extractParamsFromUri,
2827
logOutBody,
28+
organizationDomain,
2929
prepareConfig,
3030
refreshBody,
3131
tokensBody,
3232
} from '../utils/helpers';
3333
import { DeviceEventEmitter, Platform } from 'react-native';
34+
import Jwt from '../utils/jwt';
3435

3536
const CryptrProvider: React.FC<ProviderProps> = ({
3637
children,
@@ -95,7 +96,10 @@ const CryptrProvider: React.FC<ProviderProps> = ({
9596
}, [config]);
9697

9798
const handleNewTokens = (json: any, callback?: (data: any) => any) => {
98-
json.refresh_token &&
99+
if (json.refresh_token) {
100+
const organization_domain = organizationDomain(json.refresh_token);
101+
Jwt.validatesAccessToken(json.access_token, config, organization_domain);
102+
Jwt.validatesIdToken(json.id_token, config, organization_domain);
99103
Cryptr.setRefresh(
100104
json.refresh_token,
101105
(_data: any) => {},
@@ -105,6 +109,7 @@ const CryptrProvider: React.FC<ProviderProps> = ({
105109
} catch (_error) {}
106110
}
107111
);
112+
}
108113
const actionType = json.access_token
109114
? CryptrReducerActionKind.AUTHENTICATED
110115
: CryptrReducerActionKind.UNAUTHENTICATED;
@@ -261,6 +266,13 @@ const CryptrProvider: React.FC<ProviderProps> = ({
261266
errorCallback && errorCallback(json);
262267
} else {
263268
if (json.refresh_token) {
269+
const organization_domain = organizationDomain(json.refresh_token);
270+
Jwt.validatesAccessToken(
271+
json.access_token,
272+
config,
273+
organization_domain
274+
);
275+
Jwt.validatesIdToken(json.id_token, config, organization_domain);
264276
Cryptr.setRefresh(
265277
json.refresh_token,
266278
(_data: any) => {},
@@ -322,7 +334,7 @@ const CryptrProvider: React.FC<ProviderProps> = ({
322334

323335
const getUser = (): CryptrUser | undefined => {
324336
if (state.idToken) {
325-
return jwtDecode(state.idToken);
337+
return Jwt.body(state.idToken) as CryptrUser;
326338
}
327339
return undefined;
328340
};

src/utils/constants.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@ export const DEFAULT_SCOPE = 'openid email profile';
33
export const CRYPTR_BASE_URL_EU = 'https://auth.cryptr.eu';
44

55
export const CRYPTR_BASE_URL_US = 'https://auth.cryptr.us';
6+
7+
export const JWT = 'JWT';
8+
9+
export const RS256 = 'RS256';

src/utils/interfaces.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,25 +62,31 @@ type MetadataType = {
6262
[key: string]: any;
6363
};
6464

65+
export type JwtHeaderType = {
66+
alg: string;
67+
typ: string;
68+
[key: string]: any;
69+
};
70+
6571
export type CryptrUser = {
6672
application_metadata?: MetadataType;
67-
at_hash?: string;
68-
aud?: string;
69-
c_hash?: string;
70-
cid?: string;
73+
at_hash: string;
74+
aud: string;
75+
c_hash: string;
76+
cid: string;
7177
dbs?: string;
7278
email: string;
7379
exp: number;
7480
family_name?: string;
7581
given_name?: string;
7682
iat: number;
7783
iss: string;
78-
jti?: string;
84+
jti: string;
7985
jtt: string;
8086
nonce: string;
8187
resource_owner_metadata?: MetadataType;
8288
s_hash?: string;
83-
scp?: string[];
89+
scp: string[];
8490
sub: string;
8591
tnt: string;
8692
ver: number;

0 commit comments

Comments
 (0)