Skip to content

Commit c5bff66

Browse files
hickfordjbrichetto
authored andcommitted
oauth2: support PKCE
Fixes golang#603 Fixes golang/go#59835 Change-Id: Ica0cfef975ba9511e00f097498d33ba27dafca0d GitHub-Last-Rev: f01f759 GitHub-Pull-Request: golang#625 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/463979 Reviewed-by: Cherry Mui <[email protected]> Run-TryBot: Matt Hickford <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]>
1 parent d8a7cab commit c5bff66

File tree

3 files changed

+88
-12
lines changed

3 files changed

+88
-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

+14-10
Original file line numberDiff line numberDiff line change
@@ -144,15 +144,19 @@ func SetAuthURLParam(key, value string) AuthCodeOption {
144144
// AuthCodeURL returns a URL to OAuth 2.0 provider's consent page
145145
// that asks for permissions for the required scopes explicitly.
146146
//
147-
// State is a token to protect the user from CSRF attacks. You must
148-
// always provide a non-empty string and validate that it matches the
149-
// state query parameter on your redirect callback.
150-
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
147+
// State is an opaque value used by the client to maintain state between the
148+
// request and callback. The authorization server includes this value when
149+
// redirecting the user agent back to the client.
151150
//
152151
// Opts may include AccessTypeOnline or AccessTypeOffline, as well
153152
// as ApprovalForce.
154-
// It can also be used to pass the PKCE challenge.
155-
// See https://www.oauth.com/oauth2-servers/pkce/ for more info.
153+
//
154+
// To protect against CSRF attacks, opts should include a PKCE challenge
155+
// (S256ChallengeOption). Not all servers support PKCE. An alternative is to
156+
// generate a random state parameter and verify it after exchange.
157+
// See https://datatracker.ietf.org/doc/html/rfc6749#section-10.12 (predating
158+
// PKCE), https://www.oauth.com/oauth2-servers/pkce/ and
159+
// https://www.ietf.org/archive/id/draft-ietf-oauth-v2-1-09.html#name-cross-site-request-forgery (describing both approaches)
156160
func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
157161
var buf bytes.Buffer
158162
buf.WriteString(c.Endpoint.AuthURL)
@@ -167,7 +171,6 @@ func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
167171
v.Set("scope", strings.Join(c.Scopes, " "))
168172
}
169173
if state != "" {
170-
// TODO(light): Docs say never to omit state; don't allow empty.
171174
v.Set("state", state)
172175
}
173176
for _, opt := range opts {
@@ -212,10 +215,11 @@ func (c *Config) PasswordCredentialsToken(ctx context.Context, username, passwor
212215
// The provided context optionally controls which HTTP client is used. See the HTTPClient variable.
213216
//
214217
// The code will be in the *http.Request.FormValue("code"). Before
215-
// calling Exchange, be sure to validate FormValue("state").
218+
// calling Exchange, be sure to validate FormValue("state") if you are
219+
// using it to protect against CSRF attacks.
216220
//
217-
// Opts may include the PKCE verifier code if previously used in AuthCodeURL.
218-
// See https://www.oauth.com/oauth2-servers/pkce/ for more info.
221+
// If using PKCE to protect against CSRF attacks, opts should include a
222+
// VerifierOption.
219223
func (c *Config) Exchange(ctx context.Context, code string, opts ...AuthCodeOption) (*Token, error) {
220224
v := url.Values{
221225
"grant_type": {"authorization_code"},

pkce.go

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

0 commit comments

Comments
 (0)