OpenIddict with EnableDegradedMode on the server and UseIntrospection for multiple API projects - openiddict

.NET 5 / OpenIddict 3.0
I would like to keep authentications centralized on a project called AuthProject.
If I have multiple API projects called API1, API2, API3, API4, how do I authenticate them with the token received by AuthProject?
My AuthProject has the following settings:
services.AddOpenIddict()
.AddServer(options =>
{
options
.SetTokenEndpointUris("/connect/token")
.SetAuthorizationEndpointUris("/connect/authorize")
.SetIntrospectionEndpointUris("/connect/introspect");
options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles);
options.AllowPasswordFlow()
.AllowRefreshTokenFlow()
.AllowClientCredentialsFlow()
.AllowImplicitFlow();
options.AddDevelopmentEncryptionCertificate();
options.AddDevelopmentSigningCertificate();
options.UseAspNetCore();
options.EnableDegradedMode();
options.AddEventHandler<ValidateAuthorizationRequestContext>(builder => builder.UseScopedHandler<MyValidateAuthorizationRequestContext>());
options.AddEventHandler<ValidateTokenRequestContext>(builder => builder.UseScopedHandler<MyValidateTokenRequestContext>());
options.AddEventHandler<HandleAuthorizationRequestContext>(builder => builder.UseScopedHandler<MyHandleAuthorizationRequestContext>());
options.AddEventHandler<HandleTokenRequestContext>(builder => builder.UseScopedHandler<MyHandleTokenRequestContext>());
options.AddEventHandler<ValidateIntrospectionRequestContext>(builder => builder.UseScopedHandler<MyValidateIntrospectionRequestContext>());
options.AddEventHandler<HandleIntrospectionRequestContext>(builder => builder.UseScopedHandler<MyHandleIntrospectionRequestContext>());
})
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
I normally connect to connect/token, I receive the token, I can connect normally to the APIs that are in the AuthProject.
All APIs are decorated by [Authorized]
Everything works.
When I try to access any API decorated by [Authorize] being in API1, API2, API3, API4 (other API projects), I always receive Status 401 - Unauthorized.
The configurations of these API projects are:
services.AddAuthentication(options =>
{
options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
});
services.AddOpenIddict()
.AddValidation(options =>
{
options.SetIssuer("https://authProjectUrl/");
options.AddAudiences("api1");
options.UseIntrospection()
.SetClientId("api1")
.SetClientSecret("xxxxxxxxxx");
options.UseSystemNetHttp();
options.UseAspNetCore();
});
In AuthProject project, the implementations for ValidateIntrospectionRequestContext are:
public class MyValidateIntrospectionRequestContext : IOpenIddictServerHandler<ValidateIntrospectionRequestContext>
{
public ValueTask HandleAsync(ValidateIntrospectionRequestContext context)
{
if (context.ClientId != "api1" || context.ClientSecret != "xxxx")
context.Reject("Error");
return default;
}
}
In AuthProject, the implementations for HandleIntrospectionRequestContext are:
public class MyHandleIntrospectionRequestContext : IOpenIddictServerHandler<HandleIntrospectionRequestContext>
{
public ValueTask HandleAsync(HandleIntrospectionRequestContext context)
{
return default;
}
}
What is missing? What's wrong?
What should I do to API1, API2, API3, API4 projects understand that if I pass the token to them, they must also understand that they are authorized?
Detail:
I call the APIs by correctly passing the token through Bearer, and the APIs automatically correctly call the MyValidateIntrospectionRequestContext and MyHandleIntrospectionRequestContext handlers.
Debugging, everything works without errors.
But the return by POSTMAN is always code 401.

It worked.
I needed to add the following code in MyHandleIntrospectionRequestContext:
context.Audiences.Add("api1");
context.Audiences.Add("api2");
context.Audiences.Add("api3");

Related

OpenIdDict not returning Token from .Net Core API

I have a .Net Core API project, where I'm using OpenIDDict to authenticate. I refered there official repository however it is not returning Token to end user (currently i'm testing with postman)
Here is my Program.cs file
//OPENID
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{ options.UseSqlServer(builder.Configuration.GetConnectionString("ApplicationDb"));
options.UseOpenIddict();
});
// Register the Identity services.
//builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
// .AddEntityFrameworkStores<ApplicationDbContext>()
// .AddDefaultTokenProviders();
builder.Services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
options.ClaimsIdentity.EmailClaimType = Claims.Email;
});
builder.Services.AddQuartz(options =>
{
options.UseMicrosoftDependencyInjectionJobFactory();
options.UseSimpleTypeLoader();
options.UseInMemoryStore();
});
builder.Services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and models.
// Note: call ReplaceDefaultEntities() to replace the default OpenIddict entities.
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>();
// Enable Quartz.NET integration.
options.UseQuartz();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
// Enable the token endpoint.
options.SetTokenEndpointUris("/connect/token");
options.AllowPasswordFlow();
options.AcceptAnonymousClients();
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
options.UseAspNetCore()
.EnableTokenEndpointPassthrough();
})
// Register the OpenIddict validation components.
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});...
My AuthorizationController.cs file
[HttpPost("~/connect/token"), Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest();
var claimsPrincipal = new ClaimsPrincipal();
if (request.IsPasswordGrantType())
{
try
{
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null)
{
//Return Error message
}
var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true);
if (!result.Succeeded)
{
//Return Error message
}
var principal = await _signInManager.CreateUserPrincipalAsync(user);
principal.SetScopes(new[]
{
Scopes.OpenId,
Scopes.Email,
Scopes.Profile,
Scopes.Roles
}.Intersect(request.GetScopes()));
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
...
It should return a token similar to this answer however I am getting 500 status in postman.
The error is The entity type 'OpenIddictEntityFrameworkCoreToken' was not found. Ensure that the entity type has been added to the model.
Not sure, I need to create tables for this ? Or I missed something ? I can see in official OpenIdDict site, they haven't mentioned anything like that.
I'm using .Net 6, VS 2022.
Did you try adding ?
services.AddIdentity<User, Role>()
.AddSignInManager()
.AddUserStore<UserStore>()
.AddRoleStore<RoleStore>()
.AddUserManager<UserManager<User>>();
Check this blog

OpenIdDict Degraded Mode with ClientCredentials flow

I'm following this blog about allowing OpenIDDict to wrap an alternative authentication provider but return a JWT token from OpenIDDict itself:
https://kevinchalet.com/2020/02/18/creating-an-openid-connect-server-proxy-with-openiddict-3-0-s-degraded-mode/
This is really about intercepting the Authorization Code flow rather than the Client Credentials flow, but it provides a good starting point.
Unfortunately it states that "we don't need to override the HandleTokenRequestContext", which is appropriate for the blog but not (as far as I know) for my use case.
I think I need to implement a custom HandleTokenRequestContext but when I do so, the code runs, no errors but the HTTP response is empty. No token is generated.
How should I properly intercept the Client Credentials flow so that I can call out to another provider to validate the credentials, get a result and include that in the custom claims that I need to add to the JWT?
Code below:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DbContext>(options =>
{
// Configure the context to use an in-memory store - probably not needed?
options.UseInMemoryDatabase(nameof(DbContext));
// Register the entity sets needed by OpenIddict.
options.UseOpenIddict();
});
services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<DbContext>();
})
.AddServer(options =>
{
options.SetTokenEndpointUris("/connect/token");
options
//.AllowRefreshTokenFlow()
.AllowClientCredentialsFlow();
// Register the signing and encryption credentials.
// options.AddDevelopmentEncryptionCertificate()
// .AddDevelopmentSigningCertificate();
//Development only
options
.AddEphemeralEncryptionKey()
.AddEphemeralSigningKey()
.DisableAccessTokenEncryption();
// Register scopes (i.e. the modes we can operate in - there may be a better way to do this (different endpoints?)
options.RegisterScopes("normal", "registration");
//TODO: Include Quartz for cleaning up old tokens
options.UseAspNetCore()
.EnableTokenEndpointPassthrough();
options.EnableDegradedMode(); //Activates our custom handlers as the only authentication mechansim, otherwise the workflow attempt to invoke our handler *after* the default ones have already failed
//the request
options.AddEventHandler<ValidateTokenRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
//TODO: Check that the client Id is known
if (!string.Equals(context.ClientId, "client-1", StringComparison.Ordinal))
{
context.Reject(
error: Errors.InvalidClient,
description: "The specified 'client_id' doesn't match a known Client ID.");
return default;
}
return default;
}));
options.AddEventHandler<HandleTokenRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType, OpenIddictConstants.Claims.Name, OpenIddictConstants.Claims.Role);
identity.AddClaim(OpenIddictConstants.Claims.Subject, context.ClientId, OpenIddictConstants.Destinations.AccessToken, OpenIddictConstants.Destinations.IdentityToken);
if (context.Request.Scope == "registration")
{
//TODO: Authenticate against BackOffice system to get it's token so we can add it as a claim
identity.AddClaim("backoffice_token", Guid.NewGuid().ToString(), OpenIddictConstants.Destinations.AccessToken);
}
else
{
//TODO: Authenticate against internal authentication database as normal
}
var cp = new ClaimsPrincipal(identity);
cp.SetScopes(context.Request.GetScopes());
context.Principal = cp;
//This doesn't work either
//context.SignIn(context.Principal);
//ERROR: When this exits the response is empty
return default;
}));
});
//.AddValidation(options =>
//{
// options.UseLocalServer();
// options.UseAspNetCore();
//});
services.AddControllers();
services.AddHostedService<CredentialLoader>();
}
In the end, I got this working with 3 changes:
In the HandleTokenRequestContext:
var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType);
Not sure if this is essential or not. Then remove the
context.Principal = cp;
but re-add the
context.SignIn(context.Principal);

Fetch data return Untheorized 401 access to asp.net core API protected in Azure AD

Im new to `webassembly blazor, Im spend too much time trying to figure out what's wrong here but I couldnt manage.
I have the following scenario:
Asp.net API registered and protected in Azure AD
Expose API with Scope AcessApi with status enabled
A Client application is added to authorized client applications
Token configuration both are checked Access Token and ID Token
And a client app that will call the API, developed in webassembly blazor
client app is registered in Azure AD
Client API permissions has delegated permission to use my client API
with correct scope AccessApi.
I tested the API using swagger interface, it forces user to authenticate first before accessing the API.
I tested using curl and grabbed the token from swagger interface and works perfectly fine.
curl -X GET "http://localhost:9400/api/getdata" -H "accept: text/plain" -H "Authorization: Bearer XX"
However, when my client application trying to access the API, a sign-in page pop-up for credentials, I could see the Token ID at browser bar being retrieved and while calling the API the app logs error not authorized
program class of the client application:
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
//builder.Logging.SetMinimumLevel(LogLevel.Debug);
////builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<CustomAuthorizationMessageHandler>();
builder.Services.AddHttpClient("AccessApi",
client => client.BaseAddress = new Uri("http://localhost:9400"))
.AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
.CreateClient("AccessApi"));
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add(scope);
});
await builder.Build().RunAsync();
}
in CustomAuthorizationMessageHandler class I have defined:
private static string scope = #"api://xxx-35fc2470889f/AccessApi";
public CustomAuthorizationMessageHandler(IAccessTokenProvider provider,
NavigationManager navigationManager)
: base(provider, navigationManager)
{
ConfigureHandler(
authorizedUrls: new[] { "http://localhost:9400" },
}
In appsettings.json a defined the client id of the API and tenant id without scopes since they are been defined in the CustomAuthorizationMessageHandlerclass:
{
"AzureAd": {
"Authority": "https://login.microsoftonline.com/<tenant_id>",
"ClientId": "<clientid>",
"CallbackPath": "/signin-oidc",
"ValidateAuthority": "true"
}
}
After a successful login via Azure AD, I call to fetch data from the API here
protected override async Task OnInitializedAsync()
{
...
try
{
responseBody = await Http.GetStringAsync("/api/getdata"); # use base URL of the API
}
catch (AccessTokenNotAvailableException ex)
{
ex.Redirect();
}
}
the console logs
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[1]
Authorization was successful.
info: System.Net.Http.HttpClient.AccessApi.ClientHandler[100]
Sending HTTP request GET http://localhost:9400/api/getdata
:9400/customer-manager/api/getdata:1 Failed to load resource: the server responded with a status of 401 (Unauthorized)
What could be wrong here?
Is there a way how to print the return token?
Update
I tested the API using Postman where auth Grant type is Implicit, after successful login, I store token on variable and passed in the header as Bearer the API return 401 Unauthroized. I decoded the token it contains the right scope AccessApi , with the correct clientId. what could be wrong here ?
If you want to call Microsoft graph and your custom API in one blazor webassembly project, we can implement it by creating different HTTP client to call different API
For example
Register a server API app
Register an AAD app for the Server API app
Expose an API
Register a client app
Register a client app
Enable Implicit grant flow
Add API permissions. ( API app permissions)
Configure API app
Please add the following code in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddCors(options =>
{
options.AddDefaultPolicy(
builder => builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod());
});
services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
.AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
options.Authority += "/v2.0";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuers = new[] {
$"https://sts.windows.net/{Configuration["AzureAD:TenantId"]}/",
$"https://login.microsoftonline.com/{Configuration["AzureAD:TenantId"]}/v2.0"
},
RoleClaimType = "roles",
// The web API accepts as audiences both the Client ID (options.Audience) and api://{ClientID}.
ValidAudiences = new[]
{
options.Audience,
$"api://{options.Audience}"
}
};
});
....
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.OAuthClientId(Configuration["Swagger:ClientId"]);
c.OAuthScopeSeparator(" ");
c.OAuthAppName("Protected Api");
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Configure Client APP
Create custom AuthorizationMessageHandler for Graph API and custom API
// custom API
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
{
public CustomAuthorizationMessageHandler(IAccessTokenProvider provider,
NavigationManager navigationManager)
: base(provider, navigationManager)
{
ConfigureHandler(
authorizedUrls: new[] { "<your web API url>" },
scopes: new[] { "the API app scope" });
}
}
Add the following code to the program.cs
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddScoped<CustomAuthorizationMessageHandler>();
// register HTTP client to call our own api
builder.Services.AddHttpClient("MyAPI", client => client.BaseAddress = new Uri("<your web API url>"))
.AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("<the API app scope>");
});
await builder.Build().RunAsync();
}
}
Call the api
#inject IHttpClientFactory _clientFactory
var httpClient = _clientFactory.CreateClient("<the client name you register>");
await apiClient.GetStringAsync("path");
Finally I found the issue was on the server side ASP.net core where I was validating the token in ConfigureServices at startup class:
// For token parameters validation
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o =>
{
o.Audience = "<xx>"; // Application id
o.Authority = "https://login.microsoftonline.com/<xx>"; // Tenant ID
//Token validation
o.TokenValidationParameters = new TokenValidationParameters {ValidateIssuerSigningKey = false, ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true};
});
I had to disable Issuer since the token is coming from a different application.

Custom HTTP response headers on internal IdentityServer4 endpoints

I've been trying to add some custom HTTP response headers to the built-in IdentityServer endpoints like /connect/checksession with global filter approach depicted in the code snippet below (taken from Startup.cs ConfigureServices method):
services.AddMvc(options =>
{
options.EnableEndpointRouting = false;
options.Filters.Add(typeof(SecurityHeadersAttribute));
});
While the headers appear just fine on custom MVC endpoints like /AccountSelect and /Login the internal IdentityServer endpoints seem to ignore those altogether.
I was thinking whether the order of the registration in the startup overrides the global filters. In my case the code below is executed after AddMvc
services.AddIdentityServer(options =>
{
//code omitted for brevity
});
One option is to add your own middleware step in the request pipeline that will execute for every incoming request to your IdentityServer application.
Just add this code in Startup.Configure:
app.Use(async (context, next) =>
{
context.Response.OnStarting(() =>
{
context.Response.Headers.Add("MyMagic", "Header");
return Task.FromResult(0);
});
await next();
});

ASP.Net Core Identity with JwtBearer AuthenticationScheme map claims to context User object

I have a React Front end using the msal lib to authenticate the user client side with our Azure AD. This works great and authentication has no issues. I also have an ASP.Net Core WebApi to provide data to the client. I am using the JwtTokens to pass the Bearer token in the request. The WebApi is able to validate the token and all is well... I thought, however, when the WebApi method is invoked the only way I can get the User's email or name is to query the User.Claims with Linq.
this.User.Claims.Where(c=> c.Type == "preferred_username").FirstOrDefault().Value
I was about to go down the road of mapping these linq queries to an object which could be injected into the WebApi's controller, but that seems wrong.
I am obviously missing something in my Startup.cs for the WebApi, Any help or suggestions would be great!:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
//add authentication JwtBearer Scheme
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Audience = Configuration["JwtSettings:Audience"];
options.Authority = Configuration["JwtSettings:Authority"];
options.Events = new JwtBearerEvents
{
OnTokenValidated = ctx =>
{
//log
return Task.CompletedTask;
},
OnAuthenticationFailed = ctx =>
{
//log
return Task.CompletedTask;
}
};
options.SaveToken = true;
});
services.AddAuthorization();
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
}