PrettyNeat.Keycloak 1.3.2

dotnet add package PrettyNeat.Keycloak --version 1.3.2
NuGet\Install-Package PrettyNeat.Keycloak -Version 1.3.2
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PrettyNeat.Keycloak" Version="1.3.2" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add PrettyNeat.Keycloak --version 1.3.2
#r "nuget: PrettyNeat.Keycloak, 1.3.2"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install PrettyNeat.Keycloak as a Cake Addin
#addin nuget:?package=PrettyNeat.Keycloak&version=1.3.2

// Install PrettyNeat.Keycloak as a Cake Tool
#tool nuget:?package=PrettyNeat.Keycloak&version=1.3.2

PrettyNeat KeyCloak Provider

Intro and scope

This package should be used on the "Client" server to communicate with a KeyCloak ("OP") instance. This is useful especially if the KeyCloak instance is concealed either through a DMZ or by using back-channel communication when redirection to a separate service/server compromises the UX.

Configuration

The configuration file must be present in the root folder of whichever project is actually using the package and has several values which must be filled in if the package is to work. It is vastly important to fill all of the values.

in appsettings.json

{
  "KeyCloak": {
    "ServiceId": "case-sensitive id of service account (for logins)",
    "ServiceSecret": "secret key of service account",
    "AdminId": "case-sentitive id of account with admin role (for API calls) - may be the same as above",
    "AdminSecret": "secret key of account with admin role",
    "PublicUrl": "scheme://host where your Client application is publicly available",
    "OAuthCatchPath": "relative, no-trail-slash path to the endpoint cathcing auth codes",
    "ApiUrl": "scheme://host where the KeyCloak instance is publicly available",
    "PublicLogoutPath": "scheme://host where the KeyCloak instance will send OIDC Logout POSTs",
    "Realm": "case-sensitive realm name"
  },
  
  "Example": {
    "ServiceId": "broker",
    "ServiceSecret": "aaBBDd43fgFFgrGG5TGHYh",
    "AdminId": "realm-service",
    "AdminSecret": "GHYhBd4BDaaG5T3fgFFgrG",
    "PublicUrl": "https://www.secureclient.org",
    "OAuthCatchPath": "auth/oauth/receive",
    "ApiUrl": "https://keycloak.secureclient.org/auth",
    "PublicLogoutPath": "https://www.secureclient.org/auth/oauth/logged-out",
    "Realm": "kc_SecureClient"
  }	
}

notes (relative to the Example)

  1. The setting of the OAuthCatchPath would mean that the authorization codes from OIDC logic would be redirected to https://www.secureclient.org/auth/oauth/receive which should be expected to retrieve a code query parameter
  2. The Realm setting has to be precise as it needs to be used in the calls to KeyCloak URIs, such as https://keycloak.secureclient.org/auth/realms/kc_SecureClient/protocol/openid-connect/auth and this is case sensitive
  3. The ServiceId and AdminId accounts may be the same one - this is irrelevant. The AdminId account has to have the rights to access and modify the users of the Realm
  4. The logic for the PublicLogoutPath being absolute rather than relative is both to allow localized testing via public tunnels such as ngrok while still allowing redirects to localhost in other instances and to have a logout server separate from the main instance - in cases where keycloak is shared and logout needs to propagate. It can (and most likely will) be on the same domain as the catch.

Using the provider

The static method AddKeyCloakProvider() will inject KeyCloakProvider as a Singleton, which is the recommended usage, mostly for proper retention of admin tokens. The provider handles connections using HttpClientFactory and the common pool.

Calling methods that modify users via the API will trigger a pre-requisite call to retrieve a valid admin token by using the AdminId account.

Specifically, these methods use/require administrative rights on the KeyCloak account:

  • RegisterUserViaBackend
  • ManipulateUserAttribute
  • ChangePasswordViaBackend

The provider does NOT cater for the following:

  • The storage of tokens transferred to the Client- the recommendation for OIDC is storage and transmission using the JWT standard
  • The automation of Authentication/Authorization- these may be managed through inbuilt Authn/Authz middleware or custom implementations
  • Caching/checking of transmitted credentials - tokens may be introspected at any time to check for validity

About KeyCloak

While the installation, configuration and general management of KeyCloak merit books on their own, it is important to underline just a few points about the requirements of the configuration:

  • KeyCloak need not be entirely publicly available, however at least some endpoints should be. A brief reference may be viewed here (lr 09/22)
  • KeyCloak, especially for brokered, social logins, will require a hostname which is accessible by the End User, rather than the Client - that is, the user/browser using the application.
  • KeyCloak's accounts aren't always especially clear - admin roles are usually available only in the master realm, however any of the -service accounts can be granted roles and scopes to call API endpoints

A QuickStart

This quickstart is made to aid in the installation, configuration and first steps of using KeyCloak as a provider of authentication for any project

Foreword

Throughout the documentation of both KeyCloak, OpenIDConnect and the OAuth2 protocol's RFCs, the following conventions are observed:

  • The OP is the Oidc Provider, that is, the server granting identity services (Identity Server). For us, this means KeyCloak
  • The RP is the Replying Party which is usually the other party communicating with the Identity Server. This is usually the application in itself, not the user of the application.
  • The application using the services of the Identity Server is generally defined as the Client outside of descriptions of request/response sequences
  • When the actual user of the application - that is, the user of the Client - is involved in the process, it is referred to as End User

This Quickstart is going to stick to these definitions whenever possible. Additionally, we are going to refer to the actions taken, whenever possible, by their correct name in the OIDC nomenclature - this will allow easily finding the proper references (if needed)

The Setup

Check your toolbox for the following:

  • A running instance of KeyCloak, configured with at least one realm, ideally also have at least one account ready to do the grunt work
  • An app in C# that needs authentication
  • A reference/local copy/smoke signals of the Microsoft.AspNetCore.Authentication.JwtBearer nuget package - this is for handling JWTs and version 6.0.7 will be used, but it can be omitted if you want to save your stuff cleartext (like MS Teams does references current events)
  • While most things will work on localhost, a publicly available endpoint is required for the logout POST

Add the KeyCloak Provider

You can use the extension method AddKeyCloakProvider() contained in the PrettyNeat.Keycloak.Helpers namespace. Alternatively, you may just register the class KeyCloakProvider in your services. The helper registers it as a Singleton.

Configure the KeyCloak Provider

Refer to the configuration section in this readme to add a KeyCloak JSON object to your appsettings.json.

Add some extremely basic Authentication and Authorization

To gain access to the very basic functionality of authentication and authorization, even in a Minimal API scenario, you want to use the AddAuthentication() and AddAuthorization() extension methods in your service compilation region. For the time being, the parameter-less constructors will do, although they will likely have to be adjusted later.

At this stage, for the purpose of testing, just ensure that any one test endpoint of the application requires authorization, by using either the [Authorizaton(...)] attribute or adding the RequiresAuthorization() call to the endpoint construction.

Easier auth - Direct Grant

A Direct Grant is a way of obtaining authentication information that short-circuits some steps of the classic OAuth/OIDC cycle:

  • The End User submits the credentials directly to the Client, usually through a form
  • In the backend, the Client securely authenticates with the Identity Server and passes down the credentials
  • The Identity Server attempts to perform an authentication with the provided credentials and, if successful, returns tokens
  • The End User now has an active session with the Identity Server and the tokens can be exchanged for claims or refreshed

To produce this effect:

Register a user in KeyCloak

Registration of a user will then be optionally moved to the Client app, but for now it's just important to have a user for which credentials are known.

Collect the user's credentials

How the credentials are collected depends on your Client, most likely you will have a form sending data to an endpoint, although the exact mechanics are not important, as long as both the username and the password reach the Client unmodified (as it needs to pass them as-is).

Submit the request of a Direct Grant

The credentials have to be passed to the DirectGrantLogin(string username, string password) and once the method is awaited, it will return a KeyCloakProviderResponse object:

  • You may check the Success property to ensure that the authentication is valid - this is enough to ascertain that the authentication went well
  • The Result property will contain a KCTokenizedIdentityResponse if the authentication was successful, with claims and tokens for the user resulting from the authentication
  • The HttpErrorCode and ErrorMessage properties can help identify issues if Success is false

At this point, the End User needs to be marked as Authenticated on the side of the Client application - while how this is exactly done may wildly vary, the suggest solution will use JWT tokens:

Retain the result of the Direct Grant

A common way to maintain a local login information between an End User and a Client application is the usage of JWT tokens - this is also the default assumption of the OIDC documentation and the transmission protocol of the OIDC tokens.

If the authentication is unsuccessful, the only thing returned should be either a 401 Unauthorized or a 401 Challenge with a redirect, or a 403 Forbidden, mildly depending on the logic.

If the authentication is, however, successful, it's easy to create a new Identity and save it inside the context.

The most straightforward way to handle JWT is to add the default JWT Bearer handler to the Authentication in the services:

// in the Services initialization

// Expand Authentication extension by declaring the usage of JWT scheme
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
// Add the options for the token
.AddJwtBearer(options =>
{
    options.SaveToken = true;
    options.RequireHttpsMetadata = false;
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateIssuer = true,      //Use validation to check the Issuer claim
        ValidateAudience = true,    //Use validation to check the Audience claim
        ValidAudience = configuration["JWT:ValidAudience"], //Get a standard, config-bound Audience
        ValidIssuer = configuration["JWT:ValidIssuer"],     //Get a standard, config-bound Issuer
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:Secret"]))    //Use a config-bound 'Secret' to generate a static, symmetric key 
    };
});

The above code expects some extra parameters in the appsettings.json -

  "JWT": {
    "ValidAudience": "http://localhost:5000", 
    "ValidIssuer": "http://localhost:5000",
    "Secret": "JWTAuthenticationHIGHsecuredPasswordVVVp1OH7Xzyr"
  }

Both ValidAudience and ValidIssuer are expected to be valid urn: values, while Secret is a string seed.

In the standard C# application structure, a user's identity within the context of a web request is retained in the HttpContext.User as a ClaimsPrincipal, which contains several Claims about the identity.

Constructing a principal starts with creating a list of the Claim type, usually using the common and/or RFC approved types:

    //code of method handling the retrieval of username and password omitted

    var userResult = await keycloakProviderSvc.DirectGrantLogin(username, password);

    //validation omitted
    if (!userResult.Success){ /* ... */ }

    var userClaims = new List<Claim>
    {
        new Claim(ClaimTypes.Name, userResult.Result.User.name),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
    };

    //add additional claims as fit

    //assume configurationSvc is IConfiguration

    var jwtKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configurationSvc["JWT:Secret"]));

    var token = new JwtSecurityToken(
        issuer: configurationSvc["JWT:ValidIssuer"],
        audience: configurationSvc["JWT:ValidAudience"],
        expires: DateTime.Now.AddHours(3), //any expiration as seen fit
        claims: userClaims,
        signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
        );

    //the JwtSecurityTokenHandler is a helper class forming part of the JwtBearer package
    //this is used to properly serialize the JWT token and make it passable as Bearer authorization

    return Ok(new
    {
        token = new JwtSecurityTokenHandler().WriteToken(token),
        expiration = token.ValidTo
    });

As per above, once the list of claims is constructed, the token can be prepared using the standard issuer, audience and key, that have to match the declared configuration. Finally, the token is prepared for sending to the frontend and returned, enabling authentication for all future requests if passed along.

It is important to note that any frontend receiving the OK 200 response will have to save the token for future usage.

The token should be saved and passed on as a Bearer authorization header- this code part (added previously) handles the other side of the authentication:

    builder.Services.AddAuthentication(options =>
    {
        //By default, attempt to authenticate using middleware of the "JwtBearerScheme"
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;

        //If authentication fails, issue a 401 by using default middleware of the scheme
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

        //For any other circumstance, refer to this scheme
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    })

Proper OAuth/OIDC

So far, authentication works in a simple, yet effective way. However, from the point of view of a typical Identity Server, the big issue is the lack of a proper third-party communication:

OAuth uses a three-point system to asks the user consent outside of the influence of the Client ⇒

important: while the following charts may help understand how and why the KeycloakProvider works but not necessary for the quickstart.

here comes theory

OAuth:
    
|   Client App  |   User    |   ID/Data Server  |
|               '           '                   |
| asks for user data ------------------,        |
|                                      |        |
|                    ,-- asks consent to user   |
|                    |                          |
|           consents to give access ---,        |
|                                      |        |
|      ,--------------- provides an auth token  |
|      |                                        |
|   exchanges token for data --/ <-> /---(      |

OIDC uses a convention-restricted OAuth loop for Authentication ⇒

OIDC:
    
|   Client App  |   User    |  Identity Server  |
|               '           '                   |
|       ,------asks to be verified              |
|       |                                       |
| asks Identity Server for identity----,        |
|                                      |        |
|                    ,-- asks consent to user   |
|                    |                          |
|           consents to be verified----,        |
|                                      |        |
|      ,----------------provides an auth token  |
|      |                                        |
|   exchanges token for identity-------(        |

Direct grant effectively makes the Client a middle-man, breaking the three-point trust system ⇒

OIDC:
    
|   Client App  |   User    |  Identity Server  |
|               '           '                   |
|       ,------asks to be verified              |
|       |                                       |
| asks credentials----,                         |
|                     |                         |
|              provides credentials             |
|                     |                         |
| passes credentials--'                         |
|       |                                       |
|       '------ exchanges creds with auth token |
|                                      |        |
|      ,-------------------------------'        |
|      |                                        |
|   exchanges token for identity-------(        |

no more theory: from here onwards, proceed with the quickstart.

To provide a more proper alternative, a minimal OIDC/OAuth flow should be implemented. This is also mandatory for a brokered social login, which sees the Identity Server act as a Client to another server which holds Identity details, such as social logins with popular platforms.

In effect this means that KeyCloak authenticates the user with, e.g., Google, verifies the authentication and creates a valid session in KeyCloak.

For the Client App a normal OIDC login or a brokered social login work effectively the same, as KeyCloak, or the Identity Server (OpenID Provider) is the only point of contact.

You will require the following:

  • An endpoint which can accept the auth-code returned by the Identity Server - this will be a GET redirect which will contain a code query parameter that needs to be captured
  • If a Social Login is required, valid API credentials within the chosen provider must be obtained and correctly configured in KeyCloak
  • If the backend is an API hosted on a separate Origin, proper CORS settings (including credentials and correct Origin settings) should be provided.

Catching the auth-code

KeyCloak must be configured to accept the redirect_uri parameter that will be provided. KeyCloak can accept wildcard-based URIs or any URI (wildcard *).

It's important to note that the user must be redirected to a URI on the Identity Server while also providing a valid redirect_uri as part of the query string - it's an OIDC requirement and the response will be sent to that URI

While there are several frontend libraries to generate the URI, the basic format is as follows:

{KeyCloakUri}/auth/realms/{realmName}/protocol/openid-connect/auth?scope=openid,profile,email&response_type=code&client_id={clientId}&redirect_uri={catchUri}

This URI is broken down as follows:

URI explanation
{KeyCloakUri} root domain of KeyCloak instance
/auth/realms/ static path
{realmName} case-sensitive name of the realm to use (as per KeyCloak)
/protocol/openid-connect/ static path for OIDC endpoints
auth static path for the OIDC authorization endpoint
? Beginning of Query parameters
scope= the scopes or types of information requested. openid must be present
response_type= type of authentication response required. For the typical flow it's always code
client_id= case-sensitive id of the public client to use, as configured in KeyCloak
redirect_uri= absolute path where the End User will be redirected after completing the authentication. Must be registered in KeyCloak for the client

Once the End User is redirected, using a simple href to this link, they will be able to log-in. After they do, a code will be received at the endpoint in redirect_uri.

It is very important that the OAuthCatchPath configuration parameter is correctly set to match the redirect_uri that will be provided in the link.

Completing the authentication

Once the code is received, it may be passed directly to the BrokeredCodeLogin(string access_token, bool custom_claims = false) method of the provider.

This will return the same type of information as the previous authentication flow:

    //code of method handling the retrieval of code omitted

    var userResult = await keycloakProviderSvc.BrokeredCodeLogin(code);

    //unless claims containing information not present in the user_info scope as defined in OIDC are required, they should not be requested
    //omitting the search for custom claims makes the processing of the response slightly faster and more secure

    //validation omitted
    if (!userResult.Success){ /* ... */ }

    var userClaims = new List<Claim>
    {
        new Claim(ClaimTypes.Name, userResult.Result.User.name),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
    };

    //the rest of the code is the same as above

The retrieved information can be used exactly as the one retrieved by Direct Grant.

The procedure to directly initiate a Brokered Social Login is the same as above, however with the addition of one query parameter:

URI explanation
(...) (as above up to the query params)
kc_idp_hint= This query parameter must have the value of the id of a brokered idp, such as google or facebook (the values are in KeyCloak)
(...) (rest of the params as above)

Important note about Brokered IdP providers

Once an external IdP is registered, it will be also accessible through the login page, unless explicitly hidden via configuration in KeyCloak. By default, KeyCloak will not return information on which Brokered IdP (if any) has authenticated the client.

This information can be retrieved although it's usually not relevant for basic Auth operations.

Token management

(tbd)

Basic user management

(tbd)

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net6.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.3.2 372 7/20/2023
1.2.1 653 9/26/2022
1.2.0 410 9/19/2022
1.0.0.3 619 5/25/2022

Quality of life improvements with exception handling and logging