Skip to content

Commit 22b0ada

Browse files
andyrzhaoshinfan
authored andcommitted
authhandler: Add support for 3-legged-OAuth
Added authhandler.go, which implements a TokenSource to support "three-legged OAuth 2.0" via a custom AuthorizationHandler. Added example_test.go with a sample command line implementation for AuthorizationHandler. This patch adds support for 3-legged-OAuth flow using an OAuth Client ID file downloaded from Google Cloud Console. Change-Id: Iefe54494d6f3ee326a6b1b2a81a7d5d1a7ba3331 GitHub-Last-Rev: 48fc036 GitHub-Pull-Request: #419 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/232238 Reviewed-by: Tyler Bui-Palsulich <tbp@google.com> Reviewed-by: Shin Fan <shinfan@google.com> Reviewed-by: Cody Oss <codyoss@google.com> Trust: Shin Fan <shinfan@google.com> Trust: Cody Oss <codyoss@google.com>
1 parent cd4f82c commit 22b0ada

File tree

3 files changed

+234
-0
lines changed

3 files changed

+234
-0
lines changed

‎authhandler/authhandler.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2021 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+
5+
// Package authhandler implements a TokenSource to support
6+
// "three-legged OAuth 2.0" via a custom AuthorizationHandler.
7+
package authhandler
8+
9+
import (
10+
"context"
11+
"errors"
12+
13+
"golang.org/x/oauth2"
14+
)
15+
16+
// AuthorizationHandler is a 3-legged-OAuth helper that prompts
17+
// the user for OAuth consent at the specified auth code URL
18+
// and returns an auth code and state upon approval.
19+
type AuthorizationHandler func(authCodeURL string) (code string, state string, err error)
20+
21+
// TokenSource returns an oauth2.TokenSource that fetches access tokens
22+
// using 3-legged-OAuth flow.
23+
//
24+
// The provided context.Context is used for oauth2 Exchange operation.
25+
//
26+
// The provided oauth2.Config should be a full configuration containing AuthURL,
27+
// TokenURL, and Scope.
28+
//
29+
// An environment-specific AuthorizationHandler is used to obtain user consent.
30+
//
31+
// Per the OAuth protocol, a unique "state" string should be specified here.
32+
// This token source will verify that the "state" is identical in the request
33+
// and response before exchanging the auth code for OAuth token to prevent CSRF
34+
// attacks.
35+
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})
37+
}
38+
39+
type authHandlerSource struct {
40+
ctx context.Context
41+
config *oauth2.Config
42+
authHandler AuthorizationHandler
43+
state string
44+
}
45+
46+
func (source authHandlerSource) Token() (*oauth2.Token, error) {
47+
url := source.config.AuthCodeURL(source.state)
48+
code, state, err := source.authHandler(url)
49+
if err != nil {
50+
return nil, err
51+
}
52+
if state != source.state {
53+
return nil, errors.New("state mismatch in 3-legged-OAuth flow")
54+
}
55+
return source.config.Exchange(source.ctx, code)
56+
}

‎authhandler/authhandler_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright 2021 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+
5+
package authhandler
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"net/http"
11+
"net/http/httptest"
12+
"testing"
13+
14+
"golang.org/x/oauth2"
15+
)
16+
17+
func TestTokenExchange_Success(t *testing.T) {
18+
authhandler := func(authCodeURL string) (string, string, error) {
19+
if authCodeURL == "testAuthCodeURL?client_id=testClientID&response_type=code&scope=pubsub&state=testState" {
20+
return "testCode", "testState", nil
21+
}
22+
return "", "", fmt.Errorf("invalid authCodeURL: %q", authCodeURL)
23+
}
24+
25+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
26+
r.ParseForm()
27+
if r.Form.Get("code") == "testCode" {
28+
w.Header().Set("Content-Type", "application/json")
29+
w.Write([]byte(`{
30+
"access_token": "90d64460d14870c08c81352a05dedd3465940a7c",
31+
"scope": "pubsub",
32+
"token_type": "bearer",
33+
"expires_in": 3600
34+
}`))
35+
}
36+
}))
37+
defer ts.Close()
38+
39+
conf := &oauth2.Config{
40+
ClientID: "testClientID",
41+
Scopes: []string{"pubsub"},
42+
Endpoint: oauth2.Endpoint{
43+
AuthURL: "testAuthCodeURL",
44+
TokenURL: ts.URL,
45+
},
46+
}
47+
48+
tok, err := TokenSource(context.Background(), conf, "testState", authhandler).Token()
49+
if err != nil {
50+
t.Fatal(err)
51+
}
52+
if !tok.Valid() {
53+
t.Errorf("got invalid token: %v", tok)
54+
}
55+
if got, want := tok.AccessToken, "90d64460d14870c08c81352a05dedd3465940a7c"; got != want {
56+
t.Errorf("access token = %q; want %q", got, want)
57+
}
58+
if got, want := tok.TokenType, "bearer"; got != want {
59+
t.Errorf("token type = %q; want %q", got, want)
60+
}
61+
if got := tok.Expiry.IsZero(); got {
62+
t.Errorf("token expiry is zero = %v, want false", got)
63+
}
64+
scope := tok.Extra("scope")
65+
if got, want := scope, "pubsub"; got != want {
66+
t.Errorf("scope = %q; want %q", got, want)
67+
}
68+
}
69+
70+
func TestTokenExchange_StateMismatch(t *testing.T) {
71+
authhandler := func(authCodeURL string) (string, string, error) {
72+
return "testCode", "testStateMismatch", nil
73+
}
74+
75+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
76+
w.Header().Set("Content-Type", "application/json")
77+
w.Write([]byte(`{
78+
"access_token": "90d64460d14870c08c81352a05dedd3465940a7c",
79+
"scope": "pubsub",
80+
"token_type": "bearer",
81+
"expires_in": 3600
82+
}`))
83+
}))
84+
defer ts.Close()
85+
86+
conf := &oauth2.Config{
87+
ClientID: "testClientID",
88+
Scopes: []string{"pubsub"},
89+
Endpoint: oauth2.Endpoint{
90+
AuthURL: "testAuthCodeURL",
91+
TokenURL: ts.URL,
92+
},
93+
}
94+
95+
_, err := TokenSource(context.Background(), conf, "testState", authhandler).Token()
96+
if want_err := "state mismatch in 3-legged-OAuth flow"; err == nil || err.Error() != want_err {
97+
t.Errorf("err = %q; want %q", err, want_err)
98+
}
99+
}

‎authhandler/example_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2021 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+
5+
package authhandler_test
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"net/http"
11+
"net/http/httptest"
12+
13+
"golang.org/x/oauth2"
14+
"golang.org/x/oauth2/authhandler"
15+
)
16+
17+
// CmdAuthorizationHandler returns a command line auth handler that prints
18+
// the auth URL to the console and prompts the user to authorize in the
19+
// browser and paste the auth code back via stdin.
20+
//
21+
// Per the OAuth protocol, a unique "state" string should be specified here.
22+
// The authhandler token source will verify that the "state" is identical in
23+
// the request and response before exchanging the auth code for OAuth token to
24+
// prevent CSRF attacks.
25+
//
26+
// For convenience, this handler returns a pre-configured state instead of
27+
// asking the user to additionally paste the state from the auth response.
28+
// In order for this to work, the state configured here must match the state
29+
// used in authCodeURL.
30+
func CmdAuthorizationHandler(state string) authhandler.AuthorizationHandler {
31+
return func(authCodeURL string) (string, string, error) {
32+
fmt.Printf("Go to the following link in your browser:\n\n %s\n\n", authCodeURL)
33+
fmt.Println("Enter authorization code:")
34+
var code string
35+
fmt.Scanln(&code)
36+
return code, state, nil
37+
}
38+
}
39+
40+
func Example() {
41+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
42+
r.ParseForm()
43+
w.Header().Set("Content-Type", "application/json")
44+
w.Write([]byte(`{
45+
"access_token": "90d64460d14870c08c81352a05dedd3465940a7c",
46+
"scope": "pubsub",
47+
"token_type": "bearer",
48+
"expires_in": 3600
49+
}`))
50+
}))
51+
defer ts.Close()
52+
53+
ctx := context.Background()
54+
conf := &oauth2.Config{
55+
ClientID: "testClientID",
56+
Scopes: []string{"pubsub"},
57+
Endpoint: oauth2.Endpoint{
58+
AuthURL: "testAuthCodeURL",
59+
TokenURL: ts.URL,
60+
},
61+
}
62+
state := "unique_state"
63+
64+
token, err := authhandler.TokenSource(ctx, conf, state, CmdAuthorizationHandler(state)).Token()
65+
66+
if err != nil {
67+
fmt.Println(err)
68+
}
69+
70+
fmt.Printf("AccessToken: %s", token.AccessToken)
71+
72+
// Output:
73+
// Go to the following link in your browser:
74+
//
75+
// testAuthCodeURL?client_id=testClientID&response_type=code&scope=pubsub&state=unique_state
76+
//
77+
// Enter authorization code:
78+
// AccessToken: 90d64460d14870c08c81352a05dedd3465940a7c
79+
}

0 commit comments

Comments
 (0)