Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2184231
Refactor Passkey logic
brionmario Oct 16, 2025
4b053c6
chore: move components around
brionmario Oct 16, 2025
79dcf16
chore: move current sign-in to non-component driven scope
brionmario Oct 20, 2025
d17e082
feat: Add support for Asgardeo V2 embedded sign-in flow
brionmario Oct 21, 2025
42b9cd0
feat: Enhance Asgardeo V2 embedded sign-in flow with session data han…
brionmario Oct 21, 2025
8b2a006
feat: Add BaseSignIn styles and refactor component-driven imports
brionmario Oct 22, 2025
797fb44
chore(react): add SMS OTP button and update translations for new sign…
brionmario Oct 22, 2025
a8ccd6e
feat: Enhance SignIn component with render props for custom UI and im…
brionmario Oct 22, 2025
9b44ec1
chore(javascript): make `clientId` optional in DefaultAuthClientConfi…
brionmario Oct 23, 2025
9990152
feat: expose `clearSession` method across various clients and update …
brionmario Oct 23, 2025
92d77f1
fix(react): enhance SignIn component to retrieve applicationId from U…
brionmario Oct 23, 2025
c3a4693
fix(react): add error handling for user and organization retrieval in…
brionmario Oct 23, 2025
36a475a
fix: implement getDisplayName utility for user profile display name m…
brionmario Oct 23, 2025
f7ad555
chore: add zindex to user dropdown
brionmario Oct 23, 2025
5a5707e
chore: temp avoid scim and org calls for Asgardeo V2 mode
brionmario Oct 23, 2025
2bb321b
chore: temp avoid branding calls for Asgardeo V2 mode
brionmario Oct 23, 2025
b411539
chore: add changeset 🦋
brionmario Oct 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/khaki-ends-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@asgardeo/javascript': patch
'@asgardeo/browser': patch
'@asgardeo/nextjs': patch
'@asgardeo/react': patch
'@asgardeo/i18n': patch
---

Add `asagrdeo/thunder` support
8 changes: 8 additions & 0 deletions packages/browser/src/__legacy__/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1157,4 +1157,12 @@ export class AsgardeoSPAClient {

return;
}

/**
* This method clears the session information from the storage.
* @param sessionId - The session ID of the session to be cleared. If not provided, the current session will be cleared.
*/
public clearSession(sessionId?: string): void {
AsgardeoAuthClient.clearSession(sessionId);
}
}
1 change: 1 addition & 0 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,5 @@ export {
} from './theme/themeDetection';
export {default as getActiveTheme} from './theme/getActiveTheme';

export {default as handleWebAuthnAuthentication} from './utils/handleWebAuthnAuthentication';
export {default as http} from './utils/http';
264 changes: 264 additions & 0 deletions packages/browser/src/utils/handleWebAuthnAuthentication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
/**
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import {arrayBufferToBase64url, base64urlToArrayBuffer, AsgardeoRuntimeError} from '@asgardeo/javascript';

/**
* Handles WebAuthn/Passkey authentication flow for browser environments.
*
* This function processes a WebAuthn challenge, performs the authentication ceremony,
* and returns the authentication response that can be sent to the server for verification.
*
* The function handles various aspects of WebAuthn authentication including:
* - Browser compatibility checks for WebAuthn support
* - HTTPS requirement validation (except for localhost development)
* - Relying Party ID validation and domain compatibility
* - Challenge data decoding and credential request options processing
* - User authentication ceremony via navigator.credentials.get()
* - Response formatting for server consumption
*
* @param challengeData - Base64-encoded challenge data containing WebAuthn request options.
* This data typically includes the challenge, RP ID, allowed credentials,
* user verification requirements, and other authentication parameters.
*
* @returns Promise that resolves to a JSON string containing the WebAuthn authentication response.
* The response includes the credential ID, authenticator data, client data JSON,
* signature, and optional user handle that can be verified by the server.
*
* @throws {AsgardeoRuntimeError} When WebAuthn is not supported in the current browser
* @throws {AsgardeoRuntimeError} When the page is not served over HTTPS (except localhost)
* @throws {AsgardeoRuntimeError} When the user cancels or times out the authentication
* @throws {AsgardeoRuntimeError} When there's a domain/RP ID mismatch
* @throws {AsgardeoRuntimeError} When no valid passkey is found for the account
* @throws {AsgardeoRuntimeError} When WebAuthn is not supported on the device/browser
* @throws {AsgardeoRuntimeError} When there's a network error during authentication
* @throws {AsgardeoRuntimeError} For any other authentication failures
*
* @example
* ```typescript
* try {
* const challengeData = 'eyJwdWJsaWNLZXlDcmVkZW50aWFsUmVxdWVzdE9wdGlvbnMiOi4uLn0=';
* const authResponse = await handleWebAuthnAuthentication(challengeData);
*
* // Send the response to your server for verification
* const result = await fetch('/api/verify-webauthn', {
* method: 'POST',
* headers: { 'Content-Type': 'application/json' },
* body: authResponse
* });
* } catch (error) {
* if (error instanceof AsgardeoRuntimeError) {
* console.error('WebAuthn authentication failed:', error.message);
* }
* }
* ```
*
* @example
* ```typescript
* // Usage in an authentication flow
* const authenticateWithPasskey = async (challengeFromServer: string) => {
* try {
* const response = await handleWebAuthnAuthentication(challengeFromServer);
* return JSON.parse(response);
* } catch (error) {
* // Handle specific error cases
* if (error instanceof AsgardeoRuntimeError) {
* switch (error.code) {
* case 'browser-webauthn-not-supported':
* showFallbackAuth();
* break;
* case 'browser-webauthn-user-cancelled':
* showRetryOption();
* break;
* default:
* showGenericError();
* }
* }
* }
* };
* ```
*
* @see {@link https://webauthn.guide/} - WebAuthn specification guide
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API} - MDN WebAuthn API documentation
*/
const handleWebAuthnAuthentication = async (challengeData: string): Promise<string> => {
if (!window.navigator.credentials || !window.navigator.credentials.get) {
throw new AsgardeoRuntimeError(
'WebAuthn is not supported in this browser. Please use a modern browser or try a different authentication method.',
'browser-webauthn-not-supported',
'browser',
'WebAuthn/Passkey authentication requires a browser that supports the Web Authentication API.',
);
}

if (window.location.protocol !== 'https:' && window.location.hostname !== 'localhost') {
throw new AsgardeoRuntimeError(
'Passkey authentication requires a secure connection (HTTPS). Please ensure you are accessing this site over HTTPS.',
'browser-webauthn-insecure-connection',
'browser',
'WebAuthn authentication requires HTTPS for security reasons, except when running on localhost for development.',
);
}

try {
const decodedChallenge = JSON.parse(atob(challengeData));
const {publicKeyCredentialRequestOptions} = decodedChallenge;

const currentDomain = window.location.hostname;
const challengeRpId = publicKeyCredentialRequestOptions.rpId;

let rpIdToUse = challengeRpId;

if (challengeRpId && !currentDomain.endsWith(challengeRpId) && challengeRpId !== currentDomain) {
console.warn(`RP ID mismatch detected. Challenge RP ID: ${challengeRpId}, Current domain: ${currentDomain}`);
rpIdToUse = currentDomain;
}

const adjustedOptions = {
...publicKeyCredentialRequestOptions,
rpId: rpIdToUse,
challenge: base64urlToArrayBuffer(publicKeyCredentialRequestOptions.challenge),
...(publicKeyCredentialRequestOptions.userVerification && {
userVerification: publicKeyCredentialRequestOptions.userVerification,
}),
...(publicKeyCredentialRequestOptions.allowCredentials && {
allowCredentials: publicKeyCredentialRequestOptions.allowCredentials.map((cred: any) => ({
...cred,
id: base64urlToArrayBuffer(cred.id),
})),
}),
};

const credential = (await navigator.credentials.get({
publicKey: adjustedOptions,
})) as PublicKeyCredential;

if (!credential) {
throw new AsgardeoRuntimeError(
'No credential returned from WebAuthn authentication',
'browser-webauthn-no-credential',
'browser',
'The WebAuthn authentication ceremony completed but did not return a valid credential.',
);
}

const authData = credential.response as AuthenticatorAssertionResponse;

const tokenResponse = {
requestId: decodedChallenge.requestId,
credential: {
id: credential.id,
rawId: arrayBufferToBase64url(credential.rawId),
response: {
authenticatorData: arrayBufferToBase64url(authData.authenticatorData),
clientDataJSON: arrayBufferToBase64url(authData.clientDataJSON),
signature: arrayBufferToBase64url(authData.signature),
...(authData.userHandle && {
userHandle: arrayBufferToBase64url(authData.userHandle),
}),
},
type: credential.type,
},
};

return JSON.stringify(tokenResponse);
} catch (error) {
console.error('WebAuthn authentication failed:', error);

if (error instanceof AsgardeoRuntimeError) {
throw error;
}

if (error instanceof Error) {
switch (error.name) {
case 'NotAllowedError':
throw new AsgardeoRuntimeError(
'Passkey authentication was cancelled or timed out. Please try again.',
'browser-webauthn-user-cancelled',
'browser',
'The user cancelled the WebAuthn authentication request or the request timed out.',
);

case 'SecurityError':
if (error.message.includes('relying party ID') || error.message.includes('RP ID')) {
throw new AsgardeoRuntimeError(
'Domain mismatch error. The passkey was registered for a different domain. Please contact support or try a different authentication method.',
'browser-webauthn-domain-mismatch',
'browser',
'The WebAuthn relying party ID does not match the current domain.',
);
}
throw new AsgardeoRuntimeError(
'Passkey authentication failed due to a security error. Please ensure you are using HTTPS and that your browser supports passkeys.',
'browser-webauthn-security-error',
'browser',
'A security error occurred during WebAuthn authentication.',
);

case 'InvalidStateError':
throw new AsgardeoRuntimeError(
'No valid passkey found for this account. Please register a passkey first or use a different authentication method.',
'browser-webauthn-no-passkey',
'browser',
'No registered passkey credentials were found for the current user account.',
);

case 'NotSupportedError':
throw new AsgardeoRuntimeError(
'Passkey authentication is not supported on this device or browser. Please use a different authentication method.',
'browser-webauthn-not-supported',
'browser',
'WebAuthn is not supported on the current device or browser configuration.',
);

case 'NetworkError':
throw new AsgardeoRuntimeError(
'Network error during passkey authentication. Please check your connection and try again.',
'browser-webauthn-network-error',
'browser',
'A network error occurred while communicating with the authenticator.',
);

case 'UnknownError':
throw new AsgardeoRuntimeError(
'An unknown error occurred during passkey authentication. Please try again or use a different authentication method.',
'browser-webauthn-unknown-error',
'browser',
'An unidentified error occurred during the WebAuthn authentication process.',
);

default:
throw new AsgardeoRuntimeError(
`Passkey authentication failed: ${error.message}`,
'browser-webauthn-general-error',
'browser',
`WebAuthn authentication failed with error: ${error.name}`,
);
}
}

throw new AsgardeoRuntimeError(
'Passkey authentication failed due to an unexpected error.',
'browser-webauthn-unexpected-error',
'browser',
'An unexpected error occurred during WebAuthn authentication.',
);
}
};

export default handleWebAuthnAuthentication;
3 changes: 3 additions & 0 deletions packages/i18n/src/models/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@ export interface I18nTranslations {
'elements.buttons.microsoft': string;
'elements.buttons.linkedin': string;
'elements.buttons.ethereum': string;
'elements.buttons.smsotp': string;
'elements.buttons.multi.option': string;
'elements.buttons.social': string;

/* Fields */
'elements.fields.placeholder': string;
'elements.fields.username': string;
'elements.fields.password': string;

/* |---------------------------------------------------------------| */
/* | Widgets | */
Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/src/translations/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,14 @@ const translations: I18nTranslations = {
'elements.buttons.microsoft': 'Continue with Microsoft',
'elements.buttons.linkedin': 'Continue with LinkedIn',
'elements.buttons.ethereum': 'Continue with Sign In Ethereum',
'elements.buttons.smsotp': 'Continue with SMS OTP',
'elements.buttons.multi.option': 'Continue with {connection}',
'elements.buttons.social': 'Continue with {connection}',

/* Fields */
'elements.fields.placeholder': 'Enter your {field}',
'elements.fields.username': 'Username',
'elements.fields.password': 'Password',

/* |---------------------------------------------------------------| */
/* | Widgets | */
Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/src/translations/fr-FR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,14 @@ const translations: I18nTranslations = {
'elements.buttons.microsoft': 'Continuer avec Microsoft',
'elements.buttons.linkedin': 'Continuer with LinkedIn',
'elements.buttons.ethereum': 'Continuer avec Sign In Ethereum',
'elements.buttons.smsotp': 'Continuer avec SMS',
'elements.buttons.multi.option': 'Continuer avec {connection}',
'elements.buttons.social': 'Continuer avec {connection}',

/* Fields */
'elements.fields.placeholder': 'Entrez votre {field}',
'elements.fields.username': "Nom d'utilisateur",
'elements.fields.password': 'Mot de passe',

/* |---------------------------------------------------------------| */
/* | Widgets | */
Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/src/translations/hi-IN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,14 @@ const translations: I18nTranslations = {
'elements.buttons.microsoft': 'Microsoft के साथ जारी रखें',
'elements.buttons.linkedin': 'LinkedIn के साथ जारी रखें',
'elements.buttons.ethereum': 'Ethereum के साथ साइन इन करें',
'elements.buttons.smsotp': 'SMS के साथ जारी रखें',
'elements.buttons.multi.option': '{connection} के साथ जारी रखें',
'elements.buttons.social': '{connection} के साथ जारी रखें',

/* Fields */
'elements.fields.placeholder': '{field} दर्ज करें',
'elements.fields.username': 'उपयोगकर्ता नाम',
'elements.fields.password': 'पासवर्ड',

/* |---------------------------------------------------------------| */
/* | Widgets | */
Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/src/translations/ja-JP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,14 @@ const translations: I18nTranslations = {
'elements.buttons.microsoft': 'Microsoftで続行',
'elements.buttons.linkedin': 'LinkedInで続行',
'elements.buttons.ethereum': 'Ethereumでサインイン',
'elements.buttons.smsotp': 'SMSで続行',
'elements.buttons.multi.option': '{connection}で続行',
'elements.buttons.social': '{connection}で続行',

/* Fields */
'elements.fields.placeholder': '{field}を入力してください',
'elements.fields.username': 'ユーザー名',
'elements.fields.password': 'パスワード',

/* |---------------------------------------------------------------| */
/* | Widgets | */
Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/src/translations/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,14 @@ const translations: I18nTranslations = {
'elements.buttons.microsoft': 'Entrar com Microsoft',
'elements.buttons.linkedin': 'Entrar com LinkedIn',
'elements.buttons.ethereum': 'Entrar com Ethereum',
'elements.buttons.smsotp': 'Entrar com SMS',
'elements.buttons.multi.option': 'Entrar com {connection}',
'elements.buttons.social': 'Entrar com {connection}',

/* Fields */
'elements.fields.placeholder': 'Digite seu {field}',
'elements.fields.username': 'Nome de usuário',
'elements.fields.password': 'Senha',

/* |---------------------------------------------------------------| */
/* | Widgets | */
Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/src/translations/pt-PT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,14 @@ const translations: I18nTranslations = {
'elements.buttons.microsoft': 'Iniciar Sessão com Microsoft',
'elements.buttons.linkedin': 'Iniciar Sessão com LinkedIn',
'elements.buttons.ethereum': 'Iniciar Sessão com Ethereum',
'elements.buttons.smsotp': 'Iniciar Sessão com SMS',
'elements.buttons.multi.option': 'Iniciar Sessão com {connection}',
'elements.buttons.social': 'Iniciar Sessão com {connection}',

/* Fields */
'elements.fields.placeholder': 'Introduza o seu {field}',
'elements.fields.username': 'Nome de utilizador',
'elements.fields.password': 'Palavra-passe',

/* |---------------------------------------------------------------| */
/* | Widgets | */
Expand Down
Loading
Loading