@authress/login-react-native 0.0.0
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/LICENSE +201 -0
- package/README.md +466 -0
- package/package.json +63 -0
- package/src/authStorageManager.ts +142 -0
- package/src/httpClient.ts +165 -0
- package/src/index.ts +24 -0
- package/src/jwtManager.ts +101 -0
- package/src/loginClient.ts +371 -0
- package/src/settingsValidator.ts +55 -0
- package/src/types.ts +122 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { Linking } from 'react-native';
|
|
2
|
+
import { Result, ok, err } from 'neverthrow';
|
|
3
|
+
import HttpClient, { AuthressHttpError, Logger } from './httpClient.ts';
|
|
4
|
+
import jwtManager from './jwtManager.ts';
|
|
5
|
+
import authStorageManager, { PendingAuthentication, SecurityContextError } from './authStorageManager.ts'; // SecurityContextError re-exported for public API
|
|
6
|
+
import { validateSettings, ValidatedSettings } from './settingsValidator.ts';
|
|
7
|
+
import {
|
|
8
|
+
TokenTimeoutError, NoAuthenticationRequestInProgressError, AuthenticationRequestMismatchError,
|
|
9
|
+
NotLoggedInError, InvalidConnectionError
|
|
10
|
+
} from './types.ts';
|
|
11
|
+
import type {
|
|
12
|
+
Settings, AuthenticateResponse, AuthenticationParameters, LinkIdentityParameters,
|
|
13
|
+
UserProfile, UserIdentity, TokenParameters, Device, AuthFlowError
|
|
14
|
+
} from './types.ts';
|
|
15
|
+
|
|
16
|
+
const defaultLogger: Logger = {
|
|
17
|
+
debug() {},
|
|
18
|
+
log() {},
|
|
19
|
+
warn: (...args) => console.warn(...args),
|
|
20
|
+
error: (...args) => console.error(...args)
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class LoginClient {
|
|
24
|
+
private readonly settings: ValidatedSettings;
|
|
25
|
+
private readonly httpClient: HttpClient;
|
|
26
|
+
private readonly logger: Logger;
|
|
27
|
+
private _sessionCheckPromise: Promise<boolean> = Promise.resolve(false);
|
|
28
|
+
private _sessionCheckIsInProgress = false;
|
|
29
|
+
private _sessionPromise!: Promise<void>;
|
|
30
|
+
private _sessionResolver!: () => void;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param settings Authress LoginClient settings — authressApiUrl, applicationId, and redirectUri are all required.
|
|
34
|
+
* @param logger Optional logger (e.g. `console`) for debug and warning messages.
|
|
35
|
+
*/
|
|
36
|
+
constructor(settings: Settings, logger?: Logger) {
|
|
37
|
+
this.settings = validateSettings(settings);
|
|
38
|
+
this.logger = logger ?? defaultLogger;
|
|
39
|
+
this.httpClient = new HttpClient(this.settings.authressApiUrl, this.logger);
|
|
40
|
+
this._resetSessionPromise();
|
|
41
|
+
|
|
42
|
+
// fire-and-forget — restore cookies from encrypted storage into native cookie jar on startup
|
|
43
|
+
authStorageManager.restoreCookies(this.settings.authressApiUrl);
|
|
44
|
+
|
|
45
|
+
// Automatically handle deep link callbacks — no Linking boilerplate needed in app code
|
|
46
|
+
Linking.addEventListener('url', ({ url }) => {
|
|
47
|
+
if (!url.startsWith(this.settings.redirectUri)) { return; }
|
|
48
|
+
const parsed = new URL(url);
|
|
49
|
+
const code = parsed.searchParams.get('code') ?? '';
|
|
50
|
+
const authenticationRequestId = parsed.searchParams.get('authenticationRequestId') ?? '';
|
|
51
|
+
this.completeAuthenticationRequest({ code, authenticationRequestId });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private _resetSessionPromise(): void {
|
|
56
|
+
this._sessionPromise = new Promise<void>(resolve => { this._sessionResolver = resolve; });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private _resolveSession(): void {
|
|
60
|
+
this._sessionResolver?.();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── userIsLoggedIn ───────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Checks if the user's session is still valid, even if their current token is expired. May call
|
|
67
|
+
* Authress API to validate the session. Recommendation: call on every route change.
|
|
68
|
+
* @returns `true` if a valid session exists, `false` if not logged in or if the server call fails.
|
|
69
|
+
*/
|
|
70
|
+
async userIsLoggedIn(): Promise<boolean> {
|
|
71
|
+
const tokenResult = await this.getToken();
|
|
72
|
+
if (tokenResult.isOk()) {
|
|
73
|
+
this._resolveSession();
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (this._sessionCheckIsInProgress) {
|
|
78
|
+
return this._sessionCheckPromise;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this._sessionCheckIsInProgress = true;
|
|
82
|
+
this._sessionCheckPromise = this._doSessionCheck().finally(() => {
|
|
83
|
+
this._sessionCheckIsInProgress = false;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return this._sessionCheckPromise;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private async _doSessionCheck(): Promise<boolean> {
|
|
90
|
+
const sessionResult = await this.httpClient.patch('/session', {});
|
|
91
|
+
if (sessionResult.isErr()) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const tokenResult = await this.getToken();
|
|
96
|
+
if (tokenResult.isErr()) { return false; }
|
|
97
|
+
|
|
98
|
+
await authStorageManager.backupCookies(this.settings.authressApiUrl);
|
|
99
|
+
this._resolveSession();
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── authenticate ─────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Begins the login flow. Redirects the user to their selected connection/provider and then back to the
|
|
107
|
+
* `redirectUri` from Settings. If neither `connectionId` nor `tenantLookupIdentifier` is specified the user
|
|
108
|
+
* will be directed to the Authress hosted login page to select their preferred login method.
|
|
109
|
+
* @returns `Ok` with the `authenticationUrl` to open in the device browser and the `authenticationRequestId` to pass to {@link completeAuthenticationRequest}.
|
|
110
|
+
*/
|
|
111
|
+
async authenticate(options?: AuthenticationParameters): Promise<Result<AuthenticateResponse, AuthressHttpError | SecurityContextError>> {
|
|
112
|
+
await authStorageManager.setAuthenticationRequest(null);
|
|
113
|
+
|
|
114
|
+
let codeVerifier: string; let codeChallenge: string;
|
|
115
|
+
try {
|
|
116
|
+
({ codeVerifier, codeChallenge } = await jwtManager.getAuthCodes());
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return err(new SecurityContextError(String(e)));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const body = {
|
|
122
|
+
redirectUrl: this.settings.redirectUri,
|
|
123
|
+
applicationId: this.settings.applicationId,
|
|
124
|
+
codeChallenge,
|
|
125
|
+
codeChallengeMethod: 'S256',
|
|
126
|
+
...options
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const postResult = await this.httpClient.post<{ authenticationUrl: string; authenticationRequestId: string }>('/authentication', body);
|
|
130
|
+
if (postResult.isErr()) {return postResult;}
|
|
131
|
+
|
|
132
|
+
const { authenticationUrl, authenticationRequestId } = postResult.value.data;
|
|
133
|
+
const pendingAuth: PendingAuthentication = {
|
|
134
|
+
codeVerifier,
|
|
135
|
+
authenticationRequestId,
|
|
136
|
+
redirectUrl: this.settings.redirectUri
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const storeResult = await authStorageManager.setAuthenticationRequest(pendingAuth);
|
|
140
|
+
if (storeResult.isErr()) {return storeResult;}
|
|
141
|
+
|
|
142
|
+
return ok({ authenticationUrl, authenticationRequestId });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── completeAuthenticationRequest ────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Completes the PKCE login flow after the user returns from the Authress-hosted login page via the deep link.
|
|
149
|
+
* Call this from your deep link handler with the `code` and `authenticationRequestId` query parameters from the redirect URL.
|
|
150
|
+
* @returns `Ok` when the session is established, `Err` if parameters are wrong or the server rejects the exchange.
|
|
151
|
+
*/
|
|
152
|
+
async completeAuthenticationRequest(params: { code: string; authenticationRequestId: string }): Promise<Result<void, AuthFlowError | AuthressHttpError>> {
|
|
153
|
+
const pendingAuth = (await authStorageManager.getAuthenticationRequest()).unwrapOr(null);
|
|
154
|
+
if (!pendingAuth) {return err(new NoAuthenticationRequestInProgressError());}
|
|
155
|
+
if (pendingAuth.authenticationRequestId !== params.authenticationRequestId) {return err(new AuthenticationRequestMismatchError());}
|
|
156
|
+
|
|
157
|
+
const tokenResult = await this.httpClient.post(`/authentication/${params.authenticationRequestId}/tokens`, {
|
|
158
|
+
code: params.code,
|
|
159
|
+
codeVerifier: pendingAuth.codeVerifier,
|
|
160
|
+
redirectUri: pendingAuth.redirectUrl
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (tokenResult.isErr()) {
|
|
164
|
+
const error = tokenResult.error;
|
|
165
|
+
// already-used code — auth is done, clean up and return silently
|
|
166
|
+
if (error.name !== 'AuthressHttpNetworkError' && error.status < 500) {
|
|
167
|
+
return ok();
|
|
168
|
+
}
|
|
169
|
+
return tokenResult;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await authStorageManager.backupCookies(this.settings.authressApiUrl);
|
|
173
|
+
this._resolveSession();
|
|
174
|
+
return ok();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── getToken ─────────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Returns the current bearer token from the session cookie, or `null` if the user is not logged in.
|
|
181
|
+
* Use {@link waitForToken} if you need to block until a session exists.
|
|
182
|
+
*/
|
|
183
|
+
async getToken(): Promise<Result<string, NotLoggedInError>> {
|
|
184
|
+
const cookieResult = await authStorageManager.getAuthorizationCookie(this.settings.authressApiUrl);
|
|
185
|
+
const token = cookieResult.unwrapOr(null);
|
|
186
|
+
if (!token) { return err(new NotLoggedInError()); }
|
|
187
|
+
|
|
188
|
+
const payload = jwtManager.decode(token);
|
|
189
|
+
if (!payload) { return err(new NotLoggedInError()); }
|
|
190
|
+
|
|
191
|
+
const expectedOrigin = new URL(this.settings.authressApiUrl).origin;
|
|
192
|
+
if (payload.iss !== expectedOrigin) { return err(new NotLoggedInError()); }
|
|
193
|
+
|
|
194
|
+
return ok(token);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── waitForToken ──────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Waits until a bearer token is available and returns it. Blocks until {@link authenticate} +
|
|
201
|
+
* {@link completeAuthenticationRequest} or {@link userIsLoggedIn} establishes a session.
|
|
202
|
+
* Use in the `Authorization: Bearer <token>` header for API calls.
|
|
203
|
+
* @returns `Ok(token)` when available, `Err({ code: 'TokenTimeout' })` if not available within `timeoutInMillis`.
|
|
204
|
+
*/
|
|
205
|
+
async waitForToken(options?: TokenParameters): Promise<Result<void, TokenTimeoutError>> {
|
|
206
|
+
const { timeoutInMillis = 5000 } = options ?? {};
|
|
207
|
+
|
|
208
|
+
const tokenResult = await this.getToken();
|
|
209
|
+
if (tokenResult.isOk()) {return ok();}
|
|
210
|
+
|
|
211
|
+
if (timeoutInMillis === 0) {return err(new TokenTimeoutError());}
|
|
212
|
+
|
|
213
|
+
const clampedTimeout = timeoutInMillis === -1 || timeoutInMillis > (2 ** 31 - 1) ? (2 ** 31 - 1) : timeoutInMillis;
|
|
214
|
+
|
|
215
|
+
let timeoutId: ReturnType<typeof setTimeout>;
|
|
216
|
+
const timeoutPromise = new Promise<Result<void, TokenTimeoutError>>(resolve => {
|
|
217
|
+
timeoutId = setTimeout(() => resolve(err(new TokenTimeoutError())), clampedTimeout);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const sessionPromise = this._sessionPromise.then(async () => {
|
|
221
|
+
const newTokenResult = await this.getToken();
|
|
222
|
+
return newTokenResult.isOk() ? ok() : err(new TokenTimeoutError());
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
return await Promise.race([timeoutPromise, sessionPromise]);
|
|
227
|
+
} finally {
|
|
228
|
+
clearTimeout(timeoutId!);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── logout ───────────────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Logs the user out, removing their session. If the user is not logged in this has no effect.
|
|
236
|
+
*/
|
|
237
|
+
async logout(): Promise<Result<void, never>> {
|
|
238
|
+
// DELETE /session while the native cookie jar is still intact so the server can identify the session
|
|
239
|
+
await this.httpClient.delete('/session');
|
|
240
|
+
// Now expire native cookies now that the server-side session is gone
|
|
241
|
+
await authStorageManager.clear(this.settings.authressApiUrl);
|
|
242
|
+
this._resetSessionPromise();
|
|
243
|
+
return ok();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── getUserIdentity ──────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Returns the decoded user identity from the current session token. Should be called after
|
|
250
|
+
* {@link userIsLoggedIn} or it will return `null`. Use for populating user personalization in your UI.
|
|
251
|
+
* For linked identities use {@link getUserProfile}.
|
|
252
|
+
*/
|
|
253
|
+
async getUserIdentity(): Promise<Result<UserIdentity, NotLoggedInError>> {
|
|
254
|
+
const idTokenResult = await authStorageManager.getUserCookie(this.settings.authressApiUrl);
|
|
255
|
+
if (idTokenResult.isErr()) { return err(new NotLoggedInError()); }
|
|
256
|
+
const idToken = idTokenResult.unwrapOr(null);
|
|
257
|
+
|
|
258
|
+
const IdTokenPayload = jwtManager.decode(idToken);
|
|
259
|
+
if (!IdTokenPayload) {return err(new NotLoggedInError());}
|
|
260
|
+
|
|
261
|
+
const expectedOrigin = new URL(this.settings.authressApiUrl).origin;
|
|
262
|
+
if (IdTokenPayload.iss !== expectedOrigin) {return err(new NotLoggedInError());}
|
|
263
|
+
|
|
264
|
+
return ok({ ...IdTokenPayload, userId: IdTokenPayload.sub as string, sub: IdTokenPayload.sub as string });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── getUserProfile ───────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Retrieves the user's full profile from Authress, including all linked identities.
|
|
271
|
+
* @returns `Err({ code: 'NotLoggedIn' })` if the user is not logged in.
|
|
272
|
+
*/
|
|
273
|
+
async getUserProfile(): Promise<Result<UserProfile, AuthressHttpError | NotLoggedInError>> {
|
|
274
|
+
const tokenResult = await this.getToken();
|
|
275
|
+
if (tokenResult.isErr()) { return err(new NotLoggedInError()); }
|
|
276
|
+
const token = tokenResult.unwrapOr(null);
|
|
277
|
+
|
|
278
|
+
const payload = jwtManager.decode(token);
|
|
279
|
+
if (!payload) {return err(new NotLoggedInError());}
|
|
280
|
+
|
|
281
|
+
const expectedOrigin = new URL(this.settings.authressApiUrl).origin;
|
|
282
|
+
if (payload.iss !== expectedOrigin) {return err(new NotLoggedInError());}
|
|
283
|
+
|
|
284
|
+
const profileResult = await this.httpClient.get<UserProfile>('/session/profile');
|
|
285
|
+
if (profileResult.isErr()) {return profileResult;}
|
|
286
|
+
|
|
287
|
+
return ok(profileResult.value.data);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── linkIdentity ─────────────────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Links a new identity to the currently logged-in user. The user will be asked to authenticate
|
|
294
|
+
* with the specified connection. Either `connectionId` or `tenantLookupIdentifier` is required.
|
|
295
|
+
* @returns `Err({ code: 'NotLoggedIn' })` if the user is not logged in.
|
|
296
|
+
*/
|
|
297
|
+
async linkIdentity(options: LinkIdentityParameters): Promise<Result<AuthenticateResponse, AuthressHttpError | SecurityContextError | NotLoggedInError | InvalidConnectionError>> {
|
|
298
|
+
await authStorageManager.setAuthenticationRequest(null);
|
|
299
|
+
|
|
300
|
+
if (!options.connectionId && !options.tenantLookupIdentifier) {
|
|
301
|
+
return err(new InvalidConnectionError());
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if ((await this.getToken()).isErr()) { return err(new NotLoggedInError()); }
|
|
305
|
+
|
|
306
|
+
let codeVerifier: string; let codeChallenge: string;
|
|
307
|
+
try {
|
|
308
|
+
({ codeVerifier, codeChallenge } = await jwtManager.getAuthCodes());
|
|
309
|
+
} catch (e) {
|
|
310
|
+
return err(new SecurityContextError(String(e)));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const body = {
|
|
314
|
+
redirectUrl: this.settings.redirectUri,
|
|
315
|
+
applicationId: this.settings.applicationId,
|
|
316
|
+
codeChallenge,
|
|
317
|
+
codeChallengeMethod: 'S256',
|
|
318
|
+
linkIdentity: true,
|
|
319
|
+
...options
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const postResult = await this.httpClient.post<{ authenticationUrl: string; authenticationRequestId: string }>('/authentication', body);
|
|
323
|
+
if (postResult.isErr()) {return postResult;}
|
|
324
|
+
|
|
325
|
+
const { authenticationUrl, authenticationRequestId } = postResult.value.data;
|
|
326
|
+
const pendingAuth: PendingAuthentication = {
|
|
327
|
+
codeVerifier,
|
|
328
|
+
authenticationRequestId,
|
|
329
|
+
redirectUrl: this.settings.redirectUri
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const storeResult = await authStorageManager.setAuthenticationRequest(pendingAuth);
|
|
333
|
+
if (storeResult.isErr()) {return storeResult;}
|
|
334
|
+
|
|
335
|
+
return ok({ authenticationUrl, authenticationRequestId });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ── getDevices ───────────────────────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Fetches the list of MFA devices registered to the current user.
|
|
342
|
+
* @returns `Ok([])` if not logged in or no devices, `Err` on server/network failure.
|
|
343
|
+
*/
|
|
344
|
+
async getDevices(): Promise<Result<Device[], AuthressHttpError | NotLoggedInError>> {
|
|
345
|
+
const tokenResult = await this.getToken();
|
|
346
|
+
if (tokenResult.isErr()) { return err(new NotLoggedInError()); }
|
|
347
|
+
|
|
348
|
+
const result = await this.httpClient.get<{ devices?: Device[] }>('/session/devices');
|
|
349
|
+
if (result.isErr()) {
|
|
350
|
+
const error = result.error;
|
|
351
|
+
if (error.name !== 'AuthressHttpNetworkError' && (error.status === 401 || error.status === 404)) {return ok([]);}
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
return ok(result.value.data.devices ?? []);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ── deleteDevice ─────────────────────────────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Removes an MFA device from the current user's profile.
|
|
361
|
+
* @param deviceId The ID of the device to remove.
|
|
362
|
+
*/
|
|
363
|
+
async deleteDevice(deviceId: string): Promise<Result<void, AuthressHttpError | NotLoggedInError>> {
|
|
364
|
+
const tokenResult = await this.getToken();
|
|
365
|
+
if (tokenResult.isErr()) { return err(new NotLoggedInError()); }
|
|
366
|
+
|
|
367
|
+
const result = await this.httpClient.delete(`/session/devices/${deviceId}`);
|
|
368
|
+
if (result.isErr()) {return result;}
|
|
369
|
+
return ok();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface ValidatedSettings {
|
|
2
|
+
authressApiUrl: string;
|
|
3
|
+
applicationId: string;
|
|
4
|
+
redirectUri: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function sanitizeUrl(rawUrlString: string): string {
|
|
8
|
+
let sanitizedUrl = rawUrlString;
|
|
9
|
+
if (!sanitizedUrl.startsWith('http')) {
|
|
10
|
+
sanitizedUrl = `https://${sanitizedUrl}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const url = new URL(sanitizedUrl);
|
|
14
|
+
const domainBaseUrlMatch = url.host.match(/^([a-z0-9-]+)[.][a-z0-9-]+[.]authress[.]io$/);
|
|
15
|
+
if (domainBaseUrlMatch) {
|
|
16
|
+
url.host = `${domainBaseUrlMatch[1]}.login.authress.io`;
|
|
17
|
+
sanitizedUrl = url.toString();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return sanitizedUrl.replace(/[/]+$/, '');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function validateSettings(settings: { authressApiUrl: string; applicationId: string; redirectUri: string }): ValidatedSettings {
|
|
24
|
+
if (!settings.authressApiUrl) {
|
|
25
|
+
throw new Error('Missing required property "authressApiUrl" in LoginClient constructor. Custom Authress Domain Host is required.');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const applicationId = settings.applicationId?.trim();
|
|
29
|
+
if (!applicationId) {
|
|
30
|
+
const error = Object.assign(new Error('Application ID is required.'), { code: 'InvalidApplication' });
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (applicationId.match(/^(sc_|ext_)/)) {
|
|
35
|
+
const error = Object.assign(
|
|
36
|
+
new Error(
|
|
37
|
+
'You have incorrectly specified an Authress Service Client or Extension as the applicationId instead of a valid application. '
|
|
38
|
+
+ 'The applicationId is your application that your users will log into. '
|
|
39
|
+
+ 'Users cannot log into a Service Client — specify the Service Client as the connectionId instead.'
|
|
40
|
+
),
|
|
41
|
+
{ code: 'InvalidApplication' }
|
|
42
|
+
);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!settings.redirectUri) {
|
|
47
|
+
throw new Error('Missing required property "redirectUri" in LoginClient constructor.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
authressApiUrl: sanitizeUrl(settings.authressApiUrl),
|
|
52
|
+
applicationId,
|
|
53
|
+
redirectUri: settings.redirectUri
|
|
54
|
+
};
|
|
55
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export interface Settings {
|
|
2
|
+
/** Your Authress custom domain - see https://authress.io/app/#/setup?focus=domain */
|
|
3
|
+
authressApiUrl: string;
|
|
4
|
+
/** The Authress applicationId for this app - see https://authress.io/app/#/manage?focus=applications */
|
|
5
|
+
applicationId: string;
|
|
6
|
+
/** The deep link URI that Authress will redirect back to after authentication. Must match a registered redirect URI for the application. */
|
|
7
|
+
redirectUri: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface AuthenticateResponse {
|
|
11
|
+
/** The second step of the authentication flow requires the user to log in with their selected provider. Redirect the user to this location. */
|
|
12
|
+
authenticationUrl?: string;
|
|
13
|
+
authenticationRequestId: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AuthenticationParameters {
|
|
17
|
+
/** Specify which provider connection that user would like to use to log in - see https://authress.io/app/#/manage?focus=connections */
|
|
18
|
+
connectionId?: string;
|
|
19
|
+
/** Instead of connectionId, specify the tenant lookup identifier to log the user with the mapped tenant. Takes precedent over the connectionId - see https://authress.io/app/#/manage?focus=tenants */
|
|
20
|
+
tenantLookupIdentifier?: string;
|
|
21
|
+
/** Invite to use to login, only one of the connectionId, tenantLookupIdentifier, or the inviteId is required. */
|
|
22
|
+
inviteId?: string;
|
|
23
|
+
/** Store the credentials response in the specified location. Options are either 'cookie' or 'query'. (Default: **cookie**) */
|
|
24
|
+
responseLocation?: string;
|
|
25
|
+
/** The type of credentials returned in the response. The list of options is any of 'code token id_token' separated by a space. (Default: **token id_token**) */
|
|
26
|
+
flowType?: string;
|
|
27
|
+
/** A list of scopes to populate into the scope claim of the generated JWT. */
|
|
28
|
+
scopes?: Array<string>;
|
|
29
|
+
/** Specify where the provider should redirect the user to in your application. Must be a valid redirect url matching what is defined in the application in the Authress Management portal. (Default: **redirectUri from Settings**) */
|
|
30
|
+
redirectUrl?: string;
|
|
31
|
+
/** A list of audiences to add to the JWT in the `aud` claim. This list must be a subset of the audiences defined for the application. */
|
|
32
|
+
audiences?: Array<string>;
|
|
33
|
+
/** Overrides the connection specific properties from the Authress Identity Connection to pass to the identity provider */
|
|
34
|
+
connectionProperties?: Record<string, string>;
|
|
35
|
+
/** Enable multi-account login. (Default: **false**) */
|
|
36
|
+
multiAccount?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface LinkIdentityParameters {
|
|
40
|
+
/** Specify which provider connection that user would like to use to log in - see https://authress.io/app/#/manage?focus=connections */
|
|
41
|
+
connectionId?: string;
|
|
42
|
+
/** Instead of connectionId, specify the tenant lookup identifier to log the user with the mapped tenant - see https://authress.io/app/#/manage?focus=tenants */
|
|
43
|
+
tenantLookupIdentifier?: string;
|
|
44
|
+
/** Specify where the provider should redirect the user to in your application. */
|
|
45
|
+
redirectUrl?: string;
|
|
46
|
+
/** Overrides the connection specific properties from the Authress Identity Connection to pass to the identity provider */
|
|
47
|
+
connectionProperties?: Record<string, string>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface UserProfile {
|
|
51
|
+
/** List of Linked Identities for the user. */
|
|
52
|
+
linkedIdentities: Array<LinkedIdentity>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface LinkedIdentity {
|
|
56
|
+
/** The linked identity originating identity provider user information. */
|
|
57
|
+
connection: LinkedIdentityConnection;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface LinkedIdentityConnection {
|
|
61
|
+
/** The linked identity provider connection ID. */
|
|
62
|
+
connectionId: string;
|
|
63
|
+
/** The user's user ID from the linked identity provider. */
|
|
64
|
+
userId: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Options for getting a token including timeout configuration. */
|
|
68
|
+
export interface TokenParameters {
|
|
69
|
+
/** Timeout in milliseconds waiting for a user token. After this time an error will be thrown. Use -1 for no timeout. (Default: **5000**) */
|
|
70
|
+
timeoutInMillis?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Decoded user identity from the Authress-issued ID token. */
|
|
74
|
+
export interface UserIdentity {
|
|
75
|
+
/** The user's unique identifier. */
|
|
76
|
+
userId: string;
|
|
77
|
+
/** Token subject — same value as userId. */
|
|
78
|
+
sub: string;
|
|
79
|
+
/** Any additional claims present in the token. */
|
|
80
|
+
[key: string]: unknown;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** MFA device */
|
|
84
|
+
export interface Device {
|
|
85
|
+
/** Unique Device ID for the this user specified MFA device. */
|
|
86
|
+
deviceId: string;
|
|
87
|
+
/** User specified name for this device. */
|
|
88
|
+
name: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Token not available within the requested timeout. */
|
|
92
|
+
export class TokenTimeoutError extends Error {
|
|
93
|
+
readonly code = 'TokenTimeoutError' as const;
|
|
94
|
+
constructor() { super('Token not available within the requested timeout'); this.name = 'TokenTimeoutError'; }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The authentication flow was called with no pending request. */
|
|
98
|
+
export class NoAuthenticationRequestInProgressError extends Error {
|
|
99
|
+
readonly code = 'NoAuthenticationRequestInProgressError' as const;
|
|
100
|
+
constructor() { super('No authentication request is currently in progress'); this.name = 'NoAuthenticationRequestInProgressError'; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The authentication flow was called with a mismatched authenticationRequestId. */
|
|
104
|
+
export class AuthenticationRequestMismatchError extends Error {
|
|
105
|
+
readonly code = 'AuthenticationRequestMismatchError' as const;
|
|
106
|
+
constructor() { super('Authentication request ID does not match the pending request'); this.name = 'AuthenticationRequestMismatchError'; }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Union of all authentication flow errors. */
|
|
110
|
+
export type AuthFlowError = NoAuthenticationRequestInProgressError | AuthenticationRequestMismatchError;
|
|
111
|
+
|
|
112
|
+
/** The requested operation requires the user to be logged in. */
|
|
113
|
+
export class NotLoggedInError extends Error {
|
|
114
|
+
readonly code = 'NotLoggedInError' as const;
|
|
115
|
+
constructor() { super('The requested operation requires the user to be logged in'); this.name = 'NotLoggedInError'; }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The requested operation was called with missing or invalid connection parameters. */
|
|
119
|
+
export class InvalidConnectionError extends Error {
|
|
120
|
+
readonly code = 'InvalidConnectionError' as const;
|
|
121
|
+
constructor() { super('connectionId or tenantLookupIdentifier is required'); this.name = 'InvalidConnectionError'; }
|
|
122
|
+
}
|