Skip to content

Commit

Permalink
Implement JWT management API (#14)
Browse files Browse the repository at this point in the history
* Implement JWT management API

* PR fixes
  • Loading branch information
itaihanski authored Mar 26, 2024
1 parent 0e51344 commit 9d81a36
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
93 changes: 93 additions & 0 deletions Descope.Test/IntegrationTests/Management/JwtTests.cs
Original file line number Diff line number Diff line change
@@ -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<string, object> { { "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<string> { "Impersonate" });

// Create impersonating user
loginId = Guid.NewGuid().ToString();
var response = await _descopeClient.Management.User.Create(loginId: loginId, new UserRequest()
{
Phone = "+972555555555",
RoleNames = new List<string> { 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 { }
}
}
}
}
}
1 change: 1 addition & 0 deletions Descope/Internal/Http/Routes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
46 changes: 46 additions & 0 deletions Descope/Internal/Management/Jwt.cs
Original file line number Diff line number Diff line change
@@ -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<string> UpdateJwtWithCustomClaims(string jwt, Dictionary<string, object> 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<SimpleJwtResponse>(Routes.JwtUpdate, _managementKey, body);
return response.Jwt;
}

public async Task<string> 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<SimpleJwtResponse>(Routes.Impersonate, _managementKey, body);
return response.Jwt;
}

}

internal class SimpleJwtResponse
{
[JsonPropertyName("jwt")]
public string Jwt { get; set; }

public SimpleJwtResponse(string jwt)
{
Jwt = jwt;
}
}

}
3 changes: 3 additions & 0 deletions Descope/Internal/Management/Managment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ 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;

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;
Expand All @@ -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);
Expand Down
17 changes: 17 additions & 0 deletions Descope/Sdk/Managment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,18 @@ public interface IJwt
/// <param name="customClaims">The custom claims to be added to the JWT</param>
/// <returns>An updated JWT</returns>
Task<string> UpdateJwtWithCustomClaims(string jwt, Dictionary<string, object> customClaims);

/// <summary>
/// Impersonate another user
/// <para>
/// The impersonator user must have the <c>Impersonation</c> permission in order for this request to work
/// </para>
/// </summary>
/// <param name="impersonatorId">The user ID performing the impersonation</param>
/// <param name="loginId">The login ID of the user being impersonated</param>
/// <param name="validateConcent">Whether to validate concent or not</param>
/// <returns>A refresh JWT for the impersonated user</returns>
Task<string> Impersonate(string impersonatorId, string loginId, bool validateConcent = false);
}

/// <summary>
Expand Down Expand Up @@ -691,6 +703,11 @@ public interface IManagement
/// </summary>
public IAccessKey AccessKey { get; }

/// <summary>
/// Provides functions for manipulating valid JWTs
/// </summary>
public IJwt Jwt { get; }

/// <summary>
/// Provides functions for managing permissions in a project.
/// </summary>
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 9d81a36

Please sign in to comment.