[chore]: Bump golang.org/x/oauth2 from 0.12.0 to 0.13.0 (#2258)
This commit is contained in:
parent
274dfb5304
commit
37fe8aea49
2
go.mod
2
go.mod
|
@ -64,7 +64,7 @@ require (
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
||||||
golang.org/x/image v0.13.0
|
golang.org/x/image v0.13.0
|
||||||
golang.org/x/net v0.16.0
|
golang.org/x/net v0.16.0
|
||||||
golang.org/x/oauth2 v0.12.0
|
golang.org/x/oauth2 v0.13.0
|
||||||
golang.org/x/text v0.13.0
|
golang.org/x/text v0.13.0
|
||||||
gopkg.in/mcuadros/go-syslog.v2 v2.3.0
|
gopkg.in/mcuadros/go-syslog.v2 v2.3.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -765,8 +765,8 @@ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ
|
||||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
|
golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
|
||||||
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
|
golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
|
|
@ -0,0 +1,198 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -76,6 +76,7 @@ type TokenSource interface {
|
||||||
// endpoint URLs.
|
// endpoint URLs.
|
||||||
type Endpoint struct {
|
type Endpoint struct {
|
||||||
AuthURL string
|
AuthURL string
|
||||||
|
DeviceAuthURL string
|
||||||
TokenURL string
|
TokenURL string
|
||||||
|
|
||||||
// AuthStyle optionally specifies how the endpoint wants the
|
// AuthStyle optionally specifies how the endpoint wants the
|
||||||
|
@ -143,15 +144,19 @@ func SetAuthURLParam(key, value string) AuthCodeOption {
|
||||||
// AuthCodeURL returns a URL to OAuth 2.0 provider's consent page
|
// AuthCodeURL returns a URL to OAuth 2.0 provider's consent page
|
||||||
// that asks for permissions for the required scopes explicitly.
|
// that asks for permissions for the required scopes explicitly.
|
||||||
//
|
//
|
||||||
// State is a token to protect the user from CSRF attacks. You must
|
// State is an opaque value used by the client to maintain state between the
|
||||||
// always provide a non-empty string and validate that it matches the
|
// request and callback. The authorization server includes this value when
|
||||||
// state query parameter on your redirect callback.
|
// redirecting the user agent back to the client.
|
||||||
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
|
|
||||||
//
|
//
|
||||||
// Opts may include AccessTypeOnline or AccessTypeOffline, as well
|
// Opts may include AccessTypeOnline or AccessTypeOffline, as well
|
||||||
// as ApprovalForce.
|
// as ApprovalForce.
|
||||||
// It can also be used to pass the PKCE challenge.
|
//
|
||||||
// See https://www.oauth.com/oauth2-servers/pkce/ for more info.
|
// To protect against CSRF attacks, opts should include a PKCE challenge
|
||||||
|
// (S256ChallengeOption). Not all servers support PKCE. An alternative is to
|
||||||
|
// generate a random state parameter and verify it after exchange.
|
||||||
|
// See https://datatracker.ietf.org/doc/html/rfc6749#section-10.12 (predating
|
||||||
|
// PKCE), https://www.oauth.com/oauth2-servers/pkce/ and
|
||||||
|
// https://www.ietf.org/archive/id/draft-ietf-oauth-v2-1-09.html#name-cross-site-request-forgery (describing both approaches)
|
||||||
func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
|
func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
buf.WriteString(c.Endpoint.AuthURL)
|
buf.WriteString(c.Endpoint.AuthURL)
|
||||||
|
@ -166,7 +171,6 @@ func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
|
||||||
v.Set("scope", strings.Join(c.Scopes, " "))
|
v.Set("scope", strings.Join(c.Scopes, " "))
|
||||||
}
|
}
|
||||||
if state != "" {
|
if state != "" {
|
||||||
// TODO(light): Docs say never to omit state; don't allow empty.
|
|
||||||
v.Set("state", state)
|
v.Set("state", state)
|
||||||
}
|
}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
@ -211,10 +215,11 @@ func (c *Config) PasswordCredentialsToken(ctx context.Context, username, passwor
|
||||||
// The provided context optionally controls which HTTP client is used. See the HTTPClient variable.
|
// The provided context optionally controls which HTTP client is used. See the HTTPClient variable.
|
||||||
//
|
//
|
||||||
// The code will be in the *http.Request.FormValue("code"). Before
|
// The code will be in the *http.Request.FormValue("code"). Before
|
||||||
// calling Exchange, be sure to validate FormValue("state").
|
// calling Exchange, be sure to validate FormValue("state") if you are
|
||||||
|
// using it to protect against CSRF attacks.
|
||||||
//
|
//
|
||||||
// Opts may include the PKCE verifier code if previously used in AuthCodeURL.
|
// If using PKCE to protect against CSRF attacks, opts should include a
|
||||||
// See https://www.oauth.com/oauth2-servers/pkce/ for more info.
|
// VerifierOption.
|
||||||
func (c *Config) Exchange(ctx context.Context, code string, opts ...AuthCodeOption) (*Token, error) {
|
func (c *Config) Exchange(ctx context.Context, code string, opts ...AuthCodeOption) (*Token, error) {
|
||||||
v := url.Values{
|
v := url.Values{
|
||||||
"grant_type": {"authorization_code"},
|
"grant_type": {"authorization_code"},
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
// Copyright 2023 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
package oauth2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
codeChallengeKey = "code_challenge"
|
||||||
|
codeChallengeMethodKey = "code_challenge_method"
|
||||||
|
codeVerifierKey = "code_verifier"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateVerifier generates a PKCE code verifier with 32 octets of randomness.
|
||||||
|
// This follows recommendations in RFC 7636.
|
||||||
|
//
|
||||||
|
// A fresh verifier should be generated for each authorization.
|
||||||
|
// S256ChallengeOption(verifier) should then be passed to Config.AuthCodeURL
|
||||||
|
// (or Config.DeviceAccess) and VerifierOption(verifier) to Config.Exchange
|
||||||
|
// (or Config.DeviceAccessToken).
|
||||||
|
func GenerateVerifier() string {
|
||||||
|
// "RECOMMENDED that the output of a suitable random number generator be
|
||||||
|
// used to create a 32-octet sequence. The octet sequence is then
|
||||||
|
// base64url-encoded to produce a 43-octet URL-safe string to use as the
|
||||||
|
// code verifier."
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
|
||||||
|
data := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(data); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return base64.RawURLEncoding.EncodeToString(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifierOption returns a PKCE code verifier AuthCodeOption. It should be
|
||||||
|
// passed to Config.Exchange or Config.DeviceAccessToken only.
|
||||||
|
func VerifierOption(verifier string) AuthCodeOption {
|
||||||
|
return setParam{k: codeVerifierKey, v: verifier}
|
||||||
|
}
|
||||||
|
|
||||||
|
// S256ChallengeFromVerifier returns a PKCE code challenge derived from verifier with method S256.
|
||||||
|
//
|
||||||
|
// Prefer to use S256ChallengeOption where possible.
|
||||||
|
func S256ChallengeFromVerifier(verifier string) string {
|
||||||
|
sha := sha256.Sum256([]byte(verifier))
|
||||||
|
return base64.RawURLEncoding.EncodeToString(sha[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// S256ChallengeOption derives a PKCE code challenge derived from verifier with
|
||||||
|
// method S256. It should be passed to Config.AuthCodeURL or Config.DeviceAccess
|
||||||
|
// only.
|
||||||
|
func S256ChallengeOption(verifier string) AuthCodeOption {
|
||||||
|
return challengeOption{
|
||||||
|
challenge_method: "S256",
|
||||||
|
challenge: S256ChallengeFromVerifier(verifier),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type challengeOption struct{ challenge_method, challenge string }
|
||||||
|
|
||||||
|
func (p challengeOption) setValue(m url.Values) {
|
||||||
|
m.Set(codeChallengeMethodKey, p.challenge_method)
|
||||||
|
m.Set(codeChallengeKey, p.challenge)
|
||||||
|
}
|
|
@ -870,7 +870,7 @@ golang.org/x/net/ipv4
|
||||||
golang.org/x/net/ipv6
|
golang.org/x/net/ipv6
|
||||||
golang.org/x/net/publicsuffix
|
golang.org/x/net/publicsuffix
|
||||||
golang.org/x/net/trace
|
golang.org/x/net/trace
|
||||||
# golang.org/x/oauth2 v0.12.0
|
# golang.org/x/oauth2 v0.13.0
|
||||||
## explicit; go 1.18
|
## explicit; go 1.18
|
||||||
golang.org/x/oauth2
|
golang.org/x/oauth2
|
||||||
golang.org/x/oauth2/internal
|
golang.org/x/oauth2/internal
|
||||||
|
|
Loading…
Reference in New Issue