Implemented a forever-token feature
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.NamingConventionBinder;
|
||||
|
||||
namespace Duplicati.CommandLine.ServerUtil.Commands;
|
||||
|
||||
public static class IssueForeverToken
|
||||
{
|
||||
public static Command Create() =>
|
||||
new Command("issue-forever-token", "Issues a long-lived access token")
|
||||
{
|
||||
}
|
||||
.WithHandler(CommandHandler.Create<Settings>(async (settings) =>
|
||||
{
|
||||
var token = await (await settings.GetConnection()).CreateForeverToken();
|
||||
Console.WriteLine("Token issued:");
|
||||
Console.WriteLine(token);
|
||||
Console.WriteLine();
|
||||
}));
|
||||
}
|
||||
@@ -453,6 +453,19 @@ public class Connection
|
||||
return await response.Content.ReadAsStreamAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a forever token
|
||||
/// </summary>
|
||||
/// <returns>The token</returns>
|
||||
public async Task<string> CreateForeverToken()
|
||||
{
|
||||
var response = await client.PostAsync($"auth/issue-forever-token", null);
|
||||
await EnsureSuccessStatusCodeWithParsing(response);
|
||||
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The server error structure for JSON deserialization
|
||||
/// </summary>
|
||||
|
||||
@@ -31,6 +31,7 @@ public static class Program
|
||||
Import.Create(),
|
||||
Export.Create(),
|
||||
Health.Create(),
|
||||
IssueForeverToken.Create(),
|
||||
};
|
||||
|
||||
rootCmd = SettingsBinder.AddGlobalOptions(rootCmd);
|
||||
|
||||
@@ -493,6 +493,10 @@ namespace Duplicati.Server.Database
|
||||
}
|
||||
}
|
||||
|
||||
// Enabling forever-tokens is not persisted in the database,
|
||||
// as it is meant to be a temporary setting
|
||||
public bool EnableForeverTokens { get; set; } = false;
|
||||
|
||||
public string RemoteControlConfig
|
||||
{
|
||||
get => settings[CONST.REMOTE_CONTROL_CONFIG];
|
||||
|
||||
@@ -48,6 +48,7 @@ Error message: {0}", error); }
|
||||
public static string TempdirLong { get { return LC.L(@"Use this option to supply an alternative folder for temporary storage. By default the system default temporary folder is used. Note that also SQLite will put temporary files in this temporary folder."); } }
|
||||
public static string TempdirShort { get { return LC.L(@"Temporary storage folder"); } }
|
||||
public static string WebserverResetJwtConfigDescription { get { return LC.L(@"Reset the JWT configuration, invalidating any issued login tokens"); } }
|
||||
public static string WebserverEnableForeverTokenDescription { get { return LC.L(@"Enable the use of long-lived access tokens"); } }
|
||||
public static string WebserverDisableVisualCaptchaDescription { get { return LC.L(@"Disable the visual captcha"); } }
|
||||
public static string WebserverApiOnlyDescription { get { return LC.L(@"Disable the web interface and only allow API access"); } }
|
||||
public static string WebserverDisableSigninTokensDescription { get { return LC.L(@"Disable the use of signin tokens"); } }
|
||||
|
||||
@@ -533,6 +533,9 @@ namespace Duplicati.Server
|
||||
DataConnection.ExecuteWithCommand((con) => con.ExecuteNonQuery("DELETE FROM TokenFamily"));
|
||||
}
|
||||
|
||||
if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, WebServerLoader.OPTION_WEBSERVICE_ENABLE_FOREVER_TOKEN))
|
||||
DataConnection.ApplicationSettings.EnableForeverTokens = true;
|
||||
|
||||
if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_DISABLE_VISUAL_CAPTCHA))
|
||||
DataConnection.ApplicationSettings.DisableVisualCaptcha = Library.Utility.Utility.ParseBool(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_DISABLE_VISUAL_CAPTCHA], true);
|
||||
|
||||
@@ -947,6 +950,7 @@ namespace Duplicati.Server
|
||||
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_PASSWORD, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Password, Strings.Program.WebserverPasswordDescription, Strings.Program.WebserverPasswordDescription),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverAllowedhostnamesDescription, Strings.Program.WebserverAllowedhostnamesDescription, null, [WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES_ALT]),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_RESET_JWT_CONFIG, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverResetJwtConfigDescription, Strings.Program.WebserverResetJwtConfigDescription),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_ENABLE_FOREVER_TOKEN, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverEnableForeverTokenDescription, Strings.Program.WebserverEnableForeverTokenDescription),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_DISABLE_VISUAL_CAPTCHA, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverDisableVisualCaptchaDescription, Strings.Program.WebserverDisableVisualCaptchaDescription),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_API_ONLY, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverApiOnlyDescription, Strings.Program.WebserverApiOnlyDescription),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_DISABLE_SIGNIN_TOKENS, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverDisableSigninTokensDescription, Strings.Program.WebserverDisableSigninTokensDescription),
|
||||
|
||||
@@ -65,6 +65,11 @@ public static class WebServerLoader
|
||||
/// </summary>
|
||||
public const string OPTION_WEBSERVICE_RESET_JWT_CONFIG = "webservice-reset-jwt-config";
|
||||
|
||||
/// <summary>
|
||||
/// Option for enabling the forever token
|
||||
/// </summary>
|
||||
public const string OPTION_WEBSERVICE_ENABLE_FOREVER_TOKEN = "webservice-enable-forver-token";
|
||||
|
||||
/// <summary>
|
||||
/// Option for disabling the visual captcha
|
||||
/// </summary>
|
||||
|
||||
@@ -57,13 +57,20 @@ public interface IJWTTokenProvider
|
||||
/// <param name="userId">The user ID the token is for.</param>
|
||||
/// <param name="tokenFamilyId">The token family ID the token is for.</param>
|
||||
/// <param name="expiration">The expiration time of the token, can only be shorter than the current.</param>
|
||||
/// <returns>The JWT token.</returns>
|
||||
string CreateAccessToken(string userId, string tokenFamilyId, TimeSpan? expiration = null);
|
||||
/// <summary>
|
||||
/// Creates a JWT token that can be used to access resources "forever".
|
||||
/// </summary>
|
||||
/// <returns>The JWT token.</returns>
|
||||
string CreateForeverToken();
|
||||
/// <summary>
|
||||
/// Creates a JWT token that can be used to refresh an access token.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user ID the token is for.</param>
|
||||
/// <param name="tokenFamilyId">The token family ID the token is for.</param>
|
||||
/// <param name="counter">The counter of the token family the token is for.</param>
|
||||
/// <returns>The JWT token.</returns>
|
||||
string CreateRefreshToken(string userId, string tokenFamilyId, int counter);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -134,6 +134,14 @@ public partial class Auth : IEndpointV1
|
||||
var singleOperationToken = tokenProvider.CreateSingleOperationToken("web-api", operation);
|
||||
return new Dto.SingleOperationTokenOutputDto(singleOperationToken);
|
||||
}).RequireAuthorization();
|
||||
|
||||
group.MapPost("auth/issue-forever-token", ([FromServices] Connection connection, [FromServices] IJWTTokenProvider tokenProvider) =>
|
||||
{
|
||||
if (!connection.ApplicationSettings.EnableForeverTokens)
|
||||
throw new UnauthorizedException("Forever tokens are not enabled");
|
||||
|
||||
return new Dto.AccessTokenOutput(tokenProvider.CreateForeverToken());
|
||||
}).RequireAuthorization();
|
||||
}
|
||||
|
||||
private static void AddCookie(HttpContext context, string name, string value, DateTimeOffset expires)
|
||||
|
||||
@@ -32,6 +32,7 @@ public record JWTConfig
|
||||
public class JWTTokenProvider(JWTConfig jWTConfig) : IJWTTokenProvider
|
||||
{
|
||||
private const string TemporaryFamilyId = "temporary";
|
||||
private const string ForeverTokenUserId = "forever-token";
|
||||
public string CreateSingleOperationToken(string userId, string operation)
|
||||
=> GenerateToken([
|
||||
new Claim(Claims.Type, TokenType.SingleOperationToken.ToString()),
|
||||
@@ -52,6 +53,13 @@ public class JWTTokenProvider(JWTConfig jWTConfig) : IJWTTokenProvider
|
||||
new Claim(Claims.Family, tokenFamilyId)
|
||||
], DateTime.Now, expires: DateTime.Now.AddMinutes(Math.Min(jWTConfig.AccessTokenDurationInMinutes, expiration?.TotalMinutes ?? jWTConfig.AccessTokenDurationInMinutes)));
|
||||
|
||||
public string CreateForeverToken()
|
||||
=> GenerateToken([
|
||||
new Claim(Claims.Type, TokenType.AccessToken.ToString()),
|
||||
new Claim(Claims.UserId, ForeverTokenUserId),
|
||||
new Claim(Claims.Family, TemporaryFamilyId)
|
||||
], DateTime.Now, expires: DateTime.Now.AddYears(10));
|
||||
|
||||
public string CreateRefreshToken(string userId, string tokenFamilyId, int counter)
|
||||
=> GenerateToken([
|
||||
new Claim(Claims.Type, TokenType.RefreshToken.ToString()),
|
||||
|
||||
Reference in New Issue
Block a user