Skip to content

Commit

Permalink
Support wildcard scopes in M2M auth
Browse files Browse the repository at this point in the history
  • Loading branch information
logan-stytch committed Jun 3, 2024
1 parent 4badea0 commit 551064b
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 25 deletions.
12 changes: 6 additions & 6 deletions dist/b2c/m2m.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions dist/b2c/m2m_local.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 6 additions & 13 deletions lib/b2c/m2m.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import * as jose from "jose";
import {} from "../shared/method_options";
import { Clients } from "./m2m_clients";
import { fetchConfig } from "../shared";
import { performAuthorizationCheck } from "./m2m_local";

import { authenticateM2MJwtLocal, JwtConfig } from "../shared/sessions";
import { ClientError } from "../shared/errors";
import { request } from "../shared";

export interface M2MClient {
Expand Down Expand Up @@ -235,8 +235,8 @@ export class M2M {

// MANUAL(authenticateToken)(SERVICE_METHOD)
// ADDIMPORT: import { authenticateM2MJwtLocal, JwtConfig } from "../shared/sessions";
// ADDIMPORT: import { ClientError } from "../shared/errors";
// ADDIMPORT: import { request } from "../shared";
// ADDIMPORT: import { performAuthorizationCheck } from "./m2m_local";
// I do not know why, but it only works if we add the ADDIMPORT here, not on the ^ manual section
/**
* Authenticate an access token issued by Stytch from the Token endpoint.
Expand All @@ -260,17 +260,10 @@ export class M2M {
const scopes = scope.split(" ");

if (data.required_scopes && data.required_scopes.length > 0) {
const missingScopes = data.required_scopes.filter(
(scope) => !scopes.includes(scope)
);

if (missingScopes.length > 0) {
throw new ClientError(
"missing_scopes",
"Missing required scopes",
missingScopes
);
}
performAuthorizationCheck({
hasScopes: scopes,
requiredScopes: data.required_scopes,
});
}

return {
Expand Down
47 changes: 47 additions & 0 deletions lib/b2c/m2m_local.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ClientError } from "../shared/errors";

export function performAuthorizationCheck({
hasScopes,
requiredScopes,
}: {
hasScopes: string[];
requiredScopes: string[];
}): void {
const clientScopes: { [key: string]: Set<string> } = {};
hasScopes.forEach((scope) => {
let action = scope;
let resource = "*";
if (scope.includes(":")) {
[action, resource] = scope.split(":");
}
if (!clientScopes[action]) {
clientScopes[action] = new Set();
}
clientScopes[action].add(resource);
});

requiredScopes.forEach((requiredScope) => {
let requiredAction = requiredScope;
let requiredResource = "*";
if (requiredScope.includes(":")) {
[requiredAction, requiredResource] = requiredScope.split(":");
}
if (!clientScopes[requiredAction]) {
throw new ClientError(
"missing_scopes",
"Missing required action",
requiredAction
);
}
const resources = clientScopes[requiredAction];

// The client can either have a wildcard resource or the specific resource
if (!resources.has("*") && !resources.has(requiredResource)) {
throw new ClientError(
"missing_scopes",
"Missing required scope",
requiredResource
);
}
});
}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "stytch",
"version": "10.17.0",
"version": "10.18.0",
"description": "A wrapper for the Stytch API",
"types": "./types/lib/index.d.ts",
"main": "./dist/index.js",
Expand Down
66 changes: 63 additions & 3 deletions test/b2c/m2m.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { MOCK_FETCH_CONFIG } from "../helpers";
import { request } from "../../lib/shared";
import { M2M } from "../../lib/b2c/m2m";
import * as jose from "jose";
import { performAuthorizationCheck } from "../../lib/b2c/m2m_local";
import { ClientError } from "../../lib/shared/errors";

jest.mock("../../lib/shared");

Expand Down Expand Up @@ -142,9 +144,7 @@ describe("m2m.authenticateToken", () => {
access_token: accessToken,
required_scopes: ["write:giraffes"],
})
).rejects.toThrow(
"missing_scopes: Missing required scopes: write:giraffes"
);
).rejects.toThrow(ClientError);
});

it("fails when stale", async () => {
Expand All @@ -156,3 +156,63 @@ describe("m2m.authenticateToken", () => {
).rejects.toThrow(/jwt_too_old/);
});
});

describe("performAuthorizationCheck", () => {
test("basic", () => {
const has = ["read:users", "write:users"];
const needs = ["read:users"];

// Assertion is just that no exception is raised
expect(() =>
performAuthorizationCheck({ hasScopes: has, requiredScopes: needs })
).not.toThrow();
});

test("multiple required scopes", () => {
const has = ["read:users", "write:users", "read:books"];
const needs = ["read:users", "read:books"];

// Assertion is just that no exception is raised
expect(() =>
performAuthorizationCheck({ hasScopes: has, requiredScopes: needs })
).not.toThrow();
});

test("simple scopes", () => {
const has = ["read_users", "write_users"];
const needs = ["read_users"];

// Assertion is just that no exception is raised
expect(() =>
performAuthorizationCheck({ hasScopes: has, requiredScopes: needs })
).not.toThrow();
});

test("wildcard resource", () => {
const has = ["read:*", "write:*"];
const needs = ["read:users"];

// Assertion is just that no exception is raised
expect(() =>
performAuthorizationCheck({ hasScopes: has, requiredScopes: needs })
).not.toThrow();
});

test("missing required scope", () => {
const has = ["read:users"];
const needs = ["write:users"];

expect(() =>
performAuthorizationCheck({ hasScopes: has, requiredScopes: needs })
).toThrow(ClientError);
});

test("missing required scope with wildcard", () => {
const has = ["read:users", "write:*"];
const needs = ["delete:books"];

expect(() =>
performAuthorizationCheck({ hasScopes: has, requiredScopes: needs })
).toThrow(ClientError);
});
});
2 changes: 1 addition & 1 deletion test/b2c/sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ describeIf(
access_token,
required_scopes: ["pet:dogs"],
})
).rejects.toThrow("Missing required scopes");
).rejects.toThrow("missing_scopes");
});
});
}
Expand Down
4 changes: 4 additions & 0 deletions types/lib/b2c/m2m_local.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 551064b

Please sign in to comment.