OAuth 2.1 Authorization Server

BrewHoard's built-in OAuth 2.1 authorization server for MCP clients and third-party integrations.

Overview

BrewHoard includes a fully compliant OAuth 2.1 Authorization Server built on @jmondi/oauth2-server. It supports:

  • Dynamic Client Registration (RFC 7591)
  • Authorization Code + PKCE flow (required for public clients)
  • Access & Refresh Tokens with scope-based permissions
  • Token Revocation (RFC 7009)

The server is used primarily by the MCP server (/api/mcp) to authenticate AI assistants, but can be used by any third-party application.

Endpoints

Dynamic Client Registration

POST /oauth/register

Register a new OAuth client dynamically.

JSON
{
  "client_name": "My MCP Client",
  "redirect_uris": ["http://localhost:3000/callback"],
  "grant_types": ["authorization_code"],
  "token_endpoint_auth_method": "none",
  "scope": "beers:read collection:read ratings:write"
}

Response:

JSON
{
  "client_id": "generated-client-id",
  "client_name": "My MCP Client",
  "redirect_uris": ["http://localhost:3000/callback"],
  "grant_types": ["authorization_code"],
  "token_endpoint_auth_method": "none",
  "scope": "beers:read collection:read ratings:write"
}

Authorization

GET /oauth/authorize

Start the authorization flow. The user must be logged in and will see a consent screen.

Query Parameters:

ParameterRequiredDescription
response_typeYesMust be code
client_idYesClient ID from registration
redirect_uriYesMust match registered URI
scopeYesSpace-separated scopes
stateRecommendedCSRF protection
code_challengeYes (PKCE)S256 or plain challenge
code_challenge_methodYes (PKCE)S256 or plain

Flow:

  1. User is redirected to the login page if not authenticated
  2. User sees a consent screen listing requested scopes
  3. On approval, user is redirected to redirect_uri?code=AUTH_CODE&state=STATE

Consent

GET /oauth/consent

The consent page where users review and approve scope requests. Rendered as a Svelte page with the app name and requested permissions.

POST /oauth/consent/approve

Approve the authorization request.

Token Exchange

POST /oauth/token

Exchange an authorization code for access and refresh tokens.

Request:

JSON
{
  "grant_type": "authorization_code",
  "code": "authorization-code",
  "redirect_uri": "http://localhost:3000/callback",
  "client_id": "your-client-id",
  "code_verifier": "pkce-verifier"
}

Response:

JSON
{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "dGhpcyBpcyBh...",
  "scope": "beers:read collection:read"
}

Token Revocation

POST /oauth/revoke

Revoke an access or refresh token.

JSON
{
  "token": "access-or-refresh-token",
  "token_type_hint": "access_token"
}

Client Revocation

POST /api/v1/oauth/revoke-client

Revoke all tokens for a specific OAuth client. Requires authentication.

JSON
{
  "clientId": "client-id-to-revoke"
}

Discovery

Authorization Server Metadata

GET /.well-known/oauth-authorization-server

Returns OAuth 2.0 Authorization Server Metadata (RFC 8414):

JSON
{
  "issuer": "http://localhost:5173",
  "authorization_endpoint": "http://localhost:5173/oauth/authorize",
  "token_endpoint": "http://localhost:5173/oauth/token",
  "registration_endpoint": "http://localhost:5173/oauth/register",
  "revocation_endpoint": "http://localhost:5173/oauth/revoke",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_post"],
  "code_challenge_methods_supported": ["S256"],
  "scopes_supported": [
    "beers:read", "beers:search", "beers:styles",
    "collection:read", "collection:write", "collection:consume", "collection:history",
    "ratings:write",
    "recommendations:read",
    "analytics:read",
    "export"
  ]
}

Available Scopes

ScopeDescription
beers:readView beer details
beers:searchSearch for beers
beers:stylesList beer styles
collection:readView your collection
collection:writeAdd/remove collection items
collection:consumeMark beers as consumed
collection:historyView consumption history
ratings:writeCreate and update ratings
recommendations:readGet personalized recommendations
analytics:readView collection statistics and trends
exportExport collection data

Database Schema

OAuth data is stored in dedicated tables:

TablePurpose
oauth_scopeAvailable OAuth scopes
oauth_clientRegistered applications
oauth_client_scopeClient ↔ scope mappings
oauth_auth_codeAuthorization codes (short-lived)
oauth_auth_code_scopeAuth code ↔ scope mappings
oauth_tokenAccess & refresh tokens
oauth_token_scopeToken ↔ scope mappings

All tokens and auth codes support revocation. Expired entries should be cleaned up periodically.

Security Considerations

  • PKCE is required for all public clients (no client_secret)
  • Authorization codes expire after 60 seconds and are single-use
  • Access tokens expire after 1 hour
  • Refresh tokens can be exchanged for new access tokens
  • All tokens can be revoked individually or per-client
  • Scope changes require re-authorization

Next Steps