Skip to content

Commit fd043fe

Browse files
andyrzhaocodyoss
authored andcommitted
authhandler: Add support for PKCE
- Added new TokenSourceWithPKCE function to authhandler package. - Updated Token method to support PKCE flow, sending code challenge and challenge method on the auth-code request, and sending code verifier on the exchange request. - Updated google/default.go to support PKCE param. Change-Id: Iab895bc01407c4742706061753f5329a772068ec GitHub-Last-Rev: c1fddd2 GitHub-Pull-Request: golang#568 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/410515 Run-TryBot: Cody Oss <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Shin Fan <[email protected]> Reviewed-by: Cody Oss <[email protected]>
1 parent d0670ef commit fd043fe

File tree

3 files changed

+105
-6
lines changed

3 files changed

+105
-6
lines changed

authhandler/authhandler.go

+41-3
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,36 @@ 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+
}
31+
1632
// AuthorizationHandler is a 3-legged-OAuth helper that prompts
1733
// the user for OAuth consent at the specified auth code URL
1834
// and returns an auth code and state upon approval.
1935
type AuthorizationHandler func(authCodeURL string) (code string, state string, err error)
2036

37+
// TokenSourceWithPKCE is an enhanced version of TokenSource with PKCE support.
38+
//
39+
// The pkce parameter supports PKCE flow, which uses code challenge and code verifier
40+
// to prevent CSRF attacks. A unique code challenge and code verifier should be generated
41+
// by the caller at runtime. See https://www.oauth.com/oauth2-servers/pkce/ for more info.
42+
func TokenSourceWithPKCE(ctx context.Context, config *oauth2.Config, state string, authHandler AuthorizationHandler, pkce *PKCEParams) oauth2.TokenSource {
43+
return oauth2.ReuseTokenSource(nil, authHandlerSource{config: config, ctx: ctx, authHandler: authHandler, state: state, pkce: pkce})
44+
}
45+
2146
// TokenSource returns an oauth2.TokenSource that fetches access tokens
2247
// using 3-legged-OAuth flow.
2348
//
@@ -33,24 +58,37 @@ type AuthorizationHandler func(authCodeURL string) (code string, state string, e
3358
// and response before exchanging the auth code for OAuth token to prevent CSRF
3459
// attacks.
3560
func TokenSource(ctx context.Context, config *oauth2.Config, state string, authHandler AuthorizationHandler) oauth2.TokenSource {
36-
return oauth2.ReuseTokenSource(nil, authHandlerSource{config: config, ctx: ctx, authHandler: authHandler, state: state})
61+
return TokenSourceWithPKCE(ctx, config, state, authHandler, nil)
3762
}
3863

3964
type authHandlerSource struct {
4065
ctx context.Context
4166
config *oauth2.Config
4267
authHandler AuthorizationHandler
4368
state string
69+
pkce *PKCEParams
4470
}
4571

4672
func (source authHandlerSource) Token() (*oauth2.Token, error) {
47-
url := source.config.AuthCodeURL(source.state)
73+
// 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...)
4880
code, state, err := source.authHandler(url)
4981
if err != nil {
5082
return nil, err
5183
}
5284
if state != source.state {
5385
return nil, errors.New("state mismatch in 3-legged-OAuth flow")
5486
}
55-
return source.config.Exchange(source.ctx, code)
87+
88+
// 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...)
5694
}

authhandler/authhandler_test.go

+58
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,61 @@ func TestTokenExchange_StateMismatch(t *testing.T) {
9797
t.Errorf("err = %q; want %q", err, want_err)
9898
}
9999
}
100+
101+
func TestTokenExchangeWithPKCE_Success(t *testing.T) {
102+
authhandler := func(authCodeURL string) (string, string, error) {
103+
if authCodeURL == "testAuthCodeURL?client_id=testClientID&code_challenge=codeChallenge&code_challenge_method=plain&response_type=code&scope=pubsub&state=testState" {
104+
return "testCode", "testState", nil
105+
}
106+
return "", "", fmt.Errorf("invalid authCodeURL: %q", authCodeURL)
107+
}
108+
109+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
110+
r.ParseForm()
111+
if r.Form.Get("code") == "testCode" && r.Form.Get("code_verifier") == "codeChallenge" {
112+
w.Header().Set("Content-Type", "application/json")
113+
w.Write([]byte(`{
114+
"access_token": "90d64460d14870c08c81352a05dedd3465940a7c",
115+
"scope": "pubsub",
116+
"token_type": "bearer",
117+
"expires_in": 3600
118+
}`))
119+
}
120+
}))
121+
defer ts.Close()
122+
123+
conf := &oauth2.Config{
124+
ClientID: "testClientID",
125+
Scopes: []string{"pubsub"},
126+
Endpoint: oauth2.Endpoint{
127+
AuthURL: "testAuthCodeURL",
128+
TokenURL: ts.URL,
129+
},
130+
}
131+
pkce := PKCEParams{
132+
Challenge: "codeChallenge",
133+
ChallengeMethod: "plain",
134+
Verifier: "codeChallenge",
135+
}
136+
137+
tok, err := TokenSourceWithPKCE(context.Background(), conf, "testState", authhandler, &pkce).Token()
138+
if err != nil {
139+
t.Fatal(err)
140+
}
141+
if !tok.Valid() {
142+
t.Errorf("got invalid token: %v", tok)
143+
}
144+
if got, want := tok.AccessToken, "90d64460d14870c08c81352a05dedd3465940a7c"; got != want {
145+
t.Errorf("access token = %q; want %q", got, want)
146+
}
147+
if got, want := tok.TokenType, "bearer"; got != want {
148+
t.Errorf("token type = %q; want %q", got, want)
149+
}
150+
if got := tok.Expiry.IsZero(); got {
151+
t.Errorf("token expiry is zero = %v, want false", got)
152+
}
153+
scope := tok.Extra("scope")
154+
if got, want := scope, "pubsub"; got != want {
155+
t.Errorf("scope = %q; want %q", got, want)
156+
}
157+
}

google/default.go

+6-3
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,14 @@ type CredentialsParams struct {
5454
// Optional.
5555
Subject string
5656

57-
// AuthHandler is the AuthorizationHandler used for 3-legged OAuth flow. Optional.
57+
// AuthHandler is the AuthorizationHandler used for 3-legged OAuth flow. Required for 3LO flow.
5858
AuthHandler authhandler.AuthorizationHandler
5959

60-
// State is a unique string used with AuthHandler. Optional.
60+
// State is a unique string used with AuthHandler. Required for 3LO flow.
6161
State string
62+
63+
// PKCE is used to support PKCE flow. Optional for 3LO flow.
64+
PKCE *authhandler.PKCEParams
6265
}
6366

6467
func (params CredentialsParams) deepCopy() CredentialsParams {
@@ -176,7 +179,7 @@ func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params
176179
if config != nil {
177180
return &Credentials{
178181
ProjectID: "",
179-
TokenSource: authhandler.TokenSource(ctx, config, params.State, params.AuthHandler),
182+
TokenSource: authhandler.TokenSourceWithPKCE(ctx, config, params.State, params.AuthHandler, params.PKCE),
180183
JSON: jsonData,
181184
}, nil
182185
}

0 commit comments

Comments
 (0)