Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions .github/skills/auth-and-identity/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
---
name: auth-and-identity
description: Use this skill when asked to set up authentication, authorization, or identity in a Cratis Arc project — backend, frontend, or both. Covers implementing identity providers, protecting commands/queries with authorization attributes, integrating Microsoft Identity Platform, connecting backend identity to React frontends, and local development testing with generated principals. Also covers customizing IProvideIdentityDetails for enriching identity from databases, blocking users, multi-tenant identity, user preferences, and modifying identity at runtime. Use this whenever the user mentions auth, login, roles, permissions, identity details, user context, protecting endpoints, identity provider customization, or anything related to who the user is and what they can access.
---

# Auth & Identity in Cratis Arc

This skill covers the full auth and identity stack in a Cratis Arc application. Read the relevant reference files below for detailed API usage.

> **Read the relevant instruction files first.** This skill references concepts from the core copilot instructions in `.github/copilot-instructions.md`. If you need details on vertical slices, commands, queries, or proxy generation, consult those instructions.

## Architecture Overview

Identity and auth in Arc follow a cookie-first, convention-based pattern:

```
Frontend (React) Backend (ASP.NET Core)
───────────────── ──────────────────────
<IdentityProvider> app.MapIdentityProvider()
└── useIdentity() hook └── GET /.cratis/me
│ │
├─ 1. Read .cratis-identity cookie │
└─ 2. If no cookie → fetch /.cratis/me │
AuthenticationMiddleware
└── IAuthenticationHandler[]
│ sets HttpContext.User
IIdentityProviderResultHandler
└── IProvideIdentityDetails.Provide()
IdentityProviderResult
→ JSON response
→ .cratis-identity cookie (base64)

Authorization:
[Authorize] / [Roles("Admin")] / [AllowAnonymous]
└── AuthorizationEvaluator checks per command/query
```

**Key design decisions:**
- The `.cratis-identity` cookie is `HttpOnly=false` so the frontend JavaScript can read it directly — no extra HTTP call needed on page load.
- Identity details are base64-encoded JSON in the cookie, automatically decoded by the frontend `IdentityProvider`.
- Only one `IProvideIdentityDetails` implementation is allowed per application (auto-discovered). If none exists, a default provider grants access to everyone.
- Cratis has its own `[Authorize]`, `[AllowAnonymous]`, and `[Roles]` attributes in `Cratis.Arc.Authorization` — these are distinct from ASP.NET Core's and are evaluated by `AuthorizationEvaluator` in the command and query pipeline.

---

## Decision Tree — Which Reference to Read

Use this decision tree to determine which reference file(s) to read based on the user's task:

| User wants to... | Read this reference |
|---|---|
| Add identity details to their app | [references/backend-identity.md](references/backend-identity.md) |
| Customize `IProvideIdentityDetails` (enrich from DB, block users, multi-tenant, preferences) | [references/backend-identity.md](references/backend-identity.md) |
| Modify identity at runtime (stateless selections, `ModifyDetails`) | [references/backend-identity.md](references/backend-identity.md) |
| Use Azure AD / Entra ID / Microsoft Identity | [references/authentication.md](references/authentication.md) |
| Write a custom authentication handler (API key, JWT, etc.) | [references/authentication.md](references/authentication.md) |
| Protect commands or queries with roles | [references/authorization.md](references/authorization.md) |
| Set up `[Authorize]`, `[AllowAnonymous]`, or `[Roles]` | [references/authorization.md](references/authorization.md) |
| Consume identity in React | [references/frontend.md](references/frontend.md) |
| Use identity in MVVM or vanilla TypeScript | [references/frontend.md](references/frontend.md) |
| Test identity locally without Azure | [references/local-development.md](references/local-development.md) |
| Full-stack setup (backend + frontend) | Read all references in order |

**For full-stack tasks, read in this order:**
1. `references/backend-identity.md` — identity provider and startup
2. `references/authentication.md` — how users are authenticated
3. `references/authorization.md` — protecting commands and queries
4. `references/frontend.md` — consuming identity in the UI
5. `references/local-development.md` — testing without real infrastructure

---

## Quick-Start: Full-Stack Identity Setup

This is the minimum checklist for an application with identity. Read the reference files for details on each step.

### Backend

1. **Details record**: Define a C# record for application-specific user information
2. **Identity provider**: Implement `IProvideIdentityDetails<TDetails>` (auto-discovered, one per app)
3. **Startup**: Call `app.MapIdentityProvider()` to register `GET /.cratis/me`
4. **Authentication**: Add `AddMicrosoftIdentityPlatformIdentityAuthentication()` or implement `IAuthenticationHandler`
5. **Authorization**: Add `[Authorize]` / `[Roles]` / `[AllowAnonymous]` attributes from `Cratis.Arc.Authorization` to commands and queries

### Frontend

6. **Provider**: Wrap app root with `<IdentityProvider>` from `@cratis/arc.react/identity`
7. **Hook**: Use `useIdentity<TDetails>()` to access identity anywhere in the component tree
8. **Roles**: Use `identity.isInRole('Admin')` for UI-level role gating

### Proxy Generation

9. Using `IProvideIdentityDetails<TDetails>` (generic) enables automatic TypeScript type generation at `dotnet build` time — the generated types can be imported in the frontend for end-to-end type safety.

---

## Critical Rules

These rules are frequently violated — always enforce them:

1. **One identity provider per app**: Only one `IProvideIdentityDetails` implementation is allowed. Multiple throws `MultipleIdentityDetailsProvidersFound`.
2. **Use Cratis attributes, not ASP.NET Core's**: `[Authorize]`, `[Roles]`, `[AllowAnonymous]` must come from `Cratis.Arc.Authorization`, not `Microsoft.AspNetCore.Authorization`.
3. **Never combine `[Authorize]` and `[AllowAnonymous]` on the same target**: This throws `AmbiguousAuthorizationLevel`.
4. **Prefer the generic interface**: Use `IProvideIdentityDetails<TDetails>` over `IProvideIdentityDetails` to enable proxy generation.
5. **Auto-discovery**: Both `IProvideIdentityDetails` and `IAuthenticationHandler` implementations are auto-discovered — no DI registration needed.
6. **Frontend role checks are UX, not security**: `isInRole()` on the frontend hides UI elements. The backend `[Roles]` attribute is the actual security boundary.
7. **Build before frontend**: TypeScript proxy types for identity details are generated by `dotnet build`. The backend must compile before the frontend can import them.

---

## Common Code Patterns

### Protecting a command with roles

```csharp
[Command]
[Roles("Admin")]
public record PromoteUser(UserId Id)
{
public void Handle(IUserService users) => users.Promote(Id);
}
```

### Conditionally rendering UI based on roles

```tsx
const identity = useIdentity();

return identity.isInRole('Admin')
? <AdminPanel />
: <AccessDenied />;
```

### Modifying identity at runtime (stateless selections)

```csharp
public class SetDepartment(IIdentityProviderResultHandler identityHandler)
{
public async Task Handle(string department) =>
await identityHandler.ModifyDetails<UserDetails>(
details => details with { SelectedDepartment = department });
}
```

---

## Reference Documentation

### Skill references (detailed implementation guidance)

- [Backend Identity Provider](references/backend-identity.md) — `IProvideIdentityDetails`, `IdentityProviderContext`, cookie mechanics, proxy generation, `ModifyDetails`
- [Authentication](references/authentication.md) — `IAuthenticationHandler`, `AuthenticationResult`, Microsoft Identity Platform, combining handlers
- [Authorization](references/authorization.md) — `[Authorize]`, `[Roles]`, `[AllowAnonymous]`, inheritance rules, fallback policies
- [Frontend Identity](references/frontend.md) — React `IdentityProvider`, `useIdentity()`, MVVM, core identity, role checking
- [Local Development](references/local-development.md) — Generating principals, ModHeader, cookie fallback, dev testing

### Project documentation

- [Backend Identity](Documentation/backend/identity.md)
- [Microsoft Identity](Documentation/backend/asp-net-core/microsoft-identity.md)
- [ASP.NET Core Authorization](Documentation/backend/asp-net-core/authorization.md)
- [Core Authentication](Documentation/backend/core/authentication.md)
- [Core Authorization](Documentation/backend/core/authorization.md)
- [Command Authorization](Documentation/backend/commands/model-bound/authorization.md)
- [Query Authorization](Documentation/backend/queries/model-bound/authorization.md)
- [Proxy Generation for Identity](Documentation/backend/proxy-generation/identity-details.md)
- [Frontend Core Identity](Documentation/frontend/core/identity.md)
- [Frontend React Identity](Documentation/frontend/react/identity.md)
- [Frontend MVVM Identity](Documentation/frontend/react.mvvm/identity.md)
- [Generating Principals for Local Dev](Documentation/general/generating-principal.md)
129 changes: 129 additions & 0 deletions .github/skills/auth-and-identity/references/authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Authentication

This reference covers implementing custom authentication handlers and integrating Microsoft Identity Platform.

## Authentication Pipeline

Arc's authentication system is built around `IAuthenticationHandler` in `Cratis.Arc.Authentication`. Handlers are called in sequence until one succeeds or fails:

1. Each registered handler is called in order
2. If a handler returns `Succeeded` → request is authenticated, pipeline stops
3. If a handler returns `Failed` → request is rejected (401), pipeline stops
4. If a handler returns `Anonymous` → try the next handler
5. If all handlers return `Anonymous` → request is unauthenticated

## `AuthenticationResult` Outcomes

| Method | Meaning | When to Use |
|--------|---------|-------------|
| `AuthenticationResult.Anonymous` | Handler doesn't apply | No relevant credentials found — let other handlers try |
| `AuthenticationResult.Succeeded(principal)` | Authentication succeeded | Valid credentials → return a `ClaimsPrincipal` |
| `AuthenticationResult.Failed(reason)` | Authentication explicitly failed | Credentials present but invalid → return 401 |

## Custom Authentication Handler

Handlers are **auto-discovered** — implement `IAuthenticationHandler` and Arc registers it. No manual DI wiring needed.

```csharp
using System.Security.Claims;
using Cratis.Arc.Authentication;
using Cratis.Arc.Http;

public class ApiKeyAuthenticationHandler(IApiKeyValidator validator) : IAuthenticationHandler
{
public async Task<AuthenticationResult> HandleAuthentication(IHttpRequestContext context)
{
if (!context.Headers.TryGetValue("X-API-Key", out var apiKey))
return AuthenticationResult.Anonymous;

if (!await validator.IsValid(apiKey))
return AuthenticationResult.Failed(new AuthenticationFailureReason("Invalid API key"));

var claims = new[]
{
new Claim(ClaimTypes.Name, "API Client"),
new Claim(ClaimTypes.AuthenticationMethod, "ApiKey")
};

var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "ApiKey"));
return AuthenticationResult.Succeeded(principal);
}
}
```

### Common Patterns

**Bearer Token:**
```csharp
public class BearerTokenAuthenticationHandler : IAuthenticationHandler
{
public async Task<AuthenticationResult> HandleAuthentication(IHttpRequestContext context)
{
if (!context.Headers.TryGetValue("Authorization", out var header))
return AuthenticationResult.Anonymous;

if (!header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
return AuthenticationResult.Anonymous;

var token = header["Bearer ".Length..].Trim();
// validate token, build ClaimsPrincipal...
return AuthenticationResult.Succeeded(principal);
}
}
```

**Custom Header:**
```csharp
public class CustomHeaderAuthenticationHandler : IAuthenticationHandler
{
public Task<AuthenticationResult> HandleAuthentication(IHttpRequestContext context)
{
if (!context.Headers.TryGetValue("X-User-ID", out var userId))
return Task.FromResult(AuthenticationResult.Anonymous);

var claims = new[] { new Claim(ClaimTypes.NameIdentifier, userId) };
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "CustomHeader"));
return Task.FromResult(AuthenticationResult.Succeeded(principal));
}
}
```

### Best Practices

- **Return `Anonymous` when your handler doesn't apply** — never `Failed` just because your header is missing
- **Provide clear failure reasons** — `AuthenticationFailureReason("API key is expired")`
- **Dependency injection works** — handlers can take constructor dependencies
- **Handle exceptions gracefully** — catch validation errors and return `Failed` with a reason

## Microsoft Identity Platform

For Azure-hosted apps using Azure AD / Entra ID, Arc provides built-in support:

```csharp
builder.Services.AddMicrosoftIdentityPlatformIdentityAuthentication();
```

This registers an ASP.NET Core `AuthenticationHandler` that reads Azure-provided headers:

| Header | Description |
|--------|-------------|
| `x-ms-client-principal` | Base64-encoded Microsoft Client Principal token |
| `x-ms-client-principal-id` | User's unique ID from Azure AD |
| `x-ms-client-principal-name` | User's display name |

These headers are automatically set by Azure Container Apps, Web Apps, and Static Web Apps. You also need:

```csharp
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapIdentityProvider();
```

## Combining Multiple Handlers

Multiple handlers coexist naturally because `Anonymous` means "I don't handle this request":

1. **ApiKeyAuthenticationHandler** — if `X-API-Key` present, authenticates or rejects. If absent, returns `Anonymous`.
2. **MicrosoftIdentityPlatformHandler** — checks for `x-ms-client-principal`. If absent, returns `Anonymous`.
3. If all return `Anonymous` → request is unauthenticated.
Loading
Loading