@axa-fr/oidc-client 6.26.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +209 -0
- package/bin/post-install.mjs +58 -0
- package/dist/OidcServiceWorker.js +561 -0
- package/dist/OidcTrustedDomains.js +27 -0
- package/dist/cache.d.ts +3 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/checkSession.d.ts +4 -0
- package/dist/checkSession.d.ts.map +1 -0
- package/dist/checkSessionIFrame.d.ts +17 -0
- package/dist/checkSessionIFrame.d.ts.map +1 -0
- package/dist/crypto.d.ts +4 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/events.d.ts +29 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1236 -0
- package/dist/index.umd.cjs +2 -0
- package/dist/iniWorker.spec.d.ts +2 -0
- package/dist/iniWorker.spec.d.ts.map +1 -0
- package/dist/initSession.d.ts +22 -0
- package/dist/initSession.d.ts.map +1 -0
- package/dist/initWorker.d.ts +30 -0
- package/dist/initWorker.d.ts.map +1 -0
- package/dist/login.d.ts +8 -0
- package/dist/login.d.ts.map +1 -0
- package/dist/logout.d.ts +8 -0
- package/dist/logout.d.ts.map +1 -0
- package/dist/logout.spec.d.ts +1 -0
- package/dist/logout.spec.d.ts.map +1 -0
- package/dist/oidc.d.ts +101 -0
- package/dist/oidc.d.ts.map +1 -0
- package/dist/parseTokens.d.ts +37 -0
- package/dist/parseTokens.d.ts.map +1 -0
- package/dist/parseTokens.spec.d.ts +2 -0
- package/dist/parseTokens.spec.d.ts.map +1 -0
- package/dist/renewTokens.d.ts +4 -0
- package/dist/renewTokens.d.ts.map +1 -0
- package/dist/requests.d.ts +33 -0
- package/dist/requests.d.ts.map +1 -0
- package/dist/requests.spec.d.ts +2 -0
- package/dist/requests.spec.d.ts.map +1 -0
- package/dist/route-utils.d.ts +13 -0
- package/dist/route-utils.d.ts.map +1 -0
- package/dist/route-utils.spec.d.ts +2 -0
- package/dist/route-utils.spec.d.ts.map +1 -0
- package/dist/silentLogin.d.ts +10 -0
- package/dist/silentLogin.d.ts.map +1 -0
- package/dist/timer.d.ts +13 -0
- package/dist/timer.d.ts.map +1 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/user.d.ts +2 -0
- package/dist/user.d.ts.map +1 -0
- package/dist/vanillaOidc.d.ts +85 -0
- package/dist/vanillaOidc.d.ts.map +1 -0
- package/package.json +60 -0
- package/src/cache.ts +26 -0
- package/src/checkSession.ts +60 -0
- package/src/checkSessionIFrame.ts +83 -0
- package/src/crypto.ts +61 -0
- package/src/events.ts +28 -0
- package/src/index.ts +10 -0
- package/src/iniWorker.spec.ts +21 -0
- package/src/initSession.ts +89 -0
- package/src/initWorker.ts +321 -0
- package/src/login.ts +174 -0
- package/src/logout.spec.ts +65 -0
- package/src/logout.ts +101 -0
- package/src/oidc.ts +613 -0
- package/src/parseTokens.spec.ts +50 -0
- package/src/parseTokens.ts +194 -0
- package/src/renewTokens.ts +37 -0
- package/src/requests.spec.ts +9 -0
- package/src/requests.ts +169 -0
- package/src/route-utils.spec.ts +24 -0
- package/src/route-utils.ts +79 -0
- package/src/silentLogin.ts +144 -0
- package/src/timer.ts +163 -0
- package/src/types.ts +41 -0
- package/src/user.ts +40 -0
- package/src/vanillaOidc.ts +108 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { sleepAsync } from './initWorker.js';
|
|
2
|
+
|
|
3
|
+
const b64DecodeUnicode = (str) =>
|
|
4
|
+
decodeURIComponent(Array.prototype.map.call(atob(str), (c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));
|
|
5
|
+
const parseJwt = (token) => JSON.parse(b64DecodeUnicode(token.split('.')[1].replace('-', '+').replace('_', '/')));
|
|
6
|
+
|
|
7
|
+
const extractTokenPayload = (token) => {
|
|
8
|
+
try {
|
|
9
|
+
if (!token) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
if (countLetter(token, '.') === 2) {
|
|
13
|
+
return parseJwt(token);
|
|
14
|
+
} else {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
} catch (e) {
|
|
18
|
+
console.warn(e);
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const countLetter = (str, find) => {
|
|
24
|
+
return (str.split(find)).length - 1;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type Tokens = {
|
|
28
|
+
refreshToken: string;
|
|
29
|
+
idTokenPayload:any;
|
|
30
|
+
idToken:string;
|
|
31
|
+
accessTokenPayload:any;
|
|
32
|
+
accessToken:string;
|
|
33
|
+
expiresAt: number;
|
|
34
|
+
issuedAt: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type TokenRenewModeType = {
|
|
38
|
+
access_token_or_id_token_invalid: string;
|
|
39
|
+
access_token_invalid:string;
|
|
40
|
+
id_token_invalid: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const TokenRenewMode = {
|
|
44
|
+
access_token_or_id_token_invalid: 'access_token_or_id_token_invalid',
|
|
45
|
+
access_token_invalid: 'access_token_invalid',
|
|
46
|
+
id_token_invalid: 'id_token_invalid',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const setTokens = (tokens, oldTokens = null, tokenRenewMode: string):Tokens => {
|
|
50
|
+
if (!tokens) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
let accessTokenPayload;
|
|
54
|
+
|
|
55
|
+
if (!tokens.issuedAt) {
|
|
56
|
+
const currentTimeUnixSecond = new Date().getTime() / 1000;
|
|
57
|
+
tokens.issuedAt = currentTimeUnixSecond;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (tokens.accessTokenPayload !== undefined) {
|
|
61
|
+
accessTokenPayload = tokens.accessTokenPayload;
|
|
62
|
+
} else {
|
|
63
|
+
accessTokenPayload = extractTokenPayload(tokens.accessToken);
|
|
64
|
+
}
|
|
65
|
+
const _idTokenPayload = tokens.idTokenPayload ? tokens.idTokenPayload : extractTokenPayload(tokens.idToken);
|
|
66
|
+
|
|
67
|
+
const idTokenExpireAt = (_idTokenPayload && _idTokenPayload.exp) ? _idTokenPayload.exp : Number.MAX_VALUE;
|
|
68
|
+
const accessTokenExpiresAt = (accessTokenPayload && accessTokenPayload.exp) ? accessTokenPayload.exp : tokens.issuedAt + tokens.expiresIn;
|
|
69
|
+
|
|
70
|
+
let expiresAt;
|
|
71
|
+
|
|
72
|
+
if (tokenRenewMode === TokenRenewMode.access_token_invalid) {
|
|
73
|
+
expiresAt = accessTokenExpiresAt;
|
|
74
|
+
} else if (tokenRenewMode === TokenRenewMode.id_token_invalid) {
|
|
75
|
+
expiresAt = idTokenExpireAt;
|
|
76
|
+
} else {
|
|
77
|
+
expiresAt = idTokenExpireAt < accessTokenExpiresAt ? idTokenExpireAt : accessTokenExpiresAt;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const newTokens = { ...tokens, idTokenPayload: _idTokenPayload, accessTokenPayload, expiresAt };
|
|
81
|
+
// When refresh_token is not rotated we reuse ald refresh_token
|
|
82
|
+
if (oldTokens != null && 'refreshToken' in oldTokens && !('refreshToken' in tokens)) {
|
|
83
|
+
const refreshToken = oldTokens.refreshToken;
|
|
84
|
+
return { ...newTokens, refreshToken };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return newTokens;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const parseOriginalTokens = (tokens, oldTokens, tokenRenewMode: string) => {
|
|
91
|
+
if (!tokens) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
if (!tokens.issued_at) {
|
|
95
|
+
const currentTimeUnixSecond = new Date().getTime() / 1000;
|
|
96
|
+
tokens.issued_at = currentTimeUnixSecond;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const data = {
|
|
100
|
+
accessToken: tokens.access_token,
|
|
101
|
+
expiresIn: tokens.expires_in,
|
|
102
|
+
idToken: tokens.id_token,
|
|
103
|
+
scope: tokens.scope,
|
|
104
|
+
tokenType: tokens.token_type,
|
|
105
|
+
issuedAt: tokens.issued_at,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if ('refresh_token' in tokens) {
|
|
109
|
+
// @ts-ignore
|
|
110
|
+
data.refreshToken = tokens.refresh_token;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (tokens.accessTokenPayload !== undefined) {
|
|
114
|
+
// @ts-ignore
|
|
115
|
+
data.accessTokenPayload = tokens.accessTokenPayload;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (tokens.idTokenPayload !== undefined) {
|
|
119
|
+
// @ts-ignore
|
|
120
|
+
data.idTokenPayload = tokens.idTokenPayload;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return setTokens(data, oldTokens, tokenRenewMode);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const computeTimeLeft = (refreshTimeBeforeTokensExpirationInSecond, expiresAt) => {
|
|
127
|
+
const currentTimeUnixSecond = new Date().getTime() / 1000;
|
|
128
|
+
return Math.round(((expiresAt - refreshTimeBeforeTokensExpirationInSecond) - currentTimeUnixSecond));
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const isTokensValid = (tokens) => {
|
|
132
|
+
if (!tokens) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
return computeTimeLeft(0, tokens.expiresAt) > 0;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export type ValidToken = {
|
|
139
|
+
isTokensValid: boolean;
|
|
140
|
+
tokens: Tokens;
|
|
141
|
+
numberWaited: number;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface OidcToken{
|
|
145
|
+
tokens?: Tokens;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const getValidTokenAsync = async (oidc: OidcToken, waitMs = 200, numberWait = 50): Promise<ValidToken> => {
|
|
149
|
+
let numberWaitTemp = numberWait;
|
|
150
|
+
if (!oidc.tokens) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
while (!isTokensValid(oidc.tokens) && numberWaitTemp > 0) {
|
|
154
|
+
await sleepAsync(waitMs);
|
|
155
|
+
numberWaitTemp = numberWaitTemp - 1;
|
|
156
|
+
}
|
|
157
|
+
const isValid = isTokensValid(oidc.tokens);
|
|
158
|
+
return {
|
|
159
|
+
isTokensValid: isValid,
|
|
160
|
+
tokens: oidc.tokens,
|
|
161
|
+
numberWaited: numberWaitTemp - numberWait,
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation (excluding rules #1, #4, #5, #7, #8, #12, and #13 which did not apply).
|
|
166
|
+
// https://github.com/openid/AppAuth-JS/issues/65
|
|
167
|
+
export const isTokensOidcValid = (tokens, nonce, oidcServerConfiguration) => {
|
|
168
|
+
if (tokens.idTokenPayload) {
|
|
169
|
+
const idTokenPayload = tokens.idTokenPayload;
|
|
170
|
+
// 2: The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) MUST exactly match the value of the iss (issuer) Claim.
|
|
171
|
+
if (oidcServerConfiguration.issuer !== idTokenPayload.iss) {
|
|
172
|
+
return { isValid: false, reason: 'Issuer does not match' };
|
|
173
|
+
}
|
|
174
|
+
// 3: The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer identified by the iss (issuer) Claim as an audience. The aud (audience) Claim MAY contain an array with more than one element. The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, or if it contains additional audiences not trusted by the Client.
|
|
175
|
+
|
|
176
|
+
// 6: If the ID Token is received via direct communication between the Client and the Token Endpoint (which it is in this flow), the TLS server validation MAY be used to validate the issuer in place of checking the token signature. The Client MUST validate the signature of all other ID Tokens according to JWS [JWS] using the algorithm specified in the JWT alg Header Parameter. The Client MUST use the keys provided by the Issuer.
|
|
177
|
+
|
|
178
|
+
// 9: The current time MUST be before the time represented by the exp Claim.
|
|
179
|
+
const currentTimeUnixSecond = new Date().getTime() / 1000;
|
|
180
|
+
if (idTokenPayload.exp && idTokenPayload.exp < currentTimeUnixSecond) {
|
|
181
|
+
return { isValid: false, reason: 'Token expired' };
|
|
182
|
+
}
|
|
183
|
+
// 10: The iat Claim can be used to reject tokens that were issued too far away from the current time, limiting the amount of time that nonces need to be stored to prevent attacks. The acceptable range is Client specific.
|
|
184
|
+
const timeInSevenDays = 60 * 60 * 24 * 7;
|
|
185
|
+
if (idTokenPayload.iat && (idTokenPayload.iat + timeInSevenDays) < currentTimeUnixSecond) {
|
|
186
|
+
return { isValid: false, reason: 'Token is used from too long time' };
|
|
187
|
+
}
|
|
188
|
+
// 11: If a nonce value was sent in the Authentication Request, a nonce Claim MUST be present and its value checked to verify that it is the same value as the one that was sent in the Authentication Request. The Client SHOULD check the nonce value for replay attacks. The precise method for detecting replay attacks is Client specific.
|
|
189
|
+
if (idTokenPayload.nonce && idTokenPayload.nonce !== nonce) {
|
|
190
|
+
return { isValid: false, reason: 'Nonce does not match' };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { isValid: true, reason: '' };
|
|
194
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { initSession } from './initSession.js';
|
|
2
|
+
import { initWorkerAsync } from './initWorker.js';
|
|
3
|
+
import Oidc from './oidc.js';
|
|
4
|
+
import { computeTimeLeft } from './parseTokens.js';
|
|
5
|
+
import timer from './timer.js';
|
|
6
|
+
import { StringMap } from './types.js';
|
|
7
|
+
|
|
8
|
+
export async function renewTokensAndStartTimerAsync(oidc, refreshToken, forceRefresh = false, extras:StringMap = null) {
|
|
9
|
+
const updateTokens = (tokens) => { oidc.tokens = tokens; };
|
|
10
|
+
const { tokens, status } = await oidc.synchroniseTokensAsync(refreshToken, 0, forceRefresh, extras, updateTokens);
|
|
11
|
+
|
|
12
|
+
const serviceWorker = await initWorkerAsync(oidc.configuration.service_worker_relative_url, oidc.configurationName);
|
|
13
|
+
if (!serviceWorker) {
|
|
14
|
+
const session = initSession(oidc.configurationName, oidc.configuration.storage);
|
|
15
|
+
await session.setTokens(oidc.tokens);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!oidc.tokens) {
|
|
19
|
+
await oidc.destroyAsync(status);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (oidc.timeoutId) {
|
|
24
|
+
oidc.timeoutId = autoRenewTokens(oidc, tokens.refreshToken, oidc.tokens.expiresAt, extras);
|
|
25
|
+
}
|
|
26
|
+
return oidc.tokens;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const autoRenewTokens = (oidc, refreshToken, expiresAt, extras:StringMap = null) => {
|
|
30
|
+
const refreshTimeBeforeTokensExpirationInSecond = oidc.configuration.refresh_time_before_tokens_expiration_in_second;
|
|
31
|
+
return timer.setTimeout(async () => {
|
|
32
|
+
const timeLeft = computeTimeLeft(refreshTimeBeforeTokensExpirationInSecond, expiresAt);
|
|
33
|
+
const timeInfo = { timeLeft };
|
|
34
|
+
oidc.publishEvent(Oidc.eventNames.token_timer, timeInfo);
|
|
35
|
+
await renewTokensAndStartTimerAsync(oidc, refreshToken, false, extras);
|
|
36
|
+
}, 1000);
|
|
37
|
+
};
|
package/src/requests.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { getFromCache, setCache } from './cache.js';
|
|
2
|
+
import { deriveChallengeAsync, generateRandom } from './crypto.js';
|
|
3
|
+
import { OidcAuthorizationServiceConfiguration } from './oidc.js';
|
|
4
|
+
import { parseOriginalTokens } from './parseTokens.js';
|
|
5
|
+
import { Fetch, StringMap } from './types.js';
|
|
6
|
+
|
|
7
|
+
const oneHourSecond = 60 * 60;
|
|
8
|
+
export const fetchFromIssuer = (fetch) => async (openIdIssuerUrl: string, timeCacheSecond = oneHourSecond, storage = window.sessionStorage, timeoutMs = 10000):
|
|
9
|
+
Promise<OidcAuthorizationServiceConfiguration> => {
|
|
10
|
+
const fullUrl = `${openIdIssuerUrl}/.well-known/openid-configuration`;
|
|
11
|
+
|
|
12
|
+
const localStorageKey = `oidc.server:${openIdIssuerUrl}`;
|
|
13
|
+
const data = getFromCache(localStorageKey, storage, timeCacheSecond);
|
|
14
|
+
if (data) {
|
|
15
|
+
return new OidcAuthorizationServiceConfiguration(data);
|
|
16
|
+
}
|
|
17
|
+
const response = await internalFetch(fetch)(fullUrl, {}, timeoutMs);
|
|
18
|
+
|
|
19
|
+
if (response.status !== 200) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const result = await response.json();
|
|
24
|
+
|
|
25
|
+
setCache(localStorageKey, result, storage);
|
|
26
|
+
return new OidcAuthorizationServiceConfiguration(result);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const internalFetch = (fetch) => async (url, headers = {}, timeoutMs = 10000, numberRetry = 0) : Promise<Response> => {
|
|
30
|
+
let response;
|
|
31
|
+
try {
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
setTimeout(() => controller.abort(), timeoutMs);
|
|
34
|
+
response = await fetch(url, { ...headers, signal: controller.signal });
|
|
35
|
+
} catch (e: any) {
|
|
36
|
+
if (e.name === 'AbortError' ||
|
|
37
|
+
e.message === 'Network request failed') {
|
|
38
|
+
if (numberRetry <= 1) {
|
|
39
|
+
return await internalFetch(fetch)(url, headers, timeoutMs, numberRetry + 1);
|
|
40
|
+
} else {
|
|
41
|
+
throw e;
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
console.error(e.message);
|
|
45
|
+
throw e; // rethrow other unexpected errors
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return response;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const TOKEN_TYPE = {
|
|
52
|
+
refresh_token: 'refresh_token',
|
|
53
|
+
access_token: 'access_token',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const performRevocationRequestAsync = (fetch) => async (url, token, token_type = TOKEN_TYPE.refresh_token, client_id, timeoutMs = 10000) => {
|
|
57
|
+
const details = {
|
|
58
|
+
token,
|
|
59
|
+
token_type_hint: token_type,
|
|
60
|
+
client_id,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const formBody = [];
|
|
64
|
+
for (const property in details) {
|
|
65
|
+
const encodedKey = encodeURIComponent(property);
|
|
66
|
+
const encodedValue = encodeURIComponent(details[property]);
|
|
67
|
+
formBody.push(`${encodedKey}=${encodedValue}`);
|
|
68
|
+
}
|
|
69
|
+
const formBodyString = formBody.join('&');
|
|
70
|
+
|
|
71
|
+
const response = await internalFetch(fetch)(url, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
|
75
|
+
},
|
|
76
|
+
body: formBodyString,
|
|
77
|
+
}, timeoutMs);
|
|
78
|
+
if (response.status !== 200) {
|
|
79
|
+
return { success: false };
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
success: true,
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const performTokenRequestAsync = (fetch:Fetch) => async (url, details, extras, oldTokens, tokenRenewMode: string, timeoutMs = 10000) => {
|
|
87
|
+
for (const [key, value] of Object.entries(extras)) {
|
|
88
|
+
if (details[key] === undefined) {
|
|
89
|
+
details[key] = value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const formBody = [];
|
|
94
|
+
for (const property in details) {
|
|
95
|
+
const encodedKey = encodeURIComponent(property);
|
|
96
|
+
const encodedValue = encodeURIComponent(details[property]);
|
|
97
|
+
formBody.push(`${encodedKey}=${encodedValue}`);
|
|
98
|
+
}
|
|
99
|
+
const formBodyString = formBody.join('&');
|
|
100
|
+
|
|
101
|
+
const response = await internalFetch(fetch)(url, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: {
|
|
104
|
+
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
|
105
|
+
},
|
|
106
|
+
body: formBodyString,
|
|
107
|
+
}, timeoutMs);
|
|
108
|
+
if (response.status !== 200) {
|
|
109
|
+
return { success: false, status: response.status };
|
|
110
|
+
}
|
|
111
|
+
const tokens = await response.json();
|
|
112
|
+
return {
|
|
113
|
+
success: true,
|
|
114
|
+
data: parseOriginalTokens(tokens, oldTokens, tokenRenewMode),
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const performAuthorizationRequestAsync = (storage: any) => async (url, extras: StringMap) => {
|
|
119
|
+
extras = extras ? { ...extras } : {};
|
|
120
|
+
const codeVerifier = generateRandom(128);
|
|
121
|
+
const codeChallenge = await deriveChallengeAsync(codeVerifier);
|
|
122
|
+
await storage.setCodeVerifierAsync(codeVerifier);
|
|
123
|
+
await storage.setStateAsync(extras.state);
|
|
124
|
+
extras.code_challenge = codeChallenge;
|
|
125
|
+
extras.code_challenge_method = 'S256';
|
|
126
|
+
let queryString = '';
|
|
127
|
+
if (extras) {
|
|
128
|
+
for (const [key, value] of Object.entries(extras)) {
|
|
129
|
+
if (queryString === '') {
|
|
130
|
+
queryString += '?';
|
|
131
|
+
} else {
|
|
132
|
+
queryString += '&';
|
|
133
|
+
}
|
|
134
|
+
queryString += `${key}=${encodeURIComponent(value)}`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
window.location.href = `${url}${queryString}`;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const performFirstTokenRequestAsync = (storage:any) => async (url, extras, tokenRenewMode: string, timeoutMs = 10000) => {
|
|
141
|
+
extras = extras ? { ...extras } : {};
|
|
142
|
+
extras.code_verifier = await storage.getCodeVerifierAsync();
|
|
143
|
+
const formBody = [];
|
|
144
|
+
for (const property in extras) {
|
|
145
|
+
const encodedKey = encodeURIComponent(property);
|
|
146
|
+
const encodedValue = encodeURIComponent(extras[property]);
|
|
147
|
+
formBody.push(`${encodedKey}=${encodedValue}`);
|
|
148
|
+
}
|
|
149
|
+
const formBodyString = formBody.join('&');
|
|
150
|
+
const response = await internalFetch(fetch)(url, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: {
|
|
153
|
+
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
|
154
|
+
},
|
|
155
|
+
body: formBodyString,
|
|
156
|
+
}, timeoutMs);
|
|
157
|
+
await Promise.all([storage.setCodeVerifierAsync(null), storage.setStateAsync(null)]);
|
|
158
|
+
if (response.status !== 200) {
|
|
159
|
+
return { success: false, status: response.status };
|
|
160
|
+
}
|
|
161
|
+
const tokens = await response.json();
|
|
162
|
+
return {
|
|
163
|
+
success: true,
|
|
164
|
+
data: {
|
|
165
|
+
state: extras.state,
|
|
166
|
+
tokens: parseOriginalTokens(tokens, null, tokenRenewMode),
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect,it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { getPath } from './route-utils';
|
|
4
|
+
|
|
5
|
+
describe('Route test Suite', () => {
|
|
6
|
+
it.each([['http://example.com/pathname', '/pathname'],
|
|
7
|
+
['http://example.com:3000/pathname/?search=test#hash', '/pathname#hash'],
|
|
8
|
+
['http://example.com:3000/pathname/#hash?search=test', '/pathname#hash'],
|
|
9
|
+
['http://example.com:3000/pathname#hash?search=test', '/pathname#hash'],
|
|
10
|
+
['capacitor://localhost/index.html', '/index.html'],
|
|
11
|
+
['capacitor://localhost/pathname#hash?search=test', '/pathname#hash'],
|
|
12
|
+
['http://example.com:3000/', '']])(
|
|
13
|
+
'getPath should return the full path of an url',
|
|
14
|
+
(uri, expected) => {
|
|
15
|
+
|
|
16
|
+
const path = getPath(uri);
|
|
17
|
+
expect(path).toBe(expected);
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
it('wrong uri format', () => {
|
|
22
|
+
expect(() => getPath("urimybad/toto.com")).toThrowError();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export const getLocation = (href: string) => {
|
|
2
|
+
const match = href.match(
|
|
3
|
+
// eslint-disable-next-line no-useless-escape
|
|
4
|
+
/^([a-z][\w-]+\:)\/\/(([^:\/?#]*)(?:\:([0-9]+))?)([\/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/,
|
|
5
|
+
);
|
|
6
|
+
if (!match) {
|
|
7
|
+
throw new Error('Invalid URL');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let search = match[6];
|
|
11
|
+
let hash = match[7];
|
|
12
|
+
|
|
13
|
+
if (hash) {
|
|
14
|
+
const splits = hash.split('?');
|
|
15
|
+
if (splits.length === 2) {
|
|
16
|
+
hash = splits[0];
|
|
17
|
+
search = splits[1];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (search.startsWith('?')) {
|
|
22
|
+
search = search.slice(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
match && {
|
|
27
|
+
href,
|
|
28
|
+
protocol: match[1],
|
|
29
|
+
host: match[2],
|
|
30
|
+
hostname: match[3],
|
|
31
|
+
port: match[4],
|
|
32
|
+
path: match[5],
|
|
33
|
+
search,
|
|
34
|
+
hash,
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const getPath = (href: string) => {
|
|
40
|
+
const location = getLocation(href);
|
|
41
|
+
let { path } = location;
|
|
42
|
+
|
|
43
|
+
if (path.endsWith('/')) {
|
|
44
|
+
path = path.slice(0, -1);
|
|
45
|
+
}
|
|
46
|
+
let { hash } = location;
|
|
47
|
+
|
|
48
|
+
if (hash === '#_=_') {
|
|
49
|
+
hash = '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (hash) {
|
|
53
|
+
path += hash;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return path;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const getParseQueryStringFromLocation = (href: string) => {
|
|
60
|
+
const location = getLocation(href);
|
|
61
|
+
const { search } = location;
|
|
62
|
+
|
|
63
|
+
return parseQueryString(search);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const parseQueryString = (queryString:string) => {
|
|
67
|
+
const params:any = {}; let temp; let i; let l;
|
|
68
|
+
|
|
69
|
+
// Split into key/value pairs
|
|
70
|
+
const queries = queryString.split('&');
|
|
71
|
+
|
|
72
|
+
// Convert the array of strings into an object
|
|
73
|
+
for (i = 0, l = queries.length; i < l; i++) {
|
|
74
|
+
temp = queries[i].split('=');
|
|
75
|
+
params[decodeURIComponent(temp[0])] = decodeURIComponent(temp[1]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return params;
|
|
79
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { eventNames } from './events.js';
|
|
2
|
+
import { Tokens } from './parseTokens.js';
|
|
3
|
+
import { autoRenewTokens } from './renewTokens.js';
|
|
4
|
+
import timer from './timer.js';
|
|
5
|
+
import { OidcConfiguration, StringMap } from './types.js';
|
|
6
|
+
export type SilentLoginResponse = {
|
|
7
|
+
tokens:Tokens;
|
|
8
|
+
sessionState:string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
12
|
+
export const _silentLoginAsync = (configurationName:string, configuration:OidcConfiguration, publishEvent:Function) => (extras:StringMap = null, state:string = null, scope:string = null):Promise<SilentLoginResponse> => {
|
|
13
|
+
if (!configuration.silent_redirect_uri || !configuration.silent_login_uri) {
|
|
14
|
+
return Promise.resolve(null);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
publishEvent(eventNames.silentLoginAsync_begin, {});
|
|
19
|
+
let queries = '';
|
|
20
|
+
|
|
21
|
+
if (state) {
|
|
22
|
+
if (extras == null) {
|
|
23
|
+
extras = {};
|
|
24
|
+
}
|
|
25
|
+
extras.state = state;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (scope) {
|
|
29
|
+
if (extras == null) {
|
|
30
|
+
extras = {};
|
|
31
|
+
}
|
|
32
|
+
extras.scope = scope;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (extras != null) {
|
|
36
|
+
for (const [key, value] of Object.entries(extras)) {
|
|
37
|
+
if (queries === '') {
|
|
38
|
+
queries = `?${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
39
|
+
} else {
|
|
40
|
+
queries += `&${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const link = configuration.silent_login_uri + queries;
|
|
45
|
+
const idx = link.indexOf('/', link.indexOf('//') + 2);
|
|
46
|
+
const iFrameOrigin = link.substr(0, idx);
|
|
47
|
+
const iframe = document.createElement('iframe');
|
|
48
|
+
iframe.width = '0px';
|
|
49
|
+
iframe.height = '0px';
|
|
50
|
+
|
|
51
|
+
iframe.id = `${configurationName}_oidc_iframe`;
|
|
52
|
+
iframe.setAttribute('src', link);
|
|
53
|
+
document.body.appendChild(iframe);
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
try {
|
|
56
|
+
let isResolved = false;
|
|
57
|
+
window.onmessage = (e: MessageEvent<any>) => {
|
|
58
|
+
if (e.origin === iFrameOrigin &&
|
|
59
|
+
e.source === iframe.contentWindow
|
|
60
|
+
) {
|
|
61
|
+
const key = `${configurationName}_oidc_tokens:`;
|
|
62
|
+
const key_error = `${configurationName}_oidc_error:`;
|
|
63
|
+
const data = e.data;
|
|
64
|
+
if (data && typeof (data) === 'string') {
|
|
65
|
+
if (!isResolved) {
|
|
66
|
+
if (data.startsWith(key)) {
|
|
67
|
+
const result = JSON.parse(e.data.replace(key, ''));
|
|
68
|
+
publishEvent(eventNames.silentLoginAsync_end, {});
|
|
69
|
+
iframe.remove();
|
|
70
|
+
isResolved = true;
|
|
71
|
+
resolve(result);
|
|
72
|
+
} else if (data.startsWith(key_error)) {
|
|
73
|
+
const result = JSON.parse(e.data.replace(key_error, ''));
|
|
74
|
+
publishEvent(eventNames.silentLoginAsync_error, result);
|
|
75
|
+
iframe.remove();
|
|
76
|
+
isResolved = true;
|
|
77
|
+
reject(new Error('oidc_' + result.error));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const silentSigninTimeout = configuration.silent_login_timeout;
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
if (!isResolved) {
|
|
86
|
+
publishEvent(eventNames.silentLoginAsync_error, { reason: 'timeout' });
|
|
87
|
+
iframe.remove();
|
|
88
|
+
isResolved = true;
|
|
89
|
+
reject(new Error('timeout'));
|
|
90
|
+
}
|
|
91
|
+
}, silentSigninTimeout);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
iframe.remove();
|
|
94
|
+
publishEvent(eventNames.silentLoginAsync_error, e);
|
|
95
|
+
reject(e);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
} catch (e) {
|
|
99
|
+
publishEvent(eventNames.silentLoginAsync_error, e);
|
|
100
|
+
throw e;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
105
|
+
export const defaultSilentLoginAsync = (window, configurationName, configuration:OidcConfiguration, publishEvent :(string, any)=>void, oidc:any) => (extras:StringMap = null, scope:string = undefined) => {
|
|
106
|
+
extras = { ...extras };
|
|
107
|
+
|
|
108
|
+
const silentLoginAsync = (extras, state, scope) => {
|
|
109
|
+
return _silentLoginAsync(configurationName, configuration, publishEvent.bind(oidc))(extras, state, scope);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const loginLocalAsync = async () => {
|
|
113
|
+
if (oidc.timeoutId) {
|
|
114
|
+
timer.clearTimeout(oidc.timeoutId);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let state;
|
|
118
|
+
if (extras && 'state' in extras) {
|
|
119
|
+
state = extras.state;
|
|
120
|
+
delete extras.state;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const extraFinal = !configuration.extras ? extras : { ...configuration.extras, ...extras };
|
|
125
|
+
const silentResult = await silentLoginAsync({
|
|
126
|
+
...extraFinal,
|
|
127
|
+
prompt: 'none',
|
|
128
|
+
}, state, scope);
|
|
129
|
+
|
|
130
|
+
if (silentResult) {
|
|
131
|
+
oidc.tokens = silentResult.tokens;
|
|
132
|
+
publishEvent(eventNames.token_aquired, {});
|
|
133
|
+
// @ts-ignore
|
|
134
|
+
oidc.timeoutId = autoRenewTokens(oidc, oidc.tokens.refreshToken, oidc.tokens.expiresAt, extras);
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
} catch (e) {
|
|
138
|
+
return e;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
return loginLocalAsync();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export default defaultSilentLoginAsync;
|