Skip to content

Commit 5d5311f

Browse files
cjbellcellomattscotidodson
authored
feat: introduce explicit token grants (#1395)
* feat: introduce explicit token grants * chore: add user delete and user merge * Update content/in-app-ui/security-and-authentication.mdx Co-authored-by: Matt Kufchak <matt@knock.app> * Update content/in-app-ui/security-and-authentication.mdx Co-authored-by: Scoti Dodson <scoti@knock.app> --------- Co-authored-by: Matt Kufchak <matt@knock.app> Co-authored-by: Scoti Dodson <scoti@knock.app>
1 parent b6996f5 commit 5d5311f

2 files changed

Lines changed: 148 additions & 1 deletion

File tree

content/in-app-ui/security-and-authentication.mdx

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,145 @@ knockClient.authenticate({ id: currentUser.id }, currentUser.knockToken);
174174
>
175175
```
176176

177+
## Limiting access to specific resources
178+
179+
You can limit access to the endpoints that can be called with a public API key by setting access `grants` on the user token.
180+
181+
You can ensure that **no other actions** can be taken on any other resources by setting the `explicit_grants` option to `true` when generating the user token. Without this option, the token will be granted access to all resources by default.
182+
183+
<Table
184+
headers={["Type", "Grants", "Description"]}
185+
rows={[
186+
["Read profile", "user/read", "Allow the token to read the user's profile."],
187+
["Write profile", "user/write", "Allow the token to write to the user's profile."],
188+
["Delete user", "user/delete", "Allow the token to delete the user."],
189+
["Merge user", "user/merge", "Allow the token to merge the user with another user."],
190+
["Read feed", "user_feed/read", "Allow the token to read the user's feed."],
191+
[
192+
"Write feed",
193+
"user_feed/write",
194+
"Allow the token to perform write action on the user's feed. This includes marking messages as read, seen, or archived.",
195+
],
196+
[
197+
"Read preferences",
198+
"preferences/read",
199+
"Allow the token to read the user's preferences.",
200+
],
201+
[
202+
"Write preferences",
203+
"preferences/write",
204+
"Allow the token to write to the user's preferences.",
205+
],
206+
[
207+
"Read channel data",
208+
"channel_data/read",
209+
"Allow the token to read the channel data for the user.",
210+
],
211+
[
212+
"Write channel data",
213+
"channel_data/write",
214+
"Allow the token to write the channel data for the user.",
215+
],
216+
217+
]}
218+
/>
219+
220+
Grants are structured according to the <a href="https://github.com/ucan-wg/spec" target="_blank" rel="noopener noreferrer">UCAN spec</a>. They consist of an array of maps, with each map representing a resource.
221+
222+
A grant for a user should always be structured with the user's ID as the key (`https://api.knock.app/v1/users/<user_id>`). This should always match the `sub` claim in the user token.
223+
224+
```json title="Example of a grant for a user"
225+
{
226+
"https://api.knock.app/v1/users/<user_id>": {
227+
"user/read": [{}]
228+
}
229+
}
230+
```
231+
232+
```json title="Example of a user token with grants to make it read-only"
233+
{
234+
"sub": "<user_id>",
235+
"explicit_grants": true,
236+
"grants": {
237+
"https://api.knock.app/v1/users/<user_id>": {
238+
"user/read": [{}],
239+
"feed/read": [{}],
240+
"preferences/read": [{}],
241+
"channel_data/read": [{}]
242+
}
243+
}
244+
}
245+
```
246+
247+
### Scoping a token to a tenant
248+
249+
Some per-tenant resources can further be scoped to a specific tenant by including a set of grants for one or more tenants in the user token. Doing so will restrict the updates that can be made to the resource to only the tenants that are included in the user token.
250+
251+
<Callout
252+
type="alert"
253+
title="Note:"
254+
text={
255+
<>
256+
Without a tenant-scope, any user actions will apply to all tenants. When
257+
at-least one tenant-scope is included, any user actions will apply to the
258+
tenants included in the user token.
259+
</>
260+
}
261+
/>
262+
263+
Per-tenant resources can be applied to:
264+
265+
<Table
266+
headers={["Type", "Grants", "Description"]}
267+
rows={[
268+
[
269+
"Tenant-scoped feeds",
270+
"user_feed/read, user_feed/write",
271+
"Allow the user to read and write feeds for the tenants included in the user token.",
272+
],
273+
[
274+
"Tenant-scoped preferences",
275+
"preferences/read, preferences/write",
276+
"Allow the user to read and write preferences for the tenants included in the user token.",
277+
],
278+
]}
279+
/>
280+
281+
Grants for a tenant should always be structured with the tenant's ID as the key (`https://api.knock.app/v1/tenants/<tenant_id>`). One or more tenant grants can be included in the user token.
282+
283+
**Example policies**
284+
285+
This policy will **only** allow the user to read in-app feeds for the tenant `acme_corp`. Feed reads to any other tenant will be denied, as will feed reads to a nil tenant scope.
286+
287+
```json title="Example of a user token with grants for a specific tenant"
288+
{
289+
"sub": "<user_id>",
290+
"explicit_grants": true,
291+
"grants": {
292+
"https://api.knock.app/v1/tenants/acme_corp": {
293+
"user_feed/read": [{}]
294+
}
295+
}
296+
}
297+
```
298+
299+
To allow the user to read a feed for the unscoped usecase (e.g. tenant = null), and also allow them to read feeds for the tenant `acme_corp`, you can include a grant the user's resource and a grant for the tenant.
300+
301+
```json title="Example of a user token with grants for a specific tenant"
302+
{
303+
"sub": "<user_id>",
304+
"explicit_grants": true,
305+
"grants": {
306+
"https://api.knock.app/v1/users/<user_id>": {
307+
"user_feed/read": [{}]
308+
},
309+
"https://api.knock.app/v1/tenants/acme_corp": {
310+
"user_feed/read": [{}]
311+
}
312+
}
313+
}
314+
```
315+
177316
## Handling token expiration
178317

179318
Generally, it's advisable to set the token expiration to be equal to your session token expiration. That way you can regenerate both of the tokens together from within your application. There may, however, be cases where this is not possible, in which case it's best practice to opt for a relatively short-lived expiration time for your user tokens and refresh them from your backend before the expiration window.

lib/mdxComponents.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,15 @@ export const MDX_COMPONENTS = {
6161
h3: (props) => <SectionHeading tag="h3" mb="2" mt="8" {...props} />,
6262
h4: (props) => <SectionHeading tag="h4" mb="1" mt="8" {...props} />,
6363
code: (props) => (
64-
<Code as="code" backgroundColor="gray-2" data-tgph-code {...props}>
64+
<Code
65+
as="code"
66+
color="blue"
67+
backgroundColor="transparent"
68+
px="0_5"
69+
size="0"
70+
data-tgph-code
71+
{...props}
72+
>
6573
{props.children}
6674
</Code>
6775
),

0 commit comments

Comments
 (0)