From 2e376eb3b95aa502e4830ed4684c5b74df63eae2 Mon Sep 17 00:00:00 2001 From: albertony <12441419+albertony@users.noreply.github.com> Date: Sat, 4 Oct 2025 13:56:29 +0200 Subject: [PATCH] jottacloud: added support for traditional oauth authentication also for the main service This renames whitelabel authentication to traditional authentication and adds support for the main Jottacloud service also here, as it can be used as an alternative to the authentication based on personal login token for those who prefer it. Documentation also adjusted correspondingly, and restructured the authentication section a bit more since some of the sections that was under standard authentication in reality also applies to the traditional authentication. --- backend/jottacloud/jottacloud.go | 65 ++++++----- docs/content/jottacloud.md | 185 +++++++++++++++++++++---------- 2 files changed, 169 insertions(+), 81 deletions(-) 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>