package oauth2
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"golang.org/x/oauth2/internal"
)
const (
errAuthorizationPending = "authorization_pending"
errSlowDown = "slow_down"
errAccessDenied = "access_denied"
errExpiredToken = "expired_token"
)
type DeviceAuthResponse struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
VerificationURIComplete string `json:"verification_uri_complete,omitempty"`
Expiry time .Time `json:"expires_in,omitempty"`
Interval int64 `json:"interval,omitempty"`
}
func (d DeviceAuthResponse ) MarshalJSON () ([]byte , error ) {
type Alias DeviceAuthResponse
var expiresIn int64
if !d .Expiry .IsZero () {
expiresIn = int64 (time .Until (d .Expiry ).Seconds ())
}
return json .Marshal (&struct {
ExpiresIn int64 `json:"expires_in,omitempty"`
*Alias
}{
ExpiresIn : expiresIn ,
Alias : (*Alias )(&d ),
})
}
func (c *DeviceAuthResponse ) UnmarshalJSON (data []byte ) error {
type Alias DeviceAuthResponse
aux := &struct {
ExpiresIn int64 `json:"expires_in"`
VerificationURL string `json:"verification_url"`
*Alias
}{
Alias : (*Alias )(c ),
}
if err := json .Unmarshal (data , &aux ); err != nil {
return err
}
if aux .ExpiresIn != 0 {
c .Expiry = time .Now ().UTC ().Add (time .Second * time .Duration (aux .ExpiresIn ))
}
if c .VerificationURI == "" {
c .VerificationURI = aux .VerificationURL
}
return nil
}
func (c *Config ) DeviceAuth (ctx context .Context , opts ...AuthCodeOption ) (*DeviceAuthResponse , error ) {
v := url .Values {
"client_id" : {c .ClientID },
}
if len (c .Scopes ) > 0 {
v .Set ("scope" , strings .Join (c .Scopes , " " ))
}
for _ , opt := range opts {
opt .setValue (v )
}
return retrieveDeviceAuth (ctx , c , v )
}
func retrieveDeviceAuth(ctx context .Context , c *Config , v url .Values ) (*DeviceAuthResponse , error ) {
if c .Endpoint .DeviceAuthURL == "" {
return nil , errors .New ("endpoint missing DeviceAuthURL" )
}
req , err := http .NewRequest ("POST" , c .Endpoint .DeviceAuthURL , strings .NewReader (v .Encode ()))
if err != nil {
return nil , err
}
req .Header .Set ("Content-Type" , "application/x-www-form-urlencoded" )
req .Header .Set ("Accept" , "application/json" )
t := time .Now ()
r , err := internal .ContextClient (ctx ).Do (req )
if err != nil {
return nil , err
}
body , err := io .ReadAll (io .LimitReader (r .Body , 1 <<20 ))
if err != nil {
return nil , fmt .Errorf ("oauth2: cannot auth device: %v" , err )
}
if code := r .StatusCode ; code < 200 || code > 299 {
return nil , &RetrieveError {
Response : r ,
Body : body ,
}
}
da := &DeviceAuthResponse {}
err = json .Unmarshal (body , &da )
if err != nil {
return nil , fmt .Errorf ("unmarshal %s" , err )
}
if !da .Expiry .IsZero () {
da .Expiry = da .Expiry .Add (-time .Since (t ))
}
return da , nil
}
func (c *Config ) DeviceAccessToken (ctx context .Context , da *DeviceAuthResponse , opts ...AuthCodeOption ) (*Token , error ) {
if !da .Expiry .IsZero () {
var cancel context .CancelFunc
ctx , cancel = context .WithDeadline (ctx , da .Expiry )
defer cancel ()
}
v := url .Values {
"client_id" : {c .ClientID },
"grant_type" : {"urn:ietf:params:oauth:grant-type:device_code" },
"device_code" : {da .DeviceCode },
}
if len (c .Scopes ) > 0 {
v .Set ("scope" , strings .Join (c .Scopes , " " ))
}
for _ , opt := range opts {
opt .setValue (v )
}
interval := da .Interval
if interval == 0 {
interval = 5
}
ticker := time .NewTicker (time .Duration (interval ) * time .Second )
defer ticker .Stop ()
for {
select {
case <- ctx .Done ():
return nil , ctx .Err ()
case <- ticker .C :
tok , err := retrieveToken (ctx , c , v )
if err == nil {
return tok , nil
}
e , ok := err .(*RetrieveError )
if !ok {
return nil , err
}
switch e .ErrorCode {
case errSlowDown :
interval += 5
ticker .Reset (time .Duration (interval ) * time .Second )
case errAuthorizationPending :
case errAccessDenied , errExpiredToken :
fallthrough
default :
return tok , err
}
}
}
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .