123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198 |
- package oauth2
- import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "strings"
- "time"
- "golang.org/x/oauth2/internal"
- )
- // https://datatracker.ietf.org/doc/html/rfc8628#section-3.5
- const (
- errAuthorizationPending = "authorization_pending"
- errSlowDown = "slow_down"
- errAccessDenied = "access_denied"
- errExpiredToken = "expired_token"
- )
- // DeviceAuthResponse describes a successful RFC 8628 Device Authorization Response
- // https://datatracker.ietf.org/doc/html/rfc8628#section-3.2
- type DeviceAuthResponse struct {
- // DeviceCode
- DeviceCode string `json:"device_code"`
- // UserCode is the code the user should enter at the verification uri
- UserCode string `json:"user_code"`
- // VerificationURI is where user should enter the user code
- VerificationURI string `json:"verification_uri"`
- // VerificationURIComplete (if populated) includes the user code in the verification URI. This is typically shown to the user in non-textual form, such as a QR code.
- VerificationURIComplete string `json:"verification_uri_complete,omitempty"`
- // Expiry is when the device code and user code expire
- Expiry time.Time `json:"expires_in,omitempty"`
- // Interval is the duration in seconds that Poll should wait between requests
- 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"`
- // workaround misspelling of verification_uri
- 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
- }
- // DeviceAuth returns a device auth struct which contains a device code
- // and authorization information provided for users to enter on another device.
- func (c *Config) DeviceAuth(ctx context.Context, opts ...AuthCodeOption) (*DeviceAuthResponse, error) {
- // https://datatracker.ietf.org/doc/html/rfc8628#section-3.1
- 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() {
- // Make a small adjustment to account for time taken by the request
- da.Expiry = da.Expiry.Add(-time.Since(t))
- }
- return da, nil
- }
- // DeviceAccessToken polls the server to exchange a device code for a token.
- 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()
- }
- // https://datatracker.ietf.org/doc/html/rfc8628#section-3.4
- 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)
- }
- // "If no value is provided, clients MUST use 5 as the default."
- // https://datatracker.ietf.org/doc/html/rfc8628#section-3.2
- 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:
- // https://datatracker.ietf.org/doc/html/rfc8628#section-3.5
- // "the interval MUST be increased by 5 seconds for this and all subsequent requests"
- interval += 5
- ticker.Reset(time.Duration(interval) * time.Second)
- case errAuthorizationPending:
- // Do nothing.
- case errAccessDenied, errExpiredToken:
- fallthrough
- default:
- return tok, err
- }
- }
- }
- }
|