Skip to content

Commit 17dfe11

Browse files
committed
oauth2: support PKCE
1 parent a835fc4 commit 17dfe11

File tree

3 files changed

+83
-12
lines changed

3 files changed

+83
-12
lines changed

example_test.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,13 @@ func ExampleConfig() {
2626
},
2727
}
2828

29+
// use PKCE to protect against CSRF attacks
30+
// https://www.ietf.org/archive/id/draft-ietf-oauth-security-topics-22.html#name-countermeasures-6
31+
verifier := oauth2.GenerateVerifier()
32+
2933
// Redirect user to consent page to ask for permission
3034
// for the scopes specified above.
31-
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
35+
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier))
3236
fmt.Printf("Visit the URL for the auth dialog: %v", url)
3337

3438
// Use the authorization code that is pushed to the redirect
@@ -39,7 +43,7 @@ func ExampleConfig() {
3943
if _, err := fmt.Scan(&code); err != nil {
4044
log.Fatal(err)
4145
}
42-
tok, err := conf.Exchange(ctx, code)
46+
tok, err := conf.Exchange(ctx, code, oauth2.VerifierOption(verifier))
4347
if err != nil {
4448
log.Fatal(err)
4549
}

oauth2.go

+12-10
Original file line numberDiff line numberDiff line change
@@ -143,15 +143,17 @@ func SetAuthURLParam(key, value string) AuthCodeOption {
143143
// AuthCodeURL returns a URL to OAuth 2.0 provider's consent page
144144
// that asks for permissions for the required scopes explicitly.
145145
//
146-
// State is a token to protect the user from CSRF attacks. You must
147-
// always provide a non-empty string and validate that it matches the
148-
// state query parameter on your redirect callback.
149-
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
146+
// An opaque value used by the client to maintain state between the request and
147+
// callback. The authorization server includes this value when redirecting the
148+
// user agent back to the client. See https://www.ietf.org/archive/id/draft-ietf-oauth-v2-1-09.html#section-4.1.1-8.12.1
150149
//
151150
// Opts may include AccessTypeOnline or AccessTypeOffline, as well
152151
// as ApprovalForce.
153-
// It can also be used to pass the PKCE challenge.
154-
// See https://www.oauth.com/oauth2-servers/pkce/ for more info.
152+
//
153+
// To protect against CSRF attacks, opts should include a PKCE challenge
154+
// (S256ChallengeOption). Not all servers support PKCE. An alternative is to
155+
// generate a random state parameter and verify it after exchange.
156+
// See https://www.ietf.org/archive/id/draft-ietf-oauth-v2-1-09.html#name-cross-site-request-forgery
155157
func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
156158
var buf bytes.Buffer
157159
buf.WriteString(c.Endpoint.AuthURL)
@@ -166,7 +168,6 @@ func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
166168
v.Set("scope", strings.Join(c.Scopes, " "))
167169
}
168170
if state != "" {
169-
// TODO(light): Docs say never to omit state; don't allow empty.
170171
v.Set("state", state)
171172
}
172173
for _, opt := range opts {
@@ -211,10 +212,11 @@ func (c *Config) PasswordCredentialsToken(ctx context.Context, username, passwor
211212
// The provided context optionally controls which HTTP client is used. See the HTTPClient variable.
212213
//
213214
// The code will be in the *http.Request.FormValue("code"). Before
214-
// calling Exchange, be sure to validate FormValue("state").
215+
// calling Exchange, be sure to validate FormValue("state") if you are
216+
// using it to protect against CSRF attacks.
215217
//
216-
// Opts may include the PKCE verifier code if previously used in AuthCodeURL.
217-
// See https://www.oauth.com/oauth2-servers/pkce/ for more info.
218+
// If using PKCE to protect against CSRF attacks, opts should include the
219+
// verifier (VerifierOption).
218220
func (c *Config) Exchange(ctx context.Context, code string, opts ...AuthCodeOption) (*Token, error) {
219221
v := url.Values{
220222
"grant_type": {"authorization_code"},

pkce.go

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
"net/url"
11+
)
12+
13+
const (
14+
codeChallengeKey = "code_challenge"
15+
codeChallengeMethodKey = "code_challenge_method"
16+
codeVerifierKey = "code_verifier"
17+
)
18+
19+
// GenerateVerifier generates a PKCE code verifier with 32 octets of randomness.
20+
// This follows recommendations in RFC 7636.
21+
//
22+
// A fresh verifier should be generated for each authorization.
23+
// S256ChallengeOption(verifier) should then be passed to Config.AuthCodeURL and
24+
// VerifierOption(verifier) to Config.Exchange.
25+
func GenerateVerifier() string {
26+
// "RECOMMENDED that the output of a suitable random number generator be
27+
// used to create a 32-octet sequence. The octet sequence is then
28+
// base64url-encoded to produce a 43-octet URL-safe string to use as the
29+
// code verifier."
30+
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
31+
data := make([]byte, 32)
32+
if _, err := rand.Read(data); err != nil {
33+
panic(err)
34+
}
35+
return base64.RawURLEncoding.EncodeToString(data)
36+
}
37+
38+
// VerifierOption describes a PKCE code verifier. It should be
39+
// passed to Config.Exchange only.
40+
func VerifierOption(verifier string) AuthCodeOption {
41+
return setParam{k: codeVerifierKey, v: verifier}
42+
}
43+
44+
// S256ChallengeFromVerifier returns a PKCE code challenge derived from verifier with method S256.
45+
//
46+
// Prefer to use S256ChallengeOption where possible.
47+
func S256ChallengeFromVerifier(verifier string) string {
48+
sha := sha256.Sum256([]byte(verifier))
49+
return base64.RawURLEncoding.EncodeToString(sha[:])
50+
}
51+
52+
// S256ChallengeOption derives a PKCE code challenge derived from verifier with method S256. It should be passed to Config.AuthCodeURL only.
53+
func S256ChallengeOption(verifier string) AuthCodeOption {
54+
return challengeOption{
55+
challenge_method: "S256",
56+
challenge: S256ChallengeFromVerifier(verifier),
57+
}
58+
}
59+
60+
type challengeOption struct{ challenge_method, challenge string }
61+
62+
func (p challengeOption) setValue(m url.Values) {
63+
m.Set(codeChallengeMethodKey, p.challenge_method)
64+
m.Set(codeChallengeKey, p.challenge)
65+
}

0 commit comments

Comments
 (0)