AuthorizeDevice begins the device authorization flow. See: https://tools.ietf.org/html/rfc8628#section-3.1
(ctx context.Context)
| 669 | // AuthorizeDevice begins the device authorization flow. |
| 670 | // See: https://tools.ietf.org/html/rfc8628#section-3.1 |
| 671 | func (c *DeviceAuth) AuthorizeDevice(ctx context.Context) (*codersdk.ExternalAuthDevice, error) { |
| 672 | if c.CodeURL == "" { |
| 673 | return nil, xerrors.New("oauth2: device code URL not set") |
| 674 | } |
| 675 | codeURL, err := c.formatDeviceCodeURL() |
| 676 | if err != nil { |
| 677 | return nil, err |
| 678 | } |
| 679 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, codeURL, nil) |
| 680 | if err != nil { |
| 681 | return nil, err |
| 682 | } |
| 683 | req.Header.Set("Accept", "application/json") |
| 684 | |
| 685 | do := http.DefaultClient.Do |
| 686 | if c.Config != nil { |
| 687 | // The cfg can be nil in unit tests. |
| 688 | do = func(req *http.Request) (*http.Response, error) { |
| 689 | return c.Config.Do(ctx, promoauth.SourceAuthorizeDevice, req) |
| 690 | } |
| 691 | } |
| 692 | |
| 693 | resp, err := do(req) |
| 694 | if err != nil { |
| 695 | return nil, err |
| 696 | } |
| 697 | defer resp.Body.Close() |
| 698 | var r struct { |
| 699 | codersdk.ExternalAuthDevice |
| 700 | ErrorDescription string `json:"error_description"` |
| 701 | } |
| 702 | err = json.NewDecoder(resp.Body).Decode(&r) |
| 703 | if err != nil { |
| 704 | mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) |
| 705 | if err != nil { |
| 706 | mediaType = "unknown" |
| 707 | } |
| 708 | |
| 709 | // If the json fails to decode, do a best effort to return a better error. |
| 710 | switch { |
| 711 | case resp.StatusCode == http.StatusTooManyRequests: |
| 712 | retryIn := "please try again later" |
| 713 | resetIn := resp.Header.Get("x-ratelimit-reset") |
| 714 | if resetIn != "" { |
| 715 | // Best effort to tell the user exactly how long they need |
| 716 | // to wait for. |
| 717 | unix, err := strconv.ParseInt(resetIn, 10, 64) |
| 718 | if err == nil { |
| 719 | waitFor := time.Unix(unix, 0).Sub(time.Now().Truncate(time.Second)) |
| 720 | retryIn = fmt.Sprintf(" retry after %s", waitFor.Truncate(time.Second)) |
| 721 | } |
| 722 | } |
| 723 | // 429 returns a plaintext payload with a message. |
| 724 | return nil, xerrors.New(fmt.Sprintf("rate limit hit, unable to authorize device. %s", retryIn)) |
| 725 | case mediaType == "application/x-www-form-urlencoded": |
| 726 | return nil, xerrors.Errorf("status_code=%d, payload response is form-url encoded, expected a json payload", resp.StatusCode) |
| 727 | default: |
| 728 | return nil, xerrors.Errorf("status_code=%d, mediaType=%s: %w", resp.StatusCode, mediaType, err) |
no test coverage detected