Add Auth to a React App
This guide walks through adding Aero2 authentication to your application using the Authorization Code flow with PKCE.
Prerequisites
- Your application's Aero2 URL (e.g.,
https://your-app.aero2.dev) - Admin access to register an OAuth client
1. Register an OAuth Client
First, create an OAuth client for your application:
curl -X POST https://your-app.aero2.dev/api/clients \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{
"name": "My Application",
"redirect_uris": ["https://myapp.com/callback"]
}'Save the client_id and client_secret from the response. The secret is only shown once.
2. Implement the Login Flow
Generate PKCE values and redirect
// Generate PKCE
const array = new Uint8Array(32);
crypto.getRandomValues(array);
const codeVerifier = base64UrlEncode(array);
const hash = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(codeVerifier)
);
const codeChallenge = base64UrlEncode(hash);
// Store code_verifier in session (you'll need it later)
sessionStorage.setItem('code_verifier', codeVerifier);
// Generate state for CSRF protection
const state = crypto.randomUUID();
sessionStorage.setItem('oauth_state', state);
// Redirect to Aero2
const params = new URLSearchParams({
client_id: 'your-client-id',
redirect_uri: 'https://myapp.com/callback',
response_type: 'code',
scope: 'openid profile email',
state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
});
window.location.href =
`https://your-app.aero2.dev/oauth2/authorize?${params}`;Handle the callback
// On your /callback page
const url = new URL(window.location.href);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
// Verify state matches
const savedState = sessionStorage.getItem('oauth_state');
if (state !== savedState) {
throw new Error('State mismatch — possible CSRF attack');
}
// Exchange code for tokens (do this server-side!)
const codeVerifier = sessionStorage.getItem('code_verifier');
const response = await fetch('https://your-app.aero2.dev/oauth2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: 'https://myapp.com/callback',
client_id: 'your-client-id',
client_secret: 'your-client-secret',
code_verifier: codeVerifier,
}),
});
const tokens = await response.json();
// tokens.access_token, tokens.id_token, tokens.refresh_tokenFetch user info
const userResponse = await fetch(
'https://your-app.aero2.dev/oauth2/userinfo',
{
headers: {
Authorization: `Bearer ${tokens.access_token}`,
},
}
);
const user = await userResponse.json();
// { sub: "...", name: "Jane Doe", email: "jane@example.com" }Refresh tokens when expired
const refreshResponse = await fetch(
'https://your-app.aero2.dev/oauth2/token',
{
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: tokens.refresh_token,
client_id: 'your-client-id',
client_secret: 'your-client-secret',
}),
}
);
const newTokens = await refreshResponse.json();
// Store the new refresh_token — the old one is invalidated3. Verify Tokens
For server-side token verification, use the JWKS endpoint:
jose
import * as jose from 'jose';
const JWKS = jose.createRemoteJWKSet(
new URL('https://your-app.aero2.dev/oauth2/jwks.json')
);
const { payload } = await jose.jwtVerify(accessToken, JWKS, {
issuer: 'https://your-app.aero2.dev',
audience: 'your-client-id',
});Security Checklist
- Always use PKCE with S256
- Verify the
stateparameter on callback - Exchange codes server-side (never expose
client_secretin the browser) - Store refresh tokens securely
- Always save the new refresh token after refresh (rotation is enforced)
- Verify token signatures using JWKS
- Check
issandaudclaims