github.com/camlistore/go4@v0.0.0-20200104003542-c7e774b10ea0/oauthutil/oauth.go (about)

     1  /*
     2  Copyright 2015 The Perkeep Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8       http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package oauthutil contains OAuth 2 related utilities.
    18  package oauthutil // import "go4.org/oauthutil"
    19  
    20  import (
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"time"
    25  
    26  	"go4.org/wkfs"
    27  	"golang.org/x/oauth2"
    28  )
    29  
    30  // TitleBarRedirectURL is the OAuth2 redirect URL to use when the authorization
    31  // code should be returned in the title bar of the browser, with the page text
    32  // prompting the user to copy the code and paste it in the application.
    33  const TitleBarRedirectURL = "urn:ietf:wg:oauth:2.0:oob"
    34  
    35  // ErrNoAuthCode is returned when Token() has not found any valid cached token
    36  // and TokenSource does not have an AuthCode for getting a new token.
    37  var ErrNoAuthCode = errors.New("oauthutil: unspecified TokenSource.AuthCode")
    38  
    39  // TokenSource is an implementation of oauth2.TokenSource. It uses CacheFile to store and
    40  // reuse the the acquired token, and AuthCode to provide the authorization code that will be
    41  // exchanged for a token otherwise.
    42  type TokenSource struct {
    43  	Config *oauth2.Config
    44  
    45  	// CacheFile is where the token will be stored JSON-encoded. Any call to Token
    46  	// first tries to read a valid token from CacheFile.
    47  	CacheFile string
    48  
    49  	// AuthCode provides the authorization code that Token will exchange for a token.
    50  	// It usually is a way to prompt the user for the code. If CacheFile does not provide
    51  	// a token and AuthCode is nil, Token returns ErrNoAuthCode.
    52  	AuthCode func() string
    53  }
    54  
    55  var errExpiredToken = errors.New("expired token")
    56  
    57  // cachedToken returns the token saved in cacheFile. It specifically returns
    58  // errTokenExpired if the token is expired.
    59  func cachedToken(cacheFile string) (*oauth2.Token, error) {
    60  	tok := new(oauth2.Token)
    61  	tokenData, err := wkfs.ReadFile(cacheFile)
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  	if err = json.Unmarshal(tokenData, tok); err != nil {
    66  		return nil, err
    67  	}
    68  	if !tok.Valid() {
    69  		if tok != nil && time.Now().After(tok.Expiry) {
    70  			return nil, errExpiredToken
    71  		}
    72  		return nil, errors.New("invalid token")
    73  	}
    74  	return tok, nil
    75  }
    76  
    77  // Token first tries to find a valid token in CacheFile, and otherwise uses
    78  // Config and AuthCode to fetch a new token. This new token is saved in CacheFile
    79  // (if not blank). If CacheFile did not provide a token and AuthCode is nil,
    80  // ErrNoAuthCode is returned.
    81  func (src TokenSource) Token() (*oauth2.Token, error) {
    82  	var tok *oauth2.Token
    83  	var err error
    84  	if src.CacheFile != "" {
    85  		tok, err = cachedToken(src.CacheFile)
    86  		if err == nil {
    87  			return tok, nil
    88  		}
    89  		if err != errExpiredToken {
    90  			fmt.Printf("Error getting token from %s: %v\n", src.CacheFile, err)
    91  		}
    92  	}
    93  	if src.AuthCode == nil {
    94  		return nil, ErrNoAuthCode
    95  	}
    96  	tok, err = src.Config.Exchange(oauth2.NoContext, src.AuthCode())
    97  	if err != nil {
    98  		return nil, fmt.Errorf("could not exchange auth code for a token: %v", err)
    99  	}
   100  	if src.CacheFile == "" {
   101  		return tok, nil
   102  	}
   103  	tokenData, err := json.Marshal(&tok)
   104  	if err != nil {
   105  		return nil, fmt.Errorf("could not encode token as json: %v", err)
   106  	}
   107  	if err := wkfs.WriteFile(src.CacheFile, tokenData, 0600); err != nil {
   108  		return nil, fmt.Errorf("could not cache token in %v: %v", src.CacheFile, err)
   109  	}
   110  	return tok, nil
   111  }
   112  
   113  // NewRefreshTokenSource returns a token source that obtains its initial token
   114  // based on the provided config and the refresh token.
   115  func NewRefreshTokenSource(config *oauth2.Config, refreshToken string) oauth2.TokenSource {
   116  	var noInitialToken *oauth2.Token = nil
   117  	return oauth2.ReuseTokenSource(noInitialToken, config.TokenSource(
   118  		oauth2.NoContext, // TODO: maybe accept a context later.
   119  		&oauth2.Token{RefreshToken: refreshToken},
   120  	))
   121  }