blob: 69e54b15c40b3afd17c0928210aeb7aaa837cc06 [file] [log] [blame]
// Copyright 2015 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package internal
import (
"context"
"fmt"
"time"
"golang.org/x/oauth2"
"go.chromium.org/luci/common/gcloud/googleoauth"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/retry/transient"
)
type userAuthTokenProvider struct {
config *oauth2.Config
cacheKey CacheKey
}
// NewUserAuthTokenProvider returns TokenProvider that can perform 3-legged
// OAuth flow involving interaction with a user.
func NewUserAuthTokenProvider(ctx context.Context, clientID, clientSecret string, scopes []string) (TokenProvider, error) {
return &userAuthTokenProvider{
config: &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: "https://accounts.google.com/o/oauth2/auth",
TokenURL: "https://accounts.google.com/o/oauth2/token",
},
RedirectURL: "urn:ietf:wg:oauth:2.0:oob",
Scopes: scopes,
},
cacheKey: CacheKey{
Key: fmt.Sprintf("user/%s", clientID),
Scopes: scopes,
},
}, nil
}
func (p *userAuthTokenProvider) RequiresInteraction() bool {
return true
}
func (p *userAuthTokenProvider) Lightweight() bool {
return false
}
func (p *userAuthTokenProvider) Email() string {
// We don't know the email before user logs in.
return UnknownEmail
}
func (p *userAuthTokenProvider) CacheKey(ctx context.Context) (*CacheKey, error) {
return &p.cacheKey, nil
}
func (p *userAuthTokenProvider) MintToken(ctx context.Context, base *Token) (*Token, error) {
if p.config.ClientID == "" || p.config.ClientSecret == "" {
return nil, fmt.Errorf("OAuth client is not set, can't use 3-legged login flow")
}
// The list of scopes is displayed on the consent page as well, but show it
// in the terminal too, for clarity.
fmt.Println("Getting a refresh token with following OAuth scopes:")
for _, scope := range p.config.Scopes {
fmt.Printf(" * %s\n", scope)
}
fmt.Println()
// Grab the authorization code by redirecting a user to a consent screen.
url := p.config.AuthCodeURL("", oauth2.AccessTypeOffline, oauth2.ApprovalForce)
fmt.Printf("Visit the following URL to get the authorization code and copy-paste it below.\n\n%s\n\n", url)
fmt.Printf("Authorization code: ")
var code string
if _, err := fmt.Scan(&code); err != nil {
return nil, err
}
fmt.Println()
// Exchange it for a token.
tok, err := p.config.Exchange(ctx, code)
if err != nil {
return nil, err
}
// Grab an email associated with the token, if possible. May return NoEmail.
email, err := p.grabEmail(ctx, tok)
if err != nil {
return nil, err
}
return &Token{
Token: *tok,
Email: email,
}, nil
}
func (p *userAuthTokenProvider) RefreshToken(ctx context.Context, prev, base *Token) (*Token, error) {
// Clear expiration time to force token refresh. Do not use 0 since it means
// that token never expires.
t := prev.Token
t.Expiry = time.Unix(1, 0)
switch newTok, err := grabToken(p.config.TokenSource(ctx, &t)); {
case err == nil:
// If we didn't have an email before, grab it now. This is important to
// "upgrade" existing cached tokens to include email.
email := prev.Email
if email == UnknownEmail {
var err error
email, err = p.grabEmail(ctx, newTok)
if err != nil {
return nil, err
}
}
return &Token{
Token: *newTok,
Email: email,
}, nil
case transient.Tag.In(err):
logging.Warningf(ctx, "Transient error when refreshing the token - %s", err)
return nil, err
default:
logging.Warningf(ctx, "Bad refresh token - %s", err)
return nil, ErrBadRefreshToken
}
}
// grabEmail fetches an email associated with the given token.
//
// May return (NoEmail, nil) if the token can't be resolved into an email.
func (p *userAuthTokenProvider) grabEmail(ctx context.Context, tok *oauth2.Token) (string, error) {
info, err := googleoauth.GetTokenInfo(ctx, googleoauth.TokenInfoParams{
AccessToken: tok.AccessToken,
})
if err != nil {
return "", err
}
if info.Email == "" {
return NoEmail, nil
}
return info.Email, nil
}