Skip to content

Authentication and Authorization

Valerio edited this page Apr 28, 2026 · 2 revisions

Authentication and Authorization

UncannyPrompt separates who you are (authentication) from what you can do (authorization). Identity is delegated to external IdPs or stored as an API key; authorization is enforced locally against a two-role model persisted in SQL Server.

  • Authentication — handled at the WebApp edge by cookie, API key, and OAuth/OIDC schemes.
  • Authorization — enforced by TenantScopeProvider / TenantScopeContext, PermissionService, and AccessControlQueries.

Authentication

Schemes

Scheme Purpose
Cookie Browser sessions for Razor Pages and cookie-backed API calls
API key Programmatic callers using X-API-Key or Authorization: Bearer ...
Google OAuth Optional external sign-in provider
Microsoft Entra ID Optional OIDC sign-in provider through Microsoft.Identity.Web
GitHub OAuth Optional external sign-in provider

External providers are registered conditionally by AddExternalProviderIfConfigured in src/UncannyPrompt.WebApp/Program.cs; a provider is enabled only when its required configuration values are present.

External identity provisioning

On first successful external login, the provider ticket is passed to IUserService.ProvisionExternalUserAsync.

That flow:

  1. Reads the provider subject id and email/name claims.
  2. Creates or finds the internal User.
  3. Links the external identity through AuthIdentity.
  4. Creates the user's personal Tenant / Workspace / Project scaffolding on first sign-in, with the user as TenantOwner and CanGrant = true on their own personal tenant.
  5. Rewrites the session principal so ClaimTypes.NameIdentifier is the internal User.Id.

This keeps external provider ids out of the rest of the application. Application services and authorization handlers always work with the internal user id.

Cookie sessions and CSRF

The browser session uses a hardened cookie named __Host-UncannyPrompt. Mutating cookie-authenticated JSON calls are protected by CookieAntiforgeryFilter, which expects the antiforgery token header:

X-CSRF-TOKEN

Razor Pages require authentication by default, with explicit anonymous access for login and public-prompt pages.

API keys

API key authentication is implemented by ApiKeyAuthenticationHandler. Keys are represented by UserApiKey rows and verified using IApiKeyHasher; plaintext is never stored.

X-API-Key: <key>

or:

Authorization: Bearer <key>

API keys inherit the owning user's UserRole / TenantRole surface — they don't unlock capabilities the underlying user doesn't already have.

Users manage keys from Developers -> Keys. The detailed endpoint reference is available after sign-in at /Developer/Api/Docs; the public overview is available anonymously at /Help/Api. See API for the developer-facing contract.

Authorization

UncannyPrompt runs two independent role models side by side:

  • UserRole is a single, global field on the User row — it describes what the account can do at the platform level, regardless of tenant.
  • TenantRole lives on the TenantMembership row — it describes what the account can do inside one specific tenant.

The two are orthogonal: a user can be Standard globally but TenantOwner in one tenant, or Admin globally with no TenantMembership row at all. Effective access is the union of the two checks, with platform Admin short-circuiting every tenant-level capability (see Admin bypass below).

Platform roles (UserRole)

Defined in UserRole:

Role Intent
Admin Full platform administration (tenants, users, settings, audit). Bypasses tenant ACLs.
Standard Default role. No platform-wide privileges; access is governed entirely by TenantRole and explicit ShareGrant rows.

New users default to Standard. The AdminOnly policy (see AppConstants.AdminOnlyPolicy) restricts admin-only surfaces; it evaluates through AdminRequirementHandler, which resolves the current user from ClaimTypes.NameIdentifier and succeeds iff Status == Active && Role == UserRole.Admin.

Tenant roles (TenantRole)

Defined in TenantRole. Values are ordered numerically from most to least privileged — authorization checks compare ordinals (role <= TenantRole.WorkspaceManager), so adding a new role in the middle is a breaking change.

Ord Role Intent
0 TenantOwner Full control of the tenant, including ownership transfer
1 TenantManager Manage members, workspaces, projects; cannot remove the last owner
2 WorkspaceManager Manage workspaces and projects inside the tenant
3 ProjectContributor Create/edit projects, folders, and prompts
4 Reviewer Use chat and review content, no write access to structure
5 Viewer Read-only; default for new memberships

New TenantMembership rows are created as Viewer with CanGrant = false. The entity also tracks GrantedByUserId / GrantedAt so the chain of delegation is auditable and cascadable.

Capability matrix

TenantScopeContext is the single place where tenant-level capabilities are decided. Each capability requires the tenant to be in scope and one of:

  • the caller is a platform Admin (bypass), or
  • the caller's TenantRole is at or above the threshold below.
Capability Minimum TenantRole Extra requirement
CanManageMembers TenantManager (≤ 1) CanGrant == true
CanManageWorkspaces WorkspaceManager (≤ 2)
CanManageProjects ProjectContributor (≤ 3)
CanUseChat Reviewer (≤ 4)

The CanGrant flag

CanGrant on TenantMembership is orthogonal to the role. It gates who can delegate permissions, so you can have a TenantManager without the authority to add new members. TenantMembershipService.CanGrantForRole auto-sets it to true only for TenantOwner and TenantManager assignments; other roles keep it false.

Membership invariants

Enforced by TenantMembershipService, non-negotiable for non-admin actors:

  • At least one TenantOwner — revoking or demoting the last owner throws. Applies to both admin and non-admin actors.
  • No privilege escalation — an actor cannot assign a role higher than their own (targetRole < actorRole throws).
  • No privilege inversion — an actor cannot modify or revoke a member whose role is stronger than theirs.
  • Personal tenants — the owner of a TenantKind.Personal tenant cannot be moved off TenantOwner or removed.
  • Cascading revocation — revoking a member also revokes the memberships that member granted, transitively, via the GrantedByUserId chain.

Platform admins bypass the actor-side invariants (escalation / inversion / "can manage") but not the structural ones (last-owner, personal-tenant owner).

Admin bypass

TenantScopeContext short-circuits every capability with if (IsAdmin) return true before evaluating membership. In practice this means a platform Admin:

  • Sees all tenants as accessible — TenantScopeProvider populates AccessibleTenantIds from the full tenant list rather than from TenantMembership.
  • Can activate "all tenants" mode by setting the uncannyprompt_tenant_scope=all cookie (via SetAllTenantsAsync); non-admins get false back and fall through to per-tenant scoping.
  • Passes every CanManage* / CanUseChat check even without a TenantMembership row for that tenant.

The admin bypass is the relationship between the two role systems — it's what lets a small number of global operators administer tenants they aren't members of, without having to insert synthetic memberships.

Note: the admin bypass is general-administrative. It does not grant decryption of other users' encrypted user-scoped secrets — those remain encrypted under per-user keys regardless of UserRole.

Resource ACLs

Orthogonal to tenant roles, ShareGrant represents explicit access to a target:

Target type Meaning
Project Access to a project and its contained resources
Folder Access to a folder subtree and contained prompts
Prompt Access to a specific prompt

Grant permissions are modeled by SharePermission (View, Edit, Manage) and mapped to application-level permissions by PermissionService.

SQL-level authorization

Collection reads must not fetch everything and filter in memory. AccessControlQueries exposes composable IQueryable helpers so prompt, folder, sharing, and listing services apply access checks in SQL:

Caller -> Application service -> AccessControlQueries -> EF query -> SQL Server

Seeded admins

Bootstrap platform admins are provisioned from SeedOptions:AdminUsers on WebApp startup (WebApplicationSeedExtensions), so the first sign-in is never locked out of itself. Seeded admins are normal User rows with Role = Admin; after bootstrap, they flow through the same AdminOnly policy and AdminRequirementHandler as every other admin account. See Configuration for the Provider / UniqueId / Email format.

Audit

Security-relevant events are persisted as AuditEvent rows via IAuditService — the only entry point, so the audit trail is uniform and append-only. Public-link resolution is audited as public_link.access.

Public links

Public links bypass interactive authentication but not validation. PublicShareLink stores:

  • TokenLookupHash — deterministic lookup hash used for indexed lookup.
  • TokenHash — verification hash used after the indexed match.
  • ExpiresAt, RevokedAt, IsDeleted — lifecycle controls.

Sign-out

The cookie and the external OIDC/OAuth session are cleared on sign-out. API keys are revoked from the Developers area through IUserApiKeyService and immediately invalidate all subsequent requests bearing the key.

Source pointers

Clone this wiki locally