@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 +178 -0
- package/dist/AuthenticationMiddleware.d.ts +14 -0
- package/dist/AuthenticationMiddleware.js +146 -0
- package/dist/RequesterMiddleware.d.ts +6 -0
- package/dist/RequesterMiddleware.js +24 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +23 -0
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,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
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
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
|
+
}
|