Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/duplicati/duplicati.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKenneth Skovhede <kenneth@hexad.dk>2022-06-12 23:14:23 +0300
committerGitHub <noreply@github.com>2022-06-12 23:14:23 +0300
commit179efa04725542b32798a59f4c0fc9affb0619a1 (patch)
treeea341e4bdf9b675071eb4582be7e8510d9553757
parent6ca25d3e7395fcdefb651691f6adaba669c3760e (diff)
parent5e3a651d16c4283d52ee965c381363528caf4ede (diff)
Merge pull request #4699 from albertony/jottacloud_oauth
Jottacloud backend oauth authentication
-rw-r--r--Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj9
-rw-r--r--Duplicati/Library/Backend/Jottacloud/Jottacloud.cs60
-rw-r--r--Duplicati/Library/Backend/Jottacloud/JottacloudAuthHelper.cs70
-rw-r--r--Duplicati/Library/Backend/Jottacloud/Strings.cs9
-rw-r--r--Duplicati/Library/Backend/Jottacloud/packages.config4
-rw-r--r--Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js47
-rw-r--r--Duplicati/Server/webroot/ngax/templates/backends/jottacloud.html13
7 files changed, 119 insertions, 93 deletions
diff --git a/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj b/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj
index 9cdfd56bf..07d34fd01 100644
--- a/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj
+++ b/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj
@@ -36,11 +36,15 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
+ <Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
+ <HintPath>..\..\..\..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
+ </Reference>
<Reference Include="System" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Jottacloud.cs" />
+ <Compile Include="JottacloudAuthHelper.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Strings.cs" />
</ItemGroup>
@@ -65,9 +69,14 @@
<Project>{D63E53E4-A458-4C2F-914D-92F715F58ACF}</Project>
<Name>Duplicati.Library.Common</Name>
</ProjectReference>
+ <ProjectReference Include="..\OAuthHelper\Duplicati.Library.OAuthHelper.csproj">
+ <Project>{d4c37c33-5e73-4b56-b2c3-dc4a6baa36bb}</Project>
+ <Name>Duplicati.Library.OAuthHelper</Name>
+ </ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="Duplicati.snk" />
+ <None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
diff --git a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs
index 9f297abf2..2ebfcc7c1 100644
--- a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs
+++ b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs
@@ -31,9 +31,9 @@ namespace Duplicati.Library.Backend
// This class is instantiated dynamically in the BackendLoader.
public class Jottacloud : IBackend, IStreamingBackend
{
+ private const string AUTHID_OPTION = "authid";
private const string JFS_ROOT = "https://jfs.jottacloud.com/jfs";
private const string JFS_ROOT_UPLOAD = "https://up.jottacloud.com/jfs"; // Separate host for uploading files
- private const string API_VERSION = "2.4"; // Hard coded per 09. March 2017.
private const string JFS_BUILTIN_DEVICE = "Jotta"; // The built-in device used for the built-in Sync and Archive mount points.
private static readonly string JFS_DEFAULT_BUILTIN_MOUNT_POINT = "Archive"; // When using the built-in device we pick this mount point as our default.
private static readonly string JFS_DEFAULT_CUSTOM_MOUNT_POINT = "Duplicati"; // When custom device is specified then we pick this mount point as our default.
@@ -51,7 +51,6 @@ namespace Duplicati.Library.Backend
private readonly string m_url_device;
private readonly string m_url;
private readonly string m_url_upload;
- private readonly System.Net.NetworkCredential m_userInfo;
private readonly byte[] m_copybuffer = new byte[Duplicati.Library.Utility.Utility.DEFAULT_BUFFER_SIZE];
private static readonly string JFS_DEFAULT_CHUNKSIZE = "5mb";
@@ -59,6 +58,8 @@ namespace Duplicati.Library.Backend
private readonly int m_threads;
private readonly long m_chunksize;
+ private readonly JottacloudAuthHelper m_oauth;
+
/// <summary>
/// The default maximum number of concurrent connections allowed by a ServicePoint object is 2.
/// It should be increased to allow multiple download threads.
@@ -132,40 +133,21 @@ namespace Duplicati.Library.Backend
m_mountPoint = JFS_DEFAULT_CUSTOM_MOUNT_POINT; // Set a suitable default mount point for custom (backup) devices.
}
+ string authid = null;
+ if (options.ContainsKey(AUTHID_OPTION))
+ authid = options[AUTHID_OPTION];
+ m_oauth = new JottacloudAuthHelper(authid);
+
// Build URL
var u = new Utility.Uri(url);
m_path = u.HostAndPath; // Host and path of "jottacloud://folder/subfolder" is "folder/subfolder", so the actual folder path within the mount point.
if (string.IsNullOrEmpty(m_path)) // Require a folder. Actually it is possible to store files directly on the root level of the mount point, but that does not seem to be a good option.
throw new UserInformationException(Strings.Jottacloud.NoPathError, "JottaNoPath");
m_path = Util.AppendDirSeparator(m_path, "/");
- if (!string.IsNullOrEmpty(u.Username))
- {
- m_userInfo = new System.Net.NetworkCredential();
- m_userInfo.UserName = u.Username;
- if (!string.IsNullOrEmpty(u.Password))
- m_userInfo.Password = u.Password;
- else if (options.ContainsKey("auth-password"))
- m_userInfo.Password = options["auth-password"];
- }
- else
- {
- if (options.ContainsKey("auth-username"))
- {
- m_userInfo = new System.Net.NetworkCredential();
- m_userInfo.UserName = options["auth-username"];
- if (options.ContainsKey("auth-password"))
- m_userInfo.Password = options["auth-password"];
- }
- }
- if (m_userInfo == null || string.IsNullOrEmpty(m_userInfo.UserName))
- throw new UserInformationException(Strings.Jottacloud.NoUsernameError, "JottaNoUsername");
- if (m_userInfo == null || string.IsNullOrEmpty(m_userInfo.Password))
- throw new UserInformationException(Strings.Jottacloud.NoPasswordError, "JottaNoPassword");
- if (m_userInfo != null) // Bugfix, see http://connect.microsoft.com/VisualStudio/feedback/details/695227/networkcredential-default-constructor-leaves-domain-null-leading-to-null-object-reference-exceptions-in-framework-code
- m_userInfo.Domain = "";
- m_url_device = JFS_ROOT + "/" + m_userInfo.UserName + "/" + m_device;
+
+ m_url_device = JFS_ROOT + "/" + m_oauth.Username + "/" + m_device;
m_url = m_url_device + "/" + m_mountPoint + "/" + m_path;
- m_url_upload = JFS_ROOT_UPLOAD + "/" + m_userInfo.UserName + "/" + m_device + "/" + m_mountPoint + "/" + m_path; // Different hostname, else identical to m_url.
+ m_url_upload = JFS_ROOT_UPLOAD + "/" + m_oauth.Username + "/" + m_device + "/" + m_mountPoint + "/" + m_path; // Different hostname, else identical to m_url.
m_threads = int.Parse(options.ContainsKey(JFS_THREADS) ? options[JFS_THREADS] : JFS_DEFAULT_THREADS);
@@ -186,7 +168,7 @@ namespace Duplicati.Library.Backend
m_chunksize = chunksize;
}
- #region IBackend Members
+#region IBackend Members
public string DisplayName
{
@@ -329,8 +311,7 @@ namespace Duplicati.Library.Backend
get
{
return new List<ICommandLineArgument>(new ICommandLineArgument[] {
- new CommandLineArgument("auth-password", CommandLineArgument.ArgumentType.Password, Strings.Jottacloud.DescriptionAuthPasswordShort, Strings.Jottacloud.DescriptionAuthPasswordLong),
- new CommandLineArgument("auth-username", CommandLineArgument.ArgumentType.String, Strings.Jottacloud.DescriptionAuthUsernameShort, Strings.Jottacloud.DescriptionAuthUsernameLong),
+ new CommandLineArgument(AUTHID_OPTION, CommandLineArgument.ArgumentType.Password, Strings.Jottacloud.AuthidShort, Strings.Jottacloud.AuthidLong(OAuthHelper.OAUTH_LOGIN_URL("jottacloud"))),
new CommandLineArgument(JFS_DEVICE_OPTION, CommandLineArgument.ArgumentType.String, Strings.Jottacloud.DescriptionDeviceShort, Strings.Jottacloud.DescriptionDeviceLong(JFS_MOUNT_POINT_OPTION)),
new CommandLineArgument(JFS_MOUNT_POINT_OPTION, CommandLineArgument.ArgumentType.String, Strings.Jottacloud.DescriptionMountPointShort, Strings.Jottacloud.DescriptionMountPointLong(JFS_DEVICE_OPTION)),
new CommandLineArgument(JFS_THREADS, CommandLineArgument.ArgumentType.Integer, Strings.Jottacloud.ThreadsShort, Strings.Jottacloud.ThreadsLong, JFS_DEFAULT_THREADS),
@@ -368,26 +349,19 @@ namespace Duplicati.Library.Backend
}
}
- #endregion
+#endregion
- #region IDisposable Members
+#region IDisposable Members
public void Dispose()
{
}
- #endregion
+#endregion
private System.Net.HttpWebRequest CreateRequest(string method, string url, string queryparams)
{
- System.Net.HttpWebRequest req = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(url + (string.IsNullOrEmpty(queryparams) || queryparams.Trim().Length == 0 ? "" : "?" + queryparams));
- req.Method = method;
- req.Credentials = m_userInfo;
- req.PreAuthenticate = true; // We need this under Mono for some reason, and it appears some servers require this as well
- req.KeepAlive = false;
- req.UserAgent = "Duplicati Jottacloud Client v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
- req.Headers.Add("x-jftp-version", API_VERSION);
- return req;
+ return m_oauth.CreateRequest(url + (string.IsNullOrEmpty(queryparams) || queryparams.Trim().Length == 0 ? "" : "?" + queryparams), method);
}
private System.Net.HttpWebRequest CreateRequest(string method, string remotename, string queryparams, bool upload)
diff --git a/Duplicati/Library/Backend/Jottacloud/JottacloudAuthHelper.cs b/Duplicati/Library/Backend/Jottacloud/JottacloudAuthHelper.cs
new file mode 100644
index 000000000..d8a5d2f48
--- /dev/null
+++ b/Duplicati/Library/Backend/Jottacloud/JottacloudAuthHelper.cs
@@ -0,0 +1,70 @@
+// Copyright (C) 2015, The Duplicati Team
+// http://www.duplicati.com, info@duplicati.com
+//
+// This library is free software; you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+using Duplicati.Library.Interface;
+using Newtonsoft.Json;
+
+namespace Duplicati.Library.Backend
+{
+ public class JottacloudAuthHelper : OAuthHelper
+ {
+ private const string USERINFO_URL = "https://id.jottacloud.com/auth/realms/jottacloud/protocol/openid-connect/userinfo";
+ private string m_username;
+
+ public JottacloudAuthHelper(string accessToken)
+ : base(accessToken, "jottacloud")
+ {
+ base.AutoAuthHeader = true;
+
+ var userinfo = GetJSONData<UserInfo>(USERINFO_URL);
+ if (userinfo == null || string.IsNullOrEmpty(userinfo.Username))
+ throw new UserInformationException(Strings.Jottacloud.NoUsernameError, "JottaNoUsername");
+ m_username = userinfo.Username;
+ }
+
+ public string Username
+ {
+ get
+ {
+ return m_username;
+ }
+ }
+
+ private class UserInfo
+ {
+ [JsonProperty("sub")]
+ public string Subject { get; set; }
+ [JsonProperty("email_verified")]
+ public bool EmailVerified { get; set; }
+ [JsonProperty("name")]
+ public string Name { get; set; }
+ [JsonProperty("realm")]
+ public string Realm { get; set; }
+ [JsonProperty("preferred_username")]
+ public string PreferredUsername { get; set; } // The numeric internal username, same as Username
+ [JsonProperty("given_name")]
+ public string GivenName { get; set; }
+ [JsonProperty("family_name")]
+ public string FamilyName { get; set; }
+ [JsonProperty("email")]
+ public string Email { get; set; }
+ [JsonProperty("username")]
+ public string Username { get; set; } // The numeric internal username, same as PreferredUsername
+ }
+ }
+
+}
+
diff --git a/Duplicati/Library/Backend/Jottacloud/Strings.cs b/Duplicati/Library/Backend/Jottacloud/Strings.cs
index 08ba011b8..010050022 100644
--- a/Duplicati/Library/Backend/Jottacloud/Strings.cs
+++ b/Duplicati/Library/Backend/Jottacloud/Strings.cs
@@ -4,15 +4,12 @@ namespace Duplicati.Library.Backend.Strings {
{
public static string DisplayName { get { return LC.L(@"Jottacloud"); } }
public static string Description { get { return LC.L(@"This backend can read and write data to Jottacloud using it's REST protocol. Allowed format is ""jottacloud://folder/subfolder""."); } }
- public static string NoUsernameError { get { return LC.L(@"No username given"); } }
- public static string NoPasswordError { get { return LC.L(@"No password given"); } }
+ public static string AuthidShort { get { return LC.L(@"The authorization code"); } }
+ public static string AuthidLong(string url) { return LC.L(@"The authorization token retrieved from {0}", url); }
+ public static string NoUsernameError { get { return LC.L(@"No username found"); } }
public static string NoPathError { get { return LC.L(@"No path given, cannot upload files to the root folder"); } }
public static string IllegalMountPoint { get { return LC.L(@"Illegal mount point given."); } }
public static string FileUploadError { get { return LC.L(@"Failed to upload file"); } }
- public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
- public static string DescriptionAuthUsernameLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
- public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
- public static string DescriptionAuthPasswordLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string DescriptionDeviceShort { get { return LC.L(@"Supplies the backup device to use"); } }
public static string DescriptionDeviceLong(string mountPointOption) { return LC.L(@"The backup device to use. Will be created if not already exists. You can manage your devices from the backup panel in the Jottacloud web interface. When you specify a custom device you should also specify the mount point to use on this device with the ""{0}"" option.", mountPointOption); }
public static string DescriptionMountPointShort { get { return LC.L(@"Supplies the mount point to use on the server"); } }
diff --git a/Duplicati/Library/Backend/Jottacloud/packages.config b/Duplicati/Library/Backend/Jottacloud/packages.config
new file mode 100644
index 000000000..a75532f5f
--- /dev/null
+++ b/Duplicati/Library/Backend/Jottacloud/packages.config
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Newtonsoft.Json" version="12.0.2" targetFramework="net472" />
+</packages> \ No newline at end of file
diff --git a/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js b/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js
index 8a103104e..c9abb3ebf 100644
--- a/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js
+++ b/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js
@@ -25,15 +25,15 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
EditUriBackendConfig.templates['gcs'] = 'templates/backends/gcs.html';
EditUriBackendConfig.templates['b2'] = 'templates/backends/b2.html';
EditUriBackendConfig.templates['mega'] = 'templates/backends/mega.html';
+ EditUriBackendConfig.templates['jottacloud'] = 'templates/backends/oauth.html';
EditUriBackendConfig.templates['idrive'] = 'templates/backends/idrive.html';
- EditUriBackendConfig.templates['jottacloud'] = 'templates/backends/jottacloud.html';
EditUriBackendConfig.templates['box'] = 'templates/backends/oauth.html';
- EditUriBackendConfig.templates['dropbox'] = 'templates/backends/oauth.html';
- EditUriBackendConfig.templates['sia'] = 'templates/backends/sia.html';
- EditUriBackendConfig.templates['storj'] = 'templates/backends/storj.html';
+ EditUriBackendConfig.templates['dropbox'] = 'templates/backends/oauth.html';
+ EditUriBackendConfig.templates['sia'] = 'templates/backends/sia.html';
+ EditUriBackendConfig.templates['storj'] = 'templates/backends/storj.html';
EditUriBackendConfig.templates['tardigrade'] = 'templates/backends/tardigrade.html';
- EditUriBackendConfig.templates['rclone'] = 'templates/backends/rclone.html';
- EditUriBackendConfig.templates['cos'] = 'templates/backends/cos.html';
+ EditUriBackendConfig.templates['rclone'] = 'templates/backends/rclone.html';
+ EditUriBackendConfig.templates['cos'] = 'templates/backends/cos.html';
EditUriBackendConfig.testers['s3'] = function(scope, callback) {
@@ -268,6 +268,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
EditUriBackendConfig.loaders['onedrivev2'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.loaders['sharepoint'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.loaders['msgroup'] = function() { return this['oauth-base'].apply(this, arguments); };
+ EditUriBackendConfig.loaders['jottacloud'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.loaders['box'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.loaders['dropbox'] = function() { return this['oauth-base'].apply(this, arguments); };
@@ -422,6 +423,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
EditUriBackendConfig.parsers['onedrive'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.parsers['onedrivev2'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.parsers['sharepoint'] = function() { return this['oauth-base'].apply(this, arguments); };
+ EditUriBackendConfig.parsers['jottacloud'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.parsers['box'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.parsers['dropbox'] = function() { return this['oauth-base'].apply(this, arguments); };
@@ -483,10 +485,6 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
EditUriBackendConfig.mergeServerAndPath(scope);
};
- EditUriBackendConfig.parsers['jottacloud'] = function (scope, module, server, port, path, options) {
- EditUriBackendConfig.mergeServerAndPath(scope);
- };
-
EditUriBackendConfig.parsers['rclone'] = function (scope, module, server, port, path, options) {
if (options['--rclone-local-repository'])
scope.rclone_local_repository = options['--rclone-local-repository'];
@@ -644,6 +642,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
EditUriBackendConfig.builders['onedrivev2'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.builders['sharepoint'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.builders['msgroup'] = function() { return this['oauth-base'].apply(this, arguments); };
+ EditUriBackendConfig.builders['jottacloud'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.builders['box'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.builders['dropbox'] = function() { return this['oauth-base'].apply(this, arguments); };
@@ -866,23 +865,6 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
}
- EditUriBackendConfig.builders['jottacloud'] = function (scope) {
- var opts = {};
-
- EditUriBackendConfig.merge_in_advanced_options(scope, opts);
-
- // Slightly better error message
- scope.Folder = scope.Path;
-
- var url = AppUtils.format('{0}://{1}{2}',
- scope.Backend.Key,
- scope.Path,
- AppUtils.encodeDictAsUrl(opts)
- );
-
- return url;
- };
-
EditUriBackendConfig.builders['cos'] = function (scope) {
var opts = {
@@ -949,6 +931,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
EditUriBackendConfig.validaters['googledrive'] = EditUriBackendConfig.validaters['authid-base'];
EditUriBackendConfig.validaters['gcs'] = EditUriBackendConfig.validaters['authid-base'];
+ EditUriBackendConfig.validaters['jottacloud'] = EditUriBackendConfig.validaters['authid-base'];
EditUriBackendConfig.validaters['box'] = EditUriBackendConfig.validaters['authid-base'];
EditUriBackendConfig.validaters['dropbox'] = EditUriBackendConfig.validaters['authid-base'];
EditUriBackendConfig.validaters['onedrive'] = EditUriBackendConfig.validaters['authid-base'];
@@ -1158,16 +1141,6 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
continuation();
};
- EditUriBackendConfig.validaters['jottacloud'] = function (scope, continuation) {
- scope.Path = scope.Path || '';
- var res =
- EditUriBackendConfig.require_field(scope, 'Username', gettextCatalog.getString('Username')) &&
- EditUriBackendConfig.require_field(scope, 'Password', gettextCatalog.getString('Password'));
-
- if (res)
- continuation();
- };
-
EditUriBackendConfig.validaters['sia'] = function (scope, continuation) {
var res =
EditUriBackendConfig.require_field(scope, 'Server', gettextCatalog.getString('Server'));
diff --git a/Duplicati/Server/webroot/ngax/templates/backends/jottacloud.html b/Duplicati/Server/webroot/ngax/templates/backends/jottacloud.html
index 04efe6d28..dcff7dab5 100644
--- a/Duplicati/Server/webroot/ngax/templates/backends/jottacloud.html
+++ b/Duplicati/Server/webroot/ngax/templates/backends/jottacloud.html
@@ -1,13 +1,12 @@
<div class="input text">
<label for="jottacloud_path" translate>Folder path</label>
- <input type="text" name="jottacloud_path" id="jottacloud_path" ng-model="$parent.Path" placeholder="{{'Enter folder path name' | translate}}" />
+ <input type="text" name="jottacloud_path" id="jottacloud_path" ng-model="$parent.Path" placeholder="{{'Enter the destination path' | translate}}" />
</div>
<div class="input text">
- <label for="jottacloud_username" translate>Username</label>
- <input type="text" name="jottacloud_username" id="jottacloud_username" ng-model="$parent.Username" placeholder="{{'Username' | translate}}" />
-</div>
-<div class="input password">
- <label for="jottacloud_password" translate>Password</label>
- <input autocomplete="new-password" type="password" name="jottacloud_password" id="jottacloud_password" ng-model="$parent.Password" placeholder="{{'Password' | translate}}" />
+ <label for="jottacloud_authid">
+ <a ng-show="oauth_in_progress" href="{{oauth_start_link}}" target="_blank" translate>AuthID</a>
+ <a ng-hide="oauth_in_progress" href ng-click="oauth_start_token_creation()" translate>AuthID</a>
+ </label>
+ <input type="text" name="jottacloud_authid" id="jottacloud_authid" ng-model="$parent.AuthID" placeholder="{{'Click the AuthID link to create an AuthID' | translate}}" />
</div>