---
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).
[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!
On Wednesday, April 19th, 2023 at 23:45, Jamie Mansfield <jmansfield@cadixdev.org> wrote:
> 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.