@tideorg/mcp 1.4.2 → 1.9.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/CHANGELOG.md +88 -0
- package/GAP_REGISTER.md +18 -8
- package/PRIVACY.md +41 -0
- package/README.md +207 -197
- package/adapters/AGENTS.md +3 -2
- package/adapters/CLAUDE.md +1 -1
- package/canon/anti-patterns.md +55 -62
- package/canon/concepts.md +46 -34
- package/canon/custom-contracts.md +12 -8
- package/canon/feature-mapping.md +27 -75
- package/canon/framework-matrix.md +69 -22
- package/canon/hosting-options.md +111 -0
- package/canon/iga-change-requests-api.md +211 -0
- package/canon/invariants.md +33 -35
- package/canon/security-gap-mapping.md +506 -0
- package/canon/security-runtime-probes.md +177 -0
- package/canon/tidecloak-bootstrap.md +21 -16
- package/canon/tidecloak-endpoints.md +60 -98
- package/canon/troubleshooting.md +115 -53
- package/canon/version-policy.md +8 -12
- package/mcp-server/dist/server.js +221 -19
- package/package.json +48 -45
- package/playbooks/add-auth-nextjs-existing.md +33 -17
- package/playbooks/add-auth-nextjs-fresh.md +1 -1
- package/playbooks/add-rbac-nextjs.md +7 -2
- package/playbooks/bootstrap-realm-from-template.md +33 -18
- package/playbooks/deploy-tidecloak-docker.md +31 -28
- package/playbooks/diagnose-missing-roles-or-claims.md +2 -2
- package/playbooks/initialize-admin-and-link-account.md +22 -19
- package/playbooks/protect-api-nextjs.md +40 -1
- package/playbooks/protect-aspnet-core-asgard.md +313 -0
- package/playbooks/protect-routes-nextjs.md +24 -10
- package/playbooks/provision-tidecloak-skycloak.md +213 -0
- package/playbooks/setup-forseti-e2ee.md +32 -50
- package/playbooks/setup-iga-admin-panel.md +113 -171
- package/playbooks/verify-jwt-server-side.md +20 -1
- package/prompts/build-private-customer-portal.md +1 -1
- package/prompts/security-gap-analysis.md +55 -0
- package/reference-apps/encrypted-communication/anti-patterns.md +3 -3
- package/reference-apps/encrypted-communication/bootstrap-sequence.md +2 -2
- package/reference-apps/encrypted-communication/scenario.md +2 -2
- package/reference-apps/git-pr-signing-service/bootstrap-sequence.md +8 -8
- package/reference-apps/iga-admin-governance/anti-patterns.md +18 -16
- package/reference-apps/iga-admin-governance/bootstrap-sequence.md +10 -5
- package/reference-apps/iga-admin-governance/role-policy-matrix.md +12 -13
- package/reference-apps/iga-admin-governance/scenario.md +15 -13
- package/reference-apps/organisation-password-manager/bootstrap-sequence.md +5 -6
- package/reference-apps/policy-governed-signing/bootstrap-sequence.md +9 -9
- package/skills/tide-diagnostics/SKILL.md +1 -1
- package/skills/tide-integration/SKILL.md +9 -18
- package/skills/tide-mcp-qa/SKILL.md +139 -0
- package/skills/tide-rbac-and-e2ee/SKILL.md +5 -5
- package/skills/tide-reviewer/SKILL.md +1 -1
- package/skills/tide-security-analyst/SKILL.md +182 -0
- package/skills/tide-setup/SKILL.md +1 -1
- package/canon/delegation.md +0 -195
- package/playbooks/setup-server-delegation.md +0 -142
|
@@ -79,6 +79,14 @@ This activates the Tide license and generates the VRK. No manual "Manage License
|
|
|
79
79
|
|
|
80
80
|
**Critical:** Content-Type must be `application/x-www-form-urlencoded`. JSON causes failure.
|
|
81
81
|
|
|
82
|
+
**Form parameters** (`setUpTideRealm` on `main`):
|
|
83
|
+
|
|
84
|
+
| Param | Required | Default | Notes |
|
|
85
|
+
|-------|----------|---------|-------|
|
|
86
|
+
| `email` | Yes (for licensing) | — | Used to request the free-tier license. Ignored only when `skipLicense=true`. |
|
|
87
|
+
| `isRagnarokEnabled` | No | `true` | Enables realm offboarding (Ragnarok). The endpoint already defaults it to `true`; passing `isRagnarokEnabled=true` explicitly is harmless and matches the default. |
|
|
88
|
+
| `skipLicense` | No | `false` | When `true`, skips license activation (`email` unused) and returns early. Leave at the default for normal bootstrap. |
|
|
89
|
+
|
|
82
90
|
### Step 5: Enable IGA
|
|
83
91
|
|
|
84
92
|
```bash
|
|
@@ -94,28 +102,35 @@ curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/tide-admin/toggle-iga"
|
|
|
94
102
|
Realm creation + Tide setup generates change requests that must be approved before proceeding.
|
|
95
103
|
|
|
96
104
|
```bash
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
# Current /iga/change-requests/... surface (replaces legacy /tide-admin/change-set/...).
|
|
106
|
+
# Bootstrap runs in FirstAdmin/Tideless mode, so authorize signs server-side (no enclave).
|
|
107
|
+
# Full spec: canon/iga-change-requests-api.md.
|
|
108
|
+
approve_all_pending() {
|
|
109
|
+
local TOKEN ready id
|
|
102
110
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
# 1. Authorize all pending CREATE/DELETE change requests in one call.
|
|
112
|
+
TOKEN="$(get_token)"
|
|
113
|
+
curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/iga/change-requests/bulk-authorize" \
|
|
114
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
115
|
+
-H "Content-Type: application/json" \
|
|
116
|
+
-d '{"actionTypeIn":["CREATE","DELETE"],"limit":100}' > /dev/null 2>&1
|
|
108
117
|
|
|
118
|
+
# 2. Commit ready CRs; loop passes so dependent CRs become ready.
|
|
119
|
+
for pass in 1 2 3 4 5; do
|
|
109
120
|
TOKEN="$(get_token)"
|
|
110
|
-
curl -s
|
|
111
|
-
-H "Authorization: Bearer $TOKEN" \
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
-
|
|
121
|
+
ready=$(curl -s "$TIDECLOAK_URL/admin/realms/$REALM_NAME/iga/change-requests?status=PENDING" \
|
|
122
|
+
-H "Authorization: Bearer $TOKEN" 2>/dev/null \
|
|
123
|
+
| jq -r '.[] | select(.readyToCommit==true) | .id' 2>/dev/null)
|
|
124
|
+
[ -z "$ready" ] && break
|
|
125
|
+
for id in $ready; do
|
|
126
|
+
TOKEN="$(get_token)"
|
|
127
|
+
curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/iga/change-requests/$id/commit" \
|
|
128
|
+
-H "Authorization: Bearer $TOKEN" > /dev/null 2>&1
|
|
129
|
+
done
|
|
117
130
|
done
|
|
118
131
|
}
|
|
132
|
+
# Legacy call sites pass a type arg the new list-all surface ignores.
|
|
133
|
+
approve_and_commit() { approve_all_pending; }
|
|
119
134
|
|
|
120
135
|
approve_and_commit clients
|
|
121
136
|
```
|
|
@@ -135,7 +150,7 @@ approve_and_commit clients
|
|
|
135
150
|
|
|
136
151
|
| Symptom | Cause | Fix |
|
|
137
152
|
|---------|-------|-----|
|
|
138
|
-
| "Could not
|
|
153
|
+
| "Tide realm setup failed" / "Could not set up the Tide realm" | JSON sent to `setUpTideRealm` instead of form data | Use `application/x-www-form-urlencoded` |
|
|
139
154
|
| "email is null or empty" | Missing email parameter | Add `--data-urlencode "email=..."` |
|
|
140
155
|
| 404 on setUpTideRealm | Wrong path (`/tide-admin/setUpTideRealm`) | Correct path: `/vendorResources/setUpTideRealm` |
|
|
141
156
|
| Realm import fails | Placeholders not replaced in template | Verify `sed` replaced all three: `REALM_NAME`, `CLIENT_NAME`, `CLIENT_APP_URL` |
|
|
@@ -372,34 +372,38 @@ curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/tide-admin/toggle-iga"
|
|
|
372
372
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
373
373
|
--data-urlencode "isIGAEnabled=true"
|
|
374
374
|
|
|
375
|
-
# 5. Auto-approve
|
|
376
|
-
|
|
377
|
-
|
|
375
|
+
# 5. Auto-approve change requests created during realm setup.
|
|
376
|
+
# Uses the current /iga/change-requests/... surface (replaces the legacy
|
|
377
|
+
# /tide-admin/change-set/... surface). During bootstrap the realm is in
|
|
378
|
+
# FirstAdmin/Tideless mode, so authorize signs server-side (no enclave).
|
|
379
|
+
# See canon/iga-change-requests-api.md.
|
|
380
|
+
approve_all_pending() {
|
|
381
|
+
local TOKEN ready id
|
|
382
|
+
|
|
383
|
+
# 1. Authorize every pending CREATE/DELETE change request in one call.
|
|
378
384
|
TOKEN="$(get_token)"
|
|
379
|
-
|
|
380
|
-
"$
|
|
381
|
-
-H "
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
--arg at "$(echo "$req" | jq -r .actionType)" \
|
|
388
|
-
'{changeSetId:$id,changeSetType:$cst,actionType:$at}')
|
|
389
|
-
|
|
390
|
-
# Sign then commit
|
|
385
|
+
curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/iga/change-requests/bulk-authorize" \
|
|
386
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
387
|
+
-H "Content-Type: application/json" \
|
|
388
|
+
-d '{"actionTypeIn":["CREATE","DELETE"],"limit":100}' > /dev/null 2>&1
|
|
389
|
+
|
|
390
|
+
# 2. Commit everything that is ready. Loop passes so dependent CRs
|
|
391
|
+
# (e.g. a role must exist before its assignment) become ready and commit.
|
|
392
|
+
for pass in 1 2 3 4 5; do
|
|
391
393
|
TOKEN="$(get_token)"
|
|
392
|
-
curl -s
|
|
393
|
-
-H "Authorization: Bearer $TOKEN" \
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
-
|
|
394
|
+
ready=$(curl -s "$TIDECLOAK_URL/admin/realms/$REALM_NAME/iga/change-requests?status=PENDING" \
|
|
395
|
+
-H "Authorization: Bearer $TOKEN" 2>/dev/null \
|
|
396
|
+
| jq -r '.[] | select(.readyToCommit==true) | .id' 2>/dev/null)
|
|
397
|
+
[ -z "$ready" ] && break
|
|
398
|
+
for id in $ready; do
|
|
399
|
+
TOKEN="$(get_token)"
|
|
400
|
+
curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/iga/change-requests/$id/commit" \
|
|
401
|
+
-H "Authorization: Bearer $TOKEN" > /dev/null 2>&1
|
|
402
|
+
done
|
|
399
403
|
done
|
|
400
404
|
}
|
|
401
405
|
|
|
402
|
-
|
|
406
|
+
approve_all_pending
|
|
403
407
|
|
|
404
408
|
# 6. Create admin user
|
|
405
409
|
TOKEN="$(get_token)"
|
|
@@ -410,7 +414,7 @@ curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/users" \
|
|
|
410
414
|
|
|
411
415
|
# 6b. Approve user creation (user is not queryable until committed with IGA enabled)
|
|
412
416
|
sleep 2
|
|
413
|
-
|
|
417
|
+
approve_all_pending
|
|
414
418
|
|
|
415
419
|
# 6c. Assign tide-realm-admin role
|
|
416
420
|
TOKEN="$(get_token)"
|
|
@@ -461,8 +465,7 @@ done
|
|
|
461
465
|
|
|
462
466
|
# 9. Approve role assignment change requests
|
|
463
467
|
sleep 2
|
|
464
|
-
|
|
465
|
-
approve_and_commit roles
|
|
468
|
+
approve_all_pending
|
|
466
469
|
|
|
467
470
|
# 10. Update CustomAdminUIDomain for enclave approval popups
|
|
468
471
|
# CustomAdminUIDomain is OPTIONAL and APP-SPECIFIC: only needed when a separate
|
|
@@ -539,8 +542,8 @@ curl -s -X GET "$TIDECLOAK_URL/admin/realms/$REALM_NAME/vendorResources/get-inst
|
|
|
539
542
|
| `POST /admin/realms` | Create realm from JSON template |
|
|
540
543
|
| `POST /admin/realms/{realm}/vendorResources/setUpTideRealm` | Initialize Tide licensing + VRK |
|
|
541
544
|
| `POST /admin/realms/{realm}/tide-admin/toggle-iga` | Enable IGA governance |
|
|
542
|
-
| `POST /admin/realms/{realm}/
|
|
543
|
-
| `POST /admin/realms/{realm}/
|
|
545
|
+
| `POST /admin/realms/{realm}/iga/change-requests/{id}/authorize` | Approve (sign) a change request. Batch: `POST /iga/change-requests/bulk-authorize`. Replaces legacy `tide-admin/change-set/sign`. |
|
|
546
|
+
| `POST /admin/realms/{realm}/iga/change-requests/{id}/commit` | Commit an approved change. Replaces legacy `tide-admin/change-set/commit`. See `canon/iga-change-requests-api.md`. |
|
|
544
547
|
| `POST /admin/realms/{realm}/tideAdminResources/get-required-action-link` | Generate admin invite link |
|
|
545
548
|
| `GET /admin/realms/{realm}/vendorResources/get-installations-provider` | Download adapter config |
|
|
546
549
|
| `POST /admin/realms/{realm}/vendorResources/sign-idp-settings` | Sign IdP settings with VRK. **ALWAYS required** after any Tide IDP config change (logo, background, backup toggle, custom admin domain, or client origin changes). Without it, the enclave rejects settings as unsigned. Requires active VRK/license. |
|
|
@@ -93,7 +93,7 @@ function hasAnyRole(user: any, role: string): boolean {
|
|
|
93
93
|
|
|
94
94
|
1. TideCloak Admin → Users → {user} → Role Mappings
|
|
95
95
|
2. Assign role → {role-name}
|
|
96
|
-
3. If IGA enabled:
|
|
96
|
+
3. If IGA enabled: authorize + commit the change request (requires quorum; see `canon/iga-change-requests-api.md`)
|
|
97
97
|
4. User logout/login to get new token
|
|
98
98
|
|
|
99
99
|
**Fix (default roles)**:
|
|
@@ -189,7 +189,7 @@ async function forceTokenRefresh() {
|
|
|
189
189
|
|
|
190
190
|
## Symptom: E2EE Role Not Working
|
|
191
191
|
|
|
192
|
-
**What you see**: `doEncrypt('tag'
|
|
192
|
+
**What you see**: `doEncrypt([{ data, tags: ['tag'] }])` fails even though user has role.
|
|
193
193
|
|
|
194
194
|
**Diagnostic**:
|
|
195
195
|
|
|
@@ -129,29 +129,32 @@ done
|
|
|
129
129
|
### Step 5: Approve user change requests
|
|
130
130
|
|
|
131
131
|
```bash
|
|
132
|
-
# Reuse
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
local
|
|
137
|
-
-H "Authorization: Bearer $TOKEN" 2>/dev/null || echo "[]")
|
|
132
|
+
# Reuse approve_all_pending from bootstrap-realm-from-template.
|
|
133
|
+
# Current /iga/change-requests/... surface (replaces legacy /tide-admin/change-set/...).
|
|
134
|
+
# Bootstrap = FirstAdmin/Tideless: authorize signs server-side. Spec: canon/iga-change-requests-api.md.
|
|
135
|
+
approve_all_pending() {
|
|
136
|
+
local TOKEN ready id
|
|
138
137
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
138
|
+
TOKEN="$(get_token)"
|
|
139
|
+
curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/iga/change-requests/bulk-authorize" \
|
|
140
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
141
|
+
-H "Content-Type: application/json" \
|
|
142
|
+
-d '{"actionTypeIn":["CREATE","DELETE"],"limit":100}' > /dev/null 2>&1
|
|
144
143
|
|
|
144
|
+
for pass in 1 2 3 4 5; do
|
|
145
145
|
TOKEN="$(get_token)"
|
|
146
|
-
curl -s
|
|
147
|
-
-H "Authorization: Bearer $TOKEN" \
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
-
|
|
146
|
+
ready=$(curl -s "$TIDECLOAK_URL/admin/realms/$REALM_NAME/iga/change-requests?status=PENDING" \
|
|
147
|
+
-H "Authorization: Bearer $TOKEN" 2>/dev/null \
|
|
148
|
+
| jq -r '.[] | select(.readyToCommit==true) | .id' 2>/dev/null)
|
|
149
|
+
[ -z "$ready" ] && break
|
|
150
|
+
for id in $ready; do
|
|
151
|
+
TOKEN="$(get_token)"
|
|
152
|
+
curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/iga/change-requests/$id/commit" \
|
|
153
|
+
-H "Authorization: Bearer $TOKEN" > /dev/null 2>&1
|
|
154
|
+
done
|
|
153
155
|
done
|
|
154
156
|
}
|
|
157
|
+
approve_and_commit() { approve_all_pending; }
|
|
155
158
|
|
|
156
159
|
approve_and_commit users
|
|
157
160
|
approve_and_commit roles
|
|
@@ -216,7 +219,7 @@ node -e "const c=require('$ADAPTER_OUTPUT'); console.log('jwk:', !!c.jwk, 'vendo
|
|
|
216
219
|
| "Could not find configuration for Required Action" | Tide env vars missing from container | Restart container with all required env vars |
|
|
217
220
|
| `tideUserKey` never appears | Admin didn't complete the linking flow | Open invite link in browser, complete the flow |
|
|
218
221
|
| Enclave popups don't work from app | CustomAdminUIDomain not set | Run Step 6 |
|
|
219
|
-
| Adapter JSON missing `jwk` |
|
|
222
|
+
| Adapter JSON missing `jwk` | `setUpTideRealm` did not run / no `tide-vendor-key` component + EdDSA key on the realm (the `jwk` is gated on those, not on IGA) | Re-run `setUpTideRealm` from `bootstrap-realm-from-template` Step 4 and confirm it returned 200 |
|
|
220
223
|
|
|
221
224
|
---
|
|
222
225
|
|
|
@@ -76,7 +76,46 @@ Any FAIL means the API route is not properly protected. Use the implementation b
|
|
|
76
76
|
|
|
77
77
|
---
|
|
78
78
|
|
|
79
|
-
##
|
|
79
|
+
## Recommended: Use the Shipped `verifyTideCloakToken` Helper
|
|
80
|
+
|
|
81
|
+
Tide ships a server-side verification helper — you do not have to hand-roll JWKS verification. `@tidecloak/nextjs/server` re-exports `verifyTideCloakToken` from `@tidecloak/verify`:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// app/api/users/route.ts
|
|
85
|
+
import { NextRequest } from 'next/server';
|
|
86
|
+
import { verifyTideCloakToken } from '@tidecloak/nextjs/server';
|
|
87
|
+
import tcConfig from '../../../data/tidecloak.json';
|
|
88
|
+
|
|
89
|
+
export async function GET(req: NextRequest) {
|
|
90
|
+
const authHeader = req.headers.get('authorization');
|
|
91
|
+
if (!authHeader) {
|
|
92
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
93
|
+
}
|
|
94
|
+
const token = authHeader.replace(/^(Bearer|DPoP) /, '');
|
|
95
|
+
|
|
96
|
+
// Signature (arg order): verifyTideCloakToken(config, token, allowedRoles=[])
|
|
97
|
+
// config is FIRST. Pass roles to enforce them; [] to just verify the token.
|
|
98
|
+
const payload = await verifyTideCloakToken(tcConfig, token, ['admin']);
|
|
99
|
+
if (!payload) {
|
|
100
|
+
// Returns false on any failure (bad signature, issuer, azp, expiry, or role).
|
|
101
|
+
return Response.json({ error: 'Forbidden' }, { status: 403 });
|
|
102
|
+
}
|
|
103
|
+
return Response.json({ users: [...] });
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`verifyTideCloakToken`:
|
|
108
|
+
- verifies the signature against the adapter's embedded `jwk` (local JWKS — no network call),
|
|
109
|
+
- validates the issuer (`auth-server-url` + `/realms/<realm>`) and `azp` (against `config.resource`),
|
|
110
|
+
- checks time claims with a small `clockTolerance`,
|
|
111
|
+
- optionally enforces `allowedRoles` (matches across `realm_access.roles` and `resource_access[client].roles`),
|
|
112
|
+
- returns the decoded payload on success, `false` on failure.
|
|
113
|
+
|
|
114
|
+
Prefer this for standard API protection. The manual `jose` implementation below is the **under-the-hood alternative** — use it when you need custom behavior the helper does not cover (e.g. app-side DPoP proof re-verification, separate 401-vs-403 semantics, or `iat` future-validation). See [verify-jwt-server-side.md](verify-jwt-server-side.md).
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Quick Implementation — Manual (App Router)
|
|
80
119
|
|
|
81
120
|
This section creates minimal versions of `lib/auth/tidecloakConfig.ts`, `lib/auth/tideJWT.ts`, and `lib/auth/protect.ts`. If you also follow [verify-jwt-server-side.md](verify-jwt-server-side.md), its richer versions **replace** these files (adds DPoP verification, client-role checking, `extractToken`, and `iat` validation).
|
|
82
121
|
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# Protect an ASP.NET Core API with TideCloak (via Tide.Asgard.AspNetCore)
|
|
2
|
+
|
|
3
|
+
Wire TideCloak authentication into an ASP.NET Core resource server using the **Tide.Asgard.AspNetCore** SDK. Validates Tidecloak-issued EdDSA JWTs locally and performs RFC 8693 OAuth Token Exchange when your API needs to call another protected service.
|
|
4
|
+
|
|
5
|
+
This playbook supersedes the stock `Microsoft.AspNetCore.Authentication.OpenIdConnect` approach floated in PUC-092 / PUC-093. The asgard SDK ships Tide-specific helpers (`Utils.GetEd25519IssuerKey`, `AddTokenExchange`, EdDSA `SignatureProvider`) that remove most of the manual JWKS wiring the OIDC path required.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
> ## Current Status (temporary — 2026-05-14)
|
|
10
|
+
>
|
|
11
|
+
> The asgard .NET SDK only works against TideCloak realms where **IGA is OFF**. This is a hard constraint at present, not a recommendation.
|
|
12
|
+
>
|
|
13
|
+
> Implications:
|
|
14
|
+
> - Use the **staging** TideCloak Docker image: `tideorg/tidecloak-stg-dev`. The non-staging dev image does not give you the auth flow the asgard SDK targets.
|
|
15
|
+
> - Run the realm in **BYOiD-only mode** (license applied, IGA not toggled on). This is documented as a valid deployment mode in [canon/concepts.md](../canon/concepts.md) — "BYOiD-only (license + no IGA)".
|
|
16
|
+
> - **No `tide-realm-admin` role assignment is needed** for the admin user. That role is part of the IGA quorum machinery and only relevant when IGA is on. With IGA off, normal Keycloak admin permissions are sufficient.
|
|
17
|
+
> - **No `toggle-iga` call** during realm setup. Stop the canonical bootstrap order (license → IGA → E2EE) at the licence step.
|
|
18
|
+
> - E2EE is not available in this mode (E2EE requires IGA). Asgard .NET today is auth + token-exchange only, so this matches the SDK's surface.
|
|
19
|
+
>
|
|
20
|
+
> When the SDK adds IGA support, this section will be removed and the prerequisites updated. Tracking note: [notes/asgard-dotnet-current-status.md](../notes/asgard-dotnet-current-status.md).
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## When to Use
|
|
25
|
+
|
|
26
|
+
- You have an ASP.NET Core (.NET 10) API that needs to validate JWTs issued by Tidecloak.
|
|
27
|
+
- Your front end is a Tide-aware SPA (uses `@tidecloak/js`) and your backend is C#.
|
|
28
|
+
- You need server-to-server token exchange (RFC 8693) — e.g. your API calls a downstream protected service on behalf of the user.
|
|
29
|
+
|
|
30
|
+
## When NOT to Use
|
|
31
|
+
|
|
32
|
+
- Your stack is Next.js / React / Vanilla JS — use [protect-api-nextjs.md](protect-api-nextjs.md) / [verify-jwt-server-side.md](verify-jwt-server-side.md) instead.
|
|
33
|
+
- You need browser-side **DPoP**: the asgard `.WithDPoP(...)` registration is commented out at HEAD (see [Common Failures](#common-failures)).
|
|
34
|
+
- You need to **build, sign, or test Tide Policies / contracts** server-side from .NET. The asgard .NET SDK does not include the TS `asgard-tide` wire-format library; policy signing today lives in the JS stack (`heimdall-tide` + `@tidecloak/js`).
|
|
35
|
+
|
|
36
|
+
## Status classifications
|
|
37
|
+
|
|
38
|
+
- `Tide.Asgard.AspNetCore.Authentication` v0.1.0 — **VERIFIED** against `sources/asgard-sdk/aspnet/Tide.Asgard.AspNetCore/Tide.Asgard.AspNetCore/`.
|
|
39
|
+
- `Tide.Asgard.Core` v0.1.0 — **VERIFIED**. Provides EdDSA `SignatureProvider` over `Microsoft.IdentityModel.Tokens 8.15.0` and `BouncyCastle.Cryptography 2.6.2`.
|
|
40
|
+
- `Tide.Asgard.AspNetCore.DPoP` — **OBSERVED_PATTERN**. Built and ProjectReferenced by the Example, but no `<PackageId>` is set and the registration helper `.WithDPoP(...)` is commented out. Validation primitives exist; usage requires manual service registration.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Prerequisites
|
|
45
|
+
|
|
46
|
+
- **.NET 10 SDK** (preview-tier). Every csproj pins `<TargetFramework>net10.0</TargetFramework>`.
|
|
47
|
+
- TideCloak running using the **staging** Docker image (`tideorg/tidecloak-stg-dev`) — see Current Status above and [deploy-tidecloak-docker.md](deploy-tidecloak-docker.md).
|
|
48
|
+
- A realm licensed via `setUpTideRealm`, with **IGA NOT toggled on** (BYOiD-only mode). Do NOT run `toggle-iga` for an asgard-.NET-backed app today.
|
|
49
|
+
- An admin user able to manage the realm. **No `tide-realm-admin` role assignment is required** — that role is IGA-specific and not in play here. A standard Keycloak realm admin is sufficient.
|
|
50
|
+
- The asgard repo is already vendored under `sources/asgard-sdk/` in this pack — the .NET SDK is **consumed via `<ProjectReference>`**, not NuGet. INFERRED: NuGet publication is not yet verified for `0.1.0`.
|
|
51
|
+
- Your ASP.NET Core project ready to authenticate.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Files to Inspect
|
|
56
|
+
|
|
57
|
+
- `sources/asgard-sdk/aspnet/Tide.Asgard.AspNetCore/Tide.Asgard.AspNetCore.Example/Program.cs` — canonical wiring (note: README form and Example form **differ**, see Step 3).
|
|
58
|
+
- `sources/asgard-sdk/aspnet/Tide.Asgard.AspNetCore/Tide.Asgard.AspNetCore/Utils.cs` — `GetEd25519IssuerKey` extension.
|
|
59
|
+
- `sources/asgard-sdk/aspnet/Tide.Asgard.AspNetCore/Tide.Asgard.AspNetCore/ServiceCollectionExtensions.cs` — `AddTokenExchange` overloads.
|
|
60
|
+
- `sources/asgard-sdk/aspnet/Tide.Asgard.AspNetCore/Tide.Asgard.AspNetCore/TokenExchange/TokenExchangeService.cs` — exchange implementation; only the **Bearer** path works.
|
|
61
|
+
- `sources/asgard-sdk/README.md` — official .NET wiring guide (the README is exclusively about the .NET SDK; the TypeScript `asgard-tide` package is not documented there).
|
|
62
|
+
|
|
63
|
+
## Files to Edit
|
|
64
|
+
|
|
65
|
+
- `*.csproj` — add `<ProjectReference>` to the asgard projects.
|
|
66
|
+
- `Program.cs` — register authentication and (optionally) token exchange.
|
|
67
|
+
- `appsettings.json` — paste the **backend** client's adapter config under a `Keycloak` key.
|
|
68
|
+
- Controllers — add `[Authorize]`; inject `ITokenExchangeService` only where needed.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Step 1: Configure two Tidecloak clients
|
|
73
|
+
|
|
74
|
+
In your realm, create one **public** client for the browser SPA and one **confidential** client for the .NET backend. The audience mapper is mandatory — without it the backend rejects browser-issued tokens with 401.
|
|
75
|
+
|
|
76
|
+
| Client | Type | Settings | Why |
|
|
77
|
+
|---|---|---|---|
|
|
78
|
+
| `browser-login-page` | Public | Set valid redirect URIs and web origins matching your SPA host (e.g. `http://localhost:3000`). | SPA login. |
|
|
79
|
+
| `backend` | Confidential | **Client authentication** ON. **Standard Token Exchange** ON (only if Step 5 needed). | Backend JWT validation + token exchange. |
|
|
80
|
+
|
|
81
|
+
Then add an **Audience mapper** to the browser client:
|
|
82
|
+
|
|
83
|
+
- In your realm -> Clients -> `browser-login-page` -> Client scopes -> `browser-login-page-dedicated` -> Add mapper -> By configuration -> Audience.
|
|
84
|
+
- Name: `backend-mapper`. Included Client Audience: `backend`. Save.
|
|
85
|
+
|
|
86
|
+
> If your realm uses Tide for user authentication, create your licence **before** creating any clients. VERIFIED (`sources/asgard-sdk/README.md` line 35).
|
|
87
|
+
|
|
88
|
+
> **Do NOT toggle IGA on** for this realm. The asgard .NET SDK does not work against IGA-enabled realms today — see Current Status above. Stop the canonical bootstrap order at the licence step; skip `toggle-iga`.
|
|
89
|
+
|
|
90
|
+
## Step 2: Add the asgard projects to your .NET solution
|
|
91
|
+
|
|
92
|
+
Until NuGet publication is verified, vendor the asgard repo into your workspace and reference the projects directly:
|
|
93
|
+
|
|
94
|
+
```xml
|
|
95
|
+
<!-- Your.Api.csproj -->
|
|
96
|
+
<ItemGroup>
|
|
97
|
+
<ProjectReference Include="..\asgard\aspnet\Tide.Asgard.AspNetCore\Tide.Asgard.AspNetCore\Tide.Asgard.AspNetCore.Authentication.csproj" />
|
|
98
|
+
<!-- Optional, only if you plan to register DPoP services manually -->
|
|
99
|
+
<ProjectReference Include="..\asgard\aspnet\Tide.Asgard.AspNetCore\Tide.Asgard.AspNetCore.DPoP\Tide.Asgard.AspNetCore.DPoP.csproj" />
|
|
100
|
+
</ItemGroup>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Transitive package dependencies that get pulled in:
|
|
104
|
+
|
|
105
|
+
| Package | Version | Source |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `Keycloak.AuthServices.Authentication` | 2.9.0 | Authentication.csproj |
|
|
108
|
+
| `Keycloak.AuthServices.Authorization` | 2.9.0 | Authentication.csproj |
|
|
109
|
+
| `Microsoft.AspNetCore.Authentication.JwtBearer` | 10.0.7 | Authentication.csproj |
|
|
110
|
+
| `BouncyCastle.Cryptography` | 2.6.2 | Core.csproj |
|
|
111
|
+
| `Microsoft.IdentityModel.Tokens` | 8.15.0 | Core.csproj |
|
|
112
|
+
|
|
113
|
+
## Step 3: Paste the backend adapter config into `appsettings.json`
|
|
114
|
+
|
|
115
|
+
Download the **backend** client's adapter config: realm -> Clients -> `backend` -> top-right **Action** -> **Download adapter config**. Paste under a `Keycloak` key. The section name is fixed — `Keycloak.AuthServices` reads it by default and `AddTokenExchange(IConfiguration)` hardcodes the same name (`ServiceCollectionExtensions.cs` line 117).
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"Logging": { "LogLevel": { "Default": "Information", "Keycloak.AuthServices": "Debug" } },
|
|
120
|
+
"AllowedHosts": "*",
|
|
121
|
+
"Keycloak": {
|
|
122
|
+
"realm": "<your-realm>",
|
|
123
|
+
"auth-server-url": "https://<your-tidecloak-host>",
|
|
124
|
+
"ssl-required": "external",
|
|
125
|
+
"resource": "backend",
|
|
126
|
+
"credentials": { "secret": "<from-credentials-tab>" },
|
|
127
|
+
"confidential-port": 0,
|
|
128
|
+
"jwk": {
|
|
129
|
+
"keys": [
|
|
130
|
+
{
|
|
131
|
+
"kid": "<from-realm-keys>",
|
|
132
|
+
"kty": "OKP",
|
|
133
|
+
"alg": "EdDSA",
|
|
134
|
+
"use": "sig",
|
|
135
|
+
"crv": "Ed25519",
|
|
136
|
+
"x": "<base64url-public-key>"
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
},
|
|
140
|
+
"homeOrkUrl": "https://<your-home-ork>"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
> Do not commit real client secrets. The asgard example commits a demo secret in two places (`appsettings.json` and `README.md`), which the NuGet README inherits because `Authentication.csproj` packs the root README. **Use user secrets (`dotnet user-secrets`) or environment variables in real apps.** VERIFIED footgun.
|
|
146
|
+
|
|
147
|
+
## Step 4: Register authentication in `Program.cs`
|
|
148
|
+
|
|
149
|
+
Use the **README form**, not the Example form. The Example `Program.cs` has the `IssuerSigningKey` line commented out (`sources/asgard-sdk/.../Example/Program.cs:17`); copying it gives you a runtime that cannot validate Tidecloak's EdDSA tokens.
|
|
150
|
+
|
|
151
|
+
```csharp
|
|
152
|
+
using Keycloak.AuthServices.Authentication;
|
|
153
|
+
using Tide.Asgard.AspNetCore.Authentication;
|
|
154
|
+
|
|
155
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
156
|
+
builder.Services.AddControllers();
|
|
157
|
+
|
|
158
|
+
builder.Services
|
|
159
|
+
.AddKeycloakWebApiAuthentication(builder.Configuration, options =>
|
|
160
|
+
{
|
|
161
|
+
options.RequireHttpsMetadata = false; // set true in production
|
|
162
|
+
options.TokenValidationParameters.IssuerSigningKey =
|
|
163
|
+
Utils.GetEd25519IssuerKey(builder.Configuration);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
var app = builder.Build();
|
|
167
|
+
app.UseAuthentication();
|
|
168
|
+
app.UseAuthorization();
|
|
169
|
+
app.MapControllers();
|
|
170
|
+
app.Run();
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
What this does:
|
|
174
|
+
|
|
175
|
+
- `AddKeycloakWebApiAuthentication` wires JWT Bearer validation against the `Keycloak` section.
|
|
176
|
+
- `Utils.GetEd25519IssuerKey` walks `Keycloak:jwk:keys[]` for `crv=Ed25519` and returns a `SecurityKey` backed by the EdDSA `SignatureProvider` in `Tide.Asgard.Core`. Tidecloak signs tokens with EdDSA — the stock Microsoft stack does not ship EdDSA, so this step is required.
|
|
177
|
+
- `GetEd25519IssuerKey` is an extension method on both `IConfiguration` (`Utils.cs:18`) and `IConfigurationSection` (`Utils.cs:25`). Either form works; the static call shown above is the most common.
|
|
178
|
+
|
|
179
|
+
## Step 5: (Optional) Wire Token Exchange
|
|
180
|
+
|
|
181
|
+
Only needed if your API calls another protected service on behalf of the caller. Register the service:
|
|
182
|
+
|
|
183
|
+
```csharp
|
|
184
|
+
builder.Services.AddTokenExchange(builder.Configuration); // reads the "Keycloak" section
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
To register multiple exchange clients in one app (e.g. one per downstream audience), use the `IConfigurationSection` overload and point each registration at a different section.
|
|
188
|
+
|
|
189
|
+
Inject `ITokenExchangeService` into a controller and call `ExchangeToken`:
|
|
190
|
+
|
|
191
|
+
```csharp
|
|
192
|
+
using Microsoft.AspNetCore.Authorization;
|
|
193
|
+
using Microsoft.AspNetCore.Mvc;
|
|
194
|
+
using Tide.Asgard.AspNetCore.Authentication.TokenExchange;
|
|
195
|
+
|
|
196
|
+
[Authorize]
|
|
197
|
+
[ApiController]
|
|
198
|
+
[Route("[controller]")]
|
|
199
|
+
public class HelloController(ITokenExchangeService exchange) : ControllerBase
|
|
200
|
+
{
|
|
201
|
+
[HttpGet]
|
|
202
|
+
public async Task<IActionResult> Get()
|
|
203
|
+
{
|
|
204
|
+
var token = await exchange.ExchangeToken(
|
|
205
|
+
HttpContext.Request.Headers,
|
|
206
|
+
requestingClientId: "backend",
|
|
207
|
+
requestedAudience: "downstream-service"); // must exist as a realm client
|
|
208
|
+
return Ok(token);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Behavioral notes:
|
|
214
|
+
|
|
215
|
+
- The exchange posts `grant_type=urn:ietf:params:oauth:grant-type:token-exchange` to `protocol/openid-connect/token` and returns the new `access_token` (`TokenExchangeService.cs:42-85`).
|
|
216
|
+
- The HTTP client is registered as a **singleton with the backend client secret baked into the Basic-auth header** (`ServiceCollectionExtensions.cs:91-97`). Rotating the secret requires an app restart.
|
|
217
|
+
- `requestedAudience` must be a realm client. The asgard Example uses Keycloak's built-in `"account"` client as a demo; replace with a real audience.
|
|
218
|
+
|
|
219
|
+
## Step 6: Protect endpoints
|
|
220
|
+
|
|
221
|
+
Standard ASP.NET Core authorization. Nothing Tide-specific.
|
|
222
|
+
|
|
223
|
+
```csharp
|
|
224
|
+
[Authorize]
|
|
225
|
+
[ApiController]
|
|
226
|
+
[Route("[controller]")]
|
|
227
|
+
public class SecretController : ControllerBase
|
|
228
|
+
{
|
|
229
|
+
[HttpGet]
|
|
230
|
+
public IActionResult Get() => Ok($"Hello, {User.Identity?.Name}");
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
For role-based authorization, use `Keycloak.AuthServices.Authorization` policies — the `Authorization` package is referenced transitively.
|
|
235
|
+
|
|
236
|
+
## Step 7: SPA side (if you control it)
|
|
237
|
+
|
|
238
|
+
Your SPA uses `@tidecloak/js` with the **public** client's adapter config. Reference pattern (`sources/asgard-sdk/.../Example/ClientApp/src/main.js`):
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
import { TideCloak } from "@tidecloak/js";
|
|
242
|
+
const kc = new TideCloak("keycloak.json");
|
|
243
|
+
await kc.init({ onLoad: "login-required", checkLoginIframe: false });
|
|
244
|
+
await kc.secureFetch(`${origin}/Secret`, {
|
|
245
|
+
headers: { Authorization: `Bearer ${kc.token}` },
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The audience mapper from Step 1 is what lets the .NET backend accept tokens issued for `browser-login-page`. If `aud` does not include `backend` you will see 401.
|
|
250
|
+
|
|
251
|
+
**Do not enable `useDPoP`** in `kc.init({...})` at this point. The .NET token-exchange path crashes on DPoP-tagged requests — see [Common Failures](#common-failures).
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Verification
|
|
256
|
+
|
|
257
|
+
- [ ] `Tide.Asgard.AspNetCore.Authentication.csproj` is referenced from your csproj.
|
|
258
|
+
- [ ] `appsettings.json` has a `Keycloak` section with `realm`, `auth-server-url`, `resource`, `credentials.secret`, and `jwk.keys[]` containing an Ed25519 key.
|
|
259
|
+
- [ ] `Program.cs` calls `AddKeycloakWebApiAuthentication` **with** `options.TokenValidationParameters.IssuerSigningKey = Utils.GetEd25519IssuerKey(...)`.
|
|
260
|
+
- [ ] An unauthenticated request to a `[Authorize]` endpoint returns 401.
|
|
261
|
+
- [ ] A request with a valid `Authorization: Bearer <token>` from `browser-login-page` (containing `backend` in `aud`) succeeds.
|
|
262
|
+
- [ ] If Step 5 is wired: `ITokenExchangeService.ExchangeToken(...)` returns a non-empty access token for the configured audience.
|
|
263
|
+
- [ ] Logs show `Keycloak.AuthServices` at Debug level emitting validation traces.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Common Failures
|
|
268
|
+
|
|
269
|
+
| Symptom | Cause | Repair |
|
|
270
|
+
|---|---|---|
|
|
271
|
+
| 401 with `IDX10503: Signature validation failed. Token does not have a kid` or `Unable to match keys` | Step 4's `IssuerSigningKey = Utils.GetEd25519IssuerKey(...)` line missing or commented (matches the Example, not the README) | Add the line back. See `sources/asgard-sdk/README.md:129`. |
|
|
272
|
+
| 401 with `Audience validation failed` on browser-issued tokens | Audience mapper missing from `browser-login-page` | Step 1 — add `backend-mapper` to the `browser-login-page-dedicated` client scope. |
|
|
273
|
+
| `NotImplementedException` thrown from `ExchangeToken` | The SPA enabled DPoP. `TokenExchangeService.cs:28-35` routes DPoP-tagged requests to `ExchangeDPoPToken`, which is a stub at line 93 | Disable `useDPoP` on the SPA (`kc.init`) until the SDK ships a DPoP-aware exchange path. |
|
|
274
|
+
| `NotImplementedException` thrown from a "Doken" auth header | `ExchangeTideDoken` is a stub at `TokenExchangeService.cs:132` | Do not use the Doken auth scheme with this SDK today. |
|
|
275
|
+
| Backend ignores DPoP proof on inbound requests | `.WithDPoP(...)` is commented out at `KeycloakWebApiAuthenticationBuilderExtensions.cs:33-68`; `DPoPProofValidationService` exists but is never registered | If you need DPoP today, register `DPoPProofValidationService` and the `MessageReceivedHandler` / `TokenValidationHandler` / `ChallengeHandler` event handlers manually. Or wait for the SDK to ship `.WithDPoP`. |
|
|
276
|
+
| 404 on `/` when running the asgard Example cold | The SPA at `ClientApp/` is **not built by MSBuild**. Vite outputs to `wwwroot/` but `Example.csproj` has no `<Target>` running `npm run build` | Run `npm install && npm run build` in `ClientApp/` before `dotnet run`. |
|
|
277
|
+
| `@tidecloak/js` cannot be resolved when running the asgard Example | `ClientApp/package.json` references `@tidecloak/js` from filesystem path `~/tidecloak-js/packages/tidecloak-js`, not npm | Clone `github.com/tide-foundation/tidecloak-js` to `~/tidecloak-js/` or update the reference to an npm version. |
|
|
278
|
+
| `BadImageFormat`, target framework mismatch | Project targets a pre-net10.0 framework | Bump `<TargetFramework>net10.0</TargetFramework>` and install the .NET 10 SDK. |
|
|
279
|
+
| Realm secret rotated but exchanges still use the old secret | The exchange `HttpClient` is a singleton with Basic-auth baked in at registration time (`ServiceCollectionExtensions.cs:91-97`) | Restart the app after rotation. ASSUMED: there is no hot-reload path. |
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Anti-patterns and Do Not Do This
|
|
284
|
+
|
|
285
|
+
- **Do not copy the Example `Program.cs` verbatim.** The Example has `IssuerSigningKey` commented out — JWT validation will silently fail on EdDSA tokens. Use the README form.
|
|
286
|
+
- **Do not commit `credentials.secret`** to source control. The asgard example does this twice for demo purposes (`appsettings.json:20`, `README.md:91`). Use `dotnet user-secrets` for development and environment variables / a secrets manager for production.
|
|
287
|
+
- **Do not enable browser DPoP today.** `useDPoP` in the SPA causes every Token Exchange call to throw `NotImplementedException` (see Common Failures).
|
|
288
|
+
- **Do not point `requestedAudience` at the wrong client.** It must exist as a realm client with Standard Token Exchange enabled. `"account"` works in demos because it is Keycloak's built-in client.
|
|
289
|
+
- **Do not register `AddTokenExchange(IConfiguration)` for multi-client scenarios.** Use the `IConfigurationSection` overload — the `IConfiguration` form hardcodes the `"Keycloak"` section name (`ServiceCollectionExtensions.cs:117`).
|
|
290
|
+
- **Do not rely on the commented-out helpers.** `UseTidecloakDashboard`, `UseTideSecuredDPoP`, `AddAutoClientCeritificationToDashboard`, `.WithDPoP` — all four are commented out at HEAD. The public type signatures (`TidecloakDashboardOptions`, `MTLSOptions`, `ClientCertificationOptions`) ship but nothing wires them.
|
|
291
|
+
- **Do not assume the .NET SDK validates Tide Policies / Forseti contracts.** Policy enforcement happens on ORKs (C# Forseti contracts, see [custom-contracts.md](../canon/custom-contracts.md)). The .NET SDK only handles JWT auth and token exchange.
|
|
292
|
+
- **Do not treat folder names as canonical.** The source tree contains typos that are reachable via `using` statements: `ClientCeritifcation/` (should be `ClientCertification`), `MessageRecievedHandler.cs` (should be `Received`). Grep for the wrong spelling or you will miss them.
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Repair Path
|
|
297
|
+
|
|
298
|
+
If you have an existing ASP.NET Core API on TideCloak that is misbehaving:
|
|
299
|
+
|
|
300
|
+
1. **JWT validation failures** — confirm `IssuerSigningKey = Utils.GetEd25519IssuerKey(...)` is present in `Program.cs`. The Microsoft default JWT handler does not ship EdDSA.
|
|
301
|
+
2. **401 with audience mismatch** — confirm the SPA's client has an audience mapper to the backend client.
|
|
302
|
+
3. **Token exchange throws** — confirm no DPoP / Doken auth header is being sent. Today only Bearer works.
|
|
303
|
+
4. **Inbound DPoP** — confirm whether you actually need it. If yes, you are off the supported path and must register DPoP services manually (see Common Failures row). If no, ignore the `Tide.Asgard.AspNetCore.DPoP` project entirely.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Cross-references
|
|
308
|
+
|
|
309
|
+
- [add-auth-nextjs-fresh.md](add-auth-nextjs-fresh.md) — Next.js equivalent (different SDK).
|
|
310
|
+
- [protect-api-nextjs.md](protect-api-nextjs.md) / [verify-jwt-server-side.md](verify-jwt-server-side.md) — Node-side JWT verification path.
|
|
311
|
+
- [Step 5: Wire Token Exchange](#step-5-optional-wire-token-exchange) — RFC 8693 token exchange is the shipped path for .NET server-to-server delegation. (Browser-driven server-side delegation is an asgard feature — the `asgard-tide` Node SDK, the same one keylessh's server-delegation uses — and is unmerged/experimental, not on main, so there is no standalone server-delegation playbook.)
|
|
312
|
+
- [custom-contracts.md](../canon/custom-contracts.md) — Forseti contracts; out of scope for this playbook but relevant if your .NET API is downstream of policy-governed signing.
|
|
313
|
+
- [framework-matrix.md](../canon/framework-matrix.md#net--aspnet-core-via-asgard) — concise framework entry for .NET via asgard.
|