diff --git a/backend/jottacloud/jottacloud.go b/backend/jottacloud/jottacloud.go index 235856700..e9355b74f 100644 --- a/backend/jottacloud/jottacloud.go +++ b/backend/jottacloud/jottacloud.go @@ -60,7 +60,7 @@ const ( configVersion = 1 defaultTokenURL = "https://id.jottacloud.com/auth/realms/jottacloud/protocol/openid-connect/token" - defaultClientID = "jottacli" + defaultClientID = "jottacli" // Identified as "Jottacloud CLI" in "My logged in devices" legacyTokenURL = "https://api.jottacloud.com/auth/v1/token" legacyRegisterURL = "https://api.jottacloud.com/auth/v1/register" @@ -69,7 +69,7 @@ const ( legacyConfigVersion = 0 ) -func getWhitelabelServices() map[string]struct { +func getServices() map[string]struct { name, domain, realm, clientID string scopes []string } { @@ -80,16 +80,17 @@ func getWhitelabelServices() map[string]struct { clientID string scopes []string }{ + "jottacloud": {"Jottacloud", "id.jottacloud.com", "jottacloud", "desktop", []string{"openid", "jotta-default", "offline_access"}}, // Chose client id "desktop" here, will be identified as "Jottacloud for Desktop" in "My logged in devices", but could have used "jottacli" here as well. + "elkjop": {"Elkjøp Cloud (Norway)", "cloud.elkjop.no", "elkjop", "desktop", []string{"openid", "jotta-default", "offline_access"}}, + "elgiganten_dk": {"Elgiganten Cloud (Denmark)", "cloud.elgiganten.dk", "elgiganten", "desktop", []string{"openid", "jotta-default", "offline_access"}}, + "elgiganten_se": {"Elgiganten Cloud (Sweden)", "cloud.elgiganten.se", "elgiganten", "desktop", []string{"openid", "jotta-default", "offline_access"}}, + "elko": {"ELKO Cloud (Iceland)", "cloud.elko.is", "elko", "desktop", []string{"openid", "jotta-default", "offline_access"}}, + "gigantti": {"Gigantti Cloud (Finland)", "cloud.gigantti.fi", "gigantti", "desktop", []string{"openid", "jotta-default", "offline_access"}}, "telia_se": {"Telia Cloud (Sweden)", "cloud-auth.telia.se", "telia_se", "desktop", []string{"openid", "jotta-default", "offline_access"}}, "telia_no": {"Telia Sky (Norway)", "sky-auth.telia.no", "get", "desktop", []string{"openid", "jotta-default", "offline_access"}}, "tele2": {"Tele2 Cloud (Sweden)", "mittcloud-auth.tele2.se", "comhem", "desktop", []string{"openid", "jotta-default", "offline_access"}}, "onlime": {"Onlime (Denmark)", "cloud-auth.onlime.dk", "onlime_wl", "desktop", []string{"openid", "jotta-default", "offline_access"}}, - "elkjop": {"Elkjøp Cloud (Norway)", "cloud.elkjop.no", "elkjop", "desktop", []string{"openid", "jotta-default", "offline_access"}}, - "elgiganten_se": {"Elgiganten Cloud (Sweden)", "cloud.elgiganten.se", "elgiganten", "desktop", []string{"openid", "jotta-default", "offline_access"}}, - "elgiganten_dk": {"Elgiganten Cloud (Denmark)", "cloud.elgiganten.dk", "elgiganten", "desktop", []string{"openid", "jotta-default", "offline_access"}}, - "gigantti": {"Gigantti Cloud (Finland)", "cloud.gigantti.fi", "gigantti", "desktop", []string{"openid", "jotta-default", "offline_access"}}, - "elko": {"ELKO Cloud (Iceland)", "cloud.elko.is", "elko", "desktop", []string{"openid", "jotta-default", "offline_access"}}, - "mediamarkt": {"MediaMarkt Cloud", "mediamarkt.jottacloud.com", "mediamarkt", "desktop", []string{"openid", "jotta-default", "offline_access"}}, + "mediamarkt": {"MediaMarkt Cloud (Multiregional)", "mediamarkt.jottacloud.com", "mediamarkt", "desktop", []string{"openid", "jotta-default", "offline_access"}}, "letsgo": {"Let's Go Cloud (Germany)", "letsgo.jotta.cloud", "letsgo", "desktop-win", []string{"openid", "offline_access"}}, } } @@ -177,20 +178,34 @@ func Config(ctx context.Context, name string, m configmap.Mapper, conf fs.Config } return fs.ConfigChooseExclusiveFixed("auth_type_done", "config_type", `Type of authentication.`, []fs.OptionExample{{ Value: "standard", - Help: "Standard authentication.\nUse this if you're a normal Jottacloud user.", + Help: `Standard authentication. +This is primarily supported by the official service, but may also be +supported by some white-label services. It is designed for command-line +applications, and you will be asked to enter a single-use personal login +token which you must manually generate from the account security settings +in the web interface of your service.`, }, { - Value: "whitelabel", - Help: "Whitelabel authentication.\nUse this if you are using the service offered by a third party such as Telia, Tele2, Onlime, Elkjøp, etc.", + Value: "traditional", + Help: `Traditional authentication. +This is supported by the official service and all white-label services +that rclone knows about. You will be asked which service to connect to. +It has a limitation of only a single active authentication at a time. You +need to be on, or have access to, a machine with an internet-connected +web browser.`, }, { Value: "legacy", - Help: "Legacy authentication.\nThis is no longer supported by any known services and not recommended for normal users.", + Help: `Legacy authentication. +This is no longer supported by any known services and not recommended +used. You will be asked for your account's username and password.`, }}) case "auth_type_done": // Jump to next state according to config chosen return fs.ConfigGoto(conf.Result) case "standard": // configure a jottacloud backend using the modern JottaCli token based authentication m.Set("configVersion", fmt.Sprint(configVersion)) - return fs.ConfigInput("standard_token", "config_login_token", "Personal login token.\nGenerate here: https://www.jottacloud.com/web/secure") + return fs.ConfigInput("standard_token", "config_login_token", `Personal login token. +Generate it from the account security settings in the web interface of your +service, for the official service on https://www.jottacloud.com/web/secure.`) case "standard_token": loginToken := conf.Result m.Set(configClientID, defaultClientID) @@ -207,10 +222,10 @@ func Config(ctx context.Context, name string, m configmap.Mapper, conf fs.Config return nil, fmt.Errorf("error while saving token: %w", err) } return fs.ConfigGoto("choose_device") - case "whitelabel": - whitelabels := getWhitelabelServices() - options := make([]fs.OptionExample, 0, len(whitelabels)) - for key, val := range whitelabels { + case "traditional": + services := getServices() + options := make([]fs.OptionExample, 0, len(services)) + for key, val := range services { options = append(options, fs.OptionExample{ Value: key, Help: val.name, @@ -219,17 +234,17 @@ func Config(ctx context.Context, name string, m configmap.Mapper, conf fs.Config sort.Slice(options, func(i, j int) bool { return options[i].Help < options[j].Help }) - return fs.ConfigChooseExclusiveFixed("whitelabel_type", "config_whitelabel", + return fs.ConfigChooseExclusiveFixed("traditional_type", "config_traditional", "White-label service. This decides the domain name to connect to and\nthe authentication configuration to use.", options) - case "whitelabel_type": - whitelabel, ok := getWhitelabelServices()[conf.Result] + case "traditional_type": + service, ok := getServices()[conf.Result] if !ok { - return nil, fmt.Errorf("unexpected whitelabel %q", conf.Result) + return nil, fmt.Errorf("unexpected service %q", conf.Result) } opts := rest.Opts{ Method: "GET", - RootURL: "https://" + whitelabel.domain + "/auth/realms/" + whitelabel.realm + "/.well-known/openid-configuration", + RootURL: "https://" + service.domain + "/auth/realms/" + service.realm + "/.well-known/openid-configuration", } var wellKnown api.WellKnown srv := rest.NewClient(fshttp.NewClient(ctx)) @@ -238,14 +253,14 @@ func Config(ctx context.Context, name string, m configmap.Mapper, conf fs.Config return nil, fmt.Errorf("failed to get authentication provider configuration: %w", err) } m.Set("configVersion", fmt.Sprint(configVersion)) - m.Set(configClientID, whitelabel.clientID) + m.Set(configClientID, service.clientID) m.Set(configTokenURL, wellKnown.TokenEndpoint) return oauthutil.ConfigOut("choose_device", &oauthutil.Options{ OAuth2Config: &oauthutil.Config{ AuthURL: wellKnown.AuthorizationEndpoint, TokenURL: wellKnown.TokenEndpoint, - ClientID: whitelabel.clientID, - Scopes: whitelabel.scopes, + ClientID: service.clientID, + Scopes: service.scopes, RedirectURL: oauthutil.RedirectLocalhostURL, }, }) diff --git a/docs/content/jottacloud.md b/docs/content/jottacloud.md index 2cc67db7c..e0765c7e5 100644 --- a/docs/content/jottacloud.md +++ b/docs/content/jottacloud.md @@ -35,34 +35,30 @@ Paths are specified as `remote:path` Paths may be as deep as required, e.g. `remote:directory/subdirectory`. -## Authentication types +## Authentication -Some of the white-label versions uses a different authentication method than the -official service, and you have to choose the correct one when setting up the remote. +Authentication in Jottacloud is in general based on OAuth and OpenID Connect +(OIDC). There are different variants to choose from, depending on which service +you are using, e.g. a white-label service may only support one of them. Note +that there is no documentation to rely on, so the descriptions provided here +are based on observations and may not be accurate. -### Standard authentication +Jottacloud uses two optional OAuth security mechanisms, referred to as "Refresh +Token Rotation" and "Automatic Reuse Detection", which has some implications. +Access tokens normally have one hour expiry, after which they need to be +refreshed (rotated), an operation that requires the refresh token to be +supplied. Rclone does this automatically. This is standard OAuth. But in +Jottacloud, such a refresh operation not only creates a new access token, but +also refresh token, and invalidates the existing refresh token, the one that +was supplied. It keeps track of the history of refresh tokens, sometimes +referred to as a token family, descending from the original refresh token that +was issued after the initial authentication. This is used to detect any +attempts at reusing old refresh tokens, and trigger an immedate invalidation of +the current refresh token, and effectively the entire refresh token family. -The standard authentication method used by the official service (jottacloud.com), -as well as some of the white-label services, is based on OAuth 2.0 and OpenID -Connect (OIDC), and requires you to generate a single-use personal login token -from the account security settings in the service's web interface. Log in to your -account, go to "Settings" and then "Security", or use the direct link presented -to you by rclone when configuring the remote: -. Scroll down to the section "Personal login -token", and click the "Generate" button. Note that if you are using a white-label -service you probably can't use the direct link, you need to find the same page in -their dedicated web interface, and also it may be in a different location than -described above. - -To access your account from multiple instances of rclone, you need to configure -each of them with a separate personal login token. E.g. you create a Jottacloud -remote with rclone in one location, and copy the configuration file to a second -location where you also want to run rclone and access the same remote. Then you -need to replace the token for one of them, using the [config reconnect](https://rclone.org/commands/rclone_config_reconnect/) -command, which requires you to generate a new personal login token and supply -as input. If you do not do this, the token may easily end up being invalidated, -resulting in both instances failing with an error message, something along -the lines of: +When the current refresh token has been invalidated, next time rclone tries to +perform a token refresh, it will fail with an error message something along the +lines of: ```text CRITICAL: Failed to create file system for "remote:": (...): couldn't fetch token: invalid_grant: maybe token expired? - try refreshing with "rclone config reconnect remote:" @@ -77,38 +73,105 @@ DEBUG : remote: got fatal oauth error: oauth2: "invalid_grant" "Session doesn't (The error description used to be "Stale token" instead of "Session doesn't have required client", so you may see references to that in older descriptions -of this case.) +of this situation.) -When this happens, you need to replace the token as described above to be able -to use your remote again. +When this happens, you need to re-authenticate to be able to use your remote +again, e.g. using the [config reconnect](/commands/rclone_config_reconnect/) +command as suggested in the error message. This will create an entirely new +refresh token (family). -All personal login tokens you have taken into use will be listed in the web -interface under "My logged in devices", and from the right side of that list -you can click the "X" button to revoke individual tokens. +A typical example of how you may end up in this situation, is if you create +a Jottacloud remote with rclone in one location, and then copy the +configuration file to a second location where you start using rclone to access +the same remote. Eventually there will now be a token refresh attempt with an +invalidated token, i.e. refresh token reuse, resulting in both instances +starting to fail with the "invalid_grant" error. It is possible to copy remote +configurations, but you must then replace the token for one of them using the +[config reconnect](https://rclone.org/commands/rclone_config_reconnect/) +command. -### Whitelabel authentication +You can get some overview of your active tokens in your service's web user +interface, if you navigate to "Settings" and then "Security" (in which case +you end up at or similar). Down on +that page you have a section "My logged in devices". This contains a list +of entries which seemingly represents currently valid refresh tokens, or +refresh token families. From the right side of that list you can click a +button ("X") to revoke (invalidate) it, which means you will still have access +using an existing access token until that expires, but you will not be able to +perform a token refresh. Note that this entire "My logged in devices" feature +seem to behave a bit differently with different authentication variants and +with use of the different (white-label) services. -Most of the white-label versions uses a slightly different authentication flow, -where it doesn't offer the option of creating a CLI token, and the username -is generated internally. To setup rclone to use one of these, choose white-label -authentication in the setup process, and then select the specific service -in the next step. +### Standard -Note that when setting this up, you need to be on a machine with an -internet-connected web browser. If you need it on a machine where this is not -the case, then you will have to create the configuration on a different machine -and copy it from there. The jottacloud backend does not support the -`rclone authorize` command. See the [remote setup docs](/remote_setup) for -details. +This is an OAuth variant designed for command-line applications. It is +primarily supported by the official service (jottacloud.com), but may also be +supported by some of the white-label services. The information necessary to be +able to perform authentication, like domain name and endpoint to connect to, +are found automatically (it is encoded into the supplied login token, described +next), so you do not need to specify which service to configure. -### Legacy authentication +When configuring a remote, you are asked to enter a single-use personal login +token, which you must manually generate from the account security settings in +the service's web interface. You do not need a web browser on the same machine +like with traditional OAuth, but need to use a web browser somewhere, and be +able to be copy the generated string into your rclone configuration session. +Log in to your service's web user interface, navigate to "Settings" and then +"Security", or, for the official service, use the direct link presented to you +by rclone when configuring the remote: . +Scroll down to the section "Personal login token", and click the "Generate" +button. Copy the presented string and paste it where rclone asks for it. Rclone +will then use this to perform an initial token request, and receive a regular +OAuth token which it stores in your remote configuration. There will then also +be a new entry in the "My logged in devices" list in the web interface, with +device name and application name "Jottacloud CLI". -Originally Jottacloud used an older authentication method, not based on OpenID -Connect, which required the username and password to be specified. Since -Jottacloud migrated to the newer method, handled by the standard authentication, -some white-label versions (those from Elkjøp) still used the legacy method for -a long time. Currently there are no known uses of this, it is still supported -by rclone, but the support will be removed in a future version. +Each time a new token is created this way, i.e. a new personal login token is +generated and traded in for an OAuth token, you get an entirely new refresh +token family, with a new entry in the "My logged in devices". You can create as +many remotes as you want, and use multiple instances of rclone on same or +different machine, as long as you configure them separately like this, and not +get your self into the refresh token reuse issue described above. + +### Traditional + +Jottacloud also supports a more traditional OAuth variant. Most of the +white-label services support this, and for many of them this is the only +alternative because they do not support personal login tokens. This method +relies on pre-defined service-specific domain names and endpoints, and rclone +need you to specify which service to configure. This also means that any +changes to existing or additions of new white-label services needs an update +in the rclone backend implementation. + +When configuring a remote, you must interactively login to an OAuth +authorization web site, and a one-time authorization code is sent back to +rclone behind the scene, which it uses to request an OAuth token. This means +that you need to be on a machine with an internet-connected web browser. If you +need it on a machine where this is not the case, then you will have to create +the configuration on a different machine and copy it from there. The Jottacloud +backend does not support the `rclone authorize` command. See the +[remote setup docs](/remote_setup) for details. + +Jottacloud exerts some form of strict session management when authenticating +using this method. This leads to some unexpected cases of the "invalid_grant" +error described above, and effectively limits you to only use of a single +active authentication on the same machine. I.e. you can only create a single +rclone remote, and you can't even log in with the service's official desktop +client while having a rclone remote configured, or else you will eventually get +all sessions invalidated and are forced to re-authenticate. + +When you have successfully authenticated, there will be an entry in the +"My logged in devices" list in the web interface representing your session. It +will typically be listed with application name "Jottacloud for Desktop" or +similar (it depends on the white-label service configuration). + +### Legacy + +Originally Jottacloud used an OAuth variant which required your account's +username and password to be specified. When Jottacloud migrated to the newer +methods, some white-label versions (those from Elkjøp) still used this legacy +method for a long time. Currently there are no known uses of this, it is still +supported by rclone, but the support will be removed in a future version. ## Configuration @@ -162,19 +225,29 @@ Type of authentication. Choose a number from below, or type in an existing value of type string. Press Enter for the default (standard). / Standard authentication. - 1 | Use this if you're a normal Jottacloud user. + | This is primarily supported by the official service, but may also be + | supported by some white-label services. It is designed for command-line + 1 | applications, and you will be asked to enter a single-use personal login + | token which you must manually generate from the account security settings + | in the web interface of your service. \ (standard) - / Whitelabel authentication. - 2 | Use this if you are using the service offered by a third party such as Telia, Tele2, Onlime, Elkjøp, etc. - \ (whitelabel) + / Traditional authentication. + | This is supported by the official service and all white-label services + | that rclone knows about. You will be asked which service to connect to. + 2 | It has a limitation of only a single active authentication at a time. You + | need to be on, or have access to, a machine with an internet-connected + | web browser. + \ (traditional) / Legacy authentication. - 3 | This is no longer supported by any known services and not recommended for normal users. + 3 | This is no longer supported by any known services and not recommended + | used. You will be asked for your account's username and password. \ (legacy) config_type> 1 Option config_login_token. Personal login token. -Generate here: https://www.jottacloud.com/web/secure +Generate it from the account security settings in the web interface of your +service, for the official service on https://www.jottacloud.com/web/secure. Enter a value. config_login_token>