From Implicit to Authorization Code with PKCE & BFF
Introduction
OpenID Connect flows evolved from Implicit to PKCE with BFF, and each step closed specific security holes. This article walks through each flow and its weaknesses.
The flows covered:
Frontend-only Implementations:
- Implicit Flow
- Authorization Code Flow with Proof Key for Code Exchange (PKCE)
Frontend and Backend Implementations:
- Authorization Code Flow with Token-Mediating Backend
- Authorization Code Flow with Token-Mediating Backend and PKCE
- Authorization Code Flow with PKCE and Backend for Frontend (BFF)
For convenience, some information will be repeated in each section. This way, you can read about the specific flows that interest you without needing to read the entire article.
Vulnerabilities present in authentication processes
Modern web applications can be attacked through several vectors:
Common attack vectors
-
XSS (Cross-Site Scripting) attacks involve injecting malicious JavaScript code into an application through input fields, URL parameters, or other entry points. This happens when input data isn't properly escaped or sanitized.
-
CSRF (Cross-Site Request Forgery) occurs when malicious scripts or browser extensions perform actions on behalf of the user without their consent. These scripts can use the user's session and credentials to carry out unauthorized actions, emulating legitimate user behavior.
-
Dependency Compromise occurs when modern web applications rely on numerous external libraries and resources. If these dependencies are compromised, they can introduce vulnerabilities into the application.
-
Browser extension vulnerabilities arise when browser extensions, which have access to web application code and user data, are compromised or malicious. These compromised extensions can introduce vulnerabilities, allowing attackers to steal sensitive information or inject malicious code into web applications.
These attack vectors allow an attacker to run arbitrary code in the user's application execution context. This may lead to the following threats:
Specific threats
-
A Code Interception Attack occurs when an attacker intercepts authorization codes exposed in the URL during the redirect, resulting in unauthorized access to the user's resources.
-
Persistent Token Theft is a risk when tokens are stored within the browser's storage, making them susceptible to continuous theft (such as every 10 seconds) by malicious scripts.
-
Acquisition and Extraction of New Tokens occurs when a session is active on the OpenID Provider side, and malicious JavaScript initiates a silent authentication process in a hidden iframe to obtain new access tokens without the user's knowledge.
-
Proxying Requests via the User's Browser occurs when malicious JavaScript exploits an authenticated session by simulating user actions within the application, sending unauthorized requests to the Protected Resource on behalf of the user.
Vulnerabilities specific to certain OpenID Connect implementations
-
Token Transmission via URL occurs when tokens are visible in the address bar, stored in browser history, logs, and passed via HTTP referrers, making them vulnerable to interception and unauthorized use. This is especially relevant to Implicit Flow in frontend-only implementations.
-
Lack of Refresh Token Support occurs when there are no refresh tokens: the client must request new tokens frequently, which increases the chance of token leakage and misuse. This is especially relevant to Implicit Flow in frontend-only implementations.
Evolution of OpenID Connect flows
Description
The diagram shows how each successive flow removed vulnerabilities left by the previous one.
Initially, there were two different types of authorization flows in OAuth 2.0: the Authorization Code Flow and the Implicit Flow. The Authorization Code Flow was designed for scenarios where a backend server acted as a client, handling communication with the authorization server. On the other hand, the Implicit Flow was a simplified alternative intended mainly for frontend applications.
However, the Implicit Flow had several security vulnerabilities, such as token interception and theft. To close the code-interception hole, the Authorization Code Flow with Proof Key for Code Exchange (PKCE) was developed. PKCE addressed significant vulnerabilities, including code interception attacks.
To address more issues, the Authorization Code Flow with PKCE and a token-mediating backend was the next step. This approach involved both the frontend and the backend, reducing some vulnerabilities like token exposure. However, it still had some issues, such as Persistent Token Theft.
The latest advancement shown in the diagram is the Authorization Code Flow with both PKCE and BFF (Backend for Frontend). This approach combines the benefits of PKCE with additional backend protections, further mitigating vulnerabilities like persistent token theft.
Implicit Flow in a frontend-only implementation
Overview
-
In OpenID Connect, the End-User is the person using an application to access services. The Relying Party (RP) is the application itself, relying on the OpenID Provider for user authentication. These applications can be mobile apps, web apps, desktop apps, IoT devices, or gaming consoles. They act as clients, requesting tokens from the OpenID Provider to authenticate users and gain access to protected resources.
-
The OpenID Provider (OP) is an authorization server responsible for verifying the identity of the End-User. It authenticates the user and issues identity and access tokens to the Relying Party. For instance, in the diagram, the Abblix OIDC Server, a library from Abblix, certified by the OpenID Foundation, with source code on GitHub, is the OpenID Provider. This server ensures that users are who they claim to be and provides the necessary tokens to the applications that need to interact securely with protected resources.
-
The Resource Server stores and manages protected information, hosting sensitive data like personal details, financial information, and private documents. In OpenID Connect, the Resource Server uses tokens issued by the OpenID Provider to decide whether to grant or deny access. The tokens verify the user's identity and permissions, ensuring only authorized users can access the sensitive data.
Step-by-step process
Step 1. User redirects to OpenID Provider
When a user tries to access a protected resource, the application checks their authentication status. If the user is not logged in, the application will redirect their browser to the OpenID Provider to start the authentication process. This request includes parameters such as client_id, redirect_uri, response_type=id_token token (indicating that the client expects both an ID token and an access token in the response, characteristic of the Implicit Flow), scope, and state.
The parameters in this request serve specific purposes:
-
client_id: This identifies the client application making the request. For example,client_id=myapp-12345. -
redirect_uri: This specifies the URL to which the OpenID Provider should redirect the user after authentication. For example,redirect_uri=https://myapp.com/callback. -
response_type=id_token token: This indicates that the client expects both an ID token and an access token in the response.-
ID Token: An ID token is a JSON Web Token (JWT) that contains information about the authenticated user. It typically includes claims such as the user's identity, authentication time, and other relevant data. For example, it may contain the user's name, email address, and a unique identifier.
-
Access Token: An access token is a credential used to access protected resources. It is also usually a JWT and includes information about the permissions granted to the client application. This token is sent along with requests to the Resource Server to authorize access to specific resources. An access token is a bearer token, meaning it is used by clients as-is, without parsing its contents or performing validation.
-
-
scope: This defines the access privileges requested, such as access to the user's profile information. For example,scope=openid profile email. -
state: This parameter helps prevent CSRF attacks by maintaining state between the request and the callback, ensuring the response matches the original request made by the client. For example,state=abc123.
Here are examples of what the parameters might look like in a real request:
client_id=myapp-12345redirect_uri=https://myapp.com/callbackresponse_type=id_token tokenscope=openid profile emailstate=abc123
By including these parameters, the client application ensures that the OpenID Provider can correctly identify the client, know where to send the response, understand what types of tokens are being requested, define the access privileges, and secure communication against CSRF attacks.
Step 2. Show login form
The OpenID Provider displays a login form to the user. This form prompts the user to enter their login credentials (typically a username and password).
Step 3. User enters login and password and submits the form
The user enters their login credentials into the form, including their username and password, and submits it. The OpenID Provider processes these credentials to authenticate the user.
Step 4. Access token response
Upon successful authentication, the OpenID Provider redirects the user's browser back to the specified redirect_uri with an access_token and an ID token included in the URL fragment. This response contains the tokens needed for the client (Relying Party) to authenticate the user and make authorized requests to protected resources.
After this step, once the access_token is received, several types of attacks can occur.
Step 5. Protected resource request
The client application (Relying Party) uses the access token to make requests to the Resource Server for access to protected resources. The access_token is sent as part of the request to prove that the client has permission to access the requested resources.
Step 6. Protected resource response
The Resource Server verifies the access token and, if valid, responds to the client's request by providing access to the protected data. This information is sent back to the client application for use by the user.
Summary of OpenID Connect Implicit Flow in a frontend-only implementation
The Implicit Flow in OpenID Connect involves redirecting the user to the OpenID Provider, submitting credentials, and receiving an access token and ID token in the redirect response, which are then used to access protected resources. However, the Implicit Flow has several security vulnerabilities:
- Persistent Token Theft: Tokens are present in the browser, and fresh tokens can be stolen continuously, for example, every 10 seconds.
- Acquisition and Extraction of New Tokens: The user is authenticated on the OpenID Provider side, and malicious JavaScript can initiate a silent request to the OpenID Provider in a hidden iframe and obtain its own unique access token.
- Proxying Requests via the User's Browser: Malicious JavaScript can "click buttons in the application" on behalf of the user, sending requests to the Protected Resource in the user's name.
- Token Transmission via URL: Tokens are visible in the address bar, stored in browser history, logs, and passed via HTTP referrers, making them vulnerable to interception and unauthorized use. This is particularly relevant to Implicit Flow in frontend-only implementations.
- Lack of Refresh Token Support: Without refresh tokens, frequent requests for new tokens are necessary, which increases the chances of token leakage and misuse. This is particularly relevant to Implicit Flow in frontend-only implementations.
To address these security concerns, more secure flows, such as the Authorization Code Flow with a backend, are recommended. If the application is frontend-only, many of these vulnerabilities can be mitigated by using the OpenID Connect Authorization Code Flow with PKCE. The next section shows how PKCE closes these holes.
Authorization Code Flow with PKCE in a frontend-only implementation
Overview
In OpenID Connect, the End-User is the person using an application to access services. The Relying Party (RP) is the application itself, relying on the OpenID Provider for user authentication. These applications can be mobile apps, web apps, desktop apps, IoT devices, or gaming consoles. They act as clients, requesting tokens from the OpenID Provider to authenticate users and gain access to protected resources.
The OpenID Provider (OP) is an authorization server responsible for verifying the identity of the End-User. It authenticates the user and issues identity and access tokens to the Relying Party. For instance, in the diagram, the Abblix OIDC Server, a library from Abblix, certified by the OpenID Foundation, with source code on GitHub, is the OpenID Provider. This server ensures that users are who they claim to be and provides the necessary tokens to the applications that need to interact securely with protected resources.
The Resource Server stores and manages protected information, hosting sensitive data like personal details, financial information, and private documents. In OpenID Connect, the Resource Server uses tokens issued by the OpenID Provider to decide whether to grant or deny access. The tokens verify the user's identity and permissions, ensuring only authorized users can access the sensitive data.
The Proof Key for Code Exchange (PKCE) is designed to prevent authorization code interception and injection attacks. It ensures that only the client that requested the authorization code can use it. PKCE allows public clients, such as mobile and single-page applications, to use the Authorization Code Flow securely.
Step-by-step process
Step 1. User redirects to OpenID Provider
When a user tries to access a protected resource, the application checks their authentication status. If the user is not logged in, the application will redirect their browser to the OpenID Provider to start the authentication process. This request includes parameters such as client_id, redirect_uri, response_type=code, scope, state, and, specifically for PKCE, code_challenge and code_challenge_method.
Before making the request, the client generates a high-entropy cryptographic random string known as the code_verifier. The code_challenge is then derived by hashing the code_verifier using the specified code_challenge_method. This code_challenge is sent with the initial authorization request, ensuring that the code_verifier is used later to prove that the true originator of the authorization request is asking for a token in exchange for the authorization code.
The parameters in this request serve specific purposes:
client_id: This identifies the client application making the request. For example,client_id=myapp-12345.redirect_uri: This specifies the URL to which the OpenID Provider should redirect the user after authentication. For example,redirect_uri=https://myapp.com/callback.response_type=code: This indicates that the client expects an authorization code in the response.scope: This defines the access privileges requested, such as access to the user's profile information. For example,scope=openid profile email.state: This parameter helps prevent CSRF attacks by maintaining state between the request and the callback, ensuring the response matches the original request made by the client. For example,state=abc123.code_challenge: This is a derived value used to ensure that the token exchange is secure.code_challenge_method: This specifies the method used to generate the code challenge. For example,code_challenge_method=S256.
Here are examples of what the parameters might look like in a real request:
client_id=myapp-12345redirect_uri=https://myapp.com/callbackresponse_type=codescope=openid profile emailstate=abc123code_challenge=abc123def456acd789code_challenge_method=S256
By including these parameters, the client application ensures that the OpenID Provider can correctly identify the client, know where to send the response, understand what types of tokens are being requested, define the access privileges, and secure the communication against attacks.
Step 2. Show login form
The OpenID Provider displays a login form to the user. This form prompts the user to enter their login credentials (typically a username and password).
Step 3. User enters login and password and submits the form
The user enters their login credentials into the form, including their username and password, and submits it. The OpenID Provider processes these credentials to authenticate the user.
Step 4. Authorization code response
Upon successful authentication, the OpenID Provider redirects the user's browser back to the specified redirect_uri with an authorization_code included in the URL. The authorization_code is a short-lived, one-time code (e.g., authorization_code=SplxlOBeZQQYbYS6WxSbIA) that the client exchanges for an access token. This ensures secure token issuance. This code is used by the client to request an access token from the OpenID Provider.
Step 5. Asynchronous access token request
The client application (Relying Party) sends an asynchronous request to the OpenID Provider's token endpoint to exchange the authorization code for an access token. This request includes the authorization_code, client_id, redirect_uri, and, specifically for PKCE, the original value of code_verifier.
Step 6. Access token response
The OpenID Provider validates the request and, if successful, returns an access_token and a refresh_token to the client application.
The access_token is a credential used to access protected resources. It typically has a short lifespan to minimize risk. Example: access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
The refresh_token is used to obtain a new access token without requiring the user to re-authenticate. Example: refresh_token=def502b0b22b3d8f67d1b7c2c1b5a6b2c7d1f7f8c8b9c4e2.
After this step, once the tokens are received, several types of attacks can occur.
Step 7. Async user info request
The client application sends an asynchronous request to the OpenID Provider's user info endpoint to retrieve the user's profile information, using the access_token.
Step 8. User info response
The OpenID Provider responds with the user's claims (e.g., name, email).
Step 9. Protected resource request
The client application (Relying Party) uses the access token to make requests to the Resource Server to access protected resources. The access_token is sent as part of the request to prove that the client has permission to access the requested resources.
Step 10. Protected resource response
The Resource Server verifies the access token and, if valid, responds to the client's request by providing access to the protected data. This information is sent back to the client application for use by the user.
Summary of OpenID Connect Authorization Code Flow with PKCE in a frontend-only implementation
The Authorization Code Flow with PKCE in OpenID Connect involves redirecting the user to the OpenID Provider, submitting credentials, receiving an authorization code in the redirect response, and then securely exchanging this code for an access token using PKCE. This flow ensures that the authorization code can only be used by the client that requested it, significantly reducing the risk of token interception.
Using PKCE mitigates several vulnerabilities:
- Code Interception Attack: PKCE ensures that the authorization code can only be exchanged for tokens by the client that requested it, preventing attackers from intercepting the code during the redirect.
- Token Transmission via URL: PKCE does not send tokens in the URL, reducing the risk of tokens being intercepted from the address bar, browser history, logs, and HTTP referrers.
- Lack of Refresh Token Support: PKCE supports the use of refresh tokens, allowing secure, long-term session maintenance without frequent requests for new tokens.
However, even with PKCE, some vulnerabilities remain:
- Persistent Token Theft: Tokens are present in the browser, and fresh tokens can be stolen continuously, for example, every 10 seconds.
- Acquisition and Extraction of New Tokens: The user is authenticated on the OpenID Provider side, and malicious JavaScript can initiate a silent request to the OpenID Provider in a hidden iframe and obtain its own unique access token.
- Proxying Requests via the User's Browser: Malicious JavaScript can "click buttons in the application" on behalf of the user, sending requests to the Protected Resource in the user's name.
To keep tokens out of the browser, the backend component should be used. The Authorization Code Flow with PKCE, combined with a backend, addresses many security concerns effectively, providing a more secure mechanism for token handling and transmission.
Authorization Code Flow with token-mediating backend
Overview
-
In OpenID Connect, the End-User is the person using an application to access services. These applications can include mobile apps, web apps, desktop apps, IoT devices, gaming applications, and smartwatches. The End-User interacts with these applications to access various services and resources.
-
The Relying Party (RP) is the backend component of the application that the End-User interacts with. The RP depends on the OpenID Provider for user authentication. It requests tokens from the OpenID Provider to authenticate users and gain access to protected resources. In our scheme, the backend is the RP, adding an extra layer of security in handling authentication and tokens.
-
The OpenID Provider (OP) is an authorization server responsible for verifying the identity of the End-User. It authenticates the user and issues identity and access tokens to the Relying Party. For instance, in the diagram, the Abblix OIDC Server, a library from Abblix, certified by the OpenID Foundation, with source code on GitHub, is the OpenID Provider. This server ensures that users are who they claim to be and provides the necessary tokens to the applications that need to interact securely with protected resources.
-
The Resource Server is where the protected information is stored and managed. It hosts sensitive data like personal details, financial information, and private documents. In OpenID Connect, the Resource Server uses the tokens issued by the OpenID Provider to decide whether to grant or deny access to this information. The tokens verify the user's identity and permissions, ensuring that only authorized users can access the sensitive data.
Step-by-step process
Step 1. User requests a protected resource
The user requests a protected resource, prompting the application to check the authentication status. Seeing that the user is not logged in, the application redirects the user's browser to the OpenID Provider to initiate the authentication process.
Step 2. Redirect to authentication
The Relying Party (RP) initiates a redirect of the user's browser to the OpenID Provider for authentication, using parameters such as client_id, redirect_uri, response_type=code, scope, and state.
client_id: This identifies the client application making the request. For example,client_id=myapp-12345.redirect_uri: This specifies the URL to which the OpenID Provider should redirect the user after authentication. For example,redirect_uri=https://myapp.com/callback.response_type=code: This indicates that the client expects an authorization code in the response.scope: This defines the access privileges requested, such as access to the user's profile information. For example,scope=openid profile email.state: This parameter helps prevent CSRF attacks by maintaining state between the request and the callback, ensuring the response matches the original request made by the client. For example,state=abc123.
Here are examples of what the parameters might look like in a real request:
client_id=myapp-12345redirect_uri=https://myapp.com/callbackresponse_type=codescope=openid profile emailstate=abc123
By including these parameters, the client application ensures that the OpenID Provider can correctly identify the client, know where to send the response, understand what types of tokens are being requested, define the access privileges, and secure communication against CSRF attacks.
Step 3. Show login form
The OpenID Provider displays a login form to the user. This form prompts the user to enter their login credentials.
Step 4. Form submission
The user enters their login credentials into the form, including their username and password, and submits it. The OpenID Provider processes these credentials to authenticate the user.
Step 5. Authorization code response
Upon successful authentication, the OpenID Provider redirects the user's browser back to the specified redirect_uri with an authorization_code included in the URL. The authorization_code is a short-lived, one-time code (e.g., authorization_code=SplxlOBeZQQYbYS6WxSbIA) that the client exchanges for an access token. This ensures secure token issuance. This code is used by the client to request an access token from the OpenID Provider.
After this step, once the authorization_code is received in the browser, several types of attacks can occur.
Step 6. Asynchronous access token request
The client application (Relying Party) sends an asynchronous request to the OpenID Provider's token endpoint to exchange the authorization code for an access token. This request includes the authorization_code, client_id, client_secret, and redirect_uri.
authorization_code: The code received in the redirect response. Example:authorization_code=SplxlOBeZQQYbYS6WxSbIA.client_id: The same client identifier used in the initial request. Example:client_id=myapp-12345.client_secret: A secret known only to the client and the OpenID Provider, used to authenticate the client. Example:client_secret=shhh-its-a-secret.redirect_uri: The same URL used in the initial request to ensure consistency. Example:redirect_uri=https://myapp.com/callback.
Step 7. Access token response
The OpenID Provider validates the request and, if successful, returns an access_token and a refresh_token to the client application.
access_token: A credential used to access protected resources. Example:access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.refresh_token: Used to obtain a new access token without requiring the user to re-authenticate. Example:refresh_token=def502b0b22b3d8f67d1b7c2c1b5a6b2c7d1f7f8c8b9c4e2.
Step 8. Async user info request
The client application sends an asynchronous request to the OpenID Provider's user info endpoint to retrieve the user's profile information, using the access_token.
Step 9. User info response
The OpenID Provider responds with the user's claims (e.g., name, email).
Step 10. Response from protected resource
The client application receives the protected resource data in response to the access_token and user_claims request.
Step 11. Protected resource request
The client application sends a request to the Resource Server using the access_token to obtain protected data.
Step 12. Protected resource response
The Resource Server verifies the access token and, if valid, responds to the client's request by providing access to the protected data. This information is sent back to the client application for use by the user.
Summary of OpenID Connect Authorization Code Flow with token-mediating backend
The OpenID Connect Authorization Code Flow with Token-Mediating Backend involves a multi-step process to ensure secure authentication and authorization. It includes redirecting the user to the OpenID Provider, submitting credentials, receiving an authorization code in the redirect response, and then securely exchanging this code for an access token. This flow ensures that the authorization code can only be used by the client that requested it, significantly reducing the risk of token interception.
Using a backend Relying Party (RP) helps address several vulnerabilities:
-
Acquisition and Extraction of New Tokens: By handling token requests on the server side, a backend RP prevents malicious JavaScript from initiating silent requests to the OpenID Provider and obtaining new access tokens.
-
Token Transmission via URL: When tokens are managed by a backend RP, they are not exposed in the URL, browser history, logs, or HTTP referrers, reducing the risk of interception and unauthorized use.
-
Lack of Refresh Token Support: Backend RPs can securely store and use refresh tokens, allowing for long-term session maintenance without frequent token requests, thus minimizing the chances of token leakage and misuse.
However, even with this enhanced flow, some vulnerabilities remain:
- Code Interception Attack: An attacker can intercept the authorization code that is exposed in the URL during the redirect, resulting in unauthorized access to the user's resources.
- Persistent Token Theft: Tokens are present in the browser, and fresh tokens can be stolen continuously, for example, every 10 seconds.
- Proxying Requests via the User's Browser: Malicious JavaScript can "click buttons in the application" on behalf of the user, sending requests to the Protected Resource in the user's name.
To address some of these security concerns, using the OpenID Connect Authorization Code Flow with PKCE in a backend implementation can be effective. The next section shows how PKCE closes these holes.
Authorization Code Flow with token-mediating backend and PKCE
Overview
-
In OpenID Connect, the End-User is the person using an application to access services. These applications can include mobile apps, web apps, desktop apps, IoT devices, gaming applications, and smartwatches. The End-User interacts with these applications to access various services and resources.
-
The Relying Party (RP) is the backend component of the application that the End-User interacts with. The RP depends on the OpenID Provider for user authentication. It requests tokens from the OpenID Provider to authenticate users and gain access to protected resources. In our scheme, the backend is the RP, adding an extra layer of security in handling authentication and tokens.
-
The OpenID Provider (OP) is an authorization server responsible for verifying the identity of the End-User. It authenticates the user and issues identity and access tokens to the Relying Party. For instance, in the diagram, the Abblix OIDC Server, a library from Abblix, certified by the OpenID Foundation, with source code on GitHub, is the OpenID Provider. This server ensures that users are who they claim to be and provides the necessary tokens to the applications that need to interact securely with protected resources.
-
The Resource Server is where the protected information is stored and managed. It hosts sensitive data like personal details, financial information, and private documents. In OpenID Connect, the Resource Server uses the tokens issued by the OpenID Provider to decide whether to grant or deny access to this information. The tokens verify the user's identity and permissions, ensuring that only authorized users can access the sensitive data.
-
The Proof Key for Code Exchange (PKCE) is designed to prevent authorization code interception and injection attacks. It ensures that only the client that requested the authorization code can use it. PKCE allows public clients, such as mobile and single-page applications, to use the Authorization Code Flow securely.
Step-by-step process
Step 1. User requests a protected resource
The user requests a protected resource, prompting the application to check the authentication status. Seeing that the user is not logged in, the application redirects the user's browser to the OpenID Provider to initiate the authentication process.
Step 2. Redirect to authentication
The Relying Party (RP) initiates a redirect of the user's browser to the OpenID Provider for authentication, using parameters such as client_id, redirect_uri, response_type=code, scope, state, and, specifically for PKCE, code_challenge and code_challenge_method.
Before making the request, the client generates a high-entropy cryptographic random string known as the code_verifier. The code_challenge is then derived by hashing the code_verifier using the specified code_challenge_method. This code_challenge is sent with the initial authorization request, ensuring that the code_verifier is used later to prove that the true originator of the authorization request is asking for a token in exchange for the authorization code.
client_id: This identifies the client application making the request. For example,client_id=myapp-12345.redirect_uri: This specifies the URL to which the OpenID Provider should redirect the user after authentication. For example,redirect_uri=https://myapp.com/callback.response_type=code: This indicates that the client expects an authorization code in the response.scope: This defines the access privileges requested, such as access to the user's profile information. For example,scope=openid profile email.state: This parameter helps prevent CSRF attacks by maintaining state between the request and the callback, ensuring the response matches the original request made by the client. For example,state=abc123.code_challenge: This is a derived value used to ensure that the token exchange is secure. Example:code_challenge=abc123def456acd789.code_challenge_method: This specifies the method used to generate the code challenge. For example,code_challenge_method=S256.
Here are examples of what the parameters might look like in a real request:
client_id=myapp-12345redirect_uri=https://myapp.com/callbackresponse_type=codescope=openid profile emailstate=abc123code_challenge=abc123def456acd789code_challenge_method=S256
By including these parameters, the client application ensures that the OpenID Provider can correctly identify the client, know where to send the response, understand what types of tokens are being requested, define the access privileges, and secure communication against CSRF attacks.
Step 3. Show login form
The OpenID Provider displays a login form to the user. This form prompts the user to enter their login credentials.
Step 4. Form submission
The user enters their login credentials into the form, including their username and password, and submits it. The OpenID Provider processes these credentials to authenticate the user.
Step 5. Authorization code response
Upon successful authentication, the OpenID Provider redirects the user's browser back to the specified redirect_uri with an authorization code included in the URL. The authorization_code is a short-lived, one-time code (e.g., authorization_code=SplxlOBeZQQYbYS6WxSbIA) that the client exchanges for an access token. This ensures secure token issuance. This code is used by the client to request an access token from the OpenID Provider.
Step 6. Asynchronous access token request
The client application (Relying Party) sends an asynchronous request to the OpenID Provider's token endpoint to exchange the authorization code for an access token. This request includes the authorization_code, client_id, client_secret, redirect_uri, and the original value of code_verifier.
authorization_code: The code received in the redirect response. Example:authorization_code=SplxlOBeZQQYbYS6WxSbIA.client_id: The same client identifier used in the initial request. Example:client_id=myapp-12345.client_secret: A secret known only to the client and the OpenID Provider, used to authenticate the client. Example:client_secret=shhh-its-a-secret.redirect_uri: The same URL used in the initial request to ensure consistency. Example:redirect_uri=https://myapp.com/callback.code_verifier: The original random string that was used to generate the code challenge. Example:code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk.
Step 7. Access token response
The OpenID Provider validates the request and, if successful, returns an access_token and a refresh_token to the client application.
access_token: A credential used to access protected resources. Example:access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.refresh_token: Used to obtain a new access token without requiring the user to re-authenticate. Example:refresh_token=def502b0b22b3d8f67d1b7c2c1b5a6b2c7d1f7f8c8b9c4e2.
Step 8. Async user info request
The client application sends an asynchronous request to the OpenID Provider's user info endpoint to retrieve the user's profile information, using the access_token.
Step 9. User info response
The OpenID Provider responds with the user's claims (e.g., name, email).
Step 10. Response from protected resource
The client application receives the protected resource data in response to the access_token and user_claims request.
After this step, once the access_token is received in the browser, several types of attacks can occur.
Step 11. Protected resource request
The client application sends a request to the Resource Server using the access_token to obtain protected data.
Step 12. Protected resource response
The Resource Server verifies the access token and, if valid, responds to the client's request by providing access to the protected data. This information is sent back to the client application for use by the user.
Summary of OpenID Connect Authorization Code Flow with token-mediating backend and PKCE
The OpenID Connect Authorization Code Flow with PKCE in a backend implementation involves a multi-step process to ensure secure authentication and authorization. It includes redirecting the user to the OpenID Provider, submitting credentials, receiving an authorization code in the redirect response, and then securely exchanging this code for an access token. By ensuring that only the client that requested the authorization code can use it, this flow significantly reduces the risk of token interception.
Using a backend Relying Party (RP) helps address several vulnerabilities:
- Acquisition and Extraction of New Tokens: By handling token requests on the server side, a backend RP prevents malicious JavaScript from initiating silent requests to the OpenID Provider and obtaining new access tokens.
- Token Transmission via URL: When tokens are managed by a backend RP, they are not exposed in the URL, browser history, logs, or HTTP referrers, reducing the risk of interception and unauthorized use.
- Lack of Refresh Token Support: Backend RPs can securely store and use refresh tokens, allowing for long-term session maintenance without frequent token requests, thus minimizing the chances of token leakage and misuse.
Using PKCE (Proof Key for Code Exchange) addresses the Code Interception Attack vulnerability. PKCE ensures that the authorization code can only be exchanged for tokens by the client that requested it, preventing attackers from intercepting the code during the redirect.
However, even with these enhancements, some vulnerabilities remain:
- Persistent Token Theft: Tokens are present in the browser, and fresh tokens can be stolen continuously, for example, every 10 seconds.
- Proxying Requests via the User's Browser: Malicious JavaScript can "click buttons in the application" on behalf of the user, sending requests to the Protected Resource in the user's name.
Using the Backend for Frontend (BFF) pattern addresses the vulnerability of access token interception. The BFF pattern involves creating a backend service that is an intermediary between the end-user's client application and the authentication provider. By handling access tokens on the server side, the BFF pattern prevents the access token from being exposed to the end-user's device. This approach mitigates the risk of access token interception by malicious JavaScript, ensuring that tokens are never directly accessible from the user's browser.
Authorization Code Flow with PKCE and BFF
Overview
-
In OpenID Connect, the End-User is the person using an application to access services. These applications can include mobile apps, web apps, desktop apps, IoT devices, gaming applications, and smartwatches. The End-User interacts with these applications to access various services and resources.
-
The Relying Party (RP) in this case is a backend component that uses the Backend for Frontend (BFF) pattern. The BFF acts as an intermediary between the client application and the OpenID Provider. It handles authentication and token management, adding an extra layer of security.
-
The OpenID Provider (OP) is an authorization server responsible for verifying the identity of the End-User. It authenticates the user and issues identity and access tokens to the Relying Party. For instance, in the diagram, the Abblix OIDC Server, a library from Abblix, certified by the OpenID Foundation, with source code on GitHub, is the OpenID Provider. This server ensures that users are who they claim to be and provides the necessary tokens to the applications that need to interact securely with protected resources.
-
The Resource Server is where the protected information is stored and managed. It hosts sensitive data like personal details, financial information, and private documents. In OpenID Connect, the Resource Server uses the tokens issued by the OpenID Provider to decide whether to grant or deny access to this information. The tokens verify the user's identity and permissions, ensuring that only authorized users can access the sensitive data.
-
The Proof Key for Code Exchange (PKCE) is designed to prevent authorization code interception and injection attacks. It ensures that the authorization code can only be used by the client that requested it. PKCE allows public clients, such as mobile and single-page applications, to use the Authorization Code Flow securely.
-
The Backend for Frontend (BFF) pattern involves creating a backend service that serves as an intermediary between the client application and the authentication provider. The BFF handles authentication and token management on the server side, preventing the access token from being exposed to the end-user. This approach mitigates the risk of access token interception by malicious JavaScript, ensuring that tokens are never directly accessible from the user's browser.
Step-by-step process
Step 1. User requests a protected resource
The user requests a protected resource, prompting the application to check the authentication status. Seeing that the user is not logged in, the application redirects the user's browser to the OpenID Provider to initiate the authentication process.
Step 2. Redirect to authentication
The Relying Party (RP) initiates a redirect of the user's browser to the OpenID Provider for authentication, using parameters such as client_id, redirect_uri, response_type=code, scope, state, and, specifically for PKCE, code_challenge and code_challenge_method.
Before making the request, the client generates a high-entropy cryptographic random string known as the code_verifier. The code_challenge is then derived by hashing the code_verifier using the specified code_challenge_method. This code_challenge is sent with the initial authorization request, ensuring that the code_verifier is used later to prove that the true originator of the authorization request is asking for a token in exchange for the authorization code.
client_id: This identifies the client application making the request. For example,client_id=myapp-12345.redirect_uri: This specifies the URL to which the OpenID Provider should redirect the user after authentication. For example,redirect_uri=https://myapp.com/callback.response_type=code: This indicates that the client expects an authorization code in the response.scope: This defines the access privileges requested, such as access to the user's profile information. For example,scope=openid profile email.state: This parameter helps prevent CSRF attacks by maintaining state between the request and the callback, ensuring the response matches the original request made by the client. For example,state=abc123.code_challenge: This is a derived value used to ensure that the token exchange is secure. Example:code_challenge=abc123def456acd789.code_challenge_method: This specifies the method used to generate the code challenge. For example,code_challenge_method=S256.
Here are examples of what the parameters might look like in a real request:
client_id=myapp-12345redirect_uri=https://myapp.com/callbackresponse_type=codescope=openid profile emailstate=abc123code_challenge=abc123def456acd789code_challenge_method=S256
By including these parameters, the client application ensures that the OpenID Provider can correctly identify the client, know where to send the response, understand what types of tokens are being requested, define the access privileges, and secure communication against CSRF attacks.
Step 3. Show login form
The OpenID Provider displays a login form to the user. This form prompts the user to enter their login credentials.
Step 4. Form submission
The user enters their login credentials into the form, including their username and password, and submits it. The OpenID Provider processes these credentials to authenticate the user.
Step 5. Authorization code response
Upon successful authentication, the OpenID Provider redirects the user's browser back to the specified redirect_uri with an authorization code included in the URL. The authorization_code is a short-lived, one-time code (e.g., authorization_code=SplxlOBeZQQYbYS6WxSbIA) that the client exchanges for an access token. This ensures secure token issuance. This code is used by the client to request an access token from the OpenID Provider.
Step 6. Asynchronous access token request
The client application (Relying Party) sends an asynchronous request to the OpenID Provider's token endpoint to exchange the authorization code for an access token. This request includes the authorization_code, client_id, client_secret, redirect_uri, and, specifically for PKCE, the original value of code_verifier.
authorization_code: The code received in the redirect response. Example:authorization_code=SplxlOBeZQQYbYS6WxSbIA.client_id: The same client identifier used in the initial request. Example:client_id=myapp-12345.redirect_uri: The same URL used in the initial request to ensure consistency. Example:redirect_uri=https://myapp.com/callback.code_verifier: The original random string that was used to generate the code challenge. Example:code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk.
Step 7. Access token response
The OpenID Provider validates the request and, if successful, returns an access_token and a refresh_token to the client application. At this stage, the BFF stores the access_token.
access_token: A credential used to access protected resources. Example:access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.refresh_token: Used to obtain a new access token without requiring the user to re-authenticate. Example:refresh_token=def502b0b22b3d8f67d1b7c2c1b5a6b2c7d1f7f8c8b9c4e2.
Step 8. Async user info request
The client application sends an asynchronous request to the OpenID Provider's user info endpoint to retrieve the user's profile information, using the access_token.
Step 9. User info response
The OpenID Provider responds with the user's claims (e.g., name, email).
Step 10. Response from protected resource
The client application receives the protected resource data in response to the user_claims request.
Step 11. Protected resource request
The client application sends a request to the BFF, which then uses the access_token to make a request to the Resource Server to obtain protected data.
Step 12. Protected resource response
The Resource Server verifies the access token and, if valid, responds to the BFF's request by providing access to the protected data. This data is sent back to the client application via the BFF.
Summary of OpenID Connect Authorization Code Flow with PKCE and BFF
The OpenID Connect Authorization Code Flow with PKCE and BFF involves a multi-step process to ensure secure authentication and authorization. It includes redirecting the user to the OpenID Provider, submitting credentials, receiving an authorization code in the redirect response, and then securely exchanging this code for an access token. By ensuring that the authorization code can only be used by the client that requested it, this flow significantly reduces the risk of token interception.
Using a backend Relying Party (RP) and the BFF pattern helps address several vulnerabilities:
- Acquisition and Extraction of New Tokens: By handling token requests on the server side, a backend RP prevents malicious JavaScript from initiating silent requests to the OpenID Provider and obtaining new access tokens.
- Token Transmission via URL: When tokens are managed by a backend RP, they are not exposed in the URL, browser history, logs, or HTTP referrers, reducing the risk of interception and unauthorized use.
- Lack of Refresh Token Support: Backend RPs can securely store and use refresh tokens, allowing for long-term session maintenance without frequent token requests, thus minimizing the chances of token leakage and misuse.
Using Proof Key for Code Exchange (PKCE) addresses the Code Interception Attack vulnerability. PKCE ensures that the authorization code can only be exchanged for tokens by the client that requested it, preventing attackers from intercepting the code during the redirect.
The BFF pattern adds another layer of security by preventing access tokens from being exposed to the end-user's device. This approach mitigates the risk of Token Theft, ensuring that tokens are never directly accessible from the user's browser.
However, even with these enhancements, one known vulnerability remains:
- Proxying Requests via the User's Browser: Malicious JavaScript can "click buttons in the application" on behalf of the user, sending requests to the Protected Resource in the user's name.
A web application running in a browser actually executes in an environment that is not controlled by the developer, making it impossible to ensure absolute security. This is why eliminating tokens from the browser is a clear trend in web application security.
To mitigate risks associated with this vulnerability, users should install only verified browser extensions.
As of 2024, this authentication scheme is the most secure and recommended for use. By keeping tokens off the browser and requiring proof of client identity, it prevents code interception, token theft, and unauthorized token acquisition.
Conclusion
We explored various flows, starting from the Implicit Flow in a frontend-only implementation, which, while simpler, is fraught with vulnerabilities. Moving to more secure methods, the Authorization Code Flow with PKCE, both in frontend-only and backend-involving implementations, significantly reduces risks associated with token interception and theft.
The combination of Authorization Code Flow with PKCE and the Backend for Frontend (BFF) pattern is the most secure approach available as of 2024. This setup keeps tokens out of the user's browser and mitigates code interception and token theft. However, it is still essential to be vigilant against browser-based attacks, such as those involving malicious JavaScript.
Visit the official Abblix site and join us in improving our server library, Abblix OIDC Server, on our GitHub page.