1/* 2Copyright 2015 The Perkeep Authors 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17// Package oauthutil contains OAuth 2 related utilities. 18package oauthutil // import "go4.org/oauthutil" 19 20import ( 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. 33const 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. 37var 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. 42type 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 55var errExpiredToken = errors.New("expired token") 56 57// cachedToken returns the token saved in cacheFile. It specifically returns 58// errTokenExpired if the token is expired. 59func 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. 81func (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. 115func 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} 122