oauth

package
v1.4.3 Latest Latest
Warning

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

Go to latest
Published: Mar 4, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

oauth

OAuth 2.0 helper package for Go. Provides discovery, dynamic client registration, and three authorization flows — all composable and using the standard context pattern for custom HTTP client injection.

Package overview

Function / Type Inputs Outputs Description
Discover URL *OAuthMetadata Fetch server metadata via RFC 8414 / OIDC discovery
Register *OAuthMetadata, client name, redirect URIs *OAuthCredentials RFC 7591 dynamic client registration; returned credentials carry Metadata
AuthorizeWithBrowser *OAuthCredentials, net.Listener, OpenFunc, scopes *OAuthCredentials Authorization Code + PKCE via loopback redirect
AuthorizeWithCode *OAuthCredentials, PromptFunc, scopes *OAuthCredentials Authorization Code + PKCE via manual code paste
AuthorizeWithDevice *OAuthCredentials, DevicePromptFunc, scopes *OAuthCredentials Device Authorization Grant — CLI / headless devices
AuthorizeWithCredentials *OAuthCredentials, scopes *OAuthCredentials Client Credentials grant — machine-to-machine, no user interaction
(*OAuthCredentials).Refresh *OAuthCredentials, ctx error Refresh an access token using a stored refresh token
(*OAuthCredentials).Revoke *OAuthCredentials, ctx error Revoke access and refresh tokens (RFC 7009); clears token on success
(*OAuthCredentials).Introspect *OAuthCredentials, ctx *IntrospectionResponse, error Query active status and metadata for the current token (RFC 7662)
(*OAuthCredentials).Valid *OAuthCredentials bool Returns true if a non-nil, non-expired token is set

Custom HTTP client

All functions honour a custom HTTP client injected via context — useful for testing, proxies, or mutual TLS:

ctx = context.WithValue(ctx, oauth2.HTTPClient, myClient)

Flow 1 — Browser (Authorization Code + PKCE via loopback)

The most common interactive flow. Opens a browser, starts a local HTTP server to receive the redirect, and exchanges the code for a token automatically.

import (
    "context"
    "net"
    "os/exec"

    "github.com/mutablelogic/go-client/pkg/oauth"
)

func main() {
    ctx := context.Background()

    // 1. Discover server metadata
    metadata, err := oauth.Discover(ctx, "https://mcp.asana.com/v2/mcp")
    if err != nil {
        log.Fatal(err)
    }

    // 2. Allocate listener first — the port determines the redirect URI
    ln, err := net.Listen("tcp", "localhost:0")
    if err != nil {
        log.Fatal(err)
    }
    redirectURI := "http://" + ln.Addr().String() + "/callback"

    // 3. Register the client (if the server supports it)
    //    Pass the redirect URI so the server accepts the callback
    var creds *oauth.OAuthCredentials
    if metadata.SupportsRegistration() {
        creds, err = oauth.Register(ctx, metadata, "my-app", redirectURI)
        if err != nil {
            log.Fatal(err)
        }
    } else {
        // Use a pre-registered client ID from the provider's developer portal
        creds = &oauth.OAuthCredentials{
            Metadata: metadata,
            ClientID: os.Getenv("CLIENT_ID"),
        }
    }

    // 4. Open the browser and wait for the callback
    open := func(url string) error {
        return exec.Command("open", url).Start() // macOS; use "xdg-open" on Linux
    }
    creds, err = oauth.AuthorizeWithBrowser(ctx, creds, ln, open, "openid", "email")
    if err != nil {
        log.Fatal(err)
    }

    // creds.AccessToken, creds.RefreshToken are now set
    fmt.Println("access token:", creds.AccessToken)

    // 5. Later: refresh the token when it expires
    if err := creds.Refresh(ctx); err != nil {
        log.Fatal(err)
    }
}

Flow 2 — Manual code paste (Authorization Code + PKCE)

For environments where opening a browser is possible but automating the redirect isn't — e.g. a remote SSH session. The user visits the URL manually and pastes the code back.

import (
    "bufio"
    "fmt"
    "os"

    "github.com/mutablelogic/go-client/pkg/oauth"
)

func main() {
    ctx := context.Background()

    metadata, err := oauth.Discover(ctx, "https://accounts.google.com")
    if err != nil {
        log.Fatal(err)
    }

    // PromptFunc: display the URL and read the pasted code from stdin
    prompt := func(authURL string) (string, error) {
        fmt.Println("Open this URL in your browser:")
        fmt.Println(authURL)
        fmt.Print("Paste the authorization code: ")
        scanner := bufio.NewScanner(os.Stdin)
        scanner.Scan()
        return strings.TrimSpace(scanner.Text()), nil
    }

    creds, err := oauth.AuthorizeWithCode(ctx, &oauth.OAuthCredentials{
        Metadata: metadata,
        ClientID: "my-client-id",
    }, prompt, "openid", "email")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("access token:", creds.AccessToken)
}

Flow 3 — Refresh

Once you have credentials with a refresh token (from either flow above), refresh them on demand. The oauth2 library handles the 10-second expiry buffer automatically — if the token is still valid, no network call is made.

// Persist creds to disk / keychain after authorization, then on each run:
// Wire up the callback before refreshing so rotated tokens are saved.
creds.OnRefresh = func(c *oauth.OAuthCredentials) error {
    return saveCredentials(c)
}
if err := creds.Refresh(ctx); err != nil {
    log.Fatal(err)
}
// creds.AccessToken is now fresh

To use a custom HTTP client for the refresh request:

ctx = context.WithValue(ctx, oauth2.HTTPClient, myClient)
if err := creds.Refresh(ctx); err != nil {
    log.Fatal(err)
}

Flow 4 — Device Code (CLI / headless)

For devices or CLI tools that can display a URL and code but cannot receive a browser redirect. The user visits the URL on any device, enters the code, and the CLI polls until authorization completes.

prompt := func(userCode, verificationURI string) error {
    fmt.Printf("Visit %s and enter code: %s\n", verificationURI, userCode)
    return nil
}

creds, err := oauth.AuthorizeWithDevice(ctx, &oauth.OAuthCredentials{
    Metadata: metadata,
    ClientID: clientID,
}, prompt, "openid", "email")
if err != nil {
    log.Fatal(err)
}

fmt.Println("access token:", creds.AccessToken)

Flow 5 — Client Credentials (machine-to-machine)

No user interaction. The client authenticates directly with its own credentials. Used for daemons, background services, and server-to-server calls. Note: servers do not issue refresh tokens for this grant — call AuthorizeWithCredentials again when the token expires.

metadata, err := oauth.Discover(ctx, "https://auth.example.com")
if err != nil {
    log.Fatal(err)
}

creds, err := oauth.AuthorizeWithCredentials(ctx, &oauth.OAuthCredentials{
    Metadata:     metadata,
    ClientID:     os.Getenv("CLIENT_ID"),
    ClientSecret: os.Getenv("CLIENT_SECRET"),
}, "api:read", "api:write") // scopes optional
if err != nil {
    log.Fatal(err)
}

fmt.Println("access token:", creds.AccessToken)

Discovery

Discover tries RFC 8414 root paths first (/.well-known/oauth-authorization-server, /.well-known/openid-configuration), then walks up the URL path for servers like Keycloak that publish metadata under realm-specific paths.

// Root discovery (Google, Asana, Facebook, etc.)
metadata, err := oauth.Discover(ctx, "https://accounts.google.com")

// Path-relative discovery (Keycloak realm)
metadata, err := oauth.Discover(ctx, "https://keycloak.example.com/realms/myrealm")

// Any path under the server — walks up to find the metadata
metadata, err := oauth.Discover(ctx, "https://mcp.asana.com/v2/mcp")

Useful metadata checks:

metadata.SupportsRegistration()               // RFC 7591 dynamic client registration
metadata.SupportsDeviceFlow()                 // RFC 8628 device authorization
metadata.SupportsPKCE()                       // any PKCE method
metadata.SupportsS256()                       // S256 specifically
metadata.SupportsRevocation()                 // RFC 7009 token revocation
metadata.SupportsIntrospection()              // RFC 7662 token introspection
metadata.SupportsFlow(oauth.OAuthFlowAuthorizationCode) // returns error if unsupported
metadata.ValidateScopes("openid", "email")    // returns error listing any unsupported scopes

Dynamic client registration

Register performs RFC 7591 Dynamic Client Registration. The returned *OAuthCredentials has ClientID and ClientSecret set but no token — pass it directly into an authorization flow.

// Servers that require redirect_uris (e.g. Asana MCP):
creds, err := oauth.Register(ctx, metadata, "my-app", "http://localhost:8080/callback")

// Servers that don't require redirect_uris (device / client_credentials flows):
creds, err := oauth.Register(ctx, metadata, "my-app")

Token lifecycle — Revoke, Introspect, Valid

Revoke (RFC 7009)

Revoke both the access token and (if present) the refresh token. The credentials' Token field is cleared on success so accidental reuse is prevented. If the server does not advertise a revocation_endpoint, the call is a no-op.

if metadata.SupportsRevocation() {
    if err := creds.Revoke(ctx); err != nil {
        log.Fatal(err)
    }
    // creds.Token is now nil
}
Introspect (RFC 7662)

Query the server for metadata about the current access token. Useful for debugging, or for verifying that a token received from a third party is still active.

if metadata.SupportsIntrospection() {
    info, err := creds.Introspect(ctx)
    if err != nil {
        log.Fatal(err)
    }
    if !info.Active {
        fmt.Println("token has been revoked or expired server-side")
    } else {
        fmt.Println("scopes:", info.Scope)
        fmt.Println("expires:", info.Expiry)
    }
}

The IntrospectionResponse struct mirrors RFC 7662 §2.2:

type IntrospectionResponse struct {
    Active    bool      // false means token is invalid
    Scope     string    // space-separated list of scopes
    ClientID  string
    Username  string
    TokenType string    // e.g. "Bearer"
    Expiry    time.Time // zero if not returned by server
    Subject   string    // sub
    Issuer    string    // iss
    JWTID     string    // jti
}
Valid

Convenience check that the credentials contain a non-nil, non-expired token without making any network calls:

if !creds.Valid() {
    if err := creds.Refresh(ctx); err != nil {
        log.Fatal(err)
    }
}

OAuthFlow constants

oauth.OAuthFlowAuthorizationCode   // "authorization_code"
oauth.OAuthFlowDeviceCode          // "urn:ietf:params:oauth:grant-type:device_code"
oauth.OAuthFlowClientCredentials   // "client_credentials"
oauth.OAuthFlowRefreshToken        // "refresh_token"

Used with metadata.SupportsFlow(flow) and metadata.SupportsGrantType(flow).


OAuthCredentials

type OAuthCredentials struct {
    *oauth2.Token               // AccessToken, RefreshToken, Expiry, TokenType

    ClientID     string         // OAuth client ID
    ClientSecret string         // OAuth client secret (omitted for public clients)
    TokenURL     string         // token endpoint — stored for refresh without re-discovery
    Metadata     *OAuthMetadata // server metadata — carried from Register into Authorize functions
    OnRefresh    func(*OAuthCredentials) error // called when a new token is fetched; not serialised
}

OnRefresh is invoked only when the server actually returns a new token (i.e. the old one was expired or about to expire). Set it once after loading credentials to persist rotated refresh tokens:

creds.OnRefresh = func(c *OAuthCredentials) error {
    return saveCredentials(c) // write to keychain / file / database
}

Documentation

Overview

Package oauth provides helpers for OAuth 2.0 authorization flows, token lifecycle management, and server metadata discovery.

Discovery and registration

Discover fetches OAuth 2.0 Authorization Server Metadata (RFC 8414) or OpenID Connect Discovery metadata (OIDC Discovery 1.0). Pass any URL on the server — the function walks up the path until it finds the well-known document.

Register performs RFC 7591 Dynamic Client Registration and returns an OAuthCredentials with the assigned ClientID and Secret embedded, ready to pass straight into an authorization function.

Authorization flows

Token lifecycle

Custom HTTP client

Every function that makes a network call honours a custom *http.Client injected via context:

ctx = context.WithValue(ctx, oauth2.HTTPClient, myClient)

Index

Constants

View Source
const (
	// OAuthWellKnownPath is the standard OAuth 2.0 Authorization Server Metadata endpoint (RFC 8414).
	OAuthWellKnownPath = "/.well-known/oauth-authorization-server"

	// OIDCWellKnownPath is the OpenID Connect Discovery endpoint (OpenID Connect Discovery 1.0).
	OIDCWellKnownPath = "/.well-known/openid-configuration"

	// OAuthProtectedResourcePath is the Protected Resource Metadata endpoint (RFC 9728).
	OAuthProtectedResourcePath = "/.well-known/oauth-protected-resource"
)

Variables

View Source
var (
	ErrInvalidToken = errors.New("Invalid token")
)

Functions

This section is empty.

Types

type DevicePromptFunc

type DevicePromptFunc func(userCode, verificationURI string) error

DevicePromptFunc is called with the user code and verification URI once the device authorization request succeeds. The implementation should display them to the user — e.g. print to stdout or show a QR code — and then return. AuthorizeWithDevice polls for the token automatically after this function returns.

type IntrospectionResponse

type IntrospectionResponse struct {
	// Active indicates whether the token is currently active. A false value
	// means the token is expired, revoked, or otherwise invalid.
	Active bool `json:"active"`

	// Scope is a space-separated list of scopes associated with the token.
	Scope string `json:"scope,omitempty"`

	// ClientID is the client identifier for the OAuth client that requested
	// the token.
	ClientID string `json:"client_id,omitempty"`

	// Username is a human-readable identifier for the resource owner.
	Username string `json:"username,omitempty"`

	// TokenType is the type of the token (e.g. "Bearer").
	TokenType string `json:"token_type,omitempty"`

	// Expiry is when the token expires (from the exp claim). Zero means absent.
	Expiry time.Time `json:"-"`

	// NotBefore is the earliest time the token is valid (from the nbf claim). Zero means absent.
	NotBefore time.Time `json:"-"`

	// IssuedAt is when the token was issued (from the iat claim). Zero means absent.
	IssuedAt time.Time `json:"-"`

	// Audience is the intended recipients of the token (from the aud claim).
	// Per RFC 7519 §4.1.3 aud may be a single string or an array; both are
	// normalised to a slice here.
	Audience []string `json:"-"`

	// Subject is the subject of the token (typically the user ID).
	Subject string `json:"sub,omitempty"`

	// Issuer identifies the principal that issued the token.
	Issuer string `json:"iss,omitempty"`

	// JWTID is a unique identifier for the token.
	JWTID string `json:"jti,omitempty"`
}

IntrospectionResponse represents an OAuth 2.0 Token Introspection response (RFC 7662 §2.2). The Active field is always populated; all other fields are optional and depend on what the server chooses to return.

The Unix timestamp claims (exp, nbf, iat) and the aud claim are decoded automatically; use the typed fields (Expiry, NotBefore, IssuedAt, Audience) rather than the raw JSON fields.

func (*IntrospectionResponse) UnmarshalJSON

func (r *IntrospectionResponse) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler for IntrospectionResponse. It handles the Unix timestamp fields (exp, nbf, iat) and the polymorphic aud claim (single string or array) that cannot be decoded with plain tags.

type OAuth added in v1.4.1

type OAuth struct {
	oauth2.Token

	// ClientID is the OAuth client ID used to obtain this token.
	ClientID string `json:"client_id"`

	// ClientSecret is the OAuth client secret, if any (for confidential clients).
	// Needed for token refresh with servers that require client authentication.
	ClientSecret string `json:"client_secret,omitempty"`

	// Endpoint is the server endpoint used for discovery
	Endpoint string `json:"endpoint"`

	// TokenURL is the OAuth token endpoint URL (used for refresh without re-discovery).
	TokenURL string `json:"token_url"`
}

func NewConfig added in v1.4.1

func NewConfig(cfg *oauth2.Config) (*OAuth, error)

NewConfig creates a new OAuth configuration from the given oauth2.Config.

func NewToken added in v1.4.1

func NewToken(orig *oauth2.Token) (*OAuth, error)

NewToken creates a new Token from the given oauth2.Token. The Token will be valid if the oauth2.Token is valid and not expired.

func (*OAuth) Valid added in v1.4.1

func (o *OAuth) Valid() bool

Valid returns true if the Token is valid and not expired. Otherwise, login is required in order to generate a token

type OAuthCredentials

type OAuthCredentials struct {
	*oauth2.Token

	// ClientID is the OAuth client ID used to obtain this token.
	ClientID string `json:"client_id"`

	// ClientSecret is the OAuth client secret, if any (for confidential clients).
	// Needed for token refresh with servers that require client authentication.
	ClientSecret string `json:"client_secret,omitempty"`

	// TokenURL is the OAuth token endpoint URL (used for refresh without re-discovery).
	TokenURL string `json:"token_url"`

	// Metadata is the server's OAuth 2.0 Authorization Server Metadata.
	// It is populated by Register and carried through into the authorize functions
	// so callers do not need to pass it separately.
	Metadata *OAuthMetadata `json:"metadata,omitempty"`

	// RedirectURI is the redirect URI sent in the authorization request and token
	// exchange. Defaults to empty (server default). Set to
	// "urn:ietf:wg:oauth:2.0:oob" for native/CLI apps that display the code
	// in the browser rather than redirecting to a callback server.
	RedirectURI string `json:"redirect_uri,omitempty"`

	// OnRefresh is an optional callback invoked after a new token is obtained
	// from the server (i.e. when the old token was expired or about to expire).
	// It is not called when the existing token is still valid.
	// Use it to persist updated credentials — especially important when the
	// server rotates refresh tokens on each use.
	//
	// If the callback returns an error, Refresh propagates it.
	// OnRefresh is not serialised to JSON.
	OnRefresh func(*OAuthCredentials) error `json:"-"`
}

OAuthCredentials bundles an OAuth token with the metadata needed to refresh or reuse it later without re-discovering or re-registering.

func AuthorizeWithBrowser

func AuthorizeWithBrowser(ctx context.Context, creds *OAuthCredentials, listener net.Listener, open OpenFunc, scopes ...string) (*OAuthCredentials, error)

AuthorizeWithBrowser performs an OAuth 2.0 Authorization Code flow with PKCE using a loopback redirect. It:

  1. Derives redirect_uri from the listener address (http://localhost:PORT/callback)
  2. Starts a temporary HTTP server on the listener
  3. Calls open(authURL) — typically to launch a browser
  4. Waits for the provider to redirect back with the authorization code
  5. Exchanges the code for tokens and shuts the server down

The creds parameter must have Metadata and ClientID set (e.g. obtained from Register or constructed manually). If no scopes are provided, the scope parameter is omitted and the server applies its own defaults (RFC 6749 §3.3). The caller is responsible for creating the listener, e.g.:

ln, _ := net.Listen("tcp", "localhost:0") // random port

To use a custom HTTP client for the token exchange, inject it into the context:

ctx = context.WithValue(ctx, oauth2.HTTPClient, myClient)

func AuthorizeWithCode

func AuthorizeWithCode(ctx context.Context, creds *OAuthCredentials, prompt PromptFunc, scopes ...string) (*OAuthCredentials, error)

AuthorizeWithCode performs an interactive OAuth 2.0 Authorization Code flow with PKCE. The creds parameter must have Metadata and ClientID set (e.g. obtained from Register or constructed manually). The prompt callback is called with the authorization URL; it should present the URL to the user and return the authorization code they paste back. If no scopes are provided, the scope parameter is omitted and the server applies its own defaults (RFC 6749 §3.3). The returned credentials carry the token and preserve Metadata for subsequent calls.

To use a custom HTTP client for the token exchange, inject it into the context with:

ctx = context.WithValue(ctx, oauth2.HTTPClient, myClient)

func AuthorizeWithCredentials

func AuthorizeWithCredentials(ctx context.Context, creds *OAuthCredentials, scopes ...string) (*OAuthCredentials, error)

AuthorizeWithCredentials performs an OAuth 2.0 Client Credentials grant (RFC 6749 §4.4). This is the machine-to-machine flow: no user interaction is required. The creds parameter must have Metadata, ClientID, and ClientSecret set.

Scopes are optional; pass none to request the server's default scopes. The returned credentials contain the access token. Refresh tokens are not issued by servers for this grant type — call AuthorizeWithCredentials again when the token expires, or use the returned OnRefresh pattern with a wrapper.

To use a custom HTTP client, inject it into the context:

ctx = context.WithValue(ctx, oauth2.HTTPClient, myClient)

func AuthorizeWithDevice

func AuthorizeWithDevice(ctx context.Context, creds *OAuthCredentials, prompt DevicePromptFunc, scopes ...string) (*OAuthCredentials, error)

AuthorizeWithDevice performs an OAuth 2.0 Device Authorization Grant (RFC 8628). It is suited to CLI tools and devices that cannot open a browser themselves.

  1. Requests a device code from the server.
  2. Calls prompt with the user_code and verification_uri.
  3. Polls the token endpoint until the user completes authorization or the code expires.

The creds parameter must have Metadata and ClientID set (e.g. obtained from Register or constructed manually). If no scopes are provided, "openid" is requested by default.

To use a custom HTTP client, inject it into the context:

ctx = context.WithValue(ctx, oauth2.HTTPClient, myClient)

func Register

func Register(ctx context.Context, metadata *OAuthMetadata, clientName string, redirectURIs ...string) (*OAuthCredentials, error)

Register performs RFC 7591 Dynamic Client Registration against the server described by metadata. It registers a new client with the given name and returns credentials containing the assigned ClientID and ClientSecret (if any). The returned OAuthCredentials has no token yet — pass it to AuthorizeWithCode, AuthorizeWithBrowser, or AuthorizeWithDeviceCode to obtain one.

redirectURIs are required by some servers (e.g. for authorization_code flows); omit them for device or client_credentials flows if the server allows it.

To use a custom HTTP client, inject it into the context:

ctx = context.WithValue(ctx, oauth2.HTTPClient, myClient)

func (*OAuthCredentials) Introspect

func (creds *OAuthCredentials) Introspect(ctx context.Context) (*IntrospectionResponse, error)

Introspect queries the server's introspection endpoint (RFC 7662) for metadata about the current access token. It returns an IntrospectionResponse whose Active field indicates whether the token is currently valid.

Returns an error if the server does not advertise an introspection_endpoint, if the credentials have no token, or if the network call fails.

To use a custom HTTP client, inject it into the context:

ctx = context.WithValue(ctx, oauth2.HTTPClient, myClient)

func (*OAuthCredentials) Refresh

func (creds *OAuthCredentials) Refresh(ctx context.Context) error

Refresh obtains a fresh access token. The strategy depends on the grant type:

  • If the token is still valid, it is returned as-is (no network call).
  • If a refresh token is present, it is exchanged for a new access token (RFC 6749 §6). The oauth2 library applies a 10-second expiry buffer.
  • If no refresh token is present but a client secret is set, the Client Credentials grant (RFC 6749 §4.4) is used to obtain a fresh token. This is the normal path for machine-to-machine tokens, which servers never issue refresh tokens for.

When a new token is fetched from the server, OnRefresh (if set) is called with the updated credentials. Use it to persist rotated refresh tokens.

func (*OAuthCredentials) Revoke

func (creds *OAuthCredentials) Revoke(ctx context.Context) error

Revoke revokes the current access token (and optionally the refresh token) by calling the server's revocation endpoint (RFC 7009).

The token to revoke is chosen as follows:

  • If the access token is set, it is revoked first.
  • If the refresh token is set, it is revoked as well.

After a successful call both tokens are cleared on the credentials struct so accidental reuse is prevented.

Revoke is a no-op (returns nil) when neither token is set or when the server does not advertise a revocation_endpoint.

To use a custom HTTP client, inject it into the context:

ctx = context.WithValue(ctx, oauth2.HTTPClient, myClient)

func (*OAuthCredentials) Valid

func (creds *OAuthCredentials) Valid() bool

Valid returns true if the credentials contain a non-nil, non-expired token.

type OAuthFlow

type OAuthFlow string

OAuthFlow represents an OAuth 2.0 grant type.

const (
	// OAuthFlowAuthorizationCode is the Authorization Code grant (RFC 6749 §4.1).
	OAuthFlowAuthorizationCode OAuthFlow = "authorization_code"

	// OAuthFlowDeviceCode is the Device Authorization grant (RFC 8628).
	OAuthFlowDeviceCode OAuthFlow = "urn:ietf:params:oauth:grant-type:device_code"

	// OAuthFlowClientCredentials is the Client Credentials grant (RFC 6749 §4.4).
	OAuthFlowClientCredentials OAuthFlow = "client_credentials"

	// OAuthFlowRefreshToken is the Refresh Token grant (RFC 6749 §6).
	OAuthFlowRefreshToken OAuthFlow = "refresh_token"
)

type OAuthMetadata

type OAuthMetadata struct {
	Issuer                            string   `json:"issuer"`
	AuthorizationEndpoint             string   `json:"authorization_endpoint"`
	TokenEndpoint                     string   `json:"token_endpoint"`
	DeviceAuthorizationEndpoint       string   `json:"device_authorization_endpoint,omitempty"`
	RegistrationEndpoint              string   `json:"registration_endpoint,omitempty"`
	JwksURI                           string   `json:"jwks_uri,omitempty"`
	ResponseTypesSupported            []string `json:"response_types_supported,omitempty"`
	ResponseModesSupported            []string `json:"response_modes_supported,omitempty"`
	GrantTypesSupported               []string `json:"grant_types_supported,omitempty"`
	TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"`
	ScopesSupported                   []string `json:"scopes_supported,omitempty"`
	CodeChallengeMethodsSupported     []string `json:"code_challenge_methods_supported,omitempty"`
	RevocationEndpoint                string   `json:"revocation_endpoint,omitempty"`
	IntrospectionEndpoint             string   `json:"introspection_endpoint,omitempty"`
}

OAuthMetadata represents OAuth 2.0 Authorization Server Metadata (RFC 8414).

func Discover

func Discover(ctx context.Context, endpoint string) (*OAuthMetadata, error)

Discover fetches OAuth 2.0 Authorization Server Metadata from the well-known endpoint on the server. It tries RFC 8414 root paths first, then falls back to path-relative discovery (e.g., Keycloak realms).

func SynthesizeMetadata added in v1.4.2

func SynthesizeMetadata(issuerURL string) *OAuthMetadata

SynthesizeMetadata constructs a minimal OAuthMetadata for an authorization server that does not publish an RFC 8414 discovery document. It derives the endpoints by appending standard path suffixes to issuerURL, following the fallback convention described in the MCP OAuth specification:

authorization_endpoint = issuerURL + "/authorize"
token_endpoint         = issuerURL + "/token"

This is suitable for legacy OAuth 2.0 servers such as GitHub (https://github.com/login/oauth) that predate RFC 8414.

func (*OAuthMetadata) OAuthEndpoint

func (m *OAuthMetadata) OAuthEndpoint() oauth2.Endpoint

OAuthEndpoint returns an oauth2.Endpoint built from the metadata. The AuthStyle is derived from token_endpoint_auth_methods_supported (RFC 8414):

  • "client_secret_post" only → AuthStyleInParams (body)
  • "client_secret_basic" only → AuthStyleInHeader (HTTP Basic)
  • both or neither → AuthStyleAutoDetect

func (*OAuthMetadata) SupportsDeviceFlow

func (m *OAuthMetadata) SupportsDeviceFlow() bool

SupportsDeviceFlow returns true if the server supports the device authorization grant. The presence of device_authorization_endpoint is the primary indicator per RFC 8628.

func (*OAuthMetadata) SupportsFlow

func (m *OAuthMetadata) SupportsFlow(flow OAuthFlow) error

SupportsFlow returns an error if the metadata is missing fields required for the given grant type flow. Supported grant types and their requirements:

  • OAuthFlowAuthorizationCode: authorization_endpoint + token_endpoint
  • OAuthFlowDeviceCode: device_authorization_endpoint + token_endpoint
  • OAuthFlowClientCredentials, OAuthFlowRefreshToken, others: token_endpoint only

func (*OAuthMetadata) SupportsGrantType

func (m *OAuthMetadata) SupportsGrantType(flow OAuthFlow) bool

SupportsGrantType returns true if the server supports the given grant type. Per RFC 8414, grant_types_supported is optional — when omitted, true is returned to avoid blocking flows that might still be supported.

func (*OAuthMetadata) SupportsIntrospection

func (m *OAuthMetadata) SupportsIntrospection() bool

SupportsIntrospection returns true if the server supports token introspection (RFC 7662).

func (*OAuthMetadata) SupportsPKCE

func (m *OAuthMetadata) SupportsPKCE() bool

SupportsPKCE returns true if the server supports PKCE.

func (*OAuthMetadata) SupportsRegistration

func (m *OAuthMetadata) SupportsRegistration() bool

SupportsRegistration returns true if the server supports dynamic client registration (RFC 7591).

func (*OAuthMetadata) SupportsRevocation

func (m *OAuthMetadata) SupportsRevocation() bool

SupportsRevocation returns true if the server supports token revocation (RFC 7009).

func (*OAuthMetadata) SupportsS256

func (m *OAuthMetadata) SupportsS256() bool

SupportsS256 returns true if the server supports the S256 challenge method.

func (*OAuthMetadata) ValidateScopes

func (m *OAuthMetadata) ValidateScopes(scopes ...string) error

ValidateScopes checks that every requested scope is listed in scopes_supported. Per RFC 8414 the field is optional — when the server does not advertise it, all scopes are accepted and nil is returned. An empty scopes argument always returns nil.

type OpenFunc

type OpenFunc func(url string) error

OpenFunc is called with the authorization URL and should open it in a browser. On macOS the caller can use:

func(u string) error { return exec.Command("open", u).Start() }

type PromptFunc

type PromptFunc func(authURL string) (code string, err error)

PromptFunc is called with the authorization URL. The implementation should display it to the user, obtain the authorization code (e.g. by opening a browser or printing the URL and reading from stdin), and return it.

type ProtectedResourceMetadata added in v1.4.2

type ProtectedResourceMetadata struct {
	Resource               string   `json:"resource"`
	AuthorizationServers   []string `json:"authorization_servers"`
	ScopesSupported        []string `json:"scopes_supported,omitempty"`
	BearerMethodsSupported []string `json:"bearer_methods_supported,omitempty"`
	ResourceName           string   `json:"resource_name,omitempty"`
}

ProtectedResourceMetadata represents OAuth 2.0 Protected Resource Metadata (RFC 9728). It is returned by a resource server's well-known endpoint and lists the authorization servers that protect the resource.

Jump to

Keyboard shortcuts

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