Skip to content
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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,36 @@ async function main() {
}
```

## Fine-grained authorization (FGA)

Authorizer supports resource:scope based fine-grained permissions. The SDK exposes them in two ways.

**1. Assert required permissions while validating** — pass `required_permissions` to `getSession`, `validateJWTToken` or `validateSession`. They are evaluated with AND semantics: every entry must be granted, otherwise the result is unauthorized.

```js
const { data } = await authRef.validateJWTToken({
token_type: 'access_token',
token,
required_permissions: [
{ resource: 'documents', scope: 'read' },
{ resource: 'documents', scope: 'write' },
],
});

if (!data?.is_valid) {
// unauthorized
}
```

**2. Fetch the principal's granted permissions** — `getPermissions` returns the resource:scope permissions for the authenticated principal. It uses the session cookie by default; in node.js pass the authorization header.

```js
const { data: permissions } = await authRef.getPermissions({
Authorization: `Bearer ${token}`,
});
// permissions => [{ resource: 'documents', scope: 'read' }, ...]
```

## Local Development Setup

### Prerequisites
Expand Down
25 changes: 25 additions & 0 deletions __test__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,31 @@ describe('Integration Tests - authorizer-js', () => {
expect(validateRes?.data?.is_valid).toEqual(true);
});

it('should mark token invalid when required_permissions are missing', async () => {
expect(loginRes?.data?.access_token).toBeDefined();
expect(loginRes?.data?.access_token).not.toBeNull();
// A new user lacks the documents:read permission, so asserting it via
// required_permissions (AND semantics) must mark the token as not valid.
const validateRes = await authorizer.validateJWTToken({
token_type: 'access_token',
token: loginRes?.data?.access_token || '',
required_permissions: [{ resource: 'documents', scope: 'read' }],
});
expect(validateRes?.errors).toHaveLength(0);
expect(validateRes?.data?.is_valid).toEqual(false);
});

it('should fetch permissions for the authenticated user', async () => {
expect(loginRes?.data?.access_token).toBeDefined();
expect(loginRes?.data?.access_token).not.toBeNull();
const permissionsRes = await authorizer.getPermissions({
Authorization: `Bearer ${loginRes?.data?.access_token}`,
});
expect(permissionsRes?.errors).toHaveLength(0);
// A freshly signed up user has no fine-grained permissions assigned.
expect(permissionsRes?.data).toEqual([]);
});

it('should update profile successfully', async () => {
expect(loginRes?.data?.access_token).toBeDefined();
expect(loginRes?.data?.access_token).not.toBeNull();
Expand Down
21 changes: 21 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,27 @@ export class Authorizer {
}
};

// fetch the fine-grained resource:scope permissions granted to the
// authenticated principal. Uses the session cookie by default; when running
// in node.js pass the authorization header.
getPermissions = async (
headers?: Types.Headers,
): Promise<Types.ApiResponse<Types.Permission[]>> => {
try {
const res = await this.graphqlQuery({
query: 'query permissions { permissions { resource scope } }',
headers,
operationName: 'permissions',
});

return res?.errors?.length
? this.errorResponse(res.errors)
: this.okResponse(res.data?.permissions);
} catch (error) {
return this.errorResponse([error]);
}
};

// this is used to verify / get session using cookie by default. If using node.js pass authorization header
getSession = async (
headers?: Types.Headers,
Expand Down
19 changes: 19 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,10 +285,27 @@ export interface DeleteUserRequest {
email: string;
}

// Fine-grained authorization (FGA) types
// PermissionInput is a resource:scope pair asserted as a required permission.
// Required permissions are evaluated with AND semantics — every entry must be
// granted, otherwise the principal is treated as unauthorized.
export interface PermissionInput {
resource: string;
scope: string;
}

// Permission is a resource:scope permission granted to a principal,
// returned by the permissions query.
export interface Permission {
resource: string;
scope: string;
}

// SessionQueryRequest
export interface SessionQueryRequest {
roles?: string[] | null;
scope?: string[] | null;
required_permissions?: PermissionInput[] | null;
}

// Keep SessionQueryInput as alias for backward compatibility
Expand All @@ -299,6 +316,7 @@ export interface ValidateJWTTokenRequest {
token_type: string;
token: string;
roles?: string[] | null;
required_permissions?: PermissionInput[] | null;
}

// Keep ValidateJWTTokenInput as alias for backward compatibility
Expand All @@ -314,6 +332,7 @@ export interface ValidateJWTTokenResponse {
export interface ValidateSessionRequest {
cookie: string;
roles?: string[] | null;
required_permissions?: PermissionInput[] | null;
}

// Keep ValidateSessionInput as alias for backward compatibility
Expand Down
Loading