@antelopejs/interface-auth 0.0.7 → 0.2.0
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 +20 -14
- package/dist/antelope.test.js +1 -5
- package/dist/antelope.test.js.map +1 -1
- package/dist/index.d.ts +79 -28
- package/dist/index.js +50 -76
- package/dist/index.js.map +1 -1
- package/dist/tests/class-decorator.test.d.ts +1 -0
- package/dist/tests/class-decorator.test.js +20 -0
- package/dist/tests/class-decorator.test.js.map +1 -0
- package/dist/tests/index.test.js +32 -57
- package/dist/tests/index.test.js.map +1 -1
- package/dist/tests/local-build.test.d.ts +1 -0
- package/dist/tests/local-build.test.js +147 -0
- package/dist/tests/local-build.test.js.map +1 -0
- package/docs/1.introduction.md +51 -11
- package/docs/2.authentication.md +51 -39
- package/docs/3.token-handling.md +60 -38
- package/docs/4.parameter-decoration.md +45 -29
- package/docs/assets/authentication-pipeline.svg +21 -0
- package/package.json +25 -6
- package/skills/auth-interface/SKILL.md +8 -2
- package/skills/auth-interface/cases.json +73 -0
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Require authentication in controllers
|
|
3
|
+
description: Protect a handler, a property, or a whole controller with @Authentication, and build reusable decorators with CreateAuthDecorator.
|
|
4
|
+
mode: how-to
|
|
5
|
+
---
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
Use a parameter decorator when one handler needs the payload. Use a property decorator when several methods need it. Use a class decorator when every route must authenticate without receiving the payload. Pick one scope per need. You do not have to combine them.
|
|
4
8
|
|
|
5
9
|
## The `@Authentication` decorator
|
|
6
10
|
|
|
7
|
-
The `@Authentication` decorator
|
|
11
|
+
The `@Authentication` decorator reads a token from `x-antelopejs-auth`, or from the `ANTELOPEJS_AUTH` cookie when the header is absent. It verifies the token and injects the verified payload into the parameter.
|
|
8
12
|
|
|
9
13
|
```typescript
|
|
10
14
|
import { Controller, Get } from "@antelopejs/interface-api";
|
|
@@ -21,7 +25,9 @@ class UserController extends Controller("/users") {
|
|
|
21
25
|
}
|
|
22
26
|
```
|
|
23
27
|
|
|
24
|
-
If the token is missing or invalid, the
|
|
28
|
+
If the token is missing or invalid, the verifier rejects and the handler does not run. With `@antelopejs/auth-jwt`, the response is a 401 for a missing token and a 403 for an invalid one.
|
|
29
|
+
|
|
30
|
+
The payload annotations in these examples describe the token format the application expects. They do not check it. Add a [shape-checking verifier](./2.authentication.md#create-a-custom-verifier) before you read claims from tokens whose format nothing enforces yet. The API provider decides how an ordinary thrown error becomes an HTTP response. Throwing an `Error` does not set a 401 or 403 status.
|
|
25
31
|
|
|
26
32
|
### Inline validators
|
|
27
33
|
|
|
@@ -34,13 +40,15 @@ import { Authentication } from "@antelopejs/interface-auth";
|
|
|
34
40
|
class PaymentController extends Controller("/payments") {
|
|
35
41
|
@Post()
|
|
36
42
|
async createPayment(
|
|
37
|
-
@Authentication((
|
|
38
|
-
if (!
|
|
43
|
+
@Authentication((claims) => {
|
|
44
|
+
if (typeof claims === "string" || !claims.paymentPermissions) {
|
|
39
45
|
throw new Error("Payment permissions required");
|
|
40
46
|
}
|
|
41
|
-
return
|
|
47
|
+
return claims;
|
|
42
48
|
})
|
|
43
|
-
user:
|
|
49
|
+
user: {
|
|
50
|
+
paymentPermissions: string[];
|
|
51
|
+
},
|
|
44
52
|
) {
|
|
45
53
|
// Process payment
|
|
46
54
|
return { success: true };
|
|
@@ -50,51 +58,59 @@ class PaymentController extends Controller("/payments") {
|
|
|
50
58
|
|
|
51
59
|
## Create custom decorators with `CreateAuthDecorator`
|
|
52
60
|
|
|
53
|
-
The `CreateAuthDecorator` function creates a reusable authentication decorator
|
|
61
|
+
The `CreateAuthDecorator` function creates a reusable authentication decorator from custom source, authenticator, and validator functions. `validateApiKey` in the example below belongs to your application.
|
|
54
62
|
|
|
55
|
-
```typescript
|
|
63
|
+
```typescript fragment
|
|
56
64
|
import { CreateAuthDecorator } from "@antelopejs/interface-auth";
|
|
57
65
|
import type { IncomingMessage } from "node:http";
|
|
58
66
|
|
|
59
67
|
// Admin-only decorator
|
|
60
68
|
export const AdminAuth = CreateAuthDecorator({
|
|
61
|
-
validator: (
|
|
62
|
-
if (
|
|
69
|
+
validator: (claims) => {
|
|
70
|
+
if (typeof claims === "string" || claims.role !== "admin") {
|
|
63
71
|
throw new Error("Admin access required");
|
|
64
72
|
}
|
|
65
|
-
return
|
|
73
|
+
return claims;
|
|
66
74
|
},
|
|
67
75
|
});
|
|
68
76
|
|
|
69
77
|
// API key decorator with custom source and authenticator
|
|
70
78
|
export const ApiKeyAuth = CreateAuthDecorator({
|
|
71
|
-
source: (req: IncomingMessage) =>
|
|
79
|
+
source: (req: IncomingMessage) => {
|
|
80
|
+
const key = req.headers["x-api-key"];
|
|
81
|
+
return typeof key === "string" ? key : undefined;
|
|
82
|
+
},
|
|
72
83
|
authenticator: async (apiKey?: string) => {
|
|
73
|
-
if (!apiKey)
|
|
84
|
+
if (!apiKey) throw new Error("API key required");
|
|
74
85
|
const user = await validateApiKey(apiKey);
|
|
86
|
+
if (!user) throw new Error("Invalid API key");
|
|
75
87
|
return user;
|
|
76
88
|
},
|
|
77
89
|
});
|
|
78
90
|
```
|
|
79
91
|
|
|
92
|
+
`validateApiKey` is your application's lookup and verification function. If the authenticator returned `null` for a missing or invalid key, the handler would run with `null`. This example throws instead.
|
|
93
|
+
|
|
80
94
|
### `CreateAuthDecorator` options
|
|
81
95
|
|
|
82
96
|
The function accepts an object with the following properties:
|
|
83
97
|
|
|
84
|
-
| Property | Type | Description
|
|
85
|
-
| ---------------------- | --------------------- |
|
|
86
|
-
| `source` | `AuthSource` | Extracts the token from the request
|
|
87
|
-
| `authenticator` | `AuthVerifier<T>` | Verifies the token and returns the payload
|
|
88
|
-
| `authenticatorOptions` | `VerifyOptions` | Options passed to the authenticator
|
|
89
|
-
| `validator` | `AuthValidator<T, R>` | Validates and transforms the authenticated data
|
|
98
|
+
| Property | Type | Description |
|
|
99
|
+
| ---------------------- | --------------------- | --------------------------------------------------------------------- |
|
|
100
|
+
| `source` | `AuthSource` | Extracts the token from the request. Defaults to header, then cookie. |
|
|
101
|
+
| `authenticator` | `AuthVerifier<T>` | Verifies the token and returns the payload |
|
|
102
|
+
| `authenticatorOptions` | `VerifyOptions` | Options passed to the authenticator |
|
|
103
|
+
| `validator` | `AuthValidator<T, R>` | Validates and transforms the authenticated data |
|
|
104
|
+
|
|
105
|
+
All properties are optional. The default source reads the header, then the cookie. The default authenticator calls `ValidateRaw`, and the default verification options are `{}`. Without a validator, the decorator injects the verified payload unchanged.
|
|
90
106
|
|
|
91
|
-
|
|
107
|
+
TypeScript infers `T` and `R` from the callbacks. Without an `authenticator`, `T` is `AuthClaims`, which is `string | JsonObject`. A validator then checks the claims before it reads them, as in `AdminAuth` above. With an `authenticator`, `T` is what the authenticator returns, and the validator receives that type. To name both type arguments, write `CreateAuthDecorator<T, R>` with an `authenticator`. A single type argument names `R` when there is no `authenticator`, and `T` when there is one.
|
|
92
108
|
|
|
93
|
-
A validator passed to a decorator call replaces the configured validator
|
|
109
|
+
A validator passed to a decorator call replaces the configured validator. The two do not compose. Return a transformed value to inject it, or throw to deny access. Returning `false` or `null` does not reject the request. A custom authenticator that accepts a missing token lets the request through unless a validator throws.
|
|
94
110
|
|
|
95
111
|
## Class-level authentication
|
|
96
112
|
|
|
97
|
-
|
|
113
|
+
Apply the `@Authentication` decorator to a controller class to require authentication on every route of that controller. You do not need to decorate each handler.
|
|
98
114
|
|
|
99
115
|
```typescript
|
|
100
116
|
import { Controller, Get, Post } from "@antelopejs/interface-api";
|
|
@@ -124,11 +140,11 @@ Combine class-level authentication with a validator to enforce role-based access
|
|
|
124
140
|
import { Controller } from "@antelopejs/interface-api";
|
|
125
141
|
import { Authentication } from "@antelopejs/interface-auth";
|
|
126
142
|
|
|
127
|
-
@Authentication((
|
|
128
|
-
if (
|
|
143
|
+
@Authentication((claims) => {
|
|
144
|
+
if (typeof claims === "string" || claims.role !== "admin") {
|
|
129
145
|
throw new Error("Admin access required");
|
|
130
146
|
}
|
|
131
|
-
return
|
|
147
|
+
return claims;
|
|
132
148
|
})
|
|
133
149
|
class AdminController extends Controller("/admin") {
|
|
134
150
|
// All routes require admin authentication
|
|
@@ -137,7 +153,7 @@ class AdminController extends Controller("/admin") {
|
|
|
137
153
|
|
|
138
154
|
## Property-level authentication
|
|
139
155
|
|
|
140
|
-
Apply `@Authentication` to a class property to
|
|
156
|
+
Apply `@Authentication` to a class property to read the authenticated user data in every handler of the controller. The API module sets the property on each request.
|
|
141
157
|
|
|
142
158
|
```typescript
|
|
143
159
|
import { Controller, Get } from "@antelopejs/interface-api";
|
|
@@ -165,7 +181,7 @@ class UserController extends Controller("/users") {
|
|
|
165
181
|
|
|
166
182
|
## Decorator scope summary
|
|
167
183
|
|
|
168
|
-
The `@Authentication` decorator
|
|
184
|
+
The `@Authentication` decorator, and any custom decorator that `CreateAuthDecorator` creates, applies at three levels:
|
|
169
185
|
|
|
170
186
|
| Scope | Effect |
|
|
171
187
|
| --------- | --------------------------------------------------------------------- |
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 480" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">Authentication: extraction, trust, policy</title>
|
|
3
|
+
<desc id="desc">A source extracts a token. An authenticator verifies it. An optional validator checks policy. Only successful processing injects the result. Throwing stops injection.</desc>
|
|
4
|
+
<rect width="480" height="480" rx="16" fill="#f5f7fb"/>
|
|
5
|
+
<g font-family="system-ui, sans-serif" fill="#172b45">
|
|
6
|
+
<text x="24" y="36" font-size="20" font-weight="700">From request to trusted input</text>
|
|
7
|
+
<rect x="24" y="58" width="432" height="84" rx="10" fill="#e1ecfa"/>
|
|
8
|
+
<text x="42" y="87" font-size="18" font-weight="700">1 · Source</text>
|
|
9
|
+
<text x="42" y="116" font-size="16">Extract the token; do not trust it yet.</text>
|
|
10
|
+
<path d="M240 148v20m-6-6 6 6 6-6" fill="none" stroke="#526780" stroke-width="2"/>
|
|
11
|
+
<rect x="24" y="176" width="432" height="84" rx="10" fill="#d9eee8"/>
|
|
12
|
+
<text x="42" y="205" font-size="18" font-weight="700">2 · Authenticator</text>
|
|
13
|
+
<text x="42" y="234" font-size="16">Verify the token and return its payload.</text>
|
|
14
|
+
<path d="M240 266v20m-6-6 6 6 6-6" fill="none" stroke="#526780" stroke-width="2"/>
|
|
15
|
+
<rect x="24" y="294" width="432" height="84" rx="10" fill="#eee6fb"/>
|
|
16
|
+
<text x="42" y="323" font-size="18" font-weight="700">3 · Validator (optional)</text>
|
|
17
|
+
<text x="42" y="352" font-size="16">Check policy; return the value to inject.</text>
|
|
18
|
+
<text x="24" y="416" font-size="16" font-weight="700">Success → inject the resulting value.</text>
|
|
19
|
+
<text x="24" y="447" font-size="16" fill="#913a31">Throw → stop. Returning false does not deny.</text>
|
|
20
|
+
</g>
|
|
21
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/interface-auth",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "AntelopeJS interface for token authentication: sign and verify tokens and inject the verified payload into API handlers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antelopejs",
|
|
@@ -46,13 +46,22 @@
|
|
|
46
46
|
"release": "pnpm run lint && pnpm run prepack && release-it",
|
|
47
47
|
"test": "pnpm run build && ajs module test .",
|
|
48
48
|
"format:check": "oxfmt --check .",
|
|
49
|
-
"knip": "knip"
|
|
49
|
+
"knip": "knip",
|
|
50
|
+
"prepare": "if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && command -v lefthook >/dev/null 2>&1; then lefthook install; fi",
|
|
51
|
+
"checks": "antelopejs-checks",
|
|
52
|
+
"size": "antelopejs-check-size",
|
|
53
|
+
"sync": "antelopejs-sync",
|
|
54
|
+
"sync:check": "antelopejs-sync --check",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"suppressions": "antelopejs-suppressions",
|
|
57
|
+
"suppressions:update": "antelopejs-suppressions --update"
|
|
50
58
|
},
|
|
51
59
|
"devDependencies": {
|
|
60
|
+
"@antelopejs/auth-jwt": "1.0.3",
|
|
52
61
|
"@antelopejs/core": "^1.16.0",
|
|
53
|
-
"@antelopejs/interface-api": "^0.0
|
|
54
|
-
"@antelopejs/interface-core": "^0.
|
|
55
|
-
"@antelopejs/tooling-configs": "^0.
|
|
62
|
+
"@antelopejs/interface-api": "^0.2.0",
|
|
63
|
+
"@antelopejs/interface-core": "^0.4.0",
|
|
64
|
+
"@antelopejs/tooling-configs": "^0.2.17",
|
|
56
65
|
"@types/chai": "^4.3.20",
|
|
57
66
|
"@types/mocha": "^10.0.10",
|
|
58
67
|
"@types/node": "^22.19.15",
|
|
@@ -60,6 +69,7 @@
|
|
|
60
69
|
"chai": "^4.5.0",
|
|
61
70
|
"eslint-plugin-perfectionist": "^5.11.0",
|
|
62
71
|
"knip": "^6.34.0",
|
|
72
|
+
"lefthook": "^2.2.2",
|
|
63
73
|
"mongodb-memory-server-core": "^10.4.3",
|
|
64
74
|
"oxfmt": "^0.66.0",
|
|
65
75
|
"oxlint": "1.81.0",
|
|
@@ -74,10 +84,19 @@
|
|
|
74
84
|
"@antelopejs/interface-core": ">=0.2.4 <1.0.0"
|
|
75
85
|
},
|
|
76
86
|
"packageManager": "pnpm@10.6.5",
|
|
87
|
+
"pnpm": {
|
|
88
|
+
"overrides": {
|
|
89
|
+
"@antelopejs/auth-jwt>@antelopejs/interface-auth": "link:.",
|
|
90
|
+
"@antelopejs/interface-core": "0.4.0"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
77
93
|
"antelopeJs": {
|
|
78
94
|
"test": "src/antelope.test.ts",
|
|
79
95
|
"skills": [
|
|
80
96
|
"./skills"
|
|
81
|
-
]
|
|
97
|
+
],
|
|
98
|
+
"docs": {
|
|
99
|
+
"standard": true
|
|
100
|
+
}
|
|
82
101
|
}
|
|
83
102
|
}
|
|
@@ -24,9 +24,12 @@ import {
|
|
|
24
24
|
ValidateRaw,
|
|
25
25
|
SignServerResponse,
|
|
26
26
|
internal, // provider side only
|
|
27
|
+
type AuthClaims,
|
|
27
28
|
type AuthSource,
|
|
28
29
|
type AuthVerifier,
|
|
29
30
|
type AuthValidator,
|
|
31
|
+
type JsonObject,
|
|
32
|
+
type JsonValue,
|
|
30
33
|
type SignOptions,
|
|
31
34
|
type VerifyOptions,
|
|
32
35
|
type CookieOptions,
|
|
@@ -61,7 +64,9 @@ class LibrarianController extends Controller("/librarians") {
|
|
|
61
64
|
}
|
|
62
65
|
```
|
|
63
66
|
|
|
64
|
-
Custom pipelines use `CreateAuthDecorator({ source?, authenticator?, authenticatorOptions?, validator? })`; the callbacks run as `source(req, res)` → `authenticator(data, authenticatorOptions)` → `validator(data)` → injected parameter.
|
|
67
|
+
Custom pipelines use `CreateAuthDecorator({ source?, authenticator?, authenticatorOptions?, validator? })`; the callbacks run as `source(req, res)` → `authenticator(data, authenticatorOptions)` → `validator(data)` → injected parameter. Without an `authenticator` the validator receives an `AuthClaims`; with one it receives what the authenticator returns.
|
|
68
|
+
|
|
69
|
+
`ValidateRaw` has no type parameter: it returns `AuthClaims` (`string | JsonObject`). To read fields, parse the claims once, in a custom `authenticator` (`async (token, options) => parseSession(await ValidateRaw(token, options))`) or at the call site.
|
|
65
70
|
|
|
66
71
|
## Providing
|
|
67
72
|
|
|
@@ -72,7 +77,7 @@ import { ImplementInterface } from "@antelopejs/interface-core";
|
|
|
72
77
|
import { internal } from "@antelopejs/interface-auth";
|
|
73
78
|
|
|
74
79
|
ImplementInterface(internal, {
|
|
75
|
-
Verify: (token, options) => decodeAndVerify(token, options), // return
|
|
80
|
+
Verify: (token, options) => decodeAndVerify(token, options), // return the AuthClaims (string or JSON object), throw on invalid
|
|
76
81
|
Sign: (data, options) => signToken(data, options), // return token string
|
|
77
82
|
});
|
|
78
83
|
```
|
|
@@ -81,6 +86,7 @@ Declare `"antelopeJs": { "implements": ["@antelopejs/interface-auth"] }` in the
|
|
|
81
86
|
|
|
82
87
|
## Gotchas
|
|
83
88
|
|
|
89
|
+
- The data passed to `SignRaw` / `SignServerResponse` is a `string`, a `Buffer` or a `JsonObject`. A `Date`, a class instance or an `interface`-typed object (no index signature) is a compile error; convert it or use a `type` alias. `undefined`-valued keys are accepted.
|
|
84
90
|
- `SignRaw` / `ValidateRaw` / `SignServerResponse` are interface proxy calls: they return Promises and only resolve once a provider module is attached. While a provider module is loaded but not yet attached, earlier calls are queued, not failed — always `await`. But when no loaded module provides the interface (e.g. a stubbed `optionalDependencies` entry), the core neutralizes the proxies and those calls reject instead of queuing.
|
|
85
91
|
- Default token source (`internal.defaultSource`): the `x-antelopejs-auth` request header, falling back to the `ANTELOPEJS_AUTH` cookie. `SignServerResponse` sets that same cookie via `Set-Cookie`.
|
|
86
92
|
- `@Authentication(validator?)` accepts an optional validator at the use site; it overrides any validator configured in `CreateAuthDecorator`.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"cases": [
|
|
4
|
+
{
|
|
5
|
+
"id": "protect-route",
|
|
6
|
+
"area": "framework",
|
|
7
|
+
"style": "clean",
|
|
8
|
+
"prompt": "How do I protect a route with authentication?",
|
|
9
|
+
"origin": "docs-assistant",
|
|
10
|
+
"expect": {
|
|
11
|
+
"skills": ["auth-interface"],
|
|
12
|
+
"acceptable": ["api-interface", "ajs-consume-interface", "antelopejs"],
|
|
13
|
+
"mustMention": ["Authentication"]
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "protect-route-casual",
|
|
18
|
+
"area": "framework",
|
|
19
|
+
"style": "casual",
|
|
20
|
+
"prompt": "what do i need to do so my route is protected?",
|
|
21
|
+
"origin": "docs-assistant",
|
|
22
|
+
"expect": {
|
|
23
|
+
"skills": ["auth-interface"],
|
|
24
|
+
"acceptable": ["api-interface", "ajs-consume-interface", "antelopejs"],
|
|
25
|
+
"mustMention": ["Authentication"]
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "auth-interface-dms-neighbour",
|
|
30
|
+
"area": "dms",
|
|
31
|
+
"style": "clean",
|
|
32
|
+
"prompt": "In the DMS, only administrators should open the billing page. Register a permission for it and gate the page.",
|
|
33
|
+
"expect": {
|
|
34
|
+
"skills": ["dms-auth"],
|
|
35
|
+
"acceptable": ["dms", "dms-pages"],
|
|
36
|
+
"avoid": ["auth-interface"]
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "auth-interface-cookie-response",
|
|
41
|
+
"area": "framework",
|
|
42
|
+
"style": "clean",
|
|
43
|
+
"prompt": "After login I want the signed token set as a cookie on the response instead of returned in the body. What do I call?",
|
|
44
|
+
"expect": {
|
|
45
|
+
"skills": ["auth-interface"],
|
|
46
|
+
"acceptable": ["api-interface", "ajs-consume-interface", "antelopejs"],
|
|
47
|
+
"mustMention": ["SignServerResponse"]
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "auth-interface-token-expiry-casual",
|
|
52
|
+
"area": "framework",
|
|
53
|
+
"style": "casual",
|
|
54
|
+
"prompt": "token dies too quick, how do i make it last a day",
|
|
55
|
+
"expect": {
|
|
56
|
+
"skills": ["auth-interface"],
|
|
57
|
+
"acceptable": ["antelopejs", "ajs-consume-interface"],
|
|
58
|
+
"mustMention": ["SignRaw", "expiresIn"]
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "auth-interface-typed-claims",
|
|
63
|
+
"area": "framework",
|
|
64
|
+
"style": "clean",
|
|
65
|
+
"prompt": "ValidateRaw<Profile>(token) no longer compiles. How do I get a typed profile out of a verified token?",
|
|
66
|
+
"expect": {
|
|
67
|
+
"skills": ["auth-interface"],
|
|
68
|
+
"acceptable": ["antelopejs", "ajs-consume-interface"],
|
|
69
|
+
"mustMention": ["AuthClaims"]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|