Secure .NET Core API with KeyCloak.
In my previous blogs which I talked about setup .NET Aspire, we set up a solution consisting of a UI project and a back-end API project. However, the endpoints in the API project did not have any authorization or authentication mechanisms, which is not a realistic representation of a typical system.
In this blog, we aim to correct that by securing the API endpoints with an open-source solution called KeyCloak. KeyCloakis an open-source identity and access management (IAM) solution that allows us to add authentication and authorization to our application or secure service with minimal effort. It accomplishes this through many features such as user federation, authentication, user management, fine-grained authorization, and many more.
To set the stage, we will setup KeyCloak inside a container. I choose to use Podman but this solution works the same if we choose to install KeyCloak directly on our machine or via a docker container.
To talk to an API protected by KeyCloak using a client like Postman, we first need to obtain an access token from KeyCloak by sending a request with our client credentials (such as client ID and client secret) to Keycloak's token endpoint. Once we receive the access token, we include it in the Authorization header of our API requests as a Bearer token. This token verifies our identity and permissions, allowing us to access the protected API endpoints.
We will cover these topics:
Install KeyCloak & Podman
Configure KeyCloak
Update API code to support KeyCloak
Install KeyCloak & Podman:
I installed Podman on my Windows machine by following Podman's installation guide. After completing the setup, we can run podman machine init
to initialize a minimal installation. Next, run podman machine start
to start Podman.
Once our Podman is up and running, we will run the command below to install a ready build image with KeyCloak. This command runs a KeyCloak server in a container we will download from quay.io, accessible on port 8080, with the admin username and password both set to admin
, and starts KeyCloak in development mode
podman run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin
quay.io/keycloak/keycloak:25.0.4
start-dev
Once that is done, you should see our KeyCloak instance through Podman desktop.
To access KeyCloak administrator portal we simply browse to http://localhost:8080/ login using the info we created above which is admin/admin.
Configure KeyCloak:
In KeyCloak we must create the following:
Realm
Client Id
Client Credential
Realm is a separate space or environment within KeyCloak where we can manage a group of users, applications, and security settings independently from other groups. Think of it as a container that holds everything needed for a specific project or organization, including user accounts, login settings, and permissions. Each realm is isolated from others, so changes in one realm don't affect the others. This makes it easier to manage different projects or clients securely within the same KeyCloak instance.
Within each realm, we can configure as many Clients as we want, a client is an application or service that requires authentication and authorization. It could be a web application, a mobile app, an API, or any other type of service that needs to verify the identity of users and control their access to resources.
The information you configured in Keycloak is saved by default to its internal H2 database. However, if you prefer to use your own database, Keycloak can easily support it. You can find more details in the Keycloak documentation.
To create a new realm, click KeyCloak next to master realm and then click Create Realm
Click on Clients on left menu and click Create client.
After this screen, we can click Save to create the client without needing to configure Login settings. This is because we are implementing a client that is being used for the Client Credentials flow. Depending on our needs, KeyCloak allows us to create different types of clients to fit our flow. While there are many factors to consider, I generally use this high-level flow diagram to determine the flow I want to use:
Since we are just using this to secure our API application and we are testing it using Postman, the Client Credential makes most sense. You can read more about different type of flow at this excellent article written by Takahiko Kawasaki.
Upon clicking on Client, we should see the new client library-api-client
in the list, click it.
Click on the Credentials tab and we will see Client Secret value, we will use this value later.
At this point, our KeyCloak should be fully setup. We can send a post to KeyCloak token endpoint to request for a JWT token using Postman. The post request consist of the details about our client which we setup in KeyCloak to be set as the request form url encoded body.
Upon taking the access_token
value and check it against jwt.io we can see the newly generated JWT token given to us from KeyCloak contains a lot of the meta data about our client.
Currently our endpoint does not have any secure measure in place. That mean we can freely call this API and get a response:
We secure our endpoint by just putting the [Authorize]
attribute to the API endpoint. With that in place, the same call now returns a 401 unauthorized
error because it fails the authorization as our request does not contain any sort of token.
Before jumping into the code change, let's make sure we understand the high level of what we are trying to do. Here is a sequence diagram again:
Auth Token Request:
- The client sends an authentication request to KeyCloak, for client credential it needs to include the client id, client secret and other requirement for this flow.
JWT Token:
- KeyCloak authenticates the client and responds back with a JWT token.
API Request with JWT Token:
- The client sends a request to the API application, including the JWT token in the Authorization header as a bearer token.
Validate JWT Token (Only first time)
The API application makes an HTTP GET request to KeyCloak's OpenID Connect metadata endpoint to retrieve the configuration information.
Example request:
Example response:
{ "issuer": "http://localhost:8080/realms/MyRealm", "jwks_uri": "http://localhost:8080/realms/MyRealm/protocol/openid-connect/certs", "authorization_endpoint": "http://localhost:8080/realms/MyRealm/protocol/openid-connect/auth", "token_endpoint": "http://localhost:8080/realms/MyRealm/protocol/openid-connect/token", ... }
The API application uses the
jwks_uri
from the metadata to fetch the public keys used to verify the JWT token's signature.Example request:
Example response:
{ "keys": [ { "kty": "RSA", "kid": "abc123", "use": "sig", "alg": "RS256", "n": "0vx7agoebGcQSuuPiLJXZptN...", "e": "AQAB" } ] }
The API application uses the public keys from the JSON Web Key Set (JWKS) to validate the JWT token's signature.
The validation process involves:
Signature Verification: Ensuring the token's signature matches the expected signature using the public key.
Claim Verification: Checking the token's claims (e.g., issuer, audience, expiration) to ensure they are valid and meet our API application's requirements.
API Response:
- The API application sends a response back to the client based on the token's validity.
Update API code to support KeyCloak:
First, we need to add KeyCloak details and authorization information to the API project's configuration to help authorize incoming requests. For development environment, this can be done through the appsettings.Development.json
file. This settings mainly used in the Validate JWT Token step we talk just above.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Authentication": {
"MetaDataAddress": "http://localhost:8080/realms/MyRealm/.well-known/openid-configuration",
"Issuer": "http://localhost:8080/realms/MyRealm",
"Audience": "account"
}
}
Next, in our Program.cs
we only need to add couple lines of codes:
builder.Services.AddAuthorization();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.Audience = builder.Configuration["Authentication:Audience"];
x.MetadataAddress = builder.Configuration["Authentication:MetaDataAddress"]!;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Authentication:Issuer"],
ValidAudience = builder.Configuration["Authentication:Audience"]
};
});
So, what this code does is set up our .NET Core API application to use JWT Bearer tokens for authentication, and it also configures authorization services. Essentially, it tells the API application how to validate the JWT tokens by checking things like the issuer, audience, and the token's signature.
One cool thing here is the MetadataAddress
—this is used to dynamically fetch the OpenID Connect metadata. This metadata includes the destination of the public keys that are needed to verify the token's signature so it reduce the needs for us to store this value somewhere in our API application configuration.
In a nutshell, this setup makes sure that only requests with valid and authenticated tokens can access the protected parts of our API application.
By calling builder.Services.AddAuthorization()
we add authorization services to the dependency injection container. It sets up the default authorization policy and allows us to use the [Authorize]
attribute in our controllers and actions to protect endpoints.
In order to test this we have to create two separate API calls in Postman, the first one makes to KeyCloak token even point to obtain the JWT token:
Then we use this newly obtained JWT Token value in our actual API call's header to the server to get the valid 200 response back.
When I look at my .NET Aspire log I can see the log showing my two attempts on calling the same API endpoints. However, if you pay enough attentions you'd see the first api call takes 2 seconds where as the second one took mere milliseconds.
If we view the details of the first call which made to GET Book{id?}
, you'd see it contains two children calls. The first one to http://localhost:8080/realms/MyRealm/.well-known/openid-configuration
took 2.06 seconds
The next one to http://localhost:8080/realms/MyRealm/protocol/openid-connect/certs
took only 1.59ms.
However, if I look at the log when I call this same API shortly after I only see one single call and it took only 13.24 ms.
When our API application receives a JWT token, it first checks with KeyCloak to get the necessary information to validate the token. It does this by reaching out to KeyCloak's metadata endpoint to find the certificate URL where it can get the public key. This public key is essential because it allows the API application to verify that the token's signature is legitimate and that the claims within the token are valid.
Once the API application has the public key, it stores this information in a cache. This way, the next time a token needs to be validated, the API application can quickly use the cached key instead of making another call to KeyCloak. This caching mechanism makes subsequent API calls much faster because the API application doesn't have to repeatedly fetch the public key.
If we want to see other useful endpoints provided by KeyCloak, it can be seen via the endpoint http://localhost:8080/realms/MyPOC/.well-known/openid-configuration
In conclusion, I've demonstrated how to set up a simple use case that leverages Keycloak as an identity provider and management system, which we can incorporate into our application.
This blog mainly focuses on the backend API application, but I hope to showcase more of Keycloak's capabilities in future posts.
Happy Reading,
Code Reference: https://github.com/amphan613/library-managment-system/tree/94bd32bdc97ec252ab3b25fe035a4a33770c13be
Subscribe to my newsletter
Read articles from Quang Phan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by