@cratis/pi 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/package.json +24 -0
- package/skills/cratis-application-react-specifications/LICENSE +2 -0
- package/skills/cratis-application-react-specifications/SKILL.md +192 -0
- package/skills/cratis-application-slice-specifications/LICENSE +2 -0
- package/skills/cratis-application-slice-specifications/SKILL.md +222 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/LICENSE +2 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/SKILL.md +233 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/authentication.md +140 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/authorization.md +112 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/frontend.md +102 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/local-development.md +85 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/tenancy.md +87 -0
- package/skills/cratis-arc-command/LICENSE +2 -0
- package/skills/cratis-arc-command/SKILL.md +286 -0
- package/skills/cratis-arc-command/references/command-result.md +111 -0
- package/skills/cratis-arc-command/references/handler-shapes.md +135 -0
- package/skills/cratis-arc-command/references/proxy-generation.md +87 -0
- package/skills/cratis-arc-command/references/read-model-injection.md +97 -0
- package/skills/cratis-arc-command-execution/LICENSE +2 -0
- package/skills/cratis-arc-command-execution/SKILL.md +148 -0
- package/skills/cratis-arc-command-validation/LICENSE +2 -0
- package/skills/cratis-arc-command-validation/SKILL.md +230 -0
- package/skills/cratis-arc-ef-core-migration/LICENSE +2 -0
- package/skills/cratis-arc-ef-core-migration/SKILL.md +231 -0
- package/skills/cratis-arc-observable-query-http/LICENSE +2 -0
- package/skills/cratis-arc-observable-query-http/SKILL.md +157 -0
- package/skills/cratis-arc-query-paging/LICENSE +2 -0
- package/skills/cratis-arc-query-paging/SKILL.md +173 -0
- package/skills/cratis-arc-react-page/LICENSE +2 -0
- package/skills/cratis-arc-react-page/SKILL.md +291 -0
- package/skills/cratis-arc-react-page/references/data-page.md +125 -0
- package/skills/cratis-arc-react-page/references/data-tables.md +136 -0
- package/skills/cratis-arc-react-page/references/dialogs.md +238 -0
- package/skills/cratis-arc-react-page/references/mvvm.md +150 -0
- package/skills/cratis-arc-react-page/references/queries-and-commands.md +200 -0
- package/skills/cratis-chronicle-cli-operations/LICENSE +2 -0
- package/skills/cratis-chronicle-cli-operations/SKILL.md +199 -0
- package/skills/cratis-chronicle-client-dotnet/LICENSE +2 -0
- package/skills/cratis-chronicle-client-dotnet/SKILL.md +433 -0
- package/skills/cratis-chronicle-client-elixir/LICENSE +2 -0
- package/skills/cratis-chronicle-client-elixir/SKILL.md +429 -0
- package/skills/cratis-chronicle-client-kotlin/LICENSE +2 -0
- package/skills/cratis-chronicle-client-kotlin/SKILL.md +498 -0
- package/skills/cratis-chronicle-client-typescript/LICENSE +2 -0
- package/skills/cratis-chronicle-client-typescript/SKILL.md +396 -0
- package/skills/cratis-chronicle-compliance/LICENSE +2 -0
- package/skills/cratis-chronicle-compliance/SKILL.md +246 -0
- package/skills/cratis-chronicle-event-constraints/LICENSE +2 -0
- package/skills/cratis-chronicle-event-constraints/SKILL.md +227 -0
- package/skills/cratis-chronicle-event-modeling/LICENSE +2 -0
- package/skills/cratis-chronicle-event-modeling/SKILL.md +157 -0
- package/skills/cratis-chronicle-event-specifications/LICENSE +2 -0
- package/skills/cratis-chronicle-event-specifications/SKILL.md +200 -0
- package/skills/cratis-chronicle-event-type-migration/LICENSE +2 -0
- package/skills/cratis-chronicle-event-type-migration/SKILL.md +235 -0
- package/skills/cratis-chronicle-mcp-inspection/LICENSE +2 -0
- package/skills/cratis-chronicle-mcp-inspection/SKILL.md +79 -0
- package/skills/cratis-chronicle-mcp-inspection/references/blocked-tools.md +12 -0
- package/skills/cratis-chronicle-mcp-inspection/references/observational-tools.md +8 -0
- package/skills/cratis-chronicle-multi-tenancy/LICENSE +2 -0
- package/skills/cratis-chronicle-multi-tenancy/SKILL.md +198 -0
- package/skills/cratis-chronicle-projection/LICENSE +2 -0
- package/skills/cratis-chronicle-projection/SKILL.md +162 -0
- package/skills/cratis-chronicle-projection/references/fluent-builder.md +126 -0
- package/skills/cratis-chronicle-projection/references/model-bound-attributes.md +118 -0
- package/skills/cratis-chronicle-reactor/LICENSE +2 -0
- package/skills/cratis-chronicle-reactor/SKILL.md +344 -0
- package/skills/cratis-chronicle-read-model/LICENSE +2 -0
- package/skills/cratis-chronicle-read-model/SKILL.md +193 -0
- package/skills/cratis-chronicle-read-model/references/queries.md +142 -0
- package/skills/cratis-chronicle-read-model-specifications/LICENSE +2 -0
- package/skills/cratis-chronicle-read-model-specifications/SKILL.md +176 -0
- package/skills/cratis-chronicle-reducer/LICENSE +2 -0
- package/skills/cratis-chronicle-reducer/SKILL.md +173 -0
- package/skills/cratis-chronicle-web-workbench/LICENSE +2 -0
- package/skills/cratis-chronicle-web-workbench/SKILL.md +177 -0
- package/skills/cratis-cli-terminal-workbench/LICENSE +2 -0
- package/skills/cratis-cli-terminal-workbench/SKILL.md +156 -0
- package/skills/cratis-code-review/LICENSE +2 -0
- package/skills/cratis-code-review/SKILL.md +177 -0
- package/skills/cratis-components-accessibility/LICENSE +2 -0
- package/skills/cratis-components-accessibility/SKILL.md +176 -0
- package/skills/cratis-components-schema-editor/LICENSE +2 -0
- package/skills/cratis-components-schema-editor/SKILL.md +238 -0
- package/skills/cratis-components-stepper-command-dialog/LICENSE +2 -0
- package/skills/cratis-components-stepper-command-dialog/SKILL.md +209 -0
- package/skills/cratis-components-styling/LICENSE +2 -0
- package/skills/cratis-components-styling/SKILL.md +221 -0
- package/skills/cratis-components-toolbar/LICENSE +2 -0
- package/skills/cratis-components-toolbar/SKILL.md +259 -0
- package/skills/cratis-documentation-writing/LICENSE +2 -0
- package/skills/cratis-documentation-writing/SKILL.md +121 -0
- package/skills/cratis-event-model-diagram/LICENSE +2 -0
- package/skills/cratis-event-model-diagram/SKILL.md +142 -0
- package/skills/cratis-fundamentals-concept/LICENSE +2 -0
- package/skills/cratis-fundamentals-concept/SKILL.md +198 -0
- package/skills/cratis-fundamentals-type-discovery/LICENSE +2 -0
- package/skills/cratis-fundamentals-type-discovery/SKILL.md +152 -0
- package/skills/cratis-governed-release-methodology/LICENSE +2 -0
- package/skills/cratis-governed-release-methodology/SKILL.md +297 -0
- package/skills/cratis-lens-browser-extension/LICENSE +2 -0
- package/skills/cratis-lens-browser-extension/SKILL.md +239 -0
- package/skills/cratis-performance-review/LICENSE +2 -0
- package/skills/cratis-performance-review/SKILL.md +139 -0
- package/skills/cratis-screenplay-model-authoring/LICENSE +2 -0
- package/skills/cratis-screenplay-model-authoring/SKILL.md +231 -0
- package/skills/cratis-security-review/LICENSE +2 -0
- package/skills/cratis-security-review/SKILL.md +141 -0
- package/skills/cratis-specification-by-example/LICENSE +2 -0
- package/skills/cratis-specification-by-example/SKILL.md +188 -0
- package/skills/cratis-specifications-csharp/LICENSE +2 -0
- package/skills/cratis-specifications-csharp/SKILL.md +230 -0
- package/skills/cratis-specifications-csharp/references/application-scenarios.md +191 -0
- package/skills/cratis-specifications-csharp/references/csharp-patterns.md +142 -0
- package/skills/cratis-specifications-csharp/references/integration-specs.md +126 -0
- package/skills/cratis-specifications-typescript/LICENSE +2 -0
- package/skills/cratis-specifications-typescript/SKILL.md +202 -0
- package/skills/cratis-specifications-typescript/references/typescript-patterns.md +168 -0
- package/skills/cratis-stage-rendering-and-sandbox/LICENSE +2 -0
- package/skills/cratis-stage-rendering-and-sandbox/SKILL.md +230 -0
- package/skills/cratis-studio-mcp-safety-guidance/LICENSE +2 -0
- package/skills/cratis-studio-mcp-safety-guidance/SKILL.md +69 -0
- package/skills/cratis-studio-mcp-safety-guidance/references/blocked-tools.md +12 -0
- package/skills/cratis-studio-mcp-safety-guidance/references/observational-tools.md +8 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cratis-arc-authentication-authorization-and-identity
|
|
3
|
+
description: Wire authentication, authorization and identity in a Cratis Arc application — IProvideIdentityDetails and the /.cratis/me endpoint, the [Authorize]/[Roles]/[AllowAnonymous] attributes Arc actually evaluates, IAuthenticationHandler, the Microsoft Identity Platform header contract, tenant resolution, and the React identity hooks. Use for Arc identity providers, endpoint protection, roles, or frontend identity integration. Do not use for isolated command validation, and do not use for Chronicle tenant namespaces alone.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Authentication, authorization and identity in Arc
|
|
8
|
+
|
|
9
|
+
Arc separates three things that are easy to conflate: **authentication** decides
|
|
10
|
+
who the caller is, **authorization** decides whether that caller may run this
|
|
11
|
+
command or query, and the **identity provider** decides what the frontend is
|
|
12
|
+
told about them. Each has its own extension point.
|
|
13
|
+
|
|
14
|
+
## Verified product sources
|
|
15
|
+
|
|
16
|
+
| Package | Version | Purpose |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| `Cratis.Arc.Core` | `22.10.4` | `Cratis.Arc.Identity`, `Cratis.Arc.Authorization`, `Cratis.Arc.Authentication`, `Cratis.Arc.Tenancy` |
|
|
19
|
+
| `Cratis.Arc` | `22.10.4` | ASP.NET Core hosting, `AddMicrosoftIdentityPlatformIdentityAuthentication` |
|
|
20
|
+
| `@cratis/arc` | `22.10.4` | `IdentityProvider`, `IIdentity` |
|
|
21
|
+
| `@cratis/arc.react` | `22.10.4` | `IdentityProvider` component, `useIdentity`, `RequireRole` |
|
|
22
|
+
|
|
23
|
+
Reverify before claiming support for another version.
|
|
24
|
+
|
|
25
|
+
## Read this first: what is and is not trusted
|
|
26
|
+
|
|
27
|
+
Arc's shipped authentication reads a **forwarded** principal from HTTP headers.
|
|
28
|
+
`x-ms-client-principal` is base64, **not a signature**, and Arc does not check
|
|
29
|
+
who sent it. Any caller that can reach the application can author the entire
|
|
30
|
+
serialized principal.
|
|
31
|
+
|
|
32
|
+
That is not a defect — it is the EasyAuth/reverse-proxy model, where the ingress
|
|
33
|
+
is the trust boundary. But it means:
|
|
34
|
+
|
|
35
|
+
- the application must be unreachable except through the ingress that sets those
|
|
36
|
+
headers;
|
|
37
|
+
- nothing Arc gives you makes the forwarded values authentic;
|
|
38
|
+
- Arc ships **no** OpenID Connect and **no** JWT bearer integration. If you need
|
|
39
|
+
the application itself to validate a token, that is ordinary ASP.NET Core
|
|
40
|
+
authentication you configure yourself.
|
|
41
|
+
|
|
42
|
+
## Identity details — what the frontend is told
|
|
43
|
+
|
|
44
|
+
Implement `IProvideIdentityDetails` to shape what `/.cratis/me` returns:
|
|
45
|
+
|
|
46
|
+
```csharp
|
|
47
|
+
using Cratis.Arc.Identity;
|
|
48
|
+
|
|
49
|
+
public class <Name>IdentityDetailsProvider : IProvideIdentityDetails<<Details>>
|
|
50
|
+
{
|
|
51
|
+
public async Task<IdentityDetails> Provide(IdentityProviderContext context)
|
|
52
|
+
{
|
|
53
|
+
// context.Id, context.Name, context.Claims
|
|
54
|
+
return new IdentityDetails(<isAuthorized>, new <Details>(<...>));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- `IdentityDetails` is `record IdentityDetails(bool IsUserAuthorized, object Details)`.
|
|
60
|
+
`Details` is **`object`**, not generic; it is serialized as JSON.
|
|
61
|
+
- `IProvideIdentityDetails<TDetails>` adds no members. It exists only so Arc can
|
|
62
|
+
capture the details type reflectively, which is what feeds the JSON schema
|
|
63
|
+
endpoint and the generated proxy. Implement **both** — the generic one for the
|
|
64
|
+
type, the non-generic `Provide` for the behavior.
|
|
65
|
+
- `IdentityProviderContext` is `(IdentityId Id, IdentityName Name, IEnumerable<KeyValuePair<string, string>> Claims)`.
|
|
66
|
+
- Registration is by discovery: exactly one non-default implementation is
|
|
67
|
+
expected. More than one is `MultipleIdentityDetailsProvidersFound`; none falls
|
|
68
|
+
back to `DefaultIdentityDetailsProvider`, which returns
|
|
69
|
+
`new IdentityDetails(true, new { })`. The provider is registered **scoped**.
|
|
70
|
+
|
|
71
|
+
### The endpoints
|
|
72
|
+
|
|
73
|
+
| Route | What it does |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `GET /.cratis/me` | The current identity. 401 when not authenticated, 403 when the provider says not authorized, otherwise the result plus the identity cookie |
|
|
76
|
+
| `GET /.cratis/identity-details/schema` | The JSON schema of the details type |
|
|
77
|
+
| `GET /.cratis/users` | Development tooling — aggregates every `ICanProvideUsers` |
|
|
78
|
+
| `GET /.cratis/tenants` | Development tooling — aggregates every `ICanProvideTenants` |
|
|
79
|
+
|
|
80
|
+
All four are mapped `AllowAnonymous`; `/.cratis/me` does its own 401/403. There
|
|
81
|
+
is no public constant for these routes — they are string literals in the endpoint
|
|
82
|
+
mapper.
|
|
83
|
+
|
|
84
|
+
`/.cratis/me` writes the result as JSON **and** as a base64 cookie named
|
|
85
|
+
`.cratis-identity`, deliberately **not `HttpOnly`** so the frontend can read it
|
|
86
|
+
without a round trip. `Secure` follows whether the request is HTTPS, `SameSite`
|
|
87
|
+
is `Lax`, `Path` is `/`.
|
|
88
|
+
|
|
89
|
+
⚠️ A non-`HttpOnly` cookie the browser can read is a cookie the user can edit.
|
|
90
|
+
Treat everything in it as a **display** input. Never let it decide anything the
|
|
91
|
+
server has not already decided.
|
|
92
|
+
|
|
93
|
+
## Authorization — what Arc actually evaluates
|
|
94
|
+
|
|
95
|
+
Arc ships exactly three attributes, all in `Cratis.Arc.Authorization`:
|
|
96
|
+
|
|
97
|
+
| Attribute | Targets | Notes |
|
|
98
|
+
| --- | --- | --- |
|
|
99
|
+
| `[AllowAnonymous]` | class, method | No arguments |
|
|
100
|
+
| `[Authorize]` | class, method, repeatable | Settable `Policy`, `Roles` (comma-delimited), `AuthenticationSchemes` |
|
|
101
|
+
| `[Roles(params string[] roles)]` | class, method | Derives from `[Authorize]`, joins the roles with commas |
|
|
102
|
+
|
|
103
|
+
There is **no** `[Policy]`, `[Scopes]` or `[Claims]` attribute anywhere in Arc.
|
|
104
|
+
|
|
105
|
+
⚠️ **`AuthorizeAttribute.Policy` and `AuthenticationSchemes` are declared but
|
|
106
|
+
never read by Arc's evaluator.** Writing `[Authorize(Policy = "…")]` compiles and
|
|
107
|
+
enforces nothing. Only `Roles` is evaluated.
|
|
108
|
+
|
|
109
|
+
Evaluation, in order:
|
|
110
|
+
|
|
111
|
+
1. every `IAnonymousEvaluator` is consulted; the first non-null answer decides
|
|
112
|
+
anonymous access;
|
|
113
|
+
2. every `IAuthorizationAttributeEvaluator` is consulted for `[Authorize]` and
|
|
114
|
+
its `Roles`;
|
|
115
|
+
3. no `[Authorize]` at all means **authorized**; with `[Authorize]`, a null or
|
|
116
|
+
unauthenticated principal fails, and a non-empty role list passes when the
|
|
117
|
+
user is in **any** of them (comma-split and trimmed — OR, not AND);
|
|
118
|
+
4. a method with no `[Authorize]` falls back to its declaring type's verdict.
|
|
119
|
+
|
|
120
|
+
Both `[Authorize]` and `[AllowAnonymous]` on the same member throws
|
|
121
|
+
`AmbiguousAuthorizationLevel`.
|
|
122
|
+
|
|
123
|
+
`ARC0011` warns when a `[Roles]` argument is a string literal: use
|
|
124
|
+
`nameof(<RoleEnum>.<Member>)` so a rename is a compile error rather than a silent
|
|
125
|
+
authorization failure — a desynchronized literal either locks the endpoint or
|
|
126
|
+
matches a stale role.
|
|
127
|
+
|
|
128
|
+
Read [authorization](references/authorization.md) for the default-access
|
|
129
|
+
question, which is genuinely different between Arc's two hosting models, and for
|
|
130
|
+
the filter and system-execution extension points.
|
|
131
|
+
|
|
132
|
+
## Authentication — the extension point
|
|
133
|
+
|
|
134
|
+
`IAuthenticationHandler` is a single method,
|
|
135
|
+
`Task<AuthenticationResult> HandleAuthentication(IHttpRequestContext context)`.
|
|
136
|
+
Implement it and it is discovered — there is **no** `IArcBuilder` extension and
|
|
137
|
+
**no** options type for authentication.
|
|
138
|
+
|
|
139
|
+
`AuthenticationResult` is
|
|
140
|
+
`record (ClaimsPrincipal? Principal = default, AuthenticationFailure? Failure = default)`
|
|
141
|
+
with `Anonymous`, `Succeeded(principal)` and `Failed(reason)`, and
|
|
142
|
+
`IsAuthenticated => Principal is not null`.
|
|
143
|
+
|
|
144
|
+
Arc ships `MicrosoftIdentityPlatformAuthenticationHandler` in `Arc.Core`, which
|
|
145
|
+
reads the forwarded principal. See
|
|
146
|
+
[authentication](references/authentication.md) for the exact header contract,
|
|
147
|
+
the claim Arc reserves, and the ASP.NET Core scheme variant.
|
|
148
|
+
|
|
149
|
+
## Tenancy
|
|
150
|
+
|
|
151
|
+
`Cratis.Arc.Tenancy` resolves a tenant per request. The default resolver is
|
|
152
|
+
**header-based** on `x-cratis-tenant-id`; `TenantResolverType` also offers
|
|
153
|
+
`Query`, `Claim`, `Development`, `Subdomain` and `Fixed`, configured through
|
|
154
|
+
`ArcOptions.Tenancy` or the `Use*Tenancy` extension methods.
|
|
155
|
+
|
|
156
|
+
`ITenantIdAccessor.Current` gives the current `TenantId`, which is
|
|
157
|
+
`TenantId.NotSet` (`"[NotSet]"`) when nothing resolved. See
|
|
158
|
+
[tenancy](references/tenancy.md) for the exact defaults and each resolver's
|
|
159
|
+
behavior.
|
|
160
|
+
|
|
161
|
+
Arc tenancy is request-scoped tenant *resolution*. It is not Chronicle's tenant
|
|
162
|
+
namespace isolation — that is separate guidance.
|
|
163
|
+
|
|
164
|
+
## Frontend
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
import { Arc } from '@cratis/arc.react';
|
|
168
|
+
|
|
169
|
+
<Arc detailsType={<Details>}>{children}</Arc>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`<Arc>` renders the `IdentityProvider` context for you. Inside it:
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import { useIdentity, RequireRole } from '@cratis/arc.react/identity';
|
|
176
|
+
|
|
177
|
+
const identity = useIdentity(<Details>, <defaultDetails>);
|
|
178
|
+
// identity.id, .name, .roles, .details, .isSet, .isInRole(role)
|
|
179
|
+
// identity.isLoading, identity.clearIdentity()
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`useIdentity` has two overloads: `(type, defaultDetails?)` for type-safe details
|
|
183
|
+
and `(defaultDetails?)` without. The default details stand in whenever there are
|
|
184
|
+
no details to give, not only when the identity is explicitly unset.
|
|
185
|
+
|
|
186
|
+
`RequireRole` gates rendering by `roles` or by an `allow` predicate, with
|
|
187
|
+
`whileLoading` and `forbidden` slots.
|
|
188
|
+
|
|
189
|
+
⚠️ **`RequireRole` hides UI; it does not protect data.** The identity it reads
|
|
190
|
+
comes from the deliberately non-`HttpOnly` cookie above. Every rule it expresses
|
|
191
|
+
must also exist on the server as `[Authorize]`/`[Roles]` or an authorization
|
|
192
|
+
filter.
|
|
193
|
+
|
|
194
|
+
`Arc.React.MVVM` has **no** identity hook. It registers `IIdentityProvider` in
|
|
195
|
+
its container so a view model can constructor-inject it; that is all.
|
|
196
|
+
|
|
197
|
+
See [frontend](references/frontend.md) for the exact exported shapes.
|
|
198
|
+
|
|
199
|
+
## Local development
|
|
200
|
+
|
|
201
|
+
Arc ships **no** development identity provider, no fake principal, and no local
|
|
202
|
+
sign-in. What exists for development is the two anonymous listing endpoints,
|
|
203
|
+
`/.cratis/users` and `/.cratis/tenants`, which aggregate whatever the application
|
|
204
|
+
implements as `ICanProvideUsers` and `ICanProvideTenants`.
|
|
205
|
+
|
|
206
|
+
To run locally with an identity, supply the forwarded headers yourself — see
|
|
207
|
+
[local development](references/local-development.md).
|
|
208
|
+
|
|
209
|
+
⚠️ Those two endpoints are `AllowAnonymous` and mapped unconditionally. If your
|
|
210
|
+
`ICanProvideUsers` implementation returns real users, that list is public. Return
|
|
211
|
+
development fixtures only, and gate the implementation on a development
|
|
212
|
+
environment check.
|
|
213
|
+
|
|
214
|
+
## Verify
|
|
215
|
+
|
|
216
|
+
- Exactly one non-default `IProvideIdentityDetails` implementation exists, and it
|
|
217
|
+
also implements `IProvideIdentityDetails<TDetails>`.
|
|
218
|
+
- No authorization decision depends on `[Authorize(Policy = …)]`.
|
|
219
|
+
- `[Roles]` arguments use `nameof`, so `ARC0011` is silent.
|
|
220
|
+
- No member carries both `[Authorize]` and `[AllowAnonymous]`.
|
|
221
|
+
- Every rule the frontend enforces also exists on the server.
|
|
222
|
+
- Nothing trusts the identity cookie or the forwarded headers beyond what the
|
|
223
|
+
ingress guarantees.
|
|
224
|
+
- The application is not reachable except through that ingress.
|
|
225
|
+
- Development user and tenant providers return fixtures, never production data.
|
|
226
|
+
- `dotnet build` is clean in Debug and Release.
|
|
227
|
+
|
|
228
|
+
## Route near misses
|
|
229
|
+
|
|
230
|
+
- Rejecting a command's input or state: `cratis-arc-command-validation` scope.
|
|
231
|
+
- Chronicle tenant namespaces and per-tenant event stores: the Chronicle
|
|
232
|
+
multi-tenancy guidance.
|
|
233
|
+
- Building the React page that consumes identity: the Arc React guidance.
|
package/skills/cratis-arc-authentication-authorization-and-identity/references/authentication.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Authentication
|
|
2
|
+
|
|
3
|
+
Verified against `Cratis.Arc.Core` and `Cratis.Arc` `22.10.4`. Types are in
|
|
4
|
+
`Cratis.Arc.Authentication` and `Cratis.Arc.Identity`.
|
|
5
|
+
|
|
6
|
+
## The contract
|
|
7
|
+
|
|
8
|
+
```csharp
|
|
9
|
+
public interface IAuthenticationHandler
|
|
10
|
+
{
|
|
11
|
+
Task<AuthenticationResult> HandleAuthentication(IHttpRequestContext context);
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`IAuthentication` aggregates the discovered handlers: `HasHandlers`, and a
|
|
16
|
+
`HandleAuthentication` that returns the **first** result which is either
|
|
17
|
+
authenticated or carries a failure; otherwise `AuthenticationResult.Anonymous`.
|
|
18
|
+
|
|
19
|
+
```csharp
|
|
20
|
+
public record AuthenticationResult(
|
|
21
|
+
ClaimsPrincipal? Principal = default,
|
|
22
|
+
AuthenticationFailure? Failure = default)
|
|
23
|
+
{
|
|
24
|
+
public static readonly AuthenticationResult Anonymous;
|
|
25
|
+
public bool IsAuthenticated => Principal is not null;
|
|
26
|
+
public static AuthenticationResult Failed(AuthenticationFailureReason reason);
|
|
27
|
+
public static AuthenticationResult Succeeded(ClaimsPrincipal principal);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`AuthenticationFailure` is `record (AuthenticationFailureReason Reason)` and
|
|
32
|
+
`AuthenticationFailureReason` is a `ConceptAs<string>` with an implicit string
|
|
33
|
+
conversion.
|
|
34
|
+
|
|
35
|
+
There is **no** `IArcBuilder` extension and **no** options type for
|
|
36
|
+
authentication. Configuration is by discovery — implement the interface on a
|
|
37
|
+
public class and it is registered through the convention bindings.
|
|
38
|
+
|
|
39
|
+
## The Microsoft Identity Platform handler
|
|
40
|
+
|
|
41
|
+
`MicrosoftIdentityPlatformAuthenticationHandler` ships in **Arc.Core** and is
|
|
42
|
+
transport-agnostic. (Documentation that says it exists only in the ASP.NET Core
|
|
43
|
+
package and must be hand-written for Arc.Core is stale.)
|
|
44
|
+
|
|
45
|
+
It reads three headers, all constants on
|
|
46
|
+
`Cratis.Arc.Identity.MicrosoftIdentityPlatformHeaders`:
|
|
47
|
+
|
|
48
|
+
| Constant | Header |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `PrincipalHeader` | `x-ms-client-principal` |
|
|
51
|
+
| `IdentityIdHeader` | `x-ms-client-principal-id` |
|
|
52
|
+
| `IdentityNameHeader` | `x-ms-client-principal-name` |
|
|
53
|
+
|
|
54
|
+
All three must be present or the result is `Anonymous`. The principal header is
|
|
55
|
+
base64-decoded and deserialized into `ClientPrincipal`:
|
|
56
|
+
|
|
57
|
+
```csharp
|
|
58
|
+
public class ClientPrincipal
|
|
59
|
+
{
|
|
60
|
+
public string IdentityProvider { get; set; }
|
|
61
|
+
public string UserId { get; set; }
|
|
62
|
+
public string UserDetails { get; set; }
|
|
63
|
+
public IEnumerable<string> UserRoles { get; set; }
|
|
64
|
+
public IEnumerable<ClientPrincipalClaim> Claims { get; set; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public class ClientPrincipalClaim
|
|
68
|
+
{
|
|
69
|
+
public string typ { get; set; }
|
|
70
|
+
public string val { get; set; }
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The lowercase `typ`/`val` members are the deliberate wire shape — do not
|
|
75
|
+
"correct" them.
|
|
76
|
+
|
|
77
|
+
A principal that fails to decode produces
|
|
78
|
+
`AuthenticationResult.Failed("Not authenticated - invalid representation of ClientPrincipal")`.
|
|
79
|
+
|
|
80
|
+
From a valid principal the handler builds a `ClaimsIdentity` whose
|
|
81
|
+
authentication type is `nameof(MicrosoftIdentityPlatformAuthenticationHandler)`:
|
|
82
|
+
|
|
83
|
+
1. every forwarded claim is copied as `Claim(typ, val)`;
|
|
84
|
+
2. `NameIdentifier`, `sub`, and any existing
|
|
85
|
+
`urn:cratis:arc:identity:provider` claim are **removed** (the last one
|
|
86
|
+
case-insensitively);
|
|
87
|
+
3. `Name` is set from `UserDetails`;
|
|
88
|
+
4. `NameIdentifier` and `sub` are set from the `x-ms-client-principal-id` header
|
|
89
|
+
— not from the body;
|
|
90
|
+
5. `urn:cratis:arc:identity:provider` is written from `IdentityProvider` when it
|
|
91
|
+
is not blank;
|
|
92
|
+
6. every entry of `UserRoles` becomes a `Role` claim.
|
|
93
|
+
|
|
94
|
+
## The reserved claim
|
|
95
|
+
|
|
96
|
+
`MicrosoftIdentityPlatformClaims.IdentityProvider` is
|
|
97
|
+
`"urn:cratis:arc:identity:provider"`. Removing any inbound claim of that type
|
|
98
|
+
before writing Arc's own buys **single provenance, not authenticity**: the claim
|
|
99
|
+
carries exactly one value, always from the same field of the forwarded principal.
|
|
100
|
+
|
|
101
|
+
Read it with `ClaimsPrincipal.FindFirst`/`FindAll` and never normalize the claim
|
|
102
|
+
type yourself. Those lookups compare the way the removal does; folding types with
|
|
103
|
+
`ToUpperInvariant` or `Trim` widens the match beyond what was removed, so a
|
|
104
|
+
forged type differing only by Unicode case folding or trailing whitespace would
|
|
105
|
+
match — and it precedes Arc's claim in the list.
|
|
106
|
+
|
|
107
|
+
The value's meaning is the ingress's choice, not Arc's: one ingress forwards a
|
|
108
|
+
canonical provider key, another an authentication scheme name or a display name
|
|
109
|
+
that changes when the provider is renamed. Treat it as metadata for telling
|
|
110
|
+
federations apart and for diagnostics. Where a durable provider key is needed,
|
|
111
|
+
read the claim the ingress publishes for that purpose.
|
|
112
|
+
|
|
113
|
+
Claim types outside the reserved set are copied through untouched.
|
|
114
|
+
|
|
115
|
+
## ASP.NET Core hosting
|
|
116
|
+
|
|
117
|
+
```csharp
|
|
118
|
+
services.AddMicrosoftIdentityPlatformIdentityAuthentication(scheme?);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
registers the standard ASP.NET Core scheme backed by
|
|
122
|
+
`MicrosoftIDentityPlatformAuthHandler` (note the capital `D` in the type name),
|
|
123
|
+
whose `SchemeName` is `"MicrosoftIdentityPlatform"`. `builder.AddCratis()` calls
|
|
124
|
+
this for you.
|
|
125
|
+
|
|
126
|
+
## What is not here
|
|
127
|
+
|
|
128
|
+
There is **no** OpenID Connect, **no** JWT bearer, and **no**
|
|
129
|
+
`Microsoft.Identity.Web` anywhere in Arc — neither in source nor in the shipped
|
|
130
|
+
documentation. Validating a token inside the application is ordinary ASP.NET
|
|
131
|
+
Core authentication that you configure yourself; Arc's model is a trusted ingress
|
|
132
|
+
forwarding a principal.
|
|
133
|
+
|
|
134
|
+
## The security boundary, stated plainly
|
|
135
|
+
|
|
136
|
+
`x-ms-client-principal` is base64, not a signature, and Arc does not check who
|
|
137
|
+
sent it. Any caller that can reach the application can author the entire
|
|
138
|
+
serialized principal, including the fields Arc reads. Trust it exactly as far as
|
|
139
|
+
you trust that your ingress is the only thing that can set it — which means the
|
|
140
|
+
application must not be reachable any other way.
|
package/skills/cratis-arc-authentication-authorization-and-identity/references/authorization.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Authorization
|
|
2
|
+
|
|
3
|
+
Verified against `Cratis.Arc.Core` and `Cratis.Arc` `22.10.4`. Everything below
|
|
4
|
+
is in `Cratis.Arc.Authorization` unless stated.
|
|
5
|
+
|
|
6
|
+
## Default access differs by hosting model
|
|
7
|
+
|
|
8
|
+
There is **no option anywhere in Arc** that sets a default authentication or
|
|
9
|
+
authorization requirement. `ArcOptions` has no such switch. What you get depends
|
|
10
|
+
on which pipeline is running.
|
|
11
|
+
|
|
12
|
+
### Self-hosted `ArcApplication` (Arc.Core)
|
|
13
|
+
|
|
14
|
+
`AuthenticationMiddleware` runs per request:
|
|
15
|
+
|
|
16
|
+
- **No `IAuthenticationHandler` registered at all → every endpoint is anonymous.**
|
|
17
|
+
The middleware returns immediately.
|
|
18
|
+
- With at least one handler registered, an endpoint marked `AllowAnonymous`
|
|
19
|
+
passes and everything else requires an authenticated principal — otherwise the
|
|
20
|
+
response is **401** with a body of `Unauthorized`.
|
|
21
|
+
|
|
22
|
+
`AllowsAnonymousAccess` defaults to **false**: `IsAnonymousAllowed()` on a
|
|
23
|
+
top-level type with no attributes has no declaring type to fall back to and
|
|
24
|
+
returns false. So under this pipeline commands and queries are
|
|
25
|
+
authenticated-by-default *once any handler exists*.
|
|
26
|
+
|
|
27
|
+
### ASP.NET Core hosting (the `Arc` package)
|
|
28
|
+
|
|
29
|
+
The endpoint mapper only ever calls `AllowAnonymous()`. It **never** calls
|
|
30
|
+
`RequireAuthorization()`, and nothing else in Arc does either. Endpoints are
|
|
31
|
+
therefore **anonymous by default** unless the application configures its own
|
|
32
|
+
fallback policy in ASP.NET Core.
|
|
33
|
+
|
|
34
|
+
Decide this deliberately for your host. Do not assume "Arc protects it".
|
|
35
|
+
|
|
36
|
+
## Role evaluation
|
|
37
|
+
|
|
38
|
+
`AuthorizationEvaluator`:
|
|
39
|
+
|
|
40
|
+
1. every `IAnonymousEvaluator` is asked; the first non-null answer wins — `true`
|
|
41
|
+
authorizes immediately, `false` stops asking and moves on;
|
|
42
|
+
2. every `IAuthorizationAttributeEvaluator` is asked for `[Authorize]` and its
|
|
43
|
+
`Roles`; the first that reports one wins;
|
|
44
|
+
3. no `[Authorize]` → **authorized**;
|
|
45
|
+
4. `[Authorize]` with a null or unauthenticated principal → denied;
|
|
46
|
+
5. `[Authorize]` with roles → authorized when the user is in **any** of them.
|
|
47
|
+
The string is comma-split and trimmed. It is OR, never AND;
|
|
48
|
+
6. a `MethodInfo` with no `[Authorize]` falls back to its declaring type.
|
|
49
|
+
|
|
50
|
+
`[Authorize]` and `[AllowAnonymous]` on the same member throws
|
|
51
|
+
`AmbiguousAuthorizationLevel` from `AnonymousEvaluator` and from
|
|
52
|
+
`AllowAnonymousExtensions.IsAnonymousAllowed`.
|
|
53
|
+
|
|
54
|
+
⚠️ The ASP.NET Core-flavoured evaluators (`AspNetAnonymousEvaluator`,
|
|
55
|
+
`AspNetAuthorizationAttributeEvaluator`) are declared inside
|
|
56
|
+
`namespace Cratis.Arc.Authorization` and do not import
|
|
57
|
+
`Microsoft.AspNetCore.Authorization`, so the attribute names in them bind to
|
|
58
|
+
**Arc's own** attributes. Do not rely on ASP.NET Core's `[Authorize]` being
|
|
59
|
+
honoured by them. `AspNetAnonymousEvaluator` also does not throw on the
|
|
60
|
+
both-attributes case; it prefers `AllowAnonymous`.
|
|
61
|
+
|
|
62
|
+
## The filters
|
|
63
|
+
|
|
64
|
+
| Interface | Namespace | Shape |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| `IAuthorizationCommandFilter` | `Cratis.Arc.Commands` | Empty marker over `ICommandFilter` (`Task<CommandResult> OnExecution(CommandContext)`) |
|
|
67
|
+
| `IAuthorizationQueryFilter` | `Cratis.Arc.Queries` | Empty marker over `IQueryFilter` (`Task<QueryResult> OnPerform(QueryContext)`) |
|
|
68
|
+
|
|
69
|
+
Both are found by discovery — implement one on a public class and it is picked
|
|
70
|
+
up. The marker is what guarantees ordering: the filter chains sort
|
|
71
|
+
authorization filters first with a stable sort, so a validation failure can never
|
|
72
|
+
short-circuit the chain before the authorization verdict is recorded.
|
|
73
|
+
|
|
74
|
+
Built-in implementations: the command `AuthorizationFilter` returns
|
|
75
|
+
`CommandResult.Unauthorized(...)` from `IAuthorizationEvaluator`; the query
|
|
76
|
+
`AuthorizationFilter` asks the query performer, and returns **success** when no
|
|
77
|
+
performer is found for the name.
|
|
78
|
+
|
|
79
|
+
⚠️ `AuthorizationResult` — `record (bool IsAuthorized, string? FailureReason)`
|
|
80
|
+
with `Success` and `Failure(reason)` — is **not** what either filter returns and
|
|
81
|
+
not what `IAuthorizationEvaluator` produces (that returns a plain `bool`). It is
|
|
82
|
+
a standalone type, usable as a command `Provide()` short-circuit value. Do not
|
|
83
|
+
describe it as the filter contract.
|
|
84
|
+
|
|
85
|
+
## Executing as a system actor
|
|
86
|
+
|
|
87
|
+
Command authorization reads the principal from the current HTTP request. A
|
|
88
|
+
reactor, hosted service, or one command orchestrating another has no request, so
|
|
89
|
+
any command carrying `[Authorize]` or `[Roles]` is **denied**.
|
|
90
|
+
|
|
91
|
+
`ISystemExecution` is the way out:
|
|
92
|
+
|
|
93
|
+
```csharp
|
|
94
|
+
using var scope = systemExecution.AsSystem(nameof(<RoleEnum>.<Member>));
|
|
95
|
+
await pipeline.Execute(new <CommandName>(<args>));
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- `AsSystem(params string[] roles)` establishes an authenticated system actor for
|
|
99
|
+
the scope. With no roles it satisfies `[Authorize]` but no `[Roles]`.
|
|
100
|
+
- `As(ClaimsPrincipal principal)` runs as a specific principal.
|
|
101
|
+
- Disposing restores the previous context.
|
|
102
|
+
|
|
103
|
+
`SystemPrincipal.AuthenticationType` is `"System"` and `SystemPrincipal.Subject`
|
|
104
|
+
is `"[System]"`, which is what appears in audit trails.
|
|
105
|
+
|
|
106
|
+
The established principal is consulted **only** when there is no HTTP request
|
|
107
|
+
context, so it can never influence the authorization of an HTTP-origin command.
|
|
108
|
+
`CurrentPrincipalAccessor` implements that: a request principal always wins over
|
|
109
|
+
the ambient override.
|
|
110
|
+
|
|
111
|
+
Keep the scope as narrow as the work. A broad `AsSystem` around a whole hosted
|
|
112
|
+
service turns every command it touches into a privileged one.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Frontend identity
|
|
2
|
+
|
|
3
|
+
Verified against `@cratis/arc` and `@cratis/arc.react` `22.10.4`.
|
|
4
|
+
|
|
5
|
+
## `@cratis/arc/identity`
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
interface IIdentity<TDetails = object> {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
roles: string[];
|
|
12
|
+
details: TDetails;
|
|
13
|
+
isSet: boolean;
|
|
14
|
+
isInRole(role: string): boolean;
|
|
15
|
+
refresh(): Promise<IIdentity<TDetails>>;
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`IdentityProvider` is the framework-neutral implementation:
|
|
20
|
+
|
|
21
|
+
| Member | Notes |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `static readonly CookieName = '.cratis-identity'` | The cookie `/.cratis/me` writes |
|
|
24
|
+
| `static setHttpHeadersCallback(callback)` | Headers to send on the refresh call |
|
|
25
|
+
| `static setApiBasePath(path)` / `static setOrigin(origin)` | Where `/.cratis/me` lives |
|
|
26
|
+
| `static getCurrent<TDetails>(type?)` | Reads the **cookie**, no network call |
|
|
27
|
+
| `static refresh<TDetails>(type?)` | Fetches `<apiBasePath>/.cratis/me` |
|
|
28
|
+
| `static clearIdentityCookie()` | Expires the cookie |
|
|
29
|
+
|
|
30
|
+
`IIdentityProvider` is an abstract class with a single
|
|
31
|
+
`getCurrent<TDetails>(type?)`, which is what a view model injects.
|
|
32
|
+
|
|
33
|
+
`IdentityProviderResult` on the wire is
|
|
34
|
+
`{ id: string; name: string; roles: string[]; details: object }`.
|
|
35
|
+
|
|
36
|
+
`getCurrent` reads the cookie and never touches the network. That is what makes
|
|
37
|
+
it cheap, and also why nothing it returns is a security decision — the cookie is
|
|
38
|
+
deliberately not `HttpOnly`, so the browser can read *and write* it.
|
|
39
|
+
|
|
40
|
+
## `@cratis/arc.react/identity`
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
interface IIdentityContext<TDetails = object> extends IIdentity<TDetails> {
|
|
44
|
+
isLoading: boolean;
|
|
45
|
+
clearIdentity: () => void;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`IdentityProvider` is the React component. Props: `children`,
|
|
50
|
+
`httpHeadersCallback`, and `detailsType` (a constructor, for type-safe details).
|
|
51
|
+
It seeds `isLoading: true` and fetches on mount. The root `<Arc>` component takes
|
|
52
|
+
the same `detailsType` prop and renders this provider for you.
|
|
53
|
+
|
|
54
|
+
`useIdentity` has two overloads:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
useIdentity<TDetails>(type: Constructor<TDetails>, defaultDetails?): IIdentityContext<TDetails>
|
|
58
|
+
useIdentity<TDetails>(defaultDetails?): IIdentityContext<TDetails>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The default details stand in **whenever there are no details to give**, not only
|
|
62
|
+
when the identity is explicitly unset — an identity that resolved while carrying
|
|
63
|
+
no details still has `isSet` true, and without the stand-in the first property
|
|
64
|
+
access on the details throws and takes the page down.
|
|
65
|
+
|
|
66
|
+
`isLoading` distinguishes "the identity has not arrived yet" from "the caller is
|
|
67
|
+
anonymous". Treating the two alike makes a signed-in user flash the forbidden
|
|
68
|
+
state on every load.
|
|
69
|
+
|
|
70
|
+
## `RequireRole`
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
<RequireRole roles={[<role>]} whileLoading={<spinner/>} forbidden={<denied/>}>
|
|
74
|
+
{children}
|
|
75
|
+
</RequireRole>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Props are `RequireRoleSlots` (`children`, `whileLoading?`, `forbidden?`) crossed
|
|
79
|
+
with either `{ roles: string[]; allow? }` or `{ roles?; allow }`, where `allow` is
|
|
80
|
+
`(details: TDetails | undefined, identity: IIdentityContext<TDetails>) => boolean`.
|
|
81
|
+
When both are supplied, **both** must pass.
|
|
82
|
+
|
|
83
|
+
Everything that is not an unambiguous yes renders `forbidden`:
|
|
84
|
+
|
|
85
|
+
- neither `roles` nor `allow` supplied (it warns and denies — that is what a
|
|
86
|
+
renamed configuration key looks like);
|
|
87
|
+
- `roles` supplied but not an array;
|
|
88
|
+
- the identity is not set;
|
|
89
|
+
- no role matches, or the predicate does not return exactly `true`;
|
|
90
|
+
- the predicate throws;
|
|
91
|
+
- an `allow` predicate with absent or `null` details.
|
|
92
|
+
|
|
93
|
+
⚠️ **This hides UI; it does not protect data.** The identity comes from a cookie
|
|
94
|
+
the browser can edit, so anyone can render these children at will. Every query
|
|
95
|
+
and command behind the gate must carry its own `[Authorize]`/`[Roles]` on the
|
|
96
|
+
server, where the decision cannot be edited.
|
|
97
|
+
|
|
98
|
+
## MVVM
|
|
99
|
+
|
|
100
|
+
`@cratis/arc.react.mvvm` has **no** identity hook, provider, context, or type. It
|
|
101
|
+
registers `IIdentityProvider` → `IdentityProvider` in its container so a view
|
|
102
|
+
model can constructor-inject it. That is the whole integration.
|
package/skills/cratis-arc-authentication-authorization-and-identity/references/local-development.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Running with an identity locally
|
|
2
|
+
|
|
3
|
+
Verified against `Cratis.Arc.Core` and `Cratis.Arc` `22.10.4`.
|
|
4
|
+
|
|
5
|
+
## What Arc does not give you
|
|
6
|
+
|
|
7
|
+
There is **no** development identity provider, **no** fake or seeded principal,
|
|
8
|
+
**no** local sign-in page, and **no** development authentication handler anywhere
|
|
9
|
+
in Arc. `ArcOptions` has no switch that relaxes authentication. The only
|
|
10
|
+
"Development" type in the framework is `DevelopmentTenantIdResolver`, which is a
|
|
11
|
+
deprecated alias over the fixed tenant resolver and has nothing to do with
|
|
12
|
+
identity.
|
|
13
|
+
|
|
14
|
+
Guidance describing a `.cratis/.ai` local identity route is describing something
|
|
15
|
+
that does not exist.
|
|
16
|
+
|
|
17
|
+
## What Arc does give you
|
|
18
|
+
|
|
19
|
+
Two anonymous listing endpoints, both tagged for development in their metadata:
|
|
20
|
+
|
|
21
|
+
| Route | Returns |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `GET /.cratis/users` | Every `User` from every registered `ICanProvideUsers` |
|
|
24
|
+
| `GET /.cratis/tenants` | Every `Tenant` from every registered `ICanProvideTenants` |
|
|
25
|
+
|
|
26
|
+
```csharp
|
|
27
|
+
public record User(ClientPrincipal MicrosoftIdentity, object Details);
|
|
28
|
+
public interface ICanProvideUsers { Task<IEnumerable<User>> Provide(); }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
⚠️ Both are mapped `AllowAnonymous` and **unconditionally** — there is no
|
|
32
|
+
environment check in the mapper. Whatever your implementations return is public
|
|
33
|
+
to anyone who can reach the application. Register them only for a development
|
|
34
|
+
environment, and return fixtures rather than production users or tenants.
|
|
35
|
+
|
|
36
|
+
## Supplying an identity locally
|
|
37
|
+
|
|
38
|
+
Because the shipped handler reads forwarded headers, the way to run with an
|
|
39
|
+
identity locally is to send those headers yourself. All three are required:
|
|
40
|
+
|
|
41
|
+
- `x-ms-client-principal-id`
|
|
42
|
+
- `x-ms-client-principal-name`
|
|
43
|
+
- `x-ms-client-principal` — base64 of the `ClientPrincipal` JSON
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"identityProvider": "<provider>",
|
|
48
|
+
"userId": "<id>",
|
|
49
|
+
"userDetails": "<display name>",
|
|
50
|
+
"userRoles": ["<role>"],
|
|
51
|
+
"claims": [{ "typ": "<claim type>", "val": "<value>" }]
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Base64-encode that document and send it as `x-ms-client-principal`. `curl`
|
|
56
|
+
example shape:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
curl -H "x-ms-client-principal-id: <id>" \
|
|
60
|
+
-H "x-ms-client-principal-name: <name>" \
|
|
61
|
+
-H "x-ms-client-principal: <base64>" \
|
|
62
|
+
"https://<host>/.cratis/me"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`NameIdentifier` and `sub` come from the **id header**, not from `userId` in the
|
|
66
|
+
body, so the two must agree if anything downstream reads both.
|
|
67
|
+
|
|
68
|
+
## A browser cannot set headers on every transport
|
|
69
|
+
|
|
70
|
+
Server-Sent Events and WebSocket connections opened by the browser cannot carry
|
|
71
|
+
custom request headers. A local setup that authenticates purely by header will
|
|
72
|
+
therefore authenticate ordinary requests and not the observable-query streams.
|
|
73
|
+
|
|
74
|
+
Arc's own test applications solve this with a **cookie-based** development
|
|
75
|
+
handler carrying the same base64 `ClientPrincipal` under a cookie. That handler
|
|
76
|
+
lives in the test applications, not in any shipped package — if you need it, it
|
|
77
|
+
is an `IAuthenticationHandler` you write in your own application and register
|
|
78
|
+
only for development.
|
|
79
|
+
|
|
80
|
+
## Do not let development shortcuts become the deployment
|
|
81
|
+
|
|
82
|
+
Any local mechanism that lets the caller author its own principal is exactly the
|
|
83
|
+
production risk described in [authentication](authentication.md). Keep it behind
|
|
84
|
+
an environment check, and make sure the deployed application is unreachable
|
|
85
|
+
except through the ingress that owns those headers.
|