@pawells/react-auth
v3.0.0
Published
Keycloak Authorization Code Flow with PKCE authentication for React SPAs
Maintainers
Readme
React Authentication Library
Description
@pawells/react-auth is a Keycloak Authorization Code + PKCE authentication library for React SPAs, built on oidc-client-ts. It provides:
- PKCE (S256) enforcement on every sign-in flow — no additional configuration required
- Automatic silent token renewal via refresh token, wired to the
oidc-client-tsevent system - Configurable session storage:
sessionStorage(default, cleared on tab close, per-tab isolated) orlocalStorage - Popup authentication flow for sign-in without a full-page redirect
- An Axios hook (
useAuthAxios) and an Apollo Link factory (createAuthApolloLink) for authenticated API and GraphQL requests
Requirements
- Node.js
>=22 - react
>=19.0.0(required peer) - react-dom
>=19.0.0(required peer) - @apollo/client
>=4.0.0(optional peer — required only forcreateAuthApolloLink) - axios
>=1.0.0(optional peer — required only foruseAuthAxios)
Installation
npm install @pawells/react-authTo use the Axios integration:
npm install axiosTo use the Apollo Link integration:
npm install @apollo/clientQuick Start
Place AuthProvider inside your router so that onSigninCallback can call useNavigate:
import { BrowserRouter, useNavigate } from 'react-router-dom';
import { AuthProvider } from '@pawells/react-auth';
import App from './App';
function AppWithAuth() {
const navigate = useNavigate();
return (
<AuthProvider
authority="https://keycloak.example.com/realms/my-realm"
client_id="my-spa-client"
redirect_uri={`${window.location.origin}/auth/callback`}
post_logout_redirect_uri={window.location.origin}
onSigninCallback={() => navigate('/')}
>
<App />
</AuthProvider>
);
}
export default function Root() {
return (
<BrowserRouter>
<AppWithAuth />
</BrowserRouter>
);
}AuthProvider handles the redirect callback automatically on mount. When the browser lands on redirect_uri with a code query parameter, the provider exchanges the code, stores the session, and calls onSigninCallback — no extra route or component is required.
API Reference
Components
AuthProvider
Provides Keycloak Authorization Code + PKCE authentication to the component tree. Silent token renewal via refresh token is enabled automatically. The storageType prop is read only on initial render; changes after mount are ignored.
Props (AuthProviderProps — extends KeycloakAuthConfig and accepts children):
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| authority | string | Yes | — | Keycloak realm base URL. Format: https://{host}/realms/{realm} (Keycloak 17+) |
| client_id | string | Yes | — | Client ID of a public (non-confidential) Keycloak client |
| redirect_uri | string | Yes | — | URI to redirect to after a successful login; must be registered in Keycloak |
| post_logout_redirect_uri | string | No | — | URI to redirect to after logout; must be registered in Keycloak's "Valid post logout redirect URIs" |
| popup_redirect_uri | string | No | — | URI for the popup authentication callback window; required for loginWithPopup() and must be registered in Keycloak |
| scope | string | No | 'openid profile email' | OAuth2/OIDC scopes to request. Add 'offline_access' for long-lived refresh tokens |
| storageType | StorageType | No | 'sessionStorage' | Where to persist the OIDC user session. See StorageType |
| onSigninCallback | (user: User) => void | No | — | Invoked after a successful signin redirect callback. Use to navigate the user to their intended destination |
| children | React.ReactNode | Yes | — | Component subtree that receives the auth context |
Hooks
useAuth(): AuthContextValue
Returns the current auth context value from the nearest AuthProvider. Provides access to authentication state, login/logout methods, and token retrieval.
Throws a BaseError when called outside an AuthProvider.
function Profile() {
const { user, logout, isAuthenticated } = useAuth();
if (!isAuthenticated) return <div>Not logged in</div>;
return <button onClick={logout}>{user?.profile.name}</button>;
}The returned AuthContextValue exposes all AuthState fields plus five methods:
| Member | Type | Description |
|---|---|---|
| isAuthenticated | boolean | Whether the user holds a valid, non-expired access token |
| isLoading | boolean | Whether an auth operation is in progress (hydration, callback, or silent renew) |
| user | User \| null | The authenticated OIDC user object, or null when not authenticated |
| error | Error \| null | The most recent auth error, or null if none |
| login() | () => Promise<void> | Initiates the Keycloak Authorization Code + PKCE redirect flow |
| loginWithPopup() | () => Promise<void> | Opens a Keycloak login popup without navigating the main page. Requires popup_redirect_uri. Throws if the popup is blocked |
| logout() | () => Promise<void> | Redirects to the Keycloak end_session_endpoint and clears the local session |
| getAccessToken() | () => Promise<string \| null> | Returns the current access token, triggering silent renewal if expired. Returns null if not authenticated or if renewal fails |
| clearSession() | () => Promise<void> | Removes the user from storage and resets auth state without redirecting to Keycloak |
useAuthAxios(instance?: AxiosInstance): AxiosInstance
Returns an Axios instance with a request interceptor that automatically attaches a valid Keycloak Bearer token to same-origin requests. The token is attached only when the resolved request URL has the same origin as window.location; cross-origin requests do not receive an Authorization header (CWE-319 mitigation). If the access token is expired, silent renewal via the refresh token is attempted before the request is dispatched. If the user is not authenticated, the request proceeds without an Authorization header.
Pass an existing Axios instance to attach the interceptor to it; omit the argument for a new axios.create() instance. The returned instance reference is stable across re-renders unless the passed instance reference changes.
// New instance (default)
function MediaService() {
const api = useAuthAxios();
const fetchItems = () => api.get('/api/items');
}
// Shared instance
const sharedAxios = axios.create({ baseURL: 'https://api.example.com' });
function MediaService() {
const api = useAuthAxios(sharedAxios);
}Note: If multiple components call
useAuthAxioswith the same sharedAxiosInstance, each call registers a separate request interceptor. Pass a dedicated instance per hook invocation to avoid duplicate interceptors.
Integrations
createAuthApolloLink(getAccessToken: AuthContextValue['getAccessToken']): ApolloLink
Creates an Apollo Link that prepends a valid Keycloak Bearer token to the Authorization header of same-origin GraphQL operations. The token is attached only when the GraphQL endpoint URI has the same origin as window.location; cross-origin requests do not receive an Authorization header (CWE-319 mitigation). Silent token renewal is triggered transparently when the access token is expired. If getAccessToken throws, the error is propagated to the GraphQL operation's error handler. If the subscription is cancelled before the token resolves, no error is emitted.
import { useMemo } from 'react';
import { ApolloClient, ApolloProvider, HttpLink, InMemoryCache } from '@apollo/client';
import { createAuthApolloLink, useAuth } from '@pawells/react-auth';
function ApolloSetup({ children }: { children: React.ReactNode }) {
const { getAccessToken } = useAuth();
const client = useMemo(() => {
const authLink = createAuthApolloLink(getAccessToken);
const httpLink = new HttpLink({ uri: '/graphql' });
return new ApolloClient({ link: authLink.concat(httpLink), cache: new InMemoryCache() });
}, [getAccessToken]);
return <ApolloProvider client={client}>{children}</ApolloProvider>;
}Sub-Path Exports
For convenience, you can import from sub-paths to reduce bundle size when using only specific modules:
// Import from specific sub-paths
import { AuthProvider, useAuth } from '@pawells/react-auth/context';
import { useAuthAxios } from '@pawells/react-auth/hooks';
import { createAuthApolloLink } from '@pawells/react-auth/integrations';
import { parseJwt, isTokenExpired } from '@pawells/react-auth/utils';Utilities
parseJwt<T extends Record<string, unknown>>(token: string): T | null
Decodes a JWT payload without verifying the signature. Intended for reading client-visible claims only — signature verification must always be performed server-side. Returns null if the token is malformed, does not have exactly three parts, or the payload cannot be parsed as a plain object.
const payload = parseJwt<{ sub: string; exp: number }>(token);
if (!payload) throw new Error('Invalid token');
console.log(payload.sub); // typed as stringisTokenExpired(token: string, clockSkewSeconds?: number): boolean
Returns true if a JWT is expired or malformed. An optional clockSkewSeconds buffer adjusts the validity window: a token with exp = T is considered expired only when now > T + clockSkewSeconds. Positive values extend the validity window (token treated as valid for up to that many seconds past its stated expiry); negative values make the token expire earlier (stricter validation). Default is 0 (no buffer).
Types
KeycloakAuthConfig
Configuration interface for AuthProvider. Extends oidc-client-ts UserManagerSettings (omitting userStore and stateStore, which are managed internally) with Keycloak-specific required fields and the storageType option.
AuthContextValue
Value exposed by the auth context and returned by useAuth(). Extends AuthState with five methods: login(), loginWithPopup(), logout(), getAccessToken(), and clearSession().
AuthState
Snapshot of the current authentication state. Fields: isAuthenticated: boolean, isLoading: boolean, user: User | null, error: Error | null.
AuthProviderProps
Props accepted by AuthProvider. Extends KeycloakAuthConfig with children: React.ReactNode.
StorageType
'sessionStorage' | 'localStorage' — controls where the OIDC user session is persisted.
'sessionStorage'(default) — cleared when the tab closes; isolated per tab.'localStorage'— survives page reloads; shared across same-origin tabs. Use with caution in XSS-prone environments; a security warning is logged to the console when this option is active.
License
MIT — See LICENSE for details.
