Skip to content
Snippets Groups Projects
Select Git revision
  • d9723f823ebeaed9b19303a6a7056688c6eda22c
  • main default protected
  • v0.1.0
3 results

config.go

Blame
  • 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)
    		}
    	}
    }