@eduzz/miau-client 0.0.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.
@@ -0,0 +1,3 @@
1
+ const { ignores, configs } = require('@eduzz/eslint-config');
2
+
3
+ module.exports = [...configs, { ignores: ignores() }];
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@eduzz/miau-client",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "0.0.1",
7
+ "description": "",
8
+ "main": "src/index.ts",
9
+ "scripts": {
10
+ "lint": "eslint && tsc --noEmit",
11
+ "prebuild": "rm -rf dist",
12
+ "build": "esbuild src/index.ts --bundle --sourcemap --platform=node --target=es2020 --outfile=dist/index.js"
13
+ },
14
+ "dependencies": {
15
+ "@eduzz/miau-domain": "workspace:*"
16
+ },
17
+ "keywords": [],
18
+ "author": "",
19
+ "license": "ISC"
20
+ }
@@ -0,0 +1,81 @@
1
+ import jwt from 'jsonwebtoken';
2
+ import { JwksClient } from 'jwks-rsa';
3
+
4
+ type MiauClientConfig = {
5
+ apiUrl: string;
6
+ appSecret: string;
7
+ };
8
+
9
+ export class MiauClient {
10
+ private apiUrl: string;
11
+ private jwtToken: string | undefined;
12
+ private basicAuthToken: string;
13
+ private jwksClient: JwksClient | undefined;
14
+
15
+ constructor(props: MiauClientConfig) {
16
+ this.apiUrl = props.apiUrl;
17
+ const apiKey = props.appSecret.substring(7, 27);
18
+ this.basicAuthToken = Buffer.from(`${apiKey}:${props.appSecret}`).toString('base64');
19
+ }
20
+
21
+ public async getToken() {
22
+ if (this.jwtToken) {
23
+ const tokenPayload = jwt.decode(this.jwtToken) as { iat: number };
24
+ const now = Math.floor(Date.now() / 1000);
25
+
26
+ const TEN_MINUTES = 10 * 60;
27
+
28
+ if (now - tokenPayload.iat < TEN_MINUTES) {
29
+ return this.jwtToken;
30
+ }
31
+ }
32
+
33
+ const response = await fetch(this.getApiJwtUrl(), {
34
+ headers: {
35
+ 'Authorization': `Basic ${this.basicAuthToken}`,
36
+ 'Content-Type': 'application/json'
37
+ }
38
+ });
39
+
40
+ if (response.status !== 200) {
41
+ throw new Error('Failed to fetch token');
42
+ }
43
+
44
+ this.jwtToken = (await response.json()).jwt;
45
+ return this.jwtToken;
46
+ }
47
+
48
+ public async getPermissions(sourceAppId: string) {
49
+ const response = await fetch(`${this.getPermissionsUrl()}/${sourceAppId}`, {
50
+ headers: { Authorization: `Basic ${this.basicAuthToken}` }
51
+ });
52
+
53
+ if (response.status !== 200) {
54
+ throw new Error('Failed to fetch permissions');
55
+ }
56
+
57
+ const permissions = await response.json();
58
+ return permissions.resources;
59
+ }
60
+
61
+ public async getPublicKey(kid: string) {
62
+ if (!this.jwksClient) {
63
+ this.jwksClient = new JwksClient({ jwksUri: this.getJwksUrl(), cache: true });
64
+ }
65
+
66
+ const signingKey = await this.jwksClient.getSigningKey(kid);
67
+ return signingKey.getPublicKey();
68
+ }
69
+
70
+ private getApiJwtUrl = () => {
71
+ return `${this.apiUrl}/jwt`;
72
+ };
73
+
74
+ private getPermissionsUrl = () => {
75
+ return `${this.apiUrl}/permissions`;
76
+ };
77
+
78
+ private getJwksUrl = () => {
79
+ return `${this.apiUrl}/jwks.json`;
80
+ };
81
+ }
@@ -0,0 +1,61 @@
1
+ import { type NextFunction, type Request, type Response } from 'express';
2
+ import jwt from 'jsonwebtoken';
3
+
4
+ import type { ApplicationToken, Resource } from '@eduzz/miau-domain';
5
+
6
+ import type { MiauClient } from './MiauClient';
7
+
8
+ const wildcardToRegex = (pattern: string) => {
9
+ // Escape regex special characters except for '*'
10
+ const escaped = pattern.replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&');
11
+ // Replace '*' with '[^/]+' to match any non-slash segment
12
+ const withWildcards = escaped.replace(/\*/g, '[^/]+');
13
+ // Build regex to match the whole path
14
+ const regexStr = `^${withWildcards}$`;
15
+ return new RegExp(regexStr);
16
+ };
17
+
18
+ export const miauMiddleware = (miauService: MiauClient) => {
19
+ return async (req: Request, res: Response, next: NextFunction) => {
20
+ try {
21
+ const token = req.headers.authorization?.split(' ').pop();
22
+
23
+ if (!token) {
24
+ res.status(401).json({ error: 'Invalid Token', message: 'Token not provided' });
25
+ return;
26
+ }
27
+
28
+ const decodedToken = jwt.decode(token, { complete: true }) as { header: { kid: string } };
29
+
30
+ if (!decodedToken?.header?.kid) {
31
+ res.status(401).json({ error: 'Invalid token', message: 'Missing kid' });
32
+ return;
33
+ }
34
+
35
+ const publicKey = await miauService.getPublicKey(decodedToken.header.kid);
36
+ const appToken = jwt.verify(token, publicKey, { algorithms: ['RS256'] }) as ApplicationToken;
37
+
38
+ req.application = { id: appToken?.id, name: appToken?.name };
39
+
40
+ const resources = await miauService.getPermissions(req.application.id);
41
+ const isAllowed = resources.some((resource: Resource) => {
42
+ return (
43
+ resource.method.toLowerCase() === req.method.toLowerCase() &&
44
+ wildcardToRegex(resource.path).test(req.path.toLowerCase())
45
+ );
46
+ });
47
+
48
+ if (!isAllowed) {
49
+ res
50
+ .status(403)
51
+ .json({ error: 'Forbidden', message: `You do not have permission to access ${req.method} ${req.path} ` });
52
+ return;
53
+ }
54
+
55
+ next();
56
+ } catch (err: any) {
57
+ res.status(401).json({ error: err.name, message: err.message });
58
+ return;
59
+ }
60
+ };
61
+ };
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './MiauClient';
2
+ export * from './MiauMiddleware';
@@ -0,0 +1,9 @@
1
+ import { type ApplicationToken } from '@eduzz/miau-domain';
2
+
3
+ declare global {
4
+ namespace Express {
5
+ interface Request {
6
+ application?: ApplicationToken;
7
+ }
8
+ }
9
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "rootDir": "src",
6
+ "outDir": "dist",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true
11
+ },
12
+ "include": ["src/**/*"],
13
+ "exclude": ["node_modules", "dist"]
14
+ }