Documentation
¶
Overview ¶
Package deviceauth is the device authorization grant (RFC 8628), better-auth's device-authorization plugin: a client that cannot open a browser (a CLI, a TV, an IoT box) asks for a device code and a short user code, shows the user code and a link, and polls while the user signs in on another device and approves. The approval ends with a session of the approving user on the polling client.
The flow, as five operations under the plugin name "device":
POST /device/code the client gets device_code, user_code, the links, expires_in and interval
GET /device/verify the approval page checks a user code; a signed-in caller claims it
POST /device/approve the claiming user approves
POST /device/deny or denies
POST /device/token the client polls: authorization_pending, slow_down, expired_token,
access_denied, or the session (access_token, token_type Bearer)
Both codes are stored as SHA-256 hashes, so a database read hands nobody a pending approval, and a code is found by its hash alone. The approved row is spent by a conditional delete, so two polls racing for one approval end with one session. The error codes RFC 8628 names are answered in its own shape by CodeHandler and TokenHandler; the default authhttp.Handler answers the same operations in the library's error JSON.
Index ¶
- Constants
- Variables
- func NormalizeUserCode(code string) string
- func RFCErrorCode(err error) string
- type Binding
- type Client
- type ClientError
- type CodeRequest
- type CodeResult
- type DecisionRequest
- type DecisionResult
- type Generator
- type Grant
- type Option
- func DeviceCodeLength(n int) Option
- func GenerateDeviceCode(fn Generator) Option
- func GenerateUserCode(fn Generator) Option
- func OnDeviceAuthRequest(fn func(ctx context.Context, clientID, scope string) error) Option
- func UserCodeLength(n int) Option
- func ValidateClient(fn func(ctx context.Context, clientID string) (bool, error)) Option
- func VerificationURI(u string) Option
- func WithExpiresIn(d time.Duration) Option
- func WithGrant(g Grant) Option
- func WithInterval(d time.Duration) Option
- func WithVerifyLimit(n int, window time.Duration) Option
- type Plugin
- func (p *Plugin) Approve(ctx context.Context, userCode string) error
- func (p *Plugin) Code(ctx context.Context, in CodeRequest) (CodeResult, error)
- func (p *Plugin) CodeHandler(opts ...authhttp.Option) http.Handler
- func (p *Plugin) Deny(ctx context.Context, userCode string) error
- func (p *Plugin) Init(a *auth.Auth) error
- func (p *Plugin) Name() string
- func (p *Plugin) Operations() []auth.Operation
- func (p *Plugin) OptionErr() error
- func (p *Plugin) RateLimits() []auth.RateRule
- func (p *Plugin) Redeem(ctx context.Context, deviceCode, clientID, grant string) (Redemption, error)
- func (p *Plugin) Sweep(ctx context.Context, cutoff time.Time) (int64, error)
- func (p *Plugin) Tables() []sqlb.Table
- func (p *Plugin) Token(ctx context.Context, in TokenRequest) (TokenResult, error)
- func (p *Plugin) TokenHandler(opts ...authhttp.Option) http.Handler
- func (p *Plugin) TypedMigrations(engine string, t auth.SQLTypes) ([]auth.Migration, error)
- func (p *Plugin) UserTables() []auth.UserTable
- func (p *Plugin) Validate(*auth.Config) error
- func (p *Plugin) Verify(ctx context.Context, userCode string) (VerifyResult, error)
- type RFCCoder
- type Redemption
- type Status
- type TokenRequest
- type TokenResult
- type VerifyRequest
- type VerifyResult
Examples ¶
Constants ¶
const ( EndpointCode auth.EndpointID = "device.code" EndpointToken auth.EndpointID = "device.token" EndpointVerify auth.EndpointID = "device.verify" EndpointApprove auth.EndpointID = "device.approve" EndpointDeny auth.EndpointID = "device.deny" )
Operation ids, exported so a hook or a rate rule can name them.
const ( // DefaultExpiresIn is how long a device code can be approved and polled: // long enough to find a phone and sign in, short enough that a code // photographed off a screen is soon worthless. //ds:def id=deviceauth-defaultexpiresin-4p8wn2xk owner=@khanakia stability=stable DefaultExpiresIn = 30 * time.Minute // DefaultInterval is the least time between two polls; a faster poll // answers slow_down. //ds:def id=deviceauth-defaultinterval-9t2vk5qm owner=@khanakia stability=stable DefaultInterval = 5 * time.Second // SlowDownStep is what RFC 8628 §3.5 adds to the interval each time a // client polls too fast, "for this and all subsequent requests". //ds:def id=deviceauth-slowdownstep-3k8xq2nv owner=@khanakia stability=stable SlowDownStep = 5 * time.Second // DefaultDeviceCodeLength is 40 characters of [A-Za-z0-9], about 238 // bits: the device code is the credential the poller holds. //ds:def id=deviceauth-defaultdevicecodelength-6w3mz8rq owner=@khanakia stability=stable DefaultDeviceCodeLength = 40 // DefaultUserCodeLength is 8 characters of UserCodeAlphabet, 40 bits: a // person types it, and the verify rate limit and the code's short life // bound guessing. //ds:def id=deviceauth-defaultusercodelength-2n7kv4xp owner=@khanakia stability=stable DefaultUserCodeLength = 8 // MaxCodeLength bounds both codes and a generator's output: 191 is the // longest key MySQL indexes in utf8mb4, better-auth's // DEVICE_AUTHORIZATION_CODE_MAX_LENGTH. //ds:def id=deviceauth-maxcodelength-8r4tw6zn owner=@khanakia stability=stable MaxCodeLength = 191 // DefaultVerificationURI is the approval page, relative to the base URL. //ds:def id=deviceauth-defaultverificationuri-5q9xm3kt owner=@khanakia stability=stable DefaultVerificationURI = "/device" )
Defaults, better-auth's.
const ( TableNameCode = "auth_device_code" TableAliasCode = "dac" ColID = "id" ColDeviceCodeHash = "device_code_hash" ColUserCodeHash = "user_code_hash" ColUserID = "user_id" ColClientID = "client_id" ColScope = "scope" ColStatus = "status" ColExpiresAt = "expires_at" ColLastPolledAt = "last_polled_at" ColPollingInterval = "polling_interval_ms" ColCreatedAt = "created_at" // ColGrant names the Grant a code was issued through ("" for the // standalone flow); ColResource is its RFC 8707 resource. ColGrant = "grant_name" ColResource = "resource" )
Table, alias and column names.
const ( RFCInvalidRequest = "invalid_request" RFCInvalidClient = "invalid_client" RFCInvalidGrant = "invalid_grant" RFCUnsupportedGrantType = "unsupported_grant_type" RFCAuthorizationPending = "authorization_pending" RFCSlowDown = "slow_down" RFCAccessDenied = "access_denied" RFCExpiredToken = "expired_token" RFCServerError = "server_error" // RFCInvalidTarget is RFC 8707's answer for a resource the server does // not serve, which a Grant's error reports through RFCCoder. RFCInvalidTarget = "invalid_target" // §4.1.2.1's code for "try again later". RFCTemporarilyUnavailable = "temporarily_unavailable" )
RFC 6749 / RFC 8628 error codes, the "error" member CodeHandler and TokenHandler answer. A device client library (golang.org/x/oauth2's DeviceAccessToken, for one) switches on these exact strings.
const GrantType = "urn:ietf:params:oauth:grant-type:device_code"
GrantType is the grant_type a poll must send (RFC 8628 §3.4).
const PluginName = "device"
PluginName is the plugin's permanent identity and its route prefix, so the routes are better-auth's: /device/code, /device/token, /device/approve.
const TokenType = "Bearer"
TokenType is the token_type of a successful poll: the session token is a bearer credential.
const UserCodeAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
UserCodeAlphabet is the default user code alphabet: upper-case letters and digits without 0, O, 1 and I, which a person misreads.
Variables ¶
var ( CodeID = sqlb.Col[string](TableCode, ColID) CodeDeviceCodeHash = sqlb.Col[[]byte](TableCode, ColDeviceCodeHash) CodeUserCodeHash = sqlb.Col[[]byte](TableCode, ColUserCodeHash) CodeUserID = sqlb.Col[*string](TableCode, ColUserID) CodeClientID = sqlb.Col[string](TableCode, ColClientID) CodeScope = sqlb.Col[string](TableCode, ColScope) CodeStatus = sqlb.Col[Status](TableCode, ColStatus) CodeExpiresAt = sqlb.Col[time.Time](TableCode, ColExpiresAt) CodeLastPolledAt = sqlb.Col[*time.Time](TableCode, ColLastPolledAt) CodePollingInterval = sqlb.Col[int64](TableCode, ColPollingInterval) CodeCreatedAt = sqlb.Col[time.Time](TableCode, ColCreatedAt) CodeGrant = sqlb.Col[*string](TableCode, ColGrant) CodeResource = sqlb.Col[*string](TableCode, ColResource) )
Typed columns of TableCode.
var ( ErrBadExpiresIn = errors.New("deviceauth: WithExpiresIn needs a positive duration") ErrBadInterval = errors.New("deviceauth: WithInterval needs a positive duration") ErrBadLength = fmt.Errorf("deviceauth: code lengths must be 1 to %d", MaxCodeLength) ErrBadLimit = errors.New("deviceauth: WithVerifyLimit needs a positive limit and window") )
Configuration errors from auth.New.
var ( // ErrInvalidRequest is a missing client_id, a repeated parameter, a value // too long for its column, or a UserID set anywhere but Code. ErrInvalidRequest = auth.NewError("device.invalid_request", http.StatusBadRequest, "The request is missing a parameter or has a bad one.") // ErrUnsupportedGrantType is a poll with a grant_type other than GrantType. ErrUnsupportedGrantType = auth.NewError("device.unsupported_grant_type", http.StatusBadRequest, "The grant type is not supported.") // ErrInvalidClient is a client ValidateClient refused when asking for a code. ErrInvalidClient = auth.NewError("device.invalid_client", http.StatusBadRequest, "The client is not allowed to use device authorization.") // ErrInvalidDeviceCode is an unknown device code, a client that is not // the one the code was issued to, or a poll that lost the race for one // approval. ErrInvalidDeviceCode = auth.NewError("device.invalid_device_code", http.StatusBadRequest, "The device code is not valid.") // ErrExpiredDeviceCode is a poll after the code expired; the row is deleted. ErrExpiredDeviceCode = auth.NewError("device.expired_device_code", http.StatusBadRequest, "The device code has expired.") // ErrAuthorizationPending is a poll before the user decided. ErrAuthorizationPending = auth.NewError("device.authorization_pending", http.StatusBadRequest, "The user has not approved the request yet.") // ErrPollingTooFrequently is a poll within the interval; the interval // grows by SlowDownStep. ErrPollingTooFrequently = auth.NewError("device.polling_too_frequently", http.StatusBadRequest, "Polling too often. Wait longer between requests.") // ErrAccessDenied is a poll after the user denied; the row is deleted. ErrAccessDenied = auth.NewError("device.access_denied", http.StatusBadRequest, "The user denied the request.") // ErrNotOwner is an approve or deny by a user other than the one who // claimed the code. ErrNotOwner = auth.NewError("device.not_owner", http.StatusForbidden, "This code belongs to another account.") // ErrInvalidUserCode is an unknown user code on the approval page. ErrInvalidUserCode = auth.NewError("device.invalid_user_code", http.StatusBadRequest, "This code is not valid. Check it and try again.") // ErrExpiredUserCode is a user code past its expiry on the approval page. ErrExpiredUserCode = auth.NewError("device.expired_user_code", http.StatusBadRequest, "This code has expired. Start again on your device.") // ErrAlreadyProcessed is an approve or deny of a code already decided. ErrAlreadyProcessed = auth.NewError("device.already_processed", http.StatusBadRequest, "This code was already used.") // ErrNotClaimed is an approve or deny of a code no one has opened on the // approval page yet (GET verify while signed in claims it). ErrNotClaimed = auth.NewError("device.not_claimed", http.StatusBadRequest, "Open this code on the approval page first.") // ErrServerError is a generator that kept returning codes in use or too // long, or an approved code whose user is gone. ErrServerError = auth.NewError("device.server_error", http.StatusInternalServerError, "The request could not be completed.") )
Request errors, better-auth's $ERROR_CODES. Each is mapped to the RFC 8628 error code CodeHandler and TokenHandler answer (RFCErrorCode).
var ErrClientConfig = errors.New("deviceauth: a Client needs CodeURL, TokenURL and ClientID")
ErrClientConfig is a Client missing its CodeURL, TokenURL or ClientID.
var StatusValues = []Status{StatusPending, StatusApproved, StatusDenied}
StatusValues is the canonical iteration order over statuses.
var TableCode = sqlb.Table{Name: TableNameCode, Alias: TableAliasCode}
TableCode is the device code table. Reads are open: an admin page may list pending approvals; writes go through the operations.
Functions ¶
func NormalizeUserCode ¶
NormalizeUserCode upper-cases a default-alphabet user code and drops every character outside its alphabet, so "wdjb-mjht" and "WDJB MJHT" match WDJBMJHT, as better-auth matches them.
func RFCErrorCode ¶
RFCErrorCode returns the RFC 8628 error code for an error of this plugin, the code of an RFCCoder, and server_error for anything else.
Types ¶
type Binding ¶
type Binding struct {
Grant string
Resource string
// Scope, when set, is the scope stored on the device code in place of
// the requested one: the grant's narrowed scopes (a resource's
// allowlist), so the approval page shows what will be granted.
Scope string
}
Binding is a Grant's decision for one code request.
type Client ¶
type Client struct {
// CodeURL is where CodeHandler is mounted, such as
// "https://app.example.com/auth/device/code".
CodeURL string
// TokenURL is where TokenHandler is mounted.
TokenURL string
// ClientID is sent with every request.
ClientID string
// HTTP sends the requests; nil is http.DefaultClient.
HTTP *http.Client
// Sleep waits between polls and returns ctx's error when ctx ends first;
// nil waits on a timer. A test replaces it to move a fake clock.
Sleep func(ctx context.Context, d time.Duration) error
}
Client is the device side of the flow in Go, better-auth's deviceAuthorizationClient: a CLI or any program without a browser asks CodeHandler for a code, shows the user code and verification URI, then polls TokenHandler until the user approves or denies. It speaks the RFC 8628 wire format only (form bodies, {"error": ...} answers), so it works against any RFC 8628 server, this plugin's or another's. Standard library only; golang.org/x/oauth2's device flow is the alternative for an app that already imports it.
Example ¶
ExampleClient is the device side of a CLI login, better-auth's `npx auth login` demo in Go: request a code, show it, poll until the user approves on another device. The server here is a stand-in that approves at once; in an app, CodeURL and TokenURL are where CodeHandler and TokenHandler are mounted.
mux := http.NewServeMux()
mux.HandleFunc("POST /auth/device/code", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"device_code":"d","user_code":"WDJB-MJHT","verification_uri":"https://app.example.com/device","expires_in":900,"interval":5}`)
})
mux.HandleFunc("POST /auth/device/token", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"access_token":"session-token","token_type":"Bearer","expires_in":604800}`)
})
srv := httptest.NewServer(mux)
defer srv.Close()
c := &Client{CodeURL: srv.URL + "/auth/device/code", TokenURL: srv.URL + "/auth/device/token", ClientID: "my-cli",
Sleep: func(context.Context, time.Duration) error { return nil }} // a real CLI leaves Sleep nil
ctx := context.Background()
code, err := c.RequestCode(ctx, "")
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Open %s and enter %s\n", code.VerificationURI, code.UserCode)
tok, err := c.Poll(ctx, code)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("signed in; send", tok.TokenType, "tokens from now on")
Output: Open https://app.example.com/device and enter WDJB-MJHT signed in; send Bearer tokens from now on
func (*Client) Poll ¶
func (c *Client) Poll(ctx context.Context, code CodeResult) (TokenResult, error)
Poll asks for the token until the user decides (RFC 8628 §3.4, §3.5): it waits the code's interval before each request, keeps going on authorization_pending, adds SlowDownStep (5s, the server's own step) to the interval on slow_down, and waits a 429 or temporarily_unavailable answer's Retry-After (or the interval). Any other refusal ends it as a *ClientError: the user denied (RFCAccessDenied), the code expired (RFCExpiredToken), and so on. It also ends when ctx does.
func (*Client) RequestCode ¶
RequestCode asks for a device code (RFC 8628 §3.1). scope may be empty. Show the user UserCode and VerificationURI (or VerificationURIComplete), then call Poll with the result.
type ClientError ¶
ClientError is a refusal the server answered in RFC 6749 §5.2's shape. ErrorName is the "error" member (RFCAccessDenied, RFCExpiredToken, RFCInvalidClient, ...), Status the HTTP status. An answer that is not the RFC shape is a ClientError with ErrorName RFCServerError.
func (*ClientError) Error ¶
func (e *ClientError) Error() string
Error names the RFC error code, and the server's description when it sent one.
type CodeRequest ¶
type CodeRequest struct {
ClientID string `json:"client_id"`
Scope string `json:"scope,omitempty"`
UserID string `json:"-"`
// Confidential-client credentials and the RFC 8707 resource, read only
// by a Grant (WithGrant); the standalone flow ignores them.
ClientSecret string `json:"client_secret,omitempty"`
ClientAssertion string `json:"client_assertion,omitempty"`
ClientAssertionType string `json:"client_assertion_type,omitempty"`
Resource string `json:"resource,omitempty"`
}
CodeRequest is the input of /device/code. UserID pre-binds the code to one user, better-auth's user_id: only that user may approve or deny it, and no claim is needed. It is set from Go (Code) only, because better-auth documents it for server-side use and a client has no business naming a user: it is not part of the wire (json:"-"), so no transport decodes it, the generated clients never offer it, and the RFC handlers ignore a user_id parameter as RFC 6749 section 3.2 says to treat an unrecognized one. An operation reached any other way than Code (Operation.Invoke with a decoder that sets it) is still refused with ErrInvalidRequest.
type CodeResult ¶
type CodeResult struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
VerificationURIComplete string `json:"verification_uri_complete"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
}
CodeResult is the answer of /device/code, RFC 8628 §3.2. The device code is the poller's credential and goes to that client only; the user code is what the user types. Durations are whole seconds.
type DecisionRequest ¶
type DecisionRequest struct {
UserCode string `json:"userCode"`
}
DecisionRequest is the input of approve and deny, better-auth's userCode.
type DecisionResult ¶
type DecisionResult struct {
Success bool `json:"success"`
}
DecisionResult is the answer of approve and deny.
type Generator ¶
Generator returns a new code; better-auth's generateDeviceCode and generateUserCode. A result longer than MaxCodeLength fails the request.
type Grant ¶
type Grant interface {
AuthorizeCode(ctx context.Context, c *auth.Call, in CodeRequest) (Binding, error)
}
Grant is better-auth's DeviceAuthorizationGrant: an extension that issues codes on behalf of another token endpoint (an OAuth provider). It authorizes each /device/code request instead of ValidateClient (authenticating a confidential client, checking scope and resource) and names itself in the Binding; a code bound to a grant is redeemed only through Redeem with that name, never for a session at /device/token. A Binding with an empty Grant leaves the request to the standalone flow.
type Option ¶
type Option func(*Plugin)
Option configures the plugin.
func DeviceCodeLength ¶
DeviceCodeLength overrides DefaultDeviceCodeLength (1 to MaxCodeLength).
func GenerateDeviceCode ¶
GenerateDeviceCode replaces the random device code. Risk: the device code is the poller's credential; a guessable generator signs in whoever guesses.
func GenerateUserCode ¶
GenerateUserCode replaces the random user code. A custom code is matched exactly; only codes of the default alphabet are normalized (NormalizeUserCode).
func OnDeviceAuthRequest ¶
OnDeviceAuthRequest runs fn on every code request before a code is drawn, better-auth's onDeviceAuthRequest; an error fails the request.
func UserCodeLength ¶
UserCodeLength overrides DefaultUserCodeLength (1 to MaxCodeLength).
func ValidateClient ¶
ValidateClient allows only the client ids fn accepts, better-auth's validateClient: a refused id answers invalid_client at /device/code and invalid_grant at /device/token. Without it every client id is accepted.
func VerificationURI ¶
VerificationURI overrides DefaultVerificationURI, the page a user opens. An absolute URL is used as is; a relative one is resolved against the request's base URL.
func WithExpiresIn ¶
WithExpiresIn overrides DefaultExpiresIn, better-auth's expiresIn.
func WithGrant ¶
WithGrant installs a Grant; only one, as better-auth allows one device authorization integration.
func WithInterval ¶
WithInterval overrides DefaultInterval, better-auth's interval.
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin is the device authorization plugin. Build it with New.
func (*Plugin) Code ¶
func (p *Plugin) Code(ctx context.Context, in CodeRequest) (CodeResult, error)
Code asks for a device code from Go, through the same operation as the route. Only this path may set UserID.
func (*Plugin) CodeHandler ¶
CodeHandler serves /device/code in RFC 8628's own shape: a form or JSON body, the RFC error body ({"error":"invalid_client"}, with the library's own error beside it under "auth_error"), Cache-Control: no-store. Mount it where authhttp.Handler would serve the route, so a stock device client and the library's TypeScript client both work unchanged; both run the same operation.
func (*Plugin) Operations ¶
Operations lists the five operations.
func (*Plugin) OptionErr ¶
OptionErr is every error the options recorded, such as auth.ErrNilOption naming an option given nil; auth.New refuses to start while it is not nil (auth.OptionChecker).
func (*Plugin) RateLimits ¶
RateLimits bounds user-code guessing on the approval page, better-auth's rule on /device. Polling is bounded by slow_down instead, and the device code is too long to guess.
func (*Plugin) Redeem ¶
func (p *Plugin) Redeem(ctx context.Context, deviceCode, clientID, grant string) (Redemption, error)
Redeem runs the polling state machine for a Grant's own token endpoint, better-auth's redeemDeviceCode: the same answers as /device/token (ErrAuthorizationPending, ErrPollingTooFrequently, ErrExpiredDeviceCode, ErrAccessDenied, ErrInvalidDeviceCode), and, for an approved code issued through the Grant named grant, the spent code's Redemption. A code issued through another grant, or the standalone flow, is ErrInvalidDeviceCode. It must run inside the caller's operation (its context), whose transaction the spend joins.
func (*Plugin) Sweep ¶
Sweep deletes codes that expired before cutoff; a poll deletes the ones it sees expire, and this catches the clients that stopped polling.
func (*Plugin) Token ¶
func (p *Plugin) Token(ctx context.Context, in TokenRequest) (TokenResult, error)
Token polls for the session from Go.
func (*Plugin) TokenHandler ¶
TokenHandler serves /device/token in RFC 8628's own shape, as CodeHandler does for /device/code: authorization_pending, slow_down, expired_token and access_denied come back as {"error": ...} with status 400.
func (*Plugin) TypedMigrations ¶
TypedMigrations creates the device code table.
func (*Plugin) UserTables ¶
UserTables declares that a claimed code belongs to its user, so DeleteUser removes it (no foreign key does, ADR-0002).
type RFCCoder ¶
type RFCCoder interface {
RFCCode() string
}
RFCCoder is an error that names its own RFC error code, for a Grant whose refusals (invalid_scope, invalid_target) this plugin does not know.
type Redemption ¶
Redemption is an approved device code, spent: the user who approved it, the scope and the resource it was issued for.
type TokenRequest ¶
type TokenRequest struct {
GrantType string `json:"grant_type"`
DeviceCode string `json:"device_code"`
ClientID string `json:"client_id"`
}
TokenRequest is a poll of /device/token, RFC 8628 §3.4.
type TokenResult ¶
type TokenResult struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
Scope string `json:"scope"`
// UserID is the signed-in user, for a Go caller; not part of the RFC answer.
UserID string `json:"-"`
}
TokenResult is the answer of an approved poll, RFC 6749 §5.1: the new session's token, carried in the body because handing it to a client with no cookie jar is the operation's purpose. A cookie or bearer header is set as well, as on every sign-in.
type VerifyRequest ¶
type VerifyRequest struct {
UserCode string `json:"user_code"`
}
VerifyRequest is the input of the approval page's lookup.
type VerifyResult ¶
type VerifyResult struct {
UserCode string `json:"user_code"`
Status Status `json:"status"`
ClientID string `json:"client_id,omitempty"`
Scope string `json:"scope,omitempty"`
// Resource is the RFC 8707 resource of a code issued through a Grant.
Resource string `json:"resource,omitempty"`
// Owned is true when the code belongs to the caller: claimed now, or
// earlier, or pre-bound.
Owned bool `json:"owned"`
}
VerifyResult is what the approval page shows. ClientID and Scope are given only to the user the code belongs to, so a stranger who types a code learns nothing about the client that asked.