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 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
+ }
@@ -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
+ }