React SPA (PKCE)
Add IdRa sign-in to a React single-page app with oidc-client-ts and PKCE, no client secret.
A browser-only app: a public client using the authorization code flow with PKCE, built with Vite and oidc-client-ts. It has no secret. IdRa's token, UserInfo, JWKS and discovery endpoints answer cross-origin requests from any origin, without cookies.
Sample repository (being opened soon): IdRaHQ/idra-sample-react-spa. It has this quickstart's code plus a protected page.
Register the app
Use one app for local development and production. Open your organization's admin console at https://<your-org>.idra.team/admin (or /admin on your own IdRa). Go to Applications → Create App Integration, choose OIDC - OpenID Connect, and set:
- Application type: Single-Page Application
- App integration name: any name
- Sign-in redirect URIs:
http://localhost:5173/callback
Leave Assign this app to me so I can test it on, then select Create.
The app's Sign On tab opens:
- Under Connect your app, copy the Issuer and Client ID. The
.envsnippet there has the same values, namedOIDC_*; this quickstart names themVITE_IDRA_*. - Add
http://localhost:5173/to Sign-out redirect URIs and select Save.
To let other people sign in, assign them or their groups on Assignments.
Local redirects and production
http redirects work on localhost, 127.0.0.1 or [::1], with a port, and match exactly. PKCE stays required while the app has one. For production, add your https URIs (such as https://app.example.com/callback and https://app.example.com/) to the same app, then remove the local ones.
Configure
VITE_IDRA_ISSUER=https://<your-org>.idra.team/api/auth
VITE_IDRA_CLIENT_ID=<Client ID>Install
npm create vite@latest idra-react -- --template react
cd idra-react
npm install oidc-client-tsCode
Replace the generated file:
import { UserManager, WebStorageStateStore } from 'oidc-client-ts';
import { createRoot } from 'react-dom/client';
const auth = new UserManager({
authority: import.meta.env.VITE_IDRA_ISSUER,
client_id: import.meta.env.VITE_IDRA_CLIENT_ID,
redirect_uri: `${location.origin}/callback`,
post_logout_redirect_uri: `${location.origin}/`,
scope: 'openid profile email groups',
// The ID token has no email or name: this reads them from UserInfo.
loadUserInfo: true,
userStore: new WebStorageStateStore({ store: sessionStorage }),
});
// Before React renders, so the code is exchanged once, whatever React does with effects.
async function currentUser() {
if (location.pathname !== '/callback') return auth.getUser();
const user = await auth.signinCallback();
history.replaceState(null, '', '/');
return user;
}
function App({ user, error }) {
if (error) return <p role="alert">Sign-in failed: {error}</p>;
if (!user || user.expired)
return (
<main>
<h1>Signed out</h1>
<button onClick={() => auth.signinRedirect()}>Sign in with IdRa</button>
</main>
);
return (
<main>
<h1>Signed in</h1>
<pre>{JSON.stringify(user.profile, null, 2)}</pre>
<button onClick={() => auth.signoutRedirect()}>Sign out</button>
</main>
);
}
const root = createRoot(document.getElementById('root'));
currentUser().then(
(user) => root.render(<App user={user} />),
(e) => root.render(<App error={e.message} />),
);Your production host must serve index.html for /callback. Tokens stay in sessionStorage, where any script on the page can read them, so keep the page free of third-party scripts you don't trust.
Run
npm run devVite serves on port 5173, which must match the registered URI exactly.
Verify sign-in and sign-out
- Open
http://localhost:5173and select Sign in with IdRa. You land on IdRa's sign-in page. - Sign in, with MFA if your policies ask for it. You come back to a Signed in page showing your claims.
- Select Sign out. You return to Signed out. This also ends the IdRa session, so the next sign-in asks for your credentials again.
Reports → System Log in the admin console shows the sign-in.
What the app receives
The ID token doesn't carry email or name. Read them from UserInfo, as this sample does with loadUserInfo.
| Claim | ID token | UserInfo | Scope |
|---|---|---|---|
sub | Yes | Yes | openid |
preferred_username (the email), updated_at | Yes | Yes | profile |
name, given_name, family_name | No | Yes | profile |
email, email_verified | No | Yes | email |
groups (group names, always including Everyone) | Yes | Yes | groups |
Add offline_access to the scopes to receive a refresh token. Access tokens last an hour. See OpenID Connect for the rest.
Troubleshooting
| What you see | Cause | Fix |
|---|---|---|
Use https on a public host, or for local development http on localhost, 127.0.0.1 or [::1] with a port (…) | The redirect URI is http on another host or without a port, or https on a loopback host. | Use http://localhost:5173/callback locally and https on a public host in production. |
Sign-in error: invalid redirect uri | The app sent a redirect URI that isn't registered. Matching is exact, including port and path. | Make it match Sign-in redirect URIs on the Sign On tab. |
Access to … denied: You are not assigned to this application. | The person isn't assigned. | Assign them on Assignments. |
Access to … denied: This application is currently inactive. | The app is deactivated. | Activate it on the app's page. |
Access denied: This application is not registered with your organization. | Wrong client ID, or a client ID from another organization. | Copy the Client ID from Sign On. |
Access denied: Your organization's sign-on policy does not allow this access., URL has code=invalid_scope | The app asked for a scope that doesn't exist. | Use openid, profile, email, groups, offline_access or a scope defined under Security → API. |
pkce is required for this client | The authorize request has no PKCE challenge. | Send code_challenge with code_challenge_method=S256. IdRa accepts only S256. |
Forbidden (403): Invalid origin, code INVALID_ORIGIN | A cross-origin call to the token endpoint sent cookies. | Don't set credentials: 'include' on calls to IdRa. oidc-client-ts doesn't by default. |
invalid_grant: redirect_uri mismatch | The token request's redirect URI differs from the authorize request's. | Use the same value in both. |
invalid_grant: code verification failed | The PKCE verifier doesn't match the challenge. | Keep the verifier from the authorize step, per sign-in. |
404 on discovery | The issuer is missing /api/auth. | Use the Issuer from Sign On: https://<your-org>.idra.team/api/auth. |
Page says Logged out. The requested post-logout redirect was not registered. | The person was signed out, but the return URL isn't registered. | Add it to Sign-out redirect URIs. |
| Confirm logout page | No id_token_hint, or it's for a different session than the browser's. | Send the ID token as the hint. Without one, the page is the expected safeguard. |
Reports → System Log records refused access as app.access with the reason.