Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Secure asp net core rest api (with keycloak)

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:

  • a keycloak docker container
  • an ASP NET core REST API (running in docker)
  • an angular front app (running in docker)

What works:

  • angular keycloak authentication
  • JWT token validation in REST API

What doesn't work:

  • any call I make to any controller decorated with the [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:

  • a realm
  • a client (openID)
  • a provider (google)

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.

enter image description here

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?

like image 323
bas Avatar asked Mar 04 '26 08:03

bas


2 Answers

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:

  • Integrating OpenID connect to you application stack
  • A JWT introduction

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:

  • I don't need them
  • I don't understand what it does
  • I have the feelings it's "yet another dead library on github"

What I will detail on below:

  • how to secure angular front app with keycloak
  • how to secure asp net core backend with keycloak
  • how to secure swashbuckle swagger with keycloak

So if you are looking for something else, or something more specific, I probably don't have the answer you are looking for. :)

Secure the front end (angular)

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:

  • A realm (projects)
  • A client (projects)
  • An identity provider (google)

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.

How to secure asp net core backend with keycloak

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:

  • Verify the JWT token (use the public key which is part of the token, and ask the keycloak instance if it's valid).
  • Once the JWT is found valid, the claims defined in the JWT can be used to do authorization.

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.

enter image description here

How to secure swashbuckle swagger with keycloak

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

enter image description here

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

enter image description here

Use Swagger in a secured Rest API without Bearer JWT

enter image description here

Retry after supplying Bearer JWT

enter image description here

like image 59
bas Avatar answered Mar 05 '26 21:03

bas


@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[] {}
    }
});
like image 39
Steve Johnston Avatar answered Mar 05 '26 21:03

Steve Johnston