~emersion/public-inbox

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
1

[PATCH go-oauth2 v2] Support RFC 8628 (device authorisation grant)

Jamie Mansfield <jmansfield@cadixdev.org>
Details
Message ID
<20230419214537.22811-1-jmansfield@cadixdev.org>
DKIM signature
pass
Download raw message
Patch: +103 -0
---
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
Details
Message ID
<0V5FinfrOI0itwpZ_vcuQwnVij5b4quoC4TI9g-R1YD55Zvk2UoQg89oL2D7KhDe8bVVFPLaDHojZ2gM6cw7rnOfzsL2sSIdt6KL9LNIms4=@emersion.fr>
In-Reply-To
<20230419214537.22811-1-jmansfield@cadixdev.org> (view parent)
DKIM signature
pass
Download raw message
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.
Reply to thread Export thread (mbox)