· IT Security · 6 min read
Secure Authentication and Authorization in SPAs: OpenID Connect and OAuth 2.0 with PKCE
A detailed guide to secure authentication and authorization strategies for Single-Page Applications (SPAs). OAuth 2.0 and OpenID Connect are combined with PKCE to ensure secure and efficient access control.
Introduction
Modern web applications, especially Single-Page Applications (SPAs), place unique demands on authentication and authorization mechanisms. Standards like OAuth 2.0 and OpenID Connect are increasingly recognized as best practices, providing a robust foundation for secure authentication processes.
What is OAuth 2.0?
OAuth 2.0 is an industry-standard protocol for authorization, allowing applications to securely access resources on behalf of a user without directly handling their credentials. Instead, applications receive an access token, which defines and limits the scope of access, offering a more secure approach particularly beneficial for dynamic web applications.
What is OpenID Connect?
OpenID Connect builds on OAuth 2.0, adding an additional authentication layer. While OAuth 2.0 is primarily for authorization, OpenID Connect enables secure and standardized authentication. This is achieved through an ID token that provides information about the user’s identity. For instance, an application that needs to securely verify not only access to specific resources but also the user’s identity, such as profile information or a unique user ID, would benefit from OpenID Connect.
Best Practice for SPAs: Authorization Code Flow with PKCE
For SPAs, the Authorization Code Flow with PKCEProof Key for Code ExchangeEnhances security when creating and using the access tokenis the recommended authentication flow. This method enhances security by introducing an additional verification layer between client and server. The previously recommended Implicit Flow for SPAs is now outdated due to security risks, such as exposing access tokens directly in the browser. The PKCE flow prevents this through a two-step token issuance, keeping access tokens securely stored in the backend.
Detailed Process
1. Initial Authentication Request to the Authorization Server
A Code VerifierA random, hard-to-predict string used once in the authentication process.and Code ChallengeThe Code Verifier is hashed with SHA-256 and base64-url-encoded to create a Code Challenge.The Code Challenge is used only once in the authentication process and does not need to be stored.are generated for PKCE, and the Code Challenge is sent to the Authorization Server.
function generateRandomString(length) {
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
return Array.from(
(_, byte) => ('0' + byte.toString(16)).slice(-2)
).join('');
}
async function generateCodeChallenge(codeVerifier) {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await window.crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
const codeVerifier = generateRandomString(64);
const codeChallenge = await generateCodeChallenge(codeVerifier);
The SPA initiates the authentication process by redirecting the user to the Authorization Server’s login page, including the generated challenge.
https://auth-server.com/authorize
?response_type=code
&client_id=your-client-id
&redirect_uri=https://your-app.com/callback
&scope=profile email
&state=xyz123
&code_challenge=generatedCodeChallenge
&code_challenge_method=S256
2. Receiving the Authorization Code and Requesting the Access Token
Upon successful authentication, the Authorization Server redirects the user to the SPA’s redirect_uri
with the Authorization Code. The SPA extracts the Authorization Code and sends it along with the Code Verifier in a secure POST request to the backend.
POST /token HTTP/1.1
Host: auth-server.com
Content-Type: application/x-www-form-urlencoded
client_id=your-client-id
&code=abcd1234
&redirect_uri=https://your-app.com/callback
&grant_type=authorization_code
&code_verifier=originalCodeVerifier
The backend securely stores the tokens and does not pass them to the SPA.
3. Creating a Short-Lived Session Token and Setting it in an HTTP-Only Cookie
The backend creates a short-lived session token and sets it as an HTTP-Only cookieHTTP-Only cookies cannot be accessed by JavaScript and are automatically sent with each request.that is used only over secure connections (HTTPS).
4. CSRF-Token für zusätzliche Sicherheit
Additionally, the backend generates a CSRF tokenThe CSRF token prevents cross-site request forgery attacksThe CSRF token is a random, hard-to-predict string (often between 32 and 128 characters long) generated for each session or user interaction.and passes it to the SPA, either in a non-HTTP-Only cookieThis cookie must be accessible by JavaScript so that it can be explicitly added to the request header.or as a <meta>
tag. The SPA includes the CSRF token in every request header, allowing the backend to verify requests.
5. Authenticated Requests from the SPA to the Backend
The SPA uses the session token for authorized requests to the backend. The HTTP-Only cookie with the session token is automatically sent with each request. The CSRF token is also sent and verified by the backend.
6. Token Refresh on Access Token Expiry
The backend manages access token renewal using the refresh token when the access token expires. If the session or access token expires, the backend automatically requests a new access token and updates the session token.
Security Benefits
- Token Management in the Backend: The access token and refresh token are stored in the backend, protecting them from frontend attacks.
- Protection via HTTP-Only Cookie: The session token in an HTTP-Only cookie is secure from JavaScript and XSS attacks.
- CSRF Protection: The CSRF token prevents cross-site request forgery attacks.
- Short-Lived Session Token: The short lifespan of the session token limits access to the backend in case of compromise.
Threat Modeling and Security Risks without PKCE
Without the PKCE mechanism, attackers could intercept the Authorization Code via “Authorization Code Injection” or “Code Interception” attacks to illegitimately access sensitive data. By introducing an additional validation layer between client and authorization server, PKCE ensures that only the initially authorized client has access. Generating a unique code verifier and hashing it to a code challenge makes PKCE a reliable solution for SPAs and enhances backend security.
Best Practices for Implementation
A careful implementation of the PKCE flow requires adherence to specific security requirements:
- Secure Storage of the Code Verifier: The code verifier should be securely stored throughout the authentication process and discarded afterward to prevent unauthorized access.
- Use of Secure Connections: All communication between the SPA, backend, and authorization server should occur over HTTPS to prevent eavesdropping attacks.
- Restrictions for Redirect URIs: Only predefined and trusted redirect URIs should be allowed in the OAuth configuration to prevent possible redirection to malicious domains.
Following these best practices minimizes the risk of security vulnerabilities and builds a stable authentication architecture.
Additional Security Measures
In addition to PKCE implementation, further security measures can be employed:
- Content Security Policy (CSP): Setting a CSP restricts sources for scripts and resources, preventing the execution of potentially harmful content.
- Subresource Integrity (SRI): SRI hashes ensure the integrity of external resources, allowing content to load only if it matches the specified hash, which protects against tampering by malicious third parties.
- Secure Storage: Any token storage on the client (if required) should use secure web APIs like
sessionStorage
or secure cookies and be strictly secured with httpOnly and SameSite attributes.
These additional security measures complement the PKCE-based authorization code flow and ensure that the SPA is optimally protected against attacks.