oauth2

package
v1.5.9 Latest Latest
Warning

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

Go to latest
Published: Aug 13, 2026 License: Apache-2.0 Imports: 25 Imported by: 2

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GeneratePKCEChallenge

func GeneratePKCEChallenge() (verifier, challenge string, err error)

GeneratePKCEChallenge generates code_verifier and code_challenge for PKCE (RFC 7636) Returns:

  • verifier: Random 128-character string (stored securely, never sent to server)
  • challenge: SHA256 hash of verifier, base64url encoded (sent in authorization request)

func SetLogger

func SetLogger(l schemas.Logger)

func ValidatePKCEChallenge

func ValidatePKCEChallenge(verifier, challenge string) bool

ValidatePKCEChallenge validates that a code_verifier matches the expected code_challenge Used during testing or debugging

Types

type DynamicClientRegistrationRequest

type DynamicClientRegistrationRequest struct {
	ClientName              string   `json:"client_name"`
	RedirectURIs            []string `json:"redirect_uris"`
	GrantTypes              []string `json:"grant_types"`
	ResponseTypes           []string `json:"response_types"`
	TokenEndpointAuthMethod string   `json:"token_endpoint_auth_method"`
	Scope                   string   `json:"scope,omitempty"`
	LogoURI                 string   `json:"logo_uri,omitempty"`
	ClientURI               string   `json:"client_uri,omitempty"`
	Contacts                []string `json:"contacts,omitempty"`
}

DynamicClientRegistrationRequest represents the client registration request (RFC 7591)

type DynamicClientRegistrationResponse

type DynamicClientRegistrationResponse struct {
	ClientID                string `json:"client_id"`
	ClientSecret            string `json:"client_secret,omitempty"`
	ClientIDIssuedAt        int64  `json:"client_id_issued_at,omitempty"`
	ClientSecretExpiresAt   int64  `json:"client_secret_expires_at,omitempty"`
	RegistrationAccessToken string `json:"registration_access_token,omitempty"`
	RegistrationClientURI   string `json:"registration_client_uri,omitempty"`
}

DynamicClientRegistrationResponse represents the server's response (RFC 7591)

func RegisterDynamicClient

func RegisterDynamicClient(ctx context.Context, registrationURL string, req *DynamicClientRegistrationRequest) (*DynamicClientRegistrationResponse, error)

RegisterDynamicClient performs dynamic client registration with the OAuth provider (RFC 7591) This allows Bifrost to automatically register as an OAuth client without manual setup.

Parameters:

  • ctx: Context for the registration request
  • registrationURL: The registration endpoint (discovered or user-provided)
  • req: Client registration details

Returns client_id and optional client_secret that can be used for OAuth flows.

type OAuth2Provider

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

OAuth2Provider implements the schemas.OAuth2Provider interface It provides OAuth 2.0 authentication functionality with database persistence

func NewOAuth2Provider

func NewOAuth2Provider(configStore configstore.ConfigStore, logger schemas.Logger) *OAuth2Provider

NewOAuth2Provider creates a new OAuth provider instance

func (*OAuth2Provider) BuildAdminUpstreamAuthorizeURL added in v1.5.9

func (p *OAuth2Provider) BuildAdminUpstreamAuthorizeURL(ctx context.Context, flowID string) (string, error)

BuildAdminUpstreamAuthorizeURL is BuildUpstreamAuthorizeURL's admin-mode counterpart: reconstructs the upstream provider authorization URL for a pending flow_mode='admin' row, used by the MCP client reauthorize endpoint to hand OAuth2Authorizer's popup a real provider URL to open directly (unlike the per-user page-based flow, which navigates to a Bifrost page that itself resolves the upstream URL later). Reads through GetOauthFlowByID rather than widening BuildUpstreamAuthorizeURL's own GetOauthUserSessionByID call: that lookup is deliberately scoped away from admin-mode rows (see its doc comment) so a caller-supplied flow ID from a per-user-facing endpoint can never reach an admin flow's PKCE state.

func (*OAuth2Provider) BuildUpstreamAuthorizeURL added in v1.3.11

func (p *OAuth2Provider) BuildUpstreamAuthorizeURL(ctx context.Context, flowID string) (string, error)

BuildUpstreamAuthorizeURL reconstructs the upstream provider authorization URL for a pending per-user OAuth flow. Called by the frontend sessions tab when the user clicks "Authenticate" — at which point the flow row already exists (from a prior InitiateUserOAuthFlow), the CSRF state + PKCE verifier are stored on it, and we just need to hand the user the upstream redirect.

The code_challenge is recomputed deterministically from the stored verifier so we don't have to persist it separately.

func (*OAuth2Provider) CompleteOAuthFlow

func (p *OAuth2Provider) CompleteOAuthFlow(ctx context.Context, state, code string) error

CompleteOAuthFlow handles the OAuth callback and exchanges code for tokens Supports PKCE verification. Handles the admin-mode flow — the shared client's production authorize and a per-user client's bootstrap-test authorize alike (see the FlowMode field comment on TableMCPOauthFlow).

func (*OAuth2Provider) CompleteUserOAuthFlow added in v1.3.1

func (p *OAuth2Provider) CompleteUserOAuthFlow(ctx context.Context, state string, code string) (string, error)

CompleteUserOAuthFlow handles the OAuth callback for a per-user flow. It looks up the session by state, exchanges code for tokens, and returns a session token.

func (*OAuth2Provider) EvictExchangedToken added in v1.5.9

func (p *OAuth2Provider) EvictExchangedToken(mode schemas.MCPAuthMode, identity, mcpClientID string)

EvictExchangedToken removes every cached exchanged token for one binding, regardless of which subject bearer token each entry was cached under (see userTokenCacheKey's doc comment on subjectDiscriminator) — the caller here (ForceRefreshAccessToken) knows the binding but not a specific token's fingerprint.

func (*OAuth2Provider) EvictUserToken added in v1.5.9

func (p *OAuth2Provider) EvictUserToken(mode schemas.MCPAuthMode, identity, mcpClientID string)

EvictUserToken drops the cached access token for one (mode, identity, mcp client) binding. Side-effect only and safe to call when the cache is absent; the next lookup for the binding reads the database.

func (*OAuth2Provider) EvictUserTokenByID added in v1.5.9

func (p *OAuth2Provider) EvictUserTokenByID(tokenID string)

EvictUserTokenByID drops the cached access token backed by the given token row ID, if any binding currently holds it. Side-effect only and safe to call when the cache is absent or the ID is not cached.

func (*OAuth2Provider) EvictUserTokensByMCPClient added in v1.5.9

func (p *OAuth2Provider) EvictUserTokensByMCPClient(mcpClientID string)

EvictUserTokensByMCPClient drops every cached access token bound to the given MCP client, across all auth modes and identities. Used after client-level mutations that invalidate its token rows as a set, such as credential rotation, access reconciliation, or client deletion. Side-effect only and safe to call when the cache is absent.

func (*OAuth2Provider) EvictUserTokensByUser added in v1.5.9

func (p *OAuth2Provider) EvictUserTokensByUser(userID string)

EvictUserTokensByUser drops every cached user-mode access token bound to the given user, across all MCP clients. Used after user-level mutations that orphan or delete the user's token rows as a set. Side-effect only and safe to call when the cache is absent.

func (*OAuth2Provider) EvictUserTokensByVirtualKey added in v1.5.9

func (p *OAuth2Provider) EvictUserTokensByVirtualKey(virtualKeyID string)

EvictUserTokensByVirtualKey drops every cached vk-mode access token bound to the given virtual key, across all MCP clients. Used after virtual key mutations that orphan or delete its token rows as a set. Side-effect only and safe to call when the cache is absent.

func (*OAuth2Provider) ExchangeAdminCredential added in v1.5.9

func (p *OAuth2Provider) ExchangeAdminCredential(ctx context.Context, config *schemas.MCPClientConfig, subjectToken string) (*schemas.OAuth2TokenExchangeResponse, error)

ExchangeAdminCredential performs a single uncached exchange of the admin's own token (a sample caller token, never associated with any identity binding), producing the admin bootstrap credential used for verification + tool discovery. The full token response is returned so the caller can retain it via RetainExchangeAdminCredential once verification succeeds.

func (*OAuth2Provider) FlushUserTokenCache added in v1.5.9

func (p *OAuth2Provider) FlushUserTokenCache()

FlushUserTokenCache drops every cached per-user access token. The coarse fallback for mutations whose blast radius cannot be scoped to one client or virtual key. Side-effect only.

func (*OAuth2Provider) ForceRefreshAccessToken added in v1.5.9

func (p *OAuth2Provider) ForceRefreshAccessToken(ctx *schemas.BifrostContext, config *schemas.MCPClientConfig) error

ForceRefreshAccessToken resolves the MCP OAuth token row backing config — the shared token linked to config.OauthConfigID for MCPAuthTypeOauth (same lookup GetAccessToken performs), or the caller's per-identity token for MCPAuthTypePerUserOauth (same lookup GetUserAccessTokenByMode performs, with (mode, identity) derived from ctx) — and refreshes it unconditionally via RefreshAccessToken, regardless of whether the token's own ExpiresAt says it's still good. RefreshAccessToken itself never consults ExpiresAt — only its callers (GetAccessToken, GetUserAccessTokenByMode) gate on it before deciding to call it — so resolving the right token row and going straight to RefreshAccessToken is the entire "force" behavior.

func (*OAuth2Provider) GetAccessToken

func (p *OAuth2Provider) GetAccessToken(ctx context.Context, oauthConfigID string) (string, error)

GetAccessToken retrieves the access token for a given oauth_config_id

func (*OAuth2Provider) GetAdminAccessToken added in v1.5.9

func (p *OAuth2Provider) GetAdminAccessToken(ctx context.Context, mcpClientID string) (string, error)

GetAdminAccessToken is GetAccessToken's admin-mode counterpart: resolves the retained bootstrap-verification credential for a per-user client's periodic tool-discovery refresh (ClientToolSyncer.performSync), rather than the shared-mode production credential. Keyed by the MCP client ID, which every admin row carries regardless of whether an oauth_configs template sits behind the credential.

func (*OAuth2Provider) GetExchangedAccessToken added in v1.5.9

func (p *OAuth2Provider) GetExchangedAccessToken(ctx *schemas.BifrostContext, config *schemas.MCPClientConfig) (string, error)

GetExchangedAccessToken returns an upstream access token for a token_exchange client, exchanging the caller's identity-provider token for one scoped to the client's audience. Cached per (auth mode, identity, mcp client) binding in the same in-memory cache as per-user OAuth lookups — exchanged tokens have no database row, so the cached entry (with the expiry-as-miss validator) is the only local state, and a miss simply performs a fresh exchange.

func (*OAuth2Provider) GetPendingMCPClient

func (p *OAuth2Provider) GetPendingMCPClient(oauthConfigID string) (*schemas.MCPClientConfig, error)

GetPendingMCPClient retrieves an MCP client config by oauth_config_id. Returns nil if no pending config is found. Unlike before PKCE/expiry fields moved off TableOauthConfig, this no longer applies its own expiry gate: the only caller (completeMCPClientOAuth) already requires oauthConfig.Status=="authorized" before reaching this call, which is a stronger, more truthful signal than a raw timestamp comparison — a config row that reached "authorized" represents a real completed OAuth exchange regardless of how much wall-clock time has passed since the bootstrap stash was written.

func (*OAuth2Provider) GetUserAccessTokenByMode added in v1.3.11

func (p *OAuth2Provider) GetUserAccessTokenByMode(ctx context.Context, mode schemas.MCPAuthMode, identity, mcpClientID string) (string, error)

GetUserAccessTokenByMode retrieves the upstream access token using exactly one identity column determined by mode. No fallback chain. Filters status='active' so orphaned rows never satisfy a lookup.

Reads are served from an in-memory cache when possible: a cached entry whose expiry has passed is dropped and the lookup falls through to the database path, which owns every expiry and refresh decision. Failed lookups are never cached, so a caller completing OAuth (or a token being reactivated) becomes visible on the very next call.

func (*OAuth2Provider) InitiateOAuthFlow

func (p *OAuth2Provider) InitiateOAuthFlow(ctx context.Context, config *schemas.OAuth2Config) (*schemas.OAuth2FlowInitiation, error)

InitiateOAuthFlow creates an OAuth config and returns the authorization URL Supports OAuth discovery and PKCE

func (*OAuth2Provider) InitiateUserOAuthFlow added in v1.3.1

func (p *OAuth2Provider) InitiateUserOAuthFlow(ctx context.Context, oauthConfigID string, mcpClientID string, redirectURI string, flowMode schemas.MCPAuthMode) (*schemas.OAuth2FlowInitiation, string, error)

InitiateUserOAuthFlow creates or refreshes the per-user OAuth flow row for a given (mode, identity, mcp_client) binding and returns the auth landing URL.

Determinism: there is exactly one flow row per binding. If one already exists (from a prior auth attempt), it's updated in place — fresh CSRF state, fresh PKCE verifier, status reset to 'pending'. Otherwise a new row is inserted. Reauth never duplicates rows; revoke deletes them.

The function errors out cleanly on any misconfig (missing identity, unknown mode, missing template config) — no fallbacks, no generated identities.

func (*OAuth2Provider) RefreshAccessToken

func (p *OAuth2Provider) RefreshAccessToken(ctx context.Context, tokenID string) error

RefreshAccessToken refreshes any MCP OAuth token row — the single shared client credential (AuthMode="shared") or a per-identity credential (AuthMode "user"/"vk"/"session") alike — looked up by the token row's own primary-key ID. This is the one refresh path for every kind of MCP OAuth credential: GetAccessToken and GetUserAccessTokenByMode both funnel their lazy pre-flight refresh through here, unifying what used to be two separate functions (one keyed by oauth_config_id and reachable only via TableOauthConfig.TokenID, one keyed by token ID directly).

Never writes to TableOauthConfig: the template config row (client_id, token_url, etc.) is read-only input to the token exchange here. Credential health lives entirely on the token row's own Status field.

The actual refresh runs on a context detached from any single caller — via DoChan rather than Do — because it is shared work: with Do, the first caller's ctx backs the whole call, so that caller disconnecting (client abort, gateway timeout) would cancel the in-flight upstream request and deliver that same cancellation error to every other concurrent caller waiting on the same token, even though their own requests are still live. Each caller here instead only ever stops waiting on its own ctx; the shared refresh keeps running to completion (bounded by refreshAccessTokenTimeout) for whoever else still needs its result.

func (*OAuth2Provider) RemovePendingMCPClient

func (p *OAuth2Provider) RemovePendingMCPClient(oauthConfigID string) error

RemovePendingMCPClient clears the pending MCP client config from the oauth config This is called after OAuth completion to clean up

func (*OAuth2Provider) RetainExchangeAdminCredential added in v1.5.9

func (p *OAuth2Provider) RetainExchangeAdminCredential(ctx context.Context, config *schemas.MCPClientConfig, response *schemas.OAuth2TokenExchangeResponse) error

RetainExchangeAdminCredential persists the outcome of a successful admin verification as the retained auth_mode='admin' token row for config — the discovery credential the tool syncer and OAuthTokenRefreshWorker keep alive, mirroring what PromoteSharedOauthTokenToAdmin does for per-user OAuth bootstrap. Upserts: a repair replaces the existing row's credential.

func (*OAuth2Provider) RevokeToken

func (p *OAuth2Provider) RevokeToken(ctx context.Context, oauthConfigID string) error

RevokeToken revokes the OAuth token

func (*OAuth2Provider) SetTempTokenService added in v1.3.11

func (p *OAuth2Provider) SetTempTokenService(svc *temptoken.Service)

SetTempTokenService installs the temp-token service used by InitiateUserOAuthFlow to mint the mcp_auth token embedded in the auth-page URL fragment. Called by server startup once both services have been constructed (the provider is built first by lib/config.go, the service later by the HTTP transport).

func (*OAuth2Provider) SetTokenExchangeIdPResolver added in v1.5.9

func (p *OAuth2Provider) SetTokenExchangeIdPResolver(r schemas.TokenExchangeIdPResolver)

SetTokenExchangeIdPResolver installs the identity-provider resolver that backs delegated token exchange. Called once at server startup after the identity integration is constructed; nil (never installed) leaves the token_exchange auth type unavailable.

func (*OAuth2Provider) StorePendingMCPClient

func (p *OAuth2Provider) StorePendingMCPClient(oauthConfigID string, mcpClientConfig schemas.MCPClientConfig) error

StorePendingMCPClient stores an MCP client config that's waiting for OAuth completion The config is persisted in the database (oauth_configs.mcp_client_config_json) to support multi-instance deployments where OAuth callback may hit a different server instance.

func (*OAuth2Provider) TokenExchangeAvailable added in v1.5.9

func (p *OAuth2Provider) TokenExchangeAvailable() bool

TokenExchangeAvailable reports whether delegated token exchange can run.

func (*OAuth2Provider) ValidateToken

func (p *OAuth2Provider) ValidateToken(ctx context.Context, oauthConfigID string) (bool, error)

ValidateToken checks if the token is still valid

type OAuthMetadata

type OAuthMetadata struct {
	AuthorizationURL string   `json:"authorization_endpoint"`
	TokenURL         string   `json:"token_endpoint"`
	RegistrationURL  *string  `json:"registration_endpoint,omitempty"`
	ScopesSupported  []string `json:"scopes_supported,omitempty"`
	Resource         string   `json:"resource,omitempty"`
	Issuer           string   `json:"issuer,omitempty"`
	ResponseTypes    []string `json:"response_types_supported,omitempty"`
	GrantTypes       []string `json:"grant_types_supported,omitempty"`
	TokenAuthMethods []string `json:"token_endpoint_auth_methods_supported,omitempty"`
	PKCEMethods      []string `json:"code_challenge_methods_supported,omitempty"`
}

OAuthMetadata contains discovered OAuth configuration from authorization server

func DiscoverOAuthMetadata

func DiscoverOAuthMetadata(ctx context.Context, serverURL string) (*OAuthMetadata, error)

DiscoverOAuthMetadata performs OAuth 2.0 discovery for the given MCP server URL Following RFC 8414 (Authorization Server Discovery) and RFC 9728 (Protected Resource Metadata)

Parameters:

  • ctx: Context for the discovery requests
  • serverURL: The MCP server URL to discover OAuth configuration from
  • logger: Logger for discovery progress (can be nil for silent operation)

The discovery process: 1. Attempt to connect to MCP server, expect 401 with WWW-Authenticate header 2. Parse WWW-Authenticate header for resource_metadata URL and scopes 3. Fetch resource metadata to get authorization server URLs 4. Try .well-known discovery if resource metadata is not available 5. Fetch authorization server metadata from discovered URLs 6. Return complete OAuth configuration

type OAuthTokenRefreshWorker added in v1.5.9

type OAuthTokenRefreshWorker struct {

	// AuthModes restricts proactive refresh to tokens whose AuthMode is in
	// this set. Defaults to {"shared", "admin"} — shared-client tokens and
	// retained per_user_oauth bootstrap credentials (auth_mode='admin') are
	// the only ones with no live caller to trigger a lazy/inline refresh:
	// admin-mode tokens back only the periodic tool-discovery syncer, never
	// real end-user traffic, so nothing else would ever refresh them.
	// Widening this further (e.g. to include "user"/"vk"/"session") opts
	// specific per-identity auth modes into the same proactive sweep.
	//
	// Set this before calling Start, not after: the background goroutine
	// reads this field directly on every sweep with no synchronization, so a
	// mutation concurrent with a running sweep is a data race. Every current
	// caller already treats this as write-once-before-Start; there is no
	// supported way to change an already-started worker's scope short of
	// Stop and constructing a new worker.
	AuthModes []string
	// contains filtered or unexported fields
}

OAuthTokenRefreshWorker manages automatic token refresh for expiring OAuth tokens

func NewOAuthTokenRefreshWorker added in v1.5.9

func NewOAuthTokenRefreshWorker(provider *OAuth2Provider, logger schemas.Logger) *OAuthTokenRefreshWorker

NewOAuthTokenRefreshWorker creates a new token refresh worker

func (*OAuthTokenRefreshWorker) SetLookAheadWindow added in v1.5.9

func (w *OAuthTokenRefreshWorker) SetLookAheadWindow(window time.Duration)

SetLookAheadWindow updates the look-ahead window for token expiry (for testing)

func (*OAuthTokenRefreshWorker) SetOnTokenRefreshed added in v1.5.9

func (w *OAuthTokenRefreshWorker) SetOnTokenRefreshed(cb func(mcpClientID, authMode string))

SetOnTokenRefreshed registers a callback invoked after each successful proactive token refresh, with the owning MCP client ID and the token's auth mode. It lets the serving layer react to a freshened credential, for example by recycling a connection that captured the old one. Tokens with no MCP client ID (legacy rows) never fire the callback. Safe to call at any time, including while the worker is running; passing nil clears the callback.

func (*OAuthTokenRefreshWorker) SetRefreshInterval added in v1.5.9

func (w *OAuthTokenRefreshWorker) SetRefreshInterval(interval time.Duration)

SetRefreshInterval updates the refresh check interval (for testing)

func (*OAuthTokenRefreshWorker) SetShouldRefreshGate added in v1.5.9

func (w *OAuthTokenRefreshWorker) SetShouldRefreshGate(gate func(ctx context.Context) bool)

SetShouldRefreshGate installs a predicate consulted at the start of every sweep tick; a tick proceeds only when the gate is unset or returns true. Lets a deployment with multiple workers sharing the same token store restrict sweeps to one at a time, avoiding concurrent refreshes of the same token. Safe to call at any time, including while the worker is running; passing nil restores the default (always run).

func (*OAuthTokenRefreshWorker) Start added in v1.5.9

func (w *OAuthTokenRefreshWorker) Start(ctx context.Context)

Start begins the token refresh worker in a background goroutine

func (*OAuthTokenRefreshWorker) Stop added in v1.5.9

func (w *OAuthTokenRefreshWorker) Stop()

Stop gracefully stops the token refresh worker. Safe to call multiple times — guarded by sync.Once so a redundant call from a secondary shutdown path can't panic by re-closing the channel.

type PerUserOAuthSweepWorker added in v1.3.11

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

PerUserOAuthSweepWorker periodically purges stale per-user OAuth state:

  • expired pending flow rows (oauth_user_sessions where ExpiresAt < now and status='pending')
  • orphaned token rows older than OrphanRetention (oauth_user_tokens where status='orphaned')

Pending-flow expiry is short (driven by the row's own ExpiresAt, default 15min) so the flow tick is fast. Orphan retention is operator-tunable (default 30 days) and the orphan tick is slow.

func NewPerUserOAuthSweepWorker added in v1.3.11

func NewPerUserOAuthSweepWorker(provider *OAuth2Provider, orphanRetention time.Duration, logger schemas.Logger) *PerUserOAuthSweepWorker

NewPerUserOAuthSweepWorker creates a sweep worker with sensible defaults. orphanRetention <= 0 disables the orphan-token sweep.

func (*PerUserOAuthSweepWorker) SetFlowSweepInterval added in v1.3.11

func (w *PerUserOAuthSweepWorker) SetFlowSweepInterval(d time.Duration)

SetFlowSweepInterval updates the pending-flow sweep cadence (for testing). Non-positive durations are ignored — run() feeds the field straight into time.NewTicker, which panics on d <= 0.

func (*PerUserOAuthSweepWorker) SetOrphanSweepInterval added in v1.3.11

func (w *PerUserOAuthSweepWorker) SetOrphanSweepInterval(d time.Duration)

SetOrphanSweepInterval updates the orphan-token sweep cadence (for testing). Same non-positive guard as SetFlowSweepInterval.

func (*PerUserOAuthSweepWorker) Start added in v1.3.11

func (w *PerUserOAuthSweepWorker) Start(ctx context.Context)

Start begins the sweep worker in a background goroutine.

func (*PerUserOAuthSweepWorker) Stop added in v1.3.11

func (w *PerUserOAuthSweepWorker) Stop()

Stop gracefully stops the sweep worker. sync.Once guards against double-close panics when called from multiple shutdown paths.

type PermanentOAuthError added in v1.2.37

type PermanentOAuthError struct {
	StatusCode int
	Body       string
}

PermanentOAuthError indicates the OAuth provider rejected the request in a way that requires user re-authorization (e.g. revoked refresh token, invalid_grant). Distinct from transient network failures which should be retried.

func (*PermanentOAuthError) Error added in v1.2.37

func (e *PermanentOAuthError) Error() string

type ResourceMetadata

type ResourceMetadata struct {
	Resource             string   `json:"resource,omitempty"`
	AuthorizationServers []string `json:"authorization_servers"`
	ScopesSupported      []string `json:"scopes_supported,omitempty"`
	Scopes               []string `json:"scopes,omitempty"` // Alternative field name
}

ResourceMetadata contains metadata from protected resource

Jump to

Keyboard shortcuts

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