Cookie authentication plus OpenID Connect against broker OIDC discovery. Each step is copy-paste ready — use WSO2 URLs, not the eID API directly, for production RPs.
curl eID /api/public/government-services?excludeClientId={ClientId} — tiles after admin approval
Generic copy-paste steps using placeholder client credentials. After onboarding, open your application in the RP portal for guides pre-filled with your client ID and redirect URIs.
Step 1
Architecture overview
Use ASP.NET Core OpenID Connect middleware against the SXM Identity Broker. The broker federates to the national eID UI — configure discovery, token, and UserInfo against WSO2, not the eID API (port 5026) in production.
Do not commit ClientSecret. Use user secrets in development.
shell
cd src/YourRpWebApp
dotnet user-secrets init
dotnet user-secrets set "Authentication:Schemes:OpenIdConnect:ClientSecret" "YOUR_CLIENT_SECRET"
Step 5
Register authentication in Program.cs
PKCE is required. GetClaimsFromUserInfoEndpoint merges broker UserInfo with the id_token. Use prompt=login for interactive login — never prompt=consent from the RP.
WSO2 may return custom attributes as full URIs. Map them to short names eid, citizen_status, rp_token so controllers and EF use consistent keys.
csharp
using System.Security.Claims;
static void NormalizeWso2CustomClaims(ClaimsPrincipal? principal)
{
if (principal?.Identity is not ClaimsIdentity identity)
{
return;
}
var map = new Dictionary<string, string>(StringComparer.Ordinal)
{
["http://wso2.org/claims/custom/eid"] = "eid",
["http://wso2.org/claims/custom/citizen_status"] = "citizen_status",
["http://wso2.org/claims/custom/rp_token"] = "rp_token",
};
foreach (var (uri, name) in map)
{
var existing = identity.FindFirst(name);
var fromUri = identity.FindFirst(uri);
if (existing is null && fromUri is not null)
{
identity.AddClaim(new Claim(name, fromUri.Value));
}
}
}
Step 7
Match redirect URI with WSO2 registration
Default middleware callback is {your-app-origin}/signin-oidc. Registered redirect in WSO2 must match exactly.
text
# Registered in WSO2 for RP-EXAMPLE-0001 (example):
https://my-portal.gov.sx/auth/callback
# Default OIDC middleware callback (if app runs at https://localhost:5001):
https://localhost:5001/signin-oidc
# Post-logout redirect (configure in WSO2 + SignedOutCallbackPath):
https://my-portal.gov.sx/
Step 8
Read identity claims in controllers
Approved claims: sub, name, given_name, family_name, email, birthdate, citizen_status, rp_token, eid. Broker username is not the eID card number — use claim eid when present.
csharp
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[Authorize]
public class DashboardController : Controller
{
public IActionResult Index()
{
var sub = User.FindFirstValue("sub");
var email = User.FindFirstValue(ClaimTypes.Email) ?? User.FindFirstValue("email");
var name = User.FindFirstValue(ClaimTypes.Name) ?? User.FindFirstValue("name");
var eid = User.FindFirstValue("eid");
var citizenStatus = User.FindFirstValue("citizen_status");
var rpToken = User.FindFirstValue("rp_token");
ViewData["Subject"] = sub;
ViewData["Email"] = email;
ViewData["Name"] = name;
ViewData["Eid"] = eid;
ViewData["CitizenStatus"] = citizenStatus;
ViewData["RpToken"] = rpToken;
return View();
}
}
Step 9
Persist user after sign-in (EF Core excerpt)
Sync broker sub and eid-scope fields to your database on token validation. Adjust entity names to your project.
csharp
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = async context =>
{
var sub = context.Principal?.FindFirstValue("sub");
if (string.IsNullOrEmpty(sub))
{
return;
}
var db = context.HttpContext.RequestServices.GetRequiredService<AppDbContext>();
var user = await db.PortalUsers.FirstOrDefaultAsync(u => u.OidcSub == sub);
if (user is null)
{
user = new PortalUser { OidcSub = sub };
db.PortalUsers.Add(user);
}
user.Email = context.Principal?.FindFirstValue("email");
user.DisplayName = context.Principal?.FindFirstValue("name");
user.Eid = context.Principal?.FindFirstValue("eid");
user.CitizenStatus = context.Principal?.FindFirstValue("citizen_status");
user.RpToken = context.Principal?.FindFirstValue("rp_token");
await db.SaveChangesAsync();
},
};
Step 10
Manual UserInfo (debugging)
When SaveTokens is true, read the access token and call broker UserInfo. Do not call eID /connect/userinfo with the broker token.
csharp
// After sign-in (e.g. in a dev-only endpoint):
var accessToken = await HttpContext.GetTokenAsync("access_token");
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
var json = await client.GetStringAsync("https://localhost:9443/oauth2/userinfo");
// Or from shell with token from SaveTokens / logs:
curl -sS -H "Authorization: Bearer {broker_access_token}" \
https://localhost:9443/oauth2/userinfo
Step 11
Test end-to-end
Confirm eID login UI (not WSO2 local login). If eid / rp_token are missing, see /developers/sso/claims troubleshooting.
shell
dotnet run --project src/YourRpWebApp
# Browse to https://localhost:5001/login
# Flow: App → broker → eID UI → /signin-oidc → signed-in user
# Optional UserInfo check (broker access token):
curl -sS -H "Authorization: Bearer {broker_access_token}" \
https://localhost:9443/oauth2/userinfo
# Troubleshooting:
# - redirect URI mismatch → register exact callback in WSO2
# - invalid_client → client ID / secret
# - only profile claims → scope must include eid; broker consent / claim mapping
# - do not POST broker access_token to eID http://localhost:5026/connect/userinfo
Inject the catalog in a View Component and render links to each ssoLoginUrl plus My eID.
csharp
// ViewComponents/PortalSwitcherViewComponent.cs
public class PortalSwitcherViewComponent(EidGovernmentPortalCatalog catalog, IConfiguration config) : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync()
{
var portals = await catalog.GetLaunchablePortalsAsync(HttpContext.RequestAborted);
var eidBase = config["Eid:FrontendUrl"]?.TrimEnd('/') ?? "";
var path = config["Eid:CitizenPortalPath"] ?? "/citizen/profile";
ViewBag.EidCitizenUrl = eidBase != "" ? eidBase + path : null;
return View(portals);
}
}
// Views/Shared/Components/PortalSwitcher/Default.cshtml
@model IReadOnlyList<PortalTile>
<div class="gov-launcher">
@if (ViewBag.EidCitizenUrl is string eidUrl)
{
<a href="@eidUrl">My eID</a>
}
@foreach (var portal in Model)
{
<a href="@portal.SsoLoginUrl" title="@portal.Label">@portal.Short</a>
}
</div>
// _Layout.cshtml (authenticated)
@await Component.InvokeAsync("PortalSwitcher")
Step 16
Portal switcher — verify catalog API
Run eID API locally on 5026. Replace YOUR_WSO2_CLIENT_ID with your broker SP client_id.
shell
curl -s "http://localhost:5026/api/public/government-services?excludeClientId=YOUR_WSO2_CLIENT_ID" | jq .
# Each portal.ssoLoginUrl should be https://your-peer.gov/login/sso
# After admin approves your RP, your app appears automatically (no manual tile config).