@nauth-toolkit/core 0.1.0 → 0.1.3
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/LICENSE +90 -0
- package/README.md +30 -0
- package/package.json +7 -2
- package/jest.config.js +0 -15
- package/jest.setup.ts +0 -6
- package/src/adapters/database-columns.ts +0 -165
- package/src/adapters/express.adapter.ts +0 -385
- package/src/adapters/fastify.adapter.ts +0 -416
- package/src/adapters/index.ts +0 -16
- package/src/adapters/storage.factory.ts +0 -143
- package/src/bootstrap.ts +0 -374
- package/src/dto/auth-challenge.dto.ts +0 -231
- package/src/dto/auth-response.dto.ts +0 -253
- package/src/dto/challenge-response.dto.ts +0 -234
- package/src/dto/change-password-request.dto.ts +0 -50
- package/src/dto/change-password-response.dto.ts +0 -29
- package/src/dto/change-password.dto.ts +0 -57
- package/src/dto/error-response.dto.ts +0 -136
- package/src/dto/get-available-methods.dto.ts +0 -55
- package/src/dto/get-challenge-data-response.dto.ts +0 -28
- package/src/dto/get-challenge-data.dto.ts +0 -69
- package/src/dto/get-client-info.dto.ts +0 -104
- package/src/dto/get-device-token-response.dto.ts +0 -25
- package/src/dto/get-events-by-type.dto.ts +0 -76
- package/src/dto/get-ip-address-response.dto.ts +0 -24
- package/src/dto/get-mfa-status.dto.ts +0 -94
- package/src/dto/get-risk-assessment-history.dto.ts +0 -39
- package/src/dto/get-session-id-response.dto.ts +0 -25
- package/src/dto/get-setup-data-response.dto.ts +0 -31
- package/src/dto/get-setup-data.dto.ts +0 -75
- package/src/dto/get-suspicious-activity.dto.ts +0 -42
- package/src/dto/get-user-agent-response.dto.ts +0 -23
- package/src/dto/get-user-auth-history.dto.ts +0 -95
- package/src/dto/get-user-by-email.dto.ts +0 -61
- package/src/dto/get-user-by-id.dto.ts +0 -46
- package/src/dto/get-user-devices.dto.ts +0 -53
- package/src/dto/get-user-response.dto.ts +0 -17
- package/src/dto/has-provider.dto.ts +0 -56
- package/src/dto/index.ts +0 -57
- package/src/dto/is-trusted-device-response.dto.ts +0 -34
- package/src/dto/list-providers-response.dto.ts +0 -23
- package/src/dto/login.dto.ts +0 -95
- package/src/dto/logout-all-response.dto.ts +0 -24
- package/src/dto/logout-all.dto.ts +0 -65
- package/src/dto/logout-response.dto.ts +0 -25
- package/src/dto/logout.dto.ts +0 -64
- package/src/dto/refresh-token.dto.ts +0 -36
- package/src/dto/remove-devices.dto.ts +0 -85
- package/src/dto/resend-code-response.dto.ts +0 -32
- package/src/dto/resend-code.dto.ts +0 -51
- package/src/dto/reset-password.dto.ts +0 -115
- package/src/dto/respond-challenge.dto.ts +0 -272
- package/src/dto/set-mfa-exemption.dto.ts +0 -112
- package/src/dto/set-must-change-password-response.dto.ts +0 -27
- package/src/dto/set-must-change-password.dto.ts +0 -46
- package/src/dto/set-preferred-method.dto.ts +0 -80
- package/src/dto/setup-mfa.dto.ts +0 -98
- package/src/dto/signup.dto.ts +0 -174
- package/src/dto/social-auth.dto.ts +0 -422
- package/src/dto/trust-device-response.dto.ts +0 -30
- package/src/dto/trust-device.dto.ts +0 -9
- package/src/dto/update-user-attributes-request.dto.ts +0 -51
- package/src/dto/user-response.dto.ts +0 -138
- package/src/dto/user-update.dto.ts +0 -222
- package/src/dto/verify-email.dto.ts +0 -313
- package/src/dto/verify-mfa-code.dto.ts +0 -103
- package/src/dto/verify-phone-by-sub.dto.ts +0 -78
- package/src/dto/verify-phone.dto.ts +0 -245
- package/src/entities/auth-audit.entity.ts +0 -232
- package/src/entities/challenge-session.entity.ts +0 -116
- package/src/entities/index.ts +0 -29
- package/src/entities/login-attempt.entity.ts +0 -64
- package/src/entities/mfa-device.entity.ts +0 -151
- package/src/entities/rate-limit.entity.ts +0 -44
- package/src/entities/session.entity.ts +0 -180
- package/src/entities/social-account.entity.ts +0 -96
- package/src/entities/storage-lock.entity.ts +0 -39
- package/src/entities/trusted-device.entity.ts +0 -112
- package/src/entities/user.entity.ts +0 -243
- package/src/entities/verification-token.entity.ts +0 -141
- package/src/enums/auth-audit-event-type.enum.ts +0 -360
- package/src/enums/error-codes.enum.ts +0 -420
- package/src/enums/mfa-method.enum.ts +0 -97
- package/src/enums/risk-factor.enum.ts +0 -111
- package/src/exceptions/nauth.exception.ts +0 -231
- package/src/handlers/auth.handler.ts +0 -260
- package/src/handlers/client-info.handler.ts +0 -101
- package/src/handlers/csrf.handler.ts +0 -156
- package/src/handlers/token-delivery.handler.ts +0 -118
- package/src/index.ts +0 -118
- package/src/interfaces/client-info.interface.ts +0 -85
- package/src/interfaces/config.interface.ts +0 -2135
- package/src/interfaces/entities.interface.ts +0 -226
- package/src/interfaces/index.ts +0 -15
- package/src/interfaces/logger.interface.ts +0 -283
- package/src/interfaces/mfa-provider.interface.ts +0 -154
- package/src/interfaces/oauth.interface.ts +0 -148
- package/src/interfaces/provider.interface.ts +0 -47
- package/src/interfaces/social-auth-provider.interface.ts +0 -131
- package/src/interfaces/storage-adapter.interface.ts +0 -82
- package/src/interfaces/template.interface.ts +0 -510
- package/src/interfaces/token-verifier.interface.ts +0 -110
- package/src/internal.ts +0 -178
- package/src/platform/interfaces.ts +0 -299
- package/src/schemas/auth-config.schema.ts +0 -646
- package/src/services/adaptive-mfa-decision.service.spec.ts +0 -1058
- package/src/services/adaptive-mfa-decision.service.ts +0 -457
- package/src/services/auth-audit.service.spec.ts +0 -675
- package/src/services/auth-audit.service.ts +0 -558
- package/src/services/auth-challenge-helper.service.spec.ts +0 -3227
- package/src/services/auth-challenge-helper.service.ts +0 -825
- package/src/services/auth-flow-context-builder.service.ts +0 -520
- package/src/services/auth-flow-rules.ts +0 -202
- package/src/services/auth-flow-state-definitions.ts +0 -190
- package/src/services/auth-flow-state-machine.service.ts +0 -207
- package/src/services/auth-flow-state-machine.types.ts +0 -316
- package/src/services/auth.service.spec.ts +0 -4195
- package/src/services/auth.service.ts +0 -3727
- package/src/services/challenge.service.spec.ts +0 -1363
- package/src/services/challenge.service.ts +0 -696
- package/src/services/client-info.service.spec.ts +0 -572
- package/src/services/client-info.service.ts +0 -374
- package/src/services/csrf.service.ts +0 -54
- package/src/services/email-verification.service.spec.ts +0 -1229
- package/src/services/email-verification.service.ts +0 -578
- package/src/services/geo-location.service.spec.ts +0 -603
- package/src/services/geo-location.service.ts +0 -599
- package/src/services/index.ts +0 -13
- package/src/services/jwt.service.spec.ts +0 -882
- package/src/services/jwt.service.ts +0 -621
- package/src/services/mfa-base.service.spec.ts +0 -246
- package/src/services/mfa-base.service.ts +0 -611
- package/src/services/mfa.service.spec.ts +0 -693
- package/src/services/mfa.service.ts +0 -960
- package/src/services/password.service.spec.ts +0 -166
- package/src/services/password.service.ts +0 -309
- package/src/services/phone-verification.service.spec.ts +0 -1120
- package/src/services/phone-verification.service.ts +0 -751
- package/src/services/risk-detection.service.spec.ts +0 -1292
- package/src/services/risk-detection.service.ts +0 -1012
- package/src/services/risk-scoring.service.spec.ts +0 -204
- package/src/services/risk-scoring.service.ts +0 -131
- package/src/services/session.service.spec.ts +0 -1293
- package/src/services/session.service.ts +0 -803
- package/src/services/social-account.service.spec.ts +0 -725
- package/src/services/social-auth-base.service.spec.ts +0 -418
- package/src/services/social-auth-base.service.ts +0 -581
- package/src/services/social-auth.service.spec.ts +0 -238
- package/src/services/social-auth.service.ts +0 -436
- package/src/services/social-provider-registry.service.spec.ts +0 -238
- package/src/services/social-provider-registry.service.ts +0 -122
- package/src/services/trusted-device.service.spec.ts +0 -505
- package/src/services/trusted-device.service.ts +0 -339
- package/src/storage/account-lockout-storage.service.spec.ts +0 -310
- package/src/storage/account-lockout-storage.service.ts +0 -89
- package/src/storage/index.ts +0 -3
- package/src/storage/memory-storage.adapter.ts +0 -443
- package/src/storage/rate-limit-storage.service.spec.ts +0 -247
- package/src/storage/rate-limit-storage.service.ts +0 -38
- package/src/templates/html-template.engine.spec.ts +0 -161
- package/src/templates/html-template.engine.ts +0 -688
- package/src/templates/index.ts +0 -7
- package/src/utils/common-passwords.spec.ts +0 -230
- package/src/utils/common-passwords.ts +0 -170
- package/src/utils/context-storage.ts +0 -188
- package/src/utils/cookie-names.util.ts +0 -67
- package/src/utils/cookies.util.ts +0 -94
- package/src/utils/index.ts +0 -12
- package/src/utils/ip-extractor.spec.ts +0 -330
- package/src/utils/ip-extractor.ts +0 -220
- package/src/utils/nauth-logger.spec.ts +0 -388
- package/src/utils/nauth-logger.ts +0 -215
- package/src/utils/pii-redactor.spec.ts +0 -130
- package/src/utils/pii-redactor.ts +0 -288
- package/src/utils/setup/get-repositories.ts +0 -140
- package/src/utils/setup/init-services.ts +0 -422
- package/src/utils/setup/init-social.ts +0 -189
- package/src/utils/setup/init-storage.ts +0 -94
- package/src/utils/setup/register-mfa.ts +0 -165
- package/src/utils/setup/run-nauth-migrations.ts +0 -61
- package/src/utils/token-delivery-policy.ts +0 -38
- package/src/validators/template.validator.ts +0 -219
- package/tsconfig.json +0 -37
- package/tsconfig.lint.json +0 -6
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
import { AuthFlowContext, Rule } from './auth-flow-state-machine.types';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Rule Builder
|
|
5
|
-
*
|
|
6
|
-
* Utility class for composing complex rules using combinators.
|
|
7
|
-
* Supports logical operations: all, any, not.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* ```typescript
|
|
11
|
-
* const complexRule = RuleBuilder.all([
|
|
12
|
-
* Rules.mustChangePassword,
|
|
13
|
-
* RuleBuilder.not(Rules.isMFAExempt)
|
|
14
|
-
* ]);
|
|
15
|
-
* ```
|
|
16
|
-
*/
|
|
17
|
-
export class RuleBuilder {
|
|
18
|
-
/**
|
|
19
|
-
* Combine multiple rules with AND logic
|
|
20
|
-
* All rules must evaluate to true
|
|
21
|
-
*
|
|
22
|
-
* @param rules - Array of rules to combine
|
|
23
|
-
* @returns Combined rule that returns true only if all rules are true
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* ```typescript
|
|
27
|
-
* const rule = RuleBuilder.all([
|
|
28
|
-
* Rules.emailVerificationPending,
|
|
29
|
-
* Rules.isNotSocialLogin
|
|
30
|
-
* ]);
|
|
31
|
-
* ```
|
|
32
|
-
*/
|
|
33
|
-
static all(rules: Rule[]): Rule {
|
|
34
|
-
return (context: AuthFlowContext): boolean => {
|
|
35
|
-
return rules.every((rule) => rule(context));
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Combine multiple rules with OR logic
|
|
41
|
-
* At least one rule must evaluate to true
|
|
42
|
-
*
|
|
43
|
-
* @param rules - Array of rules to combine
|
|
44
|
-
* @returns Combined rule that returns true if any rule is true
|
|
45
|
-
*
|
|
46
|
-
* @example
|
|
47
|
-
* ```typescript
|
|
48
|
-
* const rule = RuleBuilder.any([
|
|
49
|
-
* Rules.isDeviceTrusted,
|
|
50
|
-
* Rules.isMFAExempt
|
|
51
|
-
* ]);
|
|
52
|
-
* ```
|
|
53
|
-
*/
|
|
54
|
-
static any(rules: Rule[]): Rule {
|
|
55
|
-
return (context: AuthFlowContext): boolean => {
|
|
56
|
-
return rules.some((rule) => rule(context));
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Negate a rule
|
|
62
|
-
* Returns true when the rule returns false
|
|
63
|
-
*
|
|
64
|
-
* @param rule - Rule to negate
|
|
65
|
-
* @returns Negated rule
|
|
66
|
-
*
|
|
67
|
-
* @example
|
|
68
|
-
* ```typescript
|
|
69
|
-
* const rule = RuleBuilder.not(Rules.isMFAExempt);
|
|
70
|
-
* ```
|
|
71
|
-
*/
|
|
72
|
-
static not(rule: Rule): Rule {
|
|
73
|
-
return (context: AuthFlowContext): boolean => {
|
|
74
|
-
return !rule(context);
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Authentication Flow Rules
|
|
81
|
-
*
|
|
82
|
-
* Declarative rules for evaluating authentication flow states.
|
|
83
|
-
* Each rule is a pure function that evaluates to true or false based on context.
|
|
84
|
-
*
|
|
85
|
-
* Rules are used in state definitions to determine which state applies.
|
|
86
|
-
*/
|
|
87
|
-
export const Rules = {
|
|
88
|
-
/**
|
|
89
|
-
* User must change password
|
|
90
|
-
* Priority: 1 (highest)
|
|
91
|
-
*
|
|
92
|
-
* @param context - Authentication flow context
|
|
93
|
-
* @returns True if user must change password
|
|
94
|
-
*/
|
|
95
|
-
mustChangePassword: (context: AuthFlowContext): boolean => {
|
|
96
|
-
return context.user.mustChangePassword === true;
|
|
97
|
-
},
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Email verification is pending
|
|
101
|
-
* Priority: 2
|
|
102
|
-
*
|
|
103
|
-
* @param context - Authentication flow context
|
|
104
|
-
* @returns True if email verification is required and not completed
|
|
105
|
-
*/
|
|
106
|
-
emailVerificationPending: (context: AuthFlowContext): boolean => {
|
|
107
|
-
return context.computed.isEmailVerificationRequired;
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Phone collection is needed
|
|
112
|
-
* Priority: 3
|
|
113
|
-
*
|
|
114
|
-
* @param context - Authentication flow context
|
|
115
|
-
* @returns True if phone collection is needed (user has no phone)
|
|
116
|
-
*/
|
|
117
|
-
phoneCollectionNeeded: (context: AuthFlowContext): boolean => {
|
|
118
|
-
return context.computed.isPhoneCollectionNeeded;
|
|
119
|
-
},
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Phone verification is pending
|
|
123
|
-
* Priority: 4
|
|
124
|
-
*
|
|
125
|
-
* @param context - Authentication flow context
|
|
126
|
-
* @returns True if phone verification is required and not completed
|
|
127
|
-
*/
|
|
128
|
-
phoneVerificationPending: (context: AuthFlowContext): boolean => {
|
|
129
|
-
return context.computed.isPhoneVerificationRequired;
|
|
130
|
-
},
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* MFA setup is required
|
|
134
|
-
* Priority: 5
|
|
135
|
-
*
|
|
136
|
-
* @param context - Authentication flow context
|
|
137
|
-
* @returns True if MFA setup is required
|
|
138
|
-
*/
|
|
139
|
-
mfaSetupRequired: (context: AuthFlowContext): boolean => {
|
|
140
|
-
return context.computed.isMFASetupRequired;
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* MFA verification is required
|
|
145
|
-
* Priority: 6
|
|
146
|
-
*
|
|
147
|
-
* @param context - Authentication flow context
|
|
148
|
-
* @returns True if MFA verification is required
|
|
149
|
-
*/
|
|
150
|
-
mfaVerificationRequired: (context: AuthFlowContext): boolean => {
|
|
151
|
-
return context.computed.isMFAVerificationRequired;
|
|
152
|
-
},
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Grace period is active (ADAPTIVE mode with MFA not enabled)
|
|
156
|
-
* Priority: 7
|
|
157
|
-
*
|
|
158
|
-
* This rule applies when:
|
|
159
|
-
* - Enforcement is ADAPTIVE
|
|
160
|
-
* - Grace period is active
|
|
161
|
-
* - MFA is not enabled
|
|
162
|
-
* - User is not blocked
|
|
163
|
-
*
|
|
164
|
-
* @param context - Authentication flow context
|
|
165
|
-
* @returns True if grace period is active and MFA not enabled
|
|
166
|
-
*/
|
|
167
|
-
gracePeriodActiveAdaptive: (context: AuthFlowContext): boolean => {
|
|
168
|
-
const enforcement = context.config.mfa?.enforcement || 'OPTIONAL';
|
|
169
|
-
return (
|
|
170
|
-
enforcement === 'ADAPTIVE' &&
|
|
171
|
-
context.computed.isGracePeriodActive &&
|
|
172
|
-
!context.user.mfaEnabled &&
|
|
173
|
-
!context.computed.isBlocked
|
|
174
|
-
);
|
|
175
|
-
},
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* User is blocked from signing in
|
|
179
|
-
* Priority: 8
|
|
180
|
-
*
|
|
181
|
-
* @param context - Authentication flow context
|
|
182
|
-
* @returns True if user is blocked
|
|
183
|
-
*/
|
|
184
|
-
isBlocked: (context: AuthFlowContext): boolean => {
|
|
185
|
-
return context.computed.isBlocked;
|
|
186
|
-
},
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* User is authenticated (no challenges pending)
|
|
190
|
-
* Priority: 9 (lowest - default state)
|
|
191
|
-
*
|
|
192
|
-
* This rule applies when no other state rules match.
|
|
193
|
-
* It's the default state when all challenges are complete.
|
|
194
|
-
*
|
|
195
|
-
* @param _context - Authentication flow context (unused - always returns true)
|
|
196
|
-
* @returns True if user is authenticated (always true as fallback)
|
|
197
|
-
*/
|
|
198
|
-
authenticated: (_context: AuthFlowContext): boolean => {
|
|
199
|
-
// Always true - this is the default state when no other rules match
|
|
200
|
-
return true;
|
|
201
|
-
},
|
|
202
|
-
};
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import { AuthFlowState, StateDefinition, AuthFlowContext, ResponseMetadata } from './auth-flow-state-machine.types';
|
|
2
|
-
import { AuthChallenge } from '../dto/auth-challenge.dto';
|
|
3
|
-
import { Rules } from './auth-flow-rules';
|
|
4
|
-
import { MFAMethod } from '../enums/mfa-method.enum';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Authentication Flow State Definitions
|
|
8
|
-
*
|
|
9
|
-
* Defines all possible states in the authentication flow with their:
|
|
10
|
-
* - Priority (evaluation order, 1-9)
|
|
11
|
-
* - Condition rules (when state applies)
|
|
12
|
-
* - Challenge mappings (which AuthChallenge this state maps to)
|
|
13
|
-
* - Metadata builders (optional additional response data)
|
|
14
|
-
* - OnEnter hooks (optional actions when state is entered)
|
|
15
|
-
*
|
|
16
|
-
* States are evaluated in priority order. The first state whose condition
|
|
17
|
-
* evaluates to true is selected.
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* ```typescript
|
|
21
|
-
* const state = STATE_DEFINITIONS.find(def => def.condition(context));
|
|
22
|
-
* ```
|
|
23
|
-
*/
|
|
24
|
-
export const STATE_DEFINITIONS: StateDefinition[] = [
|
|
25
|
-
/**
|
|
26
|
-
* Priority 1: Force Password Change
|
|
27
|
-
* Highest priority - must be completed before any other challenges
|
|
28
|
-
*/
|
|
29
|
-
{
|
|
30
|
-
state: AuthFlowState.PENDING_PASSWORD_CHANGE,
|
|
31
|
-
priority: 1,
|
|
32
|
-
condition: Rules.mustChangePassword,
|
|
33
|
-
challenge: AuthChallenge.FORCE_CHANGE_PASSWORD,
|
|
34
|
-
},
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Priority 2: Email Verification
|
|
38
|
-
* Required before phone verification (sequential flow)
|
|
39
|
-
*/
|
|
40
|
-
{
|
|
41
|
-
state: AuthFlowState.PENDING_EMAIL_VERIFICATION,
|
|
42
|
-
priority: 2,
|
|
43
|
-
condition: Rules.emailVerificationPending,
|
|
44
|
-
challenge: AuthChallenge.VERIFY_EMAIL,
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Priority 3: Phone Collection
|
|
49
|
-
* User must provide phone number before verification
|
|
50
|
-
*/
|
|
51
|
-
{
|
|
52
|
-
state: AuthFlowState.PENDING_PHONE_COLLECTION,
|
|
53
|
-
priority: 3,
|
|
54
|
-
condition: Rules.phoneCollectionNeeded,
|
|
55
|
-
challenge: AuthChallenge.VERIFY_PHONE,
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Priority 4: Phone Verification
|
|
60
|
-
* Required after email verification (sequential flow)
|
|
61
|
-
*/
|
|
62
|
-
{
|
|
63
|
-
state: AuthFlowState.PENDING_PHONE_VERIFICATION,
|
|
64
|
-
priority: 4,
|
|
65
|
-
condition: Rules.phoneVerificationPending,
|
|
66
|
-
challenge: AuthChallenge.VERIFY_PHONE,
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Priority 5: MFA Setup Required
|
|
71
|
-
* Required when enforcement is REQUIRED/ADAPTIVE and grace period expired
|
|
72
|
-
*/
|
|
73
|
-
{
|
|
74
|
-
state: AuthFlowState.PENDING_MFA_SETUP,
|
|
75
|
-
priority: 5,
|
|
76
|
-
condition: Rules.mfaSetupRequired,
|
|
77
|
-
challenge: AuthChallenge.MFA_SETUP_REQUIRED,
|
|
78
|
-
/**
|
|
79
|
-
* OnEnter hook: Auto-complete SMS MFA setup if phone is already verified
|
|
80
|
-
*
|
|
81
|
-
* Special case: If user's phone is already verified and they choose SMS MFA,
|
|
82
|
-
* we can skip the SMS verification step during setup (improves UX).
|
|
83
|
-
* The phone was already verified, so we trust it for MFA setup.
|
|
84
|
-
*
|
|
85
|
-
* This sets skipMFAVerification flag which will be checked during MFA setup completion.
|
|
86
|
-
*/
|
|
87
|
-
onEnter: async (context: AuthFlowContext): Promise<void> => {
|
|
88
|
-
// Check if phone is verified and preferred method is SMS
|
|
89
|
-
if (context.user.isPhoneVerified && context.user.phone && context.user.preferredMfaMethod === MFAMethod.SMS) {
|
|
90
|
-
// Auto-complete: Set skipMFAVerification flag
|
|
91
|
-
// This will be used during MFA setup completion to skip SMS verification
|
|
92
|
-
context.skipMFAVerification = true;
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Priority 6: MFA Verification Required
|
|
99
|
-
* Required when MFA is enabled and verification is needed
|
|
100
|
-
*/
|
|
101
|
-
{
|
|
102
|
-
state: AuthFlowState.PENDING_MFA_VERIFICATION,
|
|
103
|
-
priority: 6,
|
|
104
|
-
condition: Rules.mfaVerificationRequired,
|
|
105
|
-
challenge: AuthChallenge.MFA_REQUIRED,
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Priority 7: Grace Period Active
|
|
110
|
-
* ADAPTIVE mode with grace period active and MFA not enabled
|
|
111
|
-
* This is a special state that allows login but includes metadata about grace period
|
|
112
|
-
*/
|
|
113
|
-
{
|
|
114
|
-
state: AuthFlowState.GRACE_PERIOD_ACTIVE,
|
|
115
|
-
priority: 7,
|
|
116
|
-
condition: Rules.gracePeriodActiveAdaptive,
|
|
117
|
-
/**
|
|
118
|
-
* Build metadata for grace period state
|
|
119
|
-
* Includes grace period end timestamp and risk information
|
|
120
|
-
*/
|
|
121
|
-
buildMetadata: (context: AuthFlowContext): ResponseMetadata | undefined => {
|
|
122
|
-
return {
|
|
123
|
-
gracePeriodEndsAt: context.computed.gracePeriodEndsAt,
|
|
124
|
-
riskScore: context.computed.riskScore,
|
|
125
|
-
riskLevel: context.computed.riskLevel,
|
|
126
|
-
};
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Priority 8: Blocked
|
|
132
|
-
* User is blocked from signing in due to high risk
|
|
133
|
-
*/
|
|
134
|
-
{
|
|
135
|
-
state: AuthFlowState.BLOCKED,
|
|
136
|
-
priority: 8,
|
|
137
|
-
condition: Rules.isBlocked,
|
|
138
|
-
/**
|
|
139
|
-
* Build metadata for blocked state
|
|
140
|
-
* Includes block expiration and reason
|
|
141
|
-
*/
|
|
142
|
-
buildMetadata: (context: AuthFlowContext): ResponseMetadata | undefined => {
|
|
143
|
-
return {
|
|
144
|
-
blockedUntil: context.computed.blockedUntil,
|
|
145
|
-
reason: context.computed.blockReason,
|
|
146
|
-
};
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Priority 9: Authenticated
|
|
152
|
-
* Default state when all challenges are complete
|
|
153
|
-
* This rule always evaluates to true, so it's the fallback state
|
|
154
|
-
*/
|
|
155
|
-
{
|
|
156
|
-
state: AuthFlowState.AUTHENTICATED,
|
|
157
|
-
priority: 9,
|
|
158
|
-
condition: Rules.authenticated,
|
|
159
|
-
},
|
|
160
|
-
];
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Get state definition by state
|
|
164
|
-
*
|
|
165
|
-
* @param state - State to get definition for
|
|
166
|
-
* @returns State definition or undefined if not found
|
|
167
|
-
*
|
|
168
|
-
* @example
|
|
169
|
-
* ```typescript
|
|
170
|
-
* const def = getStateDefinition(AuthFlowState.PENDING_EMAIL_VERIFICATION);
|
|
171
|
-
* ```
|
|
172
|
-
*/
|
|
173
|
-
export function getStateDefinition(state: AuthFlowState): StateDefinition | undefined {
|
|
174
|
-
return STATE_DEFINITIONS.find((def) => def.state === state);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Get state definitions sorted by priority
|
|
179
|
-
*
|
|
180
|
-
* @returns State definitions sorted by priority (1-9)
|
|
181
|
-
*
|
|
182
|
-
* @example
|
|
183
|
-
* ```typescript
|
|
184
|
-
* const sorted = getStateDefinitionsByPriority();
|
|
185
|
-
* // Evaluates states in order: PENDING_PASSWORD_CHANGE, PENDING_EMAIL_VERIFICATION, ...
|
|
186
|
-
* ```
|
|
187
|
-
*/
|
|
188
|
-
export function getStateDefinitionsByPriority(): StateDefinition[] {
|
|
189
|
-
return [...STATE_DEFINITIONS].sort((a, b) => a.priority - b.priority);
|
|
190
|
-
}
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { AuthFlowState, AuthFlowContext, StateDefinition, ResponseMetadata } from './auth-flow-state-machine.types';
|
|
2
|
-
import { AuthFlowContextBuilder } from './auth-flow-context-builder.service';
|
|
3
|
-
import { NAuthLogger } from '../utils/nauth-logger';
|
|
4
|
-
import { getStateDefinitionsByPriority, getStateDefinition } from './auth-flow-state-definitions';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Authentication Flow State Machine Service
|
|
8
|
-
*
|
|
9
|
-
* Core engine for evaluating authentication flow states using declarative rules.
|
|
10
|
-
* Replaces imperative if/else logic with a rule-based state machine.
|
|
11
|
-
*
|
|
12
|
-
* **How it works:**
|
|
13
|
-
* 1. Build context with pre-computed values
|
|
14
|
-
* 2. Evaluate states in priority order (1-9)
|
|
15
|
-
* 3. Select first state whose condition rule evaluates to true
|
|
16
|
-
* 4. Execute onEnter hook if defined
|
|
17
|
-
* 5. Return state with metadata
|
|
18
|
-
*
|
|
19
|
-
* **Benefits:**
|
|
20
|
-
* - Declarative and maintainable
|
|
21
|
-
* - Easy to test (pure functions)
|
|
22
|
-
* - Extensible (add new states/rules easily)
|
|
23
|
-
* - Clear priority ordering
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* ```typescript
|
|
27
|
-
* const state = await stateMachine.evaluateState(context);
|
|
28
|
-
* const definition = stateMachine.getStateDefinition(state);
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
export class AuthFlowStateMachineService {
|
|
32
|
-
constructor(
|
|
33
|
-
private readonly contextBuilder: AuthFlowContextBuilder,
|
|
34
|
-
private readonly logger?: NAuthLogger,
|
|
35
|
-
) {}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Evaluate authentication flow state
|
|
39
|
-
*
|
|
40
|
-
* Evaluates states in priority order and returns the first matching state.
|
|
41
|
-
* Executes onEnter hook if defined for the selected state.
|
|
42
|
-
*
|
|
43
|
-
* @param context - Authentication flow context
|
|
44
|
-
* @returns Evaluated state
|
|
45
|
-
*
|
|
46
|
-
* @example
|
|
47
|
-
* ```typescript
|
|
48
|
-
* const context = await contextBuilder.build({ user, config, authMethod: 'password' });
|
|
49
|
-
* const state = await stateMachine.evaluateState(context);
|
|
50
|
-
* // Returns: AuthFlowState.PENDING_EMAIL_VERIFICATION
|
|
51
|
-
* ```
|
|
52
|
-
*/
|
|
53
|
-
async evaluateState(context: AuthFlowContext): Promise<AuthFlowState> {
|
|
54
|
-
// Get state definitions sorted by priority
|
|
55
|
-
const stateDefinitions = getStateDefinitionsByPriority();
|
|
56
|
-
|
|
57
|
-
this.logger?.debug?.(
|
|
58
|
-
`[StateMachine] Evaluating states for user ${context.user.sub} (priority 1-9, first match wins)`,
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
// Evaluate states in priority order
|
|
62
|
-
for (const definition of stateDefinitions) {
|
|
63
|
-
// Evaluate condition rule
|
|
64
|
-
const ruleResult = definition.condition(context);
|
|
65
|
-
this.logger?.debug?.(
|
|
66
|
-
`[StateMachine] Priority ${definition.priority}: ${definition.state} → ${ruleResult ? 'MATCH' : 'skip'}`,
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
if (ruleResult) {
|
|
70
|
-
// State matches - execute onEnter hook if defined
|
|
71
|
-
if (definition.onEnter) {
|
|
72
|
-
this.logger?.debug?.(`[StateMachine] Executing onEnter hook for ${definition.state}`);
|
|
73
|
-
try {
|
|
74
|
-
await definition.onEnter(context);
|
|
75
|
-
} catch (error) {
|
|
76
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
77
|
-
this.logger?.warn?.(`onEnter hook failed for state ${definition.state}: ${errorMessage}`, {
|
|
78
|
-
error,
|
|
79
|
-
state: definition.state,
|
|
80
|
-
userId: context.user.id,
|
|
81
|
-
});
|
|
82
|
-
// Continue with state selection even if hook fails
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
this.logger?.debug?.(`[StateMachine] ✓ Selected state: ${definition.state} for user ${context.user.sub}`);
|
|
87
|
-
return definition.state;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Fallback: Should never reach here (AUTHENTICATED always matches)
|
|
92
|
-
// But return AUTHENTICATED as safe default
|
|
93
|
-
this.logger?.warn?.(`No state matched for user ${context.user.sub} - falling back to AUTHENTICATED`, {
|
|
94
|
-
userId: context.user.id,
|
|
95
|
-
});
|
|
96
|
-
return AuthFlowState.AUTHENTICATED;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Get state definition by state
|
|
101
|
-
*
|
|
102
|
-
* @param state - State to get definition for
|
|
103
|
-
* @returns State definition or undefined if not found
|
|
104
|
-
*
|
|
105
|
-
* @example
|
|
106
|
-
* ```typescript
|
|
107
|
-
* const def = stateMachine.getStateDefinition(AuthFlowState.PENDING_EMAIL_VERIFICATION);
|
|
108
|
-
* ```
|
|
109
|
-
*/
|
|
110
|
-
getStateDefinition(state: AuthFlowState): StateDefinition | undefined {
|
|
111
|
-
return getStateDefinition(state);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Build metadata for state response
|
|
116
|
-
*
|
|
117
|
-
* Calls buildMetadata function if defined for the state.
|
|
118
|
-
*
|
|
119
|
-
* @param state - State to build metadata for
|
|
120
|
-
* @param context - Authentication flow context
|
|
121
|
-
* @returns Metadata object or undefined
|
|
122
|
-
*
|
|
123
|
-
* @example
|
|
124
|
-
* ```typescript
|
|
125
|
-
* const metadata = await stateMachine.buildMetadata(state, context);
|
|
126
|
-
* // Returns: { gracePeriodEndsAt: Date, riskScore: 45, riskLevel: 'medium' }
|
|
127
|
-
* ```
|
|
128
|
-
*/
|
|
129
|
-
buildMetadata(state: AuthFlowState, context: AuthFlowContext): ResponseMetadata | undefined {
|
|
130
|
-
const definition = this.getStateDefinition(state);
|
|
131
|
-
if (!definition || !definition.buildMetadata) {
|
|
132
|
-
return undefined;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
return definition.buildMetadata(context);
|
|
137
|
-
} catch (error) {
|
|
138
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
139
|
-
this.logger?.warn?.(`buildMetadata failed for state ${state}: ${errorMessage}`, {
|
|
140
|
-
error,
|
|
141
|
-
state,
|
|
142
|
-
userId: context.user.id,
|
|
143
|
-
});
|
|
144
|
-
return undefined;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Transition after challenge completion
|
|
150
|
-
*
|
|
151
|
-
* Re-evaluates state after a challenge is completed.
|
|
152
|
-
* This is used in the challenge completion flow to determine the next state.
|
|
153
|
-
*
|
|
154
|
-
* @param params - Transition parameters
|
|
155
|
-
* @param params.completedChallenge - Challenge that was just completed
|
|
156
|
-
* @param params.context - Current authentication flow context
|
|
157
|
-
* @param params.updateFn - Function to update user data (e.g., mark email as verified)
|
|
158
|
-
* @returns New state after transition
|
|
159
|
-
*
|
|
160
|
-
* @example
|
|
161
|
-
* ```typescript
|
|
162
|
-
* const newState = await stateMachine.transitionAfterChallenge({
|
|
163
|
-
* completedChallenge: AuthChallenge.VERIFY_EMAIL,
|
|
164
|
-
* context,
|
|
165
|
-
* updateFn: async (user) => {
|
|
166
|
-
* user.isEmailVerified = true;
|
|
167
|
-
* await userRepository.save(user);
|
|
168
|
-
* }
|
|
169
|
-
* });
|
|
170
|
-
* ```
|
|
171
|
-
*/
|
|
172
|
-
async transitionAfterChallenge(params: {
|
|
173
|
-
completedChallenge: string;
|
|
174
|
-
context: AuthFlowContext;
|
|
175
|
-
updateFn?: (user: AuthFlowContext['user']) => Promise<void>;
|
|
176
|
-
}): Promise<AuthFlowState> {
|
|
177
|
-
const { completedChallenge, context, updateFn } = params;
|
|
178
|
-
|
|
179
|
-
// Update user data if update function provided
|
|
180
|
-
if (updateFn) {
|
|
181
|
-
try {
|
|
182
|
-
await updateFn(context.user);
|
|
183
|
-
} catch (error) {
|
|
184
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
185
|
-
this.logger?.error?.(`Failed to update user after challenge completion: ${errorMessage}`, {
|
|
186
|
-
error,
|
|
187
|
-
challenge: completedChallenge,
|
|
188
|
-
userId: context.user.id,
|
|
189
|
-
});
|
|
190
|
-
// Continue with re-evaluation even if update fails
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Re-build context with updated user data
|
|
195
|
-
const newContext = await this.contextBuilder.build({
|
|
196
|
-
user: context.user,
|
|
197
|
-
config: context.config,
|
|
198
|
-
authMethod: context.authMethod,
|
|
199
|
-
authProvider: context.authProvider,
|
|
200
|
-
deviceToken: context.deviceToken,
|
|
201
|
-
skipMFAVerification: context.skipMFAVerification,
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// Re-evaluate state
|
|
205
|
-
return this.evaluateState(newContext);
|
|
206
|
-
}
|
|
207
|
-
}
|