7463a7a509
* get rid of depreprecated "code.google.com/p/goauth2/oauth" * store tokens in config file as before * read old format tokens and write in new format seamlessly * set our own transport to enforce timeouts etc
166 lines
4.5 KiB
Go
166 lines
4.5 KiB
Go
package oauthutil
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/ncw/rclone/fs"
|
|
"golang.org/x/net/context"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
// configKey is the key used to store the token under
|
|
const configKey = "token"
|
|
|
|
// TitleBarRedirectURL is the OAuth2 redirect URL to use when the authorization
|
|
// code should be returned in the title bar of the browser, with the page text
|
|
// prompting the user to copy the code and paste it in the application.
|
|
const TitleBarRedirectURL = "urn:ietf:wg:oauth:2.0:oob"
|
|
|
|
// oldToken contains an end-user's tokens.
|
|
// This is the data you must store to persist authentication.
|
|
//
|
|
// From the original code.google.com/p/goauth2/oauth package - used
|
|
// for backwards compatibility in the rclone config file
|
|
type oldToken struct {
|
|
AccessToken string
|
|
RefreshToken string
|
|
Expiry time.Time
|
|
}
|
|
|
|
// getToken returns the token saved in the config file under
|
|
// section name.
|
|
func getToken(name string) (*oauth2.Token, error) {
|
|
tokenString, err := fs.ConfigFile.GetValue(string(name), configKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if tokenString == "" {
|
|
return nil, fmt.Errorf("Empty token found - please run rclone config again")
|
|
}
|
|
token := new(oauth2.Token)
|
|
err = json.Unmarshal([]byte(tokenString), token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// if has data then return it
|
|
if token.AccessToken != "" && token.RefreshToken != "" {
|
|
return token, nil
|
|
}
|
|
// otherwise try parsing as oldToken
|
|
oldtoken := new(oldToken)
|
|
err = json.Unmarshal([]byte(tokenString), oldtoken)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Fill in result into new token
|
|
token.AccessToken = oldtoken.AccessToken
|
|
token.RefreshToken = oldtoken.RefreshToken
|
|
token.Expiry = oldtoken.Expiry
|
|
// Save new format in config file
|
|
err = putToken(name, token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return token, nil
|
|
}
|
|
|
|
// putToken stores the token in the config file
|
|
//
|
|
// This saves the config file if it changes
|
|
func putToken(name string, token *oauth2.Token) error {
|
|
tokenBytes, err := json.Marshal(token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tokenString := string(tokenBytes)
|
|
old := fs.ConfigFile.MustValue(name, configKey)
|
|
if tokenString != old {
|
|
fs.ConfigFile.SetValue(name, configKey, tokenString)
|
|
fs.SaveConfig()
|
|
fs.Debug(name, "Saving new token in config file")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// tokenSource stores updated tokens in the config file
|
|
type tokenSource struct {
|
|
Name string
|
|
TokenSource oauth2.TokenSource
|
|
OldToken oauth2.Token
|
|
}
|
|
|
|
// Token returns a token or an error.
|
|
// Token must be safe for concurrent use by multiple goroutines.
|
|
// The returned Token must not be modified.
|
|
//
|
|
// This saves the token in the config file if it has changed
|
|
func (ts *tokenSource) Token() (*oauth2.Token, error) {
|
|
token, err := ts.TokenSource.Token()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if *token != ts.OldToken {
|
|
putToken(ts.Name, token)
|
|
}
|
|
return token, nil
|
|
}
|
|
|
|
// Check interface satisfied
|
|
var _ oauth2.TokenSource = (*tokenSource)(nil)
|
|
|
|
// Context returns a context with our HTTP Client baked in for oauth2
|
|
func Context() context.Context {
|
|
return context.WithValue(nil, oauth2.HTTPClient, fs.Config.Client())
|
|
}
|
|
|
|
// NewClient gets a token from the config file and configures a Client
|
|
// with it
|
|
func NewClient(name string, config *oauth2.Config) (*http.Client, error) {
|
|
token, err := getToken(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set our own http client in the context
|
|
ctx := Context()
|
|
|
|
// Wrap the TokenSource in our TokenSource which saves changed
|
|
// tokens in the config file
|
|
ts := &tokenSource{
|
|
Name: name,
|
|
OldToken: *token,
|
|
TokenSource: config.TokenSource(ctx, token),
|
|
}
|
|
return oauth2.NewClient(ctx, ts), nil
|
|
|
|
}
|
|
|
|
// Config does the initial creation of the token
|
|
func Config(name string, config *oauth2.Config) error {
|
|
// See if already have a token
|
|
tokenString := fs.ConfigFile.MustValue(name, "token")
|
|
if tokenString != "" {
|
|
fmt.Printf("Already have a token - refresh?\n")
|
|
if !fs.Confirm() {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Generate a URL for the user to visit for authorization.
|
|
authUrl := config.AuthCodeURL("state")
|
|
fmt.Printf("Go to the following link in your browser\n")
|
|
fmt.Printf("%s\n", authUrl)
|
|
fmt.Printf("Log in, then type paste the token that is returned in the browser here\n")
|
|
|
|
// Read the code, and exchange it for a token.
|
|
fmt.Printf("Enter verification code> ")
|
|
authCode := fs.ReadLine()
|
|
token, err := config.Exchange(oauth2.NoContext, authCode)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to get token: %v", err)
|
|
}
|
|
return putToken(name, token)
|
|
}
|