@jitar-plugins/authentication 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.
package/README.md ADDED
@@ -0,0 +1,178 @@
1
+
2
+ # Authentication | Jitar Plugins
3
+
4
+ This package provides plugins for integrating the [The Shelf authentication package](https://github.com/MaskingTechnology/theshelf/tree/main/packages/authentication) in Jitar applications.
5
+
6
+ It contains two types of middleware:
7
+
8
+ * **Authentication** - server side authentication handling.
9
+ * **Requester** - client side authentication handling.
10
+
11
+ Both are required for the integration.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @theshelf/authentication @jitar-plugins/authentication @jitar-plugins/http
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ Follow the following steps to configure and use the provided plugins.
22
+
23
+ ### Step 1 - Configure the middleware
24
+
25
+ Both types of middleware need to be instantiated with configuration.
26
+
27
+ The authentication middleware operates on the server side and handles the actual authentication.
28
+
29
+ ```ts
30
+ // src/middleware/authenticationMiddleware.ts
31
+
32
+ import identityProvider from '@theshelf/authentication';
33
+ import { AuthenticationMiddleware } from '@jitar-plugins/authentication';
34
+
35
+ // FQNs to the auth handling procedures
36
+ const authProcedures = {
37
+ loginUrl: 'domain/authentication/getLoginUrl',
38
+ login: 'domain/authentication/login',
39
+ logout: 'domain/authentication/logout'
40
+ };
41
+
42
+ // The client path to return to after a succesful login
43
+ const redirectPath = '/afterlogin';
44
+
45
+ const whiteList: string[] = [
46
+ // List of public FQNs
47
+ ];
48
+
49
+ export default new AuthenticationMiddleware(identityProvider, authProcedures, redirectPath, whiteList);
50
+ ```
51
+
52
+ The requester middleware operates on the client side (web browser) and provides auth informations with every request.
53
+
54
+ ```ts
55
+ // src/middleware/requesterMiddleware.ts
56
+
57
+ import { RequesterMiddleware } from '@jitar-plugins/authentication';
58
+
59
+ // The server provides a session key after login that needs to be captured.
60
+ const key = new URLSearchParams(globalThis.location?.search).get('key');
61
+ const authorization = key !== undefined ? `Bearer ${key}` : undefined;
62
+
63
+ export default new RequesterMiddleware(authorization);
64
+ ```
65
+
66
+ To make sure the client redirects to the original location after login, we also need a third middleware comming from the http package.
67
+
68
+ ```ts
69
+ // src/middleware/originMiddleware.ts
70
+
71
+ import OriginMiddleware from '@jitar-plugins/http';
72
+
73
+ export default new OriginMiddleware();
74
+ ```
75
+
76
+ ### Step 2 - Activate the middleware
77
+
78
+ With the middleware in place, the need to be activated.
79
+
80
+ For the server side, this means adding the authentication middleware to the service configuration.
81
+ This is most likily the proxy / standalone service.
82
+
83
+ ```json
84
+ /* services/proxy.json */
85
+ {
86
+ "url": "http://example.com:3000",
87
+ "middleware": [ /* add middleware here, in this order */
88
+ "./middleware/originMiddleware",
89
+ "./middleware/authenticationMiddleware"
90
+ ],
91
+ "proxy":
92
+ {
93
+ /* service configuration */
94
+ }
95
+ }
96
+ ```
97
+
98
+ On the client side, it needs to be added to the Vite configuration.
99
+
100
+ ```ts
101
+ // vite.config.ts
102
+
103
+ import { defineConfig } from 'vite';
104
+ import react from '@vitejs/plugin-react';
105
+ import jitar from '@jitar/plugin-vite';
106
+
107
+ export default defineConfig({
108
+ build: {
109
+ emptyOutDir: false
110
+ },
111
+ plugins: [
112
+ react(),
113
+ jitar({
114
+ sourceDir: 'src',
115
+ targetDir: 'dist',
116
+ jitarDir: 'domain',
117
+ jitarUrl: 'http://localhost:3000',
118
+ segments: [],
119
+ middleware: [ './middleware/requesterMiddleware' ] // Add middleware here
120
+ })
121
+ ]
122
+ });
123
+ ```
124
+
125
+ ### Step 3 - Implement the auth procedures
126
+
127
+ The authentication middleware refers to three procedures that need to be implemented in the application.
128
+
129
+ ```ts
130
+ // src/domain/authentication/getLoginUrl'.ts
131
+
132
+ export default async function getLoginUrl(): Promise<string>
133
+ {
134
+ // The authentication middleware will provide the login url.
135
+ return '';
136
+ }
137
+ ```
138
+
139
+ ```ts
140
+ // src/domain/authentication/login'.ts
141
+
142
+ export default async function login(identity: Identity): Promise<Requester>
143
+ {
144
+ // Get the requester data from the given identity.
145
+ }
146
+ ```
147
+
148
+ ```ts
149
+ // src/domain/authentication/logout'.ts
150
+
151
+ export default async function logout(): Promise<void>
152
+ {
153
+ // The authentication middleware will handle the logout.
154
+ // Implementent additional logic here.
155
+ }
156
+ ```
157
+
158
+ ### Step 4 - Expose the auth procedures
159
+
160
+ The procedures need to be exposed publicly to make them acessible.
161
+
162
+ ```json
163
+ {
164
+ "./domain/authentication/getLoginUrl": { "default": { "access": "public" } },
165
+ "./domain/authentication/login": { "default": { "access": "public" } },
166
+ "./domain/authentication/logout": { "default": { "access": "public" } }
167
+ }
168
+ ```
169
+
170
+ ### Step 5 - Implement the client redirect path
171
+
172
+ This path will be called after a succesful login with the session key.
173
+
174
+ ```http
175
+ GET http://app.example.com/afterlogin?key=XXXXXX
176
+ ```
177
+
178
+ The requester middleware grabs and stores the key, the app can ignore it.
@@ -0,0 +1,14 @@
1
+ import type { Middleware, NextHandler, Request } from 'jitar';
2
+ import { Response } from 'jitar';
3
+ import type { IdentityProvider } from '@theshelf/authentication';
4
+ type AuthProcedures = {
5
+ loginUrl: string;
6
+ login: string;
7
+ logout: string;
8
+ };
9
+ export default class AuthenticationMiddleware implements Middleware {
10
+ #private;
11
+ constructor(identityProvider: IdentityProvider, authProcedures: AuthProcedures, redirectPath: string, whiteList: string[]);
12
+ handle(request: Request, next: NextHandler): Promise<Response>;
13
+ }
14
+ export {};
@@ -0,0 +1,146 @@
1
+ import { Response, Unauthorized } from 'jitar';
2
+ import crypto from 'node:crypto';
3
+ const IDENTITY_PARAMETER = 'identity';
4
+ const REQUESTER_PARAMETER = '*requester';
5
+ const JITAR_TRUST_HEADER_KEY = 'X-Jitar-Trust-Key';
6
+ const sessions = new Map();
7
+ export default class AuthenticationMiddleware {
8
+ #identityProvider;
9
+ #authProcedures;
10
+ #redirectPath;
11
+ #whiteList;
12
+ constructor(identityProvider, authProcedures, redirectPath, whiteList) {
13
+ this.#identityProvider = identityProvider;
14
+ this.#authProcedures = authProcedures;
15
+ this.#redirectPath = redirectPath;
16
+ this.#whiteList = whiteList;
17
+ }
18
+ async handle(request, next) {
19
+ if (request.hasHeader(JITAR_TRUST_HEADER_KEY)) {
20
+ return next();
21
+ }
22
+ switch (request.fqn) {
23
+ case this.#authProcedures.loginUrl: return this.#getLoginUrl(request);
24
+ case this.#authProcedures.login: return this.#createSession(request, next);
25
+ case this.#authProcedures.logout: return this.#destroySession(request, next);
26
+ default: return this.#handleRequest(request, next);
27
+ }
28
+ }
29
+ async #getLoginUrl(request) {
30
+ const origin = this.#getOrigin(request);
31
+ const url = await this.#identityProvider.getLoginUrl(origin);
32
+ return new Response(200, url);
33
+ }
34
+ async #createSession(request, next) {
35
+ const data = Object.fromEntries(request.args);
36
+ const origin = this.#getOrigin(request);
37
+ const session = await this.#identityProvider.login(origin, data);
38
+ request.args.clear();
39
+ request.setArgument(IDENTITY_PARAMETER, session.identity);
40
+ const response = await next();
41
+ if (response.status !== 200) {
42
+ await this.#identityProvider.logout(session);
43
+ return response;
44
+ }
45
+ session.key = this.#generateKey();
46
+ session.requester = response.result;
47
+ sessions.set(session.key, session);
48
+ this.#setAuthorizationHeader(response, session);
49
+ this.#setRedirectHeader(response, session.key, origin);
50
+ return response;
51
+ }
52
+ async #destroySession(request, next) {
53
+ const key = this.#extractAuthorizationKey(request);
54
+ if (key === undefined) {
55
+ throw new Unauthorized('Invalid authorization key');
56
+ }
57
+ const session = this.#getSession(key);
58
+ await this.#identityProvider.logout(session);
59
+ sessions.delete(key);
60
+ return next();
61
+ }
62
+ async #handleRequest(request, next) {
63
+ const storedSession = this.#authorize(request);
64
+ if (storedSession === undefined) {
65
+ return next();
66
+ }
67
+ const activeSession = this.#isSessionExpired(storedSession)
68
+ ? await this.#refreshSession(storedSession)
69
+ : storedSession;
70
+ request.setArgument(REQUESTER_PARAMETER, activeSession.requester);
71
+ const response = await next();
72
+ if (activeSession !== storedSession) {
73
+ this.#setAuthorizationHeader(response, activeSession);
74
+ }
75
+ return response;
76
+ }
77
+ #authorize(request) {
78
+ const key = this.#extractAuthorizationKey(request);
79
+ return key === undefined
80
+ ? this.#authorizePublic(request.fqn)
81
+ : this.#authorizeProtected(key);
82
+ }
83
+ #authorizePublic(fqn) {
84
+ if (this.#whiteList.includes(fqn)) {
85
+ return;
86
+ }
87
+ throw new Unauthorized('Not a public resource');
88
+ }
89
+ #authorizeProtected(key) {
90
+ return this.#getSession(key);
91
+ }
92
+ #getSession(key) {
93
+ const session = sessions.get(key);
94
+ if (session === undefined) {
95
+ throw new Unauthorized('Invalid authorization key');
96
+ }
97
+ return session;
98
+ }
99
+ #isSessionExpired(session) {
100
+ const now = new Date();
101
+ return session.expires < now;
102
+ }
103
+ async #refreshSession(session) {
104
+ try {
105
+ const newSession = await this.#identityProvider.refresh(session);
106
+ newSession.key = this.#generateKey();
107
+ sessions.delete(session.key);
108
+ sessions.set(newSession.key, newSession);
109
+ return newSession;
110
+ }
111
+ catch {
112
+ throw new Unauthorized('Session expired');
113
+ }
114
+ }
115
+ #extractAuthorizationKey(request) {
116
+ const authorization = this.#getAuthorizationHeader(request);
117
+ if (authorization === undefined) {
118
+ return;
119
+ }
120
+ const [type, key] = authorization.split(' ');
121
+ if (type.toLowerCase() !== 'bearer') {
122
+ throw new Unauthorized('Invalid authorization type');
123
+ }
124
+ return key;
125
+ }
126
+ #getAuthorizationHeader(request) {
127
+ return request.getHeader('Authorization');
128
+ }
129
+ #setAuthorizationHeader(response, session) {
130
+ response.setHeader('Authorization', `Bearer ${session.key}`);
131
+ }
132
+ #setRedirectHeader(response, key, origin) {
133
+ response.setHeader('Location', new URL(`${this.#redirectPath}?key=${key}`, origin).href);
134
+ }
135
+ #getOrigin(request) {
136
+ return request.getHeader('origin');
137
+ }
138
+ #generateKey() {
139
+ const id1 = crypto.randomUUID();
140
+ const id2 = crypto.randomUUID();
141
+ const id3 = crypto.randomUUID();
142
+ const id4 = crypto.randomUUID();
143
+ const input = id1 + id2 + id3 + id4;
144
+ return crypto.createHash('sha512').update(input, 'utf8').digest('hex');
145
+ }
146
+ }
@@ -0,0 +1,6 @@
1
+ import type { Middleware, NextHandler, Request, Response } from 'jitar';
2
+ export default class RequesterMiddleware implements Middleware {
3
+ #private;
4
+ constructor(authorization?: string);
5
+ handle(request: Request, next: NextHandler): Promise<Response>;
6
+ }
@@ -0,0 +1,24 @@
1
+ export default class RequesterMiddleware {
2
+ #authorization;
3
+ constructor(authorization) {
4
+ this.#authorization = authorization;
5
+ }
6
+ async handle(request, next) {
7
+ if (this.#authorization !== undefined) {
8
+ request.setHeader('Authorization', this.#authorization);
9
+ }
10
+ try {
11
+ const response = await next();
12
+ if (response.hasHeader('Authorization')) {
13
+ this.#authorization = response.getHeader('Authorization');
14
+ }
15
+ return response;
16
+ }
17
+ catch (error) {
18
+ if (error?.constructor?.name === 'Unauthorized') {
19
+ this.#authorization = undefined;
20
+ }
21
+ throw error;
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,2 @@
1
+ export { default as AuthenticationMiddleware } from './AuthenticationMiddleware';
2
+ export { default as RequesterMiddleware } from './RequesterMiddleware';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { default as AuthenticationMiddleware } from './AuthenticationMiddleware';
2
+ export { default as RequesterMiddleware } from './RequesterMiddleware';
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@jitar-plugins/authentication",
3
+ "private": false,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "clean": "rimraf dist",
9
+ "lint": "eslint",
10
+ "review": "npm run build && npm run lint",
11
+ "prepublishOnly": "npm run build"
12
+ },
13
+ "files": [
14
+ "README.md",
15
+ "dist"
16
+ ],
17
+ "types": "dist/index.d.ts",
18
+ "exports": "./dist/index.js",
19
+ "peerDependencies": {
20
+ "@theshelf/authentication": "^0.0.2",
21
+ "jitar": "^0.10.3"
22
+ }
23
+ }