I'm building a .NET 7 MVC app that uses Azure AD for Authentication but calls out to another API to add additional claims to the Identity.
This worked great when I defined the Claim Transformation statically, but I'd like to register the Claim Transformation as a singleton instead so that it can manage its own token lifetime to the API.
This is what the code looked like to add the claims when the transformation was static:
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.Configure<MicrosoftIdentityOptions>(
OpenIdConnectDefaults.AuthenticationScheme, opt =>
{
opt.Events.OnTokenValidated = async context =>
{
if (context.Principal != null)
{
context.Principal = await ClaimsAPI.TransformAsync(context.Principal);
}
};
});
This works, but the Claim Transformation class can't store a bearer jwt, and would need to get a fresh one every time, wasting a ton of resources.
this is the closest I've come to getting it to work as a singleton, but it causes plenty of issues
builder.Services.AddSingleton<ICLaimsAPI, ClaimsAPI>();
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.Configure<MicrosoftIdentityOptions>(
OpenIdConnectDefaults.AuthenticationScheme, opt =>
{
opt.Events.OnTokenValidated = async context =>
{
if (context.Principal != null)
{
context.Principal = await builder.Services.BuildServiceProvider()
.GetRequiredService<IClaimsAPI>()
.TransformAsync(context.Principal);
}
};
});
This generates a seperate copy of each singleton, which doesn't really work for obvious reasons.
How can I inject my service so that it adds the claims correctly?
EDIT: Solved!
I had to do some slight tweaks to #Acegambit's code. here is my working solution for postierity, just in case someone in the future needs to solve a similar problem.
builder.Services.AddSingleton<IClaimsAPI, ClaimsAPI>();
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddOptions().AddSingleton<IConfigureOptions<MicrosoftIdentityOptions>>(provider =>
{
var ClaimsAPI = provider.GetRequiredService<IClaimsAPI>();
return new ConfigureNamedOptions<MicrosoftIdentityOptions>(OpenIdConnectDefaults.AuthenticationScheme, opt =>
{
opt.Events.OnTokenValidated = async context =>
{
if (context.Principal != null)
{
context.Principal = await ClaimsAPI.TransformAsync(context.Principal);
}
};
});
});
This took a little digging into the IServiceCollection extension methods. Looking at the implementation of Configure<TOptions> it really doesn't do a whole lot other than call .AddOptions() and register a singleton of type IConfigureOptions so I think you can pull out that code and do it yourself like so:
builder.Services.AddSingleton<IClaimsAPI, ClaimsAPI>();
builder.Services.AddOptions();
builder.Services.AddSingleton<IConfigureOptions<MicrosoftIdentityOptions>>(provider =>
{
var claimsApi = provider.GetRequiredService<IClaimsAPI>();
return new ConfigureNamedOptions<MicrosoftIdentityOptions>(string.Empty, options =>
{
// TODO: insert your logic to set the context.Principle here
// using the claimsApi that should resolve from the provider above
});
});
There's already an answer but I figure it would be good to show how options has evolved to make this scenario a bit more terse:
builder.Services.AddOptions<MicrosoftIdentityOptions>(OpenIdConnectDefaults.AuthenticationScheme)
.Configure<IClaimsAPI>((options, claimsApi) =>
{
options.Events = new()
{
OnTokenValidated = context =>
{
context.Principal = claimsApi.Transform(context.Principal);
return Task.CompletedTask;
}
};
});
Related
I have been very frustrated trying to use openiddict. I can't use any of the pre-existing sample since their ClaimsIdentity uses methods that to me aren't available, for example the identity.SetClaims(), identity.SetScopes() and identity.GetScopes() don't work for me.
This is the official sample Zirku.Server:
var builder = WebApplication.CreateBuilder(args);
// OpenIddict offers native integration with Quartz.NET to perform scheduled tasks
// (like pruning orphaned authorizations/tokens from the database) at regular intervals.
builder.Services.AddQuartz(options =>
{
options.UseMicrosoftDependencyInjectionJobFactory();
options.UseSimpleTypeLoader();
options.UseInMemoryStore();
});
// Register the Quartz.NET service and configure it to block shutdown until jobs are complete.
builder.Services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
builder.Services.AddDbContext<DbContext>(options =>
{
// Configure the context to use Microsoft SQL Server.
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
// Register the entity sets needed by OpenIddict.
// Note: use the generic overload if you need
// to replace the default OpenIddict entities.
options.UseOpenIddict();
});
builder.Services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<DbContext>();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
// Enable the authorization, introspection and token endpoints.
options.SetAuthorizationEndpointUris("/authorize")
.SetIntrospectionEndpointUris("/introspect")
.SetTokenEndpointUris("/token");
// Note: this sample only uses the authorization code flow but you can enable
// the other flows if you need to support implicit, password or client credentials.
options.AllowAuthorizationCodeFlow();
// Register the signing credentials.
options.AddDevelopmentSigningCertificate();
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
//
// Note: unlike other samples, this sample doesn't use token endpoint pass-through
// to handle token requests in a custom MVC action. As such, the token requests
// will be automatically handled by OpenIddict, that will reuse the identity
// resolved from the authorization code to produce access and identity tokens.
//
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough();
})
// Register the OpenIddict validation components.
.AddValidation(options =>
{
// Import the configuration from the local OpenIddict server instance.
options.UseLocalServer();
// Register the ASP.NET Core host.
options.UseAspNetCore();
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseHttpsRedirection();
// Create new application registrations matching the values configured in Zirku.Client and Zirku.Api1.
// Note: in a real world application, this step should be part of a setup script.
await using (var scope = app.Services.CreateAsyncScope())
{
var context = scope.ServiceProvider.GetRequiredService<DbContext>();
await context.Database.EnsureCreatedAsync();
await CreateApplicationsAsync();
await CreateScopesAsync();
async Task CreateApplicationsAsync()
{
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
if (await manager.FindByClientIdAsync("console_app") is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "console_app",
RedirectUris =
{
new Uri("http://localhost:8739/")
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + "api1",
Permissions.Prefixes.Scope + "api2"
}
});
}
if (await manager.FindByClientIdAsync("resource_server_1") is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "resource_server_1",
ClientSecret = "846B62D0-DEF9-4215-A99D-86E6B8DAB342",
Permissions =
{
Permissions.Endpoints.Introspection
}
});
}
// Note: no client registration is created for resource_server_2
// as it uses local token validation instead of introspection.
}
async Task CreateScopesAsync()
{
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictScopeManager>();
if (await manager.FindByNameAsync("api1") is null)
{
await manager.CreateAsync(new OpenIddictScopeDescriptor
{
Name = "api1",
Resources =
{
"resource_server_1"
}
});
}
if (await manager.FindByNameAsync("api2") is null)
{
await manager.CreateAsync(new OpenIddictScopeDescriptor
{
Name = "api2",
Resources =
{
"resource_server_2"
}
});
}
}
}
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/api", [Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
(ClaimsPrincipal user) => user.Identity!.Name);
app.MapGet("/authorize", async (HttpContext context, IOpenIddictScopeManager manager) =>
{
// Retrieve the OpenIddict server request from the HTTP context.
var request = context.GetOpenIddictServerRequest();
var identifier = (int?)request["hardcoded_identity_id"];
if (identifier is not (1 or 2))
{
return Results.Challenge(
authenticationSchemes: new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme },
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidRequest,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The specified hardcoded identity is invalid."
}));
}
// Create the claims-based identity that will be used by OpenIddict to generate tokens.
var identity = new ClaimsIdentity(
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
// Add the claims that will be persisted in the tokens.
identity.AddClaim(new Claim(Claims.Subject, identifier.Value.ToString(CultureInfo.InvariantCulture)));
identity.AddClaim(new Claim(Claims.Name, identifier switch
{
1 => "Alice",
2 => "Bob",
_ => throw new InvalidOperationException()
}));
// Note: in this sample, the client is granted all the requested scopes for the first identity (Alice)
// but for the second one (Bob), only the "api1" scope can be granted, which will cause requests sent
// to Zirku.Api2 on behalf of Bob to be automatically rejected by the OpenIddict validation handler,
// as the access token representing Bob won't contain the "resource_server_2" audience required by Api2.
identity.SetScopes(identifier switch
{
1 => request.GetScopes(),
2 => new[] { "api1" }.Intersect(request.GetScopes()),
_ => throw new InvalidOperationException()
});
identity.SetResources(await manager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
// Allow all claims to be added in the access tokens.
identity.SetDestinations(claim => new[] { Destinations.AccessToken });
return Results.SignIn(new ClaimsPrincipal(identity), properties: null, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
});
app.Run();
I have been trying to convert the samples to identity.AddClaim(), but I am not sure that it works as expected.
Please tell me what I am doing wrong. I am new to authorization and authentication so, as you can imagine, I am not good enough to figure out what is going wrong.
PS If you have any good up-to-date sources so that I can read up on the subject, that would be great.
PS 2 Excuse my english, it is not my first language
I'm trying to get result from my minimal API who configured in endpoints of my MVC web application
my Get action configured like this :
endpoints.MapGet(
"HO-CFDZU4/api/Currency/Get",
[PermissionAuthorize(PermissionName.ReadCurrencyDictionary)]
async ([FromServicesAttribute] CurrencyService curency) =>
{
var result = await DataSourceLoader.LoadAsync(curency.Get(), new DataSourceLoadOptions());
return Results.Ok(result);
});
As result i get response with object where property names changed to lowercase, and its not suit for me.
I want to get exactly same name in same case like i return form action.
To get similar effect in MVC i used this code :
services
.AddMvc()
.AddFluentValidation(x => x.RegisterValidatorsFromAssembly(AppDomain.CurrentDomain.GetAssemblies().Where(x => x.FullName.Contains("ApplicationCore")).Single()))
.AddMvcLocalization()
.AddMvcOptions(options =>{})
.AddRazorRuntimeCompilation()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
Which setup property naming policy for Json while using action in controllers, and i dont know how to setup same policy for minimalApi.
What Ive tried is to set [JsonPropertyName(name)] And it working good but we have lot of classes and i looking for more global solution.
I also tried configure JsonOptions globally like this:
services.Configure<JsonOptions>(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
But it do nothing
Use JsonOptions from Microsoft.AspNetCore.Http.Json namespace (docs):
services.Configure<JsonOptions>(options =>
{
options.SerializerOptions.PropertyNamingPolicy = null;
options.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
UPD
If your application uses both Minimal APIs endpoints and MVC ones, then you try to configure options from both namespaces:
services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options =>
{
options.SerializerOptions.PropertyNamingPolicy = null;
options.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
services.Configure<Microsoft.AspNetCore.Mvc.JsonOptions>(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
In order to get group membership functionality in our Blazor app based on Azure B2C, I add some claims in the Server project in Startup.cs. When I inspect the User object in the razor pages, the claims I added are not present.
Startup.cs, public void Configure():
app.Use(async (context, next) =>
{
if (context.User != null && context.User.Identity.IsAuthenticated)
{
var groups = GetGroupsFromAzureB2c(context.User);
// Attempt A, separate Identity
ClaimsIdentity i = new(context.User.Identity);
i.AddClaims(groups.Select(g => new Claim("Group", g)));
context.User.AddIdentity(i);
// Attempt B: Adding claims to the existing Identity
((ClaimsIdentity)context.User.Identity).AddClaims(groups.Select(g => new Claim("Group", g));
}
await next();
});
page.razor, protected override async Task OnInitializedAsync():
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity.IsAuthenticated)
{
var _claims = user.Claims;
}
_claims only holds the 12 claims that were added automatically, never the 4 claims I added in Startup.cs.
Are there different instances of the User object? Am I adding the claims incorrectly?
Apparently there are two separate Identities at play in a Blazor app. One in the Client code and a separate one in the Server code.
We fixed this by adding a CustomUserFactory that inherits from AccountClaimsPrincipalFactory<RemoteUserAccount>. We retrieve the AzureB2C roles from a central method using the Graph API for the client side which we call in Program.cs with
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAdB2C", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("xxx");
})
// here we hook up our custom factory
.AddAccountClaimsPrincipalFactory<CustomUserFactory>();
For the server side code, we use the same central method to get the roles from AzureB2C which we call from Startup.cs with
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options => {
options.Events.OnTokenValidated = async context => {/*add roles here*/}
Problem is that I get Functions runtime is unreachable error after adding AddAccessTokenManagement() in startup.cs file. Also the list of fuctions in azure is empty. The best part is that from app insights I see that my cron job is beeing executed anyway, and token is working. When running my code in local enviroment there is no problem reported, deployments also seems to be fine. This is how I configure my http client to work with identity token:
private void ConfigureAccessToken(IFunctionsHostBuilder builder)
{
var IdentityServerUrl = "<serverUrl>"; ;
builder.Services.AddHttpClient();
builder.Services.AddAccessTokenManagement(options =>
{
options.Client.Clients.Add("cloud-service", new ClientCredentialsTokenRequest
{
Address = $"{IdentityServerUrl}/connect/token",
ClientId = _authorizationConfig.ClientId,
ClientSecret = _authorizationConfig.ClientSecret,
});
});
builder.Services.AddClientAccessTokenClient("internal-client", configureClient: client => { });
}
Worth to mention that this way of configuring it works with my Web API application.
Any ideas guys?
I found the answer by myself. Looks like token confiuration for azure functions differ from Web API. Working code below:
private void ConfigureAccessToken(IFunctionsHostBuilder builder)
{
var IdentityServerUrl = "<serverUri>";
builder.Services.Configure<AccessTokenManagementOptions>(o =>
{
o.Client.Clients.Add("cloud-service", new ClientCredentialsTokenRequest
{
Address = $"{IdentityServerUrl}/connect/token",
ClientId = _authorizationConfig.ClientId,
ClientSecret = _authorizationConfig.ClientSecret,
});
});
builder.Services.AddDistributedMemoryCache();
builder.Services.AddTransient<ITokenClientConfigurationService, DefaultTokenClientConfigurationService>(s =>
{
return new DefaultTokenClientConfigurationService(
s.GetRequiredService<IOptions<AccessTokenManagementOptions>>(),
null,
null);
});
builder.Services.AddHttpClient(AccessTokenManagementDefaults.BackChannelHttpClientName);
builder.Services.TryAddTransient<ITokenEndpointService, TokenEndpointService>();
builder.Services.TryAddTransient<IClientAccessTokenCache, ClientAccessTokenCache>();
builder.Services.AddTransient<IAccessTokenManagementService, AccessTokenManagementService>(s =>
{
return new AccessTokenManagementService(
null,
null,
s.GetRequiredService<IOptions<AccessTokenManagementOptions>>(),
s.GetRequiredService<ITokenEndpointService>(),
s.GetRequiredService<IClientAccessTokenCache>(),
s.GetRequiredService<ILogger<AccessTokenManagementService>>()
);
});
builder.Services.AddTransient<ClientAccessTokenHandler>();
builder.Services.AddClientAccessTokenClient("internal-client", configureClient: config => {});
}
ConfigurationDbContext is disposed before the token is generated. When using IdentityServerTools. I've tried this both with manually adding the ConfigurationDbContext and with relying on the configuration from the Configuration store
in my Startup.cs I've configured IdentityServer like so:
//I've also tried without this line
services.AddDbContext<ConfigurationDbContext>(options
=> options.UseSqlServer(connectionString));
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddAspNetIdentity<ApplicationUser>()
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
s => s.MigrationsAssembly(migrationsAssembly));
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 30;
})
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
s => s.MigrationsAssembly(migrationsAssembly));
});
I'm trying to generate a token to consume identityserver as a client from my api. Sometimes the ConfigurationDbContext is already disposed, sometimes it throws on the first line sometimes it throws on the last
private async Task<string> CreatePaymentsTokenAsync()
{
// Get client for JWT
var idpClient = await this._configurationDbContext.Clients.Include(x => x.AllowedScopes)
.FirstOrDefaultAsync(c => c.ClientId == Config.APIServerClientId);
// Get scopes to set in JWT
var scopes = idpClient.AllowedScopes.Select(s => s.Scope).ToArray();
// Use in-built Identity Server tools to issue JWT
var token = await _identityServerTools.IssueClientJwtAsync(idpClient.ClientId,
idpClient.AccessTokenLifetime, scopes, new[] { "MyApi" });
return token;
}
This code occasionally works but most of the time it throws an error that the context is disposed, Any idea's on why?
I'd recommend not using the underlying DbContext and favour using IClientStore instead.
As for this problem - how is the class that owns _configurationDbContext scoped in the IoC container? It feels like it may have a longer lifetime than the DbContext and thus ends up trying to access a disposed instance.
It also could be because you're forgetting to await somewhere and it causing you're code to fire and forget
await CreatePaymentsTokenAsync()
Where is the CreatePaymentsTokenAsync() awaited from and is that method declared async?