Securing Single Page Applications (SPAs) with PhenixID Authentication Services (PAS)
Overview
Single Page Applications (SPAs) are popular for their seamless user experience but present unique challenges in authentication and authorization due to their client-side nature. PhenixID Authentication Services (PAS) provides robust support for securing SPAs through OpenID Connect (OIDC) and OAuth 2.0, offering secure, scalable, and user-friendly ways to manage authentication and authorization. PAS supports security features like Proof Key for Code Exchange (PKCE), CORS management via reverse proxy, token expiration and refresh handling, and secure token storage guidance—ensuring strong protection for SPA interactions with PAS.
This guide outlines best practices for securing SPAs with PAS, including implementing the recommended OIDC flow with PKCE, configuring CORS via a reverse proxy, securely handling tokens, and managing sessions effectively.
Recommended OIDC Flow for SPAs: Authorization Code Flow with PKCE
The Authorization Code Flow with PKCE is the recommended OIDC flow for SPAs, as it provides an extra layer of security by preventing unauthorized parties from intercepting the authorization code. Since SPAs cannot securely store a client secret, PKCE (Proof Key for Code Exchange) is essential to verify the authenticity of the authorization code exchange without needing a secret.
Why Use PKCE with SPAs?
PKCE was designed specifically for public clients like SPAs, which cannot safely store secrets. By requiring a dynamically generated code_challenge and code_verifier, PKCE ensures that only the SPA that requested the code can complete the token exchange. PAS supports PKCE to secure the Authorization Code Flow, protecting SPAs from interception attacks.
Step-by-Step Implementation of PKCE in an SPA with PAS
Here’s an example implementation of the Authorization Code Flow with PKCE in an SPA using PAS.
Generate a Code Verifier and Code Challenge
Before redirecting the user to authenticate with PAS, the SPA generates a code_verifier (a random string) and a code_challenge (a hashed version of the code_verifier).
JavaScript example:
// Generate code_verifier
const codeVerifier = generateRandomString(); // Use a secure random generator
// Create code_challenge from code_verifier
const codeChallenge = await sha256(codeVerifier); // SHA-256 hash the code_verifier
// Utility Functions
function generateRandomString() {
const array = new Uint32Array(32);
window.crypto.getRandomValues(array);
return Array.from(array, byte => ('0' + (byte & 0xff).toString(16)).slice(-2)).join('');
}
async function sha256(plain) {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(new Uint8Array(hash));
}
function base64UrlEncode(array) {
return btoa(String.fromCharCode.apply(null, array))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
Redirect the User to the Authorization Endpoint
Using the generated code_challenge, the SPA constructs the authorization URL and redirects the user to PAS’s authorization endpoint.
https://pas.example.com/authentication/oidc/t1/login
?response_type=code
&client_id=your_client_id
&redirect_uri=https://your-spa.com/callback
&scope=openid profile
&code_challenge=your_code_challenge
&code_challenge_method=S256
&state=optional_state_value
Authorization Parameters:
response_type=code: Specifies the Authorization Code Flow.code_challengeandcode_challenge_method=S256: These are PKCE parameters to secure the code exchange.redirect_uri: The URL where PAS will send the user after authorization.
Step 3: Handle the Authorization Response
After successful authentication, PAS redirects the user to the specified redirect_uri, including the authorization code as a query parameter.
https://your-spa.com/callback?code=authorization_code_value&state=optional_state_value
The SPA then extracts the authorization code from the URL to use in the token exchange.
Exchange the Authorization Code for Tokens
Using the original code_verifier, the SPA sends a POST request to PAS’s token endpoint to exchange the authorization code for an ID token and an access token.
POST https://pas.example.com/authentication/oidc/t1/token
Content-Type: application/x-www-form-urlencoded
client_id=your_client_id
&grant_type=authorization_code
&code=authorization_code_value
&redirect_uri=https://your-spa.com/callback
&code_verifier=your_code_verifier
If the code_verifier matches the code_challenge in PAS, the server will respond with the tokens.
Securely Store Tokens in the SPA
SPAs should avoid storing tokens in local storage or session storage, as these can be vulnerable to cross-site scripting (XSS) attacks. Instead, consider storing tokens in JavaScript memory and managing user sessions using secure HTTP-only cookies if necessary. PAS supports both opaque and JWT access tokens; JWT tokens can be validated directly in the client if necessary, while opaque tokens require a secure backend to perform introspection.
Configuring CORS for PAS via Reverse Proxy
Because CORS settings are managed through a reverse proxy, they need to be configured properly to ensure that only trusted origins can interact with PAS. This setup secures PAS resources while allowing authorized SPAs to access authentication and token endpoints.
Key CORS Settings
- Allowed Origins: Specify trusted domains that can access PAS (e.g.,
https://your-spa.com). - Allowed Methods: Limit HTTP methods to only those required by the SPA, such as
GETandPOST. - Allowed Headers: Define which headers are permitted, typically
AuthorizationandContent-Type. - Allow Credentials: If the SPA uses cookies for session management, allow credentials to be sent with requests.
- Max Age: Set a reasonable cache duration for preflight responses to improve performance.
Example CORS Configuration for NGINX Reverse Proxy
Here’s an example of configuring CORS in an NGINX reverse proxy, assuming the SPA is hosted at https://your-spa.com and the PAS instance is at https://pas.example.com.
server {
listen 443 ssl;
server_name pas.example.com;
location / {
# Define allowed origins for SPA access
if ($http_origin ~* "^https://your-spa\.com$") {
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 3600;
}
# Handle preflight requests
if ($request_method = OPTIONS ) {
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 3600;
return 204;
}
# Proxy to PAS backend
proxy_pass https://pas_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Explanation of Key Settings:
- Access-Control-Allow-Origin: Permits access from
https://your-spa.com. - Access-Control-Allow-Methods: Limits methods to
GET,POST, andOPTIONS. - Access-Control-Allow-Headers: Allows
AuthorizationandContent-Typeheaders. - Access-Control-Allow-Credentials: Enables cookies if necessary for secure session management.
- Access-Control-Max-Age: Sets caching duration for preflight responses to 3600 seconds.
Handling Preflight Requests
The reverse proxy responds to OPTIONS preflight requests directly, allowing the browser to verify the CORS policy without burdening PAS. If the request origin and headers match the configuration, the browser proceeds with the actual request.
Token Expiration and Refresh Handling
SPAs often need token refresh strategies to avoid session interruptions. PAS supports refresh tokens that SPAs can use to obtain new access tokens without re-authentication.
Security Best Practices for SPAs with PAS
- Use PKCE: Always use PKCE with Authorization Code Flow for secure code exchange.
- Store Tokens Securely: Use in-memory storage or secure cookies to manage tokens.
- Configure CORS Securely: Only allow trusted origins to access PAS endpoints.
- Enforce HTTPS: Always use HTTPS to protect data in transit.
Conclusion
By configuring PKCE and CORS securely, PhenixID Authentication Services (PAS) enables Single Page Applications (SPAs) to implement OIDC and OAuth 2.0 securely. PAS’s support for PKCE, reverse proxy CORS configuration, and secure token handling provide a strong foundation for secure, scalable authentication in SPAs.