oauthdevice

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 14, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

README

oauthdevice

The device authorization grant for plugin/oauthprovider, better-auth's oauthDeviceAuthorization: a registered OAuth client (a CLI, a TV app) gets a device code from plugin/deviceauth, the user approves it on the approval page, and the client polls the provider's token endpoint with grant_type=urn:ietf:params:oauth:grant-type:device_code for access, refresh and ID tokens instead of a session.

It is its own module because it is the one place both plugins are needed.

dev := oauthdevice.New("https://app.example.com/auth/device/code")
dp := deviceauth.New(deviceauth.WithGrant(dev))
op := oauthprovider.New(jw, oauthprovider.Issuer("https://app.example.com"),
	oauthprovider.WithGrant(dev.ExtensionGrant()),
	oauthprovider.DeviceAuthorizationEndpoint(dev.Endpoint()))
a, err := auth.New(..., auth.WithPlugins(email.New(...), password.New(), jw, op, dp, dev))

Register the client with urn:ietf:params:oauth:grant-type:device_code in its grant_types (and refresh_token for refresh tokens).

What it does

  • At /device/code, a client the provider knows is authenticated as at the token endpoint: client_secret_basic (Authorization header), client_secret_post, private_key_jwt (client_assertion, audience the endpoint URL), or none for a public client. Two methods at once is invalid_request. The client must have registered the device grant (unauthorized_client), and the scope (default: the client's registered scope) and resource must be allowed (invalid_scope, invalid_target). These codes come back in RFC shape from deviceauth's CodeHandler.
  • A client id the provider does not know falls through to deviceauth's standalone flow and its ValidateClient, as better-auth's does.
  • Codes are bound to the grant: /device/token refuses them (invalid_grant); the provider's token endpoint redeems them with deviceauth.Plugin.Redeem and issues tokens for the approving user, the code's scope and resource, through the same path as every grant.
  • Token endpoint errors are RFC 8628's: authorization_pending, slow_down (the interval grows by 5 seconds), expired_token, access_denied, and invalid_grant for an unknown, spent or other client's code.
  • Discovery advertises device_authorization_endpoint.

One resource per request; a repeated resource parameter is invalid_request.

Documentation

Overview

Package oauthdevice serves the device authorization grant (RFC 8628) of plugin/oauthprovider with plugin/deviceauth, better-auth's oauthDeviceAuthorization: a registered OAuth client (a CLI, a TV app) asks for a device code, the user approves on the approval page, and the client polls the provider's token endpoint with grant_type urn:ietf:params:oauth:grant-type:device_code for access, refresh and ID tokens instead of a session.

dev := oauthdevice.New("https://app.example.com/auth/device/code")
dp := deviceauth.New(deviceauth.WithGrant(dev))
op := oauthprovider.New(jw, oauthprovider.WithGrant(dev.ExtensionGrant()),
	oauthprovider.DeviceAuthorizationEndpoint(dev.Endpoint()))
auth.WithPlugins(..., dp, op, dev)

At /device/code a client the provider knows is authenticated as at its token endpoint (client_secret_basic, client_secret_post, private_key_jwt, or none for a public client), must have registered the device grant, and may ask only for scopes and a resource it is allowed. Its codes are bound to the grant: they are redeemed only at the provider's token endpoint, never for a session at /device/token. A client id the provider does not know falls through to deviceauth's standalone flow and its ValidateClient, as better-auth's does.

Index

Constants

View Source
const GrantName = "oauth"

GrantName is the name codes bound by this grant carry.

View Source
const PluginName = "oauthdevice"

PluginName is the plugin's permanent identity.

Variables

View Source
var (
	ErrAuthorizationPending = auth.NewError(errorNamespace+".authorization_pending", http.StatusBadRequest, "The user has not approved the request yet.")
	ErrSlowDown             = auth.NewError(errorNamespace+".slow_down", http.StatusBadRequest, "Polling too often. Wait longer between requests.")
	ErrExpiredToken         = auth.NewError(errorNamespace+".expired_token", http.StatusBadRequest, "The device code has expired.")
	ErrAccessDenied         = auth.NewError(errorNamespace+".access_denied", http.StatusBadRequest, "The user denied the request.")
)

Wire errors of the token endpoint, under errorNamespace: the provider answers "authorization_pending", "slow_down", "expired_token" and "access_denied" for them (RFC 8628 §3.5).

View Source
var ErrNoEndpoint = errors.New("oauthdevice: New needs the absolute URL of /device/code")

ErrNoEndpoint is a configuration error: New needs the absolute URL of the device authorization endpoint (the assertion audience and the discovery value).

Functions

This section is empty.

Types

type Integration

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

Integration joins the two plugins. Build it with New, pass it to deviceauth.WithGrant and its ExtensionGrant to oauthprovider.WithGrant, and register it beside both.

func New

func New(endpoint string) *Integration

New builds the integration for the device authorization endpoint at endpoint.

func (*Integration) AuthorizeCode

func (g *Integration) AuthorizeCode(ctx context.Context, c *auth.Call, in deviceauth.CodeRequest) (deviceauth.Binding, error)

AuthorizeCode authorizes a /device/code request for a client the provider knows, and leaves any other to the standalone flow.

func (*Integration) Endpoint

func (g *Integration) Endpoint() string

Endpoint is the device authorization endpoint's URL, for oauthprovider.DeviceAuthorizationEndpoint.

func (*Integration) ExtensionGrant

func (g *Integration) ExtensionGrant() oauthprovider.ExtensionGrant

ExtensionGrant is the device_code grant for oauthprovider.WithGrant.

func (*Integration) Init

func (g *Integration) Init(a *auth.Auth) error

Init finds both plugins. Requires has the core refuse a missing one before Init runs, so neither lookup fails in practice; both errors are still returned rather than assumed away.

func (*Integration) Name

func (g *Integration) Name() string

Name returns PluginName.

func (*Integration) Requires

func (g *Integration) Requires() []string

Requires names both plugins.

func (*Integration) Validate

func (g *Integration) Validate(*auth.Config) error

Validate refuses a relative endpoint.

Jump to

Keyboard shortcuts

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