@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.
Files changed (125) hide show
  1. package/README.md +43 -0
  2. package/package.json +24 -0
  3. package/skills/cratis-application-react-specifications/LICENSE +2 -0
  4. package/skills/cratis-application-react-specifications/SKILL.md +192 -0
  5. package/skills/cratis-application-slice-specifications/LICENSE +2 -0
  6. package/skills/cratis-application-slice-specifications/SKILL.md +222 -0
  7. package/skills/cratis-arc-authentication-authorization-and-identity/LICENSE +2 -0
  8. package/skills/cratis-arc-authentication-authorization-and-identity/SKILL.md +233 -0
  9. package/skills/cratis-arc-authentication-authorization-and-identity/references/authentication.md +140 -0
  10. package/skills/cratis-arc-authentication-authorization-and-identity/references/authorization.md +112 -0
  11. package/skills/cratis-arc-authentication-authorization-and-identity/references/frontend.md +102 -0
  12. package/skills/cratis-arc-authentication-authorization-and-identity/references/local-development.md +85 -0
  13. package/skills/cratis-arc-authentication-authorization-and-identity/references/tenancy.md +87 -0
  14. package/skills/cratis-arc-command/LICENSE +2 -0
  15. package/skills/cratis-arc-command/SKILL.md +286 -0
  16. package/skills/cratis-arc-command/references/command-result.md +111 -0
  17. package/skills/cratis-arc-command/references/handler-shapes.md +135 -0
  18. package/skills/cratis-arc-command/references/proxy-generation.md +87 -0
  19. package/skills/cratis-arc-command/references/read-model-injection.md +97 -0
  20. package/skills/cratis-arc-command-execution/LICENSE +2 -0
  21. package/skills/cratis-arc-command-execution/SKILL.md +148 -0
  22. package/skills/cratis-arc-command-validation/LICENSE +2 -0
  23. package/skills/cratis-arc-command-validation/SKILL.md +230 -0
  24. package/skills/cratis-arc-ef-core-migration/LICENSE +2 -0
  25. package/skills/cratis-arc-ef-core-migration/SKILL.md +231 -0
  26. package/skills/cratis-arc-observable-query-http/LICENSE +2 -0
  27. package/skills/cratis-arc-observable-query-http/SKILL.md +157 -0
  28. package/skills/cratis-arc-query-paging/LICENSE +2 -0
  29. package/skills/cratis-arc-query-paging/SKILL.md +173 -0
  30. package/skills/cratis-arc-react-page/LICENSE +2 -0
  31. package/skills/cratis-arc-react-page/SKILL.md +291 -0
  32. package/skills/cratis-arc-react-page/references/data-page.md +125 -0
  33. package/skills/cratis-arc-react-page/references/data-tables.md +136 -0
  34. package/skills/cratis-arc-react-page/references/dialogs.md +238 -0
  35. package/skills/cratis-arc-react-page/references/mvvm.md +150 -0
  36. package/skills/cratis-arc-react-page/references/queries-and-commands.md +200 -0
  37. package/skills/cratis-chronicle-cli-operations/LICENSE +2 -0
  38. package/skills/cratis-chronicle-cli-operations/SKILL.md +199 -0
  39. package/skills/cratis-chronicle-client-dotnet/LICENSE +2 -0
  40. package/skills/cratis-chronicle-client-dotnet/SKILL.md +433 -0
  41. package/skills/cratis-chronicle-client-elixir/LICENSE +2 -0
  42. package/skills/cratis-chronicle-client-elixir/SKILL.md +429 -0
  43. package/skills/cratis-chronicle-client-kotlin/LICENSE +2 -0
  44. package/skills/cratis-chronicle-client-kotlin/SKILL.md +498 -0
  45. package/skills/cratis-chronicle-client-typescript/LICENSE +2 -0
  46. package/skills/cratis-chronicle-client-typescript/SKILL.md +396 -0
  47. package/skills/cratis-chronicle-compliance/LICENSE +2 -0
  48. package/skills/cratis-chronicle-compliance/SKILL.md +246 -0
  49. package/skills/cratis-chronicle-event-constraints/LICENSE +2 -0
  50. package/skills/cratis-chronicle-event-constraints/SKILL.md +227 -0
  51. package/skills/cratis-chronicle-event-modeling/LICENSE +2 -0
  52. package/skills/cratis-chronicle-event-modeling/SKILL.md +157 -0
  53. package/skills/cratis-chronicle-event-specifications/LICENSE +2 -0
  54. package/skills/cratis-chronicle-event-specifications/SKILL.md +200 -0
  55. package/skills/cratis-chronicle-event-type-migration/LICENSE +2 -0
  56. package/skills/cratis-chronicle-event-type-migration/SKILL.md +235 -0
  57. package/skills/cratis-chronicle-mcp-inspection/LICENSE +2 -0
  58. package/skills/cratis-chronicle-mcp-inspection/SKILL.md +79 -0
  59. package/skills/cratis-chronicle-mcp-inspection/references/blocked-tools.md +12 -0
  60. package/skills/cratis-chronicle-mcp-inspection/references/observational-tools.md +8 -0
  61. package/skills/cratis-chronicle-multi-tenancy/LICENSE +2 -0
  62. package/skills/cratis-chronicle-multi-tenancy/SKILL.md +198 -0
  63. package/skills/cratis-chronicle-projection/LICENSE +2 -0
  64. package/skills/cratis-chronicle-projection/SKILL.md +162 -0
  65. package/skills/cratis-chronicle-projection/references/fluent-builder.md +126 -0
  66. package/skills/cratis-chronicle-projection/references/model-bound-attributes.md +118 -0
  67. package/skills/cratis-chronicle-reactor/LICENSE +2 -0
  68. package/skills/cratis-chronicle-reactor/SKILL.md +344 -0
  69. package/skills/cratis-chronicle-read-model/LICENSE +2 -0
  70. package/skills/cratis-chronicle-read-model/SKILL.md +193 -0
  71. package/skills/cratis-chronicle-read-model/references/queries.md +142 -0
  72. package/skills/cratis-chronicle-read-model-specifications/LICENSE +2 -0
  73. package/skills/cratis-chronicle-read-model-specifications/SKILL.md +176 -0
  74. package/skills/cratis-chronicle-reducer/LICENSE +2 -0
  75. package/skills/cratis-chronicle-reducer/SKILL.md +173 -0
  76. package/skills/cratis-chronicle-web-workbench/LICENSE +2 -0
  77. package/skills/cratis-chronicle-web-workbench/SKILL.md +177 -0
  78. package/skills/cratis-cli-terminal-workbench/LICENSE +2 -0
  79. package/skills/cratis-cli-terminal-workbench/SKILL.md +156 -0
  80. package/skills/cratis-code-review/LICENSE +2 -0
  81. package/skills/cratis-code-review/SKILL.md +177 -0
  82. package/skills/cratis-components-accessibility/LICENSE +2 -0
  83. package/skills/cratis-components-accessibility/SKILL.md +176 -0
  84. package/skills/cratis-components-schema-editor/LICENSE +2 -0
  85. package/skills/cratis-components-schema-editor/SKILL.md +238 -0
  86. package/skills/cratis-components-stepper-command-dialog/LICENSE +2 -0
  87. package/skills/cratis-components-stepper-command-dialog/SKILL.md +209 -0
  88. package/skills/cratis-components-styling/LICENSE +2 -0
  89. package/skills/cratis-components-styling/SKILL.md +221 -0
  90. package/skills/cratis-components-toolbar/LICENSE +2 -0
  91. package/skills/cratis-components-toolbar/SKILL.md +259 -0
  92. package/skills/cratis-documentation-writing/LICENSE +2 -0
  93. package/skills/cratis-documentation-writing/SKILL.md +121 -0
  94. package/skills/cratis-event-model-diagram/LICENSE +2 -0
  95. package/skills/cratis-event-model-diagram/SKILL.md +142 -0
  96. package/skills/cratis-fundamentals-concept/LICENSE +2 -0
  97. package/skills/cratis-fundamentals-concept/SKILL.md +198 -0
  98. package/skills/cratis-fundamentals-type-discovery/LICENSE +2 -0
  99. package/skills/cratis-fundamentals-type-discovery/SKILL.md +152 -0
  100. package/skills/cratis-governed-release-methodology/LICENSE +2 -0
  101. package/skills/cratis-governed-release-methodology/SKILL.md +297 -0
  102. package/skills/cratis-lens-browser-extension/LICENSE +2 -0
  103. package/skills/cratis-lens-browser-extension/SKILL.md +239 -0
  104. package/skills/cratis-performance-review/LICENSE +2 -0
  105. package/skills/cratis-performance-review/SKILL.md +139 -0
  106. package/skills/cratis-screenplay-model-authoring/LICENSE +2 -0
  107. package/skills/cratis-screenplay-model-authoring/SKILL.md +231 -0
  108. package/skills/cratis-security-review/LICENSE +2 -0
  109. package/skills/cratis-security-review/SKILL.md +141 -0
  110. package/skills/cratis-specification-by-example/LICENSE +2 -0
  111. package/skills/cratis-specification-by-example/SKILL.md +188 -0
  112. package/skills/cratis-specifications-csharp/LICENSE +2 -0
  113. package/skills/cratis-specifications-csharp/SKILL.md +230 -0
  114. package/skills/cratis-specifications-csharp/references/application-scenarios.md +191 -0
  115. package/skills/cratis-specifications-csharp/references/csharp-patterns.md +142 -0
  116. package/skills/cratis-specifications-csharp/references/integration-specs.md +126 -0
  117. package/skills/cratis-specifications-typescript/LICENSE +2 -0
  118. package/skills/cratis-specifications-typescript/SKILL.md +202 -0
  119. package/skills/cratis-specifications-typescript/references/typescript-patterns.md +168 -0
  120. package/skills/cratis-stage-rendering-and-sandbox/LICENSE +2 -0
  121. package/skills/cratis-stage-rendering-and-sandbox/SKILL.md +230 -0
  122. package/skills/cratis-studio-mcp-safety-guidance/LICENSE +2 -0
  123. package/skills/cratis-studio-mcp-safety-guidance/SKILL.md +69 -0
  124. package/skills/cratis-studio-mcp-safety-guidance/references/blocked-tools.md +12 -0
  125. 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.
@@ -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.
@@ -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.
@@ -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.