The Online API now requires the "user" project role (claim urn:zitadel:iam:org:project:roles) instead of an ALLOWED_USER_IDS allowlist. - IOnlineAuthProvider: add GetAccessTokenAsync(forceRefresh) overload - ZitadelAuthProvider: forceRefresh drops the cached token and re-runs the refresh-token grant to mint a fresh, role-bearing token - OnlineInboxApiClient: on 401, force-refresh and retry once; if still 401, throw a clear "missing 'user' role" error - OnlineSyncService: surface the 401 at Error level (no longer silent) - UI: ZitadelTokenInspector decodes the access token after login and warns early when the "user" role is absent (fail-open); shown in settings - docs: online-inbox-api-contract reflects role-based access (no allowlist) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 lines
1.9 KiB
C#
63 lines
1.9 KiB
C#
using System;
|
|
using System.Text;
|
|
using ClaudeDo.Ui.Services;
|
|
using Xunit;
|
|
|
|
namespace ClaudeDo.Ui.Tests.Services;
|
|
|
|
public class ZitadelTokenInspectorTests
|
|
{
|
|
// Builds a fake JWT (header.payload.signature) carrying the given JSON payload.
|
|
private static string MakeToken(string payloadJson)
|
|
{
|
|
static string B64Url(string s)
|
|
{
|
|
var bytes = Encoding.UTF8.GetBytes(s);
|
|
return Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');
|
|
}
|
|
return $"{B64Url("{\"alg\":\"RS256\"}")}.{B64Url(payloadJson)}.sig";
|
|
}
|
|
|
|
[Fact]
|
|
public void HasUserRole_True_ForGenericRolesClaim()
|
|
{
|
|
var token = MakeToken(
|
|
"{\"urn:zitadel:iam:org:project:roles\":{\"user\":{\"org1\":\"example.com\"}}}");
|
|
Assert.True(ZitadelTokenInspector.HasUserRole(token));
|
|
}
|
|
|
|
[Fact]
|
|
public void HasUserRole_True_ForProjectScopedRolesClaim()
|
|
{
|
|
var token = MakeToken(
|
|
"{\"urn:zitadel:iam:org:project:376787351902355727:roles\":{\"user\":{\"org1\":\"example.com\"}}}");
|
|
Assert.True(ZitadelTokenInspector.HasUserRole(token));
|
|
}
|
|
|
|
[Fact]
|
|
public void HasUserRole_False_WhenRoleMissing()
|
|
{
|
|
var token = MakeToken(
|
|
"{\"urn:zitadel:iam:org:project:roles\":{\"admin\":{\"org1\":\"example.com\"}}}");
|
|
Assert.False(ZitadelTokenInspector.HasUserRole(token));
|
|
}
|
|
|
|
[Fact]
|
|
public void HasUserRole_False_WhenNoRolesClaim()
|
|
{
|
|
var token = MakeToken("{\"sub\":\"123\",\"email\":\"a@b.c\"}");
|
|
Assert.False(ZitadelTokenInspector.HasUserRole(token));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData("not-a-jwt")]
|
|
[InlineData("only.two")]
|
|
public void HasUserRole_FailsOpen_ForUnparseableInput(string? token)
|
|
{
|
|
// Cannot decide -> fail open (server remains the source of truth).
|
|
Assert.True(ZitadelTokenInspector.HasUserRole(token));
|
|
}
|
|
}
|