forgeoauth

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 25, 2026 License: MIT Imports: 18 Imported by: 0

README

forge-oauth

OAuth 2.1 authorization server for remote MCP servers.

Go Reference v0.1.0 — MIT license.


forge-oauth is a standalone Go library that implements an OAuth 2.1 authorization server for use with remote Model Context Protocol servers. ChatGPT Plus and Claude.ai require OAuth 2.1 to connect to remote MCP servers; forge-oauth provides the server-side implementation.

Standards

  • OAuth 2.1 (draft-15): PKCE mandatory, no implicit flow, no ROPC
  • RFC 8414: Authorization Server Metadata
  • RFC 9728: Protected Resource Metadata (via forge-cms.dev/forge-mcp)
  • CIMD: Client ID Metadata Documents — stateless client validation

Features

  • Stateless client validation via CIMD (no client registration database)
  • PKCE S256 — mandatory for all authorization requests
  • Refresh tokens via offline_access scope (required for ChatGPT)
  • HTML authorization form — user pastes their existing Forge bearer token
  • SQLite storage out of the box (modernc.org/sqlite — no CGO)
  • slog-based structured logging

Installation

go get forge-cms.dev/forge-oauth

Requires Go 1.26.3+.

Quick start

import (
    "log"
    "net/http"

    "forge-cms.dev/forge"
    forgeoauth "forge-cms.dev/forge-oauth"
    forgemcp "forge-cms.dev/forge-mcp"
)

func main() {
    app := forge.New(forge.Config{...})

    store, err := forgeoauth.NewSQLiteStore("./forge-oauth.db")
    if err != nil {
        log.Fatal(err)
    }

    oauthSrv := forgeoauth.New(forgeoauth.Config{
        Issuer: "https://cms.example.com",
        VerifyBearer: func(token string) bool {
            // Validate Forge bearer token using forge.VerifyTokenString (v1.25.0+).
            _, ok := forge.VerifyTokenString(token, app.Secret(), app.TokenStore())
            return ok
        },
    }, store)

    mcpSrv := forgemcp.New(app, forgemcp.WithOAuth(oauthSrv))
    http.ListenAndServe(":8080", mcpSrv.Handler())
}

Endpoints

Method Path Description
GET /.well-known/oauth-authorization-server RFC 8414 metadata
GET /oauth/authorize Authorization form
POST /oauth/authorize Form submission
POST /oauth/token Code exchange and token refresh

ChatGPT / ngrok runbook

To test end-to-end with ChatGPT Plus:

  1. Install ngrok: winget install ngrok.ngrok
  2. Configure: ngrok config add-authtoken <your-token>
  3. Start your Forge + MCP server locally on port 8080
  4. Run ngrok http 8080 — note the HTTPS URL (e.g. https://abc123.ngrok-free.app)
  5. Set Issuer: "https://abc123.ngrok-free.app" in forgeoauth.Config
  6. Restart the server with the ngrok URL
  7. In ChatGPT Plus: Settings → Connected Apps → Add → paste the ngrok HTTPS URL
  8. ChatGPT triggers the OAuth flow → browser opens the authorization form
  9. Paste your Forge bearer token → click Approve
  10. ChatGPT receives an access token and can call MCP tools (e.g. list_posts)

Storage

Three SQLite tables are created automatically by NewSQLiteStore:

Table Purpose
forge_oauth_codes Short-lived authorization codes (5 min default)
forge_oauth_tokens Access tokens (1 hour default)
forge_oauth_refresh_tokens Refresh tokens (no expiry in v1)

License

MIT — see LICENSE.

Part of the Forge CMS ecosystem.

Documentation

Overview

Package forgeoauth implements an OAuth 2.1 authorization server for remote MCP servers. It supports the authorization code flow with mandatory PKCE (S256), stateless client validation via Client ID Metadata Documents (CIMD), and optional refresh tokens via the offline_access scope.

Standards

  • OAuth 2.1 (draft-15): PKCE mandatory, no implicit flow, no ROPC
  • RFC 8414: Authorization Server Metadata
  • CIMD: stateless client validation by fetching the client_id URL

Quick start

store, err := forgeoauth.NewSQLiteStore("./oauth.db")
if err != nil {
    log.Fatal(err)
}
srv := forgeoauth.New(forgeoauth.Config{
    Issuer: "https://cms.example.com",
    VerifyBearer: func(token string) bool {
        _, ok := forge.VerifyTokenString(token, app.Secret(), app.TokenStore())
        return ok
    },
}, store)

// srv.Handler() mounts all OAuth endpoints.
// Embed in a larger mux via forgemcp.WithOAuth(srv).

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrTokenNotFound is returned when the access token does not exist in the store.
	ErrTokenNotFound = errors.New("forgeoauth: access token not found")
	// ErrTokenExpired is returned when the access token exists but has passed its ExpiresAt.
	ErrTokenExpired = errors.New("forgeoauth: access token expired")
	// ErrCodeNotFound is returned by [Store.GetCode] when the code does not exist.
	ErrCodeNotFound = errors.New("forgeoauth: authorization code not found")
	// ErrRefreshTokenNotFound is returned by [Store.GetRefreshToken] when the token does not exist.
	ErrRefreshTokenNotFound = errors.New("forgeoauth: refresh token not found")
)

Sentinel errors returned by Server.ValidateAccessToken.

Functions

func VerifyPKCE

func VerifyPKCE(codeVerifier, codeChallenge string) bool

VerifyPKCE checks that BASE64URL(SHA256(codeVerifier)) equals codeChallenge. Uses constant-time comparison to prevent timing attacks. Returns true when the verifier is valid (S256 method only).

Types

type AccessToken

type AccessToken struct {
	// Token is the raw token value.
	Token string
	// ClientID is the HTTPS URL identifying the OAuth client.
	ClientID string
	// Scope is the space-separated scope string for this token.
	Scope string
	// ExpiresAt is the UTC time after which this token is invalid.
	ExpiresAt time.Time
}

AccessToken is a Bearer access token issued after code exchange.

type AuthCode

type AuthCode struct {
	// Code is the raw authorization code value (random hex, 32 bytes).
	Code string
	// ClientID is the HTTPS URL identifying the OAuth client (CIMD).
	ClientID string
	// RedirectURI is the callback URL for this authorization request.
	RedirectURI string
	// Scope is the space-separated scope string requested by the client.
	Scope string
	// CodeChallenge is BASE64URL(SHA256(code_verifier)) (S256 method).
	CodeChallenge string
	// ExpiresAt is the UTC time after which this code must be rejected.
	ExpiresAt time.Time
}

AuthCode is a short-lived, single-use PKCE authorization code.

type CIMDDoc

type CIMDDoc struct {
	ClientID     string   `json:"client_id"`
	ClientName   string   `json:"client_name"`
	RedirectURIs []string `json:"redirect_uris"`
}

CIMDDoc is the parsed client identity metadata document (CIMD). The server fetches this document from the client_id URL on every authorization request — no client registration database required.

type Config

type Config struct {
	// Issuer is the HTTPS base URL of this authorization server.
	// Included in RFC 8414 metadata and in WWW-Authenticate headers.
	// Example: "https://cms.example.com"
	// Required — New panics if empty.
	Issuer string

	// AccessTokenTTL is how long access tokens remain valid.
	// Default: 1 hour.
	AccessTokenTTL time.Duration

	// AuthCodeTTL is how long authorization codes remain valid.
	// Default: 5 minutes.
	AuthCodeTTL time.Duration

	// VerifyBearer validates a Forge bearer token submitted at /oauth/authorize.
	// Returns true if the token authenticates a valid user on the Forge site.
	// Required — New panics if nil.
	//
	// Example using forge.VerifyTokenString (forge-cms.dev/forge v1.25.0+):
	//
	//	VerifyBearer: func(token string) bool {
	//	    _, ok := forge.VerifyTokenString(token, app.Secret(), app.TokenStore())
	//	    return ok
	//	},
	VerifyBearer func(token string) bool

	// HTTPClient is used for CIMD metadata fetches.
	// Default: &http.Client{Timeout: 5 * time.Second}.
	HTTPClient *http.Client
}

Config holds the configuration for the OAuth 2.1 authorization server.

type RefreshToken

type RefreshToken struct {
	// Token is the raw token value.
	Token string
	// ClientID is the HTTPS URL identifying the OAuth client.
	ClientID string
	// Scope is the space-separated scope string for this token.
	Scope string
}

RefreshToken is a long-lived token used to obtain new access tokens. In v1, refresh tokens do not expire. They are issued only when the authorization request includes the [offline_access] scope.

type SQLiteStore

type SQLiteStore struct {
	// contains filtered or unexported fields
}

SQLiteStore is a SQLite-backed implementation of Store. It is safe for concurrent use. The three OAuth tables are created automatically by NewSQLiteStore if they do not already exist.

func NewSQLiteStore

func NewSQLiteStore(path string) (*SQLiteStore, error)

NewSQLiteStore opens (or creates) a SQLite database at path and initialises the three OAuth tables. Use path ":memory:" for in-process testing.

func (*SQLiteStore) Close

func (s *SQLiteStore) Close() error

Close closes the underlying database connection.

func (*SQLiteStore) DeleteCode

func (s *SQLiteStore) DeleteCode(ctx context.Context, code string) error

func (*SQLiteStore) DeleteRefreshToken

func (s *SQLiteStore) DeleteRefreshToken(ctx context.Context, token string) error

func (*SQLiteStore) GetCode

func (s *SQLiteStore) GetCode(ctx context.Context, code string) (AuthCode, error)

func (*SQLiteStore) GetRefreshToken

func (s *SQLiteStore) GetRefreshToken(ctx context.Context, token string) (RefreshToken, error)

func (*SQLiteStore) GetToken

func (s *SQLiteStore) GetToken(ctx context.Context, token string) (AccessToken, error)

func (*SQLiteStore) SaveCode

func (s *SQLiteStore) SaveCode(ctx context.Context, c AuthCode) error

func (*SQLiteStore) SaveRefreshToken

func (s *SQLiteStore) SaveRefreshToken(ctx context.Context, t RefreshToken) error

func (*SQLiteStore) SaveToken

func (s *SQLiteStore) SaveToken(ctx context.Context, t AccessToken) error

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is an OAuth 2.1 authorization server. Create with New; use [Handler] to mount endpoints.

func New

func New(cfg Config, store Store) *Server

New creates a Server with the given configuration and store. Panics if cfg.Issuer is empty or cfg.VerifyBearer is nil.

func (*Server) Handler

func (s *Server) Handler() http.Handler

Handler returns an http.Handler that serves all OAuth 2.1 endpoints:

GET  /.well-known/oauth-authorization-server  — RFC 8414 metadata
GET  /oauth/authorize                          — authorization form
POST /oauth/authorize                          — form submission
POST /oauth/token                              — code exchange and token refresh

func (*Server) Issuer

func (s *Server) Issuer() string

Issuer returns the server's configured issuer URL. Used by forge-mcp to populate the authorization_servers field in /.well-known/oauth-protected-resource (RFC 9728).

func (*Server) ValidateAccessToken

func (s *Server) ValidateAccessToken(ctx context.Context, token string) (*AccessToken, error)

ValidateAccessToken looks up a Bearer access token in the store. Returns the AccessToken record on success. Returns ErrTokenNotFound if the token is unknown, or ErrTokenExpired if the token exists but has passed its ExpiresAt time.

type Store

type Store interface {
	// SaveCode persists an authorization code.
	SaveCode(ctx context.Context, c AuthCode) error
	// GetCode retrieves an authorization code by its code value.
	// Returns [ErrCodeNotFound] if the code does not exist.
	GetCode(ctx context.Context, code string) (AuthCode, error)
	// DeleteCode removes an authorization code (e.g. after single use).
	DeleteCode(ctx context.Context, code string) error

	// SaveToken persists an access token.
	SaveToken(ctx context.Context, t AccessToken) error
	// GetToken retrieves an access token by its token value.
	// Returns [ErrTokenNotFound] if the token does not exist.
	GetToken(ctx context.Context, token string) (AccessToken, error)

	// SaveRefreshToken persists a refresh token.
	SaveRefreshToken(ctx context.Context, t RefreshToken) error
	// GetRefreshToken retrieves a refresh token by its token value.
	// Returns [ErrRefreshTokenNotFound] if the token does not exist.
	GetRefreshToken(ctx context.Context, token string) (RefreshToken, error)
	// DeleteRefreshToken removes a refresh token.
	DeleteRefreshToken(ctx context.Context, token string) error
}

Store provides persistence for OAuth 2.1 state: authorization codes, access tokens, and refresh tokens. All implementations must be safe for concurrent use. SQLiteStore is the bundled production implementation.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL