PKCE (Proof Key for Code Exchange) secures the OAuth2 flow for applications that cannot store a client_secret — such as single-page apps (React, Vue, Angular), native mobile apps, and client-side JavaScript. Instead of a client secret, you use a one-time code_verifier and its code_challenge to prove you initiated the authorization request.
Prerequisites
Before you implement this flow, ensure you have:
OAuth2 client — Register your app in Client Center / Manage Account to get client_id (no client_secret required for PKCE)
Redirect URI — Must be pre-registered in Client Center / Manage Account; use the exact URL where your app handles the callback (e.g., https://yourapp.com/oauth/callback)
Runtime support — Browser with crypto.subtle or Node.js 18+ / Python 3.8+ for generating code_verifier and code_challenge
Storage for code_verifier — Store the code_verifier between the redirect and callback (e.g., sessionStorage in browser, secure storage in mobile apps)
PKCE is recommended for all public clients (SPAs, mobile apps). Even if your backend eventually handles the token exchange, starting with PKCE improves security.
How PKCE works
Generate a random code_verifier (43–128 characters).
Create a code_challenge = Base64URL(SHA256(code_verifier)).
Include code_challenge and code_challenge_method=S256 in the authorization URL.
Store code_verifier until the callback.
When exchanging the code for tokens, send code_verifier instead of client_secret. Aries verifies it matches the challenge.
Step 1: Generate code_verifier, code_challenge, and redirect
Generate a cryptographically random code_verifier, derive the code_challenge, and redirect the user to Aries. Store the code_verifier so you can send it when exchanging the code.
Endpoint: https://app.aries.com/oauth2/authorize
Query parameters (including PKCE):
Parameter Description response_typecodeclient_idYour OAuth2 client ID redirect_uriMust match a registered URI scopeSpace-separated scopes stateRandom string for CSRF protection code_challengeBase64URL(SHA256(code_verifier)) code_challenge_methodS256
const crypto = require ( 'crypto' );
// Generate code_verifier (43–128 chars, URL-safe)
const codeVerifier = crypto . randomBytes ( 32 ). toString ( 'base64url' );
// Derive code_challenge (SHA256, Base64URL)
const codeChallenge = crypto
. createHash ( 'sha256' )
. update ( codeVerifier )
. digest ( 'base64url' );
const state = crypto . randomBytes ( 32 ). toString ( 'hex' );
// Store code_verifier in session for callback (e.g., req.session.pkce_code_verifier = codeVerifier)
const params = new URLSearchParams ({
response_type: 'code' ,
client_id: process . env . ARIES_CLIENT_ID ,
redirect_uri: process . env . ARIES_REDIRECT_URI ,
scope: 'account:information order:execution market:information' ,
state ,
code_challenge: codeChallenge ,
code_challenge_method: 'S256' ,
});
const authUrl = `https://app.aries.com/oauth2/authorize? ${ params } ` ;
res . redirect ( authUrl );
import secrets
import hashlib
import base64
from urllib.parse import urlencode
# Generate code_verifier (43–128 chars, URL-safe)
code_verifier = base64.urlsafe_b64encode(secrets.token_bytes( 32 )).decode( 'utf-8' ).rstrip( '=' )
# Derive code_challenge (SHA256, Base64URL)
digest = hashlib.sha256(code_verifier.encode()).digest()
code_challenge = base64.urlsafe_b64encode(digest).decode( 'utf-8' ).rstrip( '=' )
state = secrets.token_hex( 32 )
# Store code_verifier in session for callback (e.g., session['pkce_code_verifier'] = code_verifier)
params = urlencode({
'response_type' : 'code' ,
'client_id' : os.environ[ 'ARIES_CLIENT_ID' ],
'redirect_uri' : os.environ[ 'ARIES_REDIRECT_URI' ],
'scope' : 'account:information order:execution market:information' ,
'state' : state,
'code_challenge' : code_challenge,
'code_challenge_method' : 'S256' ,
})
auth_url = f 'https://app.aries.com/oauth2/authorize? { params } '
return redirect(auth_url)
Browser (SPA) example — use sessionStorage to persist code_verifier across the redirect:
async function generateCodeChallenge () {
const codeVerifier = crypto . randomUUID () + crypto . randomUUID ();
sessionStorage . setItem ( 'pkce_code_verifier' , codeVerifier );
const encoder = new TextEncoder ();
const data = encoder . encode ( codeVerifier );
const hash = await crypto . subtle . digest ( 'SHA-256' , data );
const codeChallenge = btoa ( String . fromCharCode ( ... new Uint8Array ( hash )))
. replace ( / \+ / g , '-' )
. replace ( / \/ / g , '_' )
. replace ( /= + $ / , '' );
const state = crypto . randomUUID ();
sessionStorage . setItem ( 'oauth_state' , state );
const params = new URLSearchParams ({
response_type: 'code' ,
client_id: 'YOUR_CLIENT_ID' ,
redirect_uri: window . location . origin + '/oauth/callback' ,
scope: 'account:information' ,
state ,
code_challenge: codeChallenge ,
code_challenge_method: 'S256' ,
});
window . location . href = `https://app.aries.com/oauth2/authorize? ${ params } ` ;
}
Step 2: Handle the callback
After the user approves, Aries redirects to your redirect_uri with the authorization code and state. Retrieve the stored code_verifier and exchange the code for tokens. Do not use the code twice — exchange it once and immediately.
app . get ( '/oauth/callback' , async ( req , res ) => {
if ( req . query . state !== req . session . oauth_state ) {
return res . status ( 400 ). send ( 'Invalid state parameter' );
}
if ( req . query . error ) {
return res . status ( 400 ). send ( `Authorization error: ${ req . query . error } ` );
}
const authCode = req . query . code ;
const codeVerifier = req . session . pkce_code_verifier ;
if ( ! authCode || ! codeVerifier ) {
return res . status ( 400 ). send ( 'Missing code or code_verifier' );
}
try {
const tokens = await exchangeCodeWithPKCE ( authCode , codeVerifier );
req . session . access_token = tokens . access_token ;
req . session . refresh_token = tokens . refresh_token ;
res . redirect ( '/dashboard' );
} catch ( err ) {
res . status ( 500 ). send ( 'Token exchange failed' );
}
});
@app.route ( '/oauth/callback' )
def oauth_callback ():
if request.args.get( 'state' ) != session.get( 'oauth_state' ):
return 'Invalid state parameter' , 400
if request.args.get( 'error' ):
return f "Authorization error: { request.args.get( 'error' ) } " , 400
auth_code = request.args.get( 'code' )
code_verifier = session.get( 'pkce_code_verifier' )
if not auth_code or not code_verifier:
return 'Missing code or code_verifier' , 400
try :
tokens = exchange_code_with_pkce(auth_code, code_verifier)
session[ 'access_token' ] = tokens[ 'access_token' ]
session[ 'refresh_token' ] = tokens[ 'refresh_token' ]
return redirect( '/dashboard' )
except Exception :
return 'Token exchange failed' , 500
Browser (SPA) callback — exchange happens from the client:
async function handleCallback () {
const params = new URLSearchParams ( window . location . search );
if ( params . get ( 'state' ) !== sessionStorage . getItem ( 'oauth_state' )) {
throw new Error ( 'Invalid state' );
}
if ( params . get ( 'error' )) {
throw new Error ( params . get ( 'error_description' ) || params . get ( 'error' ));
}
const code = params . get ( 'code' );
const codeVerifier = sessionStorage . getItem ( 'pkce_code_verifier' );
sessionStorage . removeItem ( 'pkce_code_verifier' );
sessionStorage . removeItem ( 'oauth_state' );
const tokens = await exchangeCodeWithPKCE ( code , codeVerifier );
sessionStorage . setItem ( 'access_token' , tokens . access_token );
sessionStorage . setItem ( 'refresh_token' , tokens . refresh_token );
window . location . href = '/dashboard' ;
}
Step 3: Exchange the code for tokens (with code_verifier)
Send the authorization code and code_verifier to the token endpoint. Do not include client_secret — PKCE uses code_verifier instead.
Endpoint: POST https://api.aries.com/v1/oauth2/token
Request body (PKCE):
Field Required Description client_idYes Your OAuth2 client ID grant_typeYes codecodeYes The authorization code from the callback redirect_uriYes Must match the redirect URI used in Step 1 code_verifierYes The original random value (not the hash) you sent as code_challenge
# Replace AUTHORIZATION_CODE and CODE_VERIFIER with values from your flow
curl -X POST 'https://api.aries.com/v1/oauth2/token' \
-H 'Content-Type: application/json' \
-d '{
"client_id": "YOUR_CLIENT_ID",
"grant_type": "code",
"code": "AUTHORIZATION_CODE",
"redirect_uri": "YOUR_REDIRECT_URI",
"code_verifier": "YOUR_CODE_VERIFIER"
}'
async function exchangeCodeWithPKCE ( authCode , codeVerifier ) {
const response = await fetch ( 'https://api.aries.com/v1/oauth2/token' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
client_id: process . env . ARIES_CLIENT_ID ,
grant_type: 'code' ,
code: authCode ,
redirect_uri: process . env . ARIES_REDIRECT_URI ,
code_verifier: codeVerifier ,
}),
});
if ( ! response . ok ) {
const err = await response . json ();
throw new Error ( err . error_description || `Token exchange failed: ${ response . status } ` );
}
return await response . json ();
}
def exchange_code_with_pkce ( auth_code : str , code_verifier : str ):
response = requests.post(
'https://api.aries.com/v1/oauth2/token' ,
json = {
'client_id' : os.environ[ 'ARIES_CLIENT_ID' ],
'grant_type' : 'code' ,
'code' : auth_code,
'redirect_uri' : os.environ[ 'ARIES_REDIRECT_URI' ],
'code_verifier' : code_verifier,
}
)
response.raise_for_status()
return response.json()
Response (same as Authorization Code flow):
{
"access_token" : "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." ,
"token_type" : "Bearer" ,
"expires_in" : 3600 ,
"refresh_token" : "eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAifQ..." ,
"scope" : "account:information order:execution market:information"
}
Step 4: Make authenticated API requests
Use the access token in the Authorization header for every API request.
curl -X GET 'https://api.aries.com/v1/users/me/accounts' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
async function callApi ( endpoint , accessToken ) {
const response = await fetch ( `https://api.aries.com ${ endpoint } ` , {
headers: { 'Authorization' : `Bearer ${ accessToken } ` },
signal: AbortSignal . timeout ( 10000 ),
});
if ( response . status === 401 ) {
throw new Error ( 'Token expired — refresh or re-authenticate' );
}
if ( response . status === 403 ) {
throw new Error ( 'Insufficient permissions' );
}
if ( ! response . ok ) throw new Error ( `HTTP ${ response . status } ` );
return await response . json ();
}
def call_api ( endpoint : str , access_token : str ):
response = requests.get(
f 'https://api.aries.com { endpoint } ' ,
headers = { 'Authorization' : f 'Bearer { access_token } ' },
timeout = 10 ,
)
if response.status_code == 401 :
raise Exception ( 'Token expired — refresh or re-authenticate' )
response.raise_for_status()
return response.json()
Step 5: Refresh the access token
Access tokens expire after expires_in seconds (typically 1 hour). Use the refresh token to get a new access token. For public clients (SPAs, mobile apps without a backend), refresh typically requires client_secret. If your PKCE app has no backend, consider using a Backend-for-Frontend (BFF) to perform refresh, or prompt the user to re-authenticate when the access token expires.
Endpoint: POST https://api.aries.com/v1/oauth2/token
If you have a backend (e.g., BFF or server-side PKCE), use the same refresh format as the Authorization Code flow:
curl -X POST 'https://api.aries.com/v1/oauth2/token' \
-H 'Content-Type: application/json' \
-d '{
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"grant_type": "refresh_token",
"refresh_token": "YOUR_REFRESH_TOKEN",
"redirect_uri": "YOUR_REDIRECT_URI"
}'
async function refreshAccessToken ( refreshToken ) {
const response = await fetch ( 'https://api.aries.com/v1/oauth2/token' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
client_id: process . env . ARIES_CLIENT_ID ,
client_secret: process . env . ARIES_CLIENT_SECRET ,
grant_type: 'refresh_token' ,
refresh_token: refreshToken ,
redirect_uri: process . env . ARIES_REDIRECT_URI ,
}),
});
if ( ! response . ok ) throw new Error ( `Token refresh failed: ${ response . status } ` );
return await response . json ();
}
def refresh_access_token ( refresh_token ):
response = requests.post(
'https://api.aries.com/v1/oauth2/token' ,
json = {
'client_id' : os.environ[ 'ARIES_CLIENT_ID' ],
'client_secret' : os.environ[ 'ARIES_CLIENT_SECRET' ],
'grant_type' : 'refresh_token' ,
'refresh_token' : refresh_token,
'redirect_uri' : os.environ[ 'ARIES_REDIRECT_URI' ],
}
)
response.raise_for_status()
return response.json()
Complete SPA example (React)
Minimal React example for a client-side PKCE flow:
// Login: generate challenge and redirect
async function login () {
const codeVerifier = crypto . randomUUID (). replace ( /-/ g , '' ) + crypto . randomUUID (). replace ( /-/ g , '' );
sessionStorage . setItem ( 'pkce_code_verifier' , codeVerifier );
const encoder = new TextEncoder ();
const hash = await crypto . subtle . digest ( 'SHA-256' , encoder . encode ( codeVerifier ));
const codeChallenge = btoa ( String . fromCharCode ( ... new Uint8Array ( hash )))
. replace ( / \+ / g , '-' ). replace ( / \/ / g , '_' ). replace ( /= + $ / , '' );
const state = crypto . randomUUID ();
sessionStorage . setItem ( 'oauth_state' , state );
const params = new URLSearchParams ({
response_type: 'code' ,
client_id: 'YOUR_CLIENT_ID' ,
redirect_uri: ` ${ window . location . origin } /callback` ,
scope: 'user:information account:information' ,
state ,
code_challenge: codeChallenge ,
code_challenge_method: 'S256' ,
});
window . location . href = `https://app.aries.com/oauth2/authorize? ${ params } ` ;
}
// Callback: exchange code for tokens
async function handleCallback () {
const params = new URLSearchParams ( window . location . search );
if ( params . get ( 'state' ) !== sessionStorage . getItem ( 'oauth_state' )) {
alert ( 'Invalid state' );
return ;
}
const code = params . get ( 'code' );
const codeVerifier = sessionStorage . getItem ( 'pkce_code_verifier' );
const res = await fetch ( 'https://api.aries.com/v1/oauth2/token' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
client_id: 'YOUR_CLIENT_ID' ,
grant_type: 'code' ,
code , redirect_uri: ` ${ window . location . origin } /callback` ,
code_verifier: codeVerifier ,
}),
});
const tokens = await res . json ();
sessionStorage . setItem ( 'access_token' , tokens . access_token );
sessionStorage . removeItem ( 'pkce_code_verifier' );
sessionStorage . removeItem ( 'oauth_state' );
window . location . href = '/dashboard' ;
}
Next steps
OAuth2 Overview Scopes, security, rate limits, and troubleshooting.
Authorization Code Flow Server-side flow with client_secret.
Token API Reference Interactive token endpoint reference.
Quick Start Get started in minutes.