Skip to content

Commit 53a6d31

Browse files
committed
make PKCE easier
1 parent 62b4eed commit 53a6d31

File tree

3 files changed

+71
-80
lines changed

3 files changed

+71
-80
lines changed

authhandler/authhandler.go

+4-19
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
@@ -73,8 +59,7 @@ func (source authHandlerSource) Token() (*oauth2.Token, error) {
7359
// Step 1: Obtain auth code.
7460
var authCodeUrlOptions []oauth2.AuthCodeOption
7561
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)}
62+
authCodeUrlOptions = append(authCodeUrlOptions, source.pkce.ChallengeOption())
7863
}
7964
url := source.config.AuthCodeURL(source.state, authCodeUrlOptions...)
8065
code, state, err := source.authHandler(url)
@@ -87,8 +72,8 @@ func (source authHandlerSource) Token() (*oauth2.Token, error) {
8772

8873
// Step 2: Exchange auth code for access token.
8974
var exchangeOptions []oauth2.AuthCodeOption
90-
if source.pkce != nil && source.pkce.Verifier != "" {
91-
exchangeOptions = []oauth2.AuthCodeOption{oauth2.SetAuthURLParam(codeVerifierKey, source.pkce.Verifier)}
75+
if source.pkce != nil {
76+
exchangeOptions = append(exchangeOptions, source.pkce.VerifierOption())
9277
}
9378
return source.config.Exchange(source.ctx, code, exchangeOptions...)
9479
}

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

+62-8
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ package oauth2 // import "golang.org/x/oauth2"
1111
import (
1212
"bytes"
1313
"context"
14+
"crypto/rand"
15+
"crypto/sha256"
16+
"encoding/base64"
1417
"errors"
18+
"io"
1519
"net/http"
1620
"net/url"
1721
"strings"
@@ -120,7 +124,7 @@ var (
120124
ApprovalForce AuthCodeOption = SetAuthURLParam("prompt", "consent")
121125
)
122126

123-
// An AuthCodeOption is passed to Config.AuthCodeURL.
127+
// An AuthCodeOption may be passed to Config.AuthCodeURL or Config.Exchange.
124128
type AuthCodeOption interface {
125129
setValue(url.Values)
126130
}
@@ -138,15 +142,14 @@ func SetAuthURLParam(key, value string) AuthCodeOption {
138142
// AuthCodeURL returns a URL to OAuth 2.0 provider's consent page
139143
// that asks for permissions for the required scopes explicitly.
140144
//
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.
145+
// State is an opaque value used by the client to maintain state
146+
// between the request and callback.
145147
//
146148
// Opts may include AccessTypeOnline or AccessTypeOffline, as well
147149
// 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+
//
151+
// To protect against cross-site request forgery (CSRF),
152+
// it is recommended to pass PKCEParams.ChallengeOption().
150153
func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
151154
var buf bytes.Buffer
152155
buf.WriteString(c.Endpoint.AuthURL)
@@ -161,7 +164,6 @@ func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
161164
v.Set("scope", strings.Join(c.Scopes, " "))
162165
}
163166
if state != "" {
164-
// TODO(light): Docs say never to omit state; don't allow empty.
165167
v.Set("state", state)
166168
}
167169
for _, opt := range opts {
@@ -379,3 +381,55 @@ func ReuseTokenSource(t *Token, src TokenSource) TokenSource {
379381
new: src,
380382
}
381383
}
384+
385+
// PKCEParams holds a PKCE challenge and verifier as described in RFC 7636
386+
// https://datatracker.ietf.org/doc/html/rfc7636
387+
type PKCEParams struct {
388+
Challenge string
389+
ChallengeMethod string
390+
Verifier string
391+
}
392+
393+
const (
394+
codeChallengeKey = "code_challenge"
395+
codeChallengeMethodKey = "code_challenge_method"
396+
codeVerifierKey = "code_verifier"
397+
)
398+
399+
// ChallengeOption should be passed to Config.AuthCodeURL. The option returned sets the code_challenge and code_challenge_method parameters.
400+
func (p *PKCEParams) ChallengeOption() AuthCodeOption {
401+
return set2Values{k1: codeChallengeKey, v1: p.Challenge, k2: codeChallengeMethodKey, v2: p.ChallengeMethod}
402+
}
403+
404+
type set2Values struct{ k1, v1, k2, v2 string }
405+
406+
func (p set2Values) setValue(m url.Values) {
407+
m.Set(p.k1, p.v1)
408+
m.Set(p.k2, p.v2)
409+
}
410+
411+
// VerifierOption should be passed to Config.Exchange. The option returned sets the code_verifier parameters.
412+
func (p *PKCEParams) VerifierOption() AuthCodeOption {
413+
return SetAuthURLParam(codeVerifierKey, p.Verifier)
414+
}
415+
416+
// GeneratePKCEParams generates a code verifier with 32 octets of randomness and a S256 challenge (this follows recommendations in RFC 7636).
417+
//
418+
// A fresh verifier should be generated for each AuthCodeURL call.
419+
func GeneratePKCEParams() *PKCEParams {
420+
// "RECOMMENDED that the output of a suitable random number generator be used to create a 32-octet
421+
// sequence. The octet sequence is then base64url-encoded to produce a 43-octet URL-safe string to use as the code verifier."
422+
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
423+
data := make([]byte, 32)
424+
if _, err := io.ReadFull(rand.Reader, data); err != nil {
425+
panic(err)
426+
}
427+
verifier := base64.URLEncoding.EncodeToString(data)
428+
sha := sha256.Sum256([]byte(verifier))
429+
challenge := base64.URLEncoding.EncodeToString(sha[:])
430+
return &PKCEParams{
431+
Challenge: challenge,
432+
ChallengeMethod: "S256",
433+
Verifier: verifier,
434+
}
435+
}

0 commit comments

Comments
 (0)