1
0
mirror of https://github.com/rclone/rclone.git synced 2025-12-06 00:03:32 +00:00

Compare commits

...

3 Commits

Author SHA1 Message Date
Martin Hassack
c1f085d2a5 onedrive: add support for OAuth client credential flow - fixes #6197
This adds support for the client credential flow oauth method which
requires some special handling in onedrive:

- Special scopes are required
- The tenant is required
- The tenant needs to be used in the oauth URLs

This also:

- refactors the oauth config creation so it isn't duplicated
- defaults the drive_id to the previous one in the config
- updates the documentation

Co-authored-by: Nick Craig-Wood <nick@craig-wood.com>
2024-12-07 17:20:43 +00:00
Martin Hassack
37d85d2576 lib/oauthutil: add support for OAuth client credential flow
This commit reorganises the oauth code to use our own config struct
which has all the info for the normal oauth method and also the client
credentials flow method.

It updates all backends which use lib/oauthutil to use the new config
struct which shouldn't change any functionality.

It also adds code for dealing with the client credential flow config
which doesn't require the use of a browser and doesn't have or need a
refresh token.

Co-authored-by: Nick Craig-Wood <nick@craig-wood.com>
2024-12-07 17:06:22 +00:00
Nick Craig-Wood
e612310296 lib/oauthutil: return error messages from the oauth process better 2024-12-06 12:31:02 +00:00
19 changed files with 390 additions and 197 deletions

View File

@@ -46,7 +46,6 @@ import (
"github.com/rclone/rclone/lib/random" "github.com/rclone/rclone/lib/random"
"github.com/rclone/rclone/lib/rest" "github.com/rclone/rclone/lib/rest"
"github.com/youmark/pkcs8" "github.com/youmark/pkcs8"
"golang.org/x/oauth2"
) )
const ( const (
@@ -65,12 +64,10 @@ const (
// Globals // Globals
var ( var (
// Description of how to auth for this app // Description of how to auth for this app
oauthConfig = &oauth2.Config{ oauthConfig = &oauthutil.Config{
Scopes: nil, Scopes: nil,
Endpoint: oauth2.Endpoint{ AuthURL: "https://app.box.com/api/oauth2/authorize",
AuthURL: "https://app.box.com/api/oauth2/authorize", TokenURL: "https://app.box.com/api/oauth2/token",
TokenURL: "https://app.box.com/api/oauth2/token",
},
ClientID: rcloneClientID, ClientID: rcloneClientID,
ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret), ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
RedirectURL: oauthutil.RedirectURL, RedirectURL: oauthutil.RedirectURL,

View File

@@ -80,9 +80,10 @@ const (
// Globals // Globals
var ( var (
// Description of how to auth for this app // Description of how to auth for this app
driveConfig = &oauth2.Config{ driveConfig = &oauthutil.Config{
Scopes: []string{scopePrefix + "drive"}, Scopes: []string{scopePrefix + "drive"},
Endpoint: google.Endpoint, AuthURL: google.Endpoint.AuthURL,
TokenURL: google.Endpoint.TokenURL,
ClientID: rcloneClientID, ClientID: rcloneClientID,
ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret), ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
RedirectURL: oauthutil.RedirectURL, RedirectURL: oauthutil.RedirectURL,

View File

@@ -94,7 +94,7 @@ const (
var ( var (
// Description of how to auth for this app // Description of how to auth for this app
dropboxConfig = &oauth2.Config{ dropboxConfig = &oauthutil.Config{
Scopes: []string{ Scopes: []string{
"files.metadata.write", "files.metadata.write",
"files.content.write", "files.content.write",
@@ -109,7 +109,8 @@ var (
// AuthURL: "https://www.dropbox.com/1/oauth2/authorize", // AuthURL: "https://www.dropbox.com/1/oauth2/authorize",
// TokenURL: "https://api.dropboxapi.com/1/oauth2/token", // TokenURL: "https://api.dropboxapi.com/1/oauth2/token",
// }, // },
Endpoint: dropbox.OAuthEndpoint(""), AuthURL: dropbox.OAuthEndpoint("").AuthURL,
TokenURL: dropbox.OAuthEndpoint("").TokenURL,
ClientID: rcloneClientID, ClientID: rcloneClientID,
ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret), ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
RedirectURL: oauthutil.RedirectLocalhostURL, RedirectURL: oauthutil.RedirectLocalhostURL,
@@ -134,7 +135,7 @@ var (
) )
// Gets an oauth config with the right scopes // Gets an oauth config with the right scopes
func getOauthConfig(m configmap.Mapper) *oauth2.Config { func getOauthConfig(m configmap.Mapper) *oauthutil.Config {
// If not impersonating, use standard scopes // If not impersonating, use standard scopes
if impersonate, _ := m.Get("impersonate"); impersonate == "" { if impersonate, _ := m.Get("impersonate"); impersonate == "" {
return dropboxConfig return dropboxConfig

View File

@@ -60,14 +60,17 @@ const (
minSleep = 10 * time.Millisecond minSleep = 10 * time.Millisecond
) )
// Description of how to auth for this app var (
var storageConfig = &oauth2.Config{ // Description of how to auth for this app
Scopes: []string{storage.DevstorageReadWriteScope}, storageConfig = &oauthutil.Config{
Endpoint: google.Endpoint, Scopes: []string{storage.DevstorageReadWriteScope},
ClientID: rcloneClientID, AuthURL: google.Endpoint.AuthURL,
ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret), TokenURL: google.Endpoint.TokenURL,
RedirectURL: oauthutil.RedirectURL, ClientID: rcloneClientID,
} ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
RedirectURL: oauthutil.RedirectURL,
}
)
// Register with Fs // Register with Fs
func init() { func init() {

View File

@@ -33,7 +33,6 @@ import (
"github.com/rclone/rclone/lib/oauthutil" "github.com/rclone/rclone/lib/oauthutil"
"github.com/rclone/rclone/lib/pacer" "github.com/rclone/rclone/lib/pacer"
"github.com/rclone/rclone/lib/rest" "github.com/rclone/rclone/lib/rest"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google" "golang.org/x/oauth2/google"
) )
@@ -60,13 +59,14 @@ const (
var ( var (
// Description of how to auth for this app // Description of how to auth for this app
oauthConfig = &oauth2.Config{ oauthConfig = &oauthutil.Config{
Scopes: []string{ Scopes: []string{
"openid", "openid",
"profile", "profile",
scopeReadWrite, // this must be at position scopeAccess scopeReadWrite, // this must be at position scopeAccess
}, },
Endpoint: google.Endpoint, AuthURL: google.Endpoint.AuthURL,
TokenURL: google.Endpoint.TokenURL,
ClientID: rcloneClientID, ClientID: rcloneClientID,
ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret), ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
RedirectURL: oauthutil.RedirectURL, RedirectURL: oauthutil.RedirectURL,

View File

@@ -31,7 +31,6 @@ import (
"github.com/rclone/rclone/lib/oauthutil" "github.com/rclone/rclone/lib/oauthutil"
"github.com/rclone/rclone/lib/pacer" "github.com/rclone/rclone/lib/pacer"
"github.com/rclone/rclone/lib/rest" "github.com/rclone/rclone/lib/rest"
"golang.org/x/oauth2"
) )
const ( const (
@@ -48,11 +47,9 @@ const (
// Globals // Globals
var ( var (
// Description of how to auth for this app. // Description of how to auth for this app.
oauthConfig = &oauth2.Config{ oauthConfig = &oauthutil.Config{
Endpoint: oauth2.Endpoint{ AuthURL: "https://my.hidrive.com/client/authorize",
AuthURL: "https://my.hidrive.com/client/authorize", TokenURL: "https://my.hidrive.com/oauth2/token",
TokenURL: "https://my.hidrive.com/oauth2/token",
},
ClientID: rcloneClientID, ClientID: rcloneClientID,
ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret), ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
RedirectURL: oauthutil.TitleBarRedirectURL, RedirectURL: oauthutil.TitleBarRedirectURL,

View File

@@ -277,11 +277,9 @@ machines.`)
m.Set(configClientID, teliaseCloudClientID) m.Set(configClientID, teliaseCloudClientID)
m.Set(configTokenURL, teliaseCloudTokenURL) m.Set(configTokenURL, teliaseCloudTokenURL)
return oauthutil.ConfigOut("choose_device", &oauthutil.Options{ return oauthutil.ConfigOut("choose_device", &oauthutil.Options{
OAuth2Config: &oauth2.Config{ OAuth2Config: &oauthutil.Config{
Endpoint: oauth2.Endpoint{ AuthURL: teliaseCloudAuthURL,
AuthURL: teliaseCloudAuthURL, TokenURL: teliaseCloudTokenURL,
TokenURL: teliaseCloudTokenURL,
},
ClientID: teliaseCloudClientID, ClientID: teliaseCloudClientID,
Scopes: []string{"openid", "jotta-default", "offline_access"}, Scopes: []string{"openid", "jotta-default", "offline_access"},
RedirectURL: oauthutil.RedirectLocalhostURL, RedirectURL: oauthutil.RedirectLocalhostURL,
@@ -292,11 +290,9 @@ machines.`)
m.Set(configClientID, telianoCloudClientID) m.Set(configClientID, telianoCloudClientID)
m.Set(configTokenURL, telianoCloudTokenURL) m.Set(configTokenURL, telianoCloudTokenURL)
return oauthutil.ConfigOut("choose_device", &oauthutil.Options{ return oauthutil.ConfigOut("choose_device", &oauthutil.Options{
OAuth2Config: &oauth2.Config{ OAuth2Config: &oauthutil.Config{
Endpoint: oauth2.Endpoint{ AuthURL: telianoCloudAuthURL,
AuthURL: telianoCloudAuthURL, TokenURL: telianoCloudTokenURL,
TokenURL: telianoCloudTokenURL,
},
ClientID: telianoCloudClientID, ClientID: telianoCloudClientID,
Scopes: []string{"openid", "jotta-default", "offline_access"}, Scopes: []string{"openid", "jotta-default", "offline_access"},
RedirectURL: oauthutil.RedirectLocalhostURL, RedirectURL: oauthutil.RedirectLocalhostURL,
@@ -307,11 +303,9 @@ machines.`)
m.Set(configClientID, tele2CloudClientID) m.Set(configClientID, tele2CloudClientID)
m.Set(configTokenURL, tele2CloudTokenURL) m.Set(configTokenURL, tele2CloudTokenURL)
return oauthutil.ConfigOut("choose_device", &oauthutil.Options{ return oauthutil.ConfigOut("choose_device", &oauthutil.Options{
OAuth2Config: &oauth2.Config{ OAuth2Config: &oauthutil.Config{
Endpoint: oauth2.Endpoint{ AuthURL: tele2CloudAuthURL,
AuthURL: tele2CloudAuthURL, TokenURL: tele2CloudTokenURL,
TokenURL: tele2CloudTokenURL,
},
ClientID: tele2CloudClientID, ClientID: tele2CloudClientID,
Scopes: []string{"openid", "jotta-default", "offline_access"}, Scopes: []string{"openid", "jotta-default", "offline_access"},
RedirectURL: oauthutil.RedirectLocalhostURL, RedirectURL: oauthutil.RedirectLocalhostURL,
@@ -322,11 +316,9 @@ machines.`)
m.Set(configClientID, onlimeCloudClientID) m.Set(configClientID, onlimeCloudClientID)
m.Set(configTokenURL, onlimeCloudTokenURL) m.Set(configTokenURL, onlimeCloudTokenURL)
return oauthutil.ConfigOut("choose_device", &oauthutil.Options{ return oauthutil.ConfigOut("choose_device", &oauthutil.Options{
OAuth2Config: &oauth2.Config{ OAuth2Config: &oauthutil.Config{
Endpoint: oauth2.Endpoint{ AuthURL: onlimeCloudAuthURL,
AuthURL: onlimeCloudAuthURL, TokenURL: onlimeCloudTokenURL,
TokenURL: onlimeCloudTokenURL,
},
ClientID: onlimeCloudClientID, ClientID: onlimeCloudClientID,
Scopes: []string{"openid", "jotta-default", "offline_access"}, Scopes: []string{"openid", "jotta-default", "offline_access"},
RedirectURL: oauthutil.RedirectLocalhostURL, RedirectURL: oauthutil.RedirectLocalhostURL,
@@ -924,19 +916,17 @@ func getOAuthClient(ctx context.Context, name string, m configmap.Mapper) (oAuth
} }
baseClient := fshttp.NewClient(ctx) baseClient := fshttp.NewClient(ctx)
oauthConfig := &oauth2.Config{ oauthConfig := &oauthutil.Config{
Endpoint: oauth2.Endpoint{ AuthURL: defaultTokenURL,
AuthURL: defaultTokenURL, TokenURL: defaultTokenURL,
TokenURL: defaultTokenURL,
},
} }
if ver == configVersion { if ver == configVersion {
oauthConfig.ClientID = defaultClientID oauthConfig.ClientID = defaultClientID
// if custom endpoints are set use them else stick with defaults // if custom endpoints are set use them else stick with defaults
if tokenURL, ok := m.Get(configTokenURL); ok { if tokenURL, ok := m.Get(configTokenURL); ok {
oauthConfig.Endpoint.TokenURL = tokenURL oauthConfig.TokenURL = tokenURL
// jottacloud is weird. we need to use the tokenURL as authURL // jottacloud is weird. we need to use the tokenURL as authURL
oauthConfig.Endpoint.AuthURL = tokenURL oauthConfig.AuthURL = tokenURL
} }
} else if ver == legacyConfigVersion { } else if ver == legacyConfigVersion {
clientID, ok := m.Get(configClientID) clientID, ok := m.Get(configClientID)
@@ -950,8 +940,8 @@ func getOAuthClient(ctx context.Context, name string, m configmap.Mapper) (oAuth
oauthConfig.ClientID = clientID oauthConfig.ClientID = clientID
oauthConfig.ClientSecret = obscure.MustReveal(clientSecret) oauthConfig.ClientSecret = obscure.MustReveal(clientSecret)
oauthConfig.Endpoint.TokenURL = legacyTokenURL oauthConfig.TokenURL = legacyTokenURL
oauthConfig.Endpoint.AuthURL = legacyTokenURL oauthConfig.AuthURL = legacyTokenURL
// add the request filter to fix token refresh // add the request filter to fix token refresh
if do, ok := baseClient.Transport.(interface { if do, ok := baseClient.Transport.(interface {

View File

@@ -68,14 +68,12 @@ var (
) )
// Description of how to authorize // Description of how to authorize
var oauthConfig = &oauth2.Config{ var oauthConfig = &oauthutil.Config{
ClientID: api.OAuthClientID, ClientID: api.OAuthClientID,
ClientSecret: "", ClientSecret: "",
Endpoint: oauth2.Endpoint{ AuthURL: api.OAuthURL,
AuthURL: api.OAuthURL, TokenURL: api.OAuthURL,
TokenURL: api.OAuthURL, AuthStyle: oauth2.AuthStyleInParams,
AuthStyle: oauth2.AuthStyleInParams,
},
} }
// Register with Fs // Register with Fs
@@ -438,7 +436,9 @@ func (f *Fs) authorize(ctx context.Context, force bool) (err error) {
if err != nil || !tokenIsValid(t) { if err != nil || !tokenIsValid(t) {
fs.Infof(f, "Valid token not found, authorizing.") fs.Infof(f, "Valid token not found, authorizing.")
ctx := oauthutil.Context(ctx, f.cli) ctx := oauthutil.Context(ctx, f.cli)
t, err = oauthConfig.PasswordCredentialsToken(ctx, f.opt.Username, f.opt.Password)
oauth2Conf := oauthConfig.MakeOauth2Config()
t, err = oauth2Conf.PasswordCredentialsToken(ctx, f.opt.Username, f.opt.Password)
} }
if err == nil && !tokenIsValid(t) { if err == nil && !tokenIsValid(t) {
err = errors.New("invalid token") err = errors.New("invalid token")

View File

@@ -40,7 +40,6 @@ import (
"github.com/rclone/rclone/lib/pacer" "github.com/rclone/rclone/lib/pacer"
"github.com/rclone/rclone/lib/readers" "github.com/rclone/rclone/lib/readers"
"github.com/rclone/rclone/lib/rest" "github.com/rclone/rclone/lib/rest"
"golang.org/x/oauth2"
) )
const ( const (
@@ -65,14 +64,21 @@ const (
// Globals // Globals
var ( var (
authPath = "/common/oauth2/v2.0/authorize"
tokenPath = "/common/oauth2/v2.0/token" // Define the paths used for token operations
commonPathPrefix = "/common" // prefix for the paths if tenant isn't known
authPath = "/oauth2/v2.0/authorize"
tokenPath = "/oauth2/v2.0/token"
scopeAccess = fs.SpaceSepList{"Files.Read", "Files.ReadWrite", "Files.Read.All", "Files.ReadWrite.All", "Sites.Read.All", "offline_access"} scopeAccess = fs.SpaceSepList{"Files.Read", "Files.ReadWrite", "Files.Read.All", "Files.ReadWrite.All", "Sites.Read.All", "offline_access"}
scopeAccessWithoutSites = fs.SpaceSepList{"Files.Read", "Files.ReadWrite", "Files.Read.All", "Files.ReadWrite.All", "offline_access"} scopeAccessWithoutSites = fs.SpaceSepList{"Files.Read", "Files.ReadWrite", "Files.Read.All", "Files.ReadWrite.All", "offline_access"}
// Description of how to auth for this app for a business account // When using client credential OAuth flow, scope of .default is required in order
oauthConfig = &oauth2.Config{ // to use the permissions configured for the application within the tenant
scopeAccessClientCred = fs.SpaceSepList{".default"}
// Base config for how to auth
oauthConfig = &oauthutil.Config{
Scopes: scopeAccess, Scopes: scopeAccess,
ClientID: rcloneClientID, ClientID: rcloneClientID,
ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret), ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
@@ -183,6 +189,14 @@ Choose or manually enter a custom space separated list with all scopes, that rcl
Help: "Read and write access to all resources, without the ability to browse SharePoint sites. \nSame as if disable_site_permission was set to true", Help: "Read and write access to all resources, without the ability to browse SharePoint sites. \nSame as if disable_site_permission was set to true",
}, },
}, },
}, {
Name: "tenant",
Help: `ID of the service principal's tenant. Also called its directory ID.
Set this if using
- Client Credential flow
`,
Sensitive: true,
}, { }, {
Name: "disable_site_permission", Name: "disable_site_permission",
Help: `Disable the request for Sites.Read.All permission. Help: `Disable the request for Sites.Read.All permission.
@@ -527,28 +541,54 @@ func chooseDrive(ctx context.Context, name string, m configmap.Mapper, srv *rest
}) })
} }
// Config the backend // Make the oauth config for the backend
func Config(ctx context.Context, name string, m configmap.Mapper, config fs.ConfigIn) (*fs.ConfigOut, error) { func makeOauthConfig(ctx context.Context, opt *Options) (*oauthutil.Config, error) {
region, graphURL := getRegionURL(m) // Copy the default oauthConfig
oauthConfig := *oauthConfig
if config.State == "" { // Set the scopes
var accessScopes fs.SpaceSepList oauthConfig.Scopes = opt.AccessScopes
accessScopesString, _ := m.Get("access_scopes") if opt.DisableSitePermission {
err := accessScopes.Set(accessScopesString) oauthConfig.Scopes = scopeAccessWithoutSites
}
// Construct the auth URLs
prefix := commonPathPrefix
if opt.Tenant != "" {
prefix = "/" + opt.Tenant
}
oauthConfig.TokenURL = authEndpoint[opt.Region] + prefix + tokenPath
oauthConfig.AuthURL = authEndpoint[opt.Region] + prefix + authPath
// Check to see if we are using client credentials flow
if opt.ClientCredentials {
// Override scope to .default
oauthConfig.Scopes = scopeAccessClientCred
if opt.Tenant == "" {
return nil, fmt.Errorf("tenant parameter must be set when using %s", config.ConfigClientCredentials)
}
}
return &oauthConfig, nil
}
// Config the backend
func Config(ctx context.Context, name string, m configmap.Mapper, conf fs.ConfigIn) (*fs.ConfigOut, error) {
opt := new(Options)
err := configstruct.Set(m, opt)
if err != nil {
return nil, err
}
_, graphURL := getRegionURL(m)
// Check to see if this is the start of the state machine execution
if conf.State == "" {
conf, err := makeOauthConfig(ctx, opt)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse access_scopes: %w", err) return nil, err
}
oauthConfig.Scopes = []string(accessScopes)
disableSitePermission, _ := m.Get("disable_site_permission")
if disableSitePermission == "true" {
oauthConfig.Scopes = scopeAccessWithoutSites
}
oauthConfig.Endpoint = oauth2.Endpoint{
AuthURL: authEndpoint[region] + authPath,
TokenURL: authEndpoint[region] + tokenPath,
} }
return oauthutil.ConfigOut("choose_type", &oauthutil.Options{ return oauthutil.ConfigOut("choose_type", &oauthutil.Options{
OAuth2Config: oauthConfig, OAuth2Config: conf,
}) })
} }
@@ -556,9 +596,11 @@ func Config(ctx context.Context, name string, m configmap.Mapper, config fs.Conf
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to configure OneDrive: %w", err) return nil, fmt.Errorf("failed to configure OneDrive: %w", err)
} }
// Create a REST client, build on the OAuth client created above
srv := rest.NewClient(oAuthClient) srv := rest.NewClient(oAuthClient)
switch config.State { switch conf.State {
case "choose_type": case "choose_type":
return fs.ConfigChooseExclusiveFixed("choose_type_done", "config_type", "Type of connection", []fs.OptionExample{{ return fs.ConfigChooseExclusiveFixed("choose_type_done", "config_type", "Type of connection", []fs.OptionExample{{
Value: "onedrive", Value: "onedrive",
@@ -584,7 +626,7 @@ func Config(ctx context.Context, name string, m configmap.Mapper, config fs.Conf
}}) }})
case "choose_type_done": case "choose_type_done":
// Jump to next state according to config chosen // Jump to next state according to config chosen
return fs.ConfigGoto(config.Result) return fs.ConfigGoto(conf.Result)
case "onedrive": case "onedrive":
return chooseDrive(ctx, name, m, srv, chooseDriveOpt{ return chooseDrive(ctx, name, m, srv, chooseDriveOpt{
opts: rest.Opts{ opts: rest.Opts{
@@ -602,16 +644,22 @@ func Config(ctx context.Context, name string, m configmap.Mapper, config fs.Conf
}, },
}) })
case "driveid": case "driveid":
return fs.ConfigInput("driveid_end", "config_driveid_fixed", "Drive ID") out, err := fs.ConfigInput("driveid_end", "config_driveid_fixed", "Drive ID")
if err != nil {
return out, err
}
// Default the drive_id to the previous version in the config
out.Option.Default, _ = m.Get("drive_id")
return out, nil
case "driveid_end": case "driveid_end":
return chooseDrive(ctx, name, m, srv, chooseDriveOpt{ return chooseDrive(ctx, name, m, srv, chooseDriveOpt{
finalDriveID: config.Result, finalDriveID: conf.Result,
}) })
case "siteid": case "siteid":
return fs.ConfigInput("siteid_end", "config_siteid", "Site ID") return fs.ConfigInput("siteid_end", "config_siteid", "Site ID")
case "siteid_end": case "siteid_end":
return chooseDrive(ctx, name, m, srv, chooseDriveOpt{ return chooseDrive(ctx, name, m, srv, chooseDriveOpt{
siteID: config.Result, siteID: conf.Result,
}) })
case "url": case "url":
return fs.ConfigInput("url_end", "config_site_url", `Site URL return fs.ConfigInput("url_end", "config_site_url", `Site URL
@@ -622,7 +670,7 @@ Examples:
- "https://XXX.sharepoint.com/teams/ID" - "https://XXX.sharepoint.com/teams/ID"
`) `)
case "url_end": case "url_end":
siteURL := config.Result siteURL := conf.Result
re := regexp.MustCompile(`https://.*\.sharepoint\.com(/.*)`) re := regexp.MustCompile(`https://.*\.sharepoint\.com(/.*)`)
match := re.FindStringSubmatch(siteURL) match := re.FindStringSubmatch(siteURL)
if len(match) == 2 { if len(match) == 2 {
@@ -637,12 +685,12 @@ Examples:
return fs.ConfigInput("path_end", "config_sharepoint_url", `Server-relative URL`) return fs.ConfigInput("path_end", "config_sharepoint_url", `Server-relative URL`)
case "path_end": case "path_end":
return chooseDrive(ctx, name, m, srv, chooseDriveOpt{ return chooseDrive(ctx, name, m, srv, chooseDriveOpt{
relativePath: config.Result, relativePath: conf.Result,
}) })
case "search": case "search":
return fs.ConfigInput("search_end", "config_search_term", `Search term`) return fs.ConfigInput("search_end", "config_search_term", `Search term`)
case "search_end": case "search_end":
searchTerm := config.Result searchTerm := conf.Result
opts := rest.Opts{ opts := rest.Opts{
Method: "GET", Method: "GET",
RootURL: graphURL, RootURL: graphURL,
@@ -664,10 +712,10 @@ Examples:
}) })
case "search_sites": case "search_sites":
return chooseDrive(ctx, name, m, srv, chooseDriveOpt{ return chooseDrive(ctx, name, m, srv, chooseDriveOpt{
siteID: config.Result, siteID: conf.Result,
}) })
case "driveid_final": case "driveid_final":
finalDriveID := config.Result finalDriveID := conf.Result
// Test the driveID and get drive type // Test the driveID and get drive type
opts := rest.Opts{ opts := rest.Opts{
@@ -686,12 +734,12 @@ Examples:
return fs.ConfigConfirm("driveid_final_end", true, "config_drive_ok", fmt.Sprintf("Drive OK?\n\nFound drive %q of type %q\nURL: %s\n", rootItem.Name, rootItem.ParentReference.DriveType, rootItem.WebURL)) return fs.ConfigConfirm("driveid_final_end", true, "config_drive_ok", fmt.Sprintf("Drive OK?\n\nFound drive %q of type %q\nURL: %s\n", rootItem.Name, rootItem.ParentReference.DriveType, rootItem.WebURL))
case "driveid_final_end": case "driveid_final_end":
if config.Result == "true" { if conf.Result == "true" {
return nil, nil return nil, nil
} }
return fs.ConfigGoto("choose_type") return fs.ConfigGoto("choose_type")
} }
return nil, fmt.Errorf("unknown state %q", config.State) return nil, fmt.Errorf("unknown state %q", conf.State)
} }
// Options defines the configuration for this backend // Options defines the configuration for this backend
@@ -702,7 +750,9 @@ type Options struct {
DriveType string `config:"drive_type"` DriveType string `config:"drive_type"`
RootFolderID string `config:"root_folder_id"` RootFolderID string `config:"root_folder_id"`
DisableSitePermission bool `config:"disable_site_permission"` DisableSitePermission bool `config:"disable_site_permission"`
ClientCredentials bool `config:"client_credentials"`
AccessScopes fs.SpaceSepList `config:"access_scopes"` AccessScopes fs.SpaceSepList `config:"access_scopes"`
Tenant string `config:"tenant"`
ExposeOneNoteFiles bool `config:"expose_onenote_files"` ExposeOneNoteFiles bool `config:"expose_onenote_files"`
ServerSideAcrossConfigs bool `config:"server_side_across_configs"` ServerSideAcrossConfigs bool `config:"server_side_across_configs"`
ListChunk int64 `config:"list_chunk"` ListChunk int64 `config:"list_chunk"`
@@ -990,13 +1040,10 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
} }
rootURL := graphAPIEndpoint[opt.Region] + "/v1.0" + "/drives/" + opt.DriveID rootURL := graphAPIEndpoint[opt.Region] + "/v1.0" + "/drives/" + opt.DriveID
oauthConfig.Scopes = opt.AccessScopes
if opt.DisableSitePermission { oauthConfig, err := makeOauthConfig(ctx, opt)
oauthConfig.Scopes = scopeAccessWithoutSites if err != nil {
} return nil, err
oauthConfig.Endpoint = oauth2.Endpoint{
AuthURL: authEndpoint[opt.Region] + authPath,
TokenURL: authEndpoint[opt.Region] + tokenPath,
} }
client := fshttp.NewClient(ctx) client := fshttp.NewClient(ctx)
@@ -2563,8 +2610,11 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
return errors.New("can't upload content to a OneNote file") return errors.New("can't upload content to a OneNote file")
} }
o.fs.tokenRenewer.Start() // Only start the renewer if we have a valid one
defer o.fs.tokenRenewer.Stop() if o.fs.tokenRenewer != nil {
o.fs.tokenRenewer.Start()
defer o.fs.tokenRenewer.Stop()
}
size := src.Size() size := src.Size()

View File

@@ -48,12 +48,10 @@ const (
// Globals // Globals
var ( var (
// Description of how to auth for this app // Description of how to auth for this app
oauthConfig = &oauth2.Config{ oauthConfig = &oauthutil.Config{
Scopes: nil, Scopes: nil,
Endpoint: oauth2.Endpoint{ AuthURL: "https://my.pcloud.com/oauth2/authorize",
AuthURL: "https://my.pcloud.com/oauth2/authorize", // TokenURL: "https://api.pcloud.com/oauth2_token", set by updateTokenURL
// TokenURL: "https://api.pcloud.com/oauth2_token", set by updateTokenURL
},
ClientID: rcloneClientID, ClientID: rcloneClientID,
ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret), ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
RedirectURL: oauthutil.RedirectLocalhostURL, RedirectURL: oauthutil.RedirectLocalhostURL,
@@ -61,8 +59,8 @@ var (
) )
// Update the TokenURL with the actual hostname // Update the TokenURL with the actual hostname
func updateTokenURL(oauthConfig *oauth2.Config, hostname string) { func updateTokenURL(oauthConfig *oauthutil.Config, hostname string) {
oauthConfig.Endpoint.TokenURL = "https://" + hostname + "/oauth2_token" oauthConfig.TokenURL = "https://" + hostname + "/oauth2_token"
} }
// Register with Fs // Register with Fs
@@ -79,7 +77,7 @@ func init() {
fs.Errorf(nil, "Failed to read config: %v", err) fs.Errorf(nil, "Failed to read config: %v", err)
} }
updateTokenURL(oauthConfig, optc.Hostname) updateTokenURL(oauthConfig, optc.Hostname)
checkAuth := func(oauthConfig *oauth2.Config, auth *oauthutil.AuthResult) error { checkAuth := func(oauthConfig *oauthutil.Config, auth *oauthutil.AuthResult) error {
if auth == nil || auth.Form == nil { if auth == nil || auth.Form == nil {
return errors.New("form not found in response") return errors.New("form not found in response")
} }

View File

@@ -82,13 +82,11 @@ const (
// Globals // Globals
var ( var (
// Description of how to auth for this app // Description of how to auth for this app
oauthConfig = &oauth2.Config{ oauthConfig = &oauthutil.Config{
Scopes: nil, Scopes: nil,
Endpoint: oauth2.Endpoint{ AuthURL: "https://user.mypikpak.com/v1/auth/signin",
AuthURL: "https://user.mypikpak.com/v1/auth/signin", TokenURL: "https://user.mypikpak.com/v1/auth/token",
TokenURL: "https://user.mypikpak.com/v1/auth/token", AuthStyle: oauth2.AuthStyleInParams,
AuthStyle: oauth2.AuthStyleInParams,
},
ClientID: clientID, ClientID: clientID,
RedirectURL: oauthutil.RedirectURL, RedirectURL: oauthutil.RedirectURL,
} }

View File

@@ -43,7 +43,6 @@ import (
"github.com/rclone/rclone/lib/pacer" "github.com/rclone/rclone/lib/pacer"
"github.com/rclone/rclone/lib/random" "github.com/rclone/rclone/lib/random"
"github.com/rclone/rclone/lib/rest" "github.com/rclone/rclone/lib/rest"
"golang.org/x/oauth2"
) )
const ( const (
@@ -59,12 +58,10 @@ const (
// Globals // Globals
var ( var (
// Description of how to auth for this app // Description of how to auth for this app
oauthConfig = &oauth2.Config{ oauthConfig = &oauthutil.Config{
Scopes: nil, Scopes: nil,
Endpoint: oauth2.Endpoint{ AuthURL: "https://www.premiumize.me/authorize",
AuthURL: "https://www.premiumize.me/authorize", TokenURL: "https://www.premiumize.me/token",
TokenURL: "https://www.premiumize.me/token",
},
ClientID: rcloneClientID, ClientID: rcloneClientID,
ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret), ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
RedirectURL: oauthutil.RedirectURL, RedirectURL: oauthutil.RedirectURL,

View File

@@ -13,7 +13,6 @@ import (
"github.com/rclone/rclone/lib/dircache" "github.com/rclone/rclone/lib/dircache"
"github.com/rclone/rclone/lib/encoder" "github.com/rclone/rclone/lib/encoder"
"github.com/rclone/rclone/lib/oauthutil" "github.com/rclone/rclone/lib/oauthutil"
"golang.org/x/oauth2"
) )
/* /*
@@ -41,12 +40,10 @@ const (
var ( var (
// Description of how to auth for this app // Description of how to auth for this app
putioConfig = &oauth2.Config{ putioConfig = &oauthutil.Config{
Scopes: []string{}, Scopes: []string{},
Endpoint: oauth2.Endpoint{ AuthURL: "https://api.put.io/v2/oauth2/authenticate",
AuthURL: "https://api.put.io/v2/oauth2/authenticate", TokenURL: "https://api.put.io/v2/oauth2/access_token",
TokenURL: "https://api.put.io/v2/oauth2/access_token",
},
ClientID: rcloneClientID, ClientID: rcloneClientID,
ClientSecret: obscure.MustReveal(rcloneObscuredClientSecret), ClientSecret: obscure.MustReveal(rcloneObscuredClientSecret),
RedirectURL: oauthutil.RedirectLocalhostURL, RedirectURL: oauthutil.RedirectLocalhostURL,

View File

@@ -97,7 +97,6 @@ import (
"github.com/rclone/rclone/lib/pacer" "github.com/rclone/rclone/lib/pacer"
"github.com/rclone/rclone/lib/random" "github.com/rclone/rclone/lib/random"
"github.com/rclone/rclone/lib/rest" "github.com/rclone/rclone/lib/rest"
"golang.org/x/oauth2"
) )
const ( const (
@@ -115,13 +114,11 @@ const (
) )
// Generate a new oauth2 config which we will update when we know the TokenURL // Generate a new oauth2 config which we will update when we know the TokenURL
func newOauthConfig(tokenURL string) *oauth2.Config { func newOauthConfig(tokenURL string) *oauthutil.Config {
return &oauth2.Config{ return &oauthutil.Config{
Scopes: nil, Scopes: nil,
Endpoint: oauth2.Endpoint{ AuthURL: "https://secure.sharefile.com/oauth/authorize",
AuthURL: "https://secure.sharefile.com/oauth/authorize", TokenURL: tokenURL,
TokenURL: tokenURL,
},
ClientID: rcloneClientID, ClientID: rcloneClientID,
ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret), ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
RedirectURL: oauthutil.RedirectPublicSecureURL, RedirectURL: oauthutil.RedirectPublicSecureURL,
@@ -136,7 +133,7 @@ func init() {
NewFs: NewFs, NewFs: NewFs,
Config: func(ctx context.Context, name string, m configmap.Mapper, config fs.ConfigIn) (*fs.ConfigOut, error) { Config: func(ctx context.Context, name string, m configmap.Mapper, config fs.ConfigIn) (*fs.ConfigOut, error) {
oauthConfig := newOauthConfig("") oauthConfig := newOauthConfig("")
checkAuth := func(oauthConfig *oauth2.Config, auth *oauthutil.AuthResult) error { checkAuth := func(oauthConfig *oauthutil.Config, auth *oauthutil.AuthResult) error {
if auth == nil || auth.Form == nil { if auth == nil || auth.Form == nil {
return errors.New("endpoint not found in response") return errors.New("endpoint not found in response")
} }
@@ -147,7 +144,7 @@ func init() {
} }
endpoint := "https://" + subdomain + "." + apicp endpoint := "https://" + subdomain + "." + apicp
m.Set("endpoint", endpoint) m.Set("endpoint", endpoint)
oauthConfig.Endpoint.TokenURL = endpoint + tokenPath oauthConfig.TokenURL = endpoint + tokenPath
return nil return nil
} }
return oauthutil.ConfigOut("", &oauthutil.Options{ return oauthutil.ConfigOut("", &oauthutil.Options{

View File

@@ -29,7 +29,6 @@ import (
"github.com/rclone/rclone/lib/random" "github.com/rclone/rclone/lib/random"
"github.com/rclone/rclone/lib/readers" "github.com/rclone/rclone/lib/readers"
"github.com/rclone/rclone/lib/rest" "github.com/rclone/rclone/lib/rest"
"golang.org/x/oauth2"
) )
// oAuth // oAuth
@@ -47,11 +46,9 @@ const (
// Globals // Globals
var ( var (
// Description of how to auth for this app // Description of how to auth for this app
oauthConfig = &oauth2.Config{ oauthConfig = &oauthutil.Config{
Endpoint: oauth2.Endpoint{ AuthURL: "https://oauth.yandex.com/authorize", //same as https://oauth.yandex.ru/authorize
AuthURL: "https://oauth.yandex.com/authorize", //same as https://oauth.yandex.ru/authorize TokenURL: "https://oauth.yandex.com/token", //same as https://oauth.yandex.ru/token
TokenURL: "https://oauth.yandex.com/token", //same as https://oauth.yandex.ru/token
},
ClientID: rcloneClientID, ClientID: rcloneClientID,
ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret), ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
RedirectURL: oauthutil.RedirectURL, RedirectURL: oauthutil.RedirectURL,

View File

@@ -47,7 +47,7 @@ const (
// Globals // Globals
var ( var (
// Description of how to auth for this app // Description of how to auth for this app
oauthConfig = &oauth2.Config{ oauthConfig = &oauthutil.Config{
Scopes: []string{ Scopes: []string{
"aaaserver.profile.read", "aaaserver.profile.read",
"WorkDrive.team.READ", "WorkDrive.team.READ",
@@ -55,11 +55,10 @@ var (
"WorkDrive.files.ALL", "WorkDrive.files.ALL",
"ZohoFiles.files.ALL", "ZohoFiles.files.ALL",
}, },
Endpoint: oauth2.Endpoint{
AuthURL: "https://accounts.zoho.eu/oauth/v2/auth", AuthURL: "https://accounts.zoho.eu/oauth/v2/auth",
TokenURL: "https://accounts.zoho.eu/oauth/v2/token", TokenURL: "https://accounts.zoho.eu/oauth/v2/token",
AuthStyle: oauth2.AuthStyleInParams, AuthStyle: oauth2.AuthStyleInParams,
},
ClientID: rcloneClientID, ClientID: rcloneClientID,
ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret), ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
RedirectURL: oauthutil.RedirectLocalhostURL, RedirectURL: oauthutil.RedirectLocalhostURL,
@@ -276,8 +275,8 @@ func setupRegion(m configmap.Mapper) error {
downloadURL = fmt.Sprintf("https://download.zoho.%s/v1/workdrive", region) downloadURL = fmt.Sprintf("https://download.zoho.%s/v1/workdrive", region)
uploadURL = fmt.Sprintf("https://upload.zoho.%s/workdrive-api/v1", region) uploadURL = fmt.Sprintf("https://upload.zoho.%s/workdrive-api/v1", region)
accountsURL = fmt.Sprintf("https://accounts.zoho.%s", region) accountsURL = fmt.Sprintf("https://accounts.zoho.%s", region)
oauthConfig.Endpoint.AuthURL = fmt.Sprintf("https://accounts.zoho.%s/oauth/v2/auth", region) oauthConfig.AuthURL = fmt.Sprintf("https://accounts.zoho.%s/oauth/v2/auth", region)
oauthConfig.Endpoint.TokenURL = fmt.Sprintf("https://accounts.zoho.%s/oauth/v2/token", region) oauthConfig.TokenURL = fmt.Sprintf("https://accounts.zoho.%s/oauth/v2/token", region)
return nil return nil
} }

View File

@@ -161,6 +161,27 @@ You may try to [verify you account](https://docs.microsoft.com/en-us/azure/activ
Note: If you have a special region, you may need a different host in step 4 and 5. Here are [some hints](https://github.com/rclone/rclone/blob/bc23bf11db1c78c6ebbf8ea538fbebf7058b4176/backend/onedrive/onedrive.go#L86). Note: If you have a special region, you may need a different host in step 4 and 5. Here are [some hints](https://github.com/rclone/rclone/blob/bc23bf11db1c78c6ebbf8ea538fbebf7058b4176/backend/onedrive/onedrive.go#L86).
### Using OAuth Client Credential flow
OAuth Client Credential flow will allow rclone to use permissions
directly associated with the Azure AD Enterprise application, rather
that adopting the context of an Azure AD user account.
This flow can be enabled by following the steps below:
1. Create the Enterprise App registration in the Azure AD portal and obtain a Client ID and Client Secret as described above.
2. Ensure that the application has the appropriate permissions and they are assigned as *Application Permissions*
3. Configure the remote, ensuring that *Client ID* and *Client Secret* are entered correctly.
4. In the *Advanced Config* section, enter `true` for `client_credentials` and in the `tenant` section enter the tenant ID.
When it comes to choosing the type of the connection work with the
client credentials flow. In particular the "onedrive" option does not
work. You can use the "sharepoint" option or if that does not find the
correct drive ID type it in manually with the "driveid" option.
**NOTE** Assigning permissions directly to the application means that
anyone with the *Client ID* and *Client Secret* can access your
OneDrive files. Take care to safeguard these credentials.
### Modification times and hashes ### Modification times and hashes

View File

@@ -46,6 +46,9 @@ const (
// ConfigTokenURL is the config key used to store the token server endpoint // ConfigTokenURL is the config key used to store the token server endpoint
ConfigTokenURL = "token_url" ConfigTokenURL = "token_url"
// ConfigClientCredentials - use OAUTH2 client credentials
ConfigClientCredentials = "client_credentials"
// ConfigEncoding is the config key to change the encoding for a backend // ConfigEncoding is the config key to change the encoding for a backend
ConfigEncoding = "encoding" ConfigEncoding = "encoding"

View File

@@ -11,6 +11,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -23,6 +24,7 @@ import (
"github.com/rclone/rclone/lib/random" "github.com/rclone/rclone/lib/random"
"github.com/skratchdot/open-golang/open" "github.com/skratchdot/open-golang/open"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
) )
var ( var (
@@ -85,6 +87,49 @@ All done. Please go back to rclone.
// should work for most uses, but may be overridden. // should work for most uses, but may be overridden.
var OpenURL = open.Start var OpenURL = open.Start
// Config - structure that we will use to store the OAuth configuration
// settings. This is based on the union of the configuration structures for the two
// OAuth modules that we are using (oauth2 and oauth2.clientcrentials), along with a
// flag indicating if we are going to use the client credential flow
type Config struct {
ClientID string
ClientSecret string
TokenURL string
AuthURL string
Scopes []string
EndpointParams url.Values
RedirectURL string
ClientCredentialFlow bool
AuthStyle oauth2.AuthStyle
}
// MakeOauth2Config makes an oauth2.Config from our config
func (conf *Config) MakeOauth2Config() *oauth2.Config {
return &oauth2.Config{
ClientID: conf.ClientID,
ClientSecret: conf.ClientSecret,
RedirectURL: RedirectLocalhostURL,
Scopes: conf.Scopes,
Endpoint: oauth2.Endpoint{
AuthURL: conf.AuthURL,
TokenURL: conf.TokenURL,
AuthStyle: conf.AuthStyle,
},
}
}
// MakeClientCredentialsConfig makes a clientcredentials.Config from our config
func (conf *Config) MakeClientCredentialsConfig() *clientcredentials.Config {
return &clientcredentials.Config{
ClientID: conf.ClientID,
ClientSecret: conf.ClientSecret,
Scopes: conf.Scopes,
TokenURL: conf.TokenURL,
AuthStyle: conf.AuthStyle,
// EndpointParams url.Values
}
}
// SharedOptions are shared between backends the utilize an OAuth flow // SharedOptions are shared between backends the utilize an OAuth flow
var SharedOptions = []fs.Option{{ var SharedOptions = []fs.Option{{
Name: config.ConfigClientID, Name: config.ConfigClientID,
@@ -107,6 +152,11 @@ var SharedOptions = []fs.Option{{
Name: config.ConfigTokenURL, Name: config.ConfigTokenURL,
Help: "Token server url.\n\nLeave blank to use the provider defaults.", Help: "Token server url.\n\nLeave blank to use the provider defaults.",
Advanced: true, Advanced: true,
}, {
Name: config.ConfigClientCredentials,
Default: false,
Help: "Use client credentials OAuth flow.\n\nThis will use the OAUTH2 client Credentials Flow as described in RFC 6749.",
Advanced: true,
}} }}
// oldToken contains an end-user's tokens. // oldToken contains an end-user's tokens.
@@ -178,7 +228,7 @@ type TokenSource struct {
m configmap.Mapper m configmap.Mapper
tokenSource oauth2.TokenSource tokenSource oauth2.TokenSource
token *oauth2.Token token *oauth2.Token
config *oauth2.Config config *Config
ctx context.Context ctx context.Context
expiryTimer *time.Timer // signals whenever the token expires expiryTimer *time.Timer // signals whenever the token expires
} }
@@ -264,6 +314,11 @@ func (ts *TokenSource) Token() (*oauth2.Token, error) {
) )
const maxTries = 5 const maxTries = 5
// If we have a cached valid token, use that
if ts.token.Valid() {
return ts.token, nil
}
// Try getting the token a few times // Try getting the token a few times
for i := 1; i <= maxTries; i++ { for i := 1; i <= maxTries; i++ {
// Try reading the token from the config file in case it has // Try reading the token from the config file in case it has
@@ -271,7 +326,7 @@ func (ts *TokenSource) Token() (*oauth2.Token, error) {
if !ts.token.Valid() { if !ts.token.Valid() {
if ts.reReadToken() { if ts.reReadToken() {
changed = true changed = true
} else if ts.token.RefreshToken == "" { } else if !ts.config.ClientCredentialFlow && ts.token.RefreshToken == "" {
return nil, fserrors.FatalError( return nil, fserrors.FatalError(
fmt.Errorf("token expired and there's no refresh token - manually refresh with \"rclone config reconnect %s:\"", ts.name), fmt.Errorf("token expired and there's no refresh token - manually refresh with \"rclone config reconnect %s:\"", ts.name),
) )
@@ -280,7 +335,11 @@ func (ts *TokenSource) Token() (*oauth2.Token, error) {
// Make a new token source if required // Make a new token source if required
if ts.tokenSource == nil { if ts.tokenSource == nil {
ts.tokenSource = ts.config.TokenSource(ts.ctx, ts.token) if ts.config.ClientCredentialFlow {
ts.tokenSource = ts.config.MakeClientCredentialsConfig().TokenSource(ts.ctx)
} else {
ts.tokenSource = ts.config.MakeOauth2Config().TokenSource(ts.ctx, ts.token)
}
} }
token, err = ts.tokenSource.Token() token, err = ts.tokenSource.Token()
@@ -297,7 +356,7 @@ func (ts *TokenSource) Token() (*oauth2.Token, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("couldn't fetch token: %w", err) return nil, fmt.Errorf("couldn't fetch token: %w", err)
} }
changed = changed || token.AccessToken != ts.token.AccessToken || token.RefreshToken != ts.token.RefreshToken || token.Expiry != ts.token.Expiry changed = changed || ts.token == nil || token.AccessToken != ts.token.AccessToken || token.RefreshToken != ts.token.RefreshToken || token.Expiry != ts.token.Expiry
ts.token = token ts.token = token
if changed { if changed {
// Bump on the expiry timer if it is set // Bump on the expiry timer if it is set
@@ -370,12 +429,12 @@ func Context(ctx context.Context, client *http.Client) context.Context {
return context.WithValue(ctx, oauth2.HTTPClient, client) return context.WithValue(ctx, oauth2.HTTPClient, client)
} }
// overrideCredentials sets the ClientID and ClientSecret from the // OverrideCredentials sets the ClientID and ClientSecret from the
// config file if they are not blank. // config file if they are not blank.
// If any value is overridden, true is returned. // If any value is overridden, true is returned.
// the origConfig is copied // the origConfig is copied
func overrideCredentials(name string, m configmap.Mapper, origConfig *oauth2.Config) (newConfig *oauth2.Config, changed bool) { func OverrideCredentials(name string, m configmap.Mapper, origConfig *Config) (newConfig *Config, changed bool) {
newConfig = new(oauth2.Config) newConfig = new(Config)
*newConfig = *origConfig *newConfig = *origConfig
changed = false changed = false
ClientID, ok := m.Get(config.ConfigClientID) ClientID, ok := m.Get(config.ConfigClientID)
@@ -393,12 +452,22 @@ func overrideCredentials(name string, m configmap.Mapper, origConfig *oauth2.Con
} }
AuthURL, ok := m.Get(config.ConfigAuthURL) AuthURL, ok := m.Get(config.ConfigAuthURL)
if ok && AuthURL != "" { if ok && AuthURL != "" {
newConfig.Endpoint.AuthURL = AuthURL newConfig.AuthURL = AuthURL
changed = true changed = true
} }
TokenURL, ok := m.Get(config.ConfigTokenURL) TokenURL, ok := m.Get(config.ConfigTokenURL)
if ok && TokenURL != "" { if ok && TokenURL != "" {
newConfig.Endpoint.TokenURL = TokenURL newConfig.TokenURL = TokenURL
changed = true
}
ClientCredentialStr, ok := m.Get(config.ConfigClientCredentials)
if ok && ClientCredentialStr != "" {
ClientCredential, err := strconv.ParseBool(ClientCredentialStr)
if err != nil {
fs.Errorf(nil, "Invalid setting for %q: %v", config.ConfigClientCredentials, err)
} else {
newConfig.ClientCredentialFlow = ClientCredential
}
changed = true changed = true
} }
return newConfig, changed return newConfig, changed
@@ -408,8 +477,8 @@ func overrideCredentials(name string, m configmap.Mapper, origConfig *oauth2.Con
// configures a Client with it. It returns the client and a // configures a Client with it. It returns the client and a
// TokenSource which Invalidate may need to be called on. It uses the // TokenSource which Invalidate may need to be called on. It uses the
// httpClient passed in as the base client. // httpClient passed in as the base client.
func NewClientWithBaseClient(ctx context.Context, name string, m configmap.Mapper, config *oauth2.Config, baseClient *http.Client) (*http.Client, *TokenSource, error) { func NewClientWithBaseClient(ctx context.Context, name string, m configmap.Mapper, config *Config, baseClient *http.Client) (*http.Client, *TokenSource, error) {
config, _ = overrideCredentials(name, m, config) config, _ = OverrideCredentials(name, m, config)
token, err := GetToken(name, m) token, err := GetToken(name, m)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -428,12 +497,39 @@ func NewClientWithBaseClient(ctx context.Context, name string, m configmap.Mappe
ctx: ctx, ctx: ctx,
} }
return oauth2.NewClient(ctx, ts), ts, nil return oauth2.NewClient(ctx, ts), ts, nil
}
// NewClientCredentialsClient creates a new OAuth module using the
// ClientCredential flow
func NewClientCredentialsClient(ctx context.Context, name string, m configmap.Mapper, oauthConfig *Config, baseClient *http.Client) (*http.Client, *TokenSource, error) {
oauthConfig, _ = OverrideCredentials(name, m, oauthConfig)
token, _ := GetToken(name, m)
// If the token doesn't exist then we will fetch one in the next step as we don't need a refresh token
// Set our own http client in the context
ctx = Context(ctx, baseClient)
// Wrap the TokenSource in our TokenSource which saves changed
// tokens in the config file
ts := &TokenSource{
name: name,
m: m,
token: token,
config: oauthConfig,
ctx: ctx,
}
return oauth2.NewClient(ctx, ts), ts, nil
} }
// NewClient gets a token from the config file and configures a Client // NewClient gets a token from the config file and configures a Client
// with it. It returns the client and a TokenSource which Invalidate may need to be called on // with it. It returns the client and a TokenSource which Invalidate
func NewClient(ctx context.Context, name string, m configmap.Mapper, oauthConfig *oauth2.Config) (*http.Client, *TokenSource, error) { // may need to be called on
func NewClient(ctx context.Context, name string, m configmap.Mapper, oauthConfig *Config) (*http.Client, *TokenSource, error) {
// Check whether we are using the client credentials flow
if oauthConfig.ClientCredentialFlow {
return NewClientCredentialsClient(ctx, name, m, oauthConfig, fshttp.NewClient(ctx))
}
return NewClientWithBaseClient(ctx, name, m, oauthConfig, fshttp.NewClient(ctx)) return NewClientWithBaseClient(ctx, name, m, oauthConfig, fshttp.NewClient(ctx))
} }
@@ -460,11 +556,11 @@ func (ar *AuthResult) Error() string {
} }
// CheckAuthFn is called when a good Auth has been received // CheckAuthFn is called when a good Auth has been received
type CheckAuthFn func(*oauth2.Config, *AuthResult) error type CheckAuthFn func(*Config, *AuthResult) error
// Options for the oauth config // Options for the oauth config
type Options struct { type Options struct {
OAuth2Config *oauth2.Config // Basic config for oauth2 OAuth2Config *Config // Basic config for oauth2
NoOffline bool // If set then "access_type=offline" parameter is not passed NoOffline bool // If set then "access_type=offline" parameter is not passed
CheckAuth CheckAuthFn // When the AuthResult is known the checkAuth function is called if set CheckAuth CheckAuthFn // When the AuthResult is known the checkAuth function is called if set
OAuth2Opts []oauth2.AuthCodeOption // extra oauth2 options OAuth2Opts []oauth2.AuthCodeOption // extra oauth2 options
@@ -532,6 +628,15 @@ func ConfigOAuth(ctx context.Context, name string, m configmap.Mapper, ri *fs.Re
if in.Result == "false" { if in.Result == "false" {
return fs.ConfigGoto(newState("*oauth-done")) return fs.ConfigGoto(newState("*oauth-done"))
} }
opt, err := getOAuth()
if err != nil {
return nil, err
}
oauthConfig, _ := OverrideCredentials(name, m, opt.OAuth2Config)
if oauthConfig.ClientCredentialFlow {
// If using client credential flow, skip straight to getting the token since we don't need a browser
return fs.ConfigGoto(newState("*oauth-do"))
}
return fs.ConfigConfirm(newState("*oauth-islocal"), true, "config_is_local", "Use web browser to automatically authenticate rclone with remote?\n * Say Y if the machine running rclone has a web browser you can use\n * Say N if running rclone on a (remote) machine without web browser access\nIf not sure try Y. If Y failed, try N.\n") return fs.ConfigConfirm(newState("*oauth-islocal"), true, "config_is_local", "Use web browser to automatically authenticate rclone with remote?\n * Say Y if the machine running rclone has a web browser you can use\n * Say N if running rclone on a (remote) machine without web browser access\nIf not sure try Y. If Y failed, try N.\n")
case "*oauth-islocal": case "*oauth-islocal":
if in.Result == "true" { if in.Result == "true" {
@@ -626,20 +731,27 @@ version recommended):
if err != nil { if err != nil {
return nil, err return nil, err
} }
oauthConfig, changed := overrideCredentials(name, m, opt.OAuth2Config) oauthConfig, changed := OverrideCredentials(name, m, opt.OAuth2Config)
if changed { if changed {
fs.Logf(nil, "Make sure your Redirect URL is set to %q in your custom config.\n", oauthConfig.RedirectURL) fs.Logf(nil, "Make sure your Redirect URL is set to %q in your custom config.\n", oauthConfig.RedirectURL)
} }
if code == "" { if oauthConfig.ClientCredentialFlow {
oauthConfig = fixRedirect(oauthConfig) err = clientCredentialsFlowGetToken(ctx, name, m, oauthConfig, opt)
code, err = configSetup(ctx, ri.Name, name, m, oauthConfig, opt)
if err != nil { if err != nil {
return nil, fmt.Errorf("config failed to refresh token: %w", err) return nil, err
}
} else {
if code == "" {
oauthConfig = fixRedirect(oauthConfig)
code, err = configSetup(ctx, ri.Name, name, m, oauthConfig, opt)
if err != nil {
return nil, fmt.Errorf("config failed to refresh token: %w", err)
}
}
err = configExchange(ctx, name, m, oauthConfig, code)
if err != nil {
return nil, err
} }
}
err = configExchange(ctx, name, m, oauthConfig, code)
if err != nil {
return nil, err
} }
return fs.ConfigGoto(newState("*oauth-done")) return fs.ConfigGoto(newState("*oauth-done"))
case "*oauth-done": case "*oauth-done":
@@ -656,13 +768,13 @@ func init() {
} }
// Return true if can run without a webserver and just entering a code // Return true if can run without a webserver and just entering a code
func noWebserverNeeded(oauthConfig *oauth2.Config) bool { func noWebserverNeeded(oauthConfig *Config) bool {
return oauthConfig.RedirectURL == TitleBarRedirectURL return oauthConfig.RedirectURL == TitleBarRedirectURL
} }
// get the URL we need to send the user to // get the URL we need to send the user to
func getAuthURL(name string, m configmap.Mapper, oauthConfig *oauth2.Config, opt *Options) (authURL string, state string, err error) { func getAuthURL(name string, m configmap.Mapper, oauthConfig *Config, opt *Options) (authURL string, state string, err error) {
oauthConfig, _ = overrideCredentials(name, m, oauthConfig) oauthConfig, _ = OverrideCredentials(name, m, oauthConfig)
// Make random state // Make random state
state, err = random.Password(128) state, err = random.Password(128)
@@ -670,18 +782,21 @@ func getAuthURL(name string, m configmap.Mapper, oauthConfig *oauth2.Config, opt
return "", "", err return "", "", err
} }
// Create the configuration required for the OAuth flow
oauth2Conf := oauthConfig.MakeOauth2Config()
// Generate oauth URL // Generate oauth URL
opts := opt.OAuth2Opts opts := opt.OAuth2Opts
if !opt.NoOffline { if !opt.NoOffline {
opts = append(opts, oauth2.AccessTypeOffline) opts = append(opts, oauth2.AccessTypeOffline)
} }
authURL = oauthConfig.AuthCodeURL(state, opts...) authURL = oauth2Conf.AuthCodeURL(state, opts...)
return authURL, state, nil return authURL, state, nil
} }
// If TitleBarRedirect is set but we are doing a real oauth, then // If TitleBarRedirect is set but we are doing a real oauth, then
// override our redirect URL // override our redirect URL
func fixRedirect(oauthConfig *oauth2.Config) *oauth2.Config { func fixRedirect(oauthConfig *Config) *Config {
switch oauthConfig.RedirectURL { switch oauthConfig.RedirectURL {
case TitleBarRedirectURL: case TitleBarRedirectURL:
// copy the config and set to use the internal webserver // copy the config and set to use the internal webserver
@@ -692,12 +807,33 @@ func fixRedirect(oauthConfig *oauth2.Config) *oauth2.Config {
return oauthConfig return oauthConfig
} }
// configSetup does the initial creation of the token for the client credentials flow
//
// If opt is nil it will use the default Options.
func clientCredentialsFlowGetToken(ctx context.Context, name string, m configmap.Mapper, oauthConfig *Config, opt *Options) error {
if opt == nil {
opt = &Options{}
}
_ = opt // not currently using the Options
fs.Debugf(nil, "Getting token for client credentials flow")
_, tokenSource, err := NewClientCredentialsClient(ctx, name, m, oauthConfig, fshttp.NewClient(ctx))
if err != nil {
return fmt.Errorf("client credentials flow: failed to make client: %w", err)
}
// Get the token and save it in the config file
_, err = tokenSource.Token()
if err != nil {
return fmt.Errorf("client credentials flow: failed to get token: %w", err)
}
return nil
}
// configSetup does the initial creation of the token // configSetup does the initial creation of the token
// //
// If opt is nil it will use the default Options. // If opt is nil it will use the default Options.
// //
// It will run an internal webserver to receive the results // It will run an internal webserver to receive the results
func configSetup(ctx context.Context, id, name string, m configmap.Mapper, oauthConfig *oauth2.Config, opt *Options) (string, error) { func configSetup(ctx context.Context, id, name string, m configmap.Mapper, oauthConfig *Config, opt *Options) (string, error) {
if opt == nil { if opt == nil {
opt = &Options{} opt = &Options{}
} }
@@ -749,9 +885,13 @@ func configSetup(ctx context.Context, id, name string, m configmap.Mapper, oauth
} }
// Exchange the code for a token // Exchange the code for a token
func configExchange(ctx context.Context, name string, m configmap.Mapper, oauthConfig *oauth2.Config, code string) error { func configExchange(ctx context.Context, name string, m configmap.Mapper, oauthConfig *Config, code string) error {
ctx = Context(ctx, fshttp.NewClient(ctx)) ctx = Context(ctx, fshttp.NewClient(ctx))
token, err := oauthConfig.Exchange(ctx, code)
// Create the configuration required for the OAuth flow
oauth2Conf := oauthConfig.MakeOauth2Config()
token, err := oauth2Conf.Exchange(ctx, code)
if err != nil { if err != nil {
return fmt.Errorf("failed to get token: %w", err) return fmt.Errorf("failed to get token: %w", err)
} }
@@ -813,10 +953,17 @@ func (s *authServer) handleAuth(w http.ResponseWriter, req *http.Request) {
// get code, error if empty // get code, error if empty
code := req.Form.Get("code") code := req.Form.Get("code")
if code == "" { if code == "" {
reply(http.StatusBadRequest, &AuthResult{ err := &AuthResult{
Name: "Auth Error", Name: "Auth Error",
Description: "No code returned by remote server", Description: "No code returned by remote server",
}) }
if errorCode := req.Form.Get("error"); errorCode != "" {
err.Description += ": " + errorCode
}
if errorMessage := req.Form.Get("error_description"); errorMessage != "" {
err.Description += ": " + errorMessage
}
reply(http.StatusBadRequest, err)
return return
} }