Jamie Mansfield: 1 Support RFC 8628 (device authorisation grant) 2 files changed, 103 insertions(+), 0 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~emersion/public-inbox/patches/40498/mbox | git am -3Learn more about email & git
--- This commit should address the comments made in the first patch. TIL that Go parses RFC references, that's helpful to know - thank you! This patch is derivative of an oauth library I wrote a while back [1], and that's where some of the oddities came from (all fields having omitmepty, the missing continue stems from a last minute stylistic change I made, and the missing verification_url_complete field).
Ah, I see! That package represents scope as a []string, which I think is a better idea than using a raw string like go-oauth2 does: the space-separated scope encoding is a detail of the spec library users shouldn't care about.
[1] https://github.com/jamiemansfield/oauth/tree/master/device device.go | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ oauth2.go | 11 +++++++ 2 files changed, 103 insertions(+) create mode 100644 device.go diff --git a/device.go b/device.go new file mode 100644 index 0000000..5efe147 --- /dev/null +++ b/device.go @@ -0,0 +1,92 @@ +package oauth2 + +import ( + "errors" + "net/url" + "time" +) + +// DeviceAuthOptions are optional parameters for the device authorisation endpoint. +type DeviceAuthOptions struct { + Scope string +} + +// DeviceAuthResp contains the data returned by the device authorisation endpoint. +type DeviceAuthResp struct { + DeviceCode string `json:"device_code"` + UserCode string `json:"user_code"` + VerificationURI string `json:"verification_uri"` + VerificationURIComplete string `json:"verification_uri_complete,omitempty"` + ExpiresAt time.Time `json:"-"` + Interval time.Duration `json:"-"` +} + +// DeviceAuth performs the device authorisation request. +// +// See RFC 8628. +func (c *Client) DeviceAuth(options *DeviceAuthOptions) (*DeviceAuthResp, error) { + q := make(url.Values) + q.Set("client_id", c.ClientID) + if options.Scope != "" { + q.Set("scope", options.Scope) + } + + req, err := c.newFormURLEncodedRequest(c.Server.DeviceAuthorizationEndpoint, q) + if err != nil { + return nil, err + } + + var data struct { + DeviceAuthResp + ExpiresIn int64 `json:"expires_in"` + IntervalLength int64 `json:"interval"` + } + if err := c.doJSON(req, &data); err != nil { + return nil, err + } + + if data.ExpiresIn > 0 { + data.ExpiresAt = time.Now().Add(time.Duration(data.ExpiresIn) * time.Second) + } + if data.IntervalLength == 0 { + data.IntervalLength = 5 + } + data.Interval = time.Duration(data.IntervalLength) * time.Second + + return &data.DeviceAuthResp, nil +} + +// PollAccessToken performs the device authorisation request, polling the endpoint +// until such time that either the server responds with a token, the device code +// times out, or the server responds with an error. +// +// See RFC 8628. +func (c *Client) PollAccessToken(auth *DeviceAuthResp) (*TokenResp, error) { + for { + time.Sleep(auth.Interval) + if time.Now().After(auth.ExpiresAt) { + return nil, errors.New("oauth2: timeout occurred while polling for access token") + } + + q := make(url.Values) + q.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code") + q.Set("device_code", auth.DeviceCode) + q.Set("client_id", c.ClientID) + + resp, err := c.doToken(q) + if err == nil { + return resp, nil + } + + if err, ok := err.(*Error); ok { + if err.Code == ErrorCodeSlowDown { + auth.Interval += 5 * time.Second + continue + } else if err.Code == ErrorCodeAuthorisationPending { + continue + } + } + + return nil, err + } +} diff --git a/oauth2.go b/oauth2.go index c97a48d..2b3f0f8 100644 --- a/oauth2.go +++ b/oauth2.go @@ -37,6 +37,9 @@ type ServerMetadata struct { IntrospectionEndpointAuthSigningAlgValuesSupported []string `json:"introspection_endpoint_auth_signing_alg_values_supported,omitempty"` CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported,omitempty"` + + // RFC 8628 section 4 + DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint,omitempty"` } // ClientMetadata contains registered client metadata defined in RFC 7591. @@ -93,6 +96,9 @@ const ( GrantTypeRefreshToken GrantType = "refresh_token" GrantTypeJWTBearer GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" GrantTypeSAML2Bearer GrantType = "urn:ietf:params:oauth:grant-type:saml2-bearer" + + // RFC 8628 section 4 + GrantTypeDeviceCode GrantType = "urn:ietf:params:oauth:grant-type:device_code" ) // AuthMethod indicates how the token endpoint authenticates requests. @@ -189,6 +195,11 @@ const ( // RFC 7009 ErrorCodeUnsupportedTokenType ErrorCode = "unsupported_token_type" + + // RFC 8628 section 3.5 + ErrorCodeAuthorisationPending ErrorCode = "authorization_pending" + ErrorCodeSlowDown ErrorCode = "slow_down" + ErrorCodeExpiredToken ErrorCode = "expired_token" ) // Error is an OAuth 2.0 error returned by the server. -- 2.40.0
Pushed, thanks for your contribution!