ASP.NET Core steg för steg
Den här sidan förutsätter att du gjort steg 1 och 2 på Kom igång:
du har ett CLIENT_ID, en CLIENT_SECRET och har bestämt en callback-adress.
I exemplen nedan är callback-adressen /callback, alltså
https://din-sajt.se/callback registrerad hos oss.
Det finns tre utgångslägen, och de skiljer sig mer än man tror:
- A. Sajten har ingen inloggning alls. Aegis blir det enda sättet att logga
in. Tio rader i
Program.cs, klart. - B. Sajten har redan ASP.NET Core Identity — en
AspNetUsers-tabell, lösenordsinloggning, kanske roller. Då ska Aegis läggas bredvid Identity som en extern inloggning, och en sida ska koppla Aegis-kontot till ett konto i din databas. Det är fallet där de flesta fastnar. - C. Sajten har en egen kakinloggning utan Identity — en egen
användartabell och
AddCookie(). Då tar du över iOnTicketReceivedoch loggar in med din egen kod, så att Aegis ger exakt samma session som lösenordet.
Börja med det som är gemensamt.
Gemensamt: paket, konfiguration, hemlighet
Installera paketet:
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
Lägg en sektion i appsettings.json. Bara klient-id och callback står här —
hemligheten kommer i nästa steg:
"Aegis": {
"Authority": "https://login.aegiscentralid.se/",
"ClientId": "DITT-CLIENT-ID",
"CallbackPath": "/callback",
"Scopes": [ "openid", "profile", "email", "tenant.role" ]
}
Lägg hemligheten i User Secrets under utveckling:
dotnet user-secrets init
dotnet user-secrets set "Aegis:ClientSecret" "DIN-CLIENT-SECRET"
På servern läggs den som miljövariabel Aegis__ClientSecret (två understreck),
eller i en appsettings.Production.json som inte checkas in. Hemligheten ska
aldrig stå i appsettings.json eller i kod.
A. Sajt utan inloggning
I Program.cs, före builder.Build():
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
var aegis = builder.Configuration.GetSection("Aegis");
builder.Services.AddAuthentication(options =>
{
// Sajten har sin egen kaka; Aegis används bara för att skapa den.
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = aegis["Authority"];
options.ClientId = aegis["ClientId"];
options.ClientSecret = aegis["ClientSecret"];
options.CallbackPath = aegis["CallbackPath"] ?? "/callback";
options.ResponseType = OpenIdConnectResponseType.Code;
options.ResponseMode = OpenIdConnectResponseMode.Query;
options.UsePkce = true;
options.Scope.Clear();
foreach (var scope in aegis.GetSection("Scopes").Get<string[]>() ?? ["openid", "profile", "email"])
options.Scope.Add(scope);
// Allt står i id-token. Ingen extra runda till userinfo.
options.GetClaimsFromUserInfoEndpoint = false;
// Inga tokens i kakan — se avsnittet om proxy längst ned.
options.SaveTokens = false;
options.TokenValidationParameters.NameClaimType = "name";
options.TokenValidationParameters.RoleClaimType = "tenant_role";
});
builder.Services.AddAuthorization();
Efter var app = builder.Build();, i den här ordningen:
app.UseAuthentication();
app.UseAuthorization();
Det var allt. Sätt [Authorize] på en sida eller controller, och första
besöket skickar användaren till Aegis. Callback-sidan /callback finns inte i
din kod — OIDC-handlern fångar den själv.
Läs anspråken som vanligt:
var namn = User.Identity?.Name; // "Anna Lindqvist"
var sub = User.FindFirst("sub")?.Value; // permanent id — nyckeln i din databas
var epost = User.FindFirst("email")?.Value;
var roll = User.FindFirst("tenant_role")?.Value; // Owner, Admin eller Member
Utloggning:
return SignOut(CookieAuthenticationDefaults.AuthenticationScheme,
OpenIdConnectDefaults.AuthenticationScheme);
B. Sajt som redan har ASP.NET Core Identity
Här ska Identity fortsätta äga användarna. Aegis blir en extern inloggning,
på samma sätt som "Logga in med Google" brukar kopplas: användaren loggar in
hos oss, Identity tar emot resultatet i sin externa kaka, och en sida i din
sajt bestämmer vilket konto i AspNetUsers det motsvarar.
Tre delar behövs.
B1. Registrera Aegis som externt schema
I Program.cs, efter AddIdentity/AddDefaultIdentity:
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
var aegis = builder.Configuration.GetSection("Aegis");
builder.Services.AddAuthentication()
.AddOpenIdConnect("Aegis", "Aegis Central ID", options =>
{
// Det här är raden som skiljer B från A: resultatet läggs i Identitys
// externa kaka, inte i sajtens vanliga inloggningskaka.
options.SignInScheme = IdentityConstants.ExternalScheme;
options.Authority = aegis["Authority"];
options.ClientId = aegis["ClientId"];
options.ClientSecret = aegis["ClientSecret"];
options.CallbackPath = aegis["CallbackPath"] ?? "/callback";
options.ResponseType = OpenIdConnectResponseType.Code;
options.ResponseMode = OpenIdConnectResponseMode.Query;
options.UsePkce = true;
options.Scope.Clear();
foreach (var scope in aegis.GetSection("Scopes").Get<string[]>() ?? ["openid", "profile", "email"])
options.Scope.Add(scope);
options.GetClaimsFromUserInfoEndpoint = false;
options.SaveTokens = false;
options.TokenValidationParameters.NameClaimType = "name";
options.TokenValidationParameters.RoleClaimType = "tenant_role";
// Visa felet på inloggningssidan i stället för en generisk felsida.
options.Events.OnRemoteFailure = context =>
{
var fel = context.Failure?.GetBaseException().Message ?? "okänt fel";
context.Response.Redirect("/Identity/Account/Login?remoteError=" + Uri.EscapeDataString(fel));
context.HandleResponse();
return Task.CompletedTask;
};
});
Anropa inte AddAuthentication med förvalda scheman här — Identity har redan
satt dem, och att skriva över dem bryter lösenordsinloggningen.
Notera "Aegis" som schemanamn: det är det provider-värdet som knappen och
callback-sidan i B2 och B3 använder.
B2. Knappen på inloggningssidan
Identitys inloggningssida listar redan externa leverantörer i
Model.ExternalLogins om du scaffoldat den (dotnet aspnet-codegenerator identity -fi Account.Login). Har du inte scaffoldat visas knappen automatiskt
i Identitys inbyggda UI. Vill du styra utseendet själv:
<form asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post">
<button type="submit" name="provider" value="Aegis">
Logga in med Aegis Central ID
</button>
</form>
value="Aegis" måste vara samma sträng som schemanamnet i B1.
B3. Callback-sidan som kopplar kontot
Scaffolda Identitys ExternalLogin-sida:
dotnet aspnet-codegenerator identity -fi Account.ExternalLogin
Den scaffoldade sidan visar ett formulär där användaren får skriva in sin
e-postadress. Med Aegis behövs det inte — adressen kommer verifierad i
id-token — så ersätt OnGetCallbackAsync med den här logiken:
public async Task<IActionResult> OnGetCallbackAsync(string? returnUrl = null, string? remoteError = null)
{
returnUrl ??= Url.Content("~/");
if (remoteError != null)
return Misslyckad($"Fel från Aegis Central ID: {remoteError}");
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
return Misslyckad("Kunde inte läsa inloggningsuppgifterna från Aegis Central ID.");
// 1. Aegis-kontot är redan kopplat till ett konto hos oss → logga in.
var result = await _signInManager.ExternalLoginSignInAsync(
info.LoginProvider, info.ProviderKey, isPersistent: true, bypassTwoFactor: true);
if (result.Succeeded)
return LocalRedirect(returnUrl);
if (result.IsLockedOut)
return Misslyckad("Kontot är låst.");
// 2. Ingen koppling. Hitta eller skapa kontot via den verifierade e-posten.
var email = info.Principal.FindFirstValue(ClaimTypes.Email)
?? info.Principal.FindFirstValue("email");
var verifierad = string.Equals(info.Principal.FindFirstValue("email_verified"), "true",
StringComparison.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(email) || !verifierad)
return Misslyckad("Aegis Central ID lämnade ingen verifierad e-postadress.");
var user = await _userManager.FindByEmailAsync(email);
if (user == null)
{
user = new IdentityUser { UserName = email, Email = email, EmailConfirmed = true };
var create = await _userManager.CreateAsync(user);
if (!create.Succeeded)
return Misslyckad(string.Join(" ", create.Errors.Select(e => e.Description)));
}
// 3. Spara kopplingen (sub → konto) i AspNetUserLogins, och logga in.
var link = await _userManager.AddLoginAsync(user, info);
if (!link.Succeeded)
return Misslyckad(string.Join(" ", link.Errors.Select(e => e.Description)));
await _signInManager.SignInAsync(user, isPersistent: true, info.LoginProvider);
return LocalRedirect(returnUrl);
}
private IActionResult Misslyckad(string meddelande)
{
ErrorMessage = meddelande; // [TempData]-egenskap som Login-sidan visar
return RedirectToPage("./Login");
}
Byt IdentityUser mot din egen användarklass om du har en. Har du extra
obligatoriska fält — förnamn, efternamn — fyll dem från name-anspråket:
var namn = info.Principal.FindFirstValue("name") ?? "";
Så här hänger det ihop, om du vill förstå i stället för att kopiera:
| Adress | Vem svarar | Vad händer |
|---|---|---|
POST /Identity/Account/ExternalLogin?provider=Aegis |
Din ExternalLogin-sida | Skickar användaren till Aegis |
GET /callback?code=…&state=… |
OIDC-handlern, ingen kod hos dig | Löser in koden, verifierar id-token, lägger anspråken i Identitys externa kaka |
GET /Identity/Account/ExternalLogin?handler=Callback |
Din ExternalLogin-sida (koden ovan) | Läser den externa kakan, hittar eller skapar kontot, loggar in |
Två saker som brukar förvåna: callback-adressen (/callback) och
ExternalLogin-sidan är två olika adresser, och det är bara den första som
registreras hos Aegis. Och sub lagras i AspNetUserLogins, inte i
AspNetUsers — det är så Identity kommer ihåg att ett Aegis-konto hör till ett
lokalt konto.
C. Sajt med egen kakinloggning, utan Identity
Många mindre sajter har en egen adminmodell: en tabell med användare, ett
lösenord hashat med BCrypt, AddCookie() och en egen SignInAsync som sätter
kakan. Ingen ASP.NET Core Identity. Här ska Aegis inte utfärda någon egen
session — resultatet ska bli exakt samma kaka som lösenordsinloggningen ger,
så att allt sajten byggt runt kakan (säkerhetsstämpel, sessionskontroll,
händelselogg) fungerar orört.
Nyckeln är händelsen OnTicketReceived: OIDC-handlern har då verifierat
id-token, och du tar över innan den hinner logga in någon.
C1. Registrera schemat bredvid kakan
var auth = builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options => { /* din befintliga konfiguration */ });
var aegis = builder.Configuration.GetSection("Aegis");
// Bara när klient-id och hemlighet finns — annars syns ingen knapp och inget ändras.
if (!string.IsNullOrEmpty(aegis["ClientId"]) && !string.IsNullOrEmpty(aegis["ClientSecret"]))
{
auth.AddOpenIdConnect("Aegis", "Aegis Central ID", options =>
{
options.Authority = aegis["Authority"];
options.ClientId = aegis["ClientId"];
options.ClientSecret = aegis["ClientSecret"];
options.CallbackPath = aegis["CallbackPath"] ?? "/callback";
options.ResponseType = OpenIdConnectResponseType.Code;
options.ResponseMode = OpenIdConnectResponseMode.Query;
options.UsePkce = true;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.GetClaimsFromUserInfoEndpoint = false;
options.SaveTokens = false;
// Behåll OIDC-namnen (sub, email, email_verified) i stället för .NET:s URI-namn.
options.MapInboundClaims = false;
options.Events.OnTicketReceived = OnTicketReceivedAsync;
options.Events.OnRemoteFailure = context =>
{
context.HandleResponse();
context.Response.Redirect("/login?aegis=fel");
return Task.CompletedTask;
};
});
}
SignInScheme behöver inte sättas: den ärver kakschemat från AddAuthentication,
och eftersom handlern aldrig kommer så långt som till att logga in spelar det
ingen roll.
C2. Ta över i OnTicketReceived
static async Task OnTicketReceivedAsync(TicketReceivedContext context)
{
var http = context.HttpContext;
var db = http.RequestServices.GetRequiredService<MinDbContext>();
// Handlern ska varken sätta en kaka eller omdirigera. Det gör vi.
context.HandleResponse();
var principal = context.Principal!;
var sub = principal.FindFirstValue("sub");
var email = (principal.FindFirstValue("email") ?? "").ToLowerInvariant();
var emailVerified = principal.FindFirstValue("email_verified") == "true";
// 1. Redan kopplat konto: matcha enbart på sub.
var user = await db.AdminUsers.FirstOrDefaultAsync(u => u.AegisSub == sub);
// 2. Ingen koppling: bind en inbjuden rad via verifierad e-post, en gång.
if (user is null && emailVerified)
{
user = await db.AdminUsers.FirstOrDefaultAsync(u => u.AegisSub == null && u.Email == email);
if (user is not null) user.AegisSub = sub;
}
// 3. Ingen rad alls: ingen åtkomst, hur giltigt Aegis-kontot än är.
if (user is null || !user.IsActive)
{
http.Response.Redirect("/login?aegis=obehorig");
return;
}
user.LastLoginAt = DateTime.UtcNow;
await db.SaveChangesAsync();
// Din befintliga inloggning — samma kaka som lösenordsvägen.
await MinInloggning.SignInAsync(http, user);
http.Response.Redirect(context.ReturnUri ?? "/admin");
}
C3. Knappen och inbjudan
Knappen är ett formulär som svarar med Challenge:
public IActionResult OnPostAegis(string? returnUrl) =>
Challenge(new AuthenticationProperties { RedirectUri = returnUrl ?? "/admin" }, "Aegis");
Inbjudan är en rad i din användartabell med e-post, rättigheter, en flagga
UsesAegis = true, tom lösenordshash och AegisSub = null. Lösenordsvägen
ska avvisa sådana rader innan den jämför något, och sidor för lösenordsbyte,
TOTP och passkeys ska visa en förklaring i stället för formulär — allt det
sköts av Aegis, där tvåfaktor är obligatoriskt.
Tänk på att ha kvar ett lokalt konto med lösenord för den som äger sajten. Det är den sista vägen in om Aegis inte går att nå.
Om sajten står bakom en omvänd proxy
Avslutas TLS i Nginx, Nginx Proxy Manager, IIS ARR, Traefik eller Cloudflare framför din applikation behövs det här, annars fungerar inloggningen inte:
// Före builder.Build()
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedFor;
options.KnownProxies.Add(IPAddress.Parse("192.168.1.10")); // proxyns adress
});
// Först i kedjan efter builder.Build(), före allt annat
app.UseForwardedHeaders();
Utan KnownProxies ignoreras huvudena tyst: applikationen bygger
redirect_uri som http://…, och Aegis svarar att adressen inte matchar den
registrerade. Vanliga sidor fungerar, bara inloggningen går sönder — det är
signalen.
SaveTokens = false i exemplen ovan är inte kosmetika. Med true hamnar
id-token och access token i kakan som sätts i svaret från /callback, svaret
blir över 4 kB huvuden, och de flesta proxyer svarar då 502 — efter att koden
redan lösts in, så en omladdning ger invalid_grant. Behöver du tokens
efteråt, höj proxy_buffer_size i proxyn i stället.
Kontrollista
-
Microsoft.AspNetCore.Authentication.OpenIdConnectinstallerat -
Aegis:ClientId=CLIENT_IDfrån arbetsytan -
Aegis:ClientSecreti User Secrets / miljövariabel, inte iappsettings.json -
CallbackPathidentisk med den registrerade callback-adressen, tecken för tecken - Varje scope i
Aegis:Scopesibockat på klienten hos oss - Vid Identity:
SignInScheme = IdentityConstants.ExternalSchemeoch knappensvalue= schemanamnet - Bakom proxy:
UseForwardedHeaders+KnownProxies, ochSaveTokens = false -
app.UseAuthentication()föreapp.UseAuthorization() - Har sajten en CSP med
form-action: lägg tillhttps://login.aegiscentralid.se, annars stannar klicket tyst i Chrome