N

.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.

~15 minEasyMicrosoft.AspNetCore.Authentication.OpenIdConnect
Try OAuth Flow
Integration progress
0/16

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.OpenIdConnect

04Required 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.

Program.cs
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();
Add 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"
  }
}
In production, set 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: openid email profile (add offline_access for refresh)
  • Add your redirect URI (next step)
  • Copy the client_id and client_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.

redirect URIs
# Development
https://localhost:5001/signin-oidc

# Production
https://app.example.com/signin-oidc
A mismatch is the #1 integration error. https://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 & endpoints
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.json

09Authorization 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.

Program.cs (recap)
.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:

verify (illustrative)
[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);
}
Keys rotate. Because the middleware refetches JWKS by 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.

Controllers/AccountController.cs
[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.

Controllers/AccountController.cs
[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).

Controllers/AccountController.cs
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:ClientSecret in a secrets manager (Azure Key Vault, environment variables) — never in appsettings.json
  • Serve over HTTPS — the OIDC redirect and secure cookies require it
  • If running behind a reverse proxy/load balancer, add ForwardedHeaders middleware so the correct scheme/host reach the handler
  • Set options.CorrelationCookie.SameSite = SameSiteMode.None only if the app is embedded cross-site; otherwise keep the default
Program.cs (production)
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 configured CallbackPath) registered with matching scheme/host/port, no trailing slash.
  • invalid_client — wrong ClientSecret, or check that user-secrets/environment variables loaded correctly.
  • Authority must use HTTPS or discovery fails — confirm Nia:Issuer is set to https://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 UseForwardedHeaders before UseAuthentication so the middleware builds the correct redirect URI.