diff --git a/Descope.Test/IntegrationTests/Authentication/AuthenticationTests.cs b/Descope.Test/IntegrationTests/Authentication/AuthenticationTests.cs index 922b065..c33399c 100644 --- a/Descope.Test/IntegrationTests/Authentication/AuthenticationTests.cs +++ b/Descope.Test/IntegrationTests/Authentication/AuthenticationTests.cs @@ -12,7 +12,6 @@ public async Task Authentication_ValidateAndRefresh() string? loginId = null; try { - await _descopeClient.Management.User.DeleteAllTestUsers(); // Create a logged in test user var testUser = await IntegrationTestSetup.InitTestUser(_descopeClient); loginId = testUser.User.LoginIds.First(); diff --git a/Descope.Test/IntegrationTests/Management/JwtTests.cs b/Descope.Test/IntegrationTests/Management/JwtTests.cs new file mode 100644 index 0000000..29a86e0 --- /dev/null +++ b/Descope.Test/IntegrationTests/Management/JwtTests.cs @@ -0,0 +1,93 @@ +using Xunit; + +namespace Descope.Test.Integration +{ + public class JwtTests + { + private readonly DescopeClient _descopeClient = IntegrationTestSetup.InitDescopeClient(); + + [Fact] + public async Task Jwt_CustomClaims() + { + string? loginId = null; + try + { + // Create a logged in test user + var testUser = await IntegrationTestSetup.InitTestUser(_descopeClient); + loginId = testUser.User.LoginIds.First(); + + var updateJwt = await _descopeClient.Management.Jwt.UpdateJwtWithCustomClaims(testUser.AuthInfo.SessionJwt, new Dictionary { { "a", "b" } }); + + // Make sure the session is valid + var token = await _descopeClient.Auth.ValidateSession(updateJwt); + Assert.Contains("a", token.Claims.Keys); + Assert.Equal("b", token.Claims["a"]); + } + finally + { + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + } + } + + [Fact] + public async Task Jwt_Impersonate() + { + string? loginId = null; + string? loginId2 = null; + string? roleName = null; + try + { + // Create a role that can impersonate + roleName = Guid.NewGuid().ToString()[..20]; + await _descopeClient.Management.Role.Create(roleName, permissionNames: new List { "Impersonate" }); + + // Create impersonating user + loginId = Guid.NewGuid().ToString(); + var response = await _descopeClient.Management.User.Create(loginId: loginId, new UserRequest() + { + Phone = "+972555555555", + RoleNames = new List { roleName }, + }); + var userId1 = response.UserId; + + // Create the target user + loginId2 = Guid.NewGuid().ToString(); + response = await _descopeClient.Management.User.Create(loginId: loginId2, new UserRequest() + { + Phone = "+972666666666", + }); + var userId2 = response.UserId; + + // Have user1 impersonate user2 + var jwt = await _descopeClient.Management.Jwt.Impersonate(userId1, loginId2); + + // Validate the impersonation data + var token = await _descopeClient.Auth.ValidateSession(jwt); + Assert.Equal(userId2, token.Id); + Assert.Contains(userId1, token.Claims["act"].ToString()); + } + finally + { + if (!string.IsNullOrEmpty(roleName)) + { + try { await _descopeClient.Management.Role.Delete(roleName); } + catch { } + } + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + if (!string.IsNullOrEmpty(loginId2)) + { + try { await _descopeClient.Management.User.Delete(loginId2); } + catch { } + } + } + } + } +} diff --git a/Descope/Internal/Http/Routes.cs b/Descope/Internal/Http/Routes.cs index 4c6c3a4..4c27bb0 100644 --- a/Descope/Internal/Http/Routes.cs +++ b/Descope/Internal/Http/Routes.cs @@ -81,6 +81,7 @@ public static class Routes #region JWT public const string JwtUpdate = "/v1/mgmt/jwt/update"; + public const string Impersonate = "/v1/mgmt/impersonate"; #endregion JWT diff --git a/Descope/Internal/Management/Jwt.cs b/Descope/Internal/Management/Jwt.cs new file mode 100644 index 0000000..aaf40ff --- /dev/null +++ b/Descope/Internal/Management/Jwt.cs @@ -0,0 +1,46 @@ +using System.Text.Json.Serialization; + +namespace Descope.Internal.Management +{ + internal class Jwt : IJwt + { + private readonly IHttpClient _httpClient; + private readonly string _managementKey; + + internal Jwt(IHttpClient httpClient, string managementKey) + { + _httpClient = httpClient; + _managementKey = managementKey; + } + + public async Task UpdateJwtWithCustomClaims(string jwt, Dictionary customClaims) + { + if (string.IsNullOrEmpty(jwt)) throw new DescopeException("JWT is required to update custom claims"); + var body = new { jwt, customClaims }; + var response = await _httpClient.Post(Routes.JwtUpdate, _managementKey, body); + return response.Jwt; + } + + public async Task Impersonate(string impersonatorId, string loginId, bool validateConcent) + { + if (string.IsNullOrEmpty(impersonatorId)) throw new DescopeException("impersonatorId is required to impersonate"); + if (string.IsNullOrEmpty(loginId)) throw new DescopeException("impersonatorId is required to impersonate"); + var body = new { impersonatorId, loginId, validateConcent }; + var response = await _httpClient.Post(Routes.Impersonate, _managementKey, body); + return response.Jwt; + } + + } + + internal class SimpleJwtResponse + { + [JsonPropertyName("jwt")] + public string Jwt { get; set; } + + public SimpleJwtResponse(string jwt) + { + Jwt = jwt; + } + } + +} diff --git a/Descope/Internal/Management/Managment.cs b/Descope/Internal/Management/Managment.cs index c4e4e0e..6762640 100644 --- a/Descope/Internal/Management/Managment.cs +++ b/Descope/Internal/Management/Managment.cs @@ -5,6 +5,7 @@ internal class Management : IManagement public ITenant Tenant => _tenant; public IUser User => _user; public IAccessKey AccessKey => _accessKey; + public IJwt Jwt => _jwt; public IPermission Permission => _permission; public IRole Role => _role; public IProject Project => _project; @@ -12,6 +13,7 @@ internal class Management : IManagement private readonly Tenant _tenant; private readonly User _user; private readonly AccessKey _accessKey; + private readonly Jwt _jwt; private readonly Permission _permission; private readonly Role _role; private readonly Project _project; @@ -21,6 +23,7 @@ public Management(IHttpClient client, string managementKey) _tenant = new Tenant(client, managementKey); _user = new User(client, managementKey); _accessKey = new AccessKey(client, managementKey); + _jwt = new Jwt(client, managementKey); _permission = new Permission(client, managementKey); _role = new Role(client, managementKey); _project = new Project(client, managementKey); diff --git a/Descope/Sdk/Managment.cs b/Descope/Sdk/Managment.cs index 9ad7239..0f6aa35 100644 --- a/Descope/Sdk/Managment.cs +++ b/Descope/Sdk/Managment.cs @@ -620,6 +620,18 @@ public interface IJwt /// The custom claims to be added to the JWT /// An updated JWT Task UpdateJwtWithCustomClaims(string jwt, Dictionary customClaims); + + /// + /// Impersonate another user + /// + /// The impersonator user must have the Impersonation permission in order for this request to work + /// + /// + /// The user ID performing the impersonation + /// The login ID of the user being impersonated + /// Whether to validate concent or not + /// A refresh JWT for the impersonated user + Task Impersonate(string impersonatorId, string loginId, bool validateConcent = false); } /// @@ -691,6 +703,11 @@ public interface IManagement /// public IAccessKey AccessKey { get; } + /// + /// Provides functions for manipulating valid JWTs + /// + public IJwt Jwt { get; } + /// /// Provides functions for managing permissions in a project. /// diff --git a/README.md b/README.md index 3d05712..ed3eac9 100644 --- a/README.md +++ b/README.md @@ -455,6 +455,27 @@ var loginOptions = new AccessKeyLoginOptions var token = await descopeClient.Auth.ExchangeAccessKey("accessKey", loginOptions); ``` +### Manage and Manipulate JWTs + +You can update custom claims on a valid JWT or even impersonate a different user - as long as the +impersonating user has the permission to do so. + +```cs +try +{ + // add custom claims to a valid JWT + var updatedJwt = await _descopeClient.Management.Jwt.UpdateJwtWithCustomClaims(jwt, myCustomClaims); + + // impersonate a user, assuming the user identified by `impersonatorId` has the `Impersonation` permission. + // the resulting JWT will have the actor (`act`) custom claim identifying the impersonator + var impersonatedJwt = await _descopeClient.Management.Jwt.Impersonate(impersonatorId, impersonatedLoginId); +} +catch (DescopeException e) +{ + // handle errors +} +``` + ### Manage Permissions You can create, update, delete or load permissions: