@intelicity/gates-sdk 0.1.6 → 0.2.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.
- package/README.md +99 -480
- package/dist/auth/middleware.d.ts +10 -1
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +31 -40
- package/dist/errors/error.d.ts +0 -36
- package/dist/errors/error.d.ts.map +1 -1
- package/dist/errors/error.js +6 -47
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -12
- package/dist/models/user.d.ts +6 -7
- package/dist/models/user.d.ts.map +1 -1
- package/dist/services/admin-service.d.ts +54 -0
- package/dist/services/admin-service.d.ts.map +1 -0
- package/dist/services/admin-service.js +133 -0
- package/dist/services/auth-service.d.ts +4 -7
- package/dist/services/auth-service.d.ts.map +1 -1
- package/dist/services/auth-service.js +32 -47
- package/dist/services/client-auth.d.ts +22 -0
- package/dist/services/client-auth.d.ts.map +1 -0
- package/dist/services/client-auth.js +75 -0
- package/package.json +5 -2
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { jwtVerify, errors as joseErrors } from "jose";
|
|
2
2
|
import { getJwks } from "../cache/jwks-cache.js";
|
|
3
|
-
import { InvalidParameterError, MissingParameterError, TokenExpiredError, InvalidTokenError,
|
|
4
|
-
export class
|
|
3
|
+
import { InvalidParameterError, MissingParameterError, TokenExpiredError, InvalidTokenError, } from "../errors/error.js";
|
|
4
|
+
export class AuthService {
|
|
5
5
|
region;
|
|
6
6
|
userPoolId;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
constructor(region, userPoolId, audience, requiredGroup) {
|
|
7
|
+
clientIds;
|
|
8
|
+
constructor(region, userPoolId, clientId) {
|
|
10
9
|
if (!region || typeof region !== "string" || region.trim().length === 0) {
|
|
11
10
|
throw new MissingParameterError("region");
|
|
12
11
|
}
|
|
@@ -15,35 +14,18 @@ export class GatesAuthService {
|
|
|
15
14
|
userPoolId.trim().length === 0) {
|
|
16
15
|
throw new MissingParameterError("userPoolId");
|
|
17
16
|
}
|
|
18
|
-
if (!audience ||
|
|
19
|
-
typeof audience !== "string" ||
|
|
20
|
-
audience.trim().length === 0) {
|
|
21
|
-
throw new MissingParameterError("audience");
|
|
22
|
-
}
|
|
23
|
-
// Validar formato do userPoolId (deve seguir padrão AWS)
|
|
24
17
|
if (!/^[a-zA-Z0-9_-]+$/.test(userPoolId)) {
|
|
25
|
-
throw new InvalidParameterError("userPoolId", "
|
|
18
|
+
throw new InvalidParameterError("userPoolId", "must follow AWS format (alphanumeric, hyphens, and underscores only)");
|
|
26
19
|
}
|
|
27
20
|
this.region = region;
|
|
28
21
|
this.userPoolId = userPoolId;
|
|
29
|
-
this.
|
|
30
|
-
|
|
22
|
+
this.clientIds = clientId
|
|
23
|
+
? Array.isArray(clientId) ? clientId : [clientId]
|
|
24
|
+
: [];
|
|
31
25
|
}
|
|
32
26
|
get issuer() {
|
|
33
27
|
return `https://cognito-idp.${this.region}.amazonaws.com/${this.userPoolId}`;
|
|
34
28
|
}
|
|
35
|
-
isMemberOf(groups = []) {
|
|
36
|
-
if (!Array.isArray(groups)) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
if (!this.requiredGroup) {
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
const requiredGroups = Array.isArray(this.requiredGroup)
|
|
43
|
-
? this.requiredGroup
|
|
44
|
-
: [this.requiredGroup];
|
|
45
|
-
return groups.some((g) => requiredGroups.includes(g));
|
|
46
|
-
}
|
|
47
29
|
async verifyToken(token) {
|
|
48
30
|
if (!token) {
|
|
49
31
|
throw new MissingParameterError("token");
|
|
@@ -52,47 +34,50 @@ export class GatesAuthService {
|
|
|
52
34
|
const jwks = getJwks(this.region, this.userPoolId);
|
|
53
35
|
const { payload } = await jwtVerify(token, jwks, {
|
|
54
36
|
issuer: this.issuer,
|
|
55
|
-
audience: this.audience,
|
|
56
37
|
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
38
|
+
const tokenUse = payload.token_use;
|
|
39
|
+
if (tokenUse !== "access" && tokenUse !== "id") {
|
|
40
|
+
throw new InvalidTokenError(`Unsupported token_use: expected "access" or "id", got "${tokenUse}"`);
|
|
41
|
+
}
|
|
42
|
+
if (this.clientIds.length > 0) {
|
|
43
|
+
if (tokenUse === "access") {
|
|
44
|
+
const clientId = payload.client_id;
|
|
45
|
+
if (!clientId || !this.clientIds.includes(clientId)) {
|
|
46
|
+
throw new InvalidTokenError("Token client_id does not match any expected clientId");
|
|
47
|
+
}
|
|
64
48
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
49
|
+
else {
|
|
50
|
+
const aud = payload.aud;
|
|
51
|
+
const audValue = Array.isArray(aud) ? aud[0] : aud;
|
|
52
|
+
if (!audValue || !this.clientIds.includes(audValue)) {
|
|
53
|
+
throw new InvalidTokenError("Token audience does not match any expected clientId");
|
|
54
|
+
}
|
|
69
55
|
}
|
|
70
56
|
}
|
|
71
|
-
|
|
57
|
+
const groups = payload["cognito:groups"] ?? [];
|
|
72
58
|
const user = {
|
|
73
59
|
user_id: payload.sub,
|
|
74
60
|
email: payload.email,
|
|
75
61
|
name: payload.name,
|
|
76
62
|
role: payload["custom:general_role"],
|
|
63
|
+
groups,
|
|
64
|
+
token_use: tokenUse,
|
|
77
65
|
exp: payload.exp,
|
|
78
66
|
iat: payload.iat,
|
|
79
67
|
};
|
|
80
68
|
return user;
|
|
81
69
|
}
|
|
82
70
|
catch (error) {
|
|
83
|
-
|
|
84
|
-
if (error instanceof UnauthorizedGroupError ||
|
|
71
|
+
if (error instanceof InvalidTokenError ||
|
|
85
72
|
error instanceof MissingParameterError) {
|
|
86
73
|
throw error;
|
|
87
74
|
}
|
|
88
|
-
// Handle jose-specific errors
|
|
89
75
|
if (error instanceof joseErrors.JWTExpired) {
|
|
90
|
-
throw new TokenExpiredError(
|
|
76
|
+
throw new TokenExpiredError();
|
|
91
77
|
}
|
|
92
78
|
if (error instanceof joseErrors.JWTInvalid) {
|
|
93
|
-
throw new InvalidTokenError("
|
|
79
|
+
throw new InvalidTokenError("Invalid or malformed token");
|
|
94
80
|
}
|
|
95
|
-
// Handle other jose errors
|
|
96
81
|
if (error instanceof Error) {
|
|
97
82
|
if (error.message.includes("expired")) {
|
|
98
83
|
throw new TokenExpiredError(error.message);
|
|
@@ -101,9 +86,9 @@ export class GatesAuthService {
|
|
|
101
86
|
error.message.includes("invalid")) {
|
|
102
87
|
throw new InvalidTokenError(error.message);
|
|
103
88
|
}
|
|
104
|
-
throw new InvalidTokenError(`
|
|
89
|
+
throw new InvalidTokenError(`Token verification failed: ${error.message}`);
|
|
105
90
|
}
|
|
106
|
-
throw new InvalidTokenError("
|
|
91
|
+
throw new InvalidTokenError("Token verification failed");
|
|
107
92
|
}
|
|
108
93
|
}
|
|
109
94
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type ClientCredentialsConfig = {
|
|
2
|
+
domain: string;
|
|
3
|
+
clientId: string;
|
|
4
|
+
clientSecret: string;
|
|
5
|
+
scopes?: string[];
|
|
6
|
+
};
|
|
7
|
+
export type ClientToken = {
|
|
8
|
+
access_token: string;
|
|
9
|
+
expires_in: number;
|
|
10
|
+
token_type: string;
|
|
11
|
+
};
|
|
12
|
+
export declare class GatesClientAuth {
|
|
13
|
+
private readonly tokenEndpoint;
|
|
14
|
+
private readonly clientId;
|
|
15
|
+
private readonly clientSecret;
|
|
16
|
+
private readonly scopes;
|
|
17
|
+
private cachedToken;
|
|
18
|
+
constructor(config: ClientCredentialsConfig);
|
|
19
|
+
getAccessToken(): Promise<string>;
|
|
20
|
+
clearCache(): void;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=client-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-auth.d.ts","sourceRoot":"","sources":["../../src/services/client-auth.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,OAAO,CAAC,WAAW,CAAqD;gBAE5D,MAAM,EAAE,uBAAuB;IAkBrC,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;IAiEvC,UAAU,IAAI,IAAI;CAGnB"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { MissingParameterError, ClientCredentialsError, } from "../errors/error.js";
|
|
2
|
+
export class GatesClientAuth {
|
|
3
|
+
tokenEndpoint;
|
|
4
|
+
clientId;
|
|
5
|
+
clientSecret;
|
|
6
|
+
scopes;
|
|
7
|
+
cachedToken = null;
|
|
8
|
+
constructor(config) {
|
|
9
|
+
if (!config.domain || config.domain.trim().length === 0) {
|
|
10
|
+
throw new MissingParameterError("domain");
|
|
11
|
+
}
|
|
12
|
+
if (!config.clientId || config.clientId.trim().length === 0) {
|
|
13
|
+
throw new MissingParameterError("clientId");
|
|
14
|
+
}
|
|
15
|
+
if (!config.clientSecret || config.clientSecret.trim().length === 0) {
|
|
16
|
+
throw new MissingParameterError("clientSecret");
|
|
17
|
+
}
|
|
18
|
+
const domain = config.domain.replace(/\/$/, "");
|
|
19
|
+
this.tokenEndpoint = `https://${domain}/oauth2/token`;
|
|
20
|
+
this.clientId = config.clientId;
|
|
21
|
+
this.clientSecret = config.clientSecret;
|
|
22
|
+
this.scopes = config.scopes ?? [];
|
|
23
|
+
}
|
|
24
|
+
async getAccessToken() {
|
|
25
|
+
if (this.cachedToken && Date.now() < this.cachedToken.expiresAt) {
|
|
26
|
+
return this.cachedToken.token;
|
|
27
|
+
}
|
|
28
|
+
const body = new URLSearchParams({
|
|
29
|
+
grant_type: "client_credentials",
|
|
30
|
+
});
|
|
31
|
+
if (this.scopes.length > 0) {
|
|
32
|
+
body.set("scope", this.scopes.join(" "));
|
|
33
|
+
}
|
|
34
|
+
const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64");
|
|
35
|
+
let response;
|
|
36
|
+
try {
|
|
37
|
+
response = await fetch(this.tokenEndpoint, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: {
|
|
40
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
41
|
+
Authorization: `Basic ${credentials}`,
|
|
42
|
+
},
|
|
43
|
+
body: body.toString(),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const message = error instanceof Error ? error.message : "Unknown network error";
|
|
48
|
+
throw new ClientCredentialsError(`Failed to reach token endpoint: ${message}`);
|
|
49
|
+
}
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
let detail;
|
|
52
|
+
try {
|
|
53
|
+
const errorBody = (await response.json());
|
|
54
|
+
detail = errorBody.error ?? response.statusText;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
detail = response.statusText;
|
|
58
|
+
}
|
|
59
|
+
throw new ClientCredentialsError(`Token request failed (${response.status}): ${detail}`);
|
|
60
|
+
}
|
|
61
|
+
const data = (await response.json());
|
|
62
|
+
if (!data.access_token) {
|
|
63
|
+
throw new ClientCredentialsError("Token response missing access_token field");
|
|
64
|
+
}
|
|
65
|
+
const bufferMs = 30_000;
|
|
66
|
+
this.cachedToken = {
|
|
67
|
+
token: data.access_token,
|
|
68
|
+
expiresAt: Date.now() + data.expires_in * 1000 - bufferMs,
|
|
69
|
+
};
|
|
70
|
+
return data.access_token;
|
|
71
|
+
}
|
|
72
|
+
clearCache() {
|
|
73
|
+
this.cachedToken = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intelicity/gates-sdk",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Simple SDK for authenticating users with AWS Cognito JWT tokens",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": "./dist/index.js",
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc && tsc-alias",
|
|
20
20
|
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
21
23
|
"test:token": "tsx scripts/test-token.ts",
|
|
22
24
|
"prepublishOnly": "npm run build"
|
|
23
25
|
},
|
|
@@ -37,7 +39,8 @@
|
|
|
37
39
|
"@types/node": "^24.1.0",
|
|
38
40
|
"tsc-alias": "^1.8.16",
|
|
39
41
|
"tsx": "^4.20.3",
|
|
40
|
-
"typescript": "^5.9.2"
|
|
42
|
+
"typescript": "^5.9.2",
|
|
43
|
+
"vitest": "^4.1.0"
|
|
41
44
|
},
|
|
42
45
|
"dependencies": {
|
|
43
46
|
"jose": "^6.1.1"
|