@ogcio/api-auth 4.29.0

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.
@@ -0,0 +1,89 @@
1
+ import { expect, test } from "vitest";
2
+ import {
3
+ type ScopeMap,
4
+ getMapFromScope,
5
+ validatePermission,
6
+ } from "../src/utils.js";
7
+
8
+ const mapFromScopeTestCases: {
9
+ description: string;
10
+ scope: string;
11
+ expected: ScopeMap;
12
+ }[] = [
13
+ {
14
+ description: "getMapFromScope: Single scope",
15
+ scope: "payments:payment:create",
16
+ expected: new Map([
17
+ ["payments", new Map([["payment", new Map([["create", true]])]])],
18
+ ]),
19
+ },
20
+ {
21
+ description: "getMapFromScope: Single scope with wildcard",
22
+ scope: "payments:payment:*",
23
+ expected: new Map([["payments", new Map([["payment", true]])]]),
24
+ },
25
+ {
26
+ description: "getMapFromScope: Multiple scopes",
27
+ scope: "payments:payment:create payments:transaction:read",
28
+ expected: new Map([
29
+ [
30
+ "payments",
31
+ new Map([
32
+ ["payment", new Map([["create", true]])],
33
+ ["transaction", new Map([["read", true]])],
34
+ ]),
35
+ ],
36
+ ]),
37
+ },
38
+ {
39
+ description: "getMapFromScope: Multiple scopes with resource wildcard",
40
+ scope: "payments:payment:create payments:* payments:transaction:read",
41
+ expected: new Map([["payments", true]]),
42
+ },
43
+ {
44
+ description: "getMapFromScope: Multiple scopes with action wildcard",
45
+ scope:
46
+ "payments:payment:create payments:transaction:* payments:transaction:read",
47
+ expected: new Map([
48
+ [
49
+ "payments",
50
+ new Map([
51
+ // biome-ignore lint/suspicious/noExplicitAny: <explanation>
52
+ ["payment", new Map([["create", true]]) as any],
53
+ ["transaction", true],
54
+ ]),
55
+ ],
56
+ ]),
57
+ },
58
+ ];
59
+
60
+ for (const { description, scope, expected } of mapFromScopeTestCases) {
61
+ test(description, () => {
62
+ const result = getMapFromScope(scope);
63
+ expect(result).toStrictEqual(expected);
64
+ });
65
+ }
66
+
67
+ test("validatePermission: passes with wildcard scope", () => {
68
+ const scope = "payments:payment:create payments:* payments:transaction:read";
69
+ const permission = "payments:transaction:read";
70
+
71
+ const result = validatePermission(permission, getMapFromScope(scope));
72
+ expect(result).toBe(true);
73
+ });
74
+
75
+ test("validatePermission: does not pass", () => {
76
+ const scope = "payments:payment:create payments:transaction:read";
77
+ const permission = "payments:transaction:create";
78
+
79
+ const result = validatePermission(permission, getMapFromScope(scope));
80
+ expect(result).toBe(false);
81
+ });
82
+
83
+ test("validatePermission: passes with exact match", () => {
84
+ const scope = "payments:payment:create payments:transaction:read";
85
+ const permission = "payments:transaction:read";
86
+
87
+ const result = validatePermission(permission, getMapFromScope(scope));
88
+ expect(result).toBe(true);
89
+ });
@@ -0,0 +1,29 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ type ExtractedUserData = {
3
+ userId: string;
4
+ organizationId?: string;
5
+ isM2MApplication: boolean;
6
+ accessToken: string;
7
+ };
8
+ declare module "fastify" {
9
+ interface FastifyRequest {
10
+ userData?: ExtractedUserData;
11
+ }
12
+ }
13
+ export declare const ensureUserCanAccessUser: (loggedUserData: ExtractedUserData | undefined, requestedUserId: string) => ExtractedUserData;
14
+ export declare const checkPermissions: (authHeader: string, config: {
15
+ jwkEndpoint: string;
16
+ oidcEndpoint: string;
17
+ }, requiredPermissions: string[], matchConfig?: {
18
+ method: string;
19
+ }) => Promise<ExtractedUserData>;
20
+ export type CheckPermissionsPluginOpts = {
21
+ jwkEndpoint: string;
22
+ oidcEndpoint: string;
23
+ };
24
+ export declare const checkPermissionsPlugin: (app: FastifyInstance, opts: CheckPermissionsPluginOpts) => Promise<void>;
25
+ declare const _default: (app: FastifyInstance, opts: CheckPermissionsPluginOpts) => Promise<void>;
26
+ export default _default;
27
+ export * from "./logto-client/index.js";
28
+ export * from "./jwtService.js";
29
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAK7E,KAAK,iBAAiB,GAAG;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAIF,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,cAAc;QACtB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;KAC9B;CACF;AA2BD,eAAO,MAAM,uBAAuB,mBAClB,iBAAiB,GAAG,SAAS,mBAC5B,MAAM,KACtB,iBAUF,CAAC;AAEF,eAAO,MAAM,gBAAgB,eACf,MAAM,UACV;IACN,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB,uBACoB,MAAM,EAAE;;MAE5B,OAAO,CAAC,iBAAiB,CAmC3B,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,eAAO,MAAM,sBAAsB,QAC5B,eAAe,QACd,0BAA0B,kBA2BjC,CAAC;8BA5BK,eAAe,QACd,0BAA0B;AA6BlC,wBAEG;AAEH,cAAc,yBAAyB,CAAC;AACxC,cAAc,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,71 @@
1
+ import { httpErrors } from "@fastify/sensible";
2
+ import { getErrorMessage } from "@ogcio/shared-errors";
3
+ import fp from "fastify-plugin";
4
+ import { createRemoteJWKSet, jwtVerify } from "jose";
5
+ import { getMapFromScope, validatePermission } from "./utils.js";
6
+ const extractBearerToken = (authHeader) => {
7
+ const [type, token] = authHeader.split(" ");
8
+ if (type !== "Bearer") {
9
+ throw httpErrors.unauthorized("Invalid Authorization header type, 'Bearer' expected");
10
+ }
11
+ return token;
12
+ };
13
+ const decodeLogtoToken = async (token, config) => {
14
+ // Reference: https://docs.logto.io/docs/recipes/protect-your-api/node/
15
+ const jwks = createRemoteJWKSet(new URL(config.jwkEndpoint));
16
+ const { payload } = await jwtVerify(token, jwks, {
17
+ issuer: config.oidcEndpoint,
18
+ });
19
+ return payload;
20
+ };
21
+ export const ensureUserCanAccessUser = (loggedUserData, requestedUserId) => {
22
+ if (loggedUserData && requestedUserId === loggedUserData.userId) {
23
+ return loggedUserData;
24
+ }
25
+ if (loggedUserData?.organizationId) {
26
+ return loggedUserData;
27
+ }
28
+ throw httpErrors.forbidden("You can't access this user's data");
29
+ };
30
+ export const checkPermissions = async (authHeader, config, requiredPermissions, matchConfig = { method: "OR" }) => {
31
+ const token = extractBearerToken(authHeader);
32
+ const payload = await decodeLogtoToken(token, config);
33
+ const { scope, sub, aud, client_id: clientId, } = payload;
34
+ const scopesMap = getMapFromScope(scope);
35
+ const grantAccess = matchConfig.method === "AND"
36
+ ? requiredPermissions.every((p) => validatePermission(p, scopesMap))
37
+ : requiredPermissions.some((p) => validatePermission(p, scopesMap));
38
+ if (!grantAccess) {
39
+ throw httpErrors.forbidden();
40
+ }
41
+ const organizationId = aud.includes("urn:logto:organization:")
42
+ ? aud.split("urn:logto:organization:")[1]
43
+ : undefined;
44
+ return {
45
+ userId: sub,
46
+ organizationId: organizationId,
47
+ accessToken: token,
48
+ isM2MApplication: sub === clientId,
49
+ };
50
+ };
51
+ export const checkPermissionsPlugin = async (app, opts) => {
52
+ app.decorate("checkPermissions", async (req, _rep, permissions, matchConfig) => {
53
+ const authHeader = req.headers.authorization;
54
+ if (!authHeader) {
55
+ throw httpErrors.unauthorized();
56
+ }
57
+ try {
58
+ const userData = await checkPermissions(authHeader, opts, permissions, matchConfig);
59
+ req.userData = userData;
60
+ }
61
+ catch (e) {
62
+ throw httpErrors.createError(403, getErrorMessage(e), { parent: e });
63
+ }
64
+ });
65
+ };
66
+ export default fp(checkPermissionsPlugin, {
67
+ name: "apiAuthPlugin",
68
+ });
69
+ export * from "./logto-client/index.js";
70
+ export * from "./jwtService.js";
71
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { type KMSClientConfig } from "@aws-sdk/client-kms";
2
+ import { type JWK, type JWTPayload } from "jose";
3
+ interface JWTOptions {
4
+ expirationTime?: string;
5
+ audience?: string;
6
+ issuer?: string;
7
+ }
8
+ /**
9
+ * Creates and signs a JWT using the provided payload and private key.
10
+ * @param payload - The JWT payload as an object.
11
+ * @param keyId - The key id or alias in KMS
12
+ * @param options - Optional parameters for customizing the JWT creation (e.g., expiration, audience, issuer).
13
+ */
14
+ declare function createSignedJWT(payload: Record<string, unknown>, keyId: string, options: JWTOptions, kmsConfig: KMSClientConfig): Promise<string>;
15
+ interface VerifyJWTOptions {
16
+ jwksUrl: string;
17
+ audience?: string;
18
+ issuer?: string;
19
+ }
20
+ /**
21
+ * Verifies the given JWT using the JWKS provided by the remote JWKS URL.
22
+ * @param token - The JWT token to verify.
23
+ * @param options - Object containing JWKS URL, expected audience, issuer, and algorithm.
24
+ */
25
+ declare function verifyJWT(token: string, options: VerifyJWTOptions): Promise<JWTPayload>;
26
+ /**
27
+ * Retrieves a public key from KMS and returns the JWKS (JSON Web Key Set) for the public key.
28
+ * @param keyId - The key id or alias in KMS.
29
+ */
30
+ declare function getJWKS(keyId: string, kmsConfig: KMSClientConfig): Promise<{
31
+ keys: JWK[];
32
+ }>;
33
+ declare function getIssuerFromJWT(token: string): string | undefined;
34
+ declare function unsecureVerifyJWT(token: string): JWTPayload;
35
+ export { createSignedJWT, verifyJWT, getJWKS, getIssuerFromJWT, unsecureVerifyJWT, };
36
+ //# sourceMappingURL=jwtService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwtService.d.ts","sourceRoot":"","sources":["../src/jwtService.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,eAAe,EAErB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,KAAK,GAAG,EACR,KAAK,UAAU,EAOhB,MAAM,MAAM,CAAC;AAed,UAAU,UAAU;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,iBAAe,eAAe,CAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,eAAe,mBAkC3B;AASD,UAAU,gBAAgB;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,iBAAe,SAAS,CACtB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,UAAU,CAAC,CAsBrB;AAED;;;GAGG;AACH,iBAAe,OAAO,CACpB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,eAAe,GACzB,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,EAAE,CAAA;CAAE,CAAC,CAa1B;AAGD,iBAAS,gBAAgB,CAAC,KAAK,EAAE,MAAM,sBAEtC;AAED,iBAAS,iBAAiB,CAAC,KAAK,EAAE,MAAM,cAEvC;AAED,OAAO,EACL,eAAe,EACf,SAAS,EACT,OAAO,EACP,gBAAgB,EAChB,iBAAiB,GAClB,CAAC"}
@@ -0,0 +1,96 @@
1
+ import { GetPublicKeyCommand, KMSClient, SignCommand, } from "@aws-sdk/client-kms";
2
+ import { createRemoteJWKSet, decodeJwt, exportJWK, importSPKI, jwtVerify, } from "jose";
3
+ const getKmsClient = (() => {
4
+ let kmsClient = null;
5
+ return function getKmsClient(kmsConfig) {
6
+ if (kmsClient)
7
+ return kmsClient;
8
+ kmsClient = new KMSClient(kmsConfig);
9
+ return kmsClient;
10
+ };
11
+ })();
12
+ const defaultAlgorithm = "RS256";
13
+ /**
14
+ * Creates and signs a JWT using the provided payload and private key.
15
+ * @param payload - The JWT payload as an object.
16
+ * @param keyId - The key id or alias in KMS
17
+ * @param options - Optional parameters for customizing the JWT creation (e.g., expiration, audience, issuer).
18
+ */
19
+ async function createSignedJWT(payload, keyId, options, kmsConfig) {
20
+ const { audience: aud, issuer: iss } = options;
21
+ const header = {
22
+ alg: defaultAlgorithm,
23
+ typ: "JWT",
24
+ };
25
+ // we need to create the jwt manually and sign it directly on KMS - private keys cannot be retrieved
26
+ const payloadString = JSON.stringify({ ...payload, aud, iss });
27
+ const encodedHeader = Buffer.from(JSON.stringify(header)).toString("base64url");
28
+ const encodedPayload = Buffer.from(payloadString).toString("base64url");
29
+ const messageToSign = `${encodedHeader}.${encodedPayload}`;
30
+ const input = {
31
+ KeyId: keyId,
32
+ Message: Buffer.from(messageToSign),
33
+ MessageType: "RAW",
34
+ SigningAlgorithm: "RSASSA_PKCS1_V1_5_SHA_256",
35
+ };
36
+ const command = new SignCommand(input);
37
+ const signResponse = await getKmsClient(kmsConfig).send(command);
38
+ if (!signResponse.Signature) {
39
+ throw new Error("KMS did not return a signature. Signing failed.");
40
+ }
41
+ const signature = Buffer.from(signResponse.Signature).toString("base64url");
42
+ return `${messageToSign}.${signature}`;
43
+ }
44
+ /**
45
+ * Returns the JWKS (JSON Web Key Set) for the public key.
46
+ */
47
+ async function getJWKSRoute(publicKey) {
48
+ return { keys: [publicKey] };
49
+ }
50
+ /**
51
+ * Verifies the given JWT using the JWKS provided by the remote JWKS URL.
52
+ * @param token - The JWT token to verify.
53
+ * @param options - Object containing JWKS URL, expected audience, issuer, and algorithm.
54
+ */
55
+ async function verifyJWT(token, options) {
56
+ const { jwksUrl, audience, issuer } = options;
57
+ // Create the remote JWK set from the given URL
58
+ const JWKS = createRemoteJWKSet(new URL(jwksUrl));
59
+ // Define verification options
60
+ const verifyOptions = {
61
+ algorithms: [defaultAlgorithm],
62
+ audience, // Check if token is for the expected audience
63
+ issuer, // Check if token was issued by the expected issuer
64
+ };
65
+ // Verify the JWT using the remote JWKS
66
+ const { payload } = await jwtVerify(token, JWKS, verifyOptions);
67
+ // Check if the token has expired by verifying the `exp` claim
68
+ if (payload.exp && payload.exp * 1000 < Date.now()) {
69
+ throw new Error("Token has expired");
70
+ }
71
+ return payload;
72
+ }
73
+ /**
74
+ * Retrieves a public key from KMS and returns the JWKS (JSON Web Key Set) for the public key.
75
+ * @param keyId - The key id or alias in KMS.
76
+ */
77
+ async function getJWKS(keyId, kmsConfig) {
78
+ const command = new GetPublicKeyCommand({ KeyId: keyId });
79
+ const { PublicKey } = await getKmsClient(kmsConfig).send(command);
80
+ if (!PublicKey)
81
+ throw new Error("KMS did not return a public key. Retrieval failed.");
82
+ // convert Uint8Array to JWK
83
+ const spki = `-----BEGIN PUBLIC KEY-----\n${Buffer.from(PublicKey).toString("base64")}\n-----END PUBLIC KEY-----`;
84
+ const keyObject = await importSPKI(spki, "RS256");
85
+ const jwk = await exportJWK(keyObject);
86
+ return getJWKSRoute(jwk);
87
+ }
88
+ // Function to decode JWT without validation
89
+ function getIssuerFromJWT(token) {
90
+ return decodeJwt(token).iss;
91
+ }
92
+ function unsecureVerifyJWT(token) {
93
+ return decodeJwt(token);
94
+ }
95
+ export { createSignedJWT, verifyJWT, getJWKS, getIssuerFromJWT, unsecureVerifyJWT, };
96
+ //# sourceMappingURL=jwtService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwtService.js","sourceRoot":"","sources":["../src/jwtService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ interface GetTokenBaseParams {
2
+ logtoOidcEndpoint: string;
3
+ applicationId: string;
4
+ applicationSecret: string;
5
+ scopes: string[];
6
+ }
7
+ export interface GetOrganizationTokenParams extends GetTokenBaseParams {
8
+ organizationId: string;
9
+ }
10
+ export interface GetAccessTokenParams extends GetTokenBaseParams {
11
+ resource: string;
12
+ }
13
+ export declare const getOrganizationToken: (params: GetOrganizationTokenParams) => Promise<string>;
14
+ export declare const getAccessToken: (params: GetAccessTokenParams) => Promise<string>;
15
+ export {};
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/logto-client/index.ts"],"names":[],"mappings":"AAGA,UAAU,kBAAkB;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AACD,MAAM,WAAW,0BAA2B,SAAQ,kBAAkB;IACpE,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,oBAAqB,SAAQ,kBAAkB;IAC9D,QAAQ,EAAE,MAAM,CAAC;CAClB;AASD,eAAO,MAAM,oBAAoB,WACvB,0BAA0B,KACjC,OAAO,CAAC,MAAM,CAYhB,CAAC;AAEF,eAAO,MAAM,cAAc,WACjB,oBAAoB,KAC3B,OAAO,CAAC,MAAM,CAOhB,CAAC"}
@@ -0,0 +1,46 @@
1
+ import { httpErrors } from "@fastify/sensible";
2
+ import { UserScope } from "./user-scope.js";
3
+ export const getOrganizationToken = async (params) => {
4
+ const tokenResponse = await fetchToken({
5
+ ...params,
6
+ scopes: [
7
+ ...params.scopes,
8
+ UserScope.OrganizationRoles,
9
+ UserScope.Organizations,
10
+ ],
11
+ specificBodyFields: { organization_id: params.organizationId },
12
+ });
13
+ //TODO store here
14
+ return tokenResponse.access_token;
15
+ };
16
+ export const getAccessToken = async (params) => {
17
+ const tokenResponse = await fetchToken({
18
+ ...params,
19
+ specificBodyFields: { resource: params.resource },
20
+ });
21
+ //TODO store here
22
+ return tokenResponse.access_token;
23
+ };
24
+ const fetchToken = async (params) => {
25
+ const body = {
26
+ ...params.specificBodyFields,
27
+ scope: params.scopes.join(" "),
28
+ grant_type: "client_credentials",
29
+ };
30
+ const logtoOidcEndpoint = params.logtoOidcEndpoint.endsWith("/")
31
+ ? params.logtoOidcEndpoint
32
+ : `${params.logtoOidcEndpoint}/`;
33
+ const response = await fetch(`${logtoOidcEndpoint}token`, {
34
+ method: "POST",
35
+ headers: {
36
+ "Content-Type": "application/x-www-form-urlencoded",
37
+ Authorization: `Basic ${Buffer.from(`${params.applicationId}:${params.applicationSecret}`).toString("base64")}`,
38
+ },
39
+ body: new URLSearchParams(body).toString(),
40
+ });
41
+ if (response.status !== 200) {
42
+ throw httpErrors.unauthorized(JSON.stringify(await response.json()));
43
+ }
44
+ return response.json();
45
+ };
46
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/logto-client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAuB5C,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,EACvC,MAAkC,EACjB,EAAE;IACnB,MAAM,aAAa,GAAG,MAAM,UAAU,CAAC;QACrC,GAAG,MAAM;QACT,MAAM,EAAE;YACN,GAAG,MAAM,CAAC,MAAM;YAChB,SAAS,CAAC,iBAAiB;YAC3B,SAAS,CAAC,aAAa;SACxB;QACD,kBAAkB,EAAE,EAAE,eAAe,EAAE,MAAM,CAAC,cAAc,EAAE;KAC/D,CAAC,CAAC;IACH,iBAAiB;IACjB,OAAO,aAAa,CAAC,YAAY,CAAC;AACpC,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EACjC,MAA4B,EACX,EAAE;IACnB,MAAM,aAAa,GAAG,MAAM,UAAU,CAAC;QACrC,GAAG,MAAM;QACT,kBAAkB,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE;KAClD,CAAC,CAAC;IACH,iBAAiB;IACjB,OAAO,aAAa,CAAC,YAAY,CAAC;AACpC,CAAC,CAAC;AAEF,MAAM,UAAU,GAAG,KAAK,EAAE,MAMzB,EAA8B,EAAE;IAC/B,MAAM,IAAI,GAAG;QACX,GAAG,MAAM,CAAC,kBAAkB;QAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;QAC9B,UAAU,EAAE,oBAAoB;KACjC,CAAC;IACF,MAAM,iBAAiB,GAAG,MAAM,CAAC,iBAAiB,CAAC,QAAQ,CAAC,GAAG,CAAC;QAC9D,CAAC,CAAC,MAAM,CAAC,iBAAiB;QAC1B,CAAC,CAAC,GAAG,MAAM,CAAC,iBAAiB,GAAG,CAAC;IACnC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,iBAAiB,OAAO,EAAE;QACxD,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,cAAc,EAAE,mCAAmC;YACnD,aAAa,EAAE,SAAS,MAAM,CAAC,IAAI,CACjC,GAAG,MAAM,CAAC,aAAa,IAAI,MAAM,CAAC,iBAAiB,EAAE,CACtD,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE;SACvB;QACD,IAAI,EAAE,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE;KAC3C,CAAC,CAAC;IAEH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACvE,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,EAAgC,CAAC;AACvD,CAAC,CAAC"}
@@ -0,0 +1,11 @@
1
+ export declare enum UserScope {
2
+ Profile = "profile",
3
+ Email = "email",
4
+ Phone = "phone",
5
+ CustomData = "custom_data",
6
+ Identities = "identities",
7
+ Roles = "roles",
8
+ Organizations = "urn:logto:scope:organizations",
9
+ OrganizationRoles = "urn:logto:scope:organization_roles"
10
+ }
11
+ //# sourceMappingURL=user-scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-scope.d.ts","sourceRoot":"","sources":["../../src/logto-client/user-scope.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,MAAM,SAAS;IAC3B,OAAO,YAAY;IACnB,KAAK,UAAU;IACf,KAAK,UAAU;IACf,UAAU,gBAAgB;IAC1B,UAAU,eAAe;IACzB,KAAK,UAAU;IACf,aAAa,kCAAkC;IAC/C,iBAAiB,uCAAuC;CACzD"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=user-scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-scope.js","sourceRoot":"","sources":["../../src/logto-client/user-scope.ts"],"names":[],"mappings":""}
@@ -0,0 +1,4 @@
1
+ export type ScopeMap = Map<string, ScopeMap | boolean>;
2
+ export declare const getMapFromScope: (scope?: string) => Map<any, any>;
3
+ export declare const validatePermission: (permission: string, scope: ScopeMap) => boolean | undefined;
4
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC;AAEvD,eAAO,MAAM,eAAe,WAAY,MAAM,kBAiC7C,CAAC;AAEF,eAAO,MAAM,kBAAkB,eAAgB,MAAM,SAAS,QAAQ,wBAgBrE,CAAC"}
package/dist/utils.js ADDED
@@ -0,0 +1,45 @@
1
+ export const getMapFromScope = (scope) => {
2
+ if (!scope || scope.length === 0) {
3
+ return new Map();
4
+ }
5
+ const scopes = scope.split(" ");
6
+ return scopes.reduce((acc, scope) => {
7
+ const subScope = scope.split(":");
8
+ let current = acc;
9
+ for (let i = 0; i < subScope.length; i++) {
10
+ const part = subScope[i];
11
+ if (current === true)
12
+ break;
13
+ if (current instanceof Map) {
14
+ if (subScope[i + 1] === "*") {
15
+ current.set(part, true);
16
+ break;
17
+ }
18
+ if (i === subScope.length - 1) {
19
+ current.set(part, true);
20
+ }
21
+ else if (!current.get(part)) {
22
+ current.set(part, new Map());
23
+ }
24
+ current = current.get(part);
25
+ }
26
+ }
27
+ return acc;
28
+ }, new Map());
29
+ };
30
+ export const validatePermission = (permission, scope) => {
31
+ const parts = permission.split(":");
32
+ let current = scope;
33
+ for (let i = 0; i <= parts.length; i++) {
34
+ const part = parts[i];
35
+ if (current === true)
36
+ return true;
37
+ if (current instanceof Map) {
38
+ current = current.get(part);
39
+ }
40
+ else {
41
+ return false;
42
+ }
43
+ }
44
+ };
45
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,KAAc,EAAE,EAAE;IAChD,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,OAAO,IAAI,GAAG,EAAE,CAAC;IACnB,CAAC;IAED,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAChC,OAAO,MAAM,CAAC,MAAM,CAAW,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;QAC5C,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,OAAO,GAAmC,GAAG,CAAC;QAElD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACzC,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;YAEzB,IAAI,OAAO,KAAK,IAAI;gBAAE,MAAM;YAE5B,IAAI,OAAO,YAAY,GAAG,EAAE,CAAC;gBAC3B,IAAI,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;oBAC5B,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;oBACxB,MAAM;gBACR,CAAC;gBAED,IAAI,CAAC,KAAK,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC9B,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;gBAC1B,CAAC;qBAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC9B,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;gBAC/B,CAAC;gBAED,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC9B,CAAC;QACH,CAAC;QAED,OAAO,GAAG,CAAC;IACb,CAAC,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;AAChB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,UAAkB,EAAE,KAAe,EAAE,EAAE;IACxE,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAEpC,IAAI,OAAO,GAAmC,KAAK,CAAC;IAEpD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QAEtB,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAElC,IAAI,OAAO,YAAY,GAAG,EAAE,CAAC;YAC3B,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC9B,CAAC;aAAM,CAAC;YACN,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;AACH,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@ogcio/api-auth",
3
+ "version": "4.29.0",
4
+ "main": "dist/index.js",
5
+ "type": "module",
6
+ "dependencies": {
7
+ "@aws-sdk/client-kms": "^3.709.0",
8
+ "@fastify/sensible": "5.6.0",
9
+ "@ogcio/shared-errors": "1.0.0",
10
+ "fastify": "^4.29.0",
11
+ "fastify-plugin": "^4.5.1",
12
+ "jose": "^5.9.6"
13
+ },
14
+ "scripts": {
15
+ "build": "rm -rf dist tsconfig.prod.tsbuildinfo tsconfig.tsbuildinfo && tsc -p tsconfig.prod.json",
16
+ "test": "vitest run --coverage --outputFile=results.xml",
17
+ "prepublishOnly": "npm i && npm run build && npm run test"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "22.10.1",
21
+ "typescript": "^5.7.2"
22
+ }
23
+ }
package/src/index.ts ADDED
@@ -0,0 +1,149 @@
1
+ import { httpErrors } from "@fastify/sensible";
2
+ import { getErrorMessage } from "@ogcio/shared-errors";
3
+ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
4
+ import fp from "fastify-plugin";
5
+ import { createRemoteJWKSet, jwtVerify } from "jose";
6
+ import { getMapFromScope, validatePermission } from "./utils.js";
7
+
8
+ type ExtractedUserData = {
9
+ userId: string;
10
+ organizationId?: string;
11
+ isM2MApplication: boolean;
12
+ accessToken: string;
13
+ };
14
+
15
+ type MatchConfig = { method: "AND" | "OR" };
16
+
17
+ declare module "fastify" {
18
+ interface FastifyRequest {
19
+ userData?: ExtractedUserData;
20
+ }
21
+ }
22
+
23
+ const extractBearerToken = (authHeader: string) => {
24
+ const [type, token] = authHeader.split(" ");
25
+ if (type !== "Bearer") {
26
+ throw httpErrors.unauthorized(
27
+ "Invalid Authorization header type, 'Bearer' expected",
28
+ );
29
+ }
30
+ return token;
31
+ };
32
+
33
+ const decodeLogtoToken = async (
34
+ token: string,
35
+ config: {
36
+ jwkEndpoint: string;
37
+ oidcEndpoint: string;
38
+ },
39
+ ) => {
40
+ // Reference: https://docs.logto.io/docs/recipes/protect-your-api/node/
41
+ const jwks = createRemoteJWKSet(new URL(config.jwkEndpoint));
42
+ const { payload } = await jwtVerify(token, jwks, {
43
+ issuer: config.oidcEndpoint,
44
+ });
45
+ return payload;
46
+ };
47
+
48
+ export const ensureUserCanAccessUser = (
49
+ loggedUserData: ExtractedUserData | undefined,
50
+ requestedUserId: string,
51
+ ): ExtractedUserData => {
52
+ if (loggedUserData && requestedUserId === loggedUserData.userId) {
53
+ return loggedUserData;
54
+ }
55
+
56
+ if (loggedUserData?.organizationId) {
57
+ return loggedUserData;
58
+ }
59
+
60
+ throw httpErrors.forbidden("You can't access this user's data");
61
+ };
62
+
63
+ export const checkPermissions = async (
64
+ authHeader: string,
65
+ config: {
66
+ jwkEndpoint: string;
67
+ oidcEndpoint: string;
68
+ },
69
+ requiredPermissions: string[],
70
+ matchConfig = { method: "OR" },
71
+ ): Promise<ExtractedUserData> => {
72
+ const token = extractBearerToken(authHeader);
73
+ const payload = await decodeLogtoToken(token, config);
74
+ const {
75
+ scope,
76
+ sub,
77
+ aud,
78
+ client_id: clientId,
79
+ } = payload as {
80
+ scope: string;
81
+ sub: string;
82
+ aud: string;
83
+ client_id: string;
84
+ };
85
+ const scopesMap = getMapFromScope(scope);
86
+
87
+ const grantAccess =
88
+ matchConfig.method === "AND"
89
+ ? requiredPermissions.every((p) => validatePermission(p, scopesMap))
90
+ : requiredPermissions.some((p) => validatePermission(p, scopesMap));
91
+
92
+ if (!grantAccess) {
93
+ throw httpErrors.forbidden();
94
+ }
95
+
96
+ const organizationId = aud.includes("urn:logto:organization:")
97
+ ? aud.split("urn:logto:organization:")[1]
98
+ : undefined;
99
+
100
+ return {
101
+ userId: sub,
102
+ organizationId: organizationId,
103
+ accessToken: token,
104
+ isM2MApplication: sub === clientId,
105
+ };
106
+ };
107
+
108
+ export type CheckPermissionsPluginOpts = {
109
+ jwkEndpoint: string;
110
+ oidcEndpoint: string;
111
+ };
112
+
113
+ export const checkPermissionsPlugin = async (
114
+ app: FastifyInstance,
115
+ opts: CheckPermissionsPluginOpts,
116
+ ) => {
117
+ app.decorate(
118
+ "checkPermissions",
119
+ async (
120
+ req: FastifyRequest,
121
+ _rep: FastifyReply,
122
+ permissions: string[],
123
+ matchConfig?: MatchConfig,
124
+ ) => {
125
+ const authHeader = req.headers.authorization;
126
+ if (!authHeader) {
127
+ throw httpErrors.unauthorized();
128
+ }
129
+ try {
130
+ const userData = await checkPermissions(
131
+ authHeader,
132
+ opts,
133
+ permissions,
134
+ matchConfig,
135
+ );
136
+ req.userData = userData;
137
+ } catch (e) {
138
+ throw httpErrors.createError(403, getErrorMessage(e), { parent: e });
139
+ }
140
+ },
141
+ );
142
+ };
143
+
144
+ export default fp(checkPermissionsPlugin, {
145
+ name: "apiAuthPlugin",
146
+ });
147
+
148
+ export * from "./logto-client/index.js";
149
+ export * from "./jwtService.js";
@@ -0,0 +1,165 @@
1
+ import {
2
+ GetPublicKeyCommand,
3
+ KMSClient,
4
+ type KMSClientConfig,
5
+ SignCommand,
6
+ } from "@aws-sdk/client-kms";
7
+ import {
8
+ type JWK,
9
+ type JWTPayload,
10
+ type JWTVerifyOptions,
11
+ createRemoteJWKSet,
12
+ decodeJwt,
13
+ exportJWK,
14
+ importSPKI,
15
+ jwtVerify,
16
+ } from "jose";
17
+
18
+ const getKmsClient = (() => {
19
+ let kmsClient: KMSClient | null = null;
20
+
21
+ return function getKmsClient(kmsConfig: KMSClientConfig): KMSClient {
22
+ if (kmsClient) return kmsClient;
23
+
24
+ kmsClient = new KMSClient(kmsConfig);
25
+ return kmsClient;
26
+ };
27
+ })();
28
+
29
+ const defaultAlgorithm = "RS256";
30
+
31
+ interface JWTOptions {
32
+ expirationTime?: string; // Token expiration time (e.g., "1h")
33
+ audience?: string; // Expected audience
34
+ issuer?: string; // Token issuer
35
+ }
36
+
37
+ /**
38
+ * Creates and signs a JWT using the provided payload and private key.
39
+ * @param payload - The JWT payload as an object.
40
+ * @param keyId - The key id or alias in KMS
41
+ * @param options - Optional parameters for customizing the JWT creation (e.g., expiration, audience, issuer).
42
+ */
43
+ async function createSignedJWT(
44
+ payload: Record<string, unknown>,
45
+ keyId: string,
46
+ options: JWTOptions,
47
+ kmsConfig: KMSClientConfig,
48
+ ) {
49
+ const { audience: aud, issuer: iss } = options;
50
+
51
+ const header = {
52
+ alg: defaultAlgorithm,
53
+ typ: "JWT",
54
+ };
55
+
56
+ // we need to create the jwt manually and sign it directly on KMS - private keys cannot be retrieved
57
+ const payloadString = JSON.stringify({ ...payload, aud, iss });
58
+
59
+ const encodedHeader = Buffer.from(JSON.stringify(header)).toString(
60
+ "base64url",
61
+ );
62
+ const encodedPayload = Buffer.from(payloadString).toString("base64url");
63
+
64
+ const messageToSign = `${encodedHeader}.${encodedPayload}`;
65
+
66
+ const input = {
67
+ KeyId: keyId,
68
+ Message: Buffer.from(messageToSign),
69
+ MessageType: "RAW" as const,
70
+ SigningAlgorithm: "RSASSA_PKCS1_V1_5_SHA_256" as const,
71
+ };
72
+ const command = new SignCommand(input);
73
+ const signResponse = await getKmsClient(kmsConfig).send(command);
74
+
75
+ if (!signResponse.Signature) {
76
+ throw new Error("KMS did not return a signature. Signing failed.");
77
+ }
78
+ const signature = Buffer.from(signResponse.Signature).toString("base64url");
79
+
80
+ return `${messageToSign}.${signature}`;
81
+ }
82
+
83
+ /**
84
+ * Returns the JWKS (JSON Web Key Set) for the public key.
85
+ */
86
+ async function getJWKSRoute(publicKey: JWK) {
87
+ return { keys: [publicKey] };
88
+ }
89
+
90
+ interface VerifyJWTOptions {
91
+ jwksUrl: string;
92
+ audience?: string; // Expected audience for the token
93
+ issuer?: string; // Expected issuer for the token
94
+ }
95
+
96
+ /**
97
+ * Verifies the given JWT using the JWKS provided by the remote JWKS URL.
98
+ * @param token - The JWT token to verify.
99
+ * @param options - Object containing JWKS URL, expected audience, issuer, and algorithm.
100
+ */
101
+ async function verifyJWT(
102
+ token: string,
103
+ options: VerifyJWTOptions,
104
+ ): Promise<JWTPayload> {
105
+ const { jwksUrl, audience, issuer } = options;
106
+
107
+ // Create the remote JWK set from the given URL
108
+ const JWKS = createRemoteJWKSet(new URL(jwksUrl));
109
+
110
+ // Define verification options
111
+ const verifyOptions: JWTVerifyOptions = {
112
+ algorithms: [defaultAlgorithm],
113
+ audience, // Check if token is for the expected audience
114
+ issuer, // Check if token was issued by the expected issuer
115
+ };
116
+
117
+ // Verify the JWT using the remote JWKS
118
+ const { payload } = await jwtVerify(token, JWKS, verifyOptions);
119
+
120
+ // Check if the token has expired by verifying the `exp` claim
121
+ if (payload.exp && payload.exp * 1000 < Date.now()) {
122
+ throw new Error("Token has expired");
123
+ }
124
+
125
+ return payload;
126
+ }
127
+
128
+ /**
129
+ * Retrieves a public key from KMS and returns the JWKS (JSON Web Key Set) for the public key.
130
+ * @param keyId - The key id or alias in KMS.
131
+ */
132
+ async function getJWKS(
133
+ keyId: string,
134
+ kmsConfig: KMSClientConfig,
135
+ ): Promise<{ keys: JWK[] }> {
136
+ const command = new GetPublicKeyCommand({ KeyId: keyId });
137
+ const { PublicKey } = await getKmsClient(kmsConfig).send(command);
138
+
139
+ if (!PublicKey)
140
+ throw new Error("KMS did not return a public key. Retrieval failed.");
141
+
142
+ // convert Uint8Array to JWK
143
+ const spki = `-----BEGIN PUBLIC KEY-----\n${Buffer.from(PublicKey).toString("base64")}\n-----END PUBLIC KEY-----`;
144
+ const keyObject = await importSPKI(spki, "RS256");
145
+ const jwk = await exportJWK(keyObject);
146
+
147
+ return getJWKSRoute(jwk);
148
+ }
149
+
150
+ // Function to decode JWT without validation
151
+ function getIssuerFromJWT(token: string) {
152
+ return decodeJwt(token).iss;
153
+ }
154
+
155
+ function unsecureVerifyJWT(token: string) {
156
+ return decodeJwt(token);
157
+ }
158
+
159
+ export {
160
+ createSignedJWT,
161
+ verifyJWT,
162
+ getJWKS,
163
+ getIssuerFromJWT,
164
+ unsecureVerifyJWT,
165
+ };
@@ -0,0 +1,83 @@
1
+ import { httpErrors } from "@fastify/sensible";
2
+ import { UserScope } from "./user-scope.js";
3
+
4
+ interface GetTokenBaseParams {
5
+ logtoOidcEndpoint: string;
6
+ applicationId: string;
7
+ applicationSecret: string;
8
+ scopes: string[];
9
+ }
10
+ export interface GetOrganizationTokenParams extends GetTokenBaseParams {
11
+ organizationId: string;
12
+ }
13
+
14
+ export interface GetAccessTokenParams extends GetTokenBaseParams {
15
+ resource: string;
16
+ }
17
+
18
+ interface TokenResponseBody {
19
+ access_token: string;
20
+ expires_in: number;
21
+ token_type: string;
22
+ scope: string;
23
+ }
24
+
25
+ export const getOrganizationToken = async (
26
+ params: GetOrganizationTokenParams,
27
+ ): Promise<string> => {
28
+ const tokenResponse = await fetchToken({
29
+ ...params,
30
+ scopes: [
31
+ ...params.scopes,
32
+ UserScope.OrganizationRoles,
33
+ UserScope.Organizations,
34
+ ],
35
+ specificBodyFields: { organization_id: params.organizationId },
36
+ });
37
+ //TODO store here
38
+ return tokenResponse.access_token;
39
+ };
40
+
41
+ export const getAccessToken = async (
42
+ params: GetAccessTokenParams,
43
+ ): Promise<string> => {
44
+ const tokenResponse = await fetchToken({
45
+ ...params,
46
+ specificBodyFields: { resource: params.resource },
47
+ });
48
+ //TODO store here
49
+ return tokenResponse.access_token;
50
+ };
51
+
52
+ const fetchToken = async (params: {
53
+ logtoOidcEndpoint: string;
54
+ applicationId: string;
55
+ applicationSecret: string;
56
+ scopes: string[];
57
+ specificBodyFields: { [x: string]: string };
58
+ }): Promise<TokenResponseBody> => {
59
+ const body = {
60
+ ...params.specificBodyFields,
61
+ scope: params.scopes.join(" "),
62
+ grant_type: "client_credentials",
63
+ };
64
+ const logtoOidcEndpoint = params.logtoOidcEndpoint.endsWith("/")
65
+ ? params.logtoOidcEndpoint
66
+ : `${params.logtoOidcEndpoint}/`;
67
+ const response = await fetch(`${logtoOidcEndpoint}token`, {
68
+ method: "POST",
69
+ headers: {
70
+ "Content-Type": "application/x-www-form-urlencoded",
71
+ Authorization: `Basic ${Buffer.from(
72
+ `${params.applicationId}:${params.applicationSecret}`,
73
+ ).toString("base64")}`,
74
+ },
75
+ body: new URLSearchParams(body).toString(),
76
+ });
77
+
78
+ if (response.status !== 200) {
79
+ throw httpErrors.unauthorized(JSON.stringify(await response.json()));
80
+ }
81
+
82
+ return response.json() as Promise<TokenResponseBody>;
83
+ };
@@ -0,0 +1,10 @@
1
+ export declare enum UserScope {
2
+ Profile = "profile",
3
+ Email = "email",
4
+ Phone = "phone",
5
+ CustomData = "custom_data",
6
+ Identities = "identities",
7
+ Roles = "roles",
8
+ Organizations = "urn:logto:scope:organizations",
9
+ OrganizationRoles = "urn:logto:scope:organization_roles",
10
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,54 @@
1
+ export type ScopeMap = Map<string, ScopeMap | boolean>;
2
+
3
+ export const getMapFromScope = (scope?: string) => {
4
+ if (!scope || scope.length === 0) {
5
+ return new Map();
6
+ }
7
+
8
+ const scopes = scope.split(" ");
9
+ return scopes.reduce<ScopeMap>((acc, scope) => {
10
+ const subScope = scope.split(":");
11
+ let current: ScopeMap | boolean | undefined = acc;
12
+
13
+ for (let i = 0; i < subScope.length; i++) {
14
+ const part = subScope[i];
15
+
16
+ if (current === true) break;
17
+
18
+ if (current instanceof Map) {
19
+ if (subScope[i + 1] === "*") {
20
+ current.set(part, true);
21
+ break;
22
+ }
23
+
24
+ if (i === subScope.length - 1) {
25
+ current.set(part, true);
26
+ } else if (!current.get(part)) {
27
+ current.set(part, new Map());
28
+ }
29
+
30
+ current = current.get(part);
31
+ }
32
+ }
33
+
34
+ return acc;
35
+ }, new Map());
36
+ };
37
+
38
+ export const validatePermission = (permission: string, scope: ScopeMap) => {
39
+ const parts = permission.split(":");
40
+
41
+ let current: ScopeMap | boolean | undefined = scope;
42
+
43
+ for (let i = 0; i <= parts.length; i++) {
44
+ const part = parts[i];
45
+
46
+ if (current === true) return true;
47
+
48
+ if (current instanceof Map) {
49
+ current = current.get(part);
50
+ } else {
51
+ return false;
52
+ }
53
+ }
54
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "composite": true,
5
+ "outDir": "dist",
6
+ },
7
+ "include": ["src", "__tests__"],
8
+ "references": [
9
+ { "path": "../shared-errors" }
10
+ ]
11
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "./tsconfig",
3
+ "exclude": ["__tests__", "dist"],
4
+ "compilerOptions": {"rootDir": "src"}
5
+ }
@@ -0,0 +1,19 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ reporters: "default",
6
+ coverage: {
7
+ reporter: ["text", "cobertura"],
8
+ provider: "istanbul",
9
+ },
10
+ include: [
11
+ "**/@(test?(s)|__test?(s)__)/**/*.test.@(js|cjs|mjs|tap|cts|jsx|mts|ts|tsx)",
12
+ "**/*.@(test?(s)|spec).@(js|cjs|mjs|tap|cts|jsx|mts|ts|tsx)",
13
+ "**/test?(s).@(js|cjs|mjs|tap|cts|jsx|mts|ts|tsx)",
14
+ ],
15
+ exclude: ["**/@(fixture*(s)|dist|node_modules)/**"],
16
+ maxConcurrency: 1,
17
+ testTimeout: 30000, // Timeout in milliseconds (30 seconds)
18
+ },
19
+ });