Authentication — identity providers
Provider integrations for Rask authentication: sign in through an external OpenID
Connect provider. The session Rask authenticates is still the
cookie — Rask.Auth owns that scheme, and a provider composes by adding a challenge scheme beside it
and signing in through the cookie. For the cookie flows and the Authorize gate, see the
main authentication guide.
Keycloak / OpenID Connect
For an external IdP, let ASP.NET's OIDC handler own the login redirect; Rask reads the resulting cookie.
Add the challenge scheme, not a cookie scheme. With the accounts battery on — the default —
Rask.Authowns the cookie and configures it last, so a.AddCookie()of your own is redundant here and its settings would be overwritten byAuthOptions. The IdP performs the challenge; the session it produces is still the battery's cookie. In an app that turned the battery off (app.Configure(c => c.Auth.Off())) the cookie is yours again — add.AddCookie()back and setDefaultSchemeyourself.
builder.Services.AddAuthentication()
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, o =>
{
// Sign in through the cookie the battery owns; the IdP only performs the challenge.
o.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.Authority = "https://keycloak.example.com/realms/rask"; // your realm
o.ClientId = "rask-app";
o.ClientSecret = builder.Configuration["Keycloak:ClientSecret"];
o.ResponseType = "code";
o.SaveTokens = true;
o.GetClaimsFromUserInfoEndpoint = true;
o.Scope.Add("roles");
o.TokenValidationParameters.RoleClaimType = "roles"; // map Keycloak realm roles → User.IsInRole
o.TokenValidationParameters.NameClaimType = "preferred_username";
});
// Challenge through the IdP instead of the built-in /login form.
builder.Services.Configure<AuthenticationOptions>(
o => o.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme);
builder.Services.AddRask(auth => auth.ChallengePath = "/login");
// app: UseAuthentication(); UseAuthorization(); MapRask<App>();
Because the challenge is an HTTP redirect to Keycloak (not an in-app form), expose plain endpoints for the round trip and point users at them:
app.MapGet("/login", (string? returnUrl, HttpContext ctx) =>
Results.Challenge(new AuthenticationProperties { RedirectUri = returnUrl ?? "/" },
[OpenIdConnectDefaults.AuthenticationScheme]));
app.MapPost("/logout", (HttpContext ctx) =>
Results.SignOut(new AuthenticationProperties { RedirectUri = "/" },
[CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme]));
A "Sign in" link (A.Href("/login?returnUrl=/secure")) sends the user through Keycloak; on return the
cookie is set and the next Rask GET/WS sees the authenticated User. Route [Authorize] pages now
challenge straight to Keycloak via ChallengePath.
Other OIDC providers
Auth0, AWS Cognito, and Duende IdentityServer integrate exactly like Keycloak:
ASP.NET's OIDC handler owns the login redirect, Rask reads the resulting cookie, and the
/login + /logout endpoints and "Sign in" link are identical. Only the AddOpenIdConnect(...) block
and the provider's logout quirk differ. Swap the relevant block into the Keycloak setup.
Auth0
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, o =>
{
o.Authority = $"https://{builder.Configuration["Auth0:Domain"]}"; // e.g. dev-abc123.us.auth0.com
o.ClientId = builder.Configuration["Auth0:ClientId"];
o.ClientSecret = builder.Configuration["Auth0:ClientSecret"];
o.ResponseType = "code";
o.SaveTokens = true;
o.Scope.Clear(); o.Scope.Add("openid"); o.Scope.Add("profile"); o.Scope.Add("email");
o.CallbackPath = "/callback";
o.TokenValidationParameters.NameClaimType = "name";
// Auth0 emits roles only if an Action/Rule adds a namespaced claim:
o.TokenValidationParameters.RoleClaimType = "https://your-app.example.com/roles";
// Auth0 logout needs its own endpoint (clears the Auth0 session, then returnTo):
o.Events.OnRedirectToIdentityProviderForSignOut = ctx =>
{
var returnTo = Uri.EscapeDataString($"{ctx.Request.Scheme}://{ctx.Request.Host}/");
ctx.Response.Redirect($"https://{builder.Configuration["Auth0:Domain"]}/v2/logout" +
$"?client_id={builder.Configuration["Auth0:ClientId"]}&returnTo={returnTo}");
ctx.HandleResponse();
return Task.CompletedTask;
};
});
Add an audience (and o.Scope.Add("...")) only if you also call an Auth0-protected API. Define roles with
an Auth0 Action that adds event.authorization.roles to the ID token under your namespace.
AWS Cognito
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, o =>
{
var region = builder.Configuration["Cognito:Region"]; // e.g. eu-west-1
var poolId = builder.Configuration["Cognito:UserPoolId"]; // e.g. eu-west-1_AbCdEf
o.Authority = $"https://cognito-idp.{region}.amazonaws.com/{poolId}";
o.MetadataAddress = $"{o.Authority}/.well-known/openid-configuration";
o.ClientId = builder.Configuration["Cognito:ClientId"];
o.ClientSecret = builder.Configuration["Cognito:ClientSecret"];
o.ResponseType = "code";
o.SaveTokens = true;
o.Scope.Clear(); o.Scope.Add("openid"); o.Scope.Add("profile"); o.Scope.Add("email");
o.TokenValidationParameters.NameClaimType = "cognito:username";
o.TokenValidationParameters.RoleClaimType = "cognito:groups"; // User Pool groups → User.IsInRole
// Cognito uses its own Hosted-UI logout endpoint:
o.Events.OnRedirectToIdentityProviderForSignOut = ctx =>
{
var domain = builder.Configuration["Cognito:Domain"]; // your-app.auth.{region}.amazoncognito.com
var logoutUri = Uri.EscapeDataString($"{ctx.Request.Scheme}://{ctx.Request.Host}/");
ctx.Response.Redirect($"https://{domain}/logout" +
$"?client_id={builder.Configuration["Cognito:ClientId"]}&logout_uri={logoutUri}");
ctx.HandleResponse();
return Task.CompletedTask;
};
});
Map users to roles by assigning them to Cognito User Pool groups — they arrive in the cognito:groups
claim.
Duende IdentityServer
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, o =>
{
o.Authority = builder.Configuration["Duende:Authority"]; // e.g. https://ids.example.com
o.ClientId = "rask-app";
o.ClientSecret = builder.Configuration["Duende:ClientSecret"];
o.ResponseType = "code";
o.UsePkce = true;
o.SaveTokens = true;
o.GetClaimsFromUserInfoEndpoint = true;
o.Scope.Clear(); o.Scope.Add("openid"); o.Scope.Add("profile"); o.Scope.Add("roles");
o.TokenValidationParameters.NameClaimType = "name";
o.TokenValidationParameters.RoleClaimType = "role";
o.SignedOutCallbackPath = "/signout-callback-oidc"; // Duende honors standard RP-initiated logout
});
Duende supports standard RP-initiated logout, so the /logout endpoint from the Keycloak section works as-is
(no provider-specific redirect needed) — Results.SignOut(... [Cookie, OpenIdConnect]) clears both sessions.
Define an IdentityResource/role claim and add it to the client's allowed scopes on the Duende side.