@siran/auth-core 0.9.0 → 0.14.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/CHANGELOG.md +44 -0
- package/COMPOSITE_AUTH_SERVICE.md +104 -0
- package/package.json +1 -1
- package/src/application/policies/account-already-exists.policy.ts +23 -0
- package/src/application/policies/magic-link-token.policy.ts +23 -0
- package/src/application/policies/oauth-code.policy.ts +23 -0
- package/src/application/policies/oauth-provider.policy.ts +29 -0
- package/src/application/policies/otp-code-format.policy.ts +23 -0
- package/src/application/policies/otp-code.policy.ts +23 -0
- package/src/application/policies/otp-identifier.policy.ts +23 -0
- package/src/application/policies/password-identifier.policy.ts +23 -0
- package/src/application/policies/password-min-length.policy.ts +29 -0
- package/src/application/policies/password-present.policy.ts +23 -0
- package/src/application/policies/password-strength.policy.ts +27 -0
- package/src/application/ports/auth-preference-service.port.ts +5 -0
- package/src/application/ports/auth-service.port.ts +53 -14
- package/src/application/ports/user-existence.port.ts +3 -0
- package/src/application/services/composite-auth.service.ts +96 -0
- package/src/auth-engine.factory.ts +35 -10
- package/src/index.ts +9 -0
- package/tests/application/policies/magic-link-token.policy.test.ts +74 -0
- package/tests/application/policies/oauth-code.policy.test.ts +79 -0
- package/tests/application/policies/oauth-provider.policy.test.ts +76 -0
- package/tests/application/policies/otp-code-format.policy.test.ts +90 -0
- package/tests/application/policies/otp-code.policy.test.ts +55 -0
- package/tests/application/policies/otp-identifier.policy.test.ts +55 -0
- package/tests/application/policies/password-identifier.policy.test.ts +55 -0
- package/tests/application/policies/password-min-length.policy.test.ts +66 -0
- package/tests/application/policies/password-present.policy.test.ts +55 -0
- package/tests/application/policies/password-strength.policy.test.ts +66 -0
- package/tests/application/ports/auth-service.port.mock.ts +0 -2
- package/tests/application/services/composite-auth.mock.ts +18 -0
- package/tests/application/services/composite-auth.test.ts +162 -0
- package/tsconfig.lib.json +3 -2
- package/src/application/services/account-already-exists.service.ts +0 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,47 @@
|
|
|
1
|
+
## 0.14.0 (2026-01-25)
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- update index.ts to export new application ports ([de8d09e](https://github.com/narisraz/auth/commit/de8d09e))
|
|
6
|
+
|
|
7
|
+
### ❤️ Thank You
|
|
8
|
+
|
|
9
|
+
- Naris Razafimahatratra
|
|
10
|
+
|
|
11
|
+
## 0.13.0 (2026-01-25)
|
|
12
|
+
|
|
13
|
+
### 🚀 Features
|
|
14
|
+
|
|
15
|
+
- add comprehensive tests for authentication policies ([5a4b9ff](https://github.com/narisraz/auth/commit/5a4b9ff))
|
|
16
|
+
|
|
17
|
+
### ❤️ Thank You
|
|
18
|
+
|
|
19
|
+
- Naris Razafimahatratra
|
|
20
|
+
|
|
21
|
+
## 0.12.0 (2026-01-25)
|
|
22
|
+
|
|
23
|
+
### 🚀 Features
|
|
24
|
+
|
|
25
|
+
- integrate UserExistencePort for account existence checks ([47fcde5](https://github.com/narisraz/auth/commit/47fcde5))
|
|
26
|
+
|
|
27
|
+
### ❤️ Thank You
|
|
28
|
+
|
|
29
|
+
- Naris Razafimahatratra
|
|
30
|
+
|
|
31
|
+
## 0.11.0 (2026-01-25)
|
|
32
|
+
|
|
33
|
+
### 🚀 Features
|
|
34
|
+
|
|
35
|
+
- enhance authentication framework with CompositeAuthService and policies ([ac9fa89](https://github.com/narisraz/auth/commit/ac9fa89))
|
|
36
|
+
|
|
37
|
+
### ❤️ Thank You
|
|
38
|
+
|
|
39
|
+
- Naris Razafimahatratra
|
|
40
|
+
|
|
41
|
+
## 0.10.0 (2026-01-24)
|
|
42
|
+
|
|
43
|
+
This was a version bump only for @siran/auth-core to align it with other projects, there were no code changes.
|
|
44
|
+
|
|
1
45
|
## 0.9.0 (2026-01-24)
|
|
2
46
|
|
|
3
47
|
### 🚀 Features
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# CompositeAuthService Usage
|
|
2
|
+
|
|
3
|
+
The `CompositeAuthService` allows you to configure different adapters for each authentication method type with an immutable configuration.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { CompositeAuthService } from '@siran/auth-core';
|
|
9
|
+
import { PasswordAdapter } from '@siran/password-adapter';
|
|
10
|
+
import { OAuthAdapter } from '@siran/oauth-adapter';
|
|
11
|
+
|
|
12
|
+
const authService = new CompositeAuthService({
|
|
13
|
+
password: new PasswordAdapter(),
|
|
14
|
+
oauth: new OAuthAdapter(),
|
|
15
|
+
// otp: new OTPAdapter(), // Optional
|
|
16
|
+
// magic_link: new MagicLinkAdapter(), // Optional
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Supported Methods
|
|
21
|
+
|
|
22
|
+
- `password`: For username/password authentication
|
|
23
|
+
- `oauth`: For OAuth provider authentication (Google, GitHub, Apple, Facebook)
|
|
24
|
+
- `otp`: For one-time password authentication
|
|
25
|
+
- `magic_link`: For magic link authentication
|
|
26
|
+
|
|
27
|
+
## Error Handling
|
|
28
|
+
|
|
29
|
+
The service provides specific error codes for different failure scenarios:
|
|
30
|
+
|
|
31
|
+
### Method/Payload Errors
|
|
32
|
+
|
|
33
|
+
- `ADAPTER_UNAVAILABLE`: No adapter configured for the auth method
|
|
34
|
+
- `INVALID_METHOD_PAYLOAD`: Malformed or incomplete auth method data
|
|
35
|
+
- `UNSUPPORTED_OAUTH_PROVIDER`: OAuth provider not in supported list
|
|
36
|
+
|
|
37
|
+
### Authentication Errors
|
|
38
|
+
|
|
39
|
+
- `INVALID_CREDENTIALS`: Username/password incorrect or invalid credentials
|
|
40
|
+
|
|
41
|
+
### Account Status Errors
|
|
42
|
+
|
|
43
|
+
- `ACCOUNT_DISABLED`: Account is disabled
|
|
44
|
+
- `ACCOUNT_NOT_VERIFIED`: Account email not verified
|
|
45
|
+
- `ACCOUNT_LOCKED`: Account permanently locked
|
|
46
|
+
- `ACCOUNT_LOCKED_TEMPORARILY`: Account temporarily locked
|
|
47
|
+
|
|
48
|
+
### Policy Errors
|
|
49
|
+
|
|
50
|
+
- `PRE_REGISTER_POLICY_VIOLATED`: Pre-registration policy violated
|
|
51
|
+
- `RATE_LIMIT_EXCEEDED`: Too many authentication attempts
|
|
52
|
+
|
|
53
|
+
### System Errors
|
|
54
|
+
|
|
55
|
+
- `OAUTH_PROVIDER_DOWN`: OAuth provider service unavailable
|
|
56
|
+
- `NETWORK_ERROR`: Network connectivity issues
|
|
57
|
+
- `INTERNAL_ERROR`: Unexpected server error
|
|
58
|
+
|
|
59
|
+
## Usage Example
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// Password authentication
|
|
63
|
+
const passwordResult = await authService.authenticate({
|
|
64
|
+
type: 'password',
|
|
65
|
+
identifier: 'user@example.com',
|
|
66
|
+
password: 'password123',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// OAuth authentication
|
|
70
|
+
const oauthResult = await authService.authenticate({
|
|
71
|
+
type: 'oauth',
|
|
72
|
+
provider: 'google',
|
|
73
|
+
code: 'auth-code',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// If a method is not configured, it returns a specific error:
|
|
77
|
+
const magicLinkResult = await authService.authenticate({
|
|
78
|
+
type: 'magic_link',
|
|
79
|
+
token: 'token',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
console.log(magicLinkResult.ok); // false
|
|
83
|
+
console.log(magicLinkResult.error); // 'ADAPTER_UNAVAILABLE'
|
|
84
|
+
|
|
85
|
+
// Invalid payloads are caught early:
|
|
86
|
+
const invalidPassword = await authService.authenticate({
|
|
87
|
+
type: 'password',
|
|
88
|
+
identifier: 'test@example.com',
|
|
89
|
+
password: 'short', // Too short
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
console.log(invalidPassword.error); // 'INVALID_CREDENTIALS'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Configuration Access
|
|
96
|
+
|
|
97
|
+
The service provides read-only access to the configuration:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
const config = authService.getMethodConfig();
|
|
101
|
+
// Returns: { password: PasswordAdapter, oauth: OAuthAdapter }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Note: The configuration is immutable once created. No runtime adapter management is supported.
|
package/package.json
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
import { AuthMethod, AuthMethodOtp, AuthMethodPassword } from "@application/ports/auth-service.port.js";
|
|
3
|
+
import { PreRegisterPolicy } from "@application/ports/pre-register-policy.port.js";
|
|
4
|
+
import { UserExistencePort } from "@application/ports/user-existence.port.js";
|
|
5
|
+
|
|
6
|
+
export class AccountAlreadyExistsPolicy implements PreRegisterPolicy {
|
|
7
|
+
constructor(
|
|
8
|
+
private readonly userExistence: UserExistencePort,
|
|
9
|
+
private readonly enabled: boolean,
|
|
10
|
+
) { }
|
|
11
|
+
|
|
12
|
+
code: string = 'ACCOUNT_ALREADY_EXISTS';
|
|
13
|
+
|
|
14
|
+
async check(method: AuthMethod): Promise<boolean> {
|
|
15
|
+
const identifier = (method as AuthMethodPassword | AuthMethodOtp).identifier;
|
|
16
|
+
if (!identifier) return false;
|
|
17
|
+
return this.userExistence.userExistsByIdentifier(identifier);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async isActive(method: AuthMethod): Promise<boolean> {
|
|
21
|
+
return this.enabled && method.type === 'password' || method.type === 'otp';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthMethod,
|
|
3
|
+
AuthMethodMagicLink,
|
|
4
|
+
} from '@application/ports/auth-service.port.js';
|
|
5
|
+
import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
|
|
6
|
+
|
|
7
|
+
export class MagicLinkTokenPolicy implements PreRegisterPolicy {
|
|
8
|
+
constructor(private readonly enabled: boolean) { }
|
|
9
|
+
|
|
10
|
+
code: string = 'MISSING_MAGIC_LINK_TOKEN';
|
|
11
|
+
|
|
12
|
+
private checkMagicLink(method: AuthMethodMagicLink): boolean {
|
|
13
|
+
return !!method.token && method.token.length >= 10;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async check(method: AuthMethod): Promise<boolean> {
|
|
17
|
+
return this.checkMagicLink(method as AuthMethodMagicLink);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async isActive(method: AuthMethod): Promise<boolean> {
|
|
21
|
+
return this.enabled && method.type === 'magic_link';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthMethod,
|
|
3
|
+
AuthMethodOAuth,
|
|
4
|
+
} from '@application/ports/auth-service.port.js';
|
|
5
|
+
import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
|
|
6
|
+
|
|
7
|
+
export class OAuthCodePolicy implements PreRegisterPolicy {
|
|
8
|
+
constructor(private readonly enabled: boolean) { }
|
|
9
|
+
|
|
10
|
+
code: string = 'MISSING_OAUTH_CODE';
|
|
11
|
+
|
|
12
|
+
private checkOAuth(method: AuthMethodOAuth): boolean {
|
|
13
|
+
return !!method.code && method.code.length >= 5;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async check(method: AuthMethod): Promise<boolean> {
|
|
17
|
+
return this.checkOAuth(method as AuthMethodOAuth);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async isActive(method: AuthMethod): Promise<boolean> {
|
|
21
|
+
return this.enabled && method.type === 'oauth';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthMethod,
|
|
3
|
+
AuthMethodOAuth,
|
|
4
|
+
} from '@application/ports/auth-service.port.js';
|
|
5
|
+
import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
|
|
6
|
+
|
|
7
|
+
export class OAuthProviderPolicy implements PreRegisterPolicy {
|
|
8
|
+
constructor(private readonly enabled: boolean) { }
|
|
9
|
+
|
|
10
|
+
code: string = 'UNSUPPORTED_OAUTH_PROVIDER';
|
|
11
|
+
|
|
12
|
+
private checkOAuth(method: AuthMethodOAuth): boolean {
|
|
13
|
+
const supportedProviders = [
|
|
14
|
+
'google',
|
|
15
|
+
'github',
|
|
16
|
+
'apple',
|
|
17
|
+
'facebook',
|
|
18
|
+
] as const;
|
|
19
|
+
return supportedProviders.includes(method.provider);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async check(method: AuthMethod): Promise<boolean> {
|
|
23
|
+
return this.checkOAuth(method as AuthMethodOAuth);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async isActive(method: AuthMethod): Promise<boolean> {
|
|
27
|
+
return this.enabled && method.type === 'oauth';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthMethod,
|
|
3
|
+
AuthMethodOtp,
|
|
4
|
+
} from '@application/ports/auth-service.port.js';
|
|
5
|
+
import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
|
|
6
|
+
|
|
7
|
+
export class OtpCodeFormatPolicy implements PreRegisterPolicy {
|
|
8
|
+
constructor(private readonly enabled: boolean) { }
|
|
9
|
+
|
|
10
|
+
code: string = 'INVALID_OTP_FORMAT';
|
|
11
|
+
|
|
12
|
+
private checkOtp(method: AuthMethodOtp): boolean {
|
|
13
|
+
return /^\d{6}$/.test(method.code || '');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async check(method: AuthMethod): Promise<boolean> {
|
|
17
|
+
return this.checkOtp(method as AuthMethodOtp);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async isActive(method: AuthMethod): Promise<boolean> {
|
|
21
|
+
return this.enabled && method.type === 'otp';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthMethod,
|
|
3
|
+
AuthMethodOtp,
|
|
4
|
+
} from '@application/ports/auth-service.port.js';
|
|
5
|
+
import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
|
|
6
|
+
|
|
7
|
+
export class OtpCodePolicy implements PreRegisterPolicy {
|
|
8
|
+
constructor(private readonly enabled: boolean) { }
|
|
9
|
+
|
|
10
|
+
code: string = 'MISSING_OTP_CODE';
|
|
11
|
+
|
|
12
|
+
private checkOtp(method: AuthMethodOtp): boolean {
|
|
13
|
+
return !!method.code;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async check(method: AuthMethod): Promise<boolean> {
|
|
17
|
+
return this.checkOtp(method as AuthMethodOtp);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async isActive(method: AuthMethod): Promise<boolean> {
|
|
21
|
+
return this.enabled && method.type === 'otp';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthMethod,
|
|
3
|
+
AuthMethodOtp,
|
|
4
|
+
} from '@application/ports/auth-service.port.js';
|
|
5
|
+
import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
|
|
6
|
+
|
|
7
|
+
export class OtpIdentifierPolicy implements PreRegisterPolicy {
|
|
8
|
+
constructor(private readonly enabled: boolean) { }
|
|
9
|
+
|
|
10
|
+
code: string = 'MISSING_OTP_IDENTIFIER';
|
|
11
|
+
|
|
12
|
+
private checkOtp(method: AuthMethodOtp): boolean {
|
|
13
|
+
return !!method.identifier;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async check(method: AuthMethod): Promise<boolean> {
|
|
17
|
+
return this.checkOtp(method as AuthMethodOtp);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async isActive(method: AuthMethod): Promise<boolean> {
|
|
21
|
+
return this.enabled && method.type === 'otp';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthMethod,
|
|
3
|
+
AuthMethodPassword,
|
|
4
|
+
} from '@application/ports/auth-service.port.js';
|
|
5
|
+
import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
|
|
6
|
+
|
|
7
|
+
export class PasswordIdentifierPolicy implements PreRegisterPolicy {
|
|
8
|
+
constructor(private readonly enabled: boolean) { }
|
|
9
|
+
|
|
10
|
+
code: string = 'MISSING_PASSWORD_IDENTIFIER';
|
|
11
|
+
|
|
12
|
+
private checkPassword(method: AuthMethodPassword): boolean {
|
|
13
|
+
return !!method.identifier;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async check(method: AuthMethod): Promise<boolean> {
|
|
17
|
+
return this.checkPassword(method as AuthMethodPassword);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async isActive(method: AuthMethod): Promise<boolean> {
|
|
21
|
+
return this.enabled && method.type === 'password';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthMethod,
|
|
3
|
+
AuthMethodPassword,
|
|
4
|
+
} from '@application/ports/auth-service.port.js';
|
|
5
|
+
import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
|
|
6
|
+
|
|
7
|
+
export class PasswordMinLengthPolicy implements PreRegisterPolicy {
|
|
8
|
+
constructor(
|
|
9
|
+
private readonly enabled: boolean,
|
|
10
|
+
private readonly minLength: number = 8
|
|
11
|
+
) {}
|
|
12
|
+
|
|
13
|
+
code: string = 'PASSWORD_TOO_SHORT';
|
|
14
|
+
|
|
15
|
+
private checkPassword(method: AuthMethodPassword): boolean {
|
|
16
|
+
return method.password?.length >= this.minLength;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async check(method: AuthMethod): Promise<boolean> {
|
|
20
|
+
if (method.type === 'password') {
|
|
21
|
+
return this.checkPassword(method);
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async isActive(method: AuthMethod): Promise<boolean> {
|
|
27
|
+
return this.enabled && method.type === 'password';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthMethod,
|
|
3
|
+
AuthMethodPassword,
|
|
4
|
+
} from '@application/ports/auth-service.port.js';
|
|
5
|
+
import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
|
|
6
|
+
|
|
7
|
+
export class PasswordPresentPolicy implements PreRegisterPolicy {
|
|
8
|
+
constructor(private readonly enabled: boolean) { }
|
|
9
|
+
|
|
10
|
+
code: string = 'MISSING_PASSWORD';
|
|
11
|
+
|
|
12
|
+
private checkPassword(method: AuthMethodPassword): boolean {
|
|
13
|
+
return !!method.password;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async check(method: AuthMethod): Promise<boolean> {
|
|
17
|
+
return this.checkPassword(method as AuthMethodPassword);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async isActive(method: AuthMethod): Promise<boolean> {
|
|
21
|
+
return this.enabled && method.type === 'password';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthErrorCode,
|
|
3
|
+
AuthMethod,
|
|
4
|
+
AuthMethodPassword,
|
|
5
|
+
} from '@application/ports/auth-service.port.js';
|
|
6
|
+
import type { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
|
|
7
|
+
|
|
8
|
+
export class PasswordStrengthPolicy implements PreRegisterPolicy {
|
|
9
|
+
code: AuthErrorCode = 'WEAK_PASSWORD';
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
private readonly enabled: boolean,
|
|
13
|
+
private readonly minLength: number = 8
|
|
14
|
+
) { }
|
|
15
|
+
|
|
16
|
+
private checkPassword(method: AuthMethodPassword): boolean {
|
|
17
|
+
return method.password?.length >= this.minLength;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async check(method: AuthMethod): Promise<boolean> {
|
|
21
|
+
return this.checkPassword(method as AuthMethodPassword);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async isActive(method: AuthMethod): Promise<boolean> {
|
|
25
|
+
return this.enabled && method.type === 'password';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export type AuthPreferences = {
|
|
2
2
|
allowLoginWithNotVerifiedAccount: boolean;
|
|
3
3
|
allowDuplicatedAccount: boolean;
|
|
4
|
+
allowWeakPassword: boolean;
|
|
5
|
+
allowMissingPasswordIdentifier: boolean;
|
|
6
|
+
allowMissingPassword: boolean;
|
|
7
|
+
allowMissingOtpIdentifier: boolean;
|
|
8
|
+
allowMissingOtpCode: boolean;
|
|
4
9
|
};
|
|
5
10
|
|
|
6
11
|
export interface AuthPreferencesServicePort {
|
|
@@ -1,20 +1,62 @@
|
|
|
1
|
-
import type { UserAccount } from
|
|
1
|
+
import type { UserAccount } from '@domain/user-account.js';
|
|
2
|
+
|
|
3
|
+
export type AuthMethodPassword = {
|
|
4
|
+
type: 'password';
|
|
5
|
+
identifier: string;
|
|
6
|
+
password: string;
|
|
7
|
+
};
|
|
8
|
+
export type AuthMethodOtp = { type: 'otp'; identifier: string; code: string };
|
|
9
|
+
export type AuthMethodMagicLink = { type: 'magic_link'; token: string };
|
|
10
|
+
export type AuthMethodOAuth = {
|
|
11
|
+
type: 'oauth';
|
|
12
|
+
provider: 'google' | 'github' | 'apple' | 'facebook';
|
|
13
|
+
code: string;
|
|
14
|
+
};
|
|
2
15
|
|
|
3
16
|
export type AuthMethod =
|
|
4
|
-
|
|
|
5
|
-
|
|
|
6
|
-
|
|
|
7
|
-
|
|
|
17
|
+
| AuthMethodPassword
|
|
18
|
+
| AuthMethodOtp
|
|
19
|
+
| AuthMethodMagicLink
|
|
20
|
+
| AuthMethodOAuth;
|
|
8
21
|
|
|
9
22
|
export type AuthErrorCode =
|
|
10
|
-
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
23
|
+
// Method/Payload specific errors
|
|
24
|
+
| 'ADAPTER_UNAVAILABLE' // No adapter configured for this auth method
|
|
25
|
+
| 'INVALID_CREDENTIALS' // Username/password incorrect
|
|
26
|
+
| 'INVALID_METHOD_PAYLOAD' // Malformed auth method data
|
|
27
|
+
| 'UNSUPPORTED_OAUTH_PROVIDER' // OAuth provider not supported
|
|
28
|
+
|
|
29
|
+
// Account status errors
|
|
30
|
+
| 'ACCOUNT_DISABLED' // Account is disabled
|
|
31
|
+
| 'ACCOUNT_NOT_VERIFIED' // Account email not verified
|
|
32
|
+
| 'ACCOUNT_LOCKED' // Account temporarily locked
|
|
33
|
+
| 'ACCOUNT_LOCKED_TEMPORARILY' // Account temporarily locked
|
|
34
|
+
|
|
35
|
+
// Policy errors
|
|
36
|
+
| 'PRE_REGISTER_POLICY_VIOLATED' // Pre-registration policy violated
|
|
37
|
+
| 'RATE_LIMIT_EXCEEDED' // Too many attempts
|
|
38
|
+
| 'WEAK_PASSWORD' // Password doesn't meet strength requirements
|
|
39
|
+
| 'ACCOUNT_ALREADY_EXISTS' // Account already exists
|
|
40
|
+
| 'MISSING_PASSWORD_IDENTIFIER' // Password identifier missing
|
|
41
|
+
| 'MISSING_PASSWORD' // Password missing
|
|
42
|
+
| 'PASSWORD_TOO_SHORT' // Password too short
|
|
43
|
+
| 'MISSING_OTP_IDENTIFIER' // OTP identifier missing
|
|
44
|
+
| 'MISSING_OTP_CODE' // OTP code missing
|
|
45
|
+
| 'INVALID_OTP_FORMAT' // OTP code format invalid
|
|
46
|
+
| 'MISSING_MAGIC_LINK_TOKEN' // Magic link token missing
|
|
47
|
+
| 'MISSING_OAUTH_CODE' // OAuth code missing
|
|
48
|
+
|
|
49
|
+
// System errors
|
|
50
|
+
| 'OAUTH_PROVIDER_DOWN' // OAuth provider unavailable
|
|
51
|
+
| 'NETWORK_ERROR' // Network connectivity issues
|
|
52
|
+
| 'INTERNAL_ERROR'; // Unexpected server error;
|
|
15
53
|
|
|
16
54
|
export type AuthResultSuccess = { ok: true; user: UserAccount };
|
|
17
|
-
export type AuthResultFailure = {
|
|
55
|
+
export type AuthResultFailure = {
|
|
56
|
+
ok: false;
|
|
57
|
+
error: AuthErrorCode;
|
|
58
|
+
violatedPolicies: string[];
|
|
59
|
+
};
|
|
18
60
|
|
|
19
61
|
export type AuthResult = AuthResultSuccess | AuthResultFailure;
|
|
20
62
|
|
|
@@ -22,7 +64,4 @@ export interface AuthServicePort {
|
|
|
22
64
|
authenticate(method: AuthMethod): Promise<AuthResult>;
|
|
23
65
|
register(method: AuthMethod): Promise<AuthResult>;
|
|
24
66
|
logout(): Promise<void>;
|
|
25
|
-
userExists(method: AuthMethod): Promise<boolean>;
|
|
26
67
|
}
|
|
27
|
-
|
|
28
|
-
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthMethod,
|
|
3
|
+
AuthResult,
|
|
4
|
+
AuthServicePort,
|
|
5
|
+
} from '@application/ports/auth-service.port.js';
|
|
6
|
+
|
|
7
|
+
export type AuthMethodConfig = Partial<
|
|
8
|
+
Record<AuthMethod['type'], AuthServicePort>
|
|
9
|
+
>;
|
|
10
|
+
|
|
11
|
+
export class CompositeAuthService implements AuthServicePort {
|
|
12
|
+
private readonly methodConfig: AuthMethodConfig;
|
|
13
|
+
private readonly adapters: AuthServicePort[];
|
|
14
|
+
|
|
15
|
+
constructor(config: AuthMethodConfig) {
|
|
16
|
+
this.methodConfig = config;
|
|
17
|
+
// Extract all adapters from config for operations like logout
|
|
18
|
+
this.adapters = Object.values(config).filter(
|
|
19
|
+
(adapter): adapter is AuthServicePort => adapter !== undefined
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async authenticate(method: AuthMethod): Promise<AuthResult> {
|
|
24
|
+
const adapter = this.getAdapterForMethod(method);
|
|
25
|
+
|
|
26
|
+
if (!adapter) {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
error: 'ADAPTER_UNAVAILABLE',
|
|
30
|
+
violatedPolicies: [],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
return await adapter.authenticate(method);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Authentication error in adapter:', error);
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
error: 'NETWORK_ERROR',
|
|
41
|
+
violatedPolicies: [],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async register(method: AuthMethod): Promise<AuthResult> {
|
|
47
|
+
const adapter = this.getAdapterForMethod(method);
|
|
48
|
+
|
|
49
|
+
if (!adapter) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
error: 'ADAPTER_UNAVAILABLE',
|
|
53
|
+
violatedPolicies: [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
return await adapter.register(method);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Registration error in adapter:', error);
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
error: 'NETWORK_ERROR',
|
|
64
|
+
violatedPolicies: [],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async logout(): Promise<void> {
|
|
70
|
+
// Try to logout from all adapters
|
|
71
|
+
const logoutPromises = this.adapters.map(async (adapter) => {
|
|
72
|
+
try {
|
|
73
|
+
await adapter.logout();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('Logout error in adapter:', error);
|
|
76
|
+
// Continue with other adapters even if one fails
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await Promise.allSettled(logoutPromises);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get adapter for specific auth method
|
|
85
|
+
*/
|
|
86
|
+
private getAdapterForMethod(method: AuthMethod): AuthServicePort | undefined {
|
|
87
|
+
return this.methodConfig[method.type];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get method configuration (read-only)
|
|
92
|
+
*/
|
|
93
|
+
getMethodConfig(): AuthMethodConfig {
|
|
94
|
+
return { ...this.methodConfig };
|
|
95
|
+
}
|
|
96
|
+
}
|