Rask
Rask
v0.23.1-alpha.0.496
You're reading the Rask v0.23.1-alpha.0.496 guides. Updated .View source
All guides

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.Auth owns the cookie and configures it last, so a .AddCookie() of your own is redundant here and its settings would be overwritten by AuthOptions. 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 set DefaultScheme yourself.

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.

Confirm