@rineex/auth-core 0.0.2 → 0.0.4
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 +16 -0
- package/eslint.config.mjs +1 -0
- package/package.json +7 -2
- package/src/application/mfa/events/challenge-issue-observability.event.ts +18 -0
- package/src/application/mfa/events/session-started-observability.event.ts +18 -0
- package/src/application/mfa/events/verification-failed-observability.event.ts +14 -0
- package/src/application/mfa/events/verification-succeeded-observibility.event.ts +12 -0
- package/src/application/mfa/issue-mfa-challenge.application-service.ts +75 -0
- package/src/application/mfa/start-mfa-session.application-service.ts +90 -0
- package/src/application/mfa/verify-mfa.application-service.ts +61 -0
- package/src/application/services/auth-orchestrator.service.ts +77 -0
- package/src/application/services/oauth-authorize.service.ts +12 -0
- package/src/domain/{aggregates → identity/aggregates}/authentication-attempt.aggregate.ts +43 -26
- package/src/domain/identity/aggregates/index.ts +1 -0
- package/src/domain/identity/entities/identity.entity.ts +126 -0
- package/src/domain/identity/entities/index.ts +1 -0
- package/src/domain/identity/events/index.ts +3 -0
- package/src/domain/identity/index.ts +4 -0
- package/src/domain/identity/value-objects/__tests__/auth-attempt-id.vo.spec.ts +42 -0
- package/src/domain/identity/value-objects/__tests__/auth-factor.vo.spec.ts +39 -0
- package/src/domain/identity/value-objects/__tests__/auth-method.vo.spec.ts +0 -0
- package/src/domain/{value-objects → identity/value-objects}/auth-attempt-id.vo.ts +4 -0
- package/src/domain/identity/value-objects/auth-factor.vo.ts +17 -0
- package/src/domain/identity/value-objects/auth-method.vo.ts +34 -0
- package/src/domain/identity/value-objects/auth-policy.vo.ts +19 -0
- package/src/domain/{value-objects → identity/value-objects}/identity-id.vo.ts +4 -0
- package/src/domain/identity/value-objects/identity-provider.vo.ts +13 -0
- package/src/domain/identity/value-objects/index.ts +8 -0
- package/src/domain/identity/value-objects/risk-signal.vo.ts +17 -0
- package/src/domain/index.ts +5 -0
- package/src/domain/mfa/aggregates/mfa-session.aggregate.ts +84 -0
- package/src/domain/mfa/entities/mfa-challenge.entity.ts +70 -0
- package/src/domain/mfa/types/mfa-challenge-registry.ts +21 -0
- package/src/domain/mfa/value-objects/mfa-challenge-id.vo.ts +19 -0
- package/src/domain/mfa/value-objects/mfa-challenge-status.vo.ts +31 -0
- package/src/domain/mfa/value-objects/mfa-session-id.vo.ts +19 -0
- package/src/domain/mfa/violations/mfa-active-challenge-exists.violation.ts +10 -0
- package/src/domain/mfa/violations/mfa-already-verified.violation.ts +10 -0
- package/src/domain/mfa/violations/mfa-attempts-exceeded.violation.ts +17 -0
- package/src/domain/mfa/violations/mfa-expired.violation.ts +10 -0
- package/src/domain/oauth/aggregates/oauth-authorization.aggregate.ts +106 -0
- package/src/domain/oauth/aggregates/oauth-authorize.service.ts +0 -0
- package/src/domain/oauth/entities/oauth-authorization.entity.ts +50 -0
- package/src/domain/oauth/value-objects/authorization-code-id.vo.ts +9 -0
- package/src/domain/oauth/value-objects/authorization-code.vo.ts +18 -0
- package/src/domain/oauth/value-objects/client-id.vo.ts +9 -0
- package/src/domain/oauth/value-objects/code-challenge-method.vo.ts +15 -0
- package/src/domain/oauth/value-objects/code-challenge.vo.ts +24 -0
- package/src/domain/oauth/value-objects/oauth-authorization-id.vo.ts +19 -0
- package/src/domain/oauth/value-objects/oauth-provider.vo.ts +15 -0
- package/src/domain/oauth/value-objects/pkce.vo.ts +29 -0
- package/src/domain/oauth/value-objects/redirect-uri.vo.ts +19 -0
- package/src/domain/oauth/value-objects/scope-set.vo.ts +37 -0
- package/src/domain/oauth/violations/authorization-already-used.violation.ts +10 -0
- package/src/domain/oauth/violations/authorization-expired.violation.ts +10 -0
- package/src/domain/oauth/violations/consent-required.violation.ts +10 -0
- package/src/domain/oauth/violations/invalid-authorization-code.violation.ts +12 -0
- package/src/domain/oauth/violations/invalid-oauth-provider.violation.ts +13 -0
- package/src/domain/oauth/violations/invalid-pkce.violation.ts +12 -0
- package/src/domain/oauth/violations/invalid-redirect-uri.violation.ts +10 -0
- package/src/domain/policy/contracts/auth-policy-context.ts +27 -0
- package/src/domain/policy/contracts/auth-policy-decision.ts +7 -0
- package/src/domain/policy/contracts/auth-policy.ts +17 -0
- package/src/domain/policy/contracts/index.ts +3 -0
- package/src/domain/policy/engine/auth-policy-engine.ts +41 -0
- package/src/domain/policy/index.ts +2 -0
- package/src/domain/session/entities/session.entity.ts +82 -0
- package/src/domain/session/value-objects/session-id.vo.ts +10 -0
- package/src/domain/token/aggregates/token.aggregate.ts +34 -0
- package/src/domain/token/value-objects/auth-token.vo.ts +29 -0
- package/src/domain/token/value-objects/session-token.vo.ts +14 -0
- package/src/domain/violations/auth-domain.violation.ts +9 -0
- package/src/domain/violations/invalid-auth-token.violation.ts +13 -0
- package/src/domain/violations/invalid-scope.violation.ts +10 -0
- package/src/domain/violations/invalid-session.violation.ts +13 -0
- package/src/index.ts +3 -1
- package/src/ports/inbound/auth-method.port.ts +1 -1
- package/src/ports/inbound/index.ts +2 -0
- package/src/ports/inbound/start-auth.command.ts +28 -0
- package/src/ports/index.ts +2 -0
- package/src/ports/log/log.port.ts +25 -0
- package/src/ports/mfa/mfa-clock.port.ts +11 -0
- package/src/ports/mfa/mfa-session-id-generator.port.ts +15 -0
- package/src/ports/mfa/mfa-session-repository.port.ts +31 -0
- package/src/ports/observability/observability-event.port.ts +16 -0
- package/src/ports/outbound/authentication-attempt.repository.port.ts +1 -1
- package/src/ports/outbound/domain-event-publisher.port.ts +13 -0
- package/src/ports/outbound/index.ts +2 -0
- package/src/ports/outbound/session.repository.port.ts +9 -0
- package/src/ports/repositories/oauth-authorization.repository.ts +21 -0
- package/src/ports/repositories/token.repository.ts +11 -0
- package/src/types/auth-factor.type.ts +10 -0
- package/src/types/auth-method.type.ts +20 -0
- package/src/types/auth-policy.type.ts +16 -0
- package/src/types/identity-provider.type.ts +8 -0
- package/src/types/index.ts +6 -0
- package/src/types/observability-event.ts +33 -0
- package/src/types/risk-signal.type.ts +11 -0
- package/src/utils/default-if-blank.util.ts +46 -0
- package/tsconfig.json +4 -1
- package/src/domain/entities/identity.entity.ts +0 -13
- package/src/domain/value-objects/auth-method.vo.ts +0 -21
- /package/src/domain/{events → identity/events}/authentication-failed.event.ts +0 -0
- /package/src/domain/{events → identity/events}/authentication-started.event.ts +0 -0
- /package/src/domain/{events → identity/events}/authentication-succeeded.event.ts +0 -0
- /package/src/domain/{value-objects → identity/value-objects}/auth-status.vo.ts +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @rineex/auth-core
|
|
2
2
|
|
|
3
|
+
## 0.0.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies
|
|
8
|
+
[[`0fab4a2`](https://github.com/rineex/core/commit/0fab4a28f4b5b8af947f587448804115a2fd509c)]:
|
|
9
|
+
- @rineex/ddd@1.6.0
|
|
10
|
+
|
|
11
|
+
## 0.0.3
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies
|
|
16
|
+
[[`b1e8e3a`](https://github.com/rineex/core/commit/b1e8e3a4e02644118af82b8f068259d3e5bb2f24)]:
|
|
17
|
+
- @rineex/ddd@1.5.2
|
|
18
|
+
|
|
3
19
|
## 0.0.2
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/eslint.config.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rineex/auth-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Authentication Core package for Rineex core modules",
|
|
5
5
|
"author": "Rineex Team",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@changesets/cli": "2.29.8",
|
|
25
|
+
"@types/lodash.isempty": "4.4.9",
|
|
26
|
+
"@types/lodash.isnil": "4.0.9",
|
|
25
27
|
"@types/node": "24.10.4",
|
|
26
28
|
"@vitest/ui": "4.0.16",
|
|
27
29
|
"tslib": "2.8.1",
|
|
@@ -40,12 +42,15 @@
|
|
|
40
42
|
},
|
|
41
43
|
"license": "Apache-2.0",
|
|
42
44
|
"dependencies": {
|
|
43
|
-
"
|
|
45
|
+
"lodash.isempty": "4.4.0",
|
|
46
|
+
"lodash.isnil": "4.0.0",
|
|
47
|
+
"@rineex/ddd": "1.6.0"
|
|
44
48
|
},
|
|
45
49
|
"scripts": {
|
|
46
50
|
"test": "vitest run --passWithNoTests",
|
|
47
51
|
"test:watch": "vitest --watch --passWithNoTests",
|
|
48
52
|
"lint": "eslint 'src/**/*.ts'",
|
|
53
|
+
"lint:fix": "eslint 'src/**/*.ts' --fix",
|
|
49
54
|
"check-types": "tsc --noEmit",
|
|
50
55
|
"build": "tsup"
|
|
51
56
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ObservabilityEvent } from '@/types/observability-event';
|
|
2
|
+
|
|
3
|
+
export class ChallengeIssueObservabilityEvent extends ObservabilityEvent {
|
|
4
|
+
constructor(params: {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
challengeType: string;
|
|
7
|
+
success?: boolean;
|
|
8
|
+
}) {
|
|
9
|
+
super({
|
|
10
|
+
payload: {
|
|
11
|
+
challengeType: params.challengeType,
|
|
12
|
+
sessionId: params.sessionId,
|
|
13
|
+
success: params.success,
|
|
14
|
+
},
|
|
15
|
+
name: 'authentication.mfa.challenge.issued',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ObservabilityEvent } from '@/types/observability-event';
|
|
2
|
+
|
|
3
|
+
export class SessionStartedObservabilityEvent extends ObservabilityEvent {
|
|
4
|
+
constructor(params: {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
identityId: string;
|
|
7
|
+
reused?: boolean;
|
|
8
|
+
}) {
|
|
9
|
+
super({
|
|
10
|
+
payload: {
|
|
11
|
+
reused: params.reused ?? false,
|
|
12
|
+
identityId: params.identityId,
|
|
13
|
+
sessionId: params.sessionId,
|
|
14
|
+
},
|
|
15
|
+
name: 'authentication.mfa.session.started',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ObservabilityEvent } from '@/types/observability-event';
|
|
2
|
+
|
|
3
|
+
export class VerificationFailedObservabilityEvent extends ObservabilityEvent {
|
|
4
|
+
constructor(sessionId: string, reasonCode: string) {
|
|
5
|
+
super({
|
|
6
|
+
payload: {
|
|
7
|
+
reasonCode,
|
|
8
|
+
sessionId,
|
|
9
|
+
},
|
|
10
|
+
name: 'authentication.mfa.verification_failed',
|
|
11
|
+
occurredAt: new Date(),
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ObservabilityEvent } from '@/types/observability-event';
|
|
2
|
+
|
|
3
|
+
export class VerificationSucceededObservabilityEvent extends ObservabilityEvent {
|
|
4
|
+
constructor(params: { sessionId: string }) {
|
|
5
|
+
super({
|
|
6
|
+
payload: {
|
|
7
|
+
sessionId: params.sessionId,
|
|
8
|
+
},
|
|
9
|
+
name: 'authentication.mfa.verification.succeeded',
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ObservabilityEventPort } from '@/ports/observability/observability-event.port';
|
|
2
|
+
import { MfaSessionRepository } from '@/ports/mfa/mfa-session-repository.port';
|
|
3
|
+
import { MfaSessionId } from '@/domain/mfa/value-objects/mfa-session-id.vo';
|
|
4
|
+
import { MFAChallenge } from '@/domain/mfa/entities/mfa-challenge.entity';
|
|
5
|
+
import { ApplicationServicePort, Result } from '@rineex/ddd';
|
|
6
|
+
import { MfaClock } from '@/ports/mfa/mfa-clock.port';
|
|
7
|
+
import { LoggerPort } from '@/ports/log/log.port';
|
|
8
|
+
|
|
9
|
+
import { ChallengeIssueObservabilityEvent } from './events/challenge-issue-observability.event';
|
|
10
|
+
|
|
11
|
+
type IssueMfaChallengeInput = {
|
|
12
|
+
sessionId: MfaSessionId;
|
|
13
|
+
challenge: MFAChallenge;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type IssueMfaChallengeOutput = Result<void, never>;
|
|
17
|
+
|
|
18
|
+
export class IssueMfaChallengeApplicationService implements ApplicationServicePort<
|
|
19
|
+
IssueMfaChallengeInput,
|
|
20
|
+
IssueMfaChallengeOutput
|
|
21
|
+
> {
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly repository: MfaSessionRepository,
|
|
24
|
+
private readonly clock: MfaClock,
|
|
25
|
+
private readonly logger: LoggerPort,
|
|
26
|
+
private readonly events: ObservabilityEventPort,
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
async execute({
|
|
30
|
+
challenge,
|
|
31
|
+
sessionId,
|
|
32
|
+
}: IssueMfaChallengeInput): Promise<IssueMfaChallengeOutput> {
|
|
33
|
+
try {
|
|
34
|
+
const session = await this.repository.findById(sessionId);
|
|
35
|
+
|
|
36
|
+
if (!session) {
|
|
37
|
+
this.logger.warn('MFA session not found', { sessionId });
|
|
38
|
+
throw new Error('MFA session not found'); // app-layer error
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
session.issueChallenge(challenge, this.clock.now());
|
|
42
|
+
|
|
43
|
+
await this.repository.save(session);
|
|
44
|
+
|
|
45
|
+
this.logger.info('MFA challenge issued', {
|
|
46
|
+
sessionId: sessionId.toString(),
|
|
47
|
+
challengeType: challenge,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
this.events.emit(
|
|
51
|
+
new ChallengeIssueObservabilityEvent({
|
|
52
|
+
challengeType: challenge.props.challengeType,
|
|
53
|
+
sessionId: sessionId.toString(),
|
|
54
|
+
success: true,
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return Result.ok(undefined);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
this.events.emit(
|
|
61
|
+
new ChallengeIssueObservabilityEvent({
|
|
62
|
+
challengeType: challenge.props.challengeType,
|
|
63
|
+
sessionId: sessionId.toString(),
|
|
64
|
+
success: false,
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
this.logger.error('Error issuing MFA challenge', {
|
|
69
|
+
sessionId: sessionId.toString(),
|
|
70
|
+
error,
|
|
71
|
+
});
|
|
72
|
+
return Result.fail(error as never);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ObservabilityEventPort } from '@/ports/observability/observability-event.port';
|
|
2
|
+
import { MfaSessionIdGenerator } from '@/ports/mfa/mfa-session-id-generator.port';
|
|
3
|
+
import { MfaSessionRepository } from '@/ports/mfa/mfa-session-repository.port';
|
|
4
|
+
import { MFASession } from '@/domain/mfa/aggregates/mfa-session.aggregate';
|
|
5
|
+
import { ApplicationServicePort, Result } from '@rineex/ddd';
|
|
6
|
+
import { LoggerPort } from '@/ports/log/log.port';
|
|
7
|
+
import { IdentityId } from '@/index';
|
|
8
|
+
|
|
9
|
+
import { VerificationFailedObservabilityEvent } from './events/verification-failed-observability.event';
|
|
10
|
+
import { SessionStartedObservabilityEvent } from './events/session-started-observability.event';
|
|
11
|
+
|
|
12
|
+
type StartMFASessionInput = {
|
|
13
|
+
identityId: IdentityId;
|
|
14
|
+
maxAttempts: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type StartMFASessionOutput = Result<MFASession, never>;
|
|
18
|
+
|
|
19
|
+
export class StartMfaSessionApplicationService implements ApplicationServicePort<
|
|
20
|
+
StartMFASessionInput,
|
|
21
|
+
StartMFASessionOutput
|
|
22
|
+
> {
|
|
23
|
+
constructor(
|
|
24
|
+
private readonly repository: MfaSessionRepository,
|
|
25
|
+
private readonly idGenerator: MfaSessionIdGenerator,
|
|
26
|
+
private readonly logger: LoggerPort,
|
|
27
|
+
private readonly events: ObservabilityEventPort,
|
|
28
|
+
) {}
|
|
29
|
+
|
|
30
|
+
async execute(args: StartMFASessionInput): Promise<StartMFASessionOutput> {
|
|
31
|
+
try {
|
|
32
|
+
const existing = await this.repository.findActiveByIdentity(
|
|
33
|
+
args.identityId,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (existing) {
|
|
37
|
+
this.logger.info('MFA session reused', {
|
|
38
|
+
identityId: args.identityId.toString(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
this.events.emit(
|
|
42
|
+
new SessionStartedObservabilityEvent({
|
|
43
|
+
identityId: args.identityId.toString(),
|
|
44
|
+
sessionId: existing.id.toString(),
|
|
45
|
+
reused: true,
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return Result.ok(existing);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const session = new MFASession({
|
|
53
|
+
id: this.idGenerator.generate(),
|
|
54
|
+
maxAttempts: args.maxAttempts,
|
|
55
|
+
identityId: args.identityId,
|
|
56
|
+
attemptsUsed: 0,
|
|
57
|
+
challenges: [],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await this.repository.save(session);
|
|
61
|
+
|
|
62
|
+
this.logger.info('MFA session started', {
|
|
63
|
+
identityId: args.identityId.toString(),
|
|
64
|
+
sessionId: session.id.toString(),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
this.events.emit(
|
|
68
|
+
new SessionStartedObservabilityEvent({
|
|
69
|
+
identityId: args.identityId.toString(),
|
|
70
|
+
sessionId: session.id.toString(),
|
|
71
|
+
reused: false,
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return Result.ok(session);
|
|
76
|
+
} catch (violation) {
|
|
77
|
+
this.events.emit(
|
|
78
|
+
new VerificationFailedObservabilityEvent(
|
|
79
|
+
args.identityId.toString(),
|
|
80
|
+
violation instanceof Error
|
|
81
|
+
? violation.message
|
|
82
|
+
: 'START_MFA_SESSION_ERROR',
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
this.logger.error('Error starting MFA session', { violation, ...args });
|
|
87
|
+
return Result.fail(violation as never);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ObservabilityEventPort } from '@/ports/observability/observability-event.port';
|
|
2
|
+
import { MfaSessionRepository } from '@/ports/mfa/mfa-session-repository.port';
|
|
3
|
+
import { MfaSessionId } from '@/domain/mfa/value-objects/mfa-session-id.vo';
|
|
4
|
+
import { ApplicationServicePort, Result } from '@rineex/ddd';
|
|
5
|
+
import { MfaClock } from '@/ports/mfa/mfa-clock.port';
|
|
6
|
+
import { LoggerPort } from '@/ports/log/log.port';
|
|
7
|
+
|
|
8
|
+
import { VerificationSucceededObservabilityEvent } from './events/verification-succeeded-observibility.event';
|
|
9
|
+
import { VerificationFailedObservabilityEvent } from './events/verification-failed-observability.event';
|
|
10
|
+
|
|
11
|
+
type VerifyMFAInput = {
|
|
12
|
+
sessionId: MfaSessionId;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type VerifyMFAOutput = Result<void, never>;
|
|
16
|
+
|
|
17
|
+
export class VerifyMfaApplicationService implements ApplicationServicePort<
|
|
18
|
+
VerifyMFAInput,
|
|
19
|
+
VerifyMFAOutput
|
|
20
|
+
> {
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly repository: MfaSessionRepository,
|
|
23
|
+
private readonly clock: MfaClock,
|
|
24
|
+
private readonly events: ObservabilityEventPort,
|
|
25
|
+
|
|
26
|
+
private readonly logger: LoggerPort,
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
async execute({ sessionId }: VerifyMFAInput): Promise<VerifyMFAOutput> {
|
|
30
|
+
try {
|
|
31
|
+
const session = await this.repository.findById(sessionId);
|
|
32
|
+
|
|
33
|
+
if (!session) {
|
|
34
|
+
throw new Error('MFA session not found');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
session.markAttempt();
|
|
38
|
+
session.verify(this.clock.now());
|
|
39
|
+
|
|
40
|
+
await this.repository.save(session);
|
|
41
|
+
|
|
42
|
+
this.events.emit(
|
|
43
|
+
new VerificationSucceededObservabilityEvent({
|
|
44
|
+
sessionId: session.id.toString(),
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return Result.ok(undefined);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
this.events.emit(
|
|
51
|
+
new VerificationFailedObservabilityEvent(
|
|
52
|
+
sessionId.toString(),
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
54
|
+
(error as any).code ?? 'UNKNOWN',
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
this.logger?.error('Error verifying MFA session', { sessionId, error });
|
|
58
|
+
return Result.fail(error as never);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { AuthenticationAttemptRepositoryPort } from '@/ports/outbound/authentication-attempt.repository.port';
|
|
2
|
+
import { DomainEventPublisherPort } from '@/ports/outbound/domain-event-publisher.port';
|
|
3
|
+
import { StartAuthenticationCommand } from '@/ports/inbound/start-auth.command';
|
|
4
|
+
import { AuthAttemptId, AuthenticationAttempt, AuthMethod } from '@/domain';
|
|
5
|
+
import { AuthMethodPort } from '@/ports/inbound/auth-method.port';
|
|
6
|
+
import { AuthMethodName } from '@/types/auth-method.type';
|
|
7
|
+
import { ApplicationServicePort } from '@rineex/ddd';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Application service responsible for orchestrating authentication flows.
|
|
11
|
+
*
|
|
12
|
+
* This service:
|
|
13
|
+
* - Coordinates domain objects
|
|
14
|
+
* - Delegates authentication to method modules
|
|
15
|
+
* - Remains stateless and deterministic
|
|
16
|
+
*
|
|
17
|
+
* It is safe to use in:
|
|
18
|
+
* - HTTP servers
|
|
19
|
+
* - Workers
|
|
20
|
+
* - Serverless
|
|
21
|
+
* - CLI tools
|
|
22
|
+
*/
|
|
23
|
+
export class AuthOrchestratorService implements ApplicationServicePort<
|
|
24
|
+
StartAuthenticationCommand,
|
|
25
|
+
AuthAttemptId
|
|
26
|
+
> {
|
|
27
|
+
constructor(
|
|
28
|
+
private readonly authMethods: readonly AuthMethodPort[],
|
|
29
|
+
private readonly attemptRepository: AuthenticationAttemptRepositoryPort,
|
|
30
|
+
private readonly eventPublisher: DomainEventPublisherPort,
|
|
31
|
+
private readonly idGenerator: () => AuthAttemptId,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Starts a new authentication flow.
|
|
36
|
+
*
|
|
37
|
+
* @throws Error if method is not registered
|
|
38
|
+
*/
|
|
39
|
+
async execute({
|
|
40
|
+
identityId,
|
|
41
|
+
context,
|
|
42
|
+
method,
|
|
43
|
+
}: StartAuthenticationCommand): Promise<AuthAttemptId> {
|
|
44
|
+
const authMethod = this.resolveAuthMethod(method);
|
|
45
|
+
|
|
46
|
+
const attemptId = this.idGenerator();
|
|
47
|
+
|
|
48
|
+
const attempt = AuthenticationAttempt.start(
|
|
49
|
+
attemptId,
|
|
50
|
+
AuthMethod.create(method),
|
|
51
|
+
identityId,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
await this.attemptRepository.save(attempt);
|
|
55
|
+
|
|
56
|
+
await this.eventPublisher.publish(attempt.pullDomainEvents());
|
|
57
|
+
|
|
58
|
+
await authMethod.authenticate(context);
|
|
59
|
+
|
|
60
|
+
return attemptId;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolves an authentication method implementation.
|
|
65
|
+
*
|
|
66
|
+
* Fail-fast behavior is intentional for security reasons.
|
|
67
|
+
*/
|
|
68
|
+
private resolveAuthMethod(method: AuthMethodName): AuthMethodPort {
|
|
69
|
+
const resolved = this.authMethods.find(m => m.method.is(method));
|
|
70
|
+
|
|
71
|
+
if (!resolved) {
|
|
72
|
+
throw new Error(`Authentication method not registered: ${method}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return resolved;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ApplicationServicePort } from '@rineex/ddd';
|
|
2
|
+
|
|
3
|
+
export class OAuthAuthorizeService implements ApplicationServicePort<any, any> {
|
|
4
|
+
constructor(
|
|
5
|
+
private readonly authorizationRepository: AuthorizationRepository,
|
|
6
|
+
private readonly clientRepository: ClientRepository,
|
|
7
|
+
) {}
|
|
8
|
+
|
|
9
|
+
async execute(command: any): Promise<any> {
|
|
10
|
+
// Implementation goes here
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AggregateRoot } from '@rineex/ddd';
|
|
1
|
+
import { AggregateRoot, CreateEntityProps } from '@rineex/ddd';
|
|
2
2
|
|
|
3
3
|
import { AuthenticationSucceededEvent } from '../events/authentication-succeeded.event';
|
|
4
4
|
import { AuthenticationStartedEvent } from '../events/authentication-started.event';
|
|
@@ -8,11 +8,11 @@ import { AuthStatus } from '../value-objects/auth-status.vo';
|
|
|
8
8
|
import { AuthMethod } from '../value-objects/auth-method.vo';
|
|
9
9
|
import { IdentityId } from '../value-objects/identity-id.vo';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
interface AuthenticationAttemptProps extends CreateEntityProps<AuthAttemptId> {
|
|
12
12
|
status: AuthStatus;
|
|
13
13
|
method: AuthMethod;
|
|
14
14
|
identityId?: IdentityId;
|
|
15
|
-
}
|
|
15
|
+
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Aggregate Root representing a single authentication attempt.
|
|
@@ -27,18 +27,38 @@ type Props = {
|
|
|
27
27
|
* - Talk to infrastructure
|
|
28
28
|
* - Know about HTTP or sessions
|
|
29
29
|
*/
|
|
30
|
-
export class AuthenticationAttempt extends AggregateRoot<
|
|
31
|
-
|
|
30
|
+
export class AuthenticationAttempt extends AggregateRoot<AuthAttemptId> {
|
|
31
|
+
/**
|
|
32
|
+
* Current status of the authentication attempt.
|
|
33
|
+
*/
|
|
34
|
+
private _status: AuthStatus;
|
|
35
|
+
/**
|
|
36
|
+
* Method used for authentication.
|
|
37
|
+
*/
|
|
38
|
+
private _method: AuthMethod;
|
|
39
|
+
/**
|
|
40
|
+
* Optional identity being authenticated.
|
|
41
|
+
*/
|
|
42
|
+
private _identityId?: IdentityId;
|
|
43
|
+
|
|
44
|
+
private constructor({ createdAt, id, ...props }: AuthenticationAttemptProps) {
|
|
32
45
|
super({
|
|
33
|
-
createdAt
|
|
34
|
-
props,
|
|
46
|
+
createdAt,
|
|
35
47
|
id,
|
|
36
48
|
});
|
|
49
|
+
this._status = props.status;
|
|
50
|
+
this._method = props.method;
|
|
51
|
+
this._identityId = props.identityId;
|
|
37
52
|
}
|
|
38
53
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
toObject(): Record<string, unknown> {
|
|
55
|
+
return {
|
|
56
|
+
identityId: this._identityId.getValue(),
|
|
57
|
+
createdAt: this.createdAt,
|
|
58
|
+
id: this.id.getValue(),
|
|
59
|
+
status: this._status,
|
|
60
|
+
method: this._method,
|
|
61
|
+
};
|
|
42
62
|
}
|
|
43
63
|
|
|
44
64
|
/**
|
|
@@ -49,10 +69,11 @@ export class AuthenticationAttempt extends AggregateRoot<Props> {
|
|
|
49
69
|
method: AuthMethod,
|
|
50
70
|
identityId?: IdentityId,
|
|
51
71
|
): AuthenticationAttempt {
|
|
52
|
-
const attempt = new AuthenticationAttempt(
|
|
72
|
+
const attempt = new AuthenticationAttempt({
|
|
53
73
|
status: AuthStatus.create('PENDING'),
|
|
54
74
|
identityId,
|
|
55
75
|
method,
|
|
76
|
+
id,
|
|
56
77
|
});
|
|
57
78
|
|
|
58
79
|
attempt.addEvent(new AuthenticationStartedEvent(id, method));
|
|
@@ -66,18 +87,15 @@ export class AuthenticationAttempt extends AggregateRoot<Props> {
|
|
|
66
87
|
* @throws Error if already completed
|
|
67
88
|
*/
|
|
68
89
|
succeed(): void {
|
|
69
|
-
if (this.
|
|
90
|
+
if (this._status.isNot('PENDING')) {
|
|
70
91
|
throw new Error('Authentication attempt already completed');
|
|
71
92
|
}
|
|
72
93
|
|
|
73
|
-
this.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}));
|
|
77
|
-
|
|
78
|
-
const ev = new AuthenticationSucceededEvent(this.id);
|
|
94
|
+
this.mutate(draft => {
|
|
95
|
+
draft._status = AuthStatus.create('SUCCEEDED');
|
|
96
|
+
});
|
|
79
97
|
|
|
80
|
-
this.addEvent(
|
|
98
|
+
this.addEvent(new AuthenticationSucceededEvent(this.id));
|
|
81
99
|
}
|
|
82
100
|
|
|
83
101
|
/**
|
|
@@ -86,7 +104,7 @@ export class AuthenticationAttempt extends AggregateRoot<Props> {
|
|
|
86
104
|
* @throws Error if already completed
|
|
87
105
|
*/
|
|
88
106
|
fail(reason: string): void {
|
|
89
|
-
if (this.
|
|
107
|
+
if (this._status.isNot('PENDING')) {
|
|
90
108
|
throw new Error('Authentication attempt already completed');
|
|
91
109
|
}
|
|
92
110
|
|
|
@@ -94,10 +112,9 @@ export class AuthenticationAttempt extends AggregateRoot<Props> {
|
|
|
94
112
|
throw new Error('Failure reason must be provided');
|
|
95
113
|
}
|
|
96
114
|
|
|
97
|
-
this.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}));
|
|
115
|
+
this.mutate(draft => {
|
|
116
|
+
draft._status = AuthStatus.create('FAILED');
|
|
117
|
+
});
|
|
101
118
|
|
|
102
119
|
this.addEvent(new AuthenticationFailedEvent(this.id));
|
|
103
120
|
}
|
|
@@ -108,11 +125,11 @@ export class AuthenticationAttempt extends AggregateRoot<Props> {
|
|
|
108
125
|
* Called automatically before domain events are added.
|
|
109
126
|
*/
|
|
110
127
|
validate(): void {
|
|
111
|
-
if (!this.
|
|
128
|
+
if (!this._method) {
|
|
112
129
|
throw new Error('AuthenticationAttempt must have a method');
|
|
113
130
|
}
|
|
114
131
|
|
|
115
|
-
if (!this.
|
|
132
|
+
if (!this._status) {
|
|
116
133
|
throw new Error('AuthenticationAttempt must have a status');
|
|
117
134
|
}
|
|
118
135
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './authentication-attempt.aggregate';
|