I am trying, really, I am. But I just can't make sense out of the concept of authorization in ASP NET core.
I have:
What works:
What doesn't work:
[Authorize] attribute.I've read this, this, this, every MSDN page on asp net core authentication/authorization I could find, and I've tried many different nuget packages to get the job done. But nothing gives.
The achieved most using Keycloak.AuthServices.Authentication and Keycloak.AuthServices.Authorization, but even there I get stuck on the part where I actually want to authorize. Whatever I try, I keep getting 401 responses on [Authorize] decorated calls.
My keycloak configuration is pretty straight forward:
Seems to do what it has to, I can authenticate in my angular frontend app.
The angular app uses keycloak-angular which I've setup to copy the bearer JWT into my https headers for each request I make to my REST API backend.

In my ASP NET core backend I've setup the Keycloak.AuthServices middleware as follows:
Added this section in my appsettings.Development.json:
"Keycloak": {
"realm": "projects",
"auth-server-url": "http://keycloak/keycloak",
"ssl-required": "none",
"resource": "projects",
"verify-token-audience": false,
"credentials": {
"secret": ""
},
"confidential-port": 0
},
The http://keycloak url is the docker network url. It's valid. When I change it to something which doesn't exist I get errors that the API can't reach the keycloak backend. When I set the correct address, the api will allow any call from the front app, as long as the controller method is not decorated with the [Authorize] attribute.
The only thing I needed to do in my ASP NET core app is:
// add keycloak auth service
builder.Services.AddKeycloakAuthentication(builder.Configuration);
// Specify that I want to use both authentication and authorization middleware
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseCors(PolicyName);
app.MapControllers();
According to the author of Keycloak.AuthServices, this should be the minimal app that secures the REST api. I should be able now to reject [Authorize] decorated API calls without a valid bearer JWT, and allow them with a valid bearer JWT. But it won't... It will result in a 401 with or without a valid bearer JWT.
The most annoying part is, that I just can't "debug" anything. Pretty much everything happens "under the hood". Which is great, if things are obvious, and more importantly, if things work. But if there are issues, this is massively frustrating...
Is there anybody capable of shedding a light on this nightmare? Tips for debugging? Tips for drilling down?
I'll share all I've learned, in the hope it helps somebody else. Let's start with answers in the few good blogs out there!
What helped me a great deal to understand how the world of authentication works is:
In contrast to the massive amount of other pages I've read with all kinds of fragments that I need to piece together from different "examples", these pages actually explained the theory behind it which was really helpful in my case.
I also ditched the Keycloak.AuthServices.Auth* packages for three reasons:
What I will detail on below:
So if you are looking for something else, or something more specific, I probably don't have the answer you are looking for. :)
I think this is the easiest part, at least it was for me. I first had to setup keycloak of course, but there are plenty of pages that properly explain this part I will not add more noise. I will share what I have in my keycloak instance:
I made my angular authenticate with keycloak (well, with google, really) by integrating keycloak-angular. If there are better libraries, I am all ears, I found this one, and it worked.
I followed their official documentation and it worked pretty much straight out of the box.
I can authenticate by simply using their KeycloakService as follows
constructor(
private readonly keycloak : KeycloakService) {
}
async login() {
var isLoggedIn = await this.keycloak.isLoggedIn();
if (!isLoggedIn) {
await this.keycloak.login();
}
}
Maybe on thing worth noting. I did add a thing or two to the initialize function in my app.module.ts which is very convenient in the next steps to get the JWT in the backend API.
function initializeKeycloak(keycloak: KeycloakService) {
return () =>
keycloak.init({
enableBearerInterceptor: true,
config: {
realm: 'projects',
url: `${environment.keyCloakUrl}`,
clientId: 'projects'
},
initOptions: {
onLoad: 'check-sso',
silentCheckSsoRedirectUri:
window.location.origin + '/assets/silent-check-sso.html'
}
});
}
Here, the enableBearerInterceptor: true injects a http interceptor which will copy the 'Bearer JWT token' in every http call the app will make, thus providing the JWT token to the backend API (part of the http header, with the 'Authorization' key).
In above code I also replaced the hardcoded url so that I can retrieve it from my environment. This way I can setup a url I want to use in development, and a url I want to use when the app runs on my AWS EC2 instance.
After having been through a giant forest of misleading / simply untrue / vague suggestions found throughout the internet, this turns out to be really simple. As perfectly explained in the first link in this answer (integrating OpenID connect) "authentication" in the backend boils down to two things:
For the first part (verify JWT) all I need to do is provide the keycloak url which "exposes" the API methods of keycloak. The rest will be taken care of by ASP NET core. This is done by adding the "authentication" service in the startup code.
I keep quoting the "authentication" because for me this was very confusing. I understand there are different approaches when it comes to the stack of a web app. I use angular as front end, and ASP NET rest API as backend. But of course anguar could be replaced by ASP NET MCV. The "authentication" happens in the frontend. Well, in my head anyway ;-). So every time I found stuff on google which detailed on "authenticaton in ASP NET core" I constantly had the feeling that it was in the context of ASP NET MVC. But, turns out that "checking the Bearer JWT" is also associated with "authentication". I am sure it makes sense, it just didn't for me.
I personally like to keep my Program.cs a bit organized, so I simply follow the extension method approach of ASP NET core, and put specific "setup/configure" code in extension methods. For the authentication service I ended up with the following:
public static IServiceCollection AddKeycloakAuthentication(this IServiceCollection services, IConfiguration configuration)
{
// https://dev.to/kayesislam/integrating-openid-connect-to-your-application-stack-25ch
services
.AddAuthentication()
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = Convert.ToBoolean($"{configuration["Keycloak:require-https"]}");
x.MetadataAddress = $"{configuration["Keycloak:server-url"]}/realms/projects/.well-known/openid-configuration";
x.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "groups",
NameClaimType = $"{configuration["Keycloak:name_claim"]}",
ValidAudience = $"{configuration["Keycloak:audience"]}",
// https://stackoverflow.com/questions/60306175/bearer-error-invalid-token-error-description-the-issuer-is-invalid
ValidateIssuer = Convert.ToBoolean($"{configuration["Keycloak:validate-issuer"]}"),
};
});
services.AddAuthorization(o =>
{
o.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireClaim("email_verified", "true")
.Build();
});
return services;
}
From a higher level perspective, this tells ASP NET core it will handle authentication by using the Bearer JWT, and it will do authorization by requiring an authenticated user which has a verified email. In my case the verified email is probably nonsense since I am only allowing a google provider which by definition will have a valid email. But whatever.
The Bearer JWT definition is probably more interesting.
The first JwtBearerOption is RequireHttpsMetadata. I am using keycloak in http mode in development, and in https in production. I do this, because https would require a certicate. When I am running in development, I can only provide a self signed certificate (since certificates for localhost don't exist afaik). Using a self signed certificate will be result in errors when ASP NET will call into the keycloak api, because it will try to validate the self signed cert and that will fail. In a browser, you would be able to accept the risk. But since my rest api is not running in the browser, that's a dead end. In order to validate tokens over http, you must specify RequireHttpsMetadata = false in the options.
The next property MetadataAddress is straight forward. Here I provide the url of keycloak, where keycloak exposes it's own API. The url typically looks like 'http://your-keycloak-url/realms/your-keycloak-realm/.well-known/openid-configuration'.
The token validation parameters are used to extract certain information from the JWT and provide it to ASP NET default structure. But it also tells ASP NET core how to validate, what to validate, etc.
The NameClaimType is only interesting if you want to map some field in the JWT token to the HttpContext.User.Identity.Name property. In my case I want to have the email address of the google account mapped to the user name.
The "configurable" data is obviously coming from my app settings. I'll share them here to give a complete overview if needed.
appSettings.Development.json
"Keycloak": {
"server-url": "http://keycloak/keycloak",
"audience": "account",
"name_claim" : "preferred_username",
"validate-issuer": false,
"require-https": false
},
appSettings.Production.json
"Keycloak": {
"server-url": "https://projects-admin.ddns:444/keycloak",
"audience": "account",
"name_claim" : "preferred_username",
"validate-issuer": true,
"require-https": true
},
After adding services in the startup code, the builder is used to provide the actual WebApplication. There I only need to specify which middlewares to use (both authentication and authorization).
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseCors(policyName);
app.MapControllers();
if (app.Environment.IsDevelopment() || app.Environment.IsStaging())
{
app.UseSwagger();
app.UseSwaggerUI(options => options.EnableTryItOutByDefault());
}
I have read a lot of warning signs on the order of adding middlewares. Either I copy/pasted stuff by accident in the right order, or things changed over time making that less of a hassle. Anyway, I never ran into that specific problem. This order seems to work fine for me.
With all the code in place, the only thing left to do is decorate some controllers or methods with the Authorize attribute and they will return 401s when not providing a valid JWT (hence, calling them from anything else than my angular app).
[Authorize]
[Route("api/[controller]")]
public class ProjectController : Controller
{
private readonly IProjectsService _projectsService;
private static readonly ILog Log = LogManager.GetLogger(typeof(ProjectController));
public ProjectController(
IProjectsService projectsService
)
{
_projectsService = projectsService;
}
[HttpGet]
[Route("all")]
public async Task<IActionResult> GetAll()
{
var projects = await _projectsService.GetAll().ConfigureAwait(false);
return Ok(projects);
}
}
In the above example I decorated the controller, making all API methods in the class protected. I know I copy pasted only one, in my actual class there are more :). If I would want to make one method available without a JWT token, I could decorate that method with [AllowAnonymous]. Otherwise without a valid JWT the ASP NET will return 401 - unauthorized.

The only thing left to do, is to make swagger work again. Because I've just protected all my controllers, making them effectively unavailable for swagger too.
Again, here, I've traveled through the internet for way longer than I will admit. It seems there are many approached that can be taken, from providing the Bearer JWT, to logging into a dedicated swagger client in keycloak, to doing the actual authentication again using the google provider. I only succeeded in using the Bearer JWT (copying it manually from the web browser which runs my angular app). I guess not the most convenient way, or maybe it is. I don't care, I'll go for it anyway :).
public static IServiceCollection AddSwaggerApplication(this IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Projects-admin Swagger", Version = "v1" });
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "Please provide JWT with bearer (Bearer {jwt token})",
Name = "Authorization",
Type = SecuritySchemeType.ApiKey,
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
},
new List<string>() }
});
});
return services;
}
This provides the following dialog when I press "Authorize" in my swagger page

When I copy/paste the Bearer JWT from my browser and hit Authorize, I am using the API as if I am calling it from my angular app. Of course, the JWT will expire, so if I wait to long, I will need to repeat these steps. It's all I have at the moment.
Copy the Bearer JWT

Use Swagger in a secured Rest API without Bearer JWT

Retry after supplying Bearer JWT

@bas Your notes in your response (https://stackoverflow.com/a/77104803/802379) were very helpful in getting me started. Thanks!
I was also able to get OAuth2 set up in swagger as follows, so I would not have to paste a token into Swagger. I had to enable the Implicit flow in Keycloak for my client.
// See https://stackoverflow.com/questions/66265594/oauth-implementation-in-asp-net-core-using-swagger
c.SwaggerDoc("v1", new OpenApiInfo { Title = "CombiTime API v1.0", Version = "v1" });
c.AddSecurityDefinition("OAuth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
Implicit = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri("http://localhost:8024/realms/myproject/protocol/openid-connect/auth"),
}
}
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement{
{
new OpenApiSecurityScheme{
Reference = new OpenApiReference{
Type = ReferenceType.SecurityScheme,
Id = "OAuth2" //The name of the previously defined security scheme.
}
},
new string[] {}
}
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With