Authorization Code Flow with PKCE
This guide explains how to configure your application to authenticate users to TOKI, implementing the Authorization Code Flow with PKCE for enhanced security.
Before You Start
Client Application Registration
Register your application with the OAuth2 provider and obtain the following details:
| Field | Description |
|---|---|
| client_id required | Your application's client identifier |
| redirect_uri required | The URL to which the user will be redirected after successful authentication |
| client_secret | Required only for flows without PKCE |
OAuth2 Flows
1. Authorization Code Flow with PKCE
2. Authorization Code Flow (without PKCE)
Implementation Steps
Redirect User to Authorization Endpoint
a) With PKCE
| Parameter | Value |
|---|---|
| response_type | code |
| client_id | your client id |
| redirect_uri | your redirect uri |
| code_challenge | base64url-encoded SHA256 hash of code_verifier |
| code_challenge_method | S256 |
| scope | space separated list of scopes |
| state | optional random string |
b) Without PKCE
| Parameter | Value |
|---|---|
| response_type | code |
| client_id | your client id |
| redirect_uri | your redirect uri |
| scope | space separated list of scopes |
| state | optional random string |
Handle Redirect with Authorization Code
After the user authenticates, they will be redirected to your
redirect_uri
with the following query parameters:
| Parameter | Description |
|---|---|
| code | The authorization code |
| state | (If sent) The state parameter to prevent CSRF |
Exchange Authorization Code for Access Token
a) With PKCE
b) Without PKCE (uses client_secret)
Parse the Token Response
The response will be a JSON object containing:
| Field | Description |
|---|---|
| access_token | The token to access protected resources |
| token_type | Typically Bearer |
| expires_in | Token expiration time in seconds |
| refresh_token | (Optional) A token to obtain a new access token |
| id_token | (Optional) JWT containing user claims if openid scope is used |
Get User Info
Send a GET request to the userinfo endpoint with the access token in the Authorization header:
Replace
<user_id>
with the userId from the
/token
API response.
OAuth 2.0 Framework
Authorization Code Flow with PKCE — Node.js
require('dotenv').config();
const express = require('express');
const axios = require('axios');
const crypto = require('crypto');
const app = express();
const port = 8080;
const client_id = process.env.CLIENT_ID;
const redirect_uri = process.env.REDIRECT_URI;
const auth_endpoint = process.env.AUTH_ENDPOINT;
const token_endpoint = process.env.TOKEN_ENDPOINT;
let code_verifier = '';
let state = '';
function base64URLEncode(str) {
return str.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
function generateCodeVerifier() {
return base64URLEncode(crypto.randomBytes(32));
}
function generateCodeChallenge(code_verifier) {
return base64URLEncode(
crypto.createHash('sha256').update(code_verifier).digest()
);
}
// Start auth flow
app.get('/login', async (req, res) => {
code_verifier = generateCodeVerifier();
const code_challenge = generateCodeChallenge(code_verifier);
state = crypto.randomBytes(16).toString('hex');
const authUrl = `${auth_endpoint}?`
+ `client_id=${encodeURIComponent(client_id)}`
+ `&response_type=code`
+ `&redirect_uri=${encodeURIComponent(redirect_uri)}`
+ `&code_challenge=${encodeURIComponent(code_challenge)}`
+ `&code_challenge_method=S256`
+ `&scope=openid offline_access`
+ `&state=${state}`;
await open(authUrl);
res.send('Redirecting to TOKI for authentication...');
});
// Callback handler
app.get('/callback', async (req, res) => {
const { code, state: returnedState } = req.query;
if (returnedState !== state)
return res.status(400).send('State mismatch!');
const params = new URLSearchParams();
params.append('grant_type', 'authorization_code');
params.append('code', code);
params.append('redirect_uri', redirect_uri);
params.append('client_id', client_id);
params.append('code_verifier', code_verifier);
const tokenResponse = await axios.post(token_endpoint, params, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
res.json(tokenResponse.data);
});
app.listen(port, () => console.log(`App running at http://localhost:${port}`));
Authorization Code Flow with Client Secret — Node.js
require('dotenv').config();
const express = require('express');
const axios = require('axios');
const crypto = require('crypto');
const app = express();
const port = 8080;
const client_id = process.env.CLIENT_ID;
const client_secret = process.env.CLIENT_SECRET;
const redirect_uri = process.env.REDIRECT_URI;
const auth_endpoint = process.env.AUTH_ENDPOINT;
const token_endpoint = process.env.TOKEN_ENDPOINT;
let state = '';
// Start auth flow
app.get('/login', async (req, res) => {
state = crypto.randomBytes(16).toString('hex');
const authUrl = `${auth_endpoint}?`
+ `client_id=${encodeURIComponent(client_id)}`
+ `&response_type=code`
+ `&redirect_uri=${encodeURIComponent(redirect_uri)}`
+ `&scope=openid offline_access`
+ `&state=${state}`;
await open(authUrl);
res.send('Redirecting to TOKI for authentication...');
});
// Callback handler
app.get('/callback', async (req, res) => {
const { code, state: returnedState } = req.query;
if (returnedState !== state)
return res.status(400).send('State mismatch!');
const params = new URLSearchParams();
params.append('grant_type', 'authorization_code');
params.append('code', code);
params.append('redirect_uri', redirect_uri);
params.append('client_id', client_id);
params.append('client_secret', client_secret);
const tokenResponse = await axios.post(token_endpoint, params, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
res.json(tokenResponse.data);
});
app.listen(port, () => console.log(`App running at http://localhost:${port}`));