Skip to content

Commit 77e2e53

Browse files
authored
Merge pull request #53 from criipto/upgrade-auth-js
Use PAR
2 parents 1c3f107 + 0e8ca4d commit 77e2e53

11 files changed

Lines changed: 140 additions & 70 deletions

README.md

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,25 +49,32 @@ import { useCriiptoVerify, AuthMethodSelector } from '@criipto/verify-react';
4949
import '@criipto/verify-react/index.css';
5050

5151
export default function App() {
52-
const { result } = useCriiptoVerify();
52+
const { result, error } = useCriiptoVerify();
5353

5454
if (result?.id_token) {
5555
return <pre>{JSON.stringify(result.id_token, null, 2)}</pre>;
5656
}
5757

5858
return (
5959
<React.Fragment>
60-
{result?.error ? (
61-
<p>
62-
An error occurred: {result.error} ({result.error_description}). Please try again.
63-
</p>
64-
) : null}
60+
{error ? <p>An error occurred: {String(error)}. Please try again.</p> : null}
6561
<AuthMethodSelector />
6662
</React.Fragment>
6763
);
6864
}
6965
```
7066
67+
Always render the `error` field from `useCriiptoVerify`. It surfaces both **configuration errors** (e.g. invalid `domain` or `clientID`, or CORS) raised when the provider mounts, and **runtime errors** raised during a login attempt (e.g. user cancellation, OAuth2 errors from the IdP). Without rendering `error`, misconfiguration and failed logins will appear silent to the user.
68+
69+
## CORS
70+
71+
The library makes fetch requests to Criipto for two reasons:
72+
73+
1. To load application configuration when the provider mounts.
74+
2. To push the authorization request (PAR) when a user clicks a login button.
75+
76+
Make sure that the origin your React app runs on is included in the list of callback URLs for your application. Otherwise, both calls will fail with CORS errors.
77+
7178
## Sessions
7279
7380
If you want to use `@criipto/verify-react` for session management (rather than one-off authentication) you can configure a `sessionStore`:
@@ -102,7 +109,7 @@ You may wish to increase the "Token lifetime" setting of your Criipto Applicatio
102109
```jsx
103110
// src/App.js
104111
import React from 'react';
105-
import { useCriiptoVerify, AuthMethodSelector } from '@criipto/verify-react';
112+
import { useCriiptoVerify, AuthMethodSelector, OAuth2Error } from '@criipto/verify-react';
106113

107114
export default function App() {
108115
const { claims, error, isLoading } = useCriiptoVerify();
@@ -119,7 +126,11 @@ export default function App() {
119126
<React.Fragment>
120127
{error ? (
121128
<p>
122-
An error occured: {error.error} ({error.error_description}). Please try again:
129+
An error occured:{' '}
130+
{error instanceof OAuth2Error
131+
? `${error.error} (${error.error_description})`
132+
: String(error)}
133+
. Please try again.
123134
</p>
124135
) : null}
125136
<AuthMethodSelector />
@@ -153,10 +164,6 @@ useEffect(() => {
153164
}, [isLoading, isInitializing]);
154165
```
155166
156-
## XHR/fetch caveats
157-
158-
The library may require to do fetch requests against Criipto to fetch application configuration. Make sure that the host that the React app runs on is included in the list of callback URLs for your application. Otherwise, you will encounter CORS errors.
159-
160167
## Criipto
161168
162169
Learn more about Criipto and sign up for your free developer account at [criipto.com](https://www.criipto.com).

package-lock.json

Lines changed: 15 additions & 13 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
@@ -68,7 +68,7 @@
6868
},
6969
"homepage": "https://github.com/criipto/criipto-verify-react#readme",
7070
"dependencies": {
71-
"@criipto/auth-js": "^3.7.2",
71+
"@criipto/auth-js": "^4.0.3",
7272
"classnames": "^2.5.1",
7373
"jwt-decode": "^3.1.2",
7474
"qrcode": "^1.5.4",

src/components/AuthButtonGroup.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { createContext } from 'react';
1+
import React, { createContext, useState } from 'react';
22
import {
33
AuthMethodButtonContainer,
44
type AuthMethodButtonContainerProps,
@@ -10,10 +10,14 @@ import SEBankIDQrCode from './SEBankIDQRCode';
1010
interface AuthButtonGroupContextInterface {
1111
multiple: boolean;
1212
acrValues: string[];
13+
disabled: boolean;
14+
setDisabled: (disabled: boolean) => void;
1315
}
1416
const initialContext: AuthButtonGroupContextInterface = {
1517
multiple: false,
1618
acrValues: [],
19+
disabled: false,
20+
setDisabled: () => {},
1721
};
1822
export const AuthButtonGroupContext =
1923
createContext<AuthButtonGroupContextInterface>(initialContext);
@@ -36,10 +40,13 @@ export default function AuthButtonGroup(props: { children: React.ReactNode }) {
3640
}
3741
return [];
3842
});
43+
const [disabled, setDisabled] = useState(false);
3944

4045
const context: AuthButtonGroupContextInterface = {
4146
multiple: acrValues.length > 1,
4247
acrValues,
48+
disabled,
49+
setDisabled,
4350
};
4451
return (
4552
<AuthButtonGroupContext.Provider value={context}>

src/components/AuthMethodButton.tsx

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,10 @@ export function AuthMethodButtonComponent(props: AuthMethodButtonComponentProps)
5151
const standalone = !group.multiple || isSingle(props.acrValue, group.acrValues);
5252
const language = (props.language ?? 'en') as Language;
5353
const action = (props.action ?? 'login') as Action;
54+
55+
const disabled = (props.disabled || group.disabled) && !props.loading;
5456
const className = classNames(`criipto-eid-btn`, acrValueToClassName(acrValue), props.className, {
55-
'criipto-eid-btn--disabled': props.disabled,
57+
'criipto-eid-btn--disabled': disabled,
5658
'criipto-eid-btn--loading': props.loading,
5759
});
5860

@@ -74,17 +76,20 @@ export function AuthMethodButtonComponent(props: AuthMethodButtonComponentProps)
7476
</React.Fragment>
7577
);
7678

79+
const commonProps = {
80+
...props,
81+
className,
82+
disabled,
83+
};
7784
const button = href ? (
7885
<React.Fragment>
79-
<AnchorButton {...props} href={href} className={className} onClick={props.onClick}>
86+
<AnchorButton {...commonProps} href={href}>
8087
{inner}
8188
</AnchorButton>
8289
</React.Fragment>
8390
) : (
8491
<React.Fragment>
85-
<Button {...props} className={className} onClick={props.onClick}>
86-
{inner}
87-
</Button>
92+
<Button {...commonProps}>{inner}</Button>
8893
</React.Fragment>
8994
);
9095

@@ -108,37 +113,19 @@ export function AuthMethodButtonContainer(props: AuthMethodButtonContainerProps)
108113
const group = useContext(AuthButtonGroupContext);
109114
const standalone = !group.multiple || isSingle(props.acrValue, group.acrValues);
110115
const context = useContext(CriiptoVerifyContext);
111-
const { buildAuthorizeUrl } = context;
116+
const { initializePAR } = context;
112117
const language = (props.language ?? context.uiLocales ?? 'en') as Language;
113118
const action = (props.action ?? context.action ?? 'login') as Action;
114119
const className = `criipto-eid-btn ${acrValueToClassName(acrValue)}${
115120
props.className ? ` ${props.className}` : ''
116121
}`;
117122
const [backdrop, setBackdrop] = useState<React.ReactElement | null>(null);
118-
119-
const [href, setHref] = useState(props.href);
120-
const redirectUri = props.redirectUri || context.redirectUri;
121-
123+
const [loading, setLoading] = useState<boolean>(props.loading ?? false);
122124
useEffect(() => {
123-
if (props.href) return;
124-
125-
let isSubscribed = true;
126-
let loginHint: string | undefined = undefined;
125+
setLoading(props.loading ?? false);
126+
}, [props.loading]);
127127

128-
buildAuthorizeUrl({
129-
redirectUri,
130-
acrValues: acrValue,
131-
loginHint,
132-
})
133-
.then((href) => {
134-
if (isSubscribed) setHref(href);
135-
})
136-
.catch(console.error);
137-
138-
return () => {
139-
isSubscribed = false;
140-
};
141-
}, [props.href, buildAuthorizeUrl, acrValue, context.pkce, redirectUri]);
128+
const redirectUri = props.redirectUri || context.redirectUri;
142129

143130
const handleClick: React.MouseEventHandler = (event) => {
144131
if (props.href) return;
@@ -182,11 +169,30 @@ export function AuthMethodButtonContainer(props: AuthMethodButtonContainerProps)
182169
}
183170

184171
if (props.onClick) props.onClick(event);
172+
173+
if (!event.isDefaultPrevented()) {
174+
setLoading(true);
175+
group.setDisabled(true);
176+
initializePAR({
177+
redirectUri,
178+
acrValues: acrValue,
179+
loginHint: '',
180+
})
181+
.then((authorizeUrl) => {
182+
window.location.href = authorizeUrl.toString();
183+
})
184+
.catch((error) => {
185+
setLoading(false);
186+
group.setDisabled(false);
187+
context.handleResponse(error);
188+
});
189+
}
185190
};
186191

187192
const { title, subtitle } = acrValueToTitle(language, acrValue, {
188193
disambiguate: isAmbiguous(acrValue, group.acrValues),
189194
});
195+
190196
const contents = props.children ?? (
191197
<React.Fragment>
192198
{stringifyAction(language, action) ? `${stringifyAction(language, action)} ` : ''}
@@ -199,9 +205,9 @@ export function AuthMethodButtonContainer(props: AuthMethodButtonContainerProps)
199205
<React.Fragment>
200206
<AuthMethodButtonComponent
201207
{...props}
202-
href={href}
203208
className={className}
204209
onClick={handleClick}
210+
loading={loading}
205211
/>
206212
{backdrop}
207213
</React.Fragment>
@@ -215,6 +221,7 @@ export function AuthMethodButtonContainer(props: AuthMethodButtonContainerProps)
215221
className={className}
216222
userAgent={props.userAgent}
217223
logo={<AuthMethodButtonLogo acrValue={acrValue} logo={props.logo} />}
224+
disabled={props.disabled}
218225
>
219226
<span>{contents}</span>
220227
</SEBankIDSameDeviceButton>

src/components/QRCode.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
import { OAuth2Error, savePKCEState, UserCancelledError } from '@criipto/auth-js';
2-
import type CriiptoConfiguration from '@criipto/auth-js/dist/CriiptoConfiguration';
1+
import {
2+
OAuth2Error,
3+
savePKCEState,
4+
UserCancelledError,
5+
type CriiptoConfiguration,
6+
} from '@criipto/auth-js';
7+
38
import React, {
49
useContext,
510
useRef,

src/components/SEBankIDSameDeviceButton.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import ForegroundStrategy from './SEBankIDSameDeviceButton/Foreground';
88
import ReloadStrategy from './SEBankIDSameDeviceButton/Reload';
99

1010
import { autoHydratedState, type Links } from './SEBankIDSameDeviceButton/shared';
11+
import { AuthButtonGroupContext } from './AuthButtonGroup';
12+
import classNames from 'classnames';
1113
import { Spinner } from './Spinner/Spinner';
1214

1315
interface Props {
@@ -17,6 +19,7 @@ interface Props {
1719
fallback: React.ReactElement;
1820
redirectUri?: string;
1921
userAgent?: string;
22+
disabled?: boolean;
2023
}
2124

2225
function searchParamsToPOJO(input: URLSearchParams) {
@@ -79,6 +82,8 @@ async function fetchComplete(completeUrl: string) {
7982
}
8083
export default function SEBankIDSameDeviceButton(props: Props) {
8184
const { loginHint } = useContext(CriiptoVerifyContext);
85+
const group = useContext(AuthButtonGroupContext);
86+
8287
const rawUserAgent = typeof navigator !== 'undefined' ? navigator.userAgent : props.userAgent;
8388
const strategy = useMemo(
8489
() => determineStrategy(rawUserAgent, loginHint),
@@ -101,6 +106,8 @@ export default function SEBankIDSameDeviceButton(props: Props) {
101106
} = useContext(CriiptoVerifyContext);
102107
const redirectUri = props.redirectUri || defaultRedirectURi;
103108

109+
const disabled = props.disabled || group.disabled || initiated;
110+
104111
const reset = () => {
105112
setPKCE(undefined);
106113
setLinks(null);
@@ -212,12 +219,14 @@ export default function SEBankIDSameDeviceButton(props: Props) {
212219

213220
// Track when the button is clicked to stop refreshing URL
214221
const handleInitiate = () => {
222+
group.setDisabled(true);
215223
handleLog('Initiated');
216224
setInitiated(true);
217225
setError(null);
218226
};
219227

220228
const handleError = async (error: string) => {
229+
group.setDisabled(false);
221230
setInitiated(false);
222231
setError(error);
223232

@@ -237,7 +246,7 @@ export default function SEBankIDSameDeviceButton(props: Props) {
237246

238247
const element = href ? (
239248
<a
240-
className={`${props.className} ${initiated ? 'criipto-eid-btn--disabled' : ''}`}
249+
className={classNames(props.className, { 'criipto-eid-btn--disabled': disabled })}
241250
href={href}
242251
onClick={handleInitiate}
243252
>

0 commit comments

Comments
 (0)