test(bff): endpoints, JWT auth and public-safe projection (refs #8)
Failing tests for the BFF walking-skeleton endpoints: - POST /self-service/registrations rejects missing/malformed/wrong-key/expired tokens (401) and, with a valid digid token, forwards the bsn to the domain and returns 202 (WebApplicationFactory + a local test signing key, ADR-0010). - GET /openbaar/register serves public-safe rows anonymously (never the bsn) and filters by q. - OpenbaarProjection.PublicView (pure) filters by id and maps to id+status only. Endpoints and PublicView are stubs so the tests compile and fail on their assertions; the green commit implements them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,4 +6,10 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- OIDC/JWT validation of Keycloak-issued tokens (ADR-0010) and OpenAPI generation. -->
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.8" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
47
services/bff/Bff.Api/DownstreamClients.cs
Normal file
47
services/bff/Bff.Api/DownstreamClients.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
|
||||||
|
namespace Bff.Api;
|
||||||
|
|
||||||
|
/// <summary>What the self-service submit returns to the portal (the domain's registration id + status).</summary>
|
||||||
|
public sealed record SubmitAccepted(string RegistrationId, string Status);
|
||||||
|
|
||||||
|
/// <summary>A projection row as the projection-api serves it. <c>Bsn</c>/<c>NaamPlaceholder</c> are
|
||||||
|
/// read but never surfaced by the openbaar endpoint (public-safe filtering, ADR-0010/S-09).</summary>
|
||||||
|
public sealed record ProjectionEntry(string Id, string Status, string? Bsn, string? NaamPlaceholder);
|
||||||
|
|
||||||
|
/// <summary>A public-safe openbaar register row — only non-sensitive fields leave the BFF.</summary>
|
||||||
|
public sealed record OpenbaarEntry(string Id, string Status);
|
||||||
|
|
||||||
|
/// <summary>Port to the Domain Service (§8.3: the BFF is the portals' only backend; it fans out).</summary>
|
||||||
|
public interface IDomainClient
|
||||||
|
{
|
||||||
|
Task<SubmitAccepted> SubmitRegistrationAsync(string bsn, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Port to the read projection.</summary>
|
||||||
|
public interface IProjectionClient
|
||||||
|
{
|
||||||
|
Task<IReadOnlyList<ProjectionEntry>> GetRegisterAsync(CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Calls the Domain Service's <c>POST /registrations</c>.</summary>
|
||||||
|
public sealed class DomainClient(HttpClient http) : IDomainClient
|
||||||
|
{
|
||||||
|
public async Task<SubmitAccepted> SubmitRegistrationAsync(string bsn, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
using var response = await http.PostAsJsonAsync("registrations", new { bsn }, ct);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var dto = await response.Content.ReadFromJsonAsync<DomainResponse>(ct)
|
||||||
|
?? throw new InvalidOperationException("The Domain Service returned an empty registration response.");
|
||||||
|
return new SubmitAccepted(dto.RegistrationId, dto.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record DomainResponse(string RegistrationId, string Status, string? ZaakUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Calls the projection-api's <c>GET /register</c>.</summary>
|
||||||
|
public sealed class ProjectionClient(HttpClient http) : IProjectionClient
|
||||||
|
{
|
||||||
|
public async Task<IReadOnlyList<ProjectionEntry>> GetRegisterAsync(CancellationToken ct = default)
|
||||||
|
=> await http.GetFromJsonAsync<List<ProjectionEntry>>("register", ct) ?? [];
|
||||||
|
}
|
||||||
15
services/bff/Bff.Api/OpenbaarProjection.cs
Normal file
15
services/bff/Bff.Api/OpenbaarProjection.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Bff.Api;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The public view of the read projection: filters rows by the openbaar search term and maps each to
|
||||||
|
/// a public-safe <see cref="OpenbaarEntry"/> (only <c>id</c> + <c>status</c> — bsn/naam never leave the
|
||||||
|
/// BFF). Pure so it is unit- and mutation-tested directly (ADR-0010).
|
||||||
|
/// </summary>
|
||||||
|
public static class OpenbaarProjection
|
||||||
|
{
|
||||||
|
public static IReadOnlyList<OpenbaarEntry> PublicView(IReadOnlyList<ProjectionEntry> entries, string? q)
|
||||||
|
{
|
||||||
|
// STUB (red): returns nothing until the green commit implements filter + public-safe mapping.
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,44 @@
|
|||||||
|
using Bff.Api;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
var keycloakAuthority = builder.Configuration["Keycloak:Authority"]
|
||||||
|
?? throw new InvalidOperationException("Missing configuration 'Keycloak:Authority'");
|
||||||
|
var domainBaseUrl = builder.Configuration["Downstream:Domain:BaseUrl"]
|
||||||
|
?? throw new InvalidOperationException("Missing configuration 'Downstream:Domain:BaseUrl'");
|
||||||
|
var projectionBaseUrl = builder.Configuration["Downstream:Projection:BaseUrl"]
|
||||||
|
?? throw new InvalidOperationException("Missing configuration 'Downstream:Projection:BaseUrl'");
|
||||||
|
|
||||||
|
// Validate Keycloak-issued tokens (ADR-0010). Audience validation is off for the walking skeleton —
|
||||||
|
// Keycloak's audience mapping is a later hardening; signature/issuer/expiry are validated.
|
||||||
|
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.Authority = keycloakAuthority;
|
||||||
|
options.RequireHttpsMetadata = false;
|
||||||
|
options.TokenValidationParameters.ValidateAudience = false;
|
||||||
|
});
|
||||||
|
builder.Services.AddAuthorization();
|
||||||
|
|
||||||
|
// The BFF is the portals' only backend; it fans out to the domain and projection (§8.3).
|
||||||
|
builder.Services.AddHttpClient<IDomainClient, DomainClient>(c => c.BaseAddress = new Uri(domainBaseUrl));
|
||||||
|
builder.Services.AddHttpClient<IProjectionClient, ProjectionClient>(c => c.BaseAddress = new Uri(projectionBaseUrl));
|
||||||
|
|
||||||
builder.Services.AddHealthChecks();
|
builder.Services.AddHealthChecks();
|
||||||
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.MapGet("/", () => "BFF placeholder");
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.MapHealthChecks("/health");
|
app.MapHealthChecks("/health");
|
||||||
|
app.MapOpenApi();
|
||||||
|
|
||||||
|
// STUB (red): no auth requirement, no downstream call, no filtering.
|
||||||
|
app.MapPost("/self-service/registrations", () => Results.Ok());
|
||||||
|
app.MapGet("/openbaar/register", (string? q) => Results.Ok(Array.Empty<OpenbaarEntry>()));
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
|
|||||||
@@ -5,5 +5,12 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*",
|
||||||
|
"Keycloak": {
|
||||||
|
"Authority": "http://localhost:8180/realms/digid"
|
||||||
|
},
|
||||||
|
"Downstream": {
|
||||||
|
"Domain": { "BaseUrl": "http://localhost:8130/" },
|
||||||
|
"Projection": { "BaseUrl": "http://localhost:8120/" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
78
services/bff/Bff.Tests/BffFactory.cs
Normal file
78
services/bff/Bff.Tests/BffFactory.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Bff.Api;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
|
using Microsoft.AspNetCore.TestHost;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace Bff.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test host for the BFF. It swaps the downstream clients for in-memory fakes and reconfigures the
|
||||||
|
/// JWT bearer to validate against a local test key (no live Keycloak) — so token validation is
|
||||||
|
/// exercised in-process with tokens the tests mint (ADR-0010).
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class BffFactory : WebApplicationFactory<Program>
|
||||||
|
{
|
||||||
|
public static readonly SymmetricSecurityKey TestSigningKey =
|
||||||
|
new(Encoding.UTF8.GetBytes("bff-test-signing-key-that-is-at-least-256-bits-long!"));
|
||||||
|
|
||||||
|
public FakeDomainClient Domain { get; } = new();
|
||||||
|
public FakeProjectionClient Projection { get; } = new();
|
||||||
|
|
||||||
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||||
|
{
|
||||||
|
builder.UseSetting("Keycloak:Authority", "https://keycloak.invalid/realms/digid");
|
||||||
|
builder.UseSetting("Downstream:Domain:BaseUrl", "http://domain.invalid/");
|
||||||
|
builder.UseSetting("Downstream:Projection:BaseUrl", "http://projection.invalid/");
|
||||||
|
|
||||||
|
builder.ConfigureTestServices(services =>
|
||||||
|
{
|
||||||
|
services.AddSingleton<IDomainClient>(Domain);
|
||||||
|
services.AddSingleton<IProjectionClient>(Projection);
|
||||||
|
|
||||||
|
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
|
||||||
|
{
|
||||||
|
// Validate locally against the test key; never reach out for OIDC metadata.
|
||||||
|
options.Authority = null;
|
||||||
|
options.MetadataAddress = null!;
|
||||||
|
options.RequireHttpsMetadata = false;
|
||||||
|
options.Configuration = new OpenIdConnectConfiguration();
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = false,
|
||||||
|
ValidateAudience = false,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
IssuerSigningKey = TestSigningKey,
|
||||||
|
ClockSkew = TimeSpan.Zero,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Captures the bsn the BFF forwarded and returns a canned acceptance.</summary>
|
||||||
|
internal sealed class FakeDomainClient : IDomainClient
|
||||||
|
{
|
||||||
|
public string? SubmittedBsn { get; private set; }
|
||||||
|
public SubmitAccepted Result { get; set; } = new("reg-123", "Ingediend");
|
||||||
|
|
||||||
|
public Task<SubmitAccepted> SubmitRegistrationAsync(string bsn, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
SubmittedBsn = bsn;
|
||||||
|
return Task.FromResult(Result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Serves a configurable set of projection rows.</summary>
|
||||||
|
internal sealed class FakeProjectionClient : IProjectionClient
|
||||||
|
{
|
||||||
|
public List<ProjectionEntry> Entries { get; } = [];
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<ProjectionEntry>> GetRegisterAsync(CancellationToken ct = default)
|
||||||
|
=> Task.FromResult<IReadOnlyList<ProjectionEntry>>(Entries);
|
||||||
|
}
|
||||||
38
services/bff/Bff.Tests/OpenbaarEndpointTests.cs
Normal file
38
services/bff/Bff.Tests/OpenbaarEndpointTests.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using Bff.Api;
|
||||||
|
|
||||||
|
namespace Bff.Tests;
|
||||||
|
|
||||||
|
public class OpenbaarEndpointTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Serves_public_safe_rows_anonymously()
|
||||||
|
{
|
||||||
|
using var factory = new BffFactory();
|
||||||
|
factory.Projection.Entries.Add(new ProjectionEntry("abc-111", "INGEDIEND", "123456782", "Jan"));
|
||||||
|
|
||||||
|
// No Authorization header — the openbaar register is a public lookup (ADR-0010/S-09).
|
||||||
|
var response = await factory.CreateClient().GetAsync("/openbaar/register");
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Contains("abc-111", body);
|
||||||
|
Assert.Contains("INGEDIEND", body);
|
||||||
|
// The bsn must never appear in a public response.
|
||||||
|
Assert.DoesNotContain("123456782", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Filters_by_the_query_parameter()
|
||||||
|
{
|
||||||
|
using var factory = new BffFactory();
|
||||||
|
factory.Projection.Entries.Add(new ProjectionEntry("abc-111", "INGEDIEND", null, null));
|
||||||
|
factory.Projection.Entries.Add(new ProjectionEntry("def-222", "INGEDIEND", null, null));
|
||||||
|
|
||||||
|
var rows = await factory.CreateClient()
|
||||||
|
.GetFromJsonAsync<List<OpenbaarEntry>>("/openbaar/register?q=abc");
|
||||||
|
|
||||||
|
Assert.Equal("abc-111", Assert.Single(rows!).Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
services/bff/Bff.Tests/OpenbaarProjectionTests.cs
Normal file
48
services/bff/Bff.Tests/OpenbaarProjectionTests.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using Bff.Api;
|
||||||
|
|
||||||
|
namespace Bff.Tests;
|
||||||
|
|
||||||
|
public class OpenbaarProjectionTests
|
||||||
|
{
|
||||||
|
private static readonly ProjectionEntry[] Sample =
|
||||||
|
[
|
||||||
|
new("abc-111", "INGEDIEND", "123456782", "Jan"),
|
||||||
|
new("def-222", "INGEDIEND", "987654321", "Piet"),
|
||||||
|
];
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Maps_every_row_to_public_safe_fields_when_no_query()
|
||||||
|
{
|
||||||
|
var view = OpenbaarProjection.PublicView(Sample, null);
|
||||||
|
|
||||||
|
Assert.Equal(2, view.Count);
|
||||||
|
Assert.Equal("abc-111", view[0].Id);
|
||||||
|
Assert.Equal("INGEDIEND", view[0].Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Public_view_exposes_only_id_and_status()
|
||||||
|
{
|
||||||
|
// OpenbaarEntry structurally carries only Id + Status — bsn/naam can never leak.
|
||||||
|
var props = typeof(OpenbaarEntry).GetProperties().Select(p => p.Name).ToArray();
|
||||||
|
Assert.Equal(["Id", "Status"], props);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("abc", 1)]
|
||||||
|
[InlineData("ABC", 1)]
|
||||||
|
[InlineData("2", 1)]
|
||||||
|
[InlineData("zzz", 0)]
|
||||||
|
public void Filters_by_id_containing_the_query_case_insensitively(string q, int expected)
|
||||||
|
{
|
||||||
|
var view = OpenbaarProjection.PublicView(Sample, q);
|
||||||
|
|
||||||
|
Assert.Equal(expected, view.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Blank_query_is_treated_as_no_filter()
|
||||||
|
{
|
||||||
|
Assert.Equal(2, OpenbaarProjection.PublicView(Sample, " ").Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
75
services/bff/Bff.Tests/SelfServiceEndpointTests.cs
Normal file
75
services/bff/Bff.Tests/SelfServiceEndpointTests.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
|
||||||
|
namespace Bff.Tests;
|
||||||
|
|
||||||
|
public class SelfServiceEndpointTests
|
||||||
|
{
|
||||||
|
private static HttpRequestMessage Submit(string? bearer)
|
||||||
|
{
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Post, "/self-service/registrations")
|
||||||
|
{
|
||||||
|
Content = JsonContent.Create(new { }),
|
||||||
|
};
|
||||||
|
if (bearer is not null)
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearer);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Rejects_a_request_without_a_token()
|
||||||
|
{
|
||||||
|
using var factory = new BffFactory();
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
|
var response = await client.SendAsync(Submit(bearer: null));
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||||
|
Assert.Null(factory.Domain.SubmittedBsn);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("not-a-jwt")]
|
||||||
|
public async Task Rejects_a_malformed_token(string bearer)
|
||||||
|
{
|
||||||
|
using var factory = new BffFactory();
|
||||||
|
var response = await factory.CreateClient().SendAsync(Submit(bearer));
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Rejects_a_token_signed_with_the_wrong_key()
|
||||||
|
{
|
||||||
|
using var factory = new BffFactory();
|
||||||
|
var response = await factory.CreateClient().SendAsync(Submit(TestTokens.WrongKey("123456782")));
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Rejects_an_expired_token()
|
||||||
|
{
|
||||||
|
using var factory = new BffFactory();
|
||||||
|
var response = await factory.CreateClient().SendAsync(Submit(TestTokens.Expired("123456782")));
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Accepts_a_valid_token_and_forwards_the_bsn_to_the_domain()
|
||||||
|
{
|
||||||
|
using var factory = new BffFactory();
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
|
var response = await client.SendAsync(Submit(TestTokens.Valid("123456782")));
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
|
||||||
|
Assert.Equal("123456782", factory.Domain.SubmittedBsn);
|
||||||
|
var body = await response.Content.ReadFromJsonAsync<SubmitAcceptedDto>();
|
||||||
|
Assert.Equal("reg-123", body!.RegistrationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record SubmitAcceptedDto(string RegistrationId, string Status);
|
||||||
|
}
|
||||||
30
services/bff/Bff.Tests/TestTokens.cs
Normal file
30
services/bff/Bff.Tests/TestTokens.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Microsoft.IdentityModel.JsonWebTokens;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace Bff.Tests;
|
||||||
|
|
||||||
|
/// <summary>Mints JWTs for the BFF tests — signed with the factory's test key (valid) or otherwise
|
||||||
|
/// (a wrong key / expired) to exercise the bearer validation the BFF configures.</summary>
|
||||||
|
internal static class TestTokens
|
||||||
|
{
|
||||||
|
public static string Valid(string bsn) => Create(bsn, BffFactory.TestSigningKey, expired: false);
|
||||||
|
|
||||||
|
public static string Expired(string bsn) => Create(bsn, BffFactory.TestSigningKey, expired: true);
|
||||||
|
|
||||||
|
public static string WrongKey(string bsn) => Create(
|
||||||
|
bsn,
|
||||||
|
new SymmetricSecurityKey(Encoding.UTF8.GetBytes("a-different-signing-key-256-bits-long-indeed-yes!")),
|
||||||
|
expired: false);
|
||||||
|
|
||||||
|
private static string Create(string bsn, SymmetricSecurityKey key, bool expired)
|
||||||
|
{
|
||||||
|
var handler = new JsonWebTokenHandler();
|
||||||
|
return handler.CreateToken(new SecurityTokenDescriptor
|
||||||
|
{
|
||||||
|
Claims = new Dictionary<string, object> { ["bsn"] = bsn },
|
||||||
|
Expires = DateTime.UtcNow.AddMinutes(expired ? -5 : 30),
|
||||||
|
SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user