Skip to content

Commit 0585f1c

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/totp-lockout-ui
2 parents 57be03e + 34143b3 commit 0585f1c

6 files changed

Lines changed: 153 additions & 5 deletions

File tree

‎package-lock.json‎

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
"typescript": "^5.7.2"
9292
},
9393
"dependencies": {
94-
"@authorizerdev/authorizer-js": "3.2.1",
94+
"@authorizerdev/authorizer-js": "3.3.0-rc.0",
9595
"@storybook/preset-scss": "^1.0.3",
9696
"validator": "^13.11.0"
9797
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { FC, useState } from 'react';
2+
import { AuthToken, isWebauthnSupported } from '@authorizerdev/authorizer-js';
3+
4+
import '../styles/default.css';
5+
import { ButtonAppearance, MessageType } from '../constants';
6+
import { useAuthorizer } from '../contexts/AuthorizerContext';
7+
import { StyledButton, StyledSeparator } from '../styledComponents';
8+
import { Message } from './Message';
9+
10+
// AuthorizerPasskeyLogin offers a full passwordless, usernameless "Sign in
11+
// with a passkey" option (discoverable-credential login) alongside the other
12+
// login methods. It only renders when the browser actually supports the
13+
// WebAuthn JSON ceremony APIs the SDK relies on - there is no server-side
14+
// config flag for passkeys (unlike social login), it's purely a browser
15+
// capability.
16+
export const AuthorizerPasskeyLogin: FC<{
17+
onLogin?: (data: AuthToken | void) => void;
18+
}> = ({ onLogin }) => {
19+
const [error, setError] = useState(``);
20+
const [loading, setLoading] = useState(false);
21+
const { setAuthData, config, authorizerRef } = useAuthorizer();
22+
23+
if (!isWebauthnSupported()) {
24+
return null;
25+
}
26+
27+
// Only show the "OR" separator if AuthorizerRoot is actually going to
28+
// render something below it - otherwise the passkey button ends up
29+
// followed by a trailing separator with nothing underneath. Mirrors the
30+
// exact set of conditions AuthorizerRoot uses to decide whether
31+
// AuthorizerSocialLogin, AuthorizerBasicAuthLogin, or
32+
// AuthorizerMagicLinkLogin render anything on the login view.
33+
const hasSocialLogin =
34+
config.is_google_login_enabled ||
35+
config.is_github_login_enabled ||
36+
config.is_facebook_login_enabled ||
37+
config.is_linkedin_login_enabled ||
38+
config.is_apple_login_enabled ||
39+
config.is_twitter_login_enabled ||
40+
config.is_microsoft_login_enabled ||
41+
config.is_twitch_login_enabled ||
42+
config.is_roblox_login_enabled;
43+
const hasBasicAuthLogin =
44+
(config.is_basic_authentication_enabled ||
45+
config.is_mobile_basic_authentication_enabled) &&
46+
!config.is_magic_link_login_enabled;
47+
const hasAnotherLoginMethod =
48+
hasSocialLogin || hasBasicAuthLogin || config.is_magic_link_login_enabled;
49+
50+
const onClick = async () => {
51+
setError(``);
52+
try {
53+
setLoading(true);
54+
const { data: res, errors } = await authorizerRef.loginWithPasskey();
55+
if (errors && errors.length) {
56+
setError(errors[0]?.message || ``);
57+
return;
58+
}
59+
if (res) {
60+
setAuthData({
61+
user: res.user || null,
62+
token: res,
63+
config,
64+
loading: false,
65+
});
66+
}
67+
if (onLogin) {
68+
onLogin(res);
69+
}
70+
} catch (err) {
71+
setError((err as Error).message);
72+
} finally {
73+
setLoading(false);
74+
}
75+
};
76+
77+
const onErrorClose = () => setError(``);
78+
79+
return (
80+
<>
81+
{error && (
82+
<Message type={MessageType.Error} text={error} onClose={onErrorClose} />
83+
)}
84+
<StyledButton
85+
onClick={onClick}
86+
disabled={loading}
87+
appearance={ButtonAppearance.Default}
88+
>
89+
{loading ? `Waiting for passkey ...` : `Sign in with a passkey`}
90+
</StyledButton>
91+
{hasAnotherLoginMethod && <StyledSeparator>OR</StyledSeparator>}
92+
</>
93+
);
94+
};

‎src/components/AuthorizerRoot.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { AuthorizerSignup } from './AuthorizerSignup';
99
import type { FormFieldsOverrides } from './AuthorizerSignup';
1010
import { AuthorizerForgotPassword } from './AuthorizerForgotPassword';
1111
import { AuthorizerSocialLogin } from './AuthorizerSocialLogin';
12+
import { AuthorizerPasskeyLogin } from './AuthorizerPasskeyLogin';
1213
import { AuthorizerMagicLinkLogin } from './AuthorizerMagicLinkLogin';
1314
import { createRandomString } from '../utils/common';
1415
import { hasWindow } from '../utils/window';
@@ -60,6 +61,7 @@ export const AuthorizerRoot: FC<{
6061
return (
6162
<StyledWrapper>
6263
<AuthorizerSocialLogin urlProps={urlProps} roles={roles} />
64+
{view === Views.Login && <AuthorizerPasskeyLogin onLogin={onLogin} />}
6365
{view === Views.Login &&
6466
(config.is_basic_authentication_enabled ||
6567
config.is_mobile_basic_authentication_enabled) &&

‎src/components/AuthorizerVerifyOtp.tsx‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,22 @@ import { ButtonAppearance, MessageType, Views } from '../constants';
66
import { useAuthorizer } from '../contexts/AuthorizerContext';
77
import { StyledButton, StyledFooter, StyledLink } from '../styledComponents';
88
import { Message } from './Message';
9+
import { TotpDataType } from '../types';
10+
import { AuthorizerTOTPScanner } from './AuthorizerTOTPScanner';
911

1012
interface InputDataType {
1113
otp: string | null;
1214
}
1315

16+
const initTotpData: TotpDataType = {
17+
is_screen_visible: false,
18+
email: '',
19+
phone_number: '',
20+
authenticator_scanner_image: '',
21+
authenticator_secret: '',
22+
authenticator_recovery_codes: [],
23+
};
24+
1425
export const AuthorizerVerifyOtp: FC<{
1526
setView?: (v: Views) => void;
1627
onLogin?: (data: any) => void;
@@ -22,6 +33,7 @@ export const AuthorizerVerifyOtp: FC<{
2233
const [error, setError] = useState(``);
2334
const [successMessage, setSuccessMessage] = useState(``);
2435
const [loading, setLoading] = useState(false);
36+
const [totpData, setTotpData] = useState<TotpDataType>({ ...initTotpData });
2537
const [sendingOtp, setSendingOtp] = useState(false);
2638
const [isLockedOut, setIsLockedOut] = useState(false);
2739
const [formData, setFormData] = useState<InputDataType>({
@@ -73,6 +85,26 @@ export const AuthorizerVerifyOtp: FC<{
7385
setError(errors[0]?.message || ``);
7486
return;
7587
}
88+
89+
// If TOTP validated using recovery code then show totp screen with scanner
90+
if (
91+
res &&
92+
res.should_show_totp_screen &&
93+
res.authenticator_scanner_image &&
94+
res.authenticator_secret &&
95+
res.authenticator_recovery_codes
96+
) {
97+
setTotpData({
98+
is_screen_visible: true,
99+
email: data.email || ``,
100+
phone_number: data.phone_number || ``,
101+
authenticator_scanner_image: res.authenticator_scanner_image,
102+
authenticator_secret: res.authenticator_secret,
103+
authenticator_recovery_codes: res.authenticator_recovery_codes,
104+
});
105+
return;
106+
}
107+
76108
if (res) {
77109
setError(``);
78110
setAuthData({
@@ -138,6 +170,24 @@ export const AuthorizerVerifyOtp: FC<{
138170
}
139171
}, [formData.otp]);
140172

173+
if (totpData.is_screen_visible) {
174+
return (
175+
<AuthorizerTOTPScanner
176+
{...{
177+
setView,
178+
onLogin,
179+
email: totpData.email || ``,
180+
phone_number: totpData.phone_number || ``,
181+
authenticator_scanner_image: totpData.authenticator_scanner_image,
182+
authenticator_secret: totpData.authenticator_secret,
183+
authenticator_recovery_codes:
184+
totpData.authenticator_recovery_codes || [],
185+
}}
186+
urlProps={urlProps}
187+
/>
188+
);
189+
}
190+
141191
return (
142192
<>
143193
{successMessage && (

‎src/index.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { AuthorizerResetPassword } from './components/AuthorizerResetPassword';
1414
import { AuthorizerVerifyOtp } from './components/AuthorizerVerifyOtp';
1515
import { AuthorizerRoot as Authorizer } from './components/AuthorizerRoot';
1616
import { AuthorizerTOTPScanner } from './components/AuthorizerTOTPScanner';
17+
import { AuthorizerPasskeyLogin } from './components/AuthorizerPasskeyLogin';
1718

1819
export {
1920
useAuthorizer,
@@ -27,4 +28,5 @@ export {
2728
AuthorizerResetPassword,
2829
AuthorizerVerifyOtp,
2930
AuthorizerTOTPScanner,
31+
AuthorizerPasskeyLogin,
3032
};

0 commit comments

Comments
 (0)