@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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import isNil from 'lodash.isnil';
|
|
2
|
+
|
|
3
|
+
import { defaultIfNilOrEmpty } from '@/utils/default-if-blank.util';
|
|
4
|
+
import { AggregateRoot, CreateEntityProps } from '@rineex/ddd';
|
|
5
|
+
import { IdentityId } from '@/domain/identity';
|
|
6
|
+
|
|
7
|
+
import { AuthorizationAlreadyUsedViolation } from '../violations/authorization-already-used.violation';
|
|
8
|
+
import { AuthorizationExpiredViolation } from '../violations/authorization-expired.violation';
|
|
9
|
+
import { ConsentRequiredViolation } from '../violations/consent-required.violation';
|
|
10
|
+
import { OAuthAuthorizationId } from '../value-objects/oauth-authorization-id.vo';
|
|
11
|
+
import { AuthorizationCode } from '../value-objects/authorization-code.vo';
|
|
12
|
+
import { CodeChallenge } from '../value-objects/code-challenge.vo';
|
|
13
|
+
import { RedirectUri } from '../value-objects/redirect-uri.vo';
|
|
14
|
+
import { ClientId } from '../value-objects/client-id.vo';
|
|
15
|
+
import { ScopeSet } from '../value-objects/scope-set.vo';
|
|
16
|
+
|
|
17
|
+
export interface OAuthAuthorizationProps extends CreateEntityProps<OAuthAuthorizationId> {
|
|
18
|
+
readonly identityId: IdentityId;
|
|
19
|
+
readonly clientId: ClientId;
|
|
20
|
+
readonly redirectUri: RedirectUri;
|
|
21
|
+
readonly scopes: ScopeSet;
|
|
22
|
+
readonly codeChallenge?: CodeChallenge;
|
|
23
|
+
|
|
24
|
+
consentGrantedAt?: Date;
|
|
25
|
+
authorizationCode?: AuthorizationCode;
|
|
26
|
+
readonly expiresAt: Date;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class OauthAuthorization extends AggregateRoot<OAuthAuthorizationId> {
|
|
30
|
+
protected constructor(private props: OAuthAuthorizationProps) {
|
|
31
|
+
super(props);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
validate() {
|
|
35
|
+
if (this.props.expiresAt.getTime() <= Date.now()) {
|
|
36
|
+
throw AuthorizationExpiredViolation.create();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
issueAuthorizationCode(code: AuthorizationCode): void {
|
|
41
|
+
if (this.props.authorizationCode) {
|
|
42
|
+
throw AuthorizationAlreadyUsedViolation.create();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (this.requiresConsent()) {
|
|
46
|
+
throw ConsentRequiredViolation.create();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.props.authorizationCode = code;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
isExpired(now: Date): boolean {
|
|
53
|
+
return now > this.props.expiresAt;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
grantConsent(now: Date): void {
|
|
57
|
+
if (!this.requiresConsent()) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.mutate(draft => {
|
|
62
|
+
draft.props.consentGrantedAt = now;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
this.props.consentGrantedAt = now;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
protected snapshot(): Record<string, unknown> {
|
|
69
|
+
return {
|
|
70
|
+
consentGrantedAt: this.props.consentGrantedAt?.toISOString() ?? null,
|
|
71
|
+
authorizationCode: this.props.authorizationCode?.toString() ?? null,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
protected restore(snapshot: Record<string, unknown>): void {
|
|
76
|
+
this.props.consentGrantedAt = snapshot.consentGrantedAt
|
|
77
|
+
? new Date(snapshot.consentGrantedAt as string)
|
|
78
|
+
: undefined;
|
|
79
|
+
|
|
80
|
+
this.props.authorizationCode = snapshot.authorizationCode
|
|
81
|
+
? AuthorizationCode.create(snapshot.authorizationCode as string)
|
|
82
|
+
: undefined; // Assume fromString exists
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
requiresConsent(): boolean {
|
|
86
|
+
return isNil(this.props.consentGrantedAt);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
toObject() {
|
|
90
|
+
return {
|
|
91
|
+
consentGrantedAt: defaultIfNilOrEmpty(
|
|
92
|
+
this.props.consentGrantedAt?.toISOString(),
|
|
93
|
+
),
|
|
94
|
+
authorizationCode: defaultIfNilOrEmpty(
|
|
95
|
+
this.props.authorizationCode?.toString(),
|
|
96
|
+
),
|
|
97
|
+
codeChallenge: defaultIfNilOrEmpty(this.props.codeChallenge?.toString()),
|
|
98
|
+
redirectUri: this.props.redirectUri.toString(),
|
|
99
|
+
expiresAt: this.props.expiresAt.toISOString(),
|
|
100
|
+
identityId: this.props.identityId.toString(),
|
|
101
|
+
scopes: this.props.scopes.toStringArray(),
|
|
102
|
+
clientId: this.props.clientId.toString(),
|
|
103
|
+
id: this.id.toString(),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { CreateEntityProps, Entity } from '@rineex/ddd';
|
|
2
|
+
|
|
3
|
+
import { InvalidRedirectUriViolation } from '../violations/invalid-redirect-uri.violation';
|
|
4
|
+
import { OAuthAuthorizationId } from '../value-objects/oauth-authorization-id.vo';
|
|
5
|
+
import { OAuthProvider } from '../value-objects/oauth-provider.vo';
|
|
6
|
+
import { Pkce } from '../value-objects/pkce.vo';
|
|
7
|
+
|
|
8
|
+
export interface OAuthAuthorizationProps extends CreateEntityProps<OAuthAuthorizationId> {
|
|
9
|
+
provider: OAuthProvider;
|
|
10
|
+
redirectUri: string;
|
|
11
|
+
scope: readonly string[];
|
|
12
|
+
pkce?: Pkce;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class OAuthAuthorization extends Entity<OAuthAuthorizationId> {
|
|
16
|
+
constructor(public props: OAuthAuthorizationProps) {
|
|
17
|
+
super(props);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
toObject(): Record<string, unknown> {
|
|
21
|
+
return {
|
|
22
|
+
pkce: this.props.pkce ? this.props.pkce.toJSON() : undefined,
|
|
23
|
+
redirectUri: this.props.redirectUri,
|
|
24
|
+
provider: this.props.provider,
|
|
25
|
+
scope: this.props.scope,
|
|
26
|
+
id: this.id.getValue(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected snapshot(): Record<string, unknown> {
|
|
31
|
+
return this.toObject();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected restore(snapshot: Record<string, unknown>): void {
|
|
35
|
+
this.props.pkce = snapshot.pkce
|
|
36
|
+
? Pkce.fromJSON(snapshot.pkce as Record<string, unknown>)
|
|
37
|
+
: undefined;
|
|
38
|
+
this.props.redirectUri = snapshot.redirectUri as string;
|
|
39
|
+
this.props.provider = snapshot.provider as OAuthProvider;
|
|
40
|
+
this.props.scope = snapshot.scope as string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
validate(): void {
|
|
44
|
+
if (!this.props.redirectUri.startsWith('https://')) {
|
|
45
|
+
throw InvalidRedirectUriViolation.create({
|
|
46
|
+
redirectUri: this.props.redirectUri,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { PrimitiveValueObject } from '@rineex/ddd';
|
|
2
|
+
|
|
3
|
+
export class AuthorizationCodeId extends PrimitiveValueObject<string> {
|
|
4
|
+
protected validate(value: string): void {
|
|
5
|
+
if (!value || value.length < 32) {
|
|
6
|
+
throw new Error('Invalid authorization code');
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { PrimitiveValueObject } from '@rineex/ddd';
|
|
2
|
+
|
|
3
|
+
import { InvalidAuthorizationCodeViolation } from '../violations/invalid-authorization-code.violation';
|
|
4
|
+
|
|
5
|
+
export class AuthorizationCode extends PrimitiveValueObject<string> {
|
|
6
|
+
protected validate(value: string): void {
|
|
7
|
+
if (value.length < 8) {
|
|
8
|
+
throw InvalidAuthorizationCodeViolation.create({
|
|
9
|
+
actualLength: value.length,
|
|
10
|
+
minLength: 8,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public static create(value: string): AuthorizationCode {
|
|
16
|
+
return new AuthorizationCode(value);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PrimitiveValueObject } from '@rineex/ddd';
|
|
2
|
+
|
|
3
|
+
export type CodeChallengeMethodValue = 'plain' | 'S256';
|
|
4
|
+
|
|
5
|
+
export class CodeChallengeMethod extends PrimitiveValueObject<CodeChallengeMethodValue> {
|
|
6
|
+
protected validate(value: CodeChallengeMethodValue): void {
|
|
7
|
+
if (value !== 'plain' && value !== 'S256') {
|
|
8
|
+
throw new Error('Unsupported code challenge method');
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static create(value: CodeChallengeMethodValue): CodeChallengeMethod {
|
|
13
|
+
return new CodeChallengeMethod(value);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ValueObject } from '@rineex/ddd';
|
|
2
|
+
|
|
3
|
+
import { CodeChallengeMethod } from './code-challenge-method.vo';
|
|
4
|
+
|
|
5
|
+
export type CodeChallengeProps = {
|
|
6
|
+
readonly value: string;
|
|
7
|
+
readonly method: CodeChallengeMethod;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export class CodeChallenge extends ValueObject<CodeChallengeProps> {
|
|
11
|
+
protected validate(props: CodeChallengeProps): void {
|
|
12
|
+
if (!props.value || props.value.length < 32) {
|
|
13
|
+
throw new Error('Invalid code challenge value');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get method(): CodeChallengeMethod {
|
|
18
|
+
return this.props.method;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static create(value: string, method: CodeChallengeMethod): CodeChallenge {
|
|
22
|
+
return new CodeChallenge({ method, value });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AuthDomainViolation } from '@/domain/violations/auth-domain.violation';
|
|
2
|
+
import { PrimitiveValueObject } from '@rineex/ddd';
|
|
3
|
+
|
|
4
|
+
class InvalidOAuthAuthorizationIdViolation extends AuthDomainViolation {
|
|
5
|
+
readonly code = 'OAUTH_AUTHORIZATION_ID_INVALID';
|
|
6
|
+
readonly message = 'OAuthAuthorization ID is invalid';
|
|
7
|
+
|
|
8
|
+
public static create(props: { value: string }) {
|
|
9
|
+
return new InvalidOAuthAuthorizationIdViolation(props);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class OAuthAuthorizationId extends PrimitiveValueObject<string> {
|
|
14
|
+
protected validate(value: string): void {
|
|
15
|
+
if (!value || value.length < 16) {
|
|
16
|
+
throw InvalidOAuthAuthorizationIdViolation.create({ value });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PrimitiveValueObject } from '@rineex/ddd';
|
|
2
|
+
|
|
3
|
+
import { InvalidOAuthProviderViolation } from '../violations/invalid-oauth-provider.violation';
|
|
4
|
+
|
|
5
|
+
export class OAuthProvider extends PrimitiveValueObject<string> {
|
|
6
|
+
protected validate(value: string): void {
|
|
7
|
+
if (!value || value.length < 2) {
|
|
8
|
+
throw InvalidOAuthProviderViolation.create({ value });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public static create(value: string): OAuthProvider {
|
|
13
|
+
return new OAuthProvider(value);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ValueObject } from '@rineex/ddd';
|
|
2
|
+
|
|
3
|
+
import { InvalidPkceViolation } from '../violations/invalid-pkce.violation';
|
|
4
|
+
|
|
5
|
+
export type PkceProps = {
|
|
6
|
+
readonly codeVerifier: string;
|
|
7
|
+
readonly codeChallenge: string;
|
|
8
|
+
readonly method: 'plain' | 'S256';
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export class Pkce extends ValueObject<PkceProps> {
|
|
12
|
+
protected validate(props: PkceProps): void {
|
|
13
|
+
if (!props.codeVerifier || !props.codeChallenge) {
|
|
14
|
+
throw InvalidPkceViolation.create(props);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public static create(props: PkceProps): Pkce {
|
|
19
|
+
return new Pkce(props);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public static fromJSON(json: Record<string, unknown>): Pkce {
|
|
23
|
+
return new Pkce({
|
|
24
|
+
codeChallenge: json.codeChallenge as string,
|
|
25
|
+
codeVerifier: json.codeVerifier as string,
|
|
26
|
+
method: json.method as 'plain' | 'S256',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PrimitiveValueObject } from '@rineex/ddd';
|
|
2
|
+
|
|
3
|
+
import { InvalidRedirectUriViolation } from '../violations/invalid-redirect-uri.violation';
|
|
4
|
+
|
|
5
|
+
export class RedirectUri extends PrimitiveValueObject<string> {
|
|
6
|
+
protected validate(url: string): void {
|
|
7
|
+
let parsed: URL;
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
parsed = new URL(url);
|
|
11
|
+
} catch {
|
|
12
|
+
throw InvalidRedirectUriViolation.create({ redirectUri: url });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (parsed.protocol !== 'https:' || parsed.hash) {
|
|
16
|
+
throw InvalidRedirectUriViolation.create({ redirectUri: url });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { PrimitiveValueObject, ValueObject } from '@rineex/ddd';
|
|
2
|
+
|
|
3
|
+
export class Scope extends PrimitiveValueObject<string> {
|
|
4
|
+
protected validate(value: string): void {
|
|
5
|
+
if (!/^[-0-:_a-z]+$/.test(value)) {
|
|
6
|
+
throw new Error('Invalid scope format');
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static create(scope: string): Scope {
|
|
11
|
+
return new Scope(scope);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ScopeSet extends ValueObject<ReadonlySet<Scope>> {
|
|
16
|
+
public static create() {
|
|
17
|
+
return new ScopeSet(new Set<Scope>());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
protected validate(value: ReadonlySet<Scope>): void {
|
|
21
|
+
if (value.size === 0) {
|
|
22
|
+
throw new Error('At least one scope is required');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static fromStrings(scopes: readonly string[]): ScopeSet {
|
|
27
|
+
return new ScopeSet(new Set(scopes.map(scope => Scope.create(scope))));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
has(scope: Scope): boolean {
|
|
31
|
+
return [...this.props].some(s => s.equals(scope));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
toStringArray(): string[] {
|
|
35
|
+
return [...this.props].map(s => s.getValue());
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AuthDomainViolation } from '@/domain/violations/auth-domain.violation';
|
|
2
|
+
|
|
3
|
+
export class AuthorizationAlreadyUsedViolation extends AuthDomainViolation {
|
|
4
|
+
readonly code = 'oauth.authorization.already_used';
|
|
5
|
+
readonly message = 'The OAuth authorization has already been used';
|
|
6
|
+
|
|
7
|
+
public static create(): AuthorizationAlreadyUsedViolation {
|
|
8
|
+
return new AuthorizationAlreadyUsedViolation();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AuthDomainViolation } from '@/domain/violations/auth-domain.violation';
|
|
2
|
+
|
|
3
|
+
export class AuthorizationExpiredViolation extends AuthDomainViolation {
|
|
4
|
+
readonly code = 'oauth.authorization.expired';
|
|
5
|
+
readonly message = 'The OAuth authorization has expired';
|
|
6
|
+
|
|
7
|
+
public static create(): AuthorizationExpiredViolation {
|
|
8
|
+
return new AuthorizationExpiredViolation();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AuthDomainViolation } from '@/domain/violations/auth-domain.violation';
|
|
2
|
+
|
|
3
|
+
export class ConsentRequiredViolation extends AuthDomainViolation {
|
|
4
|
+
readonly code = 'oauth.consent.required';
|
|
5
|
+
readonly message = 'User consent is required for this OAuth authorization';
|
|
6
|
+
|
|
7
|
+
public static create(): ConsentRequiredViolation {
|
|
8
|
+
return new ConsentRequiredViolation();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AuthDomainViolation } from '@/domain/violations/auth-domain.violation';
|
|
2
|
+
|
|
3
|
+
export class InvalidAuthorizationCodeViolation extends AuthDomainViolation {
|
|
4
|
+
readonly code = 'AUTH_CODE_INVALID';
|
|
5
|
+
readonly message = 'Authorization code is invalid';
|
|
6
|
+
|
|
7
|
+
public static create(
|
|
8
|
+
details?: Record<string, unknown>,
|
|
9
|
+
): InvalidAuthorizationCodeViolation {
|
|
10
|
+
return new InvalidAuthorizationCodeViolation({ details });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AuthDomainViolation } from '@/domain/violations/auth-domain.violation';
|
|
2
|
+
|
|
3
|
+
export class InvalidOAuthProviderViolation extends AuthDomainViolation {
|
|
4
|
+
readonly code = 'OAUTH_PROVIDER_INVALID';
|
|
5
|
+
readonly message = 'OAuth provider identifier is invalid';
|
|
6
|
+
|
|
7
|
+
public static create(props: { value: string }) {
|
|
8
|
+
return new InvalidOAuthProviderViolation({
|
|
9
|
+
...props,
|
|
10
|
+
message: `OAuth provider identifier "${props.value}" is invalid`,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AuthDomainViolation } from '@/domain/violations/auth-domain.violation';
|
|
2
|
+
|
|
3
|
+
export class InvalidPkceViolation extends AuthDomainViolation {
|
|
4
|
+
readonly code = 'PKCE_INVALID';
|
|
5
|
+
readonly message = 'PKCE parameters are invalid';
|
|
6
|
+
|
|
7
|
+
public static create(
|
|
8
|
+
details?: Record<string, unknown>,
|
|
9
|
+
): InvalidPkceViolation {
|
|
10
|
+
return new InvalidPkceViolation({ details });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AuthDomainViolation } from '@/domain/violations/auth-domain.violation';
|
|
2
|
+
|
|
3
|
+
export class InvalidRedirectUriViolation extends AuthDomainViolation {
|
|
4
|
+
readonly code = 'OAUTH_REDIRECT_URI_INVALID';
|
|
5
|
+
readonly message = 'Redirect URI is invalid or insecure';
|
|
6
|
+
|
|
7
|
+
public static create(props: { redirectUri: string }) {
|
|
8
|
+
return new InvalidRedirectUriViolation(props);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AuthMethod, IdentityId } from '@/domain/identity/value-objects';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Immutable context passed to all authentication policies.
|
|
5
|
+
* Represents facts, not decisions.
|
|
6
|
+
*/
|
|
7
|
+
export type AuthPolicyContext = Readonly<{
|
|
8
|
+
readonly identityId?: IdentityId;
|
|
9
|
+
readonly method: AuthMethod;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Pre-calculated risk score (0-100).
|
|
13
|
+
* Produced by an upstream adapter (not domain logic).
|
|
14
|
+
*/
|
|
15
|
+
readonly riskScore?: number;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether the identity is already known to be blocked.
|
|
19
|
+
*/
|
|
20
|
+
readonly isBlocked?: boolean;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* True if this authentication attempt is coming
|
|
24
|
+
* from a previously trusted environment.
|
|
25
|
+
*/
|
|
26
|
+
readonly isTrustedDevice?: boolean;
|
|
27
|
+
}>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AuthPolicyDecision } from './auth-policy-decision';
|
|
2
|
+
import { AuthPolicyContext } from './auth-policy-context';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Authentication policy contract.
|
|
6
|
+
* A policy evaluates context and returns a partial decision.
|
|
7
|
+
*/
|
|
8
|
+
export abstract class AuthPolicyEvaluator {
|
|
9
|
+
/**
|
|
10
|
+
* Evaluates the policy against the given context.
|
|
11
|
+
*
|
|
12
|
+
* Returning `null` means:
|
|
13
|
+
* - this policy is not applicable
|
|
14
|
+
* - it has no opinion
|
|
15
|
+
*/
|
|
16
|
+
abstract evaluate(context: AuthPolicyContext): AuthPolicyDecision | null;
|
|
17
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AuthPolicyDecision } from '../contracts/auth-policy-decision';
|
|
2
|
+
import { AuthPolicyContext } from '../contracts/auth-policy-context';
|
|
3
|
+
import { AuthPolicyEvaluator } from '../contracts/auth-policy';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Coordinates multiple authentication policies
|
|
7
|
+
* and produces a final decision.
|
|
8
|
+
*/
|
|
9
|
+
export class AuthPolicyEngine {
|
|
10
|
+
private readonly policies: readonly AuthPolicyEvaluator[];
|
|
11
|
+
|
|
12
|
+
constructor(policies: readonly AuthPolicyEvaluator[]) {
|
|
13
|
+
this.policies = policies;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Evaluates all policies in order.
|
|
18
|
+
* First hard-deny wins.
|
|
19
|
+
*/
|
|
20
|
+
public evaluate(context: AuthPolicyContext): AuthPolicyDecision {
|
|
21
|
+
let requiresStepUp = false;
|
|
22
|
+
|
|
23
|
+
for (const policy of this.policies) {
|
|
24
|
+
const decision = policy.evaluate(context);
|
|
25
|
+
if (!decision) continue;
|
|
26
|
+
|
|
27
|
+
if (decision.allowed === false) {
|
|
28
|
+
return decision;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (decision.requiresStepUp) {
|
|
32
|
+
requiresStepUp = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
requiresStepUp,
|
|
38
|
+
allowed: true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { SessionToken } from '@/domain/token/value-objects/session-token.vo';
|
|
2
|
+
import { defaultIfNilOrEmpty } from '@/utils/default-if-blank.util';
|
|
3
|
+
import { CreateEntityProps, Entity } from '@rineex/ddd';
|
|
4
|
+
import { IdentityId } from '@/domain/identity';
|
|
5
|
+
|
|
6
|
+
import { SessionId } from '../value-objects/session-id.vo';
|
|
7
|
+
|
|
8
|
+
interface CreateSessionProps extends CreateEntityProps<SessionId> {
|
|
9
|
+
readonly identityId: IdentityId;
|
|
10
|
+
readonly token: SessionToken;
|
|
11
|
+
readonly expiresAt: Date;
|
|
12
|
+
readonly revokedAt?: Date;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class Session extends Entity<SessionId> {
|
|
16
|
+
private _identityId: IdentityId;
|
|
17
|
+
private _token: SessionToken;
|
|
18
|
+
private _expiresAt: Date;
|
|
19
|
+
private _revokedAt?: Date;
|
|
20
|
+
|
|
21
|
+
protected constructor({ createdAt, id, ...props }: CreateSessionProps) {
|
|
22
|
+
super({ createdAt, id });
|
|
23
|
+
|
|
24
|
+
this._expiresAt = props.expiresAt;
|
|
25
|
+
this._revokedAt = props.revokedAt;
|
|
26
|
+
this._identityId = props.identityId;
|
|
27
|
+
this._token = props.token;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public static create(props: CreateSessionProps): Session {
|
|
31
|
+
return new Session(props);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
toObject(): Record<string, unknown> {
|
|
35
|
+
return {
|
|
36
|
+
identityId: this._identityId.getValue(),
|
|
37
|
+
revokedAt: this._revokedAt?.toString(),
|
|
38
|
+
expiresAt: this._expiresAt.toString(),
|
|
39
|
+
revoked: !!this._revokedAt,
|
|
40
|
+
id: this.id.getValue(),
|
|
41
|
+
token: this._token,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
revoke(at: Date): void {
|
|
46
|
+
if (this._revokedAt) return;
|
|
47
|
+
|
|
48
|
+
this.mutate(draft => {
|
|
49
|
+
draft._revokedAt = at;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public isActive(now = new Date()): boolean {
|
|
54
|
+
if (this._revokedAt) return false;
|
|
55
|
+
|
|
56
|
+
return now < this._expiresAt;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
protected snapshot() {
|
|
60
|
+
return {
|
|
61
|
+
revokedAt: defaultIfNilOrEmpty(this._revokedAt?.toISOString()),
|
|
62
|
+
expiresAt: this._expiresAt.toISOString(),
|
|
63
|
+
identityId: this._identityId.toJSON(),
|
|
64
|
+
token: this._token.toJSON(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
protected restore(snapshot: Record<string, unknown>): void {
|
|
69
|
+
this._revokedAt = snapshot?.revokedAt
|
|
70
|
+
? new Date(snapshot.revokedAt as string)
|
|
71
|
+
: undefined;
|
|
72
|
+
this._expiresAt = new Date(snapshot.expiresAt as string);
|
|
73
|
+
this._identityId = IdentityId.create(snapshot.identityId as string);
|
|
74
|
+
this._token = SessionToken.create(snapshot.token as string);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
validate(): void {
|
|
78
|
+
if (this._expiresAt <= new Date(0)) {
|
|
79
|
+
throw new Error('Session expiration must be valid');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { InvalidSessionViolation } from '@/domain/violations/invalid-session.violation';
|
|
2
|
+
import { PrimitiveValueObject } from '@rineex/ddd';
|
|
3
|
+
|
|
4
|
+
export class SessionId extends PrimitiveValueObject<string> {
|
|
5
|
+
protected validate(value: string): void {
|
|
6
|
+
if (!value || value.length < 16) {
|
|
7
|
+
throw InvalidSessionViolation.create();
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|