@authrim/sveltekit 0.1.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 +191 -0
- package/README.md +531 -0
- package/dist/__tests__/client-events.test.d.ts +2 -0
- package/dist/__tests__/client-events.test.d.ts.map +1 -0
- package/dist/__tests__/client-events.test.js +225 -0
- package/dist/__tests__/providers.test.d.ts +2 -0
- package/dist/__tests__/providers.test.d.ts.map +1 -0
- package/dist/__tests__/providers.test.js +68 -0
- package/dist/__tests__/response.test.d.ts +2 -0
- package/dist/__tests__/response.test.d.ts.map +1 -0
- package/dist/__tests__/response.test.js +99 -0
- package/dist/__tests__/stores.test.d.ts +2 -0
- package/dist/__tests__/stores.test.d.ts.map +1 -0
- package/dist/__tests__/stores.test.js +91 -0
- package/dist/client.d.ts +25 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +411 -0
- package/dist/components/AuthProvider.svelte +56 -0
- package/dist/components/AuthProvider.svelte.d.ts +34 -0
- package/dist/components/AuthProvider.svelte.d.ts.map +1 -0
- package/dist/components/ProtectedRoute.svelte +71 -0
- package/dist/components/ProtectedRoute.svelte.d.ts +38 -0
- package/dist/components/ProtectedRoute.svelte.d.ts.map +1 -0
- package/dist/components/SignInButton.svelte +93 -0
- package/dist/components/SignInButton.svelte.d.ts +43 -0
- package/dist/components/SignInButton.svelte.d.ts.map +1 -0
- package/dist/components/SignOutButton.svelte +72 -0
- package/dist/components/SignOutButton.svelte.d.ts +40 -0
- package/dist/components/SignOutButton.svelte.d.ts.map +1 -0
- package/dist/components/UserProfile.svelte +71 -0
- package/dist/components/UserProfile.svelte.d.ts +51 -0
- package/dist/components/UserProfile.svelte.d.ts.map +1 -0
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +5 -0
- package/dist/direct-auth/ciba.d.ts +47 -0
- package/dist/direct-auth/ciba.d.ts.map +1 -0
- package/dist/direct-auth/ciba.js +77 -0
- package/dist/direct-auth/consent.d.ts +85 -0
- package/dist/direct-auth/consent.d.ts.map +1 -0
- package/dist/direct-auth/consent.js +57 -0
- package/dist/direct-auth/device-flow.d.ts +40 -0
- package/dist/direct-auth/device-flow.d.ts.map +1 -0
- package/dist/direct-auth/device-flow.js +45 -0
- package/dist/direct-auth/email-code.d.ts +48 -0
- package/dist/direct-auth/email-code.d.ts.map +1 -0
- package/dist/direct-auth/email-code.js +265 -0
- package/dist/direct-auth/index.d.ts +9 -0
- package/dist/direct-auth/index.d.ts.map +1 -0
- package/dist/direct-auth/index.js +8 -0
- package/dist/direct-auth/login-challenge.d.ts +41 -0
- package/dist/direct-auth/login-challenge.d.ts.map +1 -0
- package/dist/direct-auth/login-challenge.js +34 -0
- package/dist/direct-auth/passkey.d.ts +30 -0
- package/dist/direct-auth/passkey.d.ts.map +1 -0
- package/dist/direct-auth/passkey.js +392 -0
- package/dist/direct-auth/session.d.ts +48 -0
- package/dist/direct-auth/session.d.ts.map +1 -0
- package/dist/direct-auth/session.js +219 -0
- package/dist/direct-auth/social.d.ts +56 -0
- package/dist/direct-auth/social.d.ts.map +1 -0
- package/dist/direct-auth/social.js +484 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/providers/crypto.d.ts +13 -0
- package/dist/providers/crypto.d.ts.map +1 -0
- package/dist/providers/crypto.js +27 -0
- package/dist/providers/http.d.ts +30 -0
- package/dist/providers/http.d.ts.map +1 -0
- package/dist/providers/http.js +65 -0
- package/dist/providers/index.d.ts +4 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +3 -0
- package/dist/providers/storage.d.ts +21 -0
- package/dist/providers/storage.d.ts.map +1 -0
- package/dist/providers/storage.js +83 -0
- package/dist/server/handle.d.ts +46 -0
- package/dist/server/handle.d.ts.map +1 -0
- package/dist/server/handle.js +60 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +3 -0
- package/dist/server/load.d.ts +83 -0
- package/dist/server/load.d.ts.map +1 -0
- package/dist/server/load.js +86 -0
- package/dist/server/session.d.ts +44 -0
- package/dist/server/session.d.ts.map +1 -0
- package/dist/server/session.js +50 -0
- package/dist/stores/auth.d.ts +56 -0
- package/dist/stores/auth.d.ts.map +1 -0
- package/dist/stores/auth.js +64 -0
- package/dist/stores/index.d.ts +2 -0
- package/dist/stores/index.d.ts.map +1 -0
- package/dist/stores/index.js +1 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/ui/account/LinkAccountButton.svelte +133 -0
- package/dist/ui/account/LinkAccountButton.svelte.d.ts +37 -0
- package/dist/ui/account/LinkAccountButton.svelte.d.ts.map +1 -0
- package/dist/ui/account/LinkedAccountsList.svelte +233 -0
- package/dist/ui/account/LinkedAccountsList.svelte.d.ts +32 -0
- package/dist/ui/account/LinkedAccountsList.svelte.d.ts.map +1 -0
- package/dist/ui/account/UnlinkAccountButton.svelte +179 -0
- package/dist/ui/account/UnlinkAccountButton.svelte.d.ts +28 -0
- package/dist/ui/account/UnlinkAccountButton.svelte.d.ts.map +1 -0
- package/dist/ui/account/index.d.ts +7 -0
- package/dist/ui/account/index.d.ts.map +1 -0
- package/dist/ui/account/index.js +6 -0
- package/dist/ui/context.d.ts +17 -0
- package/dist/ui/context.d.ts.map +1 -0
- package/dist/ui/context.js +71 -0
- package/dist/ui/forms/CIBARequestCard.svelte +315 -0
- package/dist/ui/forms/CIBARequestCard.svelte.d.ts +50 -0
- package/dist/ui/forms/CIBARequestCard.svelte.d.ts.map +1 -0
- package/dist/ui/forms/ClientInfo.svelte +232 -0
- package/dist/ui/forms/ClientInfo.svelte.d.ts +35 -0
- package/dist/ui/forms/ClientInfo.svelte.d.ts.map +1 -0
- package/dist/ui/forms/ConsentScopesList.svelte +109 -0
- package/dist/ui/forms/ConsentScopesList.svelte.d.ts +30 -0
- package/dist/ui/forms/ConsentScopesList.svelte.d.ts.map +1 -0
- package/dist/ui/forms/EmailCodeForm.svelte +224 -0
- package/dist/ui/forms/EmailCodeForm.svelte.d.ts +39 -0
- package/dist/ui/forms/EmailCodeForm.svelte.d.ts.map +1 -0
- package/dist/ui/forms/OrgSelector.svelte +95 -0
- package/dist/ui/forms/OrgSelector.svelte.d.ts +37 -0
- package/dist/ui/forms/OrgSelector.svelte.d.ts.map +1 -0
- package/dist/ui/forms/PasskeyConditionalInput.svelte +173 -0
- package/dist/ui/forms/PasskeyConditionalInput.svelte.d.ts +36 -0
- package/dist/ui/forms/PasskeyConditionalInput.svelte.d.ts.map +1 -0
- package/dist/ui/forms/QRCodeDisplay.svelte +122 -0
- package/dist/ui/forms/QRCodeDisplay.svelte.d.ts +27 -0
- package/dist/ui/forms/QRCodeDisplay.svelte.d.ts.map +1 -0
- package/dist/ui/forms/SocialLoginButtons.svelte +209 -0
- package/dist/ui/forms/SocialLoginButtons.svelte.d.ts +33 -0
- package/dist/ui/forms/SocialLoginButtons.svelte.d.ts.map +1 -0
- package/dist/ui/forms/UserCodeInput.svelte +183 -0
- package/dist/ui/forms/UserCodeInput.svelte.d.ts +34 -0
- package/dist/ui/forms/UserCodeInput.svelte.d.ts.map +1 -0
- package/dist/ui/forms/index.d.ts +13 -0
- package/dist/ui/forms/index.d.ts.map +1 -0
- package/dist/ui/forms/index.js +12 -0
- package/dist/ui/helpers/AuthError.svelte +124 -0
- package/dist/ui/helpers/AuthError.svelte.d.ts +26 -0
- package/dist/ui/helpers/AuthError.svelte.d.ts.map +1 -0
- package/dist/ui/helpers/AuthLoading.svelte +83 -0
- package/dist/ui/helpers/AuthLoading.svelte.d.ts +25 -0
- package/dist/ui/helpers/AuthLoading.svelte.d.ts.map +1 -0
- package/dist/ui/helpers/OTPInput.svelte +214 -0
- package/dist/ui/helpers/OTPInput.svelte.d.ts +34 -0
- package/dist/ui/helpers/OTPInput.svelte.d.ts.map +1 -0
- package/dist/ui/helpers/ResendCodeButton.svelte +140 -0
- package/dist/ui/helpers/ResendCodeButton.svelte.d.ts +28 -0
- package/dist/ui/helpers/ResendCodeButton.svelte.d.ts.map +1 -0
- package/dist/ui/helpers/index.d.ts +8 -0
- package/dist/ui/helpers/index.d.ts.map +1 -0
- package/dist/ui/helpers/index.js +7 -0
- package/dist/ui/index.d.ts +43 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +48 -0
- package/dist/ui/passkey/PasskeyDeleteButton.svelte +177 -0
- package/dist/ui/passkey/PasskeyDeleteButton.svelte.d.ts +26 -0
- package/dist/ui/passkey/PasskeyDeleteButton.svelte.d.ts.map +1 -0
- package/dist/ui/passkey/PasskeyList.svelte +225 -0
- package/dist/ui/passkey/PasskeyList.svelte.d.ts +30 -0
- package/dist/ui/passkey/PasskeyList.svelte.d.ts.map +1 -0
- package/dist/ui/passkey/PasskeyRegisterButton.svelte +52 -0
- package/dist/ui/passkey/PasskeyRegisterButton.svelte.d.ts +38 -0
- package/dist/ui/passkey/PasskeyRegisterButton.svelte.d.ts.map +1 -0
- package/dist/ui/passkey/index.d.ts +7 -0
- package/dist/ui/passkey/index.d.ts.map +1 -0
- package/dist/ui/passkey/index.js +6 -0
- package/dist/ui/session/SessionExpiryIndicator.svelte +109 -0
- package/dist/ui/session/SessionExpiryIndicator.svelte.d.ts +23 -0
- package/dist/ui/session/SessionExpiryIndicator.svelte.d.ts.map +1 -0
- package/dist/ui/session/SessionList.svelte +231 -0
- package/dist/ui/session/SessionList.svelte.d.ts +31 -0
- package/dist/ui/session/SessionList.svelte.d.ts.map +1 -0
- package/dist/ui/session/SessionRevokeButton.svelte +72 -0
- package/dist/ui/session/SessionRevokeButton.svelte.d.ts +26 -0
- package/dist/ui/session/SessionRevokeButton.svelte.d.ts.map +1 -0
- package/dist/ui/session/index.d.ts +7 -0
- package/dist/ui/session/index.d.ts.map +1 -0
- package/dist/ui/session/index.js +6 -0
- package/dist/ui/shared/Alert.svelte +246 -0
- package/dist/ui/shared/Alert.svelte.d.ts +36 -0
- package/dist/ui/shared/Alert.svelte.d.ts.map +1 -0
- package/dist/ui/shared/Badge.svelte +100 -0
- package/dist/ui/shared/Badge.svelte.d.ts +35 -0
- package/dist/ui/shared/Badge.svelte.d.ts.map +1 -0
- package/dist/ui/shared/Button.svelte +213 -0
- package/dist/ui/shared/Button.svelte.d.ts +42 -0
- package/dist/ui/shared/Button.svelte.d.ts.map +1 -0
- package/dist/ui/shared/Card.svelte +85 -0
- package/dist/ui/shared/Card.svelte.d.ts +39 -0
- package/dist/ui/shared/Card.svelte.d.ts.map +1 -0
- package/dist/ui/shared/CountdownTimer.svelte +150 -0
- package/dist/ui/shared/CountdownTimer.svelte.d.ts +30 -0
- package/dist/ui/shared/CountdownTimer.svelte.d.ts.map +1 -0
- package/dist/ui/shared/Dialog.svelte +240 -0
- package/dist/ui/shared/Dialog.svelte.d.ts +39 -0
- package/dist/ui/shared/Dialog.svelte.d.ts.map +1 -0
- package/dist/ui/shared/Input.svelte +192 -0
- package/dist/ui/shared/Input.svelte.d.ts +42 -0
- package/dist/ui/shared/Input.svelte.d.ts.map +1 -0
- package/dist/ui/shared/LanguageSwitcher.svelte +99 -0
- package/dist/ui/shared/LanguageSwitcher.svelte.d.ts +31 -0
- package/dist/ui/shared/LanguageSwitcher.svelte.d.ts.map +1 -0
- package/dist/ui/shared/Spinner.svelte +75 -0
- package/dist/ui/shared/Spinner.svelte.d.ts +24 -0
- package/dist/ui/shared/Spinner.svelte.d.ts.map +1 -0
- package/dist/ui/shared/index.d.ts +13 -0
- package/dist/ui/shared/index.d.ts.map +1 -0
- package/dist/ui/shared/index.js +12 -0
- package/dist/ui/styles/base.css +168 -0
- package/dist/ui/styles/theme.css +279 -0
- package/dist/ui/templates/AccountSettingsTemplate.svelte +205 -0
- package/dist/ui/templates/AccountSettingsTemplate.svelte.d.ts +49 -0
- package/dist/ui/templates/AccountSettingsTemplate.svelte.d.ts.map +1 -0
- package/dist/ui/templates/CIBATemplate.svelte +227 -0
- package/dist/ui/templates/CIBATemplate.svelte.d.ts +45 -0
- package/dist/ui/templates/CIBATemplate.svelte.d.ts.map +1 -0
- package/dist/ui/templates/ConsentTemplate.svelte +549 -0
- package/dist/ui/templates/ConsentTemplate.svelte.d.ts +76 -0
- package/dist/ui/templates/ConsentTemplate.svelte.d.ts.map +1 -0
- package/dist/ui/templates/DeviceFlowTemplate.svelte +228 -0
- package/dist/ui/templates/DeviceFlowTemplate.svelte.d.ts +47 -0
- package/dist/ui/templates/DeviceFlowTemplate.svelte.d.ts.map +1 -0
- package/dist/ui/templates/LoginTemplate.svelte +234 -0
- package/dist/ui/templates/LoginTemplate.svelte.d.ts +49 -0
- package/dist/ui/templates/LoginTemplate.svelte.d.ts.map +1 -0
- package/dist/ui/templates/ReauthTemplate.svelte +269 -0
- package/dist/ui/templates/ReauthTemplate.svelte.d.ts +54 -0
- package/dist/ui/templates/ReauthTemplate.svelte.d.ts.map +1 -0
- package/dist/ui/templates/SignUpTemplate.svelte +345 -0
- package/dist/ui/templates/SignUpTemplate.svelte.d.ts +53 -0
- package/dist/ui/templates/SignUpTemplate.svelte.d.ts.map +1 -0
- package/dist/ui/templates/index.d.ts +14 -0
- package/dist/ui/templates/index.d.ts.map +1 -0
- package/dist/ui/templates/index.js +13 -0
- package/dist/ui/types.d.ts +151 -0
- package/dist/ui/types.d.ts.map +1 -0
- package/dist/ui/types.js +4 -0
- package/dist/utils/context.d.ts +12 -0
- package/dist/utils/context.d.ts.map +1 -0
- package/dist/utils/context.js +26 -0
- package/dist/utils/error-mapping.d.ts +29 -0
- package/dist/utils/error-mapping.d.ts.map +1 -0
- package/dist/utils/error-mapping.js +38 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/response.d.ts +21 -0
- package/dist/utils/response.d.ts.map +1 -0
- package/dist/utils/response.js +84 -0
- package/dist/utils/sensitive-data.d.ts +9 -0
- package/dist/utils/sensitive-data.d.ts.map +1 -0
- package/dist/utils/sensitive-data.js +56 -0
- package/dist/utils/ssr.d.ts +38 -0
- package/dist/utils/ssr.d.ts.map +1 -0
- package/dist/utils/ssr.js +73 -0
- package/dist/utils/webauthn-converters.d.ts +9 -0
- package/dist/utils/webauthn-converters.d.ts.map +1 -0
- package/dist/utils/webauthn-converters.js +75 -0
- package/package.json +111 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passkey Authentication (WebAuthn)
|
|
3
|
+
*/
|
|
4
|
+
import { AuthrimError, PKCEHelper, } from '@authrim/core';
|
|
5
|
+
import { getAuthrimCode, mapSeverity } from '../utils/error-mapping.js';
|
|
6
|
+
import { convertToPublicKeyCredentialRequestOptions, convertToPublicKeyCredentialCreationOptions, assertionResponseToJSON, attestationResponseToJSON, } from '../utils/webauthn-converters.js';
|
|
7
|
+
const ENDPOINTS = {
|
|
8
|
+
PASSKEY_LOGIN_START: '/api/v1/auth/direct/passkey/login/start',
|
|
9
|
+
PASSKEY_LOGIN_FINISH: '/api/v1/auth/direct/passkey/login/finish',
|
|
10
|
+
PASSKEY_SIGNUP_START: '/api/v1/auth/direct/passkey/signup/start',
|
|
11
|
+
PASSKEY_SIGNUP_FINISH: '/api/v1/auth/direct/passkey/signup/finish',
|
|
12
|
+
PASSKEY_REGISTER_START: '/api/v1/auth/direct/passkey/register/start',
|
|
13
|
+
PASSKEY_REGISTER_FINISH: '/api/v1/auth/direct/passkey/register/finish',
|
|
14
|
+
};
|
|
15
|
+
export class PasskeyAuthImpl {
|
|
16
|
+
issuer;
|
|
17
|
+
clientId;
|
|
18
|
+
http;
|
|
19
|
+
pkce;
|
|
20
|
+
exchangeToken;
|
|
21
|
+
conditionalAbortController = null;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.issuer = options.issuer;
|
|
24
|
+
this.clientId = options.clientId;
|
|
25
|
+
this.http = options.http;
|
|
26
|
+
this.pkce = new PKCEHelper(options.crypto);
|
|
27
|
+
this.exchangeToken = options.exchangeToken;
|
|
28
|
+
}
|
|
29
|
+
isSupported() {
|
|
30
|
+
return (typeof window !== 'undefined' &&
|
|
31
|
+
typeof window.PublicKeyCredential !== 'undefined' &&
|
|
32
|
+
typeof navigator.credentials !== 'undefined');
|
|
33
|
+
}
|
|
34
|
+
async isConditionalUIAvailable() {
|
|
35
|
+
if (!this.isSupported())
|
|
36
|
+
return false;
|
|
37
|
+
try {
|
|
38
|
+
if (typeof PublicKeyCredential.isConditionalMediationAvailable === 'function') {
|
|
39
|
+
return await PublicKeyCredential.isConditionalMediationAvailable();
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async login(options) {
|
|
48
|
+
if (!this.isSupported()) {
|
|
49
|
+
return {
|
|
50
|
+
success: false,
|
|
51
|
+
error: {
|
|
52
|
+
error: 'passkey_not_supported',
|
|
53
|
+
error_description: 'WebAuthn is not supported in this browser',
|
|
54
|
+
code: 'AR003003',
|
|
55
|
+
meta: { retryable: false, severity: 'warn' },
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
let codeVerifier = '';
|
|
60
|
+
try {
|
|
61
|
+
const pkce = await this.pkce.generatePKCE();
|
|
62
|
+
codeVerifier = pkce.codeVerifier;
|
|
63
|
+
const codeChallenge = pkce.codeChallenge;
|
|
64
|
+
const startRequest = {
|
|
65
|
+
client_id: this.clientId,
|
|
66
|
+
code_challenge: codeChallenge,
|
|
67
|
+
code_challenge_method: 'S256',
|
|
68
|
+
};
|
|
69
|
+
const startResponse = await this.http.fetch(`${this.issuer}${ENDPOINTS.PASSKEY_LOGIN_START}`, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: { 'Content-Type': 'application/json' },
|
|
72
|
+
body: JSON.stringify(startRequest),
|
|
73
|
+
});
|
|
74
|
+
if (!startResponse.ok || !startResponse.data) {
|
|
75
|
+
throw new AuthrimError('network_error', 'Failed to start passkey login');
|
|
76
|
+
}
|
|
77
|
+
const { challenge_id, options: webauthnOptions } = startResponse.data;
|
|
78
|
+
const publicKeyOptions = convertToPublicKeyCredentialRequestOptions(webauthnOptions);
|
|
79
|
+
const abortController = new AbortController();
|
|
80
|
+
const abortHandler = () => abortController.abort();
|
|
81
|
+
if (options?.signal) {
|
|
82
|
+
options.signal.addEventListener('abort', abortHandler, { once: true });
|
|
83
|
+
}
|
|
84
|
+
if (options?.conditional || options?.mediation === 'conditional') {
|
|
85
|
+
// Cancel any existing conditional UI request before starting a new one
|
|
86
|
+
// This prevents leaking the old AbortController
|
|
87
|
+
if (this.conditionalAbortController) {
|
|
88
|
+
this.conditionalAbortController.abort();
|
|
89
|
+
}
|
|
90
|
+
this.conditionalAbortController = abortController;
|
|
91
|
+
}
|
|
92
|
+
let credential;
|
|
93
|
+
try {
|
|
94
|
+
credential = (await navigator.credentials.get({
|
|
95
|
+
publicKey: publicKeyOptions,
|
|
96
|
+
mediation: options?.mediation || (options?.conditional ? 'conditional' : 'optional'),
|
|
97
|
+
signal: abortController.signal,
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error instanceof Error) {
|
|
102
|
+
if (error.name === 'AbortError' || error.name === 'NotAllowedError') {
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
error: {
|
|
106
|
+
error: 'passkey_cancelled',
|
|
107
|
+
error_description: error.name === 'AbortError'
|
|
108
|
+
? 'Passkey authentication was cancelled'
|
|
109
|
+
: 'User denied the passkey request',
|
|
110
|
+
code: 'AR003004',
|
|
111
|
+
meta: { retryable: false, severity: 'warn' },
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
// Cleanup: remove abort handler and clear conditional controller
|
|
120
|
+
if (options?.signal) {
|
|
121
|
+
options.signal.removeEventListener('abort', abortHandler);
|
|
122
|
+
}
|
|
123
|
+
if (options?.conditional) {
|
|
124
|
+
this.conditionalAbortController = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!credential) {
|
|
128
|
+
return {
|
|
129
|
+
success: false,
|
|
130
|
+
error: {
|
|
131
|
+
error: 'passkey_not_found',
|
|
132
|
+
error_description: 'No passkey credential found',
|
|
133
|
+
code: 'AR003001',
|
|
134
|
+
meta: { retryable: false, severity: 'warn' },
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const credentialJSON = assertionResponseToJSON(credential);
|
|
139
|
+
const finishRequest = {
|
|
140
|
+
challenge_id,
|
|
141
|
+
credential: credentialJSON,
|
|
142
|
+
code_verifier: codeVerifier,
|
|
143
|
+
};
|
|
144
|
+
const finishResponse = await this.http.fetch(`${this.issuer}${ENDPOINTS.PASSKEY_LOGIN_FINISH}`, {
|
|
145
|
+
method: 'POST',
|
|
146
|
+
headers: { 'Content-Type': 'application/json' },
|
|
147
|
+
body: JSON.stringify(finishRequest),
|
|
148
|
+
});
|
|
149
|
+
if (!finishResponse.ok || !finishResponse.data) {
|
|
150
|
+
throw new AuthrimError('passkey_verification_failed', 'Failed to verify passkey');
|
|
151
|
+
}
|
|
152
|
+
const { auth_code } = finishResponse.data;
|
|
153
|
+
const result = await this.exchangeToken(auth_code, codeVerifier);
|
|
154
|
+
return {
|
|
155
|
+
success: true,
|
|
156
|
+
session: result.session,
|
|
157
|
+
user: result.user,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (error instanceof AuthrimError) {
|
|
162
|
+
return {
|
|
163
|
+
success: false,
|
|
164
|
+
error: {
|
|
165
|
+
error: error.code,
|
|
166
|
+
error_description: error.message,
|
|
167
|
+
code: getAuthrimCode(error.code, 'AR003000'),
|
|
168
|
+
meta: {
|
|
169
|
+
retryable: error.meta.retryable,
|
|
170
|
+
severity: mapSeverity(error.meta.severity),
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
error: {
|
|
178
|
+
error: 'passkey_verification_failed',
|
|
179
|
+
error_description: error instanceof Error ? error.message : 'Unknown error',
|
|
180
|
+
code: 'AR003002',
|
|
181
|
+
meta: { retryable: false, severity: 'error' },
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
// Ensure codeVerifier is cleared regardless of success or failure
|
|
187
|
+
codeVerifier = '';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async signUp(options) {
|
|
191
|
+
if (!this.isSupported()) {
|
|
192
|
+
return {
|
|
193
|
+
success: false,
|
|
194
|
+
error: {
|
|
195
|
+
error: 'passkey_not_supported',
|
|
196
|
+
error_description: 'WebAuthn is not supported in this browser',
|
|
197
|
+
code: 'AR003003',
|
|
198
|
+
meta: { retryable: false, severity: 'warn' },
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
let codeVerifier = '';
|
|
203
|
+
try {
|
|
204
|
+
const pkce = await this.pkce.generatePKCE();
|
|
205
|
+
codeVerifier = pkce.codeVerifier;
|
|
206
|
+
const codeChallenge = pkce.codeChallenge;
|
|
207
|
+
const startRequest = {
|
|
208
|
+
client_id: this.clientId,
|
|
209
|
+
email: options.email,
|
|
210
|
+
display_name: options.displayName,
|
|
211
|
+
code_challenge: codeChallenge,
|
|
212
|
+
code_challenge_method: 'S256',
|
|
213
|
+
authenticator_type: options.authenticatorType,
|
|
214
|
+
resident_key: options.residentKey,
|
|
215
|
+
user_verification: options.userVerification,
|
|
216
|
+
};
|
|
217
|
+
const startResponse = await this.http.fetch(`${this.issuer}${ENDPOINTS.PASSKEY_SIGNUP_START}`, {
|
|
218
|
+
method: 'POST',
|
|
219
|
+
headers: { 'Content-Type': 'application/json' },
|
|
220
|
+
body: JSON.stringify(startRequest),
|
|
221
|
+
});
|
|
222
|
+
if (!startResponse.ok || !startResponse.data) {
|
|
223
|
+
throw new AuthrimError('network_error', 'Failed to start passkey signup');
|
|
224
|
+
}
|
|
225
|
+
const { challenge_id, options: webauthnOptions } = startResponse.data;
|
|
226
|
+
const publicKeyOptions = convertToPublicKeyCredentialCreationOptions(webauthnOptions);
|
|
227
|
+
const abortController = new AbortController();
|
|
228
|
+
const abortHandler = () => abortController.abort();
|
|
229
|
+
if (options.signal) {
|
|
230
|
+
options.signal.addEventListener('abort', abortHandler, { once: true });
|
|
231
|
+
}
|
|
232
|
+
let credential;
|
|
233
|
+
try {
|
|
234
|
+
credential = (await navigator.credentials.create({
|
|
235
|
+
publicKey: publicKeyOptions,
|
|
236
|
+
signal: abortController.signal,
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
if (error instanceof Error) {
|
|
241
|
+
if (error.name === 'AbortError' || error.name === 'NotAllowedError') {
|
|
242
|
+
return {
|
|
243
|
+
success: false,
|
|
244
|
+
error: {
|
|
245
|
+
error: 'passkey_cancelled',
|
|
246
|
+
error_description: 'Passkey registration was cancelled',
|
|
247
|
+
code: 'AR003004',
|
|
248
|
+
meta: { retryable: false, severity: 'warn' },
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
// Cleanup: remove abort handler
|
|
257
|
+
if (options.signal) {
|
|
258
|
+
options.signal.removeEventListener('abort', abortHandler);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (!credential) {
|
|
262
|
+
return {
|
|
263
|
+
success: false,
|
|
264
|
+
error: {
|
|
265
|
+
error: 'passkey_invalid_credential',
|
|
266
|
+
error_description: 'Failed to create passkey credential',
|
|
267
|
+
code: 'AR003005',
|
|
268
|
+
meta: { retryable: false, severity: 'error' },
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const credentialJSON = attestationResponseToJSON(credential);
|
|
273
|
+
const finishRequest = {
|
|
274
|
+
challenge_id,
|
|
275
|
+
credential: credentialJSON,
|
|
276
|
+
code_verifier: codeVerifier,
|
|
277
|
+
};
|
|
278
|
+
const finishResponse = await this.http.fetch(`${this.issuer}${ENDPOINTS.PASSKEY_SIGNUP_FINISH}`, {
|
|
279
|
+
method: 'POST',
|
|
280
|
+
headers: { 'Content-Type': 'application/json' },
|
|
281
|
+
body: JSON.stringify(finishRequest),
|
|
282
|
+
});
|
|
283
|
+
if (!finishResponse.ok || !finishResponse.data) {
|
|
284
|
+
throw new AuthrimError('passkey_verification_failed', 'Failed to register passkey');
|
|
285
|
+
}
|
|
286
|
+
const { auth_code } = finishResponse.data;
|
|
287
|
+
const result = await this.exchangeToken(auth_code, codeVerifier);
|
|
288
|
+
return {
|
|
289
|
+
success: true,
|
|
290
|
+
session: result.session,
|
|
291
|
+
user: result.user,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
if (error instanceof AuthrimError) {
|
|
296
|
+
return {
|
|
297
|
+
success: false,
|
|
298
|
+
error: {
|
|
299
|
+
error: error.code,
|
|
300
|
+
error_description: error.message,
|
|
301
|
+
code: getAuthrimCode(error.code, 'AR003000'),
|
|
302
|
+
meta: {
|
|
303
|
+
retryable: error.meta.retryable,
|
|
304
|
+
severity: mapSeverity(error.meta.severity),
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
success: false,
|
|
311
|
+
error: {
|
|
312
|
+
error: 'passkey_verification_failed',
|
|
313
|
+
error_description: error instanceof Error ? error.message : 'Unknown error',
|
|
314
|
+
code: 'AR003002',
|
|
315
|
+
meta: { retryable: false, severity: 'error' },
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
finally {
|
|
320
|
+
// Ensure codeVerifier is cleared regardless of success or failure
|
|
321
|
+
codeVerifier = '';
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async register(options) {
|
|
325
|
+
if (!this.isSupported()) {
|
|
326
|
+
throw new AuthrimError('passkey_not_supported', 'WebAuthn is not supported in this browser');
|
|
327
|
+
}
|
|
328
|
+
const startResponse = await this.http.fetch(`${this.issuer}${ENDPOINTS.PASSKEY_REGISTER_START}`, {
|
|
329
|
+
method: 'POST',
|
|
330
|
+
headers: { 'Content-Type': 'application/json' },
|
|
331
|
+
body: JSON.stringify({
|
|
332
|
+
client_id: this.clientId,
|
|
333
|
+
display_name: options?.displayName,
|
|
334
|
+
authenticator_type: options?.authenticatorType,
|
|
335
|
+
resident_key: options?.residentKey,
|
|
336
|
+
user_verification: options?.userVerification,
|
|
337
|
+
}),
|
|
338
|
+
});
|
|
339
|
+
if (!startResponse.ok || !startResponse.data) {
|
|
340
|
+
throw new AuthrimError('network_error', 'Failed to start passkey registration');
|
|
341
|
+
}
|
|
342
|
+
const { challenge_id, options: webauthnOptions } = startResponse.data;
|
|
343
|
+
const publicKeyOptions = convertToPublicKeyCredentialCreationOptions(webauthnOptions);
|
|
344
|
+
const abortController = new AbortController();
|
|
345
|
+
const abortHandler = () => abortController.abort();
|
|
346
|
+
if (options?.signal) {
|
|
347
|
+
options.signal.addEventListener('abort', abortHandler, { once: true });
|
|
348
|
+
}
|
|
349
|
+
let credential;
|
|
350
|
+
try {
|
|
351
|
+
credential = (await navigator.credentials.create({
|
|
352
|
+
publicKey: publicKeyOptions,
|
|
353
|
+
signal: abortController.signal,
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
// Cleanup: remove abort handler
|
|
358
|
+
if (options?.signal) {
|
|
359
|
+
options.signal.removeEventListener('abort', abortHandler);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (!credential) {
|
|
363
|
+
throw new AuthrimError('passkey_invalid_credential', 'Failed to create passkey credential');
|
|
364
|
+
}
|
|
365
|
+
const credentialJSON = attestationResponseToJSON(credential);
|
|
366
|
+
const finishResponse = await this.http.fetch(`${this.issuer}${ENDPOINTS.PASSKEY_REGISTER_FINISH}`, {
|
|
367
|
+
method: 'POST',
|
|
368
|
+
headers: { 'Content-Type': 'application/json' },
|
|
369
|
+
body: JSON.stringify({
|
|
370
|
+
challenge_id,
|
|
371
|
+
credential: credentialJSON,
|
|
372
|
+
}),
|
|
373
|
+
});
|
|
374
|
+
if (!finishResponse.ok || !finishResponse.data) {
|
|
375
|
+
throw new AuthrimError('passkey_verification_failed', 'Failed to register passkey');
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
credentialId: finishResponse.data.credential_id,
|
|
379
|
+
publicKey: finishResponse.data.public_key,
|
|
380
|
+
authenticatorType: finishResponse.data.authenticator_type,
|
|
381
|
+
transports: finishResponse.data.transports,
|
|
382
|
+
createdAt: finishResponse.data.created_at,
|
|
383
|
+
displayName: options?.displayName,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
cancelConditionalUI() {
|
|
387
|
+
if (this.conditionalAbortController) {
|
|
388
|
+
this.conditionalAbortController.abort();
|
|
389
|
+
this.conditionalAbortController = null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Management for Direct Auth
|
|
3
|
+
*/
|
|
4
|
+
import { type SessionAuth, type Session, type DirectAuthLogoutOptions, type User } from '@authrim/core';
|
|
5
|
+
import type { BrowserHttpClient } from '../providers/http.js';
|
|
6
|
+
export interface SessionManagerOptions {
|
|
7
|
+
issuer: string;
|
|
8
|
+
clientId: string;
|
|
9
|
+
http: BrowserHttpClient;
|
|
10
|
+
}
|
|
11
|
+
export declare class SessionAuthImpl implements SessionAuth {
|
|
12
|
+
private readonly issuer;
|
|
13
|
+
private readonly clientId;
|
|
14
|
+
private readonly http;
|
|
15
|
+
private readonly storageKey;
|
|
16
|
+
private cachedSession;
|
|
17
|
+
private cachedUser;
|
|
18
|
+
private sessionCacheExpiry;
|
|
19
|
+
private readonly SESSION_CACHE_TTL;
|
|
20
|
+
constructor(options: SessionManagerOptions);
|
|
21
|
+
private getStoredToken;
|
|
22
|
+
private storeToken;
|
|
23
|
+
private removeStoredToken;
|
|
24
|
+
get(): Promise<Session | null>;
|
|
25
|
+
getUser(): Promise<User | null>;
|
|
26
|
+
validate(): Promise<boolean>;
|
|
27
|
+
logout(options?: DirectAuthLogoutOptions): Promise<void>;
|
|
28
|
+
exchangeToken(authCode: string, codeVerifier: string, requestRefreshToken?: boolean): Promise<{
|
|
29
|
+
session?: Session;
|
|
30
|
+
user?: User;
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Revalidate the current session by clearing cache and fetching fresh data.
|
|
34
|
+
*
|
|
35
|
+
* Note: This does NOT perform OAuth token refresh (grant_type: 'refresh_token').
|
|
36
|
+
* Token refresh is handled automatically by the server when the access token
|
|
37
|
+
* is still valid but needs renewal.
|
|
38
|
+
*
|
|
39
|
+
* For explicit token refresh, use the refresh token flow through the server.
|
|
40
|
+
*
|
|
41
|
+
* @returns Fresh session data or null if not authenticated
|
|
42
|
+
*/
|
|
43
|
+
refresh(): Promise<Session | null>;
|
|
44
|
+
isAuthenticated(): Promise<boolean>;
|
|
45
|
+
clearCache(): void;
|
|
46
|
+
getToken(): string | null;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/lib/direct-auth/session.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,uBAAuB,EAG5B,KAAK,IAAI,EACV,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAU9D,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,iBAAiB,CAAC;CACzB;AAaD,qBAAa,eAAgB,YAAW,WAAW;IACjD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAoB;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,aAAa,CAAwB;IAC7C,OAAO,CAAC,UAAU,CAAqB;IACvC,OAAO,CAAC,kBAAkB,CAAa;IACvC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;gBAE/B,OAAO,EAAE,qBAAqB;IAO1C,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,iBAAiB;IASnB,GAAG,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAyC9B,OAAO,IAAI,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAS/B,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IAY5B,MAAM,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;IAqCxD,aAAa,CACjB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,mBAAmB,CAAC,EAAE,OAAO,GAC5B,OAAO,CAAC;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,IAAI,CAAA;KAAE,CAAC;IAgE9C;;;;;;;;;;OAUG;IACG,OAAO,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAKlC,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC;IAUzC,UAAU,IAAI,IAAI;IAMlB,QAAQ,IAAI,MAAM,GAAG,IAAI;CAG1B"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Management for Direct Auth
|
|
3
|
+
*/
|
|
4
|
+
import { AuthrimError, } from '@authrim/core';
|
|
5
|
+
const ENDPOINTS = {
|
|
6
|
+
TOKEN: '/api/v1/auth/direct/token',
|
|
7
|
+
SESSION: '/api/v1/auth/direct/session',
|
|
8
|
+
LOGOUT: '/api/v1/auth/direct/logout',
|
|
9
|
+
};
|
|
10
|
+
const STORAGE_KEY_PREFIX = 'authrim_session';
|
|
11
|
+
function getStorageKey(issuer, clientId) {
|
|
12
|
+
const key = `${issuer}:${clientId}`;
|
|
13
|
+
let hash = 0;
|
|
14
|
+
for (let i = 0; i < key.length; i++) {
|
|
15
|
+
const char = key.charCodeAt(i);
|
|
16
|
+
hash = (hash << 5) - hash + char;
|
|
17
|
+
hash = hash & hash;
|
|
18
|
+
}
|
|
19
|
+
return `${STORAGE_KEY_PREFIX}_${Math.abs(hash).toString(36)}`;
|
|
20
|
+
}
|
|
21
|
+
export class SessionAuthImpl {
|
|
22
|
+
issuer;
|
|
23
|
+
clientId;
|
|
24
|
+
http;
|
|
25
|
+
storageKey;
|
|
26
|
+
cachedSession = null;
|
|
27
|
+
cachedUser = null;
|
|
28
|
+
sessionCacheExpiry = 0;
|
|
29
|
+
SESSION_CACHE_TTL = 60000;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.issuer = options.issuer;
|
|
32
|
+
this.clientId = options.clientId;
|
|
33
|
+
this.http = options.http;
|
|
34
|
+
this.storageKey = getStorageKey(options.issuer, options.clientId);
|
|
35
|
+
}
|
|
36
|
+
getStoredToken() {
|
|
37
|
+
if (typeof localStorage === 'undefined')
|
|
38
|
+
return null;
|
|
39
|
+
try {
|
|
40
|
+
return localStorage.getItem(this.storageKey);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
storeToken(token) {
|
|
47
|
+
if (typeof localStorage === 'undefined')
|
|
48
|
+
return;
|
|
49
|
+
try {
|
|
50
|
+
localStorage.setItem(this.storageKey, token);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
console.warn('[Authrim] Failed to store token in localStorage');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
removeStoredToken() {
|
|
57
|
+
if (typeof localStorage === 'undefined')
|
|
58
|
+
return;
|
|
59
|
+
try {
|
|
60
|
+
localStorage.removeItem(this.storageKey);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// localStorage not available
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async get() {
|
|
67
|
+
if (this.cachedSession && Date.now() < this.sessionCacheExpiry) {
|
|
68
|
+
return this.cachedSession;
|
|
69
|
+
}
|
|
70
|
+
const token = this.getStoredToken();
|
|
71
|
+
if (!token) {
|
|
72
|
+
this.clearCache();
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const response = await this.http.fetch(`${this.issuer}${ENDPOINTS.SESSION}`, {
|
|
77
|
+
method: 'GET',
|
|
78
|
+
headers: {
|
|
79
|
+
Authorization: `Bearer ${token}`,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
if (!response.ok || !response.data) {
|
|
83
|
+
if (response.status === 401) {
|
|
84
|
+
this.removeStoredToken();
|
|
85
|
+
}
|
|
86
|
+
this.clearCache();
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
this.cachedSession = response.data.session;
|
|
90
|
+
this.cachedUser = response.data.user;
|
|
91
|
+
this.sessionCacheExpiry = Date.now() + this.SESSION_CACHE_TTL;
|
|
92
|
+
return response.data.session;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
this.clearCache();
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async getUser() {
|
|
100
|
+
if (this.cachedUser && Date.now() < this.sessionCacheExpiry) {
|
|
101
|
+
return this.cachedUser;
|
|
102
|
+
}
|
|
103
|
+
await this.get();
|
|
104
|
+
return this.cachedUser;
|
|
105
|
+
}
|
|
106
|
+
async validate() {
|
|
107
|
+
try {
|
|
108
|
+
const session = await this.get();
|
|
109
|
+
if (!session)
|
|
110
|
+
return false;
|
|
111
|
+
const expiresAt = new Date(session.expiresAt).getTime();
|
|
112
|
+
return Date.now() < expiresAt;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async logout(options) {
|
|
119
|
+
const token = this.getStoredToken();
|
|
120
|
+
if (token) {
|
|
121
|
+
try {
|
|
122
|
+
const requestBody = {
|
|
123
|
+
client_id: this.clientId,
|
|
124
|
+
};
|
|
125
|
+
if (options?.revokeTokens !== undefined) {
|
|
126
|
+
requestBody.revoke_tokens = options.revokeTokens;
|
|
127
|
+
}
|
|
128
|
+
await this.http.fetch(`${this.issuer}${ENDPOINTS.LOGOUT}`, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: {
|
|
131
|
+
'Content-Type': 'application/json',
|
|
132
|
+
Authorization: `Bearer ${token}`,
|
|
133
|
+
},
|
|
134
|
+
body: JSON.stringify(requestBody),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
console.warn('Logout request failed:', error);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
this.removeStoredToken();
|
|
142
|
+
this.clearCache();
|
|
143
|
+
if (options?.redirectUri && typeof window !== 'undefined') {
|
|
144
|
+
window.location.href = options.redirectUri;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async exchangeToken(authCode, codeVerifier, requestRefreshToken) {
|
|
148
|
+
const request = {
|
|
149
|
+
grant_type: 'authorization_code',
|
|
150
|
+
code: authCode,
|
|
151
|
+
client_id: this.clientId,
|
|
152
|
+
code_verifier: codeVerifier,
|
|
153
|
+
request_refresh_token: requestRefreshToken,
|
|
154
|
+
};
|
|
155
|
+
const response = await this.http.fetch(`${this.issuer}${ENDPOINTS.TOKEN}`, {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: { 'Content-Type': 'application/json' },
|
|
158
|
+
body: JSON.stringify(request),
|
|
159
|
+
});
|
|
160
|
+
if (!response.ok || !response.data) {
|
|
161
|
+
if (response.status === 400) {
|
|
162
|
+
const errorData = response.data;
|
|
163
|
+
if (errorData?.error === 'invalid_grant') {
|
|
164
|
+
throw new AuthrimError('auth_code_invalid', errorData.error_description || 'Invalid authorization code');
|
|
165
|
+
}
|
|
166
|
+
if (errorData?.error === 'expired_token') {
|
|
167
|
+
throw new AuthrimError('auth_code_expired', errorData.error_description || 'Authorization code has expired');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
throw new AuthrimError('token_error', 'Failed to exchange authorization code for tokens');
|
|
171
|
+
}
|
|
172
|
+
const tokenResponse = response.data;
|
|
173
|
+
if (tokenResponse.access_token) {
|
|
174
|
+
this.storeToken(tokenResponse.access_token);
|
|
175
|
+
}
|
|
176
|
+
if (tokenResponse.session) {
|
|
177
|
+
this.cachedSession = tokenResponse.session;
|
|
178
|
+
this.sessionCacheExpiry = Date.now() + this.SESSION_CACHE_TTL;
|
|
179
|
+
}
|
|
180
|
+
if (tokenResponse.user) {
|
|
181
|
+
this.cachedUser = tokenResponse.user;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
session: tokenResponse.session,
|
|
185
|
+
user: tokenResponse.user,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Revalidate the current session by clearing cache and fetching fresh data.
|
|
190
|
+
*
|
|
191
|
+
* Note: This does NOT perform OAuth token refresh (grant_type: 'refresh_token').
|
|
192
|
+
* Token refresh is handled automatically by the server when the access token
|
|
193
|
+
* is still valid but needs renewal.
|
|
194
|
+
*
|
|
195
|
+
* For explicit token refresh, use the refresh token flow through the server.
|
|
196
|
+
*
|
|
197
|
+
* @returns Fresh session data or null if not authenticated
|
|
198
|
+
*/
|
|
199
|
+
async refresh() {
|
|
200
|
+
this.clearCache();
|
|
201
|
+
return this.get();
|
|
202
|
+
}
|
|
203
|
+
async isAuthenticated() {
|
|
204
|
+
const token = this.getStoredToken();
|
|
205
|
+
if (!token) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
const session = await this.get();
|
|
209
|
+
return session !== null;
|
|
210
|
+
}
|
|
211
|
+
clearCache() {
|
|
212
|
+
this.cachedSession = null;
|
|
213
|
+
this.cachedUser = null;
|
|
214
|
+
this.sessionCacheExpiry = 0;
|
|
215
|
+
}
|
|
216
|
+
getToken() {
|
|
217
|
+
return this.getStoredToken();
|
|
218
|
+
}
|
|
219
|
+
}
|