@udibo/oauth2 0.2.0 → 0.3.1

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.
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Contract test suite for a Udibo tenant's authorization answers: what
3
- * introspection reports about a signed-in person, and what `POST /api/check`
4
- * and `POST /api/check/batch` answer.
3
+ * introspection reports about a signed-in person, what `GET /api/memberships`
4
+ * lists for them, and what `POST /api/check` and `POST /api/check/batch`
5
+ * answer.
5
6
  *
6
7
  * It runs against `createFakeTenant` from `@udibo/oauth2/testing` and against the
7
8
  * real identity service in Udibo's own repository, so the fake an app tests
@@ -50,7 +51,10 @@ export interface TenantContractFixture {
50
51
  addUser(permissions: string[]): Promise<string>;
51
52
  /** Adds an organization and returns its id. */
52
53
  addOrganization(): Promise<string>;
53
- /** Makes a person a member holding `permissions` inside that organization. */
54
+ /**
55
+ * Makes a person a `member` of an organization, holding `permissions`
56
+ * inside it.
57
+ */
54
58
  addMember(organizationId: string, userId: string, permissions: string[]): Promise<void>;
55
59
  /** Ends a membership. */
56
60
  removeMember(organizationId: string, userId: string): Promise<void>;
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Contract test suite for a Udibo tenant's authorization answers: what
3
- * introspection reports about a signed-in person, and what `POST /api/check`
4
- * and `POST /api/check/batch` answer.
3
+ * introspection reports about a signed-in person, what `GET /api/memberships`
4
+ * lists for them, and what `POST /api/check` and `POST /api/check/batch`
5
+ * answer.
5
6
  *
6
7
  * It runs against `createFakeTenant` from `@udibo/oauth2/testing` and against the
7
8
  * real identity service in Udibo's own repository, so the fake an app tests
@@ -152,6 +153,26 @@ export function runTenantContractTests(options) {
152
153
  assertEquals(sorted(claims.permissions), []);
153
154
  });
154
155
  });
156
+ describe("GET /api/memberships", () => {
157
+ it("lists the organizations the caller belongs to, and no others", async () => {
158
+ const { access_token } = await tenant.signIn(people.member);
159
+ const response = await send(new URL("/api/memberships", tenant.issuer), { headers: { authorization: `Bearer ${access_token}` } });
160
+ assertEquals(response.status, 200);
161
+ const body = await response.json();
162
+ assertEquals(body.memberships.map((membership) => membership.org_id).sort(), [organizations.home, organizations.other].sort());
163
+ for (const membership of body.memberships) {
164
+ assertEquals(membership.roles, ["member"]);
165
+ assert(membership.org_slug, "every membership names its slug");
166
+ assert(membership.name, "every membership names its organization");
167
+ }
168
+ assertEquals(body.cursor, null);
169
+ });
170
+ it("refuses a caller with no bearer token", async () => {
171
+ const response = await send(new URL("/api/memberships", tenant.issuer));
172
+ await response.body?.cancel();
173
+ assertEquals(response.status, 401);
174
+ });
175
+ });
155
176
  describe("POST /api/check", () => {
156
177
  it("answers the credential's own organization when no resource is named", async () => {
157
178
  const { access_token } = await tenant.signIn(people.member, organizations.home);
@@ -32,10 +32,15 @@ export interface FakeTenantOrganization {
32
32
  id: string;
33
33
  /** The handle `org_slug` carries. Never an authorization key. */
34
34
  slug: string;
35
+ /** The display name `/api/memberships` reports. Defaults to the slug. */
36
+ name?: string;
35
37
  }
36
38
  /** A person's place in one organization. */
37
39
  export interface FakeTenantMembership {
38
- /** The role slugs `org_roles` reports. */
40
+ /**
41
+ * The membership's roles, as `org_roles` and `/api/memberships` report
42
+ * them — `member`, `admin` or `owner` on a real tenant.
43
+ */
39
44
  roles?: string[];
40
45
  /** Permissions held inside this organization only. */
41
46
  permissions?: string[];
@@ -99,9 +104,10 @@ export interface FakeTenantOptions {
99
104
  /**
100
105
  * A stand-in for a Udibo tenant, for testing an app that signs in against one
101
106
  * without running the identity service. It answers the tenant's protocol
102
- * surface — discovery, authorize, token, introspection, UserInfo, JWKS — and
103
- * the two authorization questions, `POST /api/check` and
104
- * `POST /api/check/batch`, with the same shapes a real tenant uses.
107
+ * surface — discovery, authorize, token, introspection, UserInfo, JWKS — the
108
+ * caller's own `GET /api/memberships`, and the two authorization questions,
109
+ * `POST /api/check` and `POST /api/check/batch`, with the same shapes a real
110
+ * tenant uses.
105
111
  *
106
112
  * Nothing here is interactive: {@linkcode signInAs} decides who the next
107
113
  * authorization request authenticates, and there is no consent step. Serve
@@ -277,6 +277,22 @@ export async function createFakeTenant(options) {
277
277
  }
278
278
  return organization.id;
279
279
  };
280
+ const memberships = async (request) => {
281
+ const caller = await authenticated(request);
282
+ if (caller instanceof Response)
283
+ return caller;
284
+ return Response.json({
285
+ memberships: [...organizations.values()]
286
+ .filter((organization) => organization.members.has(caller.user.id))
287
+ .map((organization) => ({
288
+ org_id: organization.id,
289
+ org_slug: organization.slug,
290
+ name: organization.name ?? organization.slug,
291
+ roles: organization.members.get(caller.user.id).roles ?? [],
292
+ })),
293
+ cursor: null,
294
+ });
295
+ };
280
296
  const authorize = async (request) => {
281
297
  const chosen = signedIn;
282
298
  const requested = new URL(request.url).searchParams.get("organization");
@@ -327,6 +343,7 @@ export async function createFakeTenant(options) {
327
343
  [`GET ${PATHS.userinfo}`]: (r) => server.handleUserInfoRequest(r),
328
344
  [`POST ${PATHS.userinfo}`]: (r) => server.handleUserInfoRequest(r),
329
345
  [`GET ${PATHS.jwks}`]: (r) => server.handleJwksRequest(r),
346
+ "GET /api/memberships": memberships,
330
347
  "POST /api/check": check,
331
348
  "POST /api/check/batch": checkBatch,
332
349
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@udibo/oauth2",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "A flexible OAuth2 toolkit: framework-agnostic authorization & resource servers, a typed client, Hono adapters (including a backend-for-frontend), and React bindings.",
5
5
  "repository": {
6
6
  "type": "git",