fastify-authz 0.1.3
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 +37 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +38 -0
- package/dist/permissions.d.ts +2 -0
- package/dist/permissions.js +30 -0
- package/dist/verify.d.ts +7 -0
- package/dist/verify.js +10 -0
- package/fastify-authz-0.1.3.tgz +0 -0
- package/package.json +25 -0
- package/src/index.ts +73 -0
- package/src/permissions.ts +35 -0
- package/src/verify.ts +16 -0
- package/tsconfig.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# fastify-authz
|
|
2
|
+
|
|
3
|
+
Fastify plugin for **JWT verification** and **Prisma-backed RBAC authorization**.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install fastify-authz
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import Fastify from "fastify";
|
|
15
|
+
import fastifyAuthz from "fastify-authz";
|
|
16
|
+
import { PrismaClient } from "@prisma/client";
|
|
17
|
+
|
|
18
|
+
const prisma = new PrismaClient();
|
|
19
|
+
const app = Fastify();
|
|
20
|
+
|
|
21
|
+
app.register(fastifyAuthz, {
|
|
22
|
+
prisma,
|
|
23
|
+
jwt: {
|
|
24
|
+
secret: process.env.JWT_ACCESS_SECRET!,
|
|
25
|
+
issuer: process.env.JWT_ISSUER!,
|
|
26
|
+
audience: process.env.JWT_AUDIENCE!,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
app.get("/roles", {
|
|
31
|
+
preHandler: [app.auth.verify, app.auth.requirePermission(["IAM_READ"])],
|
|
32
|
+
}, async () => {
|
|
33
|
+
return await prisma.role.findMany();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
app.listen({ port: 3000 });
|
|
37
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { FastifyPluginAsync, FastifyReply } from "fastify";
|
|
2
|
+
declare module "@fastify/request-context" {
|
|
3
|
+
interface RequestContextData {
|
|
4
|
+
user?: {
|
|
5
|
+
id: string;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export type FastifyAuthzOptions = {
|
|
10
|
+
prisma: unknown;
|
|
11
|
+
jwt: {
|
|
12
|
+
secret: string;
|
|
13
|
+
issuer: string;
|
|
14
|
+
audience: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
declare module "fastify" {
|
|
18
|
+
interface FastifyRequest {
|
|
19
|
+
user?: {
|
|
20
|
+
id: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
interface FastifyInstance {
|
|
24
|
+
auth: {
|
|
25
|
+
verify: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
26
|
+
requirePermission: (perms: string[]) => (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
declare const _default: FastifyPluginAsync<FastifyAuthzOptions>;
|
|
31
|
+
export default _default;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
|
|
7
|
+
const verify_1 = require("./verify");
|
|
8
|
+
const permissions_1 = require("./permissions");
|
|
9
|
+
const request_context_1 = require("@fastify/request-context");
|
|
10
|
+
const plugin = async (fastify, opts) => {
|
|
11
|
+
const { prisma, jwt } = opts;
|
|
12
|
+
const verify = async (request, reply) => {
|
|
13
|
+
try {
|
|
14
|
+
const header = request.headers["authorization"];
|
|
15
|
+
if (!header?.startsWith("Bearer ")) {
|
|
16
|
+
reply.code(401).send({ message: "Missing Authorization header" });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const token = header.slice("Bearer ".length).trim();
|
|
20
|
+
const claims = (0, verify_1.verifyJwt)(token, jwt.secret, jwt.issuer, jwt.audience);
|
|
21
|
+
if (claims.type !== "access") {
|
|
22
|
+
reply.code(401).send({ message: "Invalid token type" });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const user = { id: claims.sub };
|
|
26
|
+
request.user = user;
|
|
27
|
+
request_context_1.requestContext.set("user", user);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
request.log.error({ err }, "Token verification failed");
|
|
31
|
+
reply.code(401).send({ message: "Invalid token" });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const requirePermission = (0, permissions_1.requirePermissionFactory)(prisma);
|
|
36
|
+
fastify.decorate("auth", { verify, requirePermission });
|
|
37
|
+
};
|
|
38
|
+
exports.default = (0, fastify_plugin_1.default)(plugin, { name: "fastify-authz" });
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requirePermissionFactory = requirePermissionFactory;
|
|
4
|
+
function requirePermissionFactory(prisma) {
|
|
5
|
+
return (perms) => {
|
|
6
|
+
return async (request, reply) => {
|
|
7
|
+
const userId = request.user?.id;
|
|
8
|
+
if (!userId) {
|
|
9
|
+
reply.code(401).send({ message: "Unauthorized" });
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const userPerms = await prisma.userRole.findMany({
|
|
13
|
+
where: { userId },
|
|
14
|
+
include: {
|
|
15
|
+
role: {
|
|
16
|
+
include: { permissions: { include: { permission: true } } },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
const allPerms = new Set();
|
|
21
|
+
userPerms.forEach((ur) => ur.role?.permissions?.forEach((rp) => allPerms.add(rp.permission.code)));
|
|
22
|
+
if (allPerms.has("SUPERADMIN"))
|
|
23
|
+
return;
|
|
24
|
+
const allowed = perms.some((p) => allPerms.has(p));
|
|
25
|
+
if (!allowed) {
|
|
26
|
+
reply.code(403).send({ message: "Forbidden: missing permission" });
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
}
|
package/dist/verify.d.ts
ADDED
package/dist/verify.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.verifyJwt = verifyJwt;
|
|
7
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
8
|
+
function verifyJwt(token, secret, issuer, audience) {
|
|
9
|
+
return jsonwebtoken_1.default.verify(token, secret, { issuer, audience });
|
|
10
|
+
}
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fastify-authz",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Fastify plugin for JWT verification and Prisma-based RBAC authorization",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsc -w"
|
|
10
|
+
},
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"@prisma/client": "^5.0.0",
|
|
13
|
+
"fastify": "^5.0.0"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@fastify/request-context": "^6.2.1",
|
|
17
|
+
"fastify-plugin": "^4.0.0",
|
|
18
|
+
"jsonwebtoken": "^9.0.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
22
|
+
"typescript": "^5.4.0"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT"
|
|
25
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { FastifyPluginAsync, FastifyRequest, FastifyReply } from "fastify";
|
|
3
|
+
import fp from "fastify-plugin";
|
|
4
|
+
import { verifyJwt } from "./verify";
|
|
5
|
+
import { requirePermissionFactory } from "./permissions";
|
|
6
|
+
import { requestContext } from "@fastify/request-context";
|
|
7
|
+
|
|
8
|
+
// Augment request-context so requestContext.set("user", ...) is typed
|
|
9
|
+
declare module "@fastify/request-context" {
|
|
10
|
+
interface RequestContextData {
|
|
11
|
+
user?: { id: string };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type FastifyAuthzOptions = {
|
|
16
|
+
prisma: unknown; // expects a PrismaClient-like instance
|
|
17
|
+
jwt: {
|
|
18
|
+
secret: string;
|
|
19
|
+
issuer: string;
|
|
20
|
+
audience: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
declare module "fastify" {
|
|
25
|
+
interface FastifyRequest {
|
|
26
|
+
user?: { id: string };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface FastifyInstance {
|
|
30
|
+
auth: {
|
|
31
|
+
verify: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
32
|
+
requirePermission: (
|
|
33
|
+
perms: string[]
|
|
34
|
+
) => (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const plugin: FastifyPluginAsync<FastifyAuthzOptions> = async (fastify, opts) => {
|
|
40
|
+
const { prisma, jwt } = opts;
|
|
41
|
+
|
|
42
|
+
const verify = async (request: FastifyRequest, reply: FastifyReply) => {
|
|
43
|
+
try {
|
|
44
|
+
const header = request.headers["authorization"];
|
|
45
|
+
if (!header?.startsWith("Bearer ")) {
|
|
46
|
+
reply.code(401).send({ message: "Missing Authorization header" });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const token = header.slice("Bearer ".length).trim();
|
|
51
|
+
const claims = verifyJwt(token, jwt.secret, jwt.issuer, jwt.audience);
|
|
52
|
+
|
|
53
|
+
if (claims.type !== "access") {
|
|
54
|
+
reply.code(401).send({ message: "Invalid token type" });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const user = { id: claims.sub };
|
|
59
|
+
request.user = user;
|
|
60
|
+
requestContext.set("user", user);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
request.log.error({ err }, "Token verification failed");
|
|
63
|
+
reply.code(401).send({ message: "Invalid token" });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const requirePermission = requirePermissionFactory(prisma);
|
|
69
|
+
|
|
70
|
+
fastify.decorate("auth", { verify, requirePermission });
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default fp(plugin, { name: "fastify-authz" });
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { FastifyRequest, FastifyReply } from "fastify";
|
|
2
|
+
|
|
3
|
+
export function requirePermissionFactory(prisma: any) {
|
|
4
|
+
return (perms: string[]) => {
|
|
5
|
+
return async (request: FastifyRequest, reply: FastifyReply) => {
|
|
6
|
+
const userId = request.user?.id;
|
|
7
|
+
if (!userId) {
|
|
8
|
+
reply.code(401).send({ message: "Unauthorized" });
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const userPerms = await prisma.userRole.findMany({
|
|
13
|
+
where: { userId },
|
|
14
|
+
include: {
|
|
15
|
+
role: {
|
|
16
|
+
include: { permissions: { include: { permission: true } } },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const allPerms = new Set<string>();
|
|
22
|
+
userPerms.forEach((ur: any) =>
|
|
23
|
+
ur.role?.permissions?.forEach((rp: any) =>
|
|
24
|
+
allPerms.add(rp.permission.code)
|
|
25
|
+
)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (allPerms.has("SUPERADMIN")) return;
|
|
29
|
+
const allowed = perms.some((p) => allPerms.has(p));
|
|
30
|
+
if (!allowed) {
|
|
31
|
+
reply.code(403).send({ message: "Forbidden: missing permission" });
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
}
|
package/src/verify.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import jwt from "jsonwebtoken";
|
|
2
|
+
|
|
3
|
+
type Claims = {
|
|
4
|
+
sub: string;
|
|
5
|
+
jti: string;
|
|
6
|
+
type: "access" | "refresh";
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function verifyJwt(
|
|
10
|
+
token: string,
|
|
11
|
+
secret: string,
|
|
12
|
+
issuer: string,
|
|
13
|
+
audience: string
|
|
14
|
+
): Claims {
|
|
15
|
+
return jwt.verify(token, secret, { issuer, audience }) as Claims;
|
|
16
|
+
}
|