@opengis/fastify-table 2.1.19 → 2.2.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.
- package/dist/helper.d.ts.map +1 -1
- package/dist/helper.js +3 -0
- package/dist/server/migrations/oauth.sql +80 -0
- package/dist/server/plugins/auth/funcs/authorizeUser.js +1 -1
- package/dist/server/plugins/auth/funcs/jwt.d.ts +23 -2
- package/dist/server/plugins/auth/funcs/jwt.d.ts.map +1 -1
- package/dist/server/plugins/auth/funcs/jwt.js +24 -12
- package/dist/server/plugins/policy/funcs/checkJWT.d.ts +5 -1
- package/dist/server/plugins/policy/funcs/checkJWT.d.ts.map +1 -1
- package/dist/server/plugins/policy/funcs/checkJWT.js +42 -10
- package/dist/server/routes/auth/controllers/core/logout.js +1 -1
- package/dist/server/routes/auth/controllers/jwt/authorize.d.ts.map +1 -1
- package/dist/server/routes/auth/controllers/jwt/authorize.js +6 -5
- package/dist/server/routes/auth/controllers/jwt/token.d.ts.map +1 -1
- package/dist/server/routes/auth/controllers/jwt/token.js +8 -39
- package/dist/server/routes/table/functions/getData.d.ts.map +1 -1
- package/dist/server/routes/table/functions/getData.js +8 -6
- package/package.json +3 -1
package/dist/helper.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../helper.ts"],"names":[],"mappings":"AAGA,OAAO,MAAM,MAAM,aAAa,CAAC;AAGjC,OAAO,EAAE,SAAS,EAAkB,MAAM,YAAY,CAAC;AAEvD,QAAA,MAAQ,MAAM,KAAoB,CAAC;AAInC,wBAAsB,KAAK,kBAI1B;AAGD,wBAAsB,QAAQ,kBAW7B;AAED,wBAAsB,KAAK,
|
|
1
|
+
{"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../helper.ts"],"names":[],"mappings":"AAGA,OAAO,MAAM,MAAM,aAAa,CAAC;AAGjC,OAAO,EAAE,SAAS,EAAkB,MAAM,YAAY,CAAC;AAEvD,QAAA,MAAQ,MAAM,KAAoB,CAAC;AAInC,wBAAsB,KAAK,kBAI1B;AAGD,wBAAsB,QAAQ,kBAW7B;AAED,wBAAsB,KAAK,iBAyB1B;AAED,OAAO,EAEL,MAAM,EACN,MAAM,EACN,SAAS,GACV,CAAC"}
|
package/dist/helper.js
CHANGED
|
@@ -23,6 +23,9 @@ export async function build() {
|
|
|
23
23
|
if (!app) {
|
|
24
24
|
app = Fastify({ logger: false });
|
|
25
25
|
app.register(appService, config);
|
|
26
|
+
app.get("/jwt-scoped", { config: { auth: "user-jwt", scope: "sumdu" } }, (req) => req.headers.authorization);
|
|
27
|
+
app.get("/jwt-scoped2", { config: { auth: "user-jwt", scope: "dzk" } }, (req) => req.headers.authorization);
|
|
28
|
+
app.get("/jwt-unscoped", { config: { auth: "user-jwt" } }, (req) => req.headers.authorization);
|
|
26
29
|
app.addHook("onRequest", async (req) => {
|
|
27
30
|
req.user = req.user || { uid: "1", user_type: "admin" };
|
|
28
31
|
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
CREATE schema if not exists oauth;
|
|
2
|
+
|
|
3
|
+
CREATE TABLE if not exists oauth.clients (
|
|
4
|
+
client_id text PRIMARY KEY DEFAULT next_id(), -- ID клієнта (публічний ідентифікатор)
|
|
5
|
+
client_secret_hash text, -- Хеш секрету (NULL для public-клієнтів)
|
|
6
|
+
name text NOT NULL, -- Назва застосунку
|
|
7
|
+
type text NOT NULL CHECK (type IN ('public','confidential')),
|
|
8
|
+
token_endpoint_auth_method text NOT NULL CHECK (token_endpoint_auth_method IN ('client_secret_basic','client_secret_post','private_key_jwt','none')),
|
|
9
|
+
owner_user_id text, -- Власник/адміністратор клієнта (посилання на users.id or other id)
|
|
10
|
+
|
|
11
|
+
redirect_uris text[], -- Дозволені redirect_uri
|
|
12
|
+
grant_types text[] CHECK (case when grant_types is not null then grant_types <@ ARRAY['authorization_code','refresh_token','client_credentials','device_code']::text[] else true end),
|
|
13
|
+
require_pkce boolean NOT NULL DEFAULT true,
|
|
14
|
+
scopes text[],
|
|
15
|
+
allowed_cors_origins text[],
|
|
16
|
+
jwks jsonb, -- Вбудований JWK Set (опційно)
|
|
17
|
+
|
|
18
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
19
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
20
|
+
allowed_ips text[]
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE TABLE if not exists oauth.tokens (
|
|
24
|
+
id text PRIMARY KEY DEFAULT next_id(),
|
|
25
|
+
token_type text NOT NULL CHECK (token_type IN ('access','refresh')),
|
|
26
|
+
token_hash text NOT NULL UNIQUE, -- Argon2/bcrypt/SCrypt (хеш у застосунку)
|
|
27
|
+
token_hint text, -- останні 6-8 символів для діагностики (необов’язково)
|
|
28
|
+
jti text UNIQUE, -- JWT ID, якщо токен — JWT
|
|
29
|
+
client_id text NOT NULL REFERENCES oauth.clients(client_id) ON DELETE CASCADE,
|
|
30
|
+
user_id text, -- NULL для client_credentials
|
|
31
|
+
issuer text, -- iss
|
|
32
|
+
scopes text[],
|
|
33
|
+
claims jsonb, -- додаткові клейми
|
|
34
|
+
issued_at timestamptz NOT NULL DEFAULT now(),
|
|
35
|
+
expires_at timestamptz NOT NULL,
|
|
36
|
+
revoked_at timestamptz,
|
|
37
|
+
revocation_reason text,
|
|
38
|
+
ip inet -- IP видачі/використання (опційно)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
COMMENT ON SCHEMA oauth IS 'Schema for OAuth2 / OpenID Connect clients and tokens';
|
|
42
|
+
|
|
43
|
+
-- Comments for oauth.clients
|
|
44
|
+
COMMENT ON TABLE oauth.clients IS 'OAuth 2.0 clients (applications) that can request tokens';
|
|
45
|
+
|
|
46
|
+
COMMENT ON COLUMN oauth.clients.client_id IS 'Client identifier (public ID, generated by next_id())';
|
|
47
|
+
COMMENT ON COLUMN oauth.clients.client_secret_hash IS 'Hashed client secret (NULL for public clients)';
|
|
48
|
+
COMMENT ON COLUMN oauth.clients.name IS 'Name of the application/client';
|
|
49
|
+
COMMENT ON COLUMN oauth.clients.type IS 'Client type: public or confidential';
|
|
50
|
+
COMMENT ON COLUMN oauth.clients.token_endpoint_auth_method IS 'Authentication method at token endpoint (client_secret_basic, client_secret_post, private_key_jwt, none)';
|
|
51
|
+
COMMENT ON COLUMN oauth.clients.owner_user_id IS 'Owner/administrator of the client (reference to users.id or external id)';
|
|
52
|
+
COMMENT ON COLUMN oauth.clients.redirect_uris IS 'Allowed redirect URIs';
|
|
53
|
+
COMMENT ON COLUMN oauth.clients.grant_types IS 'Allowed grant types (authorization_code, refresh_token, client_credentials, device_code)';
|
|
54
|
+
COMMENT ON COLUMN oauth.clients.require_pkce IS 'Whether PKCE is required (default true)';
|
|
55
|
+
COMMENT ON COLUMN oauth.clients.scopes IS 'Allowed OAuth2 scopes';
|
|
56
|
+
COMMENT ON COLUMN oauth.clients.allowed_cors_origins IS 'Allowed CORS origins for browser-based apps';
|
|
57
|
+
COMMENT ON COLUMN oauth.clients.jwks IS 'Embedded JSON Web Key Set (optional)';
|
|
58
|
+
COMMENT ON COLUMN oauth.clients.created_at IS 'Creation timestamp';
|
|
59
|
+
COMMENT ON COLUMN oauth.clients.updated_at IS 'Last update timestamp';
|
|
60
|
+
|
|
61
|
+
-- Comments for oauth.tokens
|
|
62
|
+
COMMENT ON TABLE oauth.tokens IS 'Issued OAuth 2.0 tokens (access or refresh)';
|
|
63
|
+
|
|
64
|
+
COMMENT ON COLUMN oauth.tokens.id IS 'Internal token ID (generated by next_id())';
|
|
65
|
+
COMMENT ON COLUMN oauth.tokens.token_type IS 'Type of token: access or refresh';
|
|
66
|
+
COMMENT ON COLUMN oauth.tokens.token_hash IS 'Secure hash of the token (Argon2/bcrypt/SCrypt)';
|
|
67
|
+
COMMENT ON COLUMN oauth.tokens.token_hint IS 'Optional hint (last 6–8 characters of token) for diagnostics';
|
|
68
|
+
COMMENT ON COLUMN oauth.tokens.jti IS 'JWT ID if token is a JWT (unique)';
|
|
69
|
+
COMMENT ON COLUMN oauth.tokens.client_id IS 'Reference to oauth.clients (issuing client)';
|
|
70
|
+
COMMENT ON COLUMN oauth.tokens.user_id IS 'User ID if bound to user (NULL for client_credentials flow)';
|
|
71
|
+
COMMENT ON COLUMN oauth.tokens.issuer IS 'Token issuer (iss claim)';
|
|
72
|
+
COMMENT ON COLUMN oauth.tokens.scopes IS 'Granted OAuth2 scopes for this token';
|
|
73
|
+
COMMENT ON COLUMN oauth.tokens.claims IS 'Additional claims (JSONB)';
|
|
74
|
+
COMMENT ON COLUMN oauth.tokens.issued_at IS 'Timestamp when issued';
|
|
75
|
+
COMMENT ON COLUMN oauth.tokens.expires_at IS 'Timestamp when token expires';
|
|
76
|
+
COMMENT ON COLUMN oauth.tokens.revoked_at IS 'Timestamp when revoked';
|
|
77
|
+
COMMENT ON COLUMN oauth.tokens.revocation_reason IS 'Reason for revocation (if any)';
|
|
78
|
+
COMMENT ON COLUMN oauth.tokens.ip IS 'IP address of issuance/usage (optional)';
|
|
79
|
+
|
|
80
|
+
alter table oauth.clients add column if not exists allowed_ips text[];
|
|
@@ -26,7 +26,7 @@ export default async function authorizeUser(user, req, authType = "creds-user",
|
|
|
26
26
|
const ip = getIp(req);
|
|
27
27
|
Object.assign(user, { auth_type: authType, ip });
|
|
28
28
|
// fastify/passport
|
|
29
|
-
await req.login(user);
|
|
29
|
+
await req.login?.(user);
|
|
30
30
|
const st = (req.body?.keep || req.query?.keep) === "on"
|
|
31
31
|
? sessionTimeoutKeep
|
|
32
32
|
: sessionTimeout;
|
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
export declare function scryptHash(code: string): Promise<string>;
|
|
2
2
|
export declare function scryptVerify(stored: string, code: string): Promise<boolean>;
|
|
3
|
-
export declare function sign(uid: string, secret: string, exp
|
|
4
|
-
export declare function verify(token: string, secret: string,
|
|
3
|
+
export declare function sign(uid: string, secret: string, exp?: number, scopes?: string[]): string;
|
|
4
|
+
export declare function verify(token: string, secret: string, scope?: string): {
|
|
5
|
+
valid: boolean;
|
|
6
|
+
error: string;
|
|
7
|
+
payload?: undefined;
|
|
8
|
+
header?: undefined;
|
|
9
|
+
stack?: undefined;
|
|
10
|
+
} | {
|
|
11
|
+
valid: boolean;
|
|
5
12
|
payload: any;
|
|
6
13
|
header: any;
|
|
14
|
+
error?: undefined;
|
|
15
|
+
stack?: undefined;
|
|
16
|
+
} | {
|
|
17
|
+
valid: boolean;
|
|
18
|
+
payload: any;
|
|
19
|
+
header: any;
|
|
20
|
+
error: string;
|
|
21
|
+
stack?: undefined;
|
|
22
|
+
} | {
|
|
23
|
+
valid: boolean;
|
|
24
|
+
error: any;
|
|
25
|
+
stack: any;
|
|
26
|
+
payload?: undefined;
|
|
27
|
+
header?: undefined;
|
|
7
28
|
};
|
|
8
29
|
declare const _default: null;
|
|
9
30
|
export default _default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../../../../server/plugins/auth/funcs/jwt.ts"],"names":[],"mappings":"AAkBA,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,mBAI5C;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,oBAI9D;AAED,wBAAgB,IAAI,
|
|
1
|
+
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../../../../server/plugins/auth/funcs/jwt.ts"],"names":[],"mappings":"AAkBA,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,mBAI5C;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,oBAI9D;AAED,wBAAgB,IAAI,CAClB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,GAAG,SAAQ,EACX,MAAM,CAAC,EAAE,MAAM,EAAE,UAuBlB;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;EA+CnE;;AAED,wBAAoB"}
|
|
@@ -17,7 +17,7 @@ export async function scryptVerify(stored, code) {
|
|
|
17
17
|
const derived = (await scryptAsync(code, salt, 64));
|
|
18
18
|
return keyHex === derived.toString("hex");
|
|
19
19
|
}
|
|
20
|
-
export function sign(uid, secret, exp = 90000,
|
|
20
|
+
export function sign(uid, secret, exp = 90000, scopes) {
|
|
21
21
|
if (typeof uid !== "string")
|
|
22
22
|
throw new Error("uid must be a string");
|
|
23
23
|
if (secret && typeof secret !== "string") {
|
|
@@ -26,8 +26,8 @@ export function sign(uid, secret, exp = 90000, ip) {
|
|
|
26
26
|
if (typeof exp !== "number")
|
|
27
27
|
throw new Error("exp must be a number");
|
|
28
28
|
const jwtPayload = Buffer.from(JSON.stringify({
|
|
29
|
-
ip,
|
|
30
29
|
uid,
|
|
30
|
+
scopes,
|
|
31
31
|
expires: Date.now() + exp,
|
|
32
32
|
created: Date.now(),
|
|
33
33
|
})).toString("base64");
|
|
@@ -37,11 +37,18 @@ export function sign(uid, secret, exp = 90000, ip) {
|
|
|
37
37
|
.digest("base64");
|
|
38
38
|
return `${jwtEncrypted}.${signature}`;
|
|
39
39
|
}
|
|
40
|
-
export function verify(token, secret,
|
|
41
|
-
if (!token)
|
|
42
|
-
|
|
40
|
+
export function verify(token, secret, scope) {
|
|
41
|
+
if (!token) {
|
|
42
|
+
return { valid: false, error: "not enough params: token" };
|
|
43
|
+
}
|
|
44
|
+
if (typeof token !== "string") {
|
|
45
|
+
return { valid: false, error: "invalid params: token" };
|
|
46
|
+
}
|
|
43
47
|
if (secret && typeof secret !== "string") {
|
|
44
|
-
|
|
48
|
+
return { valid: false, error: "invalid params: secret" };
|
|
49
|
+
}
|
|
50
|
+
if (scope && typeof scope !== "string") {
|
|
51
|
+
return { valid: false, error: "invalid params: scope" };
|
|
45
52
|
}
|
|
46
53
|
const split = token.split(".");
|
|
47
54
|
const signature = split[2];
|
|
@@ -52,15 +59,20 @@ export function verify(token, secret, ip) {
|
|
|
52
59
|
const expectedSignature = createHmac("sha256", secret || jwtSecret)
|
|
53
60
|
.update(jwtEncryptedExpected)
|
|
54
61
|
.digest("base64");
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
payload
|
|
58
|
-
return { payload, header };
|
|
62
|
+
const isScopeValid = !scope || !payload.scopes || payload.scopes?.includes?.(scope);
|
|
63
|
+
if (signature === expectedSignature && isScopeValid) {
|
|
64
|
+
return { valid: true, payload, header };
|
|
59
65
|
}
|
|
60
|
-
|
|
66
|
+
console.log("JWT: invalid token", payload, scope);
|
|
67
|
+
return {
|
|
68
|
+
valid: false,
|
|
69
|
+
payload,
|
|
70
|
+
header,
|
|
71
|
+
error: signature !== expectedSignature ? "invalid signature" : "invalid scope",
|
|
72
|
+
};
|
|
61
73
|
}
|
|
62
74
|
catch (err) {
|
|
63
|
-
return false;
|
|
75
|
+
return { valid: false, error: err.toString(), stack: err.stack };
|
|
64
76
|
}
|
|
65
77
|
}
|
|
66
78
|
export default null;
|
|
@@ -4,9 +4,13 @@ export default function checkJWT(req: ExtendedRequest): Promise<{
|
|
|
4
4
|
code: number;
|
|
5
5
|
token?: undefined;
|
|
6
6
|
valid?: undefined;
|
|
7
|
+
payload?: undefined;
|
|
8
|
+
redirectURIs?: undefined;
|
|
7
9
|
} | {
|
|
8
|
-
token:
|
|
10
|
+
token: any;
|
|
9
11
|
valid: boolean;
|
|
12
|
+
payload: any;
|
|
13
|
+
redirectURIs: any;
|
|
10
14
|
error?: undefined;
|
|
11
15
|
code?: undefined;
|
|
12
16
|
} | null>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkJWT.d.ts","sourceRoot":"","sources":["../../../../../server/plugins/policy/funcs/checkJWT.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,eAAe,EAEhB,MAAM,wBAAwB,CAAC;AAMhC,wBAA8B,QAAQ,CAAC,GAAG,EAAE,eAAe
|
|
1
|
+
{"version":3,"file":"checkJWT.d.ts","sourceRoot":"","sources":["../../../../../server/plugins/policy/funcs/checkJWT.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,eAAe,EAEhB,MAAM,wBAAwB,CAAC;AAMhC,wBAA8B,QAAQ,CAAC,GAAG,EAAE,eAAe;;;;;;;;;;;;;;UAkG1D"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import getIP from "./getIP.js";
|
|
2
2
|
import logger from "../../logger/getLogger.js";
|
|
3
|
-
import { verify } from "../../auth/funcs/jwt.js";
|
|
3
|
+
import { scryptVerify, verify } from "../../auth/funcs/jwt.js";
|
|
4
4
|
export default async function checkJWT(req) {
|
|
5
5
|
const { originalUrl: path, headers, method, routeOptions, pg } = req;
|
|
6
6
|
const ip = getIP(req);
|
|
@@ -9,13 +9,16 @@ export default async function checkJWT(req) {
|
|
|
9
9
|
const requireUser = Array.isArray(policy)
|
|
10
10
|
? policy.includes("user")
|
|
11
11
|
: ["L1", "L2", "L3"].includes(policy || "");
|
|
12
|
-
const requireJWT = auth === "user-jwt";
|
|
12
|
+
const requireJWT = auth === "user-jwt" || req.url === "/oauth/token";
|
|
13
13
|
const user = req.user || req.session?.passport?.user;
|
|
14
14
|
// skip entirely if not required via API config or alternative exists
|
|
15
15
|
if (!requireJWT || (requireUser && user)) {
|
|
16
16
|
return null;
|
|
17
17
|
}
|
|
18
|
-
const
|
|
18
|
+
const code = req.body ? req.body.code : req.query.code;
|
|
19
|
+
const jwtToken = req.url === "/oauth/token" && code
|
|
20
|
+
? code
|
|
21
|
+
: headers.authorization?.split(" ")?.[1];
|
|
19
22
|
// restict access if header is not provided at all
|
|
20
23
|
if (!jwtToken) {
|
|
21
24
|
logger.file("policy/jwt", {
|
|
@@ -27,22 +30,51 @@ export default async function checkJWT(req) {
|
|
|
27
30
|
});
|
|
28
31
|
return { error: "unauthorized", code: 401 };
|
|
29
32
|
}
|
|
30
|
-
|
|
33
|
+
if (!pg) {
|
|
34
|
+
return { error: "empty pg", code: 400 };
|
|
35
|
+
}
|
|
36
|
+
const { clientId, tokenHash, isRevoked, isExpired } = await pg
|
|
37
|
+
.query('select client_id as "clientId", token_hash as "tokenHash", revoked_at is not null as "isRevoked", expires_at <= now() as "isExpired" from oauth.tokens where ip=$1 and token_hint=$2', [ip, jwtToken.slice(-6)])
|
|
38
|
+
.then((el) => el.rows?.[0] || {});
|
|
39
|
+
if (requireJWT && tokenHash && (isExpired || isRevoked)) {
|
|
40
|
+
logger.file("policy/jwt", {
|
|
41
|
+
path,
|
|
42
|
+
method,
|
|
43
|
+
message: isRevoked ? "token is revoked" : "token is expired",
|
|
44
|
+
ip,
|
|
45
|
+
uid: user?.uid,
|
|
46
|
+
});
|
|
47
|
+
return { error: "forbidden", code: 403 };
|
|
48
|
+
}
|
|
49
|
+
const isValid = await scryptVerify(tokenHash || "", jwtToken);
|
|
50
|
+
if (requireJWT && !isValid) {
|
|
51
|
+
logger.file("policy/jwt", {
|
|
52
|
+
path,
|
|
53
|
+
method,
|
|
54
|
+
message: tokenHash ? "scrypt verification failed" : "token not found",
|
|
55
|
+
ip,
|
|
56
|
+
uid: user?.uid,
|
|
57
|
+
});
|
|
58
|
+
return { error: "forbidden", code: 403 };
|
|
59
|
+
}
|
|
60
|
+
const q = `select client_secret_hash as "secret", redirect_uris as "redirectURIs" from oauth.clients where client_id=$1 and token_endpoint_auth_method=$2`;
|
|
61
|
+
const { secret, redirectURIs } = pg?.pk?.["oauth.clients"]
|
|
31
62
|
? await pg
|
|
32
|
-
.query(
|
|
33
|
-
.then((el) => el
|
|
34
|
-
:
|
|
35
|
-
const
|
|
63
|
+
.query(q, [clientId, "private_key_jwt"])
|
|
64
|
+
.then((el) => el.rows?.[0] || {})
|
|
65
|
+
: {};
|
|
66
|
+
const { valid, error, payload } = verify(jwtToken, secret, scope);
|
|
36
67
|
// restrict access if token is invalid
|
|
37
|
-
if (requireJWT && !
|
|
68
|
+
if (requireJWT && !valid) {
|
|
38
69
|
logger.file("policy/jwt", {
|
|
39
70
|
path,
|
|
40
71
|
method,
|
|
41
72
|
message: "forbidden",
|
|
73
|
+
error,
|
|
42
74
|
ip,
|
|
43
75
|
uid: user?.uid,
|
|
44
76
|
});
|
|
45
77
|
return { error: "forbidden", code: 403 };
|
|
46
78
|
}
|
|
47
|
-
return { token: jwtToken, valid: true };
|
|
79
|
+
return { token: jwtToken, valid: true, payload, redirectURIs };
|
|
48
80
|
}
|
|
@@ -8,7 +8,7 @@ async function logout(req, reply) {
|
|
|
8
8
|
return reply.redirect("/");
|
|
9
9
|
}
|
|
10
10
|
await req.session?.destroy?.();
|
|
11
|
-
reply
|
|
11
|
+
reply?.clearCookie?.("session_auth");
|
|
12
12
|
// Shield session from resurrection
|
|
13
13
|
Object.defineProperty(req, "session", {
|
|
14
14
|
value: null,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../../../../../server/routes/auth/controllers/jwt/authorize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAyBvC,wBAA8B,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../../../../../server/routes/auth/controllers/jwt/authorize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAyBvC,wBAA8B,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,YAAY,kBAuHpE"}
|
|
@@ -27,10 +27,11 @@ export default async function authorize(req, reply) {
|
|
|
27
27
|
.code(400)
|
|
28
28
|
.send({ error: "not enough query params: client_id", code: 400 });
|
|
29
29
|
}
|
|
30
|
-
const
|
|
31
|
-
const
|
|
30
|
+
const ip = getIp(req);
|
|
31
|
+
const q = `select * from oauth.clients where client_id=$1 and token_endpoint_auth_method=$2 and (allowed_ips is null or $3=any(allowed_ips)) and ${scope ? "$4=any(scopes)" : "1=1"} order by allowed_ips nulls last limit 1`;
|
|
32
|
+
const { owner_user_id: userId, client_secret_hash: secret, redirect_uris = [], scopes, allowed_ips: allowedIPs = [], } = pg.pk?.["oauth.clients"]
|
|
32
33
|
? await pg
|
|
33
|
-
.query(q, [client_id, "private_key_jwt"])
|
|
34
|
+
.query(q, [client_id, "private_key_jwt", ip, scope].filter(Boolean))
|
|
34
35
|
.then((el) => el.rows?.[0] || {})
|
|
35
36
|
: {};
|
|
36
37
|
if (!userId) {
|
|
@@ -54,9 +55,8 @@ export default async function authorize(req, reply) {
|
|
|
54
55
|
const href1 = req.method === "POST"
|
|
55
56
|
? null
|
|
56
57
|
: await authorizeUser(user, req, "jwt", expireMsec);
|
|
57
|
-
const ip = getIp(req);
|
|
58
58
|
// Generate authorization code
|
|
59
|
-
const code = sign(userId, secret, expireMsec,
|
|
59
|
+
const code = sign(userId, secret, expireMsec, scopes);
|
|
60
60
|
const tokenHash = await scryptHash(code);
|
|
61
61
|
// disable access via old tokens
|
|
62
62
|
if (pg.pk?.["oauth.tokens"]) {
|
|
@@ -92,6 +92,7 @@ export default async function authorize(req, reply) {
|
|
|
92
92
|
code,
|
|
93
93
|
expire: expireMsec,
|
|
94
94
|
redirect_uri: href,
|
|
95
|
+
scopes,
|
|
95
96
|
});
|
|
96
97
|
}
|
|
97
98
|
if (payload.noredirect || process.env.NODE_ENV === "test") {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../../../../../server/routes/auth/controllers/jwt/token.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../../../../../server/routes/auth/controllers/jwt/token.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAsB5D,wBAA8B,UAAU,CACtC,GAAG,EAAE,eAAe,EACpB,KAAK,EAAE,YAAY,kBAwEpB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { verify, scryptVerify } from "../../../../plugins/auth/funcs/jwt.js";
|
|
2
1
|
import pgClients from "../../../../plugins/pg/pgClients.js";
|
|
3
2
|
import authorizeUser from "../../../../plugins/auth/funcs/authorizeUser.js";
|
|
3
|
+
import checkJWT from "../../../../plugins/policy/funcs/checkJWT.js";
|
|
4
4
|
const expireMsec = 1000 * 60 * 60;
|
|
5
5
|
const getIp = (req) => (req.headers?.["x-real-ip"] ||
|
|
6
6
|
req.headers?.["x-forwarded-for"] ||
|
|
@@ -9,6 +9,7 @@ const getIp = (req) => (req.headers?.["x-real-ip"] ||
|
|
|
9
9
|
"")
|
|
10
10
|
.split(":")
|
|
11
11
|
.pop();
|
|
12
|
+
// todo: use checkJWT instead
|
|
12
13
|
export default async function oauthToken(req, reply) {
|
|
13
14
|
const { pg = pgClients.client, query, body } = req;
|
|
14
15
|
const payload = req.method === "POST" ? body : query;
|
|
@@ -26,56 +27,24 @@ export default async function oauthToken(req, reply) {
|
|
|
26
27
|
.code(400)
|
|
27
28
|
.send({ error: "not enough params: code", code: 400 });
|
|
28
29
|
}
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.query(q, [client_id, "private_key_jwt"])
|
|
33
|
-
.then((el) => el.rows?.[0] || {})
|
|
34
|
-
: {};
|
|
35
|
-
const ip = getIp(req);
|
|
36
|
-
const isCodeValid = verify(code, secret, ip);
|
|
37
|
-
const q1 = "select token_hash, expires_at, ip from oauth.tokens where client_id=$1 and revoked_at is null and expires_at > now()";
|
|
38
|
-
const { token_hash: stored, expires_at, ip: storedIp, } = await pg.query(q1, [client_id]).then((el) => el.rows?.[0] || {});
|
|
39
|
-
if (storedIp !== ip) {
|
|
40
|
-
return reply
|
|
41
|
-
.code(403)
|
|
42
|
-
.send({ error: "access restricted: wrong IP address", code: 403 });
|
|
43
|
-
}
|
|
44
|
-
if (!stored) {
|
|
45
|
-
return reply
|
|
46
|
-
.code(403)
|
|
47
|
-
.send({ error: "access restricted: code expired", code: 403 });
|
|
48
|
-
}
|
|
49
|
-
const isValid = await scryptVerify(stored, code);
|
|
50
|
-
if (!isValid) {
|
|
51
|
-
return reply
|
|
52
|
-
.code(403)
|
|
53
|
-
.send({ error: "access restricted: stored code mismatch", code: 403 });
|
|
54
|
-
}
|
|
55
|
-
if (!isCodeValid) {
|
|
56
|
-
return reply
|
|
57
|
-
.code(403)
|
|
58
|
-
.send({ error: "access restricted: invalid code", code: 403 });
|
|
59
|
-
}
|
|
60
|
-
if (!userId) {
|
|
61
|
-
return reply.code(400).send({ error: "invalid client id", code: 400 });
|
|
30
|
+
const { valid, payload: jwtPayload, redirectURIs, error, } = (await checkJWT(req)) || {};
|
|
31
|
+
if (!valid) {
|
|
32
|
+
return reply.code(401).send({ error, code: 401 });
|
|
62
33
|
}
|
|
63
|
-
if (redirect_uri &&
|
|
64
|
-
Array.isArray(redirect_uris) &&
|
|
65
|
-
!redirect_uris.includes(redirect_uri)) {
|
|
34
|
+
if (redirect_uri && !(redirectURIs || []).includes(redirect_uri)) {
|
|
66
35
|
return reply.code(400).send({ error: "invalid redirect_uri", code: 400 });
|
|
67
36
|
}
|
|
68
37
|
const user = pg.pk?.["admin.users"]
|
|
69
38
|
? await pg
|
|
70
39
|
.query("select * from admin.users where uid=$1 and enabled limit 1", [
|
|
71
|
-
|
|
40
|
+
jwtPayload.uid,
|
|
72
41
|
])
|
|
73
42
|
.then((el) => el.rows[0])
|
|
74
43
|
: null;
|
|
75
44
|
if (!user) {
|
|
76
45
|
return reply.code(404).send({ error: "user not found", code: 404 });
|
|
77
46
|
}
|
|
78
|
-
const expire =
|
|
47
|
+
const expire = Math.min(jwtPayload.expires - jwtPayload.created || expireMsec, expireMsec);
|
|
79
48
|
const href1 = await authorizeUser(user, req, "jwt", expire);
|
|
80
49
|
const backUrl = redirect_uri ? `${redirect_uri}?code=${code}` : "";
|
|
81
50
|
const href = redirect_uri && !redirect_uri.includes("?")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"getData.d.ts","sourceRoot":"","sources":["../../../../../server/routes/table/functions/getData.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAkFzD,wBAA8B,OAAO,CACnC,EACE,EAAqB,EACrB,MAAM,EACN,KAAK,EACL,EAAE,EACF,OAAY,EACZ,KAAU,EACV,IAAS,EACT,YAAY,EACZ,YAAY,EACZ,KAAY,EACZ,UAAU,EACV,OAAO,EAAE,YAAY,EACrB,WAAW,EAAE,gBAAgB,EAC7B,OAAO,EAAE,YAAY,EACrB,QAAgB,GACjB,EAAE;IACD,EAAE,CAAC,EAAE,UAAU,CAAC;IAChB,MAAM,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,UAAU,CAAC,EAAE,GAAG,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,EACD,MAAM,CAAC,EAAE,YAAY,EACrB,MAAM,CAAC,EAAE,GAAG,
|
|
1
|
+
{"version":3,"file":"getData.d.ts","sourceRoot":"","sources":["../../../../../server/routes/table/functions/getData.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAkFzD,wBAA8B,OAAO,CACnC,EACE,EAAqB,EACrB,MAAM,EACN,KAAK,EACL,EAAE,EACF,OAAY,EACZ,KAAU,EACV,IAAS,EACT,YAAY,EACZ,YAAY,EACZ,KAAY,EACZ,UAAU,EACV,OAAO,EAAE,YAAY,EACrB,WAAW,EAAE,gBAAgB,EAC7B,OAAO,EAAE,YAAY,EACrB,QAAgB,GACjB,EAAE;IACD,EAAE,CAAC,EAAE,UAAU,CAAC;IAChB,MAAM,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,UAAU,CAAC,EAAE,GAAG,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,EACD,MAAM,CAAC,EAAE,YAAY,EACrB,MAAM,CAAC,EAAE,GAAG,gBAo3Bb"}
|
|
@@ -128,9 +128,9 @@ export default async function dataAPI({ pg = pgClients.client, params, table, id
|
|
|
128
128
|
if (!pg) {
|
|
129
129
|
return reply.status(500).send("empty pg");
|
|
130
130
|
}
|
|
131
|
-
const pkey = pg.pk?.[paramsTable] ||
|
|
132
|
-
pg.pk?.[paramsTable.replace(/"/g, "")] ||
|
|
133
|
-
(await getMeta({ pg, table: paramsTable }))?.pk;
|
|
131
|
+
const pkey = pg.pk?.[resources[paramsTable] || paramsTable] ||
|
|
132
|
+
pg.pk?.[resources[paramsTable] || paramsTable.replace(/"/g, "")] ||
|
|
133
|
+
(await getMeta({ pg, table: resources[paramsTable] || paramsTable }))?.pk;
|
|
134
134
|
if (!loadTable &&
|
|
135
135
|
!(tokenData?.table && pg.pk?.[tokenData?.table]) &&
|
|
136
136
|
!(called && pkey)) {
|
|
@@ -244,7 +244,7 @@ export default async function dataAPI({ pg = pgClients.client, params, table, id
|
|
|
244
244
|
if (objectId && columnList.includes(objectId)) {
|
|
245
245
|
return gisIRColumn({
|
|
246
246
|
pg,
|
|
247
|
-
layer: paramsTable,
|
|
247
|
+
layer: resources[paramsTable] || paramsTable,
|
|
248
248
|
column: objectId,
|
|
249
249
|
sql: query?.sql,
|
|
250
250
|
filter: query?.filter,
|
|
@@ -264,7 +264,9 @@ export default async function dataAPI({ pg = pgClients.client, params, table, id
|
|
|
264
264
|
const fData = checkFilter
|
|
265
265
|
? await getFilterSQL({
|
|
266
266
|
pg,
|
|
267
|
-
table: loadTable
|
|
267
|
+
table: loadTable
|
|
268
|
+
? tokenData?.table || resources[paramsTable] || paramsTable
|
|
269
|
+
: table1,
|
|
268
270
|
filter: query?.filter,
|
|
269
271
|
search: query?.search,
|
|
270
272
|
searchColumn,
|
|
@@ -600,7 +602,7 @@ export default async function dataAPI({ pg = pgClients.client, params, table, id
|
|
|
600
602
|
}
|
|
601
603
|
const route = pg.tlist?.includes?.("admin.routes")
|
|
602
604
|
? await pg
|
|
603
|
-
.query("select route_id as path, title from admin.routes where enabled and alias=$1 limit 1", [paramsTable])
|
|
605
|
+
.query("select route_id as path, title from admin.routes where enabled and alias=$1 limit 1", [resources[paramsTable] || paramsTable])
|
|
604
606
|
.then((el) => el.rows?.[0] || {})
|
|
605
607
|
: {};
|
|
606
608
|
const res = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengis/fastify-table",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "core-plugins",
|
|
6
6
|
"keywords": [
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
"build": "tsc -b --clean && tsc && copyfiles server/plugins/grpc/utils/*.proto dist && copyfiles server/migrations/*.sql dist && copyfiles server/templates/**/*.html dist && copyfiles server/templates/**/*.hbs dist && copyfiles script/* dist && copyfiles -u 2 module/core/*/* dist/module/core",
|
|
32
32
|
"prod": "NODE_ENV=production bun dist/server",
|
|
33
33
|
"patch": "npm version patch && git push && npm publish",
|
|
34
|
+
"minor": "npm version minor && git push && npm publish",
|
|
35
|
+
"major": "npm version major && git push && npm publish",
|
|
34
36
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
|
35
37
|
"test": "npx vitest run",
|
|
36
38
|
"compress": "node compress.js",
|