Skip to content

Commit 696f883

Browse files
committed
oauth2: support PKCE
Implements incoming proposal golang/go#59835
1 parent cfe200d commit 696f883

File tree

4 files changed

+92
-93
lines changed

4 files changed

+92
-93
lines changed

authhandler/authhandler.go

+3-26
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,7 @@ import (
1313
"golang.org/x/oauth2"
1414
)
1515

16-
const (
17-
// Parameter keys for AuthCodeURL method to support PKCE.
18-
codeChallengeKey = "code_challenge"
19-
codeChallengeMethodKey = "code_challenge_method"
20-
21-
// Parameter key for Exchange method to support PKCE.
22-
codeVerifierKey = "code_verifier"
23-
)
24-
25-
// PKCEParams holds parameters to support PKCE.
26-
type PKCEParams struct {
27-
Challenge string // The unpadded, base64-url-encoded string of the encrypted code verifier.
28-
ChallengeMethod string // The encryption method (ex. S256).
29-
Verifier string // The original, non-encrypted secret.
30-
}
16+
type PKCEParams = oauth2.PKCEParams
3117

3218
// AuthorizationHandler is a 3-legged-OAuth helper that prompts
3319
// the user for OAuth consent at the specified auth code URL
@@ -71,12 +57,7 @@ type authHandlerSource struct {
7157

7258
func (source authHandlerSource) Token() (*oauth2.Token, error) {
7359
// Step 1: Obtain auth code.
74-
var authCodeUrlOptions []oauth2.AuthCodeOption
75-
if source.pkce != nil && source.pkce.Challenge != "" && source.pkce.ChallengeMethod != "" {
76-
authCodeUrlOptions = []oauth2.AuthCodeOption{oauth2.SetAuthURLParam(codeChallengeKey, source.pkce.Challenge),
77-
oauth2.SetAuthURLParam(codeChallengeMethodKey, source.pkce.ChallengeMethod)}
78-
}
79-
url := source.config.AuthCodeURL(source.state, authCodeUrlOptions...)
60+
url := source.config.AuthCodeURLWithPKCE(source.state, source.pkce)
8061
code, state, err := source.authHandler(url)
8162
if err != nil {
8263
return nil, err
@@ -86,9 +67,5 @@ func (source authHandlerSource) Token() (*oauth2.Token, error) {
8667
}
8768

8869
// Step 2: Exchange auth code for access token.
89-
var exchangeOptions []oauth2.AuthCodeOption
90-
if source.pkce != nil && source.pkce.Verifier != "" {
91-
exchangeOptions = []oauth2.AuthCodeOption{oauth2.SetAuthURLParam(codeVerifierKey, source.pkce.Verifier)}
92-
}
93-
return source.config.Exchange(source.ctx, code, exchangeOptions...)
70+
return source.config.ExchangeWithPKCE(source.ctx, code, source.pkce)
9471
}

example_test.go

+5-53
Original file line numberDiff line numberDiff line change
@@ -8,62 +8,19 @@ import (
88
"context"
99
"fmt"
1010
"log"
11-
"net/http"
12-
"time"
1311

1412
"golang.org/x/oauth2"
1513
)
1614

1715
func ExampleConfig() {
1816
ctx := context.Background()
19-
conf := &oauth2.Config{
20-
ClientID: "YOUR_CLIENT_ID",
21-
ClientSecret: "YOUR_CLIENT_SECRET",
22-
Scopes: []string{"SCOPE1", "SCOPE2"},
23-
Endpoint: oauth2.Endpoint{
24-
AuthURL: "https://provider.com/o/oauth2/auth",
25-
TokenURL: "https://provider.com/o/oauth2/token",
26-
},
27-
}
17+
var conf oauth2.Config
2818

29-
// Redirect user to consent page to ask for permission
30-
// for the scopes specified above.
31-
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
32-
fmt.Printf("Visit the URL for the auth dialog: %v", url)
33-
34-
// Use the authorization code that is pushed to the redirect
35-
// URL. Exchange will do the handshake to retrieve the
36-
// initial access token. The HTTP Client returned by
37-
// conf.Client will refresh the token as necessary.
38-
var code string
39-
if _, err := fmt.Scan(&code); err != nil {
40-
log.Fatal(err)
41-
}
42-
tok, err := conf.Exchange(ctx, code)
43-
if err != nil {
44-
log.Fatal(err)
45-
}
46-
47-
client := conf.Client(ctx, tok)
48-
client.Get("...")
49-
}
50-
51-
func ExampleConfig_customHTTP() {
52-
ctx := context.Background()
53-
54-
conf := &oauth2.Config{
55-
ClientID: "YOUR_CLIENT_ID",
56-
ClientSecret: "YOUR_CLIENT_SECRET",
57-
Scopes: []string{"SCOPE1", "SCOPE2"},
58-
Endpoint: oauth2.Endpoint{
59-
TokenURL: "https://provider.com/o/oauth2/token",
60-
AuthURL: "https://provider.com/o/oauth2/auth",
61-
},
62-
}
19+
pkce := oauth2.GeneratePKCEParams()
6320

6421
// Redirect user to consent page to ask for permission
6522
// for the scopes specified above.
66-
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
23+
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline, pkce.ChallengeOption())
6724
fmt.Printf("Visit the URL for the auth dialog: %v", url)
6825

6926
// Use the authorization code that is pushed to the redirect
@@ -74,16 +31,11 @@ func ExampleConfig_customHTTP() {
7431
if _, err := fmt.Scan(&code); err != nil {
7532
log.Fatal(err)
7633
}
77-
78-
// Use the custom HTTP client when requesting a token.
79-
httpClient := &http.Client{Timeout: 2 * time.Second}
80-
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
81-
82-
tok, err := conf.Exchange(ctx, code)
34+
tok, err := conf.Exchange(ctx, code, pkce.VerifierOption())
8335
if err != nil {
8436
log.Fatal(err)
8537
}
8638

8739
client := conf.Client(ctx, tok)
88-
_ = client
40+
client.Get("...")
8941
}

oauth2.go

+20-14
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ var (
121121
ApprovalForce AuthCodeOption = SetAuthURLParam("prompt", "consent")
122122
)
123123

124-
// An AuthCodeOption is passed to Config.AuthCodeURL.
124+
// An AuthCodeOption may be passed to Config.AuthCodeURL or Config.Exchange.
125125
type AuthCodeOption interface {
126126
setValue(url.Values)
127127
}
@@ -139,16 +139,12 @@ func SetAuthURLParam(key, value string) AuthCodeOption {
139139
// AuthCodeURL returns a URL to OAuth 2.0 provider's consent page
140140
// that asks for permissions for the required scopes explicitly.
141141
//
142-
// State is a token to protect the user from CSRF attacks. You must
143-
// always provide a non-empty string and validate that it matches the
144-
// state query parameter on your redirect callback.
145-
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
142+
// State is an opaque value used by the client to maintain state
143+
// between the request and callback.
146144
//
147145
// Opts may include AccessTypeOnline or AccessTypeOffline, as well
148146
// as ApprovalForce.
149-
// It can also be used to pass the PKCE challenge.
150-
// See https://www.oauth.com/oauth2-servers/pkce/ for more info.
151-
func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
147+
func (c *Config) AuthCodeURLWithPKCE(state string, pkce *PKCEParams, opts ...AuthCodeOption) string {
152148
var buf bytes.Buffer
153149
buf.WriteString(c.Endpoint.AuthURL)
154150
v := url.Values{
@@ -162,9 +158,11 @@ func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
162158
v.Set("scope", strings.Join(c.Scopes, " "))
163159
}
164160
if state != "" {
165-
// TODO(light): Docs say never to omit state; don't allow empty.
166161
v.Set("state", state)
167162
}
163+
if pkce != nil {
164+
pkce.challengeOption().setValue(v)
165+
}
168166
for _, opt := range opts {
169167
opt.setValue(v)
170168
}
@@ -177,6 +175,10 @@ func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
177175
return buf.String()
178176
}
179177

178+
func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
179+
return c.AuthCodeURLWithPKCE(state, nil, opts...)
180+
}
181+
180182
// PasswordCredentialsToken converts a resource owner username and password
181183
// pair into a token.
182184
//
@@ -208,24 +210,28 @@ func (c *Config) PasswordCredentialsToken(ctx context.Context, username, passwor
208210
//
209211
// The code will be in the *http.Request.FormValue("code"). Before
210212
// calling Exchange, be sure to validate FormValue("state").
211-
//
212-
// Opts may include the PKCE verifier code if previously used in AuthCodeURL.
213-
// See https://www.oauth.com/oauth2-servers/pkce/ for more info.
214-
func (c *Config) Exchange(ctx context.Context, code string, opts ...AuthCodeOption) (*Token, error) {
213+
func (c *Config) ExchangeWithPKCE(ctx context.Context, code string, pkce *PKCEParams, opts ...AuthCodeOption) (*Token, error) {
215214
v := url.Values{
216215
"grant_type": {"authorization_code"},
217216
"code": {code},
218217
}
219218
if c.RedirectURL != "" {
220219
v.Set("redirect_uri", c.RedirectURL)
221220
}
221+
if pkce != nil {
222+
pkce.verifierOption().setValue(v)
223+
}
222224
for _, opt := range opts {
223225
opt.setValue(v)
224226
}
225227
return retrieveToken(ctx, c, v)
226228
}
227229

228-
// Client returns an HTTP client using the provided token.
230+
func (c *Config) Exchange(ctx context.Context, code string, opts ...AuthCodeOption) (*Token, error) {
231+
return c.ExchangeWithPKCE(ctx, code, nil, opts...)
232+
}
233+
234+
`345432Q// Client returns an HTTP client using the provided token.
229235
// The token will auto-refresh as necessary. The underlying
230236
// HTTP transport will be obtained using the provided context.
231237
// The returned client and its Transport should not be modified.

pkce.go

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2014 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
package oauth2
5+
6+
import (
7+
"crypto/rand"
8+
"crypto/sha256"
9+
"encoding/base64"
10+
"io"
11+
"net/url"
12+
)
13+
14+
// PKCEParams holds a PKCE challenge and verifier as described in RFC 7636
15+
// https://datatracker.ietf.org/doc/html/rfc7636
16+
type PKCEParams struct {
17+
Challenge string
18+
ChallengeMethod string
19+
Verifier string
20+
}
21+
22+
const (
23+
codeChallengeKey = "code_challenge"
24+
codeChallengeMethodKey = "code_challenge_method"
25+
codeVerifierKey = "code_verifier"
26+
)
27+
28+
// challengeOption should be passed to Config.AuthCodeURL. The option returned sets the code_challenge and code_challenge_method parameters.
29+
func (p *PKCEParams) challengeOption() AuthCodeOption {
30+
return set2Values{k1: codeChallengeKey, v1: p.Challenge, k2: codeChallengeMethodKey, v2: p.ChallengeMethod}
31+
}
32+
33+
type set2Values struct{ k1, v1, k2, v2 string }
34+
35+
func (p set2Values) setValue(m url.Values) {
36+
m.Set(p.k1, p.v1)
37+
m.Set(p.k2, p.v2)
38+
}
39+
40+
// verifierOption should be passed to Config.Exchange. The option returned sets the code_verifier parameters.
41+
func (p *PKCEParams) verifierOption() AuthCodeOption {
42+
return SetAuthURLParam(codeVerifierKey, p.Verifier)
43+
}
44+
45+
// GeneratePKCEParams generates a code verifier with 32 octets of randomness and a S256 challenge (this follows recommendations in RFC 7636).
46+
//
47+
// A fresh verifier should be generated for each AuthCodeURL call.
48+
func GeneratePKCEParams() *PKCEParams {
49+
// "RECOMMENDED that the output of a suitable random number generator be used to create a 32-octet
50+
// sequence. The octet sequence is then base64url-encoded to produce a 43-octet URL-safe string to use as the code verifier."
51+
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
52+
data := make([]byte, 32)
53+
if _, err := io.ReadFull(rand.Reader, data); err != nil {
54+
panic(err)
55+
}
56+
verifier := base64.URLEncoding.EncodeToString(data)
57+
sha := sha256.Sum256([]byte(verifier))
58+
challenge := base64.URLEncoding.EncodeToString(sha[:])
59+
return &PKCEParams{
60+
Challenge: challenge,
61+
ChallengeMethod: "S256",
62+
Verifier: verifier,
63+
}
64+
}

0 commit comments

Comments
 (0)