Select Git revision
Janne Mareike Koschinski authored
config.go 3.13 KiB
package deviceauth
import (
"fmt"
"golang.org/x/oauth2"
"net/url"
"scaleway-dedibox-api/httputil"
"strings"
"time"
)
const (
errAuthorizationPending = "authorization_pending"
errSlowDown = "slow_down"
errAccessDenied = "access_denied"
errExpiredToken = "expired_token"
)
type Endpoint struct {
AuthURL string
TokenURL string
DeviceCodeURL string
// AuthStyle optionally specifies how the endpoint wants the
// client ID & client secret sent. The zero value means to
// auto-detect.
AuthStyle AuthStyle
}
type DeviceAuthConfig struct {
Client *httputil.Client
// ClientID is the application's ID.
ClientID string
// ClientSecret is the application's secret.
ClientSecret string
// Endpoint contains the resource server's token endpoint URLs.
Endpoint Endpoint
// Scope specifies optional requested permissions.
Scopes []string
}
const maximumBodySize = 1 << 2
// AuthDevice returns a device auth struct which contains a device code
// and authorization information provided for users to enter on another device.
func (config *DeviceAuthConfig) AuthDevice(opts ...AuthCodeOption) (*DeviceAuth, error) {
v := url.Values{
"client_id": {config.ClientID},
}
if len(config.Scopes) > 0 {
v.Set("scope", strings.Join(config.Scopes, " "))
}
for _, opt := range opts {
opt.setValue(v)
}
v.Set("new_credentials", "yes")
now := time.Now()
da, err := config.retrieveDeviceAuth(v)
if err != nil {
return nil, fmt.Errorf("deviceauth: unable to authenticate device\n %w", err)
}
verificationUri := da.VerificationURI
if verificationUri == "" {
verificationUri = da.VerificationURL
}
return &DeviceAuth{
DeviceCode: da.DeviceCode,
UserCode: da.UserCode,
VerificationURI: verificationUri,
VerificationURIComplete: da.VerificationURIComplete,
Expires: da.ExpiresIn.expiry(now),
Interval: time.Duration(da.Interval) * time.Second,
}, nil
}
// Poll polls to exchange a device code for a token.
func (config *DeviceAuthConfig) Poll(devideAuth *DeviceAuth, options ...AuthCodeOption) (*oauth2.Token, error) {
params := url.Values{
"client_id": {config.ClientID},
"grant_type": {"http://oauth.net/grant_type/device/1.0"},
"device_code": {devideAuth.DeviceCode},
"code": {devideAuth.DeviceCode},
}
if len(config.Scopes) > 0 {
params.Set("scope", strings.Join(config.Scopes, " "))
}
for _, option := range options {
option.setValue(params)
}
// If no interval was provided, the client MUST use a reasonable default polling interval.
// See https://tools.ietf.org/html/draft-ietf-oauth-device-flow-07#section-3.5
interval := devideAuth.Interval
if interval == 0 {
interval = 5 * time.Second
}
for {
tok, err := config.retrieveToken(params, config.Endpoint.AuthStyle)
if err == nil {
return tok, nil
}
errTyp := parseError(err)
switch errTyp {
case errAccessDenied, errExpiredToken:
return tok, fmt.Errorf("deviceauth: unable to poll token: %s\n %w", errTyp, err)
case errSlowDown:
interval += 5 * time.Second
fallthrough
case errAuthorizationPending:
time.Sleep(interval)
}
}
}