Using OAuth with PKCE Authorization Flow (Proof Key for Code Exchange)
If you've ever created a login page or auth system, you might be familiar with OAuth 2.0, the industry standard protocol for authorization. It allows an app to access resources hosted on another app securely. Access is granted using different flows, or grants, at the level of a scope.
For example, if I make an application (Client) that allows a user (Resource Owner) to make notes and save them as a repo in their GitHub account (Resource Server), then my application will need to access their GitHub data. It's not secure for the user to directly supply their GitHub username and password to my application and grant full access to the entire account. Instead, using OAuth 2.0, they can go through an authorization flow that will grant limited access to some resources based on a scope, and I will never have access to any other data or their password.
Using OAuth, a flow will ultimately request a token from the Authorization Server, and that token can be used to make all future requests in the agreed upon scope.
Note: OAuth 2.0 is used for authorization, (authZ) which gives users permission to access a resource. OpenID Connect, or OIDC, is often used for authentication, (authN) which verifies the identity of the end user.
Grant Types
The type of application you have will determine the grant type that will apply.
Grant Type | Application type | Example |
---|---|---|
Client Credentials | Machine | A server accesses 3rd-party data via cron job |
Authorization Code | Server-side web app | A Node or Python server handles the front and back end |
Authorization Code with PKCE | Single-page web app/mobile app | A client-side only application that is decoupled from the back end |
For machine-to-machine communication, like something that cron job on a server would perform, you would use the Client Credentials grant type, which uses a client id and client secret. This is acceptable because the client id and resource owner are the same, so only one is needed. This is performed using the /token
endpoint.
For a server-side web app, like a Python Django app, Ruby on Rails app, PHP Laravel, or Node/Express serving React, the Authorization Code flow is used, which still uses a client id and client secret on the server side, but the user needs to authorize via the third-party first. This is performed using both an /authorize
and /token
endpoints.
However, for a client-side only web app or a mobile app, the Authorization Code flow is not acceptable because the client secret cannot be exposed, and there's no way to protect it. For this purpose, the Proof Key for Code Exchange (PKCE) version of the authorization code flow is used. In this version, the client creates a secret from scratch and supplies it after the authorization request to retrieve the token.
Since PKCE is a relatively new addition to OAuth, a lot of authentication servers do not support it yet, in which case either a less secure legacy flow like Implicit Grant is used, where the token would return in the callback of the request, but using Implicit Grant flow is discouraged. AWS Cognito is one popular authorization server that supports PKCE.
PKCE Flow
The flow for a PKCE authentication system involves a user, a client-side app, and an authorization server, and will look something like this:
- The user arrives at the app's entry page
- The app generates a PKCE code challenge and redirects to the authorization server login page via
/authorize
- The user logs in to the authorization server and is redirected back to the app with the authorization code
- The app requests the token from the authorization server using the code verifier/challenge via
/token
- The authorization server responds with the token, which can be used by the app to access resources on behalf of the user
So all we need to know is what our /authorize
and /token
endpoints should look like. I'll go through an example of setting up PKCE for a front end web app.
GET /authorize
endpoint
The flow begins by making a GET
request to the /authorize
endpoint. We need to pass some parameters along in the URL, which includes generating a code challenge and code verifier.
Parameter | Description |
---|---|
response_type |
code |
client_id |
Your client ID |
redirect_uri |
Your redirect URI |
code_challenge |
Your code challenge |
code_challenge_method |
S256 |
scope |
Your scope |
state |
Your state (optional) |
We'll be building the URL and redirecting the user to it, but first we need to make the verifier and challenge.
Verifier
The first step is generating a code verifier, which the PKCE spec defines as:
Verifier - A high-entropy cryptographic random STRING using the unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "*" / "~" from Section 2.3 of [RFC3986], with a minimum length of 43 characters and a maximum length of 128 characters.
I'm using a random string generator that Aaron Parecki of oauth.net wrote:
function generateVerifier() {
const array = new Uint32Array(28)
window.crypto.getRandomValues(array)
return Array.from(array, (item) => `0${item.toString(16)}`.substr(-2)).join(
''
)
}
Challenge
The code challenge performs the following transformation on the code verifier:
Challenge -
BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
So the verifier gets passed into the challenge function as an argument and transformed. This is the function that will hash and encode the random verifier string:
async function generateChallenge(verifier) {
function sha256(plain) {
const encoder = new TextEncoder()
const data = encoder.encode(plain)
return window.crypto.subtle.digest('SHA-256', data)
}
function base64URLEncode(string) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(string)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+\$/, '')
}
const hashed = await sha256(verifier)
return base64URLEncode(hashed)
}
Build endpoint
Now you can take all the needed parameters, generate the verifier and challenge, set the verifier to local storage, and redirect the user to the authentication server's login page.
async function buildAuthorizeEndpointAndRedirect() {
const host = 'https://auth-server.example.com/oauth/authorize'
const clientId = 'abc123'
const redirectUri = 'https://my-app-host.example.com/callback'
const scope = 'specific,scopes,for,app'
const verifier = generateVerifier()
const challenge = await generateChallenge(verifier)
// Build endpoint
const endpoint = `${host}?
response_type=code&
client_id=${clientId}&
scope=${scope}&
redirect_uri=${redirectUri}&
code_challenge=${challenge}&
code_challenge_method=S256`
// Set verifier to local storage
localStorage.setItem('verifier', verifier)
// Redirect to authentication server's login page
window.location = endpoint
}
At what point you call this function is up to you - it might happen at the click of a button, or automatically if a user is deemed to not be authenticated when they land on the app. In a React app it would probably be in the useEffect()
.
useEffect(() => {
buildAuthorizeEndpointAndRedirect()
}, [])
Now the user will be on the authentication server's login page, and after successful login via username and password they'll be redirected to the redirect_uri
from step one.
POST /token
endpoint
The second step is retrieving the token. This is the part that is usually accomplished server side in a traditional Authorization Code flow, but for PKCE it's also through the front end. When the authorization server redirects back to your callback URI, it will come along with a code
in the query string, which you can exchange along with the verifier string for the final token
.
The POST
request for a token must be made as a x-www-form-urlencoded
request.
Header | Description |
---|---|
Content-Type |
application/x-www-form-urlencoded |
Parameter | Description |
---|---|
grant_type |
authorization_code |
client_id |
Your client ID |
code_verifier |
Your code verifier |
redirect_uri |
The same redirect URI from step 1 |
code |
Code query parameter |
async function getToken(verifier) {
const host = 'https://auth-server.example.com/oauth/token'
const clientId = 'abc123'
const redirectUri = `https://my-app-server.example.com/callback`
// Get code from query params
const urlParams = new URLSearchParams(window.location.search)
const code = urlParams.get('code')
// Build params to send to token endpoint
const params = `client_id=${clientId}&
grant_type=${grantType}&
code_verifier=${verifier}&
redirect_uri=${redirectUri}&
code=${code}`
// Make a POST request
try {
const response = await fetch(host, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params,
})
const data = await response.json()
// Token
console.log(data)
} catch (e) {
console.log(e)
}
}
Once you obtain the token, you should immediately delete the verifier from localStorage
.
const response = await getToken(localStorage.getItem('verifier'))
localStorage.removeItem('verifier')
When it comes to storing the token, if your app is truly front end only, the option is to use localStorage
. If the option of having a server is available, you can use a Backend for Frontend (BFF) to handle authentication. I recommend reading A Critical Analysis of Refresh Token Rotation in Single-page Applications.
Conclusion
And there you have it - the two steps to authenticate using PKCE. First, build a URL for /authorize
on the authorization server and redirect the user to it, then POST to the /token
endpoint on the redirect. PKCE is currently the most secure authentication system that I know of for a front-end only web or mobile app. Hopefully this helps you understand and implement PKCE in your app!
Comments