authentication-core-lib 0.1.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/README.md +240 -0
- package/package.json +43 -0
- package/src/current-user.ts +57 -0
- package/src/errors.ts +41 -0
- package/src/interfaces.ts +28 -0
- package/src/login.ts +69 -0
- package/src/register.ts +79 -0
package/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# authentication-core-lib
|
|
2
|
+
|
|
3
|
+
A small, framework-agnostic authentication library for TypeScript.
|
|
4
|
+
|
|
5
|
+
The library is intentionally designed without a fixed database layer. It only provides the core logic for:
|
|
6
|
+
|
|
7
|
+
- registration
|
|
8
|
+
- login
|
|
9
|
+
- resolving the current user from a JWT
|
|
10
|
+
- error classes and DTOs
|
|
11
|
+
|
|
12
|
+
Persistence, user lookup, and user creation are connected from the outside via callback functions. This allows you to use the library with an in-memory store, a JSON file, a custom API, or a database.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- Password hashing with argon2id
|
|
17
|
+
- JWT-based authentication
|
|
18
|
+
- Active user validation
|
|
19
|
+
- Registration validation
|
|
20
|
+
- Email notification after successful registration
|
|
21
|
+
- Clear separation between core logic and persistence
|
|
22
|
+
|
|
23
|
+
## Project structure
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
src/
|
|
27
|
+
current-user.ts # Verify JWTs and validate active users
|
|
28
|
+
errors.ts # Central error classes
|
|
29
|
+
interfaces.ts # Shared types and DTOs
|
|
30
|
+
login.ts # Login logic
|
|
31
|
+
register.ts # Registration logic
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
The library expects the following runtime dependencies:
|
|
37
|
+
|
|
38
|
+
- argon2
|
|
39
|
+
- jsonwebtoken
|
|
40
|
+
- nodemailer
|
|
41
|
+
|
|
42
|
+
You will typically also want TypeScript.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install argon2 jsonwebtoken nodemailer
|
|
46
|
+
npm install -D typescript @types/node
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
### 1. Provide a user store
|
|
52
|
+
|
|
53
|
+
The login and registration logic works with a `FetchedUser` object and callback functions. For example, you can build an in-memory store:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { AuthenticationCoreLogin } from './src/login.ts'
|
|
57
|
+
import { AuthenticationCoreRegister } from './src/register.ts'
|
|
58
|
+
import { AuthenticationCoreCurrentUser } from './src/current-user.ts'
|
|
59
|
+
import type {
|
|
60
|
+
FetchedUser,
|
|
61
|
+
RegistrationInputData,
|
|
62
|
+
VerificationMail,
|
|
63
|
+
MailTransportConfig,
|
|
64
|
+
} from './src/interfaces.ts'
|
|
65
|
+
|
|
66
|
+
const users = new Map<string, FetchedUser>()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 2. Registration
|
|
70
|
+
|
|
71
|
+
The following callbacks belong to the registration flow:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
async function mailExistsRoutine(mail: string): Promise<boolean> {
|
|
75
|
+
return users.has(mail)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function dataProcessing(
|
|
79
|
+
identification: string,
|
|
80
|
+
hashedPassword: string,
|
|
81
|
+
customInputData: Record<string, unknown>
|
|
82
|
+
): Promise<FetchedUser> {
|
|
83
|
+
const user: FetchedUser = {
|
|
84
|
+
uuid: crypto.randomUUID(),
|
|
85
|
+
mail: identification,
|
|
86
|
+
password: hashedPassword,
|
|
87
|
+
isActive: false,
|
|
88
|
+
...customInputData,
|
|
89
|
+
} as FetchedUser
|
|
90
|
+
|
|
91
|
+
users.set(identification, user)
|
|
92
|
+
return user
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const registrationInputData: RegistrationInputData = {
|
|
98
|
+
typedMail: 'demo@example.com',
|
|
99
|
+
typedPassword: 'secret-password',
|
|
100
|
+
typedPasswordRepeated: 'secret-password',
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const verificationMail: VerificationMail = {
|
|
104
|
+
from: 'no-reply@example.com',
|
|
105
|
+
subject: 'Verify your account',
|
|
106
|
+
content: (uuid: string) => `https://example.com/verify?uuid=${uuid}`,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const mailTransportConfig: MailTransportConfig = {
|
|
110
|
+
host: '127.0.0.1',
|
|
111
|
+
port: 1025,
|
|
112
|
+
secure: false,
|
|
113
|
+
auth: {
|
|
114
|
+
user: '',
|
|
115
|
+
pass: '',
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const newUser = await AuthenticationCoreRegister.register(
|
|
120
|
+
registrationInputData,
|
|
121
|
+
mailExistsRoutine,
|
|
122
|
+
{},
|
|
123
|
+
dataProcessing,
|
|
124
|
+
verificationMail,
|
|
125
|
+
mailTransportConfig,
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 3. Login
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
const token = await AuthenticationCoreLogin.login(
|
|
133
|
+
'demo@example.com',
|
|
134
|
+
'secret-password',
|
|
135
|
+
users.get('demo@example.com'),
|
|
136
|
+
'your-jwt-secret'
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 4. Resolve the current user
|
|
141
|
+
|
|
142
|
+
The following callback belongs to the current-user flow:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
async function isActiveCallback(uuid: string): Promise<boolean> {
|
|
146
|
+
for (const user of users.values()) {
|
|
147
|
+
if (user.uuid === uuid) {
|
|
148
|
+
return user.isActive
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return false
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const userUuid = await AuthenticationCoreCurrentUser.getCurrentUser(
|
|
158
|
+
token,
|
|
159
|
+
'your-jwt-secret',
|
|
160
|
+
isActiveCallback,
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## API overview
|
|
165
|
+
|
|
166
|
+
### `AuthenticationCoreLogin.login(...)`
|
|
167
|
+
|
|
168
|
+
Checks mail and password, validates the user status, and generates a JWT.
|
|
169
|
+
|
|
170
|
+
**Parameters:**
|
|
171
|
+
|
|
172
|
+
- `typedMail`: email address
|
|
173
|
+
- `typedPassword`: plaintext password
|
|
174
|
+
- `fetchedUser`: already loaded user or `undefined`
|
|
175
|
+
- `jwtKey`: JWT secret
|
|
176
|
+
- `jwtOptions`: optional JWT options
|
|
177
|
+
|
|
178
|
+
**Returns:** JWT as a string
|
|
179
|
+
|
|
180
|
+
### `AuthenticationCoreRegister.register(...)`
|
|
181
|
+
|
|
182
|
+
Validates registration input, checks whether the mail address is already taken, hashes the password, and sends a verification email after successful persistence.
|
|
183
|
+
|
|
184
|
+
**Parameters:**
|
|
185
|
+
|
|
186
|
+
- `registrationInputData`: mail, password, and password confirmation
|
|
187
|
+
- `mailExistsRoutine`: callback for mail lookup
|
|
188
|
+
- `customInputData`: additional user data
|
|
189
|
+
- `dataProcessing`: callback for storing the new user
|
|
190
|
+
- `verificationMail`: verification email configuration
|
|
191
|
+
- `mailTransportConfig`: SMTP configuration
|
|
192
|
+
- `hashOptions`: argon2 options
|
|
193
|
+
|
|
194
|
+
**Returns:** the stored user
|
|
195
|
+
|
|
196
|
+
### `AuthenticationCoreCurrentUser.getCurrentUser(...)`
|
|
197
|
+
|
|
198
|
+
Verifies a JWT, reads the user ID from `sub`, and checks whether the user is active.
|
|
199
|
+
|
|
200
|
+
**Parameters:**
|
|
201
|
+
|
|
202
|
+
- `token`: JWT
|
|
203
|
+
- `jwtKey`: secret or public key
|
|
204
|
+
- `isActiveCallback`: callback for active-user lookup
|
|
205
|
+
- `verifyOptions`: optional JWT verification options
|
|
206
|
+
|
|
207
|
+
**Returns:** the current user's UUID
|
|
208
|
+
|
|
209
|
+
## Error classes
|
|
210
|
+
|
|
211
|
+
The library provides custom error classes with `code` and `statusCode`:
|
|
212
|
+
|
|
213
|
+
- `AuthError`
|
|
214
|
+
- `InvalidCredentialsError`
|
|
215
|
+
- `UserInactiveError`
|
|
216
|
+
- `InvalidTokenError`
|
|
217
|
+
- `MailTakenError`
|
|
218
|
+
- `PasswordMismatchError`
|
|
219
|
+
|
|
220
|
+
This makes it easy to handle errors cleanly in your API or UI.
|
|
221
|
+
|
|
222
|
+
## Important notes
|
|
223
|
+
|
|
224
|
+
- The library does not include a fixed database integration.
|
|
225
|
+
- Persistence is fully provided through callbacks.
|
|
226
|
+
- For production, use a strong JWT secret key.
|
|
227
|
+
- For registration emails, use a real SMTP configuration.
|
|
228
|
+
- `argon2.verify()` expects the stored hash first and the plaintext password second.
|
|
229
|
+
|
|
230
|
+
## Example for custom persistence
|
|
231
|
+
|
|
232
|
+
You can easily connect the library to a database, a REST service, or an in-memory store. The only things you need are suitable implementations for:
|
|
233
|
+
|
|
234
|
+
- `mailExistsRoutine`
|
|
235
|
+
- `dataProcessing`
|
|
236
|
+
- `isActiveCallback`
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "authentication-core-lib",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Framework-agnostic authentication core for TypeScript.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"authentication",
|
|
9
|
+
"jwt",
|
|
10
|
+
"argon2",
|
|
11
|
+
"typescript",
|
|
12
|
+
"library"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"exports": {
|
|
19
|
+
"./login": "./src/login.ts",
|
|
20
|
+
"./register": "./src/register.ts",
|
|
21
|
+
"./current-user": "./src/current-user.ts",
|
|
22
|
+
"./errors": "./src/errors.ts",
|
|
23
|
+
"./interfaces": "./src/interfaces.ts"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"lint": "tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"argon2": "0.44.0",
|
|
33
|
+
"jsonwebtoken": "9.0.3",
|
|
34
|
+
"nodemailer": "9.0.3"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "26.1.0",
|
|
38
|
+
"typescript": "6.0.3"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// @ts-ignore jsonwebtoken is supplied by the server package in generated projects.
|
|
2
|
+
import jwt from "jsonwebtoken";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
InvalidTokenError,
|
|
6
|
+
UserInactiveError
|
|
7
|
+
} from "./errors";
|
|
8
|
+
|
|
9
|
+
export namespace AuthenticationCoreCurrentUser {
|
|
10
|
+
const DEFAULT_VERIFY_OPTIONS: jwt.VerifyOptions = {
|
|
11
|
+
algorithms: ["HS256"]
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function asyncVerify(
|
|
15
|
+
token: string,
|
|
16
|
+
secretOrPublicKey: jwt.Secret | jwt.PublicKey,
|
|
17
|
+
options: jwt.VerifyOptions = {}
|
|
18
|
+
): Promise<string | jwt.JwtPayload> {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
jwt.verify(token, secretOrPublicKey, options, (err: jwt.VerifyErrors | null, decoded: object | string | undefined) => {
|
|
21
|
+
if (err) reject(err);
|
|
22
|
+
else resolve(decoded as string | jwt.JwtPayload);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function getCurrentUser(
|
|
28
|
+
token: string,
|
|
29
|
+
jwtKey: jwt.Secret,
|
|
30
|
+
isActiveCallback: (uuid: string) => Promise<boolean>,
|
|
31
|
+
verifyOptions: jwt.VerifyOptions = {}
|
|
32
|
+
): Promise<string> {
|
|
33
|
+
const options = { ...DEFAULT_VERIFY_OPTIONS, ...verifyOptions };
|
|
34
|
+
|
|
35
|
+
let payload: string | jwt.JwtPayload;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
payload = await asyncVerify(token, jwtKey, options);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
throw new InvalidTokenError("Token verification failed");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof payload === "string") {
|
|
44
|
+
throw new InvalidTokenError("Invalid token payload type");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof payload.sub !== "string") {
|
|
48
|
+
throw new InvalidTokenError("Token payload does not contain valid 'sub'");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!(await isActiveCallback(payload.sub))) {
|
|
52
|
+
throw new UserInactiveError();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return payload.sub;
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export class AuthError extends Error {
|
|
2
|
+
code: string;
|
|
3
|
+
statusCode: number;
|
|
4
|
+
|
|
5
|
+
constructor(message: string, code: string, statusCode: number) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = new.target.name;
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.statusCode = statusCode;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class InvalidCredentialsError extends AuthError {
|
|
14
|
+
constructor() {
|
|
15
|
+
super("Invalid credentials", "INVALID_CREDENTIALS", 401);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class UserInactiveError extends AuthError {
|
|
20
|
+
constructor() {
|
|
21
|
+
super("User is inactive", "USER_INACTIVE", 403);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class InvalidTokenError extends AuthError {
|
|
26
|
+
constructor(message = "Invalid token") {
|
|
27
|
+
super(message, "INVALID_TOKEN", 401);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class MailTakenError extends AuthError {
|
|
32
|
+
constructor() {
|
|
33
|
+
super("Email is already taken", "MAIL_TAKEN", 409);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class PasswordMismatchError extends AuthError {
|
|
38
|
+
constructor() {
|
|
39
|
+
super("Passwords do not match", "PASSWORD_MISMATCH", 400);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface FetchedUser {
|
|
2
|
+
uuid: string;
|
|
3
|
+
mail: string;
|
|
4
|
+
password: string;
|
|
5
|
+
isActive: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface RegistrationInputData {
|
|
9
|
+
typedMail: string;
|
|
10
|
+
typedPassword: string;
|
|
11
|
+
typedPasswordRepeated: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface VerificationMail {
|
|
15
|
+
from: string;
|
|
16
|
+
subject: string;
|
|
17
|
+
content(uuid: string): string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MailTransportConfig {
|
|
21
|
+
host: string;
|
|
22
|
+
port: number;
|
|
23
|
+
secure: boolean;
|
|
24
|
+
auth: {
|
|
25
|
+
user: string;
|
|
26
|
+
pass: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
package/src/login.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// @ts-ignore jsonwebtoken is supplied by the server package in generated projects.
|
|
2
|
+
import jwt from "jsonwebtoken";
|
|
3
|
+
// @ts-ignore argon2 is supplied by the server package in generated projects.
|
|
4
|
+
import argon2 from "argon2";
|
|
5
|
+
|
|
6
|
+
import { FetchedUser } from "./interfaces";
|
|
7
|
+
import {
|
|
8
|
+
InvalidCredentialsError,
|
|
9
|
+
UserInactiveError
|
|
10
|
+
} from "./errors";
|
|
11
|
+
|
|
12
|
+
export namespace AuthenticationCoreLogin {
|
|
13
|
+
const DEFAULT_JWT_OPTIONS: jwt.SignOptions = {
|
|
14
|
+
algorithm: "HS256",
|
|
15
|
+
expiresIn: "7d"
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function asyncSign(
|
|
19
|
+
payload: string | object | any,
|
|
20
|
+
secretOrPrivateKey: jwt.Secret | jwt.PrivateKey,
|
|
21
|
+
options: jwt.SignOptions = {}
|
|
22
|
+
): Promise<string> {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
jwt.sign(payload, secretOrPrivateKey, options, (err: jwt.VerifyErrors | null, token: string | undefined) => {
|
|
25
|
+
if (err) reject(err);
|
|
26
|
+
else resolve(token as string);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function login(
|
|
32
|
+
typedMail: string,
|
|
33
|
+
typedPassword: string,
|
|
34
|
+
fetchedUser: FetchedUser | undefined,
|
|
35
|
+
jwtKey: jwt.Secret,
|
|
36
|
+
jwtOptions: jwt.SignOptions = {}
|
|
37
|
+
): Promise<string> {
|
|
38
|
+
// TODO: Refactor validation logic to mitigate timing attacks (Username Enumeration).
|
|
39
|
+
// Checking 'fetchedUser' and 'typedName' immediately allows attackers to determine
|
|
40
|
+
// valid usernames based on API response times, as 'argon2.verify' is significantly slower.
|
|
41
|
+
// Consider running a dummy argon2 verification when a user is not found, or returning
|
|
42
|
+
// a generic 'InvalidCredentialsError' for all authentication failures.
|
|
43
|
+
if (!fetchedUser || typedMail !== fetchedUser.mail) {
|
|
44
|
+
// DUMMY_HASH by dummy generated password
|
|
45
|
+
const DUMMY_HASH = "$argon2id$v=19$m=16,t=2,p=1$cW5BYVZZc3lqWUplbEgyRA$Bp/WqdeZVSHxIIrTR5EQCw";
|
|
46
|
+
await argon2.verify(DUMMY_HASH, typedPassword); // Dummy verification to mitigate timing attacks
|
|
47
|
+
throw new InvalidCredentialsError();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!(await argon2.verify(fetchedUser.password, typedPassword))) {
|
|
51
|
+
throw new InvalidCredentialsError();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!fetchedUser.isActive) {
|
|
55
|
+
throw new UserInactiveError();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const options = { ...DEFAULT_JWT_OPTIONS, ...jwtOptions };
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// Signing is async because JWT algorithms may vary in cost.
|
|
62
|
+
// While HS256 is cheap, other algorithms (e.g. RS256/ES256) or external signers (KMS/HSM)
|
|
63
|
+
// can be significantly more expensive or I/O-bound, so the API must avoid blocking the event loop.
|
|
64
|
+
return await asyncSign({ sub: fetchedUser.uuid }, jwtKey, options);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new Error("An unexpected error occurred during login.");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/register.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// @ts-ignore argon2 is supplied by the server package in generated projects.
|
|
2
|
+
import argon2 from "argon2";
|
|
3
|
+
// @ts-ignore nodemailer is supplied by the server package in generated projects.
|
|
4
|
+
import nodemailer from "nodemailer";
|
|
5
|
+
|
|
6
|
+
import { FetchedUser, MailTransportConfig, RegistrationInputData, VerificationMail } from "./interfaces";
|
|
7
|
+
import {
|
|
8
|
+
AuthError,
|
|
9
|
+
MailTakenError,
|
|
10
|
+
PasswordMismatchError
|
|
11
|
+
} from "./errors";
|
|
12
|
+
|
|
13
|
+
export namespace AuthenticationCoreRegister {
|
|
14
|
+
const DEFAULT_HASH_OPTIONS: argon2.Options = {
|
|
15
|
+
type: argon2.argon2id,
|
|
16
|
+
memoryCost: 2 ** 17,
|
|
17
|
+
timeCost: 4,
|
|
18
|
+
parallelism: 1
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const DEFAULT_MAIL_TRANSPORT_CONFIG: MailTransportConfig = {
|
|
22
|
+
host: "smtp.example.com",
|
|
23
|
+
port: 587,
|
|
24
|
+
secure: false,
|
|
25
|
+
auth: {
|
|
26
|
+
user: "max.mustermann@example.com",
|
|
27
|
+
pass: "password"
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export async function register(
|
|
32
|
+
registrationInputData: RegistrationInputData,
|
|
33
|
+
mailExistsRoutine: (mail: string) => Promise<boolean>,
|
|
34
|
+
customInputData: Record<string, unknown>, // e.g. name, address that should be stored in the database for the new user
|
|
35
|
+
dataProcessing: (identification: string, hashedPassword: string, customInputData: Record<string, unknown>) => Promise<FetchedUser | undefined>,
|
|
36
|
+
verificationMail: VerificationMail,
|
|
37
|
+
mailTransportConfig: MailTransportConfig = DEFAULT_MAIL_TRANSPORT_CONFIG,
|
|
38
|
+
hashOptions: argon2.Options = DEFAULT_HASH_OPTIONS
|
|
39
|
+
): Promise<FetchedUser> {
|
|
40
|
+
let mailExists: boolean;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
mailExists = await mailExistsRoutine(registrationInputData.typedMail);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
throw e;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (mailExists) {
|
|
49
|
+
throw new MailTakenError();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (registrationInputData.typedPassword !== registrationInputData.typedPasswordRepeated) {
|
|
53
|
+
throw new PasswordMismatchError();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// argon2.hash adds a random salt automatically (embedded in the hash output).
|
|
58
|
+
const hashedPassword = await argon2.hash(registrationInputData.typedPassword, hashOptions);
|
|
59
|
+
const newUser = await dataProcessing(registrationInputData.typedMail, hashedPassword, customInputData);
|
|
60
|
+
|
|
61
|
+
if (!newUser || !newUser.uuid) {
|
|
62
|
+
throw new AuthError("Data processing did not return a valid user object with a UUID", "INVALID_USER_OBJECT", 500);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const transporter = nodemailer.createTransport(mailTransportConfig);
|
|
66
|
+
|
|
67
|
+
await transporter.sendMail({
|
|
68
|
+
from: verificationMail.from,
|
|
69
|
+
to: newUser.mail,
|
|
70
|
+
subject: verificationMail.subject,
|
|
71
|
+
text: verificationMail.content(newUser.uuid)
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return newUser;
|
|
75
|
+
} catch (e) {
|
|
76
|
+
throw e;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|