Miloš Zeljko

Step-by-Step Guide: Setting up KeyCloak OAuth2 in Angular and .NET Core for Secure Authentication

Miloš Zeljko
March 20, 2023 11 mins to read
Share

Introduction

Keycloak is an open-source identity and access management solution that provides secure authentication for web and mobile applications. OAuth2 is an authorization framework that protects resources by granting access to authorized clients. In this tutorial, we will configure Keycloak 21.0.1 as an OAuth2 provider in Angular 15 and .NET Core 6.0 to enable secure authentication for our application. We will focus on configuring login and logout functionality and setting up realm roles for role-based authorization.

KeyCloak installation

In this guide, we will use KeyCloak inside a docker container. If you don’t have docker installed check here for the installation guide. Create a new file and name it docker-compose.yml. Copy the following content and place it inside the new file:

version: "3.8"
services:
  keycloak:
    image: quay.io/keycloak/keycloak:21.0.1
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
    entrypoint: ["/opt/keycloak/bin/kc.sh", "start-dev"]
    ports:
      - 28080:8080
docker-compose.yml

To start KeyCloak, open the terminal and navigate to the folder where your docker-compose.yml file is located, then run the following command:

docker compose up -d

If you want to run it directly on your machine, you can download KeyCloak here and run it following this guide

KeyCloak configuration

Open your web browser and navigate to http://localhost:28080. Click on the Administration Console and log in using:

username: admin
password: admin
keycloak 1

After logging in, open the drop-down menu on the left with master text on it, and click on the Create Realm button in order to create a realm. A realm manages a set of users, credentials, roles, and groups. A user belongs to and logs into a realm. Realms are isolated from one another and can only manage and authenticate the users that they control.

keycloak 2

Enter a desired realm name and click on the Create button.

keycloak 3

Now click on the Clients button on the left navigation bar, then the Create Client button. Clients are entities that can request Keycloak to authenticate a user. Most often, clients are applications and services that want to use Keycloak to secure themselves and provide a single sign-on solution.

keycloak 4

Under General Settings enter the Client ID for Angular application and click next. Under Capability Config leave everything default and click next. Under Login Settings fill in everything with the address at which your Angular application is hosted, mine is at http://localhost:4200. Add /* at the end of the URL for the valid redirect URIs and the valid post logout redirect URIs. Click on Save.

keycloak 5

Now repeat the process and create the client for our .NET Core application. Give it a Client ID under General Settings and click Next. Under Capability Config check the Client authentication and click Next. Under Login Settings Fill in everything the same way as for the Angular application client, but use the server address instead of the Angular application address. My server is at https://localhost:7102.

Click on the Realm Roles button on the left navigation bar and create roles for your application. In our example, we are going to use USER and ADMIN roles. After you create desired roles you should see them listed along with default roles created by KeyCloak.

keycloak 6

Now click on the Groups button on the left navigation bar and create a group for each role.

keycloak 7

Open each group and go to Role mapping.

keycloak 8

Assign corresponding roles to each group.

keycloak 9

Now let us create some accounts for our application. Click on the Users button on the left navigation bar, then on the Create new user button.

keycloak 10

Fill in the data about your test user. Don’t forget to set Email verified on true and to assign the group we created earlier. Click on Create.

keycloak 11

After the next page loads, navigate to the Credentials tab. Set the password for the created account.

keycloak 12

Repeat the process for each group you have, so that you have a test user for each group.

The last thing we need to configure is Client scopes, so that we can define the mapping between our realm roles and the roles that are going to be used by our .NET Core application in order to authorize users based on their roles.

Click on the Client scopes button on the left navigation bar, then Create client scope. Enter client scope name and select type as default. Click Save.

keycloak 13

On the next page go to Mappers tab and click on the Configure a new mapper button.

keycloak 14

Select User Realm Role from the list. Enter name for user role mapper. Check all checkboxes to be on and set Token Claim Name to be roles (do not change this as it needs to be called roles for the .NET Core application to detect it in the JWT token as a field with user roles).

keycloak 15

Now click the Clients button on the left navigation bar, then select the client for the Angular application you created earlier. Go to the Client scopes tab and click on the Add client scope button. Select the Client scope we created and add it as the Default type. Repeat the process for the dotnet client and add client scope to it as well.

keycloak 16

That is all the necessary configuration for KeyCloak to work with our application. We will now see how to connect our application to use KeyCloak as the OAuth2 provider.

Angular application setup

To connect our Angular application with keycloak, we will use the keycloak-angular library. To add it to your project type the following command in the terminal:

npm install keycloak-angular keycloak-js

After the installation of the required packages finishes, create a file init-keycloak.ts somewhere in your project and copy the following inside (don’t forget to change my-realm and my-angular-client with your realm and client names!):

import { KeycloakService } from 'keycloak-angular';

export function initKeycloak (keycloak: KeycloakService) {
  return () =>
    keycloak.init({
      config: {
        url: 'http://localhost:28080',
        realm: 'my-realm',
        clientId: 'my-angular-client',
      },
      initOptions: {
        onLoad: 'check-sso',
        checkLoginIframe: false
      },
      enableBearerInterceptor: true,
      bearerPrefix: 'Bearer',
    });
}
init-keycloak.ts

Then go to app.module.ts and add our initKeycloak() function to providers and import KeycloakAngularModule:

import { APP_INITIALIZER, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { KeycloakAngularModule, KeycloakService } from 'keycloak-angular';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { initKeycloak } from './init-keycloak';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    KeycloakAngularModule,
  ],
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: initKeycloak,
      multi: true,
      deps: [KeycloakService]
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
app.module.ts

Next, go to app.component.html and add the following code to test authentication and authorization on our frontend application:

<div *ngIf="authenticated">
  <div *ngIf="isUser">
    <h1>
      User Content
    </h1>
  </div>
  <div *ngIf="isAdmin">
    <h1>
      Admin Content
    </h1>
  </div>
</div>
<div>
  <button *ngIf="!authenticated" (click)="login()">Login</button>
  <button *ngIf="authenticated" (click)="logout()">Logout</button>
</div>
app.component.html

Also, add code in app.component.ts to handle login and logout:

import { Component } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'my-app';

  authenticated = false;
  isUser = false;
  isAdmin = false;

  constructor(private readonly keycloak: KeycloakService) {
    this.keycloak.isLoggedIn().then((authenticated) => {
      this.authenticated = authenticated;
      if (authenticated) {
        const roles = this.keycloak.getUserRoles();
        this.isUser = roles.includes('USER');
        this.isAdmin = roles.includes('ADMIN');
      }
    });
  }

  login() {
    this.keycloak.login();
  }

  logout() {
    this.keycloak.logout();
  }
}
app.component.ts

this.keycloak.login() will redirect to the KeyCloak OAuth2 server on a button click and after the user logs in, he will be redirected back to our application. So we need to check in the constructor of our component if the user authenticated successfully as our application will fully reload on redirect.

Now if we run our application with the command:

ng serve

We can see that login and logout works for both ADMIN and USER, and the application is displaying the correct content according to the role. Our library keycloak-angular will automatically add the JWT token to each HTTP request from our Angular application since we declared earlier in the configuration on initKeycloak() function enableBearerInterceptor: true. All we have to do now is to configure our backend to know how to read the JWT token and how to contact KeyCloak to validate it.

.NET Core application setup

In your ASP.NET Core 6.0 web API project go to the NuGet package manager and install Microsoft.AspNetCore.Authentication.JwtBearer.

keycloak 17

Open Program.cs and modify it to contain the following code:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    // KeyCloak
    c.CustomSchemaIds(type => type.ToString());
    var securityScheme = new OpenApiSecurityScheme
    {
        Name = "KEYCLOAK",
        Type = SecuritySchemeType.OAuth2,
        In = ParameterLocation.Header,
        BearerFormat = "JWT",
        Scheme = "bearer",
        Flows = new OpenApiOAuthFlows
        {
            AuthorizationCode = new OpenApiOAuthFlow
            {
                AuthorizationUrl = new Uri(builder.Configuration["Jwt:AuthorizationUrl"]),
                TokenUrl = new Uri(builder.Configuration["Jwt:TokenUrl"]),
                Scopes = new Dictionary<string, string> { }
            }
        },
        Reference = new OpenApiReference
        {
            Id = JwtBearerDefaults.AuthenticationScheme,
            Type = ReferenceType.SecurityScheme
        }
    };
    c.AddSecurityDefinition(securityScheme.Reference.Id, securityScheme);
    c.AddSecurityRequirement(new OpenApiSecurityRequirement{
                                                {securityScheme, new string[] { }}
                                            });
});

// KeyCloak
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

}).AddJwtBearer(o =>
{
    o.Authority = builder.Configuration["Jwt:Authority"];
    o.Audience = builder.Configuration["Jwt:Audience"];
    o.RequireHttpsMetadata = false;
    o.Events = new JwtBearerEvents()
    {
        OnAuthenticationFailed = c =>
        {
            c.NoResult();

            c.Response.StatusCode = 500;
            c.Response.ContentType = "text/plain";

            // Debug only for security reasons
            // return c.Response.WriteAsync(c.Exception.ToString());

            return c.Response.WriteAsync("An error occured processing your authentication.");
        }
    };
});

// Cross-Origin 
builder.Services
    .AddCors(options =>
    {
        options.AddPolicy("AllowOrigin",
            builder => builder.WithOrigins("http://localhost:4200")
                              .AllowAnyHeader()
                              .AllowAnyMethod());
    });


var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "MyAppAPI");
        c.OAuthClientId(builder.Configuration["Jwt:ClientId"]);
        c.OAuthClientSecret(builder.Configuration["Jwt:ClientSecret"]);
        c.OAuthRealm(builder.Configuration["Jwt:Realm"]);
        c.OAuthAppName("KEYCLOAK");
    });
}

app.UseHttpsRedirection();

app.UseCors("AllowOrigin");

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();
Program.cs

Now open appsettings.json and add the Jwt property like this:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Jwt": {
    "Authority": "http://localhost:28080/realms/my-realm",
    "AuthorizationUrl": "http://localhost:28080/realms/my-realm/protocol/openid-connect/auth",
    "TokenUrl": "http://localhost:28080/realms/my-realm/protocol/openid-connect/token",
    "Audience": "account",
    "Realm":  "my-realm",
    "ClientId": "my-dotnet-client",
    "ClientSecret": "01jYs0o0pjEefYnf6qF1sGYCKLkkh8jt"
  }
}
appsettings.json

You can find your Authority as Issuer, AuthorizationUrl as authorization_endpoint, and TokenUrl as token_endpoint in OpenID Endpoint Configuration found in your Realm settings.

keycloak 18

Leave Audience to be account, and update Realm and ClientId to match the names you have given them in KeyCloak during configuration. ClientSecret can be found under the Clients, my-dotnet-client, and Credentials tab. Generate it and copy it to appsettings.json.

keycloak 19

Now to request the user to be authenticated to access a certain endpoint on the backend, just annotate the desired endpoint with [Authorize]. If you want the user to have a certain role, annotate with [Authorize(Roles = "ADMIN")]. To permit multiple roles, separate them with commas [Authorize(Roles = "ADMIN,USER")].

To test that everything works together, import HttpClientModule in AppModule in the Angular application and modify app.component.ts to look like this:

import { HttpClient } from '@angular/common/http';
import { Component } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'my-app';

  authenticated = false;
  isUser = false;
  isAdmin = false;

  constructor(private readonly keycloak: KeycloakService, private http: HttpClient) {
    this.keycloak.isLoggedIn().then((authenticated) => {
      this.authenticated = authenticated;
      if (authenticated) {
        const roles = this.keycloak.getUserRoles();
        this.isUser = roles.includes('USER');
        this.isAdmin = roles.includes('ADMIN');
      }
    });
  }

  ngOnInit() {
    this.http.get('https://localhost:7102/WeatherForecast').subscribe({
      next: (response) =>{
        console.log(response);
      },
      error: (err) => {
        console.log(err);
      }
     });
  }

  login() {
    this.keycloak.login();
  }

  logout() {
    this.keycloak.logout();
  }
}
app.component.ts

Now if we annotate https://localhost:7102/WeatherForecast endpoint with [Authorize(Roles = "ADMIN")] we can see in browser console 401 Unauthorized response when reloading the page as an unauthenticated user. If we log in with an account that has a USER role, we will see 403 Forbidden response in the console. At last, if we log in with an account that has an ADMIN role we will see that server responded with 200 OK and the data we requested.

You can find the full source code of the project covered in this article on my GitHub by clicking here.

9 Comments on “Step-by-Step Guide: Setting up KeyCloak OAuth2 in Angular and .NET Core for Secure Authentication”

  1. Abhishek
    September 21, 2023

    Thanks, This works perfectly for me. But when I try to create a single project as Angular and ASP.NET Core this method doesn’t work for me. Do you have any ideas why?

    1. Miloš Zeljko
      September 21, 2023

      Can you provide some logs or error messages you’re getting? Nothing really comes off the top of my head why it wouldn’t work. It would help to get a bit more context.

      1. Abhishek
        September 25, 2023

        Sorry couldn’t get back to you. I created a single client for managing both my angular app and webapi. The problem is that now the Authorization header doesn’t add itself automatically as shown in your example. Adding it manually seems to fix the issue. Thanks for the great tutorial.

  2. roberto rangugni
    October 17, 2023

    Hi ! this is very great but i have a problem, i follow the full configuration, and usig the git code but i always get a 403 error in the backend when i tri to execute te controller. Any idea? thanks

    1. Miloš Zeljko
      October 17, 2023

      403 indicates that access token is valid, but is missing required role for endpoint you are accessing on .NET application. Use https://jwt.io/ to check your access token and see if it has top level property named “roles” with a role you are requiring for that endpoint. If it is missing, check client scopes and mappers configuration on keycloak, as that’s probably the misconfigured part.

  3. KM
    March 7, 2024

    Thank you so much for the article! It helped me a lot to get something working to be able to start the project.

    I found one issue, as keycloak send claims in a different format, I had to add a Claims Transformer
    using Microsoft.AspNetCore.Authentication;
    using System.Security.Claims;
    using System.Text.Json;

    namespace TestProject.App.Authorization
    {
    public class KeycloakClaimsTransformer : IClaimsTransformation
    {
    public Task TransformAsync(ClaimsPrincipal principal)
    {
    var claimsIdentities = (ClaimsIdentity)principal.Identity;
    var realmAccessClaim = claimsIdentities?.FindFirst(“realm_access”);

    if (realmAccessClaim != null)
    {
    var realmAccessAsJson = JsonDocument.Parse(realmAccessClaim.Value);
    var roles = realmAccessAsJson.RootElement.GetProperty(“roles”).EnumerateArray();
    foreach (var role in roles)
    {
    claimsIdentities.AddClaim(new Claim(ClaimTypes.Role, role.GetString() ?? string.Empty));
    }
    }

    return Task.FromResult(principal);
    }
    }
    }

  4. DS
    March 24, 2024

    this.keycloak.isLoggedIn().then((authenticated:any) =>

    i get a TS2339: Property ‘then’ does not exist on type ‘boolean’.

    1. Miloš Zeljko
      April 9, 2024

      They made a change in keycloak-angular library, in v15 and later, isLoggedIn() method returns boolean instead of Promise it used to return, so you can access the value directly now.

  5. XAMLZealot
    April 8, 2024

    Well done, Miloš! Thorough explanation, code that works straight out of source control, and clear and concise enough to help me figure out what I was missing.

    I just wanted to say thank you, I had been wrestling with Keycloak until I happened upon this tutorial, and it’s one of the highest quality step-by-steps I’ve encountered.

    Bravo, my friend!

Leave a comment

Your email address will not be published. Required fields are marked *