Skip to content

OAuth 2.1 API ​

Complete OAuth 2.1 implementation with PKCE, dynamic client registration, and refresh token rotation.

Overview ​

Go Bananas! implements OAuth 2.1 for secure, standards-compliant authentication. This enables:

  • Web Applications: Browser-based apps with secure token management
  • Desktop Apps: Native applications like Claude Desktop
  • MCP Clients: Model Context Protocol clients with automatic discovery
  • Third-Party Integrations: Secure API access for external services

Discovery Endpoints ​

Protected Resource Metadata ​

http
GET /.well-known/oauth-protected-resource

Returns information about the protected resource and its authorization servers.

Response:

json
{
  "resource": "https://gobananasai.com",
  "authorization_servers": ["https://gobananasai.com/oauth"],
  "scopes_supported": [
    "images:generate",
    "images:edit",
    "images:read",
    "images:delete",
    "characters:manage",
    "characters:read",
    "products:manage",
    "products:read",
    "styles:manage",
    "styles:read",
    "sessions:read",
    "analytics:read"
  ],
  "bearer_methods_supported": ["header"],
  "resource_documentation": "https://docs.gobananas.io/api"
}

Authorization Server Metadata ​

http
GET /oauth/.well-known/openid-configuration

Returns OAuth 2.1 authorization server configuration for automatic client discovery.

Response:

json
{
  "issuer": "https://gobananasai.com/oauth",
  "authorization_endpoint": "https://gobananasai.com/oauth/authorize",
  "token_endpoint": "https://gobananasai.com/oauth/token",
  "registration_endpoint": "https://gobananasai.com/oauth/register",
  "revocation_endpoint": "https://gobananasai.com/oauth/revoke",
  "code_challenge_methods_supported": ["S256"],
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_basic"],
  "scopes_supported": ["images:generate", "..."]
}

Authorization Endpoint ​

http
GET /oauth/authorize

Initiates the authorization code flow with PKCE.

Required Parameters ​

ParameterDescription
response_typeMust be code
client_idRegistered client identifier
redirect_uriMust match registered URI
code_challengeBASE64URL(SHA256(code_verifier))
code_challenge_methodMust be S256

Optional Parameters ​

ParameterDescription
scopeSpace-separated list of scopes
stateCSRF protection token (recommended)

Example Request ​

bash
GET /oauth/authorize?
  response_type=code&
  client_id=gobananas_abc123&
  redirect_uri=https://myapp.com/callback&
  code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
  code_challenge_method=S256&
  scope=images:generate%20images:read&
  state=xyz123

Response ​

On success, redirects to redirect_uri with:

https://myapp.com/callback?code=AUTH_CODE&state=xyz123

If no session exists, HTML clients are redirected to the console login at /?return_to=.... Non-HTML clients receive login_required JSON with a login_url.

Errors ​

ErrorDescription
unsupported_response_typeresponse_type is not "code"
invalid_requestMissing required parameter
invalid_clientUnknown or inactive client
access_deniedUser denied authorization

Token Endpoint ​

http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

Exchanges authorization codes for tokens or refreshes existing tokens.

Authorization Code Grant ​

ParameterDescription
grant_typeMust be authorization_code
codeAuthorization code from redirect
code_verifierOriginal PKCE verifier (43-128 chars)
client_idClient identifier
redirect_uriSame URI used in authorization

Example:

bash
curl -X POST "https://gobananasai.com/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE" \
  -d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" \
  -d "client_id=gobananas_abc123" \
  -d "redirect_uri=https://myapp.com/callback"

Refresh Token Grant ​

ParameterDescription
grant_typeMust be refresh_token
refresh_tokenValid refresh token
client_idClient identifier
scopeOptional: request subset of original scopes

Example:

bash
curl -X POST "https://gobananasai.com/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=REFRESH_TOKEN" \
  -d "client_id=gobananas_abc123"

Token Response ​

json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
  "scope": "images:generate images:read"
}

Token Lifetimes ​

TokenLifetime
Authorization Code10 minutes
Access Token1 hour
Refresh Token30 days

Errors ​

ErrorDescription
invalid_grantInvalid, expired, or used code/token
invalid_requestMissing required parameter
unsupported_grant_typeGrant type not supported

Dynamic Client Registration ​

http
POST /oauth/register
Content-Type: application/json

Register a new OAuth client without admin intervention.

Request Body ​

FieldRequiredDescription
client_nameYesHuman-readable client name
redirect_urisYesArray of allowed redirect URIs
grant_typesNoDefault: ["authorization_code", "refresh_token"]
token_endpoint_auth_methodNo"none" (public) or "client_secret_basic"
scopeNoDefault requested scope
client_uriNoURL of client's home page
logo_uriNoURL of client's logo

Example:

bash
curl -X POST "https://gobananasai.com/oauth/register" \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "My Image App",
    "redirect_uris": ["https://myapp.com/callback"],
    "scope": "images:generate images:read characters:read"
  }'

Response ​

json
{
  "client_id": "gobananas_abc123def456",
  "client_name": "My Image App",
  "redirect_uris": ["https://myapp.com/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_method": "none",
  "scope": "images:generate images:read characters:read",
  "client_id_issued_at": 1703001234
}

Client Secret

For confidential clients using client_secret_basic, the client_secret is only returned once upon registration. Store it securely.

Redirect URI Requirements ​

  • Must use HTTPS (except localhost and 127.0.0.1 for development)
  • Must exactly match during authorization
  • Multiple URIs can be registered

Scopes Reference ​

ScopeDescription
images:generateGenerate new images
images:editEdit existing images
images:readRead image metadata
images:deleteDelete images
characters:manageCreate, update, delete characters
characters:readList and view characters
products:manageCreate, update, delete products
products:readList and view products
styles:manageCreate, update, delete style presets
styles:readList and view style presets
sessions:readView session history
analytics:readView usage analytics

Using Access Tokens ​

Include the access token in the Authorization header:

bash
curl -X POST "https://mcp.gobananasai.com" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"method":"tools/list","params":{}}'

Dual Authentication

MCP endpoints (/mcp, and the legacy /sse) accept both OAuth access tokens and API keys. The server distinguishes between them:

  • API keys: Start with sk_live_ or sk_test_
  • OAuth tokens: BASE64URL-encoded tokens

Security Features ​

PKCE (Required) ​

PKCE (Proof Key for Code Exchange) is required for all authorization requests:

  1. Generate a random code_verifier (43-128 characters)
  2. Compute code_challenge = BASE64URL(SHA256(code_verifier))
  3. Send code_challenge with authorization request
  4. Send code_verifier when exchanging the code

JavaScript Example:

javascript
// Generate code verifier
function generateCodeVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64UrlEncode(array);
}

// Generate code challenge
async function generateCodeChallenge(verifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const hash = await crypto.subtle.digest('SHA-256', data);
  return base64UrlEncode(new Uint8Array(hash));
}

function base64UrlEncode(bytes) {
  return btoa(String.fromCharCode(...bytes))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
}

Refresh Token Rotation ​

Each time a refresh token is used:

  1. The old refresh token is invalidated
  2. A new refresh token is issued
  3. The old access token is revoked

This limits the window of vulnerability if tokens are compromised.

Replay Attack Detection ​

If a refresh token is used after it has been rotated (indicating possible theft):

  1. The entire token family is revoked
  2. All associated access tokens are invalidated
  3. The error invalid_grant is returned

Authorization Code Single-Use ​

Authorization codes can only be used once. Attempting to reuse a code:

  1. Invalidates the code
  2. Revokes all tokens issued from that authorization
  3. Returns invalid_grant error

Error Response Format ​

All OAuth errors follow RFC 6749 format:

json
{
  "error": "invalid_grant",
  "error_description": "Authorization code has expired"
}

Error Codes ​

CodeHTTP StatusDescription
invalid_request400Missing or malformed parameter
invalid_client401Client authentication failed
invalid_grant400Invalid code or token
unauthorized_client401Client not authorized for grant type
unsupported_grant_type400Grant type not supported
unsupported_response_type400Response type not supported
invalid_scope400Invalid or unknown scope
access_denied403User or server denied request
server_error500Internal server error
invalid_redirect_uri400Invalid redirect URI
invalid_client_metadata400Invalid registration metadata

Complete Flow Example ​

1. Register Client (One-Time) ​

javascript
const registration = await fetch('https://gobananasai.com/oauth/register', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    client_name: 'My App',
    redirect_uris: ['https://myapp.com/callback']
  })
}).then(r => r.json());

const clientId = registration.client_id;

2. Generate PKCE Values ​

javascript
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);

// Store codeVerifier securely for token exchange
sessionStorage.setItem('pkce_verifier', codeVerifier);

3. Redirect to Authorization ​

javascript
const authUrl = new URL('https://gobananasai.com/oauth/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', clientId);
authUrl.searchParams.set('redirect_uri', 'https://myapp.com/callback');
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('scope', 'images:generate images:read');
authUrl.searchParams.set('state', crypto.randomUUID());

window.location.href = authUrl.toString();

4. Handle Callback & Exchange Code ​

javascript
// In callback handler
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const codeVerifier = sessionStorage.getItem('pkce_verifier');

const tokens = await fetch('https://gobananasai.com/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: code,
    code_verifier: codeVerifier,
    client_id: clientId,
    redirect_uri: 'https://myapp.com/callback'
  })
}).then(r => r.json());

// Store tokens securely
localStorage.setItem('access_token', tokens.access_token);
localStorage.setItem('refresh_token', tokens.refresh_token);

5. Make MCP Requests ​

javascript
const tools = await fetch('https://mcp.gobananasai.com', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ method: 'tools/list', params: {} }),
}).then(r => r.json());

6. Refresh Tokens ​

javascript
async function refreshTokens() {
  const tokens = await fetch('https://gobananasai.com/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: localStorage.getItem('refresh_token'),
      client_id: clientId
    })
  }).then(r => r.json());

  localStorage.setItem('access_token', tokens.access_token);
  localStorage.setItem('refresh_token', tokens.refresh_token);

  return tokens;
}

Released under the MIT License.