Skip to content

Commit bc506f1

Browse files
committed
oauth2: support PKCE
1 parent 62b4eed commit bc506f1

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
@@ -120,7 +120,7 @@ var (
120120
ApprovalForce AuthCodeOption = SetAuthURLParam("prompt", "consent")
121121
)
122122

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

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

227-
// Client returns an HTTP client using the provided token.
229+
func (c *Config) Exchange(ctx context.Context, code string, opts ...AuthCodeOption) (*Token, error) {
230+
return c.ExchangeWithPKCE(ctx, code, nil, opts...)
231+
}
232+
233+
`345432Q// Client returns an HTTP client using the provided token.
228234
// The token will auto-refresh as necessary. The underlying
229235
// HTTP transport will be obtained using the provided context.
230236
// 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)