@@ 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
+ }
+}
@@ 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.