Articles · Operations & Administration
An OIDC server inside your app
Try out the running example
Your product grew an ecosystem: a partner wants “Sign in with YourApp”, the mobile team wants standard OAuth, the data team wants service-to-service tokens. The usual outcomes are a Duende invoice or a Keycloak deployment. Sentinel’s answer: mount the authorization server inside the app you already run.
Mount it
Starting from the first-login host:
builder.Services.AddSentinelOidcServer(o =>
{
o.Issuer = "https://id.example.com";
// LoginPath = "/login", ConsentPath = "/consent" — your pages, see below
});
app.MapSentinelOidc();
Check the front door:
GET /.well-known/openid-configuration
The document advertises exactly what’s implemented — response_types ["code"],
grants authorization_code | refresh_token | client_credentials, PKCE S256 only,
RS256, back-channel logout — and nothing more. No implicit, no ROPC, no
plain-PKCE: the dead ends aren’t configurable back on.
Register a client
Clients enter through declarative config (or the admin surface — never anonymous registration):
oidcClients:
- clientId: partner-portal
clientType: confidential
secretRef: PARTNER_PORTAL_SECRET # env var name; the file holds no secrets
redirectUris:
- "https://portal.partner.example/signin-oidc"
allowedScopes: [openid, profile, email, offline_access]
requireConsent: true # third party → ask the user
backChannelLogoutUri: "https://portal.partner.example/bc-logout"
Redirect URIs match by exact ordinal comparison — no wildcard subdomains, no “starts with” bugs. Secrets are hashed at rest and rotate with an overlap window, so a deploy can roll credentials without a coordinated flag day.
The interaction contract: your UI stays yours
Sentinel ships no hosted login page — the authorize endpoint delegates interaction to your app and resumes when you’re done:
GET /oidc/authorize?client_id=partner-portal&response_type=code&code_challenge=…validates the request, then redirects to yourLoginPathwith the pending request reference when no session exists.- Your page authenticates however you like — password, passkey,
federated — using the normal
/auth/*endpoints and your own markup. - If consent is required, Sentinel redirects to your
ConsentPath; your page renders the scopes and posts the decision toPOST /oidc/consent. Granted consent persists (revocably) so returning users skip the prompt;FirstPartyclients skip it always. - The flow resumes and the browser lands back on the client’s redirect URI with a code.
The token exchange
POST /oidc/token
Authorization: Basic cGFydG5lci1wb3J0YWw6…
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=…&code_verifier=…&redirect_uri=…
{
"access_token": "eyJ…", "token_type": "Bearer", "expires_in": 600,
"scope": "openid profile offline_access",
"refresh_token": "srt_…", "id_token": "eyJ…"
}
Two behaviors here are worth your attention:
- Codes are single-use with consequences. A code lives two minutes; consuming it
twice revokes the refresh family the first consumption minted and emits
oidc.code_reuse(pinned byAuthorization_code_is_single_use_and_reuse_emits_the_event). Code replay is treated as the compromise indicator it is — same philosophy as refresh-token families, which OIDC refresh tokens reuse wholesale. - Refresh can only narrow. The persisted grant remembers its scopes; a refresh request may drop scopes but never add them.
client_credentials rides the same endpoint and maps onto Sentinel
service accounts — machine tokens with no refresh and no ID
token, honoring an audience parameter.
Resource-server plumbing
Opaque needs? POST /oidc/introspect (RFC 7662) answers active, scopes, subject and
expiry for resource servers that don’t verify JWTs locally. POST /oidc/revoke
(RFC 7009) kills refresh tokens client-side. And GET /oidc/userinfo serves the
standard claims for the scopes granted.
Logout is both-directions: GET /oidc/logout (RP-initiated, with post-logout redirect
validation) ends the Sentinel session, and back-channel logout pushes signed logout
tokens to every client that holds a live grant for that session and declares a
backChannelLogoutUri — delivery failures emit oidc.backchannel_logout_failed instead
of vanishing.
Keys without ceremony
The JWKS at /oidc/jwks is the same signing key ring first-party tokens use: one
primary, rotation with a 7-day overlap, retired keys verifying until the window closes.
Rotate whenever — clients following the JWKS never notice. And because production keys
must come from the persisted store, “the IdP restarted and every session died” is a
failure mode Sentinel refuses to have.
What to require of yourself
Running an AS is a protocol responsibility. Sentinel carries the spec surface and runs
an internal conformance suite in CI (official OpenID Foundation conformance +
certification is the pre-1.0 commitment), but you own: TLS termination and the issuer
URL’s stability, the quality of your login/consent pages (they are the phishing
surface now), and client hygiene — exact redirect URIs, confidential where possible,
requireConsent for anyone you don’t own. The
OIDC provider reference has the full endpoint and option tables.