Skip to content
This repository has been archived by the owner on Aug 22, 2024. It is now read-only.

DO NOT MERGE YET - finalize frontegg-auth before first release #36

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 172 additions & 1 deletion packages/frontegg-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,175 @@ Install using:
npm install @adobe/contentlake-shared-frontegg-auth
```

TODO
TODO: code example

## Development
### Frontegg token examples

Below are the payloads of different Frontegg JWT tokens. Note that all names, emails and ids have been replaced with randomized values.

#### User token – `userToken`

Normal access token for human users received via the OAuth login flow.

```
{
"sub": "626de2ed-2bf1-4314-bd95-3bcbc232e6ef",
"name": "John Doe",
"email": "john@example.com",
"email_verified": true,
"metadata": {},
"roles": [
"Admin"
],
"permissions": [
"fe.connectivity.*",
"fe.secure.*",
"contentlake.repository.manage"
],
"tenantId": "d0d17542-7899-41a1-8f35-11a701bf1047",
"tenantIds": [
"d0d17542-7899-41a1-8f35-11a701bf1047",
"8d4f6887-6786-433e-bfe2-baad0ea869f9",
],
"profilePictureUrl": "https://www.gravatar.com/avatar/1234567890?d=https://ui-avatars.com/api/John+Doe/128/random",
"sid": "0e4dc1fe-677b-43fe-ba0c-363532247cd1",
"type": "userToken",
"aud": "4230c66f-5558-4365-863d-737ba8ffc373",
"iss": "https://app-abcdefghijkl.frontegg.com",
"iat": 1688054464,
"exp": 1688659264
}
```

Notes:
- `sub` is the user id used in Frontegg apis to identify the user
- `sid` seems to be a unique value not used anywhere else ?

#### User API token

API tokens that users can create for themselves.

Also called _User-specific API Token_ in the [API reference](https://docs.frontegg.com/reference/userapitokensv1controller_createtenantapitoken) and _Personal Token_ in the [Admin Portal](https://docs.frontegg.com/docs/personal-tokens).

The final JWT will look slightly different depending on what [type of credential](https://docs.frontegg.com/docs/m2m-tokens#client-credentials-vs-access-tokens) is chosen for M2M, _Client Credentials_ or _Access Tokens_.


Using client credentials kind – `userApiToken`:

```
{
"sub": "7141a5a2-a2c7-4fd1-bbb9-089455ba1e82",
"email": "john@example.com",
"userMetadata": {},
"tenantId": "d0d17542-7899-41a1-8f35-11a701bf1047",
"roles": [
"Admin"
],
"permissions": [
"fe.connectivity.*",
"fe.secure.*",
"contentlake.repository.manage"
],
"metadata": {},
"createdByUserId": "626de2ed-2bf1-4314-bd95-3bcbc232e6ef",
"type": "userApiToken",
"userId": "626de2ed-2bf1-4314-bd95-3bcbc232e6ef",
"aud": "4230c66f-5558-4365-863d-737ba8ffc373",
"iss": "https://app-abcdefghijkl.frontegg.com",
"iat": 1688129488,
"exp": 1688734288
}
```

Using access token kind – `userAccessToken`:

```
{
"sub": "b94c0200-6d9b-4c9f-a612-96be8dcb98b0",
"type": "userAccessToken",
"tenantId": "d0d17542-7899-41a1-8f35-11a701bf1047",
"userId": "626de2ed-2bf1-4314-bd95-3bcbc232e6ef",
"roles": [
"FETCH-ROLES-BY-API"
],
"permissions": [
"FETCH-PERMISSIONS-BY-API"
],
"aud": "4230c66f-5558-4365-863d-737ba8ffc373",
"iss": "https://app-abcdefghijkl.frontegg.com",
"iat": 1688128620,
"exp": 1688733420
}
```

Notes:
- `userId` is the same as the user who owns the personal token
- `createdByUserId` is the same as the user who owns the personal token and created it (only for `userApiToken`)
- `sub` is different from the user and seems to be a unique value
- `aud` is the same as for user tokens and tenant api tokens

#### Tenant token

Service users/tokens that admins can create for a specific tenant.

Also called _Tenant API Token_ in the [API reference](https://docs.frontegg.com/reference/tenantapitokensv2controller_createtenantapitoken) and _API Token_ in the [Admin Portal](https://docs.frontegg.com/docs/admin-portal-api-tokens).

The final JWT will look slightly different depending on what [type of credential](https://docs.frontegg.com/docs/m2m-tokens#client-credentials-vs-access-tokens) is chosen for M2M, _Client Credentials_ or _Access Tokens_.

Using client credentials kind – `tenantApiToken`:

```
{
"sub": "98c866a0-f7a4-4a3d-963b-ee5c144aecf8",
"tenantId": "d0d17542-7899-41a1-8f35-11a701bf1047",
"roles": [
"ReadOnly"
],
"permissions": [
"fe.connectivity.read.*",
"fe.secure.read.*"
],
"metadata": {},
"createdByUserId": "626de2ed-2bf1-4314-bd95-3bcbc232e6ef",
"type": "tenantApiToken",
"aud": "4230c66f-5558-4365-863d-737ba8ffc373",
"iss": "https://app-abcdefghijkl.frontegg.com",
"iat": 1688129655,
"exp": 1688734455
}
```

Using access token kind – `tenantAccessToken`:

```
{
"sub": "c82f30f5-56db-40dd-90ad-3e67bce17962",
"type": "tenantAccessToken",
"tenantId": "d0d17542-7899-41a1-8f35-11a701bf1047",
"roles": [
"FETCH-ROLES-BY-API"
],
"permissions": [
"FETCH-PERMISSIONS-BY-API"
],
"aud": "4230c66f-5558-4365-863d-737ba8ffc373",
"iss": "https://app-abcdefghijkl.frontegg.com",
"iat": 1688125033,
"exp": 1688128633
}
```

#### Vendor token – `vendor`

The powerful token for the entire Frontegg backoffice. Not to be used or exposed by customers.

```
{
"scopes": [],
"type": "vendor",
"vendorId": "f153bb24-132b-4e28-93e2-4184e2ec5fb7",
"iat": 1688118456,
"exp": 1688204856
}
```
25 changes: 10 additions & 15 deletions packages/frontegg-auth/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import { handleAuthorization } from './authorization.js';
* }
*
* export const main = wrap(run)
* .with(auth({ permissions: 'some-permission' }))
* .with(auth, { permissions: 'some-permission' })
* .with(helixStatus);
* ```
*
Expand Down Expand Up @@ -123,33 +123,28 @@ export async function handleAuth(request, context, opts = {}) {
}

/**
* A helix-shared-wrap middleware (wrapper function) that handles authentication
* & authorization for all requests by calling {@link handleAuth}.
* Wraps a function with a Frontegg authentication and authorization middleware.
* Invokes {@link handleAuth} for all requests.
*
* Note that this must be invoked before being passed to `wrap().with(fn)`, unlike
* other wrapper functions, in order to support passing of custom options:
* ```
* export const main = wrap(run)
* // minimal
* .with(auth());
* .with(auth);
*
* // alternatively with options
* .with(auth({ permissions: 'some-permission' }));
* .with(auth, { permissions: 'some-permission' });
* ```
*
* @param {UniversalFunction} fn the universal function
* @param {AuthOptions} [opts] Options
* @returns {function} wrapper for use with @adobe/helix-shared-wrap
* @returns {function} an universal function with the added middleware
*/
export function auth(opts = {}) {
if (typeof opts === 'function') {
throw new Error('Developer error: auth() must be invoked before being passed to wrap().with(*) and expects an opts object as argument: wrap().with(auth({}))');
}

return (func) => async (request, context) => {
export function auth(fn, opts = {}) {
return async (request, context) => {
// on auth failures, this will throw an error which helix-universal 4.2.0+
// will catch and turn into a 401 or 403 http response.
await handleAuth(request, context, opts);

return func(request, context);
return fn(request, context);
};
}
24 changes: 5 additions & 19 deletions packages/frontegg-auth/test/wrapper.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,6 @@ describe('auth wrapper', () => {
});
});

it('should throw error if auth is passed to wrap().with() without invocation', async () => {
const universalFn = async () => new Response('ok');

assert.throws(() => {
wrap(universalFn).with(auth);
});
});

it('should not throw error if auth() is invoked before passing to wrap().with()', async () => {
const universalFn = async (request, context) => {
// check that data is properly passed through
Expand All @@ -61,9 +53,7 @@ describe('auth wrapper', () => {
};

try {
const actualFn = wrap(universalFn).with(auth({
skip: true,
}));
const actualFn = wrap(universalFn).with(auth, { skip: true });

await actualFn(new Request('http://localhost?foo=bar'), {
log,
Expand All @@ -83,9 +73,7 @@ describe('auth wrapper', () => {
wentThrough = true;
return new Response('ok');
};
const actualFn = wrap(universalFn).with(auth({
skipAuthorization: true,
}));
const actualFn = wrap(universalFn).with(auth, { skipAuthorization: true });

const payload = {
tenantIds: ['123'],
Expand Down Expand Up @@ -117,9 +105,7 @@ describe('auth wrapper', () => {
wentThrough = true;
return new Response('ok');
};
const actualFn = wrap(universalFn).with(auth({
skipAuthorization: true,
}));
const actualFn = wrap(universalFn).with(auth, { skipAuthorization: true });

const payload = {
tenantIds: ['123'],
Expand Down Expand Up @@ -149,7 +135,7 @@ describe('auth wrapper', () => {
wentThrough = true;
return new Response('ok');
};
const actualFn = wrap(universalFn).with(auth());
const actualFn = wrap(universalFn).with(auth);

mockSpace('test.findmy.media', '123');
const token = createJWT({
Expand Down Expand Up @@ -182,7 +168,7 @@ describe('auth wrapper', () => {
wentThrough = true;
return new Response('ok');
};
const actualFn = wrap(universalFn).with(auth());
const actualFn = wrap(universalFn).with(auth);

mockSpace('test.findmy.media', '123');
const token = createJWT({
Expand Down
Loading