.NET
Sign in with NIA — ASP.NET Core
Add “Sign in with NIA” to an ASP.NET Core app with the built-in OpenID Connect middleware and the standard OpenID Connect flow. Follow the steps top to bottom — tick each one off as you go.
01Overview
This guide adds “Sign in with NIA” to an ASP.NET Core application using the standard OpenID Connect flow. NIA is a conformant OIDC provider, so you use the framework's built-in Microsoft.AspNetCore.Authentication.OpenIdConnect middleware — no NIA-specific SDK required.
ASP.NET Core runs as a confidential client: the browser is redirected to NIA to sign in, and your server exchanges the returned code for tokens using your client secret. The middleware handles PKCE, state, and ID-token signature validation for you.
- Flow: Authorization Code + PKCE (S256)
- Tokens: RS256 access & ID tokens, opaque refresh token
- You write: a few lines of middleware configuration — no custom login/callback controllers required
02Requirements
- .NET 8 SDK or later
- A NIA account with access to the client dashboard
- A registered OAuth client (
client_id+client_secret) - Your app's redirect URI registered on that client
- Cookie authentication available (built in to ASP.NET Core)
03Installation
Add the OpenID Connect authentication handler to your project.
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect04Required OAuth library
Register NIA once in Program.cs. Cookies hold the local session; the OpenID Connect handler drives the redirect to NIA and validates what comes back.
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect("nia", options =>
{
options.Authority = builder.Configuration["Nia:Issuer"];
options.ClientId = builder.Configuration["Nia:ClientId"];
options.ClientSecret = builder.Configuration["Nia:ClientSecret"];
options.ResponseType = "code";
options.UsePkce = true;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("email");
options.Scope.Add("profile");
options.CallbackPath = "/signin-oidc";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.Run();offline_access to the scope list only if you need refresh tokens (see step 14).05Environment Variables
Never hard-code secrets. Keep the issuer and client ID in appsettings.json, and keep the client secret out of source control — use dotnet user-secrets in development.
{
"Nia": {
"Issuer": "https://dev-api.niaauth.com",
"ClientId": "your-client-id"
}
}Nia__ClientSecret (double underscore) as an environment variable or pull it from a secret store — never commit it to appsettings.json.06Register OAuth Client
In the NIA dashboard, create the client your ASP.NET Core app will authenticate as:
- Open Dashboard → Clients → New client
- Client type: Web (confidential)
- Allowed scopes:
openidemailprofile(addoffline_accessfor refresh) - Add your redirect URI (next step)
- Copy the
client_idandclient_secret— the secret is shown once
07Redirect URI
NIA matches redirect URIs exactly — scheme, host, port, and path must be identical, with no trailing-slash difference. The OpenID Connect middleware defaults CallbackPath to /signin-oidc; register that exact path per environment.
# Development
https://localhost:5001/signin-oidc
# Production
https://app.example.com/signin-oidchttps://localhost:5001/signin-oidc/ (trailing slash) will be rejected. Also register the sign-out callback, SignedOutCallbackPath (default /signout-callback-oidc), as the post_logout_redirect_uri for logout to work.08Issuer
The issuer is the single URL every OIDC library needs. Its discovery document advertises all endpoints below — options.Authority is all the middleware needs to find them.
Issuer: https://dev-api.niaauth.com
Discovery: https://dev-api.niaauth.com/.well-known/openid-configuration
Authorization: https://dev-api.niaauth.com/oauth/authorize
Token: https://dev-api.niaauth.com/oauth/token
UserInfo: https://dev-api.niaauth.com/oauth/userinfo
JWKS: https://dev-api.niaauth.com/oauth/jwks.json09Authorization Flow
Decorate any endpoint with [Authorize] and the middleware automatically challenges unauthenticated requests — redirecting the browser to NIA. No custom login controller is needed, but you can add an explicit login endpoint too.
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("auth")]
public class AccountController : ControllerBase
{
[HttpGet("login")]
public IActionResult Login(string returnUrl = "/")
{
return Challenge(
new AuthenticationProperties { RedirectUri = returnUrl },
OpenIdConnectDefaults.AuthenticationScheme);
}
[Authorize]
[HttpGet("profile")]
public IActionResult Profile() => Ok(new { name = User.Identity?.Name });
}10Token Exchange
You don't write this step — the OpenID Connect middleware performs it automatically at /signin-oidc. It verifies state, sends the PKCE verifier, exchanges the authorization code, validates the ID token, and (with SaveTokens = true) stores the access, ID, and refresh tokens in the authentication cookie.
.AddOpenIdConnect("nia", options =>
{
options.ResponseType = "code";
options.UsePkce = true; // PKCE (S256) generated & sent automatically
options.SaveTokens = true; // persist access_token / id_token / refresh_token
options.CallbackPath = "/signin-oidc"; // code exchange happens here, no controller needed
});11JWKS Validation
NIA signs ID tokens with RS256. The middleware fetches the discovery document from Authority, retrieves the JWKS, caches it, and verifies the signature, iss, aud, exp, and nonce during the callback. You do not verify manually.
To confirm what was validated, read the resulting claims:
[Authorize]
[HttpGet("claims")]
public IActionResult Claims()
{
// iss, aud, exp, sub, email are already verified before this runs.
var claims = User.Claims.Select(c => new { c.Type, c.Value });
return Ok(claims);
}kid using the standard metadata refresh interval, rotation needs no code change on your side.12Session Management
The cookie authentication handler stores the session; no extra session store is required for a single instance. Read claims via User.Claims / User.Identity in any [Authorize]-protected action.
[Authorize]
[HttpGet("me")]
public IActionResult Me()
{
var sub = User.FindFirst("sub")?.Value;
var email = User.FindFirst("email")?.Value;
var name = User.Identity?.Name;
return Ok(new { sub, email, name });
}13Refresh Tokens
Request offline_access in the scope list to receive a refresh token, then read it out of the authentication properties with SaveTokens = true. For automatic rotation on expiry, consider Duende.AccessTokenManagement; the manual approach is shown below.
[Authorize]
[HttpPost("refresh")]
public async Task<IActionResult> Refresh()
{
var refreshToken = await HttpContext.GetTokenAsync("refresh_token");
if (refreshToken is null) return Unauthorized();
using var http = new HttpClient();
var response = await http.PostAsync($"{Request.HttpContext.RequestServices
.GetRequiredService<IConfiguration>()["Nia:Issuer"]}/oauth/token",
new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "refresh_token",
["refresh_token"] = refreshToken,
["client_id"] = "your-client-id",
["client_secret"] = "your-client-secret",
}));
// NIA rotates the refresh token — persist the replacement token set.
return Ok(await response.Content.ReadAsStringAsync());
}14Logout
Sign out of both the local cookie and the OpenID Connect scheme — the middleware then redirects to NIA's end-session endpoint with post_logout_redirect_uri. Register that URI on the client (see step 7).
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
[HttpGet("logout")]
public IActionResult Logout()
{
return SignOut(
new AuthenticationProperties { RedirectUri = "/" },
CookieAuthenticationDefaults.AuthenticationScheme,
OpenIdConnectDefaults.AuthenticationScheme);
}15Deployment
Before going live:
- Register the production redirect and sign-out URIs (HTTPS) on the client
- Store
Nia:ClientSecretin a secrets manager (Azure Key Vault, environment variables) — never inappsettings.json - Serve over HTTPS — the OIDC redirect and secure cookies require it
- If running behind a reverse proxy/load balancer, add
ForwardedHeadersmiddleware so the correct scheme/host reach the handler - Set
options.CorrelationCookie.SameSite = SameSiteMode.Noneonly if the app is embedded cross-site; otherwise keep the default
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();16Troubleshooting
- redirect_uri_mismatch — the callback must be exactly
/signin-oidc(or your configuredCallbackPath) registered with matching scheme/host/port, no trailing slash. - invalid_client — wrong
ClientSecret, or check thatuser-secrets/environment variables loaded correctly. - Authority must use HTTPS or discovery fails — confirm
Nia:Issueris set tohttps://dev-api.niaauth.com, not a local placeholder. - Correlation cookie not found — the state cookie was lost between challenge and callback; usually a
SameSite/HTTPS misconfiguration or the request round-tripped through a different host. - invalid_scope — the scope isn't in the client's
allowed_scopes; add it in the dashboard. - Headers/scheme wrong behind a proxy — add
UseForwardedHeadersbeforeUseAuthenticationso the middleware builds the correct redirect URI.