Skip to content
Docs

Authentication

BifrostQL supports three authentication paths: local DB-backed user login, OIDC providers (Microsoft 365 and Google), and raw JWT bearer tokens. Every path produces the same provider-agnostic identity, which drives tenant isolation, audit column population, and any custom modules that depend on user context.

No matter how a user signs in, the authentication layer produces an AppIdentity — a provider-neutral record consumed by the security modules. Local login, OIDC, and JWT all converge on this one shape, so a module never has to know which provider authenticated the request.

AppIdentity carries:

FieldPurpose
IdStable, provider-neutral user identifier
EmailUser’s email address, if known
DisplayNameHuman-readable name, if known
ProviderWhich provider produced the identity (local, oidc:microsoft365, oidc:google)
TenantIdPrimary tenant identifier for tenant isolation, if the user belongs to one tenant
OrgIdsAll organization/group identifiers the user belongs to
RolesRoles granted to the user
ClaimsAdditional provider claims, copied verbatim into the user context

OrgIds, Roles, and Claims are never null — they default to empty collections, so consumers never need to null-check them.

IdentityContextMapper projects an AppIdentity into the UserContext dictionary that modules read. It writes three mapped keys:

Mapped keyDefaultSource fieldRead by
Tenant keytenant_idAppIdentity.TenantIdTenantFilterTransformer
Roles keyrolesAppIdentity.RolesAutoFilterTransformer (bypass-role checks)
Audit user keyidAppIdentity.IdBasicAuditModule

Provider claims are copied into the dictionary first, so the mapped identity keys above always take precedence over a same-named provider claim.

For self-hosted deployments, BifrostQL can authenticate users against an app-user table in the same database it already serves. Credentials are verified server-side and never leave the server — only a session cookie is returned to the client.

AddBifrostLocalAuth adds cookie authentication and the user store. The app-user table is reached through a server-side connection factory built from the connection string you pass — typically the same bifrost connection string the GraphQL endpoint uses.

using BifrostQL.Server;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddBifrostLocalAuth(
builder.Configuration.GetConnectionString("bifrost")!);
builder.Services.AddBifrostQL(o => o.BindStandardConfig(builder.Configuration));
var app = builder.Build();
app.UseAuthentication();
app.UseBifrostLocalAuth(); // maps /auth/login and /auth/logout
app.UseBifrostQL();
await app.RunAsync();

UseBifrostLocalAuth() maps the login and logout endpoints. Call it after UseAuthentication() so the issued cookie is honored on subsequent requests.

EndpointMethodBehavior
/auth/loginPOSTAccepts { "login": "...", "password": "..." }. On valid credentials, issues a session cookie and returns 204. On a missing user or wrong password, returns 401 — the response is identical for both so account existence is never leaked.
/auth/logoutPOSTClears the session cookie and returns 204.

Passwords are verified with the vetted ASP.NET Core PasswordHasher; no plaintext password is ever stored or compared.

The table and column names are configurable so local auth can point at whatever schema your app-user rows use. Pass a configuration callback to AddBifrostLocalAuth:

builder.Services.AddBifrostLocalAuth(
builder.Configuration.GetConnectionString("bifrost")!,
options =>
{
options.UserTable = "app_users"; // default: app_users
options.LoginColumn = "email"; // default: email
options.IdColumn = "id"; // default: id
options.PasswordHashColumn = "password_hash"; // default: password_hash
options.DisplayNameColumn = "display_name"; // default: display_name
options.TenantColumn = "tenant_id"; // default: tenant_id
options.RolesColumn = "roles"; // default: roles (delimited list, e.g. admin,editor)
options.LoginPath = "/auth/login"; // default: /auth/login
options.LogoutPath = "/auth/logout"; // default: /auth/logout
});

A successful login produces an AppIdentity with Provider set to local, the email as the login name, and roles parsed from the delimited roles column.

BifrostQL normalizes authenticated OIDC principals into the same AppIdentity contract local auth produces. Each provider ships a claim mapper that knows which raw claim types carry the subject, email, name, tenant, and group memberships.

AddBifrostOidcClaimMappers registers a mapper per OIDC provider, keyed by the issuer URL the provider stamps into the iss claim. UseUiAuth() selects the mapper by issuer and re-issues the cookie in the shared claim shape. Pair this with the AddOpenIdConnect wiring configured by AddBifrostQL.

builder.Services.AddBifrostOidcClaimMappers(mappers =>
{
mappers.AddMicrosoft365("https://login.microsoftonline.com/<tenant-id>/v2.0");
mappers.AddGoogle("https://accounts.google.com");
});

Each provider has a default mapping for which raw claim type supplies the tenant and group memberships:

ProviderTenant claimGroups claim
Microsoft 365tidTenantIdgroupsOrgIds
Googlenone by defaultnone by default

Google issues no tenant claim by default, so a Google identity has no TenantId unless you supply a custom mapping. A deployment that wants Workspace-domain isolation can point the mapper at Google’s hosted-domain claim (hd) or an app-specific org claim:

mappers.AddGoogle(
"https://accounts.google.com",
new OidcClaimMapping { TenantClaimType = "hd" });

The same OidcClaimMapping override works for Microsoft 365 when a deployment presents a custom claim shape.

Subject, email, and name are resolved provider-neutrally — the mapper reads the standard OIDC claim types with ASP.NET-mapped fallbacks, so mapping works whether or not ASP.NET’s inbound claim mapping has rewritten the claim types.

Onboarding path: start local, add OIDC later by configuration

Section titled “Onboarding path: start local, add OIDC later by configuration”

A self-hosted deployment — for example a club running the Membership Manager app — can start with local DB-backed login alone and add Google or Microsoft 365 login later purely by configuration, with no code change to the app’s authorization rules.

  1. Start with local auth. AddBifrostLocalAuth against the app-user table is enough to sign users in. Tenant isolation, audit columns, and policy checks all work off the AppIdentity local login produces.
  2. Add OIDC when you want it. Register the claim mappers with AddBifrostOidcClaimMappers and add the matching AddOpenIdConnect client configuration. Gate it behind a config section so the wiring stays inert until a real issuer and client secret are supplied — see the HostedSpa sample, which ships the OIDC block commented out and runs on local auth only by default.
  3. Authorization semantics are unchanged. Both paths converge on the same AppIdentity, and IdentityContextMapper projects either one into the identical UserContext keys — the tenant key, the roles key, and the audit user key. The policy engine, TenantFilterTransformer, and role-bypass checks read those keys and never see which provider authenticated the request.

The claim-shape guarantee is enforced by tests, not just documented: OidcLocalAuthParityTests verifies that a Microsoft 365 OIDC principal and a LocalUserStore login for the same logical user produce the same tenant and roles UserContext values — including after the OIDC identity is re-issued in the shared local-auth cookie shape. So adding OIDC cannot silently change who can see or do what; if a mapper ever diverged from local auth’s claim shape, that test would fail.

BifrostQL also supports OAuth2/OIDC via raw JWT bearer tokens, without the cookie re-issue path.

Add your identity provider settings to appsettings.json:

{
"JwtSettings": {
"Authority": "https://your-idp.com",
"Audience": "your-api"
},
"BifrostQL": {
"DisableAuth": false
}
}

Add JWT bearer authentication before BifrostQL in your Program.cs:

using BifrostQL.Server;
using Microsoft.AspNetCore.Authentication.JwtBearer;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o => builder.Configuration.Bind("JwtSettings", o));
builder.Services.AddBifrostQL(o => o.BindStandardConfig(builder.Configuration));
var app = builder.Build();
app.UseAuthentication();
app.UseBifrostQL();
await app.RunAsync();

Order matters: UseAuthentication() must come before UseBifrostQL(). Otherwise, BifrostQL won’t have access to the authenticated user.

BifrostQL builds a BifrostContext from the authenticated user. This context is available to all modules and transformers, and exposes a UserContext dictionary keyed by claim names. Modules read from this dictionary to populate filters and audit columns.

ClaimUsed byDefault key
Tenant IDTenantFilterTransformertenant_id
User audit keyBasicAuditModuleconfigured by user-audit-key
Arbitrary row filtersAutoFilterTransformerconfigured by auto-filter

The tenant and audit claim keys can be overridden via model metadata. These keys are honored regardless of which authentication path produced the identity — local, OIDC, and JWT all flow through the same IdentityContextMapper:

":root { tenant-context-key: org_id; user-audit-key: sub; }"

This tells the tenant filter transformer to read org_id from the user context instead of tenant_id, and tells audit population to use the sub claim as the user key.

For additional row-level filters, map columns to claims with auto-filter:

"dbo.orders { auto-filter: organization_id:org_id,region_id:region; }"

For development and testing, set DisableAuth to true:

{
"BifrostQL": {
"DisableAuth": true
}
}

With auth disabled, all requests are treated as unauthenticated. Modules that depend on user context (tenant isolation, audit columns) will not function.

If your GraphQL client runs in a browser on a different origin, configure CORS:

builder.Services.AddCors();
var app = builder.Build();
app.UseCors(x => x.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin());
app.UseAuthentication();
app.UseBifrostQL();

For production, restrict the allowed origins:

app.UseCors(x => x
.WithOrigins("https://your-app.com")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());

When you use cookie-based local auth or the OIDC cookie re-issue path from a browser on a different origin, the client must send credentials and CORS must allow them — use .AllowCredentials() with explicit origins (it cannot be combined with .AllowAnyOrigin()).