Implemented a forever-token feature

This commit is contained in:
Kenneth Skovhede
2024-12-12 08:59:59 +01:00
parent 4eef98857f
commit 7d4f9ffab3
10 changed files with 70 additions and 0 deletions
@@ -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];
+1
View File
@@ -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"); } }
+4
View File
@@ -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),
+5
View File
@@ -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 &quot;forever&quot;.
/// </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()),