Contents
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.
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.ymlTo 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.
Open your web browser and navigate to http://localhost:28080. Click on the Administration Console
and log in using:
username: admin
password: admin
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.
Enter a desired realm name and click on the Create
button.
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.
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
.
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
.
Now click on the Groups
button on the left navigation bar and create a group for each role.
Open each group and go to Role mapping
.
Assign corresponding roles to each group.
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.
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
.
After the next page loads, navigate to the Credentials
tab. Set the password for the created account.
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
.
On the next page go to Mappers
tab and click on the Configure a new mapper
button.
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).
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.
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.
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.tsThen 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.tsNext, 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.htmlAlso, 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.tsthis.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.
In your ASP.NET Core 6.0 web API
project go to the NuGet package manager
and install Microsoft.AspNetCore.Authentication.JwtBearer
.
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.csNow 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.jsonYou can find your Authority
as Issuer
, AuthorizationUrl
as authorization_endpoint
, and TokenUrl
as token_endpoint
in OpenID Endpoint Configuration
found in your Realm settings
.
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
.
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.tsNow 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.
Abhishek
September 21, 2023Thanks, 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?
Miloš Zeljko
September 21, 2023Can 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.
Abhishek
September 25, 2023Sorry 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.
roberto rangugni
October 17, 2023Hi ! 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
Miloš Zeljko
October 17, 2023403 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.
KM
March 7, 2024Thank 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);
}
}
}
DS
March 24, 2024this.keycloak.isLoggedIn().then((authenticated:any) =>
i get a TS2339: Property ‘then’ does not exist on type ‘boolean’.
Miloš Zeljko
April 9, 2024They 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.
XAMLZealot
April 8, 2024Well 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!