PrettyNeat.Keycloak
1.2.1
See the version list below for details.
dotnet add package PrettyNeat.Keycloak --version 1.2.1
NuGet\Install-Package PrettyNeat.Keycloak -Version 1.2.1
<PackageReference Include="PrettyNeat.Keycloak" Version="1.2.1" />
paket add PrettyNeat.Keycloak --version 1.2.1
#r "nuget: PrettyNeat.Keycloak, 1.2.1"
// Install PrettyNeat.Keycloak as a Cake Addin
#addin nuget:?package=PrettyNeat.Keycloak&version=1.2.1
// Install PrettyNeat.Keycloak as a Cake Tool
#tool nuget:?package=PrettyNeat.Keycloak&version=1.2.1
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)
- The setting of the
OAuthCatchPath
would mean that the authorization codes from OIDC logic would be redirected tohttps://www.secureclient.org/auth/oauth/receive
which should be expected to retrieve acode
query parameter - The
Realm
setting has to be precise as it needs to be used in the calls to KeyCloak URIs, such ashttps://keycloak.secureclient.org/auth/realms/kc_SecureClient/protocol/openid-connect/auth
and this is case sensitive - The
ServiceId
andAdminId
accounts may be the same one - this is irrelevant. TheAdminId
account has to have the rights to access and modify the users of the Realm - 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 tolocalhost
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 aKCTokenizedIdentityResponse
if the authentication was successful, with claims and tokens for the user resulting from the authentication - The
HttpErrorCode
andErrorMessage
properties can help identify issues ifSuccess
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 acode
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 (includingcredentials
and correctOrigin
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 | Versions 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. |
-
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.
Minor change to allow custom redirect URI with brokered login.