After successful authentication, exchange the authorization code for access tokens to retrieve user data. The code exchange process happens on your backend server using OAuth 2.0 with PKCE.
How it works
The authentication flow provides you with two essential values:
code- authorization code from RevolutcodeVerifier- PKCE verification string
You'll receive these values differently depending on your chosen display mode:
Popup display mode - onAuthSuccess callback
When using popup mode, handle the authorization code in the success callback:
const { mountButton } = revolutId.initialise({
clientId: 'your-client-id',
redirectUri: 'https://your-app.com/callback',
scope: ['openid', 'profile', 'email'],
displayMode: 'popup',
onAuthSuccess: async ({ code, codeVerifier }) => {
// Send to your backend for token exchange
try {
const response = await fetch('/api/auth/revolut/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, codeVerifier }),
})
const tokens = await response.json()
console.log('Access token received:', tokens.access_token)
// Use tokens to fetch user data or create session
} catch (error) {
console.error('Token exchange failed:', error)
}
},
})
// Mount button
const buttonClient = mountButton(document.getElementById('auth-button'))Redirect display mode - processRedirectParams
When using redirect mode, process the authorization result on your redirect URI page:
// On your redirect URI page (https://your-app.com/callback)
const result = revolutId.processRedirectParams()
if (result?.status === 'success') {
const { code, codeVerifier } = result
// Send to your backend for token exchange
fetch('/api/auth/revolut/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, codeVerifier }),
})
.then((response) => response.json())
.then((tokens) => {
console.log('Access token received:', tokens.access_token)
// Redirect to authenticated area or update UI
window.location.href = '/dashboard'
})
.catch((error) => {
console.error('Token exchange failed:', error)
})
} else if (result?.status === 'error') {
console.error('Authentication failed:', result.error)
}Backend: Token exchange
Exchange authorization code for access tokens using HTTP Basic Authentication with your client credentials:
All token exchange requests must include Basic Authentication using your client ID and client secret:
- Username: Your client ID
- Password: Your client secret
- Header:
Authorization: Basic <base64(clientId:clientSecret)>
app.post('/api/auth/revolut/exchange', async (req, res) => {
const { code, codeVerifier } = req.body
try {
// Create Basic Auth header with client credentials
const basicAuth = Buffer.from(
`${process.env.REVOLUT_CLIENT_ID}:${process.env.REVOLUT_CLIENT_SECRET}`,
).toString('base64')
const response = await fetch(
'https://sandbox-sso.revolut.com/api/token',
{
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${basicAuth}`,
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
code_verifier: codeVerifier,
}),
},
)
const tokens = await response.json()
if (tokens.id_token) {
res.json({
id_token: tokens.id_token,
})
} else {
res.status(400).json({ error: 'Token exchange failed' })
}
} catch (error) {
res.status(500).json({ error: error.message })
}
})Token response
Successful exchange returns ID token with user information:
{
"id_token": "..."
}Decoding the ID token
ID token is a JWT containing user information:
{
"jti": "...",
"iss": "https://sso.revolut.com",
"aud": "your-client-id",
"iat": 1747670512, // issued at in ms
"exp": 1747670532, // expired at in ms
"amr": [],
"sub": "...",
"amrv": [],
"address": "...",
"phone": "+0000000000",
"phone_verified": "true",
"email": "user@example.com",
"email_verified": "false"
}ID token claims
| Claim | Description |
|---|---|
jti | JWT unique identifier |
iss | Token issuer (Revolut SSO) |
aud | Audience (your client ID) |
iat | Issued at timestamp |
exp | Expiration timestamp |
sub | Subject (unique user identifier) |
address | User's address (if requested in scope) |
phone | User's phone number (if requested in scope) |
phone_verified | Phone verification status |
email | User's email address (if requested in scope) |
email_verified | Email verification status |
Using ID tokens
Verify and decode ID token to access requested user data using jsonwebtoken:
npm install jsonwebtokenyarn add jsonwebtokenpnpm add jsonwebtokenbun add jsonwebtokenimport jwt from 'jsonwebtoken'
import crypto from 'crypto'
async function getPublicKey(kid) {
try {
const response = await fetch(
'https://sso.revolut.com/.well-known/jwks.json',
)
const jwks = await response.json()
const key = jwks.keys.find((k) => k.kid === kid)
if (!key) {
throw new Error(`Key with kid ${kid} not found`)
}
const publicPemKey = crypto.createPublicKey({
key: key,
format: 'jwk',
})
return publicPemKey.export({ type: 'spki', format: 'pem' })
} catch (error) {
throw new Error(`Failed to fetch public key: ${error.message}`)
}
}
async function verifyIdToken(id_token) {
try {
const header = jwt.decode(id_token, { complete: true })?.header
if (!header?.kid) {
throw new Error('Invalid token: missing key ID')
}
const publicKey = await getPublicKey(header.kid)
const decoded = jwt.verify(id_token, publicKey, {
issuer: 'https://sso.revolut.com',
audience: 'your-client-id',
algorithms: ['RS256'],
})
return decoded
} catch (error) {
console.error('Token verification failed:', error.message)
throw error
}
}
const decoded = verifyIdToken(id_token)
console.log('User ID:', decoded.sub)
console.log('Email:', decoded.email)
console.log('Phone:', decoded.phone)
console.log('Address:', decoded.address)Security best practices:
- Never expose client secrets in frontend code
- Always perform token exchange on your secure backend server
- Store tokens securely and implement proper session management
- Use HTTPS for all authentication-related requests
- Validate and verify ID tokens before trusting user information
The environment you use must match your client configuration:
-
Use
productionmode with your production client ID for live applications- exchange code endpoint:
https://sso.revolut.com/token
- exchange code endpoint:
-
Use
sandboxmode with your sandbox client ID for development and testing- exchange code endpoint:
https://sandbox-sso.revolut.com/token
- exchange code endpoint:
See environment configuration for more details.