From 11059998b34553d3f105387e9fb23fceaccb3447 Mon Sep 17 00:00:00 2001 From: Andy Zhao Date: Tue, 9 Mar 2021 14:05:58 -0800 Subject: [PATCH] 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 default_authhandler.go to provide a command line implementation for AuthorizationHandler. --- authhandler/authhandler.go | 51 +++++++++++++++++++ .../default_authhandler.go | 24 +++++---- google/google.go | 35 ------------- 3 files changed, 65 insertions(+), 45 deletions(-) create mode 100644 authhandler/authhandler.go rename google/authhandler.go => authhandler/default_authhandler.go (64%) diff --git a/authhandler/authhandler.go b/authhandler/authhandler.go new file mode 100644 index 0000000..da59975 --- /dev/null +++ b/authhandler/authhandler.go @@ -0,0 +1,51 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package authhandler implements a TokenSource to support +// "three-legged OAuth 2.0" via a custom AuthorizationHandler. +package authhandler + +import ( + "context" + "errors" + + "golang.org/x/oauth2" +) + +// AuthorizationHandler is a 3-legged-OAuth helper that +// prompts the user for OAuth consent at the specified Auth URL +// and returns an auth code and state upon approval. +type AuthorizationHandler func(string) (string, string, error) + +// TokenSource returns an oauth2.TokenSource that fetches access tokens +// using 3-legged-OAuth flow. +// +// The provided oauth2.Config should be a full configuration containing AuthURL, +// TokenURL, and scope. An environment-specific AuthorizationHandler is used to +// obtain user consent. +// +// Per OAuth protocol, a unique "state" string should be sent and verified +// before exchanging auth code for OAuth token to prevent CSRF attacks. +func TokenSource(ctx context.Context, config *oauth2.Config, authHandler AuthorizationHandler, state string) oauth2.TokenSource { + return oauth2.ReuseTokenSource(nil, authHandlerSource{config: config, ctx: ctx, authHandler: authHandler, state: state}) +} + +type authHandlerSource struct { + ctx context.Context + config *oauth2.Config + authHandler AuthorizationHandler + state string +} + +func (source authHandlerSource) Token() (*oauth2.Token, error) { + url := source.config.AuthCodeURL(source.state) + code, state, err := source.authHandler(url) + if err != nil { + return nil, err + } + if state == source.state { + return source.config.Exchange(source.ctx, code) + } + return nil, errors.New("State mismatch in 3-legged-OAuth flow.") +} diff --git a/google/authhandler.go b/authhandler/default_authhandler.go similarity index 64% rename from google/authhandler.go rename to authhandler/default_authhandler.go index 5f78a3e..22cb7f8 100644 --- a/google/authhandler.go +++ b/authhandler/default_authhandler.go @@ -1,28 +1,32 @@ -// Copyright 2020 The Go Authors. All rights reserved. +// Copyright 2021 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package google +package authhandler import ( "fmt" - - "github.com/google/uuid" ) -// RandomAuthorizationState generates a state via UUID generator. -func RandomAuthorizationState() string { - return uuid.New().String() -} - // DefaultAuthorizationHandler returns a command line auth handler // that prints the auth URL on the console and prompts the user to // authorize in the browser and paste the auth code back via stdin. // +// Per OAuth protocol, a unique "state" string should be sent and verified +// before exchanging auth code for OAuth token to prevent CSRF attacks. +// // For convenience, this handler returns a pre-configured state // instead of asking the user to additionally paste the state from // the auth response. In order for this to work, the state -// configured here should match the one in the oauth2 AuthTokenURL. +// configured here must match the state used in authCodeURL. +// +// Usage example: +// +// state := uuid.New().String() +// tokenSource:= authhandler.TokenSource(ctx, conf +// authhandler.DefaultAuthorizationHandler(state), state) +// pubsubService, err := pubsub.NewService(ctx, +// option.WithTokenSource(tokenSource)) func DefaultAuthorizationHandler(state string) AuthorizationHandler { return func(authCodeURL string) (string, string, error) { return defaultAuthorizationHandlerHelper(state, authCodeURL) diff --git a/google/google.go b/google/google.go index ec65bd5..81de32b 100644 --- a/google/google.go +++ b/google/google.go @@ -207,38 +207,3 @@ func (cs computeSource) Token() (*oauth2.Token, error) { "oauth2.google.serviceAccount": acct, }), nil } - -// AuthorizationHandler is a 3-legged-OAuth helper that -// prompts the user for OAuth consent at the specified Auth URL -// and returns an auth code and state upon approval. -type AuthorizationHandler func(string) (string, string, error) - -// OAuthClientTokenSource returns an oauth2.TokenSource that fetches access tokens -// using 3-legged-OAuth workflow. -// The provided oauth2.Config should be a full configuration containing AuthURL, -// TokenURL, and scope. -// An environment-specific AuthorizationHandler is used to obtain user consent. -// Per OAuth protocol, a unique "state" string should be sent and verified -// before token exchange to prevent CSRF attacks. -func OAuthClientTokenSource(ctx context.Context, config *oauth2.Config, authHandler AuthorizationHandler, state string) oauth2.TokenSource { - return oauth2.ReuseTokenSource(nil, oauthClientSource{config: config, ctx: ctx, authHandler: authHandler, state: state}) -} - -type oauthClientSource struct { - ctx context.Context - config *oauth2.Config - authHandler AuthorizationHandler - state string -} - -func (ocs oauthClientSource) Token() (*oauth2.Token, error) { - url := ocs.config.AuthCodeURL(ocs.state) - code, state, err := ocs.authHandler(url) - if err != nil { - return nil, err - } - if state == ocs.state { - return ocs.config.Exchange(ocs.ctx, code) - } - return nil, errors.New("State mismatch in OAuth workflow.") -}