- 26 Sep 2024
- 38 Minutes to read
- Print
From Implicit to Authorization Code with PKCE & BFF
- Updated on 26 Sep 2024
- 38 Minutes to read
- Print
Introduction
This article will review the principles behind various OpenID Connect (OIDC) authentication flows, from the simplest to the most modern, highlighting the vulnerabilities present in each.
We will explore each of the following OpenID Connect flows in detail:
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
These vulnerabilities are critical to understand when implementing OpenID Connect or OAuth 2.0 protocols in your applications.
Modern web applications can be attacked through various types of 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 to 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, frequent requests for new tokens are necessary, increasing the chances of token leakage and misuse. This is especially relevant to Implicit Flow in frontend-only implementations.
Evolution of OpenID Connect Flows
Description
The diagram illustrates the evolution of OpenID Connect flows over time, showing how the authentication methods have become increasingly secure. It highlights the shift from the initial Implicit Flow, which had numerous vulnerabilities, to the more secure Authorization Code Flow with PKCE and BFF. Each progression step is depicted to demonstrate how these new flows have effectively addressed and reduced various security vulnerabilities, ensuring a more secure authentication process in modern web applications.
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 performed 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 enhance security, the Authorization Code Flow with Proof Key for Code Exchange (PKCE) was developed. PKCE addressed significant vulnerabilities, including code interception attacks. This flow marked a considerable improvement in securing the authentication process.
To address more issues, the Authorization Code Flow with PKCE and a token-mediating backend became the next possible move. This approach involved both the frontend and the backend, thereby 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.
Now we will examine the principles of each flow in detail.
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 certified OpenID Foundation library presented by the company Abblix with open source code (available on GitHub), serves as 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-12345
redirect_uri=https://myapp.com/callback
response_type=id_token token
scope=openid profile email
state=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
To summarize, the Implicit Flow in OpenID Connect involves redirecting the user to the OpenID Provider for authentication, the user submits their login credentials, and the application receives 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. Additionally, if the application only involves a frontend component, many of these vulnerabilities can be mitigated by using the OpenID Connect Authorization Code Flow with PKCE. In the next section, we will examine these flows and explore how they mitigate the vulnerabilities present in the Implicit Flow, providing a safer mechanism for handling and transmitting tokens.
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 certified OpenID Foundation library presented by the company Abblix with open source code (available on GitHub), serves as 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-12345
redirect_uri=https://myapp.com/callback
response_type=code
scope=openid profile email
state=abc123
code_challenge=abc123def456acd789
code_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, thereby maintaining the session securely over longer periods. 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
To summarize, the Authorization Code Flow with PKCE in OpenID Connect involves redirecting the user to the OpenID Provider for authentication, the user submits their login credentials, the application receives an authorization code in the redirect response, and then securely exchanges this code for an access token using PKCE. This flow enhances security by ensuring 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, thereby reducing the chances of token leakage and misuse.
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 further enhance security, 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 functions as 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 certified OpenID Foundation library presented by the company Abblix with open source code (available on GitHub), serves as 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-12345
redirect_uri=https://myapp.com/callback
response_type=code
scope=openid profile email
state=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 for authentication, the user submits their login credentials, the OpenID Provider returns an authorization code in the redirect response, and then the application securely exchanges this code for an access token. This flow enhances security by ensuring 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. In the next section, we will examine this diagram and explore how PKCE prevents code interception attacks effectively, providing a more secure mechanism for token handling and transmission.
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 functions as 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 certified OpenID Foundation library presented by the company Abblix with open source code (available on GitHub), serves as 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-12345
redirect_uri=https://myapp.com/callback
response_type=code
scope=openid profile email
state=abc123
code_challenge=abc123def456acd789
code_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 for authentication, the user submits their login credentials, the OpenID Provider returns an authorization code in the redirect response, and then the application securely exchanges 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) further enhances security by addressing 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 can further enhance security by addressing the vulnerability of access token interception. The BFF pattern involves creating a backend service that serves as 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, thereby enhancing overall security.
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 certified OpenID Foundation library presented by the company Abblix with open source code (available on GitHub), serves as 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, thereby significantly enhancing overall security.
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-12345
redirect_uri=https://myapp.com/callback
response_type=code
scope=openid profile email
state=abc123
code_challenge=abc123def456acd789
code_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 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 for authentication, the user submits their login credentials, the OpenID Provider returns an authorization code in the redirect response, and then the application securely exchanges 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) further enhances security by addressing 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 trending direction for improving web application security.
To mitigate risks associated with this vulnerability, users should install only verified browser extensions, keep browsers updated to the latest version, and use antivirus software to enhance overall security.
As of 2024, this authentication scheme is the most secure and recommended for use, providing robust protection for accessing protected resources.
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 represents the most secure approach available as of 2024. This setup not only prevents tokens from being exposed to the user's browser but also mitigates several common attack vectors like code interception, token theft, and unauthorized token acquisition. However, it is still essential to be vigilant against browser-based attacks, such as those involving malicious JavaScript.
Visit Abblix official site and join us in improving our server library, Abblix OIDC Server, on our GitHub page.