Skip to content

Commit 6a9936c

Browse files
Merge pull request #31 from DonOmalVindula/implement-identifier-first
feat(react): Implement support for identifier first authenticator
2 parents 08dc7e5 + f697c5f commit 6a9936c

File tree

7 files changed

+199
-19
lines changed

7 files changed

+199
-19
lines changed

.changeset/fluffy-colts-rest.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@asgardeo/react": minor
3+
---
4+
5+
Implement support for identifier first authenticator

packages/react/src/components/SignIn/SignIn.tsx

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {SignIn as UISignIn} from '../../oxygen-ui-react-auth-components';
5050
import generateThemeSignIn from '../../theme/generate-theme-sign-in';
5151
import SPACryptoUtils from '../../utils/crypto-utils';
5252
import './sign-in.scss';
53+
import IdentifierFirst from './fragments/IdentifierFirst';
5354

5455
/**
5556
* This component provides the sign-in functionality.
@@ -63,7 +64,6 @@ import './sign-in.scss';
6364
const SignIn: FC<SignInProps> = (props: SignInProps): ReactElement => {
6465
const {brandingProps, showFooter = true, showLogo = true, showSignUp} = props;
6566

66-
const [authResponse, setAuthResponse] = useState<AuthApiResponse>();
6767
const [isComponentLoading, setIsComponentLoading] = useState<boolean>(true);
6868
const [alert, setAlert] = useState<AlertType>();
6969
const [showSelfSignUp, setShowSelfSignUp] = useState<boolean>(showSignUp);
@@ -93,7 +93,7 @@ const SignIn: FC<SignInProps> = (props: SignInProps): ReactElement => {
9393
*/
9494
authorize()
9595
.then((response: AuthApiResponse) => {
96-
setAuthResponse(response);
96+
authContext?.setAuthResponse(response);
9797
setIsComponentLoading(false);
9898
})
9999
.catch((error: Error) => {
@@ -111,14 +111,14 @@ const SignIn: FC<SignInProps> = (props: SignInProps): ReactElement => {
111111
const handleAuthenticate = async (authenticatorId: string, authParams?: {[key: string]: string}): Promise<void> => {
112112
setAlert(undefined);
113113

114-
if (authResponse === undefined) {
114+
if (authContext?.authResponse === undefined) {
115115
throw new AsgardeoUIException('REACT_UI-SIGN_IN-HA-IV02', 'Auth response is undefined.');
116116
}
117117

118118
authContext.setIsAuthLoading(true);
119119

120120
const resp: AuthApiResponse = await authenticate({
121-
flowId: authResponse.flowId,
121+
flowId: authContext?.authResponse.flowId,
122122
selectedAuthenticator: {
123123
authenticatorId,
124124
params: authParams,
@@ -162,13 +162,13 @@ const SignIn: FC<SignInProps> = (props: SignInProps): ReactElement => {
162162
window.removeEventListener('message', messageEventHandler);
163163
});
164164
} else if (metaData.promptType === PromptType.UserPrompt) {
165-
setAuthResponse(resp);
165+
authContext?.setAuthResponse(resp);
166166
}
167167
} else if (resp.flowStatus === FlowStatus.SuccessCompleted && resp.authData) {
168168
/**
169169
* when the authentication is successful, generate the token
170170
*/
171-
setAuthResponse(resp);
171+
authContext?.setAuthResponse(resp);
172172

173173
const authInstance: UIAuthClient = AuthClient.getInstance();
174174
const state: string = (await authInstance.getDataLayer().getTemporaryDataParameter('state')).toString();
@@ -177,14 +177,14 @@ const SignIn: FC<SignInProps> = (props: SignInProps): ReactElement => {
177177

178178
authContext.setAuthentication();
179179
} else if (resp.flowStatus === FlowStatus.FailIncomplete) {
180-
setAuthResponse({
180+
authContext?.setAuthResponse({
181181
...resp,
182-
nextStep: authResponse.nextStep,
182+
nextStep: authContext?.authResponse.nextStep,
183183
});
184184

185185
setAlert({alertType: {error: true}, key: keys.common.error});
186186
} else {
187-
setAuthResponse(resp);
187+
authContext?.setAuthResponse(resp);
188188
setShowSelfSignUp(false);
189189
}
190190

@@ -211,7 +211,7 @@ const SignIn: FC<SignInProps> = (props: SignInProps): ReactElement => {
211211
};
212212

213213
const renderSignIn = (): ReactElement => {
214-
const authenticators: Authenticator[] = authResponse?.nextStep?.authenticators;
214+
const authenticators: Authenticator[] = authContext?.authResponse?.nextStep?.authenticators;
215215

216216
if (authenticators) {
217217
const usernamePasswordAuthenticator: Authenticator = authenticators.find(
@@ -235,6 +235,27 @@ const SignIn: FC<SignInProps> = (props: SignInProps): ReactElement => {
235235
);
236236
}
237237

238+
const identifierFirstAuthenticator: Authenticator = authenticators.find(
239+
(authenticator: Authenticator) => authenticator.authenticator === 'Identifier First',
240+
);
241+
242+
if (identifierFirstAuthenticator) {
243+
return (
244+
<IdentifierFirst
245+
brandingProps={brandingProps}
246+
authenticator={identifierFirstAuthenticator}
247+
handleAuthenticate={handleAuthenticate}
248+
showSelfSignUp={showSelfSignUp}
249+
alert={alert}
250+
renderLoginOptions={renderLoginOptions(
251+
authenticators.filter(
252+
(auth: Authenticator) => auth.authenticatorId !== identifierFirstAuthenticator.authenticatorId,
253+
),
254+
)}
255+
/>
256+
);
257+
}
258+
238259
if (authenticators.length === 1) {
239260
if (authenticators[0].authenticator === 'TOTP') {
240261
return (
@@ -249,7 +270,7 @@ const SignIn: FC<SignInProps> = (props: SignInProps): ReactElement => {
249270
if (
250271
// TODO: change after api based auth gets fixed
251272
new SPACryptoUtils()
252-
.base64URLDecode(authResponse.nextStep.authenticators[0].authenticatorId)
273+
.base64URLDecode(authContext?.authResponse.nextStep.authenticators[0].authenticatorId)
253274
.split(':')[0] === 'email-otp-authenticator'
254275
) {
255276
return (
@@ -265,7 +286,7 @@ const SignIn: FC<SignInProps> = (props: SignInProps): ReactElement => {
265286
if (
266287
// TODO: change after api based auth gets fixed
267288
new SPACryptoUtils()
268-
.base64URLDecode(authResponse.nextStep.authenticators[0].authenticatorId)
289+
.base64URLDecode(authContext?.authResponse.nextStep.authenticators[0].authenticatorId)
269290
.split(':')[0] === 'sms-otp-authenticator'
270291
) {
271292
return (
@@ -326,7 +347,7 @@ const SignIn: FC<SignInProps> = (props: SignInProps): ReactElement => {
326347
{showLogo && !(isLoading || isComponentLoading) && (
327348
<UISignIn.Image className="asgardeo-sign-in-logo" src={imgUrl} />
328349
)}
329-
{authResponse?.flowStatus !== FlowStatus.SuccessCompleted && !isAuthenticated && (
350+
{authContext?.authResponse?.flowStatus !== FlowStatus.SuccessCompleted && !isAuthenticated && (
330351
<>
331352
{renderSignIn()}
332353

@@ -355,7 +376,7 @@ const SignIn: FC<SignInProps> = (props: SignInProps): ReactElement => {
355376
)}
356377
</>
357378
)}
358-
{(authResponse?.flowStatus === FlowStatus.SuccessCompleted || isAuthenticated) && (
379+
{(authContext?.authResponse?.flowStatus === FlowStatus.SuccessCompleted || isAuthenticated) && (
359380
<div style={{backgroundColor: 'white', padding: '1rem'}}>Successfully Authenticated</div>
360381
)}
361382
</UISignIn>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/**
2+
* Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com).
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
import {ScreenType, keys} from '@asgardeo/js';
20+
import {CircularProgress, Grid, Skeleton} from '@oxygen-ui/react';
21+
import {ReactElement, useContext, useState} from 'react';
22+
import AsgardeoContext from '../../../contexts/asgardeo-context';
23+
import useTranslations from '../../../hooks/use-translations';
24+
import BasicAuthProps from '../../../models/basic-auth-props';
25+
import {SignIn as UISignIn} from '../../../oxygen-ui-react-auth-components';
26+
import './basic-auth.scss';
27+
28+
/**
29+
* This component renders the IdentifierFirst authentication form.
30+
*
31+
* @param {IdentifierFirstProps} props - Props injected to the IdentifierFirst authentication component.
32+
* @param {BrandingProps} props.brandingProps - Branding props.
33+
* @param {Function} props.handleAuthenticate - Callback to handle authentication.
34+
* @param {Authenticator} props.authenticator - Authenticator.
35+
* @param {AlertType} props.alert - Alert type.
36+
* @param {ReactElement[]} props.renderLoginOptions - Login options.
37+
* @param {boolean} props.showSelfSignUp - Show self sign up.
38+
*
39+
* @return {ReactElement}
40+
*/
41+
const IdentifierFirst = ({
42+
handleAuthenticate,
43+
authenticator,
44+
alert,
45+
brandingProps,
46+
showSelfSignUp,
47+
renderLoginOptions,
48+
}: BasicAuthProps): ReactElement => {
49+
const [username, setUsername] = useState<string>('');
50+
51+
const {isAuthLoading} = useContext(AsgardeoContext);
52+
53+
const {t, isLoading} = useTranslations({
54+
componentLocaleOverride: brandingProps?.locale,
55+
componentTextOverrides: brandingProps?.preference?.text,
56+
screen: ScreenType.Login,
57+
});
58+
59+
if (isLoading) {
60+
return (
61+
<UISignIn.Paper className="asgardeo-basic-auth-skeleton">
62+
<Skeleton className="skeleton-title" variant="text" width={100} height={55} />
63+
<Skeleton className="skeleton-text-field-label" variant="text" width={70} />
64+
<Skeleton variant="rectangular" width={300} height={40} />
65+
<Skeleton className="skeleton-text-field-label" variant="text" width={70} />
66+
<Skeleton variant="rectangular" width={300} height={40} />
67+
<Skeleton className="skeleton-submit-button" variant="rectangular" width={270} height={40} />
68+
</UISignIn.Paper>
69+
);
70+
}
71+
72+
return (
73+
<UISignIn.Paper className="Paper-basicAuth">
74+
<UISignIn.Typography title className="Typography-basicAuthTitle">
75+
{t(keys.login.login.heading)}
76+
</UISignIn.Typography>
77+
78+
{alert && (
79+
<UISignIn.Alert className="asgardeo-basic-auth-alert" {...alert?.alertType}>
80+
{t(alert.key)}
81+
</UISignIn.Alert>
82+
)}
83+
84+
<UISignIn.TextField
85+
fullWidth
86+
autoComplete="off"
87+
label={t(keys.login.username)}
88+
name="text"
89+
value={username}
90+
placeholder={t(keys.login.enter.your.username)}
91+
onChange={(e: React.ChangeEvent<HTMLInputElement>): void => setUsername(e.target.value)}
92+
/>
93+
94+
<UISignIn.Button
95+
color="primary"
96+
variant="contained"
97+
type="submit"
98+
fullWidth
99+
disabled={isAuthLoading}
100+
onClick={(): void => {
101+
handleAuthenticate(authenticator.authenticatorId, {
102+
username,
103+
});
104+
setUsername('');
105+
}}
106+
>
107+
{t(keys.login.button)}
108+
</UISignIn.Button>
109+
110+
{isAuthLoading && (
111+
<div className="circular-progress-holder-authn">
112+
<CircularProgress className="sign-in-button-progress" />
113+
</div>
114+
)}
115+
116+
{showSelfSignUp && (
117+
<Grid container>
118+
<UISignIn.Typography>{t(keys.common.prefix.register)}</UISignIn.Typography>
119+
<UISignIn.Link href="./register" className="asgardeo-register-link">
120+
{t(keys.common.register)}
121+
</UISignIn.Link>
122+
</Grid>
123+
)}
124+
125+
{renderLoginOptions.length !== 0 && <UISignIn.Divider> {t(keys.common.or)} </UISignIn.Divider>}
126+
127+
{renderLoginOptions}
128+
</UISignIn.Paper>
129+
);
130+
};
131+
132+
export default IdentifierFirst;

packages/react/src/hooks/use-authentication.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import UseAuthentication from '../models/use-authentication';
3232
const useAuthentication = (): UseAuthentication => {
3333
const contextValue: AuthContext = useContext(AsgardeoContext);
3434

35-
const {user, isAuthenticated, accessToken} = contextValue;
35+
const {user, isAuthenticated, accessToken, authResponse} = contextValue;
3636

3737
const signOut: () => void = () => {
3838
signOutApiCall().then(() => {
@@ -43,7 +43,13 @@ const useAuthentication = (): UseAuthentication => {
4343
});
4444
};
4545

46-
return {accessToken, isAuthenticated, signOut, user};
46+
return {
47+
accessToken,
48+
authResponse,
49+
isAuthenticated,
50+
signOut,
51+
user,
52+
};
4753
};
4854

4955
export default useAuthentication;

packages/react/src/models/auth-context.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,19 @@
1616
* under the License.
1717
*/
1818

19-
import {UIAuthConfig, MeAPIResponse} from '@asgardeo/js';
19+
import {UIAuthConfig, MeAPIResponse, AuthApiResponse} from '@asgardeo/js';
2020

2121
interface AuthContext {
2222
accessToken: string;
23+
authResponse: AuthApiResponse;
2324
config: UIAuthConfig;
2425
isAuthLoading: boolean;
2526
isAuthenticated: boolean | undefined;
2627
isBrandingLoading: boolean;
2728
isGlobalLoading: boolean;
2829
isTextLoading: boolean;
2930
onSignOutRef: React.MutableRefObject<Function>;
31+
setAuthResponse: (response: AuthApiResponse) => void;
3032
setAuthentication: () => void;
3133
setIsAuthLoading: (value: boolean) => void;
3234
setIsBrandingLoading: (value: boolean) => void;

packages/react/src/models/use-authentication.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@
1616
* under the License.
1717
*/
1818

19-
import {MeAPIResponse} from '@asgardeo/js';
19+
import {AuthApiResponse, MeAPIResponse} from '@asgardeo/js';
2020

2121
interface UseAuthentication {
2222
accessToken: string;
23+
authResponse: AuthApiResponse;
2324
isAuthenticated: Promise<boolean> | boolean;
2425
signOut: () => void;
2526
user: MeAPIResponse;

0 commit comments

Comments
 (0)