Guides
OAuth 2.1 Flow
Understanding the OAuth authorization flow
OAuth 2.1 Flow
Complete guide to implementing OAuth 2.1 authentication with n8n MCP Bridge.
Overview
OAuth 2.1 provides secure, token-based authentication for applications that need to access n8n MCP Bridge on behalf of users.
Flow Diagram
┌──────┐ ┌──────────────┐
│Client│ │n8n MCP Bridge│
└───┬──┘ └──────┬───────┘
│ │
│ 1. Authorization Request │
├───────────────────────────────────────────>│
│ with code_challenge │
│ │
│ 2. User Login & Consent │
│<───────────────────────────────────────────┤
│ │
│ 3. Authorization Code │
│<───────────────────────────────────────────┤
│ ?code=ABC123&state=xyz │
│ │
│ 4. Token Exchange │
├───────────────────────────────────────────>│
│ with code_verifier │
│ │
│ 5. Access & Refresh Tokens │
│<───────────────────────────────────────────┤
│ │
│ 6. MCP Request │
├───────────────────────────────────────────>│
│ Authorization: Bearer {token} │
│ │
│ 7. Response │
│<───────────────────────────────────────────┤
│ │
Step-by-Step Implementation
Step 1: Register OAuth Client
- Go to Dashboard → OAuth Clients
- Click "Create OAuth Client"
- Fill in details:
- Name: Your app name
- Redirect URIs: Your callback URLs
- Save client ID and secret
Step 2: Generate PKCE Pair
import { randomBytes, createHash } from 'crypto'
// Generate code verifier (43-128 characters)
const codeVerifier = randomBytes(32).toString('base64url')
// Generate code challenge
const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url')
// Store verifier securely for later
sessionStorage.setItem('code_verifier', codeVerifier)
Step 3: Authorization Request
Redirect user to:
https://mcp.example.com/oauth/authorize?
client_id=mcp_client_abc123&
redirect_uri=https://yourapp.com/callback&
response_type=code&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256&
state=random_state_value&
scope=n8n:execute
Parameters:
client_id: Your OAuth client IDredirect_uri: Must match registered URIresponse_type: Alwayscodecode_challenge: Base64url-encoded SHA-256 hashcode_challenge_method: AlwaysS256state: Random CSRF protection tokenscope: Requested permissions (optional)
Step 4: User Authorization
User sees consent screen:
[Your App] wants to access your n8n workflows
This will allow the app to:
✓ Execute workflows
✓ View workflow results
✓ Access your n8n connection
[Authorize] [Deny]
Step 5: Handle Callback
User is redirected back with code:
// URL: https://yourapp.com/callback?code=AUTH123&state=xyz
const urlParams = new URLSearchParams(window.location.search)
const code = urlParams.get('code')
const state = urlParams.get('state')
// Verify state matches
const savedState = sessionStorage.getItem('oauth_state')
if (state !== savedState) {
throw new Error('CSRF attack detected')
}
Step 6: Exchange Code for Tokens
const codeVerifier = sessionStorage.getItem('code_verifier')
const response = await fetch('https://mcp.example.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: 'https://yourapp.com/callback',
client_id: 'mcp_client_abc123',
client_secret: 'mcp_secret_xyz789',
code_verifier: codeVerifier,
}),
})
const tokens = await response.json()
/*
{
"access_token": "mcp_access_abc123...",
"refresh_token": "mcp_refresh_xyz789...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "n8n:execute"
}
*/
// Store tokens securely
sessionStorage.setItem('access_token', tokens.access_token)
sessionStorage.setItem('refresh_token', tokens.refresh_token)
Step 7: Use Access Token
const accessToken = sessionStorage.getItem('access_token')
const response = await fetch('https://mcp.example.com/mcp', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
method: 'tools/call',
params: {
name: 'ping_n8n',
arguments: {},
},
}),
})
const result = await response.json()
Step 8: Refresh Tokens
When access token expires:
const refreshToken = sessionStorage.getItem('refresh_token')
const response = await fetch('https://mcp.example.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: 'mcp_client_abc123',
client_secret: 'mcp_secret_xyz789',
}),
})
const newTokens = await response.json()
// Update stored tokens
sessionStorage.setItem('access_token', newTokens.access_token)
sessionStorage.setItem('refresh_token', newTokens.refresh_token)
Security Best Practices
PKCE (Required)
Always use PKCE:
- ✅ Prevents authorization code interception
- ✅ Required for all clients
- ✅ Use SHA-256 for challenge method
State Parameter
Always validate state:
- ✅ Prevents CSRF attacks
- ✅ Generate random value per request
- ✅ Verify matches on callback
Redirect URI
Secure redirect URIs:
- ✅ Exact match required
- ✅ Use HTTPS in production
- ✅ Localhost OK for development
- ❌ No wildcards allowed
Token Storage
Store tokens securely:
- ✅ Access token: Memory only
- ✅ Refresh token: Encrypted storage
- ❌ Never in localStorage
- ❌ Never in URLs
Error Handling
Authorization Errors
// URL: ?error=access_denied&error_description=User+denied
const error = urlParams.get('error')
const errorDescription = urlParams.get('error_description')
if (error === 'access_denied') {
// User denied authorization
console.log('User denied access')
}
Token Exchange Errors
const response = await fetch('/oauth/token', { ... })
if (!response.ok) {
const error = await response.json()
/*
{
"error": "invalid_grant",
"error_description": "Invalid authorization code"
}
*/
}
Complete Example
class OAuth2Client {
constructor(clientId, clientSecret, redirectUri) {
this.clientId = clientId
this.clientSecret = clientSecret
this.redirectUri = redirectUri
}
// Generate PKCE pair
generatePKCE() {
const verifier = randomBytes(32).toString('base64url')
const challenge = createHash('sha256').update(verifier).digest('base64url')
return { verifier, challenge }
}
// Start authorization
authorize() {
const { verifier, challenge } = this.generatePKCE()
const state = randomBytes(16).toString('hex')
sessionStorage.setItem('code_verifier', verifier)
sessionStorage.setItem('oauth_state', state)
const authUrl = new URL('https://mcp.example.com/oauth/authorize')
authUrl.searchParams.set('client_id', this.clientId)
authUrl.searchParams.set('redirect_uri', this.redirectUri)
authUrl.searchParams.set('response_type', 'code')
authUrl.searchParams.set('code_challenge', challenge)
authUrl.searchParams.set('code_challenge_method', 'S256')
authUrl.searchParams.set('state', state)
window.location.href = authUrl.toString()
}
// Handle callback
async handleCallback() {
const params = new URLSearchParams(window.location.search)
const code = params.get('code')
const state = params.get('state')
// Verify state
if (state !== sessionStorage.getItem('oauth_state')) {
throw new Error('Invalid state')
}
// Exchange code for tokens
const response = await fetch('https://mcp.example.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: this.redirectUri,
client_id: this.clientId,
client_secret: this.clientSecret,
code_verifier: sessionStorage.getItem('code_verifier'),
}),
})
const tokens = await response.json()
return tokens
}
// Refresh access token
async refreshToken(refreshToken) {
const response = await fetch('https://mcp.example.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: this.clientId,
client_secret: this.clientSecret,
}),
})
return await response.json()
}
}
Testing
Test your OAuth implementation:
- Authorization: Verify redirect works
- PKCE: Check challenge/verifier validation
- State: Test CSRF protection
- Token exchange: Verify tokens are issued
- Refresh: Test token refresh
- Error handling: Test all error scenarios