Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .changeset/silly-words-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@asgardeo/javascript': minor
'@asgardeo/react': minor
'@asgardeo/i18n': minor
---

add passkey support
6 changes: 6 additions & 0 deletions packages/i18n/src/models/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ export interface I18nTranslations {
'username.password.heading': string;
'username.password.subheading': string;

/* Passkeys */
'passkey.button.use': string;
'passkey.signin.heading': string;
'passkey.register.heading': string;
'passkey.register.description': string;

/* |---------------------------------------------------------------| */
/* | User Profile | */
/* |---------------------------------------------------------------| */
Expand Down
6 changes: 6 additions & 0 deletions packages/i18n/src/translations/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ const translations: I18nTranslations = {
'username.password.subheading': 'Enter your username and password to continue.',
'username.password.buttons.submit.text': 'Continue',

/* Passkeys */
'passkey.button.use': 'Sign in with Passkey',
'passkey.signin.heading': 'Sign in with Passkey',
'passkey.register.heading': 'Register Passkey',
'passkey.register.description': 'Create a passkey to securely sign in to your account without a password.',

/* |---------------------------------------------------------------| */
/* | User Profile | */
/* |---------------------------------------------------------------| */
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/translations/fr-FR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ const translations: I18nTranslations = {
'username.password.heading': 'Se connecter',
'username.password.subheading': "Entrez votre nom d'utilisateur et votre mot de passe pour continuer.",

/* Passkeys */
'passkey.button.use': "Se connecter avec une clé d'accès",
'passkey.signin.heading': "Se connecter avec une clé d'accès",
'passkey.register.heading': "Enregistrer une clé d'accès",
'passkey.register.description':
"Créez une clé d'accès pour vous connecter en toute sécurité à votre compte sans mot de passe.",

/* |---------------------------------------------------------------| */
/* | User Profile | */
/* |---------------------------------------------------------------| */
Expand Down
6 changes: 6 additions & 0 deletions packages/i18n/src/translations/hi-IN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ const translations: I18nTranslations = {
'username.password.heading': 'साइन इन',
'username.password.subheading': 'अपना उपयोगकर्ता नाम और पासवर्ड दर्ज करें।',

/* Passkeys */
'passkey.button.use': 'Passkey के साथ साइन इन करें',
'passkey.signin.heading': 'Passkey के साथ साइन इन करें',
'passkey.register.heading': 'Passkey पंजीकृत करें',
'passkey.register.description': 'बिना पासवर्ड के अपने खाते में सुरक्षित रूप से साइन इन करने के लिए एक Passkey बनाएं।',

/* |---------------------------------------------------------------| */
/* | User Profile | */
/* |---------------------------------------------------------------| */
Expand Down
6 changes: 6 additions & 0 deletions packages/i18n/src/translations/ja-JP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ const translations: I18nTranslations = {
'username.password.heading': 'ログイン',
'username.password.subheading': 'ユーザー名とパスワードを入力してください。',

/* Passkeys */
'passkey.button.use': 'パスキーでサインイン',
'passkey.signin.heading': 'パスキーでサインイン',
'passkey.register.heading': 'パスキーを登録',
'passkey.register.description': 'パスワードなしでアカウントに安全にサインインするためのパスキーを作成します。',

/* |---------------------------------------------------------------| */
/* | User Profile | */
/* |---------------------------------------------------------------| */
Expand Down
6 changes: 6 additions & 0 deletions packages/i18n/src/translations/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ const translations: I18nTranslations = {
'username.password.heading': 'Entrar',
'username.password.subheading': 'Digite seu usuário e senha para continuar.',

/* Passkeys */
'passkey.button.use': 'Entrar com Passkey',
'passkey.signin.heading': 'Entrar com Passkey',
'passkey.register.heading': 'Registrar Passkey',
'passkey.register.description': 'Crie uma passkey para entrar em sua conta com segurança sem uma senha.',

/* |---------------------------------------------------------------| */
/* | User Profile | */
/* |---------------------------------------------------------------| */
Expand Down
6 changes: 6 additions & 0 deletions packages/i18n/src/translations/pt-PT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ const translations: I18nTranslations = {
'username.password.heading': 'Iniciar Sessão',
'username.password.subheading': 'Introduza o seu utilizador e palavra-passe para continuar.',

/* Passkeys */
'passkey.button.use': 'Iniciar sessão com Passkey',
'passkey.signin.heading': 'Iniciar sessão com Passkey',
'passkey.register.heading': 'Registar Passkey',
'passkey.register.description': 'Crie uma passkey para iniciar sessão na sua conta com segurança sem palavra-passe.',

/* |---------------------------------------------------------------| */
/* | User Profile | */
/* |---------------------------------------------------------------| */
Expand Down
6 changes: 6 additions & 0 deletions packages/i18n/src/translations/si-LK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ const translations: I18nTranslations = {
'username.password.heading': 'ලොග් වෙන්න',
'username.password.subheading': 'ඉදිරියට යාමට ඔබේ පරිශීලක නාමය සහ මුරපදය ඇතුළත් කරන්න.',

/* Passkeys */
'passkey.button.use': 'Passkey මගින් ඇතුල් වන්න',
'passkey.signin.heading': 'Passkey මගින් ඇතුල් වන්න',
'passkey.register.heading': 'Passkey ලියාපදිංචි කරන්න',
'passkey.register.description': 'මුරපදයක් නොමැතිව ඔබේ ගිණුමට ආරක්ෂිතව ඇතුල් වීමට passkey එකක් සාදන්න.',

/* |---------------------------------------------------------------| */
/* | User Profile | */
/* |---------------------------------------------------------------| */
Expand Down
6 changes: 6 additions & 0 deletions packages/i18n/src/translations/ta-IN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ const translations: I18nTranslations = {
'username.password.heading': 'உள்நுழை',
'username.password.subheading': 'தொடர உங்கள் பயனர்பெயர் மற்றும் கடவுச்சொல்லை உள்ளிடவும்.',

/* Passkeys */
'passkey.button.use': 'Passkey மூலம் உள்நுழையவும்',
'passkey.signin.heading': 'Passkey மூலம் உள்நுழையவும்',
'passkey.register.heading': 'Passkey-ஐ பதிவு செய்யவும்',
'passkey.register.description': 'கடவுச்சொல் இல்லாமல் பாதுகாப்பாக உள்நுழைய ஒரு passkey-ஐ உருவாக்கவும்.',

/* |---------------------------------------------------------------| */
/* | User Profile | */
/* |---------------------------------------------------------------| */
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/translations/te-IN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ const translations: I18nTranslations = {
'username.password.heading': 'సైన్ ఇన్ చేయండి',
'username.password.subheading': 'మీ యూజర్ పేరు మరియు పాస్‌వర్డ్ ఇవ్వండి.',

/* Passkeys */
'passkey.button.use': 'Passkey తో సైన్ ఇన్ చేయండి',
'passkey.signin.heading': 'Passkey తో సైన్ ఇన్ చేయండి',
'passkey.register.heading': 'Passkey ని నమోదు చేయండి',
'passkey.register.description':
'పాస్‌వర్డ్ లేకుండా మీ ఖాతాలోకి సురక్షితంగా సైన్ ఇన్ చేయడానికి Passkey ని సృష్టించండి.',

/* |---------------------------------------------------------------| */
/* | User Profile | */
/* |---------------------------------------------------------------| */
Expand Down
1 change: 1 addition & 0 deletions packages/javascript/src/models/embedded-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export enum EmbeddedFlowResponseType {
export interface EmbeddedSignUpFlowData {
components?: EmbeddedFlowComponent[];
redirectURL?: string;
additionalData?: Record<string, any>;
}

export interface EmbeddedFlowComponent {
Expand Down
6 changes: 6 additions & 0 deletions packages/javascript/src/models/v2/embedded-flow-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,12 @@ export interface EmbeddedFlowResponseData {
* Optional redirect URL for flow completion or external authentication.
*/
redirectURL?: string;

/**
* Additional data dictionary for dynamic flow response properties.
* Can be used to pass custom data like passkey challenges, server alerts, etc.
*/
additionalData?: Record<string, any>;
}

/**
Expand Down
106 changes: 105 additions & 1 deletion packages/react/src/components/presentation/auth/SignIn/v2/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
} from '@asgardeo/browser';
import {normalizeFlowResponse} from '../../../../../utils/v2/flowTransformer';
import useTranslation from '../../../../../hooks/useTranslation';
import { handlePasskeyAuthentication, handlePasskeyRegistration } from '../../../../../utils/v2/passkey';

/**
* Render props function parameters
Expand Down Expand Up @@ -104,6 +105,18 @@ export type SignInProps = {
children?: (props: SignInRenderProps) => ReactNode;
};

/**
* State for tracking passkey registration
*/
interface PasskeyState {
isActive: boolean;
challenge: string | null;
creationOptions: string | null;
flowId: string | null;
actionId: string | null;
error: Error | null;
}

/**
* A component-driven SignIn component that provides authentication flow with pre-built styling.
* This component handles the flow API calls for authentication and delegates UI logic to BaseSignIn.
Expand Down Expand Up @@ -175,9 +188,17 @@ const SignIn: FC<SignInProps> = ({className, size = 'medium', onSuccess, onError
const [isFlowInitialized, setIsFlowInitialized] = useState(false);
const [flowError, setFlowError] = useState<Error | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [passkeyState, setPasskeyState] = useState<PasskeyState>({
isActive: false,
challenge: null,
creationOptions: null,
flowId: null,
actionId: null,
error: null,
});
const initializationAttemptedRef = useRef(false);
const oauthCodeProcessedRef = useRef(false);

const passkeyProcessedRef = useRef(false);
/**
* Sets flowId between sessionStorage and state.
* This ensures both are always in sync.
Expand Down Expand Up @@ -444,6 +465,28 @@ const SignIn: FC<SignInProps> = ({className, size = 'medium', onSuccess, onError
return;
}

if (response.data?.additionalData?.['passkeyChallenge'] || response.data?.additionalData?.['passkeyCreationOptions']) {
const passkeyChallenge = response.data.additionalData['passkeyChallenge'];
const passkeyCreationOptions = response.data.additionalData['passkeyCreationOptions'];
const effectiveFlowIdForPasskey = response.flowId || effectiveFlowId;

// Reset passkey processed ref to allow processing
passkeyProcessedRef.current = false;

// Set passkey state to trigger the passkey
setPasskeyState({
isActive: true,
challenge: passkeyChallenge,
creationOptions: passkeyCreationOptions,
flowId: effectiveFlowIdForPasskey,
actionId: 'submit',
error: null,
});
setIsSubmitting(false);

return;
}

const {flowId, components, ...rest} = normalizeFlowResponse(response, t, {
resolveTranslations: !children,
});
Expand Down Expand Up @@ -564,6 +607,67 @@ const SignIn: FC<SignInProps> = ({className, size = 'medium', onSuccess, onError
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isFlowInitialized, currentFlowId, isInitialized, isLoading, isSubmitting, signIn]);


/**
* Handle passkey authentication/registration when passkey state becomes active.
* This effect auto-triggers the browser passkey popup and submits the result.
*/
useEffect(() => {
if (!passkeyState.isActive || (!passkeyState.challenge && !passkeyState.creationOptions) || !passkeyState.flowId) {
return;
}

// Prevent re-processing
if (passkeyProcessedRef.current) {
return;
}
passkeyProcessedRef.current = true;

const performPasskeyProcess = async () => {
let inputs: Record<string, string>;

if (passkeyState.challenge) {
const passkeyResponse = await handlePasskeyAuthentication(passkeyState.challenge!);
const passkeyResponseObj = JSON.parse(passkeyResponse);

inputs = {
credentialId: passkeyResponseObj.id,
authenticatorData: passkeyResponseObj.response.authenticatorData,
clientDataJSON: passkeyResponseObj.response.clientDataJSON,
signature: passkeyResponseObj.response.signature,
userHandle: passkeyResponseObj.response.userHandle,
};
} else if (passkeyState.creationOptions) {
const passkeyResponse = await handlePasskeyRegistration(passkeyState.creationOptions!);
const passkeyResponseObj = JSON.parse(passkeyResponse);

inputs = {
credentialId: passkeyResponseObj.id,
clientDataJSON: passkeyResponseObj.response.clientDataJSON,
attestationObject: passkeyResponseObj.response.attestationObject,
};
} else {
throw new Error('No passkey challenge or creation options available');
}

await handleSubmit({
flowId: passkeyState.flowId!,
inputs,
});
};

performPasskeyProcess()
.then(() => {
setPasskeyState({ isActive: false, challenge: null, creationOptions: null, flowId: null, actionId: null, error: null });
})
.catch((error) => {
setPasskeyState(prev => ({ ...prev, isActive: false, error: error as Error }));
setFlowError(error as Error);
onError?.(error as Error);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [passkeyState.isActive, passkeyState.challenge, passkeyState.creationOptions, passkeyState.flowId]);

if (children) {
const renderProps: SignInRenderProps = {
initialize: initializeFlow,
Expand Down
Loading
Loading