Documentation
¶
Overview ¶
Package oauth is Exhale's own OAuth 2.1 authorization server, the part of the MCP authorization spec that lets a cloud assistant (Claude.ai, ChatGPT, Grok) connect to a person's Exhale by its URL (ADR 0018 milestone 3, ADR 0024).
It is deliberately small: the authorization code grant with PKCE (S256 only), rotating refresh tokens, resource indicators (RFC 8707), two ways for an application to introduce itself (a Client ID Metadata Document, or dynamic registration, RFC 7591), revocation (RFC 7009) and opaque tokens stored as hashes. There are no JWTs and no signing keys. The HTTP endpoints live in internal/httpapi; this package holds the rules.
Index ¶
- Constants
- Variables
- func Code(err error) string
- func Description(err error) string
- func LoopbackRedirect(raw string) bool
- func NormalizeResource(raw string) (string, bool)
- func ScopeString(s app.Scope) string
- type App
- type AuthorizeParams
- type Client
- type Error
- type Registered
- type Registration
- type Request
- type Service
- func (s *Service) Access(ctx context.Context, plain, resource string) (app.Actor, error)
- func (s *Service) Apps(ctx context.Context, memberID string) ([]App, error)
- func (s *Service) Authorize(ctx context.Context, p AuthorizeParams, issuer, resource string) (*Request, error)
- func (s *Service) Cleanup(ctx context.Context) error
- func (s *Service) Client(ctx context.Context, id string) (Client, error)
- func (s *Service) Decide(ctx context.Context, id string, member app.Actor, approve bool, ...) (string, error)
- func (s *Service) Disconnect(ctx context.Context, memberID, grantID string) error
- func (s *Service) KnownClient(ctx context.Context, id string) (Client, error)
- func (s *Service) Pending(id string) (*Request, bool)
- func (s *Service) Register(ctx context.Context, in Registration) (Registered, error)
- func (s *Service) Revoke(ctx context.Context, clientID, secret, token string) error
- func (s *Service) Token(ctx context.Context, in TokenRequest) (Tokens, error)
- type TokenRequest
- type Tokens
Constants ¶
const ( // KindDynamic clients registered themselves (RFC 7591). Anyone can, so // nothing they say about themselves is verified. KindDynamic = "dynamic" // KindMetadata clients are identified by the https URL of a document // that Exhale fetched: who controls that address is known. KindMetadata = "metadata" )
How an application introduced itself.
const ( // AccessPrefix and RefreshPrefix mark the tokens (secret-scanning // friendly, like exhale_pat_). AccessPrefix = "exhale_oat_" RefreshPrefix = "exhale_ort_" // AccessTTL is short because an access token cannot be recalled from // where it leaked to; the refresh token renews it. AccessTTL = time.Hour // RefreshTTL: a connection nobody uses for this long ends by itself. // Every refresh issues a new refresh token with a new lifetime. RefreshTTL = 90 * 24 * time.Hour // CodeTTL bounds the time between approving and the application // fetching its tokens. CodeTTL = 5 * time.Minute // RequestTTL is how long the consent screen stays valid. RequestTTL = 10 * time.Minute )
const ( ScopeRead = "read" ScopeWrite = "write" )
Scopes an application can ask for. They are the scopes of personal tokens; "trusted" is never available through OAuth.
Variables ¶
var ErrInvalidToken = errors.New("invalid access token")
ErrInvalidToken is returned for an access token that is unknown, expired, revoked or meant for another resource.
var ErrRequestGone = errors.New("this connection request has expired or was already answered")
ErrRequestGone means the consent screen was answered already or waited too long.
var Scopes = []string{ScopeRead, ScopeWrite}
Scopes is what the protected resource metadata advertises.
Functions ¶
func Description ¶
Description returns the sentence of an OAuth error, or a generic one.
func LoopbackRedirect ¶
LoopbackRedirect reports whether a redirect URI points at this computer: any program running there could be the one that receives the code.
func NormalizeResource ¶
NormalizeResource returns the canonical form of a resource identifier (RFC 8707): lowercase scheme and host, no default port, no trailing slash. It fails for anything that is not an absolute http(s) URL without fragment.
func ScopeString ¶
ScopeString is the OAuth scope value of a granted scope: write includes read.
Types ¶
type App ¶
type App struct {
ID string `json:"id"`
Name string `json:"name"`
Verified bool `` /* 143-byte string literal not displayed */
ClientHost string `json:"clientHost,omitempty" doc:"host of the application's address, when verified"`
RedirectHost string `json:"redirectHost" doc:"where the application received its access"`
Scope string `json:"scope" enum:"read,write"`
CreatedAt string `json:"createdAt"`
LastUsedAt string `json:"lastUsedAt,omitempty"`
}
App is an application a member connected.
type AuthorizeParams ¶
type AuthorizeParams struct {
ResponseType string
ClientID string
RedirectURI string
Scope string
State string
CodeChallenge string
CodeChallengeMethod string
Resource []string
}
AuthorizeParams are the query parameters of an authorization request.
type Client ¶
type Client struct {
ID string
Kind string
Name string
URI string
RedirectURIs []string
// contains filtered or unexported fields
}
Client is an application that may ask for access.
type Error ¶
Error is an OAuth error: Code goes to the application ("invalid_grant"), Description is a sentence for whoever debugs it.
type Registered ¶
Registered is what a registration returns: the client, and its secret once for a confidential client.
type Registration ¶
type Registration struct {
RedirectURIs []string `json:"redirect_uris"`
ClientName string `json:"client_name"`
ClientURI string `json:"client_uri"`
GrantTypes []string `json:"grant_types"`
ResponseTypes []string `json:"response_types"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
}
Registration is the body of a dynamic client registration (RFC 7591). Fields we have no use for are accepted and ignored.
type Request ¶
type Request struct {
ID string
Client Client
RedirectURI string
State string
Challenge string
Resource string
Issuer string
// MaxScope is the most the application asked for.
MaxScope app.Scope
// contains filtered or unexported fields
}
Request is an authorization request waiting for the member's answer on the consent screen. It lives in memory: a restart only asks the person to connect again.
func (*Request) ErrorRedirect ¶
ErrorRedirect returns the redirect for an error from Authorize.
func (*Request) Loopback ¶
Loopback reports whether the answer goes to a program on the person's own computer, which any program running there could be.
func (*Request) RedirectHost ¶
RedirectHost is where the browser goes after the answer: the consent screen must show it (MCP security considerations).
type Service ¶
type Service struct {
// HTTP fetches client metadata documents. The default refuses private
// and local addresses and does not follow redirects.
HTTP *http.Client
// Now is the clock (tests move it).
Now func() time.Time
// contains filtered or unexported fields
}
Service implements the authorization server on top of the store.
func New ¶
New returns the service. Client metadata documents are fetched through netguard: an application's URL must never reach the server's own network.
func (*Service) Access ¶
Access returns the actor of an access token presented at resource: the member who approved it, under the application's name, with the scope they chose.
func (*Service) Apps ¶
Apps lists the applications a member has connected and that can still reach Exhale.
func (*Service) Authorize ¶
func (s *Service) Authorize(ctx context.Context, p AuthorizeParams, issuer, resource string) (*Request, error)
Authorize checks an authorization request. With a nil request the error must be shown to the person: the client or its redirect URI is not trusted, so nothing may be sent there. With a request and an error, the error goes back to the application at its redirect URI. Without error the request waits for the consent screen.
issuer is this server's issuer identifier and resource the canonical URI of its MCP endpoint, the only resource it issues tokens for.
func (*Service) Cleanup ¶
Cleanup deletes what has expired: tokens, codes, grants without tokens and clients nobody uses. It is cheap and safe to call often.
func (*Service) Client ¶
Client resolves a client_id as an authorization request presents it: a registered id, or the URL of a metadata document, fetched (or read from its cached copy) and checked.
func (*Service) Decide ¶
func (s *Service) Decide(ctx context.Context, id string, member app.Actor, approve bool, scope app.Scope) (string, error)
Decide records the member's answer and returns where to send the browser: the application's redirect URI with a code, or with access_denied. scope is what the member chose; it cannot exceed what the application asked for nor what the member may do.
func (*Service) Disconnect ¶
Disconnect ends one of a member's connections, with every token it had.
func (*Service) KnownClient ¶
KnownClient returns a client without fetching anything: the token endpoint only serves clients that went through an authorization request.
func (*Service) Register ¶
func (s *Service) Register(ctx context.Context, in Registration) (Registered, error)
Register stores a self-registered client. A client is public (no secret, PKCE protects the code) unless it asks for client_secret_basic or client_secret_post; PKCE is required either way. When the table is full the oldest registration nobody approved makes room.
type TokenRequest ¶
type TokenRequest struct {
GrantType string
ClientID string
ClientSecret string
Code string
RedirectURI string
CodeVerifier string
RefreshToken string
Scope string
Resource []string
}
TokenRequest is the body of a token request, with the client's credentials from either the body or the Authorization header.