n8n MCP Bridge
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

  1. Go to Dashboard → OAuth Clients
  2. Click "Create OAuth Client"
  3. Fill in details:
    • Name: Your app name
    • Redirect URIs: Your callback URLs
  4. 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 ID
  • redirect_uri: Must match registered URI
  • response_type: Always code
  • code_challenge: Base64url-encoded SHA-256 hash
  • code_challenge_method: Always S256
  • state: Random CSRF protection token
  • scope: 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:

  1. Authorization: Verify redirect works
  2. PKCE: Check challenge/verifier validation
  3. State: Test CSRF protection
  4. Token exchange: Verify tokens are issued
  5. Refresh: Test token refresh
  6. Error handling: Test all error scenarios

Next Steps

On this page