AddDownstreamWebApi fails after adding multiple auth schemes - asp.net-core-webapi

Problem: My as an app calls to a downstream web api throw a null exception error after adding my own jwt bearer authentication.
I have a .net 5 web API, call it AppAPI, whose ConfigureServices has the following code:
var accessTokenKey = Convert.FromBase64String(Configuration.GetValue<string>("AccessCodeSecret"));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer("AccessToken", o =>
{
o.RequireHttpsMetadata = false;
o.SaveToken = true;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(accessTokenKey),
ValidateIssuer = false,
ValidateAudience = false
};
})
.AddMicrosoftIdentityWebApi(Configuration, "AzureAd", "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDownstreamWebApi("CommonServicesApi", Configuration.GetSection("CommonServicesApi"))
.AddInMemoryTokenCaches();
//services.AddAuthorization();
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("AccessToken")
.Build();
});
I have an /auth endpoint that accepts an access token from Azure AD, and generates a new access token with my own custom claims based on the database. The controller uses an authorize attribute to ensure it uses the correct mechanism:
[Authorize(AuthenticationSchemes = "AzureAd")]
The default policy from above ensures the rest of the endpoints use this access token for every other endpoint that doesnt specify a scheme.
I have a 2nd web API, called CommonServices, that is only accessible from other APIs, not clients directly. So AppAPI uses AddDownstreamwebapi to handle those calls. This worked previously to me adding my own app access tokens, meaning I only had one auth mechanism - AddMicrosoftIdentityWebApi. I started receiving my error when I added my own JwtBearer auth - "AccessToken".
The controller that has the error injects IDownstreamWebApi commonServicesApi. It uses the default auth scheme of "AccessToken". The code looks like this:
var response = await _commonServicesApi.CallWebApiForAppAsync("CommonServicesApi", "AzureAd",
options => { options.RelativePath = "Projects"});
var json = await response.Content.ReadAsStringAsync();
The 2nd parameter "AzureAd" was my attempt to have the commonservicesApi use the correct scheme. I am not even sure if that's the right scheme to use, or if .EnbleTokenAcquisitionToCallDownstreamApi adds a 3rd scheme that should be specified.
It is this call that I receive
System.NullReferenceException: 'Object reference not set to an instance of an object.'
at Microsoft.Identity.Web.MergedOptions.PrepareAuthorityInstanceForMsal()
This exception was originally thrown at this call stack:
Microsoft.Identity.Web.MergedOptions.PrepareAuthorityInstanceForMsal()
Microsoft.Identity.Web.TokenAcquisition.BuildConfidentialClientApplication(Microsoft.Identity.Web.MergedOptions)
Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplication(Microsoft.Identity.Web.MergedOptions)
Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForAppAsync(string, string, string, Microsoft.Identity.Web.TokenAcquisitionOptions)
Microsoft.Identity.Web.DownstreamWebApi.CallWebApiForAppAsync(string, string, System.Action<Microsoft.Identity.Web.DownstreamWebApiOptions>, System.Net.Http.StringContent)
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
I can't seem to figure out what is null, or how to approach solving this problem.

Related

HttpContext.User.Claims always empty despite JWT containing claims

I'm trying to create an asp-mvc api that reads the jwt claims on an incoming request, and then mirrors them back to the user - my use case is that I'm trying to investigate why a different endpoint is failing, I get a token from an external service, so I want minimal/no auth on it, I just want to inspect the claims.
I have the following Controller and endpoint:
[ApiController]
[Route("api/[controller]")]
public class TestController : ControllerBase
{
[HttpGet]
[Route("testclaims")]
public async Task<ActionResult<string?>> TestClaims()
{
List<string> result = new List<string>();
foreach (var claim in HttpContext.User.Claims)
result.Add($"{claim.Type}: {claim.Value}");
return "{ " + String.Join(", ", result.ToArray()) + " }";
}
}
My app is setup like this:
...
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme,
options => {
options.TokenValidationParameters = new TokenValidationParameters
{
RequireExpirationTime = false,
RequireSignedTokens = false,
RequireAudience = false,
SaveSigninToken = false,
TryAllIssuerSigningKeys = false,
ValidateActor = false,
ValidateAudience = false
};
builder.Configuration.Bind("JwtSettings", options);
});
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
I'm calling the endpoint from postman I make a request with an auth token containing a know set of claims, HttpContext.User.Claims is always empty, but I can see HttpContext.Request.Headers contains the expected token, and if I decode that externally, it contains the claims.
I've tried adding an Authorize decorator to the endpoint, but when I do I get a 401 returned with no log or debug info.
I'm assuming there's just something I need to enable to have asp populate the claims?
You need to use [Authorize].
Besides, you miss the app.UseAuthentication(); before your app.UseAuthorization();.
app.UseAuthentication();
app.UseAuthorization();
result:
Have a try, hope it can help you.

keycloack with dotnet simple API

Using this or https://nikiforovall.github.io/aspnetcore/dotnet/2022/08/24/dotnet-keycloak-auth.html tutorial I have setup test user and realm. I can call localhost:8080/realms/Test/protocol/openid-connect/token with client secret and user id and password from postman and it gives me access and refresh token. Now I need to call dotnet endpoint and make sure the user is who he is. But I can not find a way to establish this part as I'm always getting 401 unauthorized. Perhaps it is not setup or my authorization bearer string is not formed correctly.
How can I simply call to an endpoint, check authorization and return a response back?
Dotnet Code:
using System.Security.Claims;
using Api;
using Keycloak.AuthServices.Authentication;
using Keycloak.AuthServices.Authorization;
using Keycloak.AuthServices.Sdk.Admin;
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
var configuration = builder.Configuration;
var host = builder.Host;
host.ConfigureLogger();
services
.AddEndpointsApiExplorer()
.AddSwagger();
var authenticationOptions = configuration
.GetSection(KeycloakAuthenticationOptions.Section)
.Get<KeycloakAuthenticationOptions>();
services.AddKeycloakAuthentication(authenticationOptions);
var authorizationOptions = configuration
.GetSection(KeycloakProtectionClientOptions.Section)
.Get<KeycloakProtectionClientOptions>();
services
.AddAuthorization(o => o.AddPolicy("IsAdmin", b =>
{
b.RequireResourceRoles("default-roles-test");
/*b.RequireRealmRoles("admin");
b.RequireResourceRoles("r-admin");
// TokenValidationParameters.RoleClaimType is overriden
// by KeycloakRolesClaimsTransformation
b.RequireRole("r-admin");*/
})
)
.AddKeycloakAuthorization(authorizationOptions);
var adminClientOptions = configuration
.GetSection(KeycloakAdminClientOptions.Section)
.Get<KeycloakAdminClientOptions>();
services.AddKeycloakAdminHttpClient(adminClientOptions);
var app = builder.Build();
app
.UseSwagger()
.UseSwaggerUI();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/", (ClaimsPrincipal user) =>
{
// TokenValidationParameters.NameClaimType is overriden based on keycloak specific claim
app.Logger.LogInformation("{#User}", user.Identity.Name);
return "Hello world. "+ user.Identity.Name;
}).RequireAuthorization("IsAdmin");
app.Run();
appsettings.json keycloack config:
"Keycloak": {
"realm": "Test",
"auth-server-url": "http://localhost:8080/",
"ssl-required": "none",
"resource": "test-client",
"verify-token-audience": false,
"client-secret": "P4JgvFhjY0ftGSLDYmYn7diZhjoLnHon",
"confidential-port": 0
}
Request sending to this endpoint from postman (perhaps the issue is here with correct sending format):

ASP .Net Core Access the DistributedRedisCache in the JWTBearerOptions.Events OnTokenValidated Event

I have an ASP .Net Core API Project. In this project I am using JWTBearer Authentication. I am also using the AddDistributedRedisCache feature of the .Net Core Dependency Injection. (Both shown below)
We have a need to blacklist the tokens on occasion (Admin user removing rights, logout, etc) so that these can take immediate effect. Essentially forcing a user to log back in before the next call can be made.
We are adding the JWT Tokens to the redis cache as well as removing them from the client side cache on logout. But a user could (in theory) store the JWT token, and still gain access until the token expires, unless we intercept the call and check it against the blacklist.
How can I access the distributed cache object in the "OnTokenValidated" event in the code below? Do I have to manually create a new connection each time? We are only checking valid tokens, as that will stop invalid requests from even being checked against the blacklist.
Bearer Token Config:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "localhost:5000",
ValidAudience = "localhost:5000",
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(Configuration.GetValue<string>("SigningKey"))),
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = context => {
//context.Fail("User has been logged out");
return Task.CompletedTask;
}
};
});
Redis Cache Config:
services.AddDistributedRedisCache(option =>
{
option.Configuration = Configuration.GetValue<string>("RedisCacheAddress");
option.InstanceName = Configuration.GetValue<string>("RedisCacheInstance");
});
You can access services in DI utilizing the HttpContext available there:
OnTokenValidated = ctx =>
{
var cache = ctx.HttpContext.RequestServices.GetRequiredService<IDistributedCache>();
return Task.CompletedTask;
}
GetRequiredService will throw an exception if the service is not found. You can use GetService<T>() if you want the service to be optional.

Multiple Authentication Middlewares ASP.NET Core

I am relatively new to the concept of middlewares. I am aware that a middleware calls the next middleware when it completes.
I am trying to authenticate a request using either Google or my Identity Server. The user can login on my mobile app with google or a local account. However, I can't figure out how to use both authentication middlewares. If I pass the id_token for google, it passes on the first middleware (UseJwtBearerAuthentication) but fails on the second one (UseIdentityServerAuthentication). How can I make it so that it doesn't throw error when it actually passes on at least 1 authentication middleware? For example, if it passes on the first middleware, the second middleware is ignored?
app.UseJwtBearerAuthentication(new JwtBearerOptions()
{
Authority = "https://accounts.google.com",
Audience = "secret.apps.googleusercontent.com",
TokenValidationParameters = new TokenValidationParameters()
{
ValidateAudience = true,
ValidIssuer = "accounts.google.com"
},
RequireHttpsMetadata = false
});
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = "http://localhost:1000/",
RequireHttpsMetadata = false,
ScopeName = "MyApp.Api"
});
Normally, when an authentication middleware is failed(i don't mean throwing exception), this doesn't affect another successful authentication middleware. Probably your second middleware throws an exception(not a validation failure). First check error message and try to resolve it. If you can't, use AuthenticationFailed event to handle error. In this case your code should be something like below:
app.UseJwtBearerAuthentication(new JwtBearerOptions()
{
// ...
Events = new JwtBearerEvents()
{
OnAuthenticationFailed = async (context) =>
{
if (context.Exception is your exception)
{
context.SkipToNextMiddleware();
}
}
}
});
However, for your scenerio i wouldn't choose your way. I would use only identity server endpoint. For signing with google you can configure identity server like below:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,
AutomaticAuthenticate = false,
AutomaticChallenge = false
});
app.UseGoogleAuthentication(new GoogleOptions
{
AuthenticationScheme = "Google",
SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,
ClientId = "",
ClientSecret = ""
});
app.UseIdentityServer();
Edit
It seems AuthenticationFailed event couldn't be used for IdentityServer4.AccessTokenValidation. I am not sure but if you will use identity server for only jwt token, you can use UseJwtBearerAuthentication for validation.

How to make [Authorize] trigger openId middleware

I'm trying out some features of ASP.NET 5 and I'm struggling a bit with authentication. I've managed to use most of this sample app to connect to my Azure AD to log in, but I can't figure out how to restrict parts of my web app to authenticated users only. The article that accompanies the sample app I used states that
You can trigger the middleware to send an OpenID Connect sign-in
request by decorating a class or method with the [Authorize]
attribute, or by issuing a challenge
Since I'd like to avoid repeating the same challenge code everywhere, I opted for the attribute approach, but it doesn't work at all. All it seems to do is block access to unauthorized users, without redirecting to the login page the way the challenge does.
Since I intended the app I am building to be more private than public, I've also tried creating a global policy and opening up some select features using the AllowAnonymous attribute. This works, but again the unauthorized pages are simply shown as blank, instead of a challenge being issued.
This is the policy code I'm using currently, taken from here:
var policy = new AuthorizationPolicyBuilder()
//This is what makes it function like the basic [Authorize] attribute
.RequireAuthenticatedUser()
.Build();
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new AuthorizeFilter(policy));
});
Am I missing some setup to the authorization attribute or the policy that issues the challenge?
For posterity and most likely my future self as well:
I was missing the AutomaticAuthentication property in the OpenIdConnectOptions. The sample app was set up like this:
// Configure the OWIN Pipeline to use Cookie Authentication
app.UseCookieAuthentication(options =>
{
// By default, all middleware are passive/not automatic. Making cookie middleware automatic so that it acts on all the messages.
options.AutomaticAuthentication = true;
});
// Configure the OWIN Pipeline to use OpenId Connect Authentication
app.UseOpenIdConnectAuthentication(options =>
{
options.ClientId = Configuration.Get("AzureAd:ClientId");
options.Authority = String.Format(Configuration.Get("AzureAd:AadInstance"), Configuration.Get("AzureAd:Tenant"));
options.PostLogoutRedirectUri = Configuration.Get("AzureAd:PostLogoutRedirectUri");
options.Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
};
});
To get everything to work I had to make small adaptations to make it look like this:
app.UseCookieAuthentication(options => { options.AutomaticAuthentication = true; });
// Configure the OWIN Pipeline to use OpenId Connect Authentication
app.UseOpenIdConnectAuthentication(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.ClientId = Configuration.Get("AzureAd:ClientId");
options.Authority = String.Format(Configuration.Get("AzureAd:AadInstance"), Configuration.Get("AzureAd:Tenant"));
options.PostLogoutRedirectUri = Configuration.Get("AzureAd:PostLogoutRedirectUri");
options.Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
};
options.AutomaticAuthentication = true;
});

Resources