1- namespace CodeBeam . UltimateAuth . Credentials . Contracts ;
1+ using CodeBeam . UltimateAuth . Core . Errors ;
2+
3+ namespace CodeBeam . UltimateAuth . Credentials . Contracts ;
24
35public sealed class CredentialSecurityState
46{
57 public DateTimeOffset ? RevokedAt { get ; }
68 public DateTimeOffset ? LockedUntil { get ; }
79 public DateTimeOffset ? ExpiresAt { get ; }
8- public DateTimeOffset ? ResetRequestedAt { get ; init ; }
10+ public DateTimeOffset ? ResetRequestedAt { get ; }
11+ public DateTimeOffset ? ResetExpiresAt { get ; }
12+ public DateTimeOffset ? ResetConsumedAt { get ; }
13+ public int FailedAttemptCount { get ; }
14+ public DateTimeOffset ? LastFailedAt { get ; }
915 public Guid SecurityStamp { get ; }
1016
17+ public CredentialSecurityState (
18+ DateTimeOffset ? revokedAt = null ,
19+ DateTimeOffset ? lockedUntil = null ,
20+ DateTimeOffset ? expiresAt = null ,
21+ DateTimeOffset ? resetRequestedAt = null ,
22+ DateTimeOffset ? resetExpiresAt = null ,
23+ DateTimeOffset ? resetConsumedAt = null ,
24+ int failedAttemptCount = 0 ,
25+ DateTimeOffset ? lastFailedAt = null ,
26+ Guid securityStamp = default )
27+ {
28+ RevokedAt = revokedAt ;
29+ LockedUntil = lockedUntil ;
30+ ExpiresAt = expiresAt ;
31+ ResetRequestedAt = resetRequestedAt ;
32+ ResetExpiresAt = resetExpiresAt ;
33+ ResetConsumedAt = resetConsumedAt ;
34+ FailedAttemptCount = failedAttemptCount ;
35+ LastFailedAt = lastFailedAt ;
36+ SecurityStamp = securityStamp ;
37+ }
38+
1139 public CredentialSecurityStatus Status ( DateTimeOffset now )
1240 {
1341 if ( RevokedAt is not null )
@@ -20,25 +48,19 @@ public CredentialSecurityStatus Status(DateTimeOffset now)
2048 return CredentialSecurityStatus . Expired ;
2149
2250 if ( ResetRequestedAt is not null )
51+ {
52+ if ( ResetConsumedAt is not null )
53+ return CredentialSecurityStatus . Active ;
54+
55+ if ( ResetExpiresAt is not null && ResetExpiresAt <= now )
56+ return CredentialSecurityStatus . Active ;
57+
2358 return CredentialSecurityStatus . ResetRequested ;
59+ }
2460
2561 return CredentialSecurityStatus . Active ;
2662 }
2763
28- public CredentialSecurityState (
29- DateTimeOffset ? revokedAt = null ,
30- DateTimeOffset ? lockedUntil = null ,
31- DateTimeOffset ? expiresAt = null ,
32- DateTimeOffset ? resetRequestedAt = null ,
33- Guid securityStamp = default )
34- {
35- RevokedAt = revokedAt ;
36- LockedUntil = lockedUntil ;
37- ExpiresAt = expiresAt ;
38- ResetRequestedAt = resetRequestedAt ;
39- SecurityStamp = securityStamp ;
40- }
41-
4264 /// <summary>
4365 /// Determines whether the credential can be used at the given time.
4466 /// </summary>
@@ -51,54 +73,158 @@ public static CredentialSecurityState Active(Guid? securityStamp = null)
5173 lockedUntil : null ,
5274 expiresAt : null ,
5375 resetRequestedAt : null ,
54- securityStamp : securityStamp ?? Guid . NewGuid ( ) ) ;
76+ resetExpiresAt : null ,
77+ resetConsumedAt : null ,
78+ failedAttemptCount : 0 ,
79+ lastFailedAt : null ,
80+ securityStamp : securityStamp ?? Guid . NewGuid ( )
81+ ) ;
5582 }
5683
84+ /// <summary>
85+ /// Revokes the credential permanently.
86+ /// </summary>
5787 public CredentialSecurityState Revoke ( DateTimeOffset now )
5888 {
89+ if ( RevokedAt is not null )
90+ return this ;
91+
5992 return new CredentialSecurityState (
6093 revokedAt : now ,
6194 lockedUntil : LockedUntil ,
6295 expiresAt : ExpiresAt ,
6396 resetRequestedAt : ResetRequestedAt ,
64- securityStamp : Guid . NewGuid ( ) ) ;
97+ resetExpiresAt : ResetExpiresAt ,
98+ resetConsumedAt : ResetConsumedAt ,
99+ failedAttemptCount : FailedAttemptCount ,
100+ lastFailedAt : LastFailedAt ,
101+ securityStamp : Guid . NewGuid ( )
102+ ) ;
65103 }
66104
105+ /// <summary>
106+ /// Sets or clears expiry while preserving the rest of the state.
107+ /// </summary>
67108 public CredentialSecurityState SetExpiry ( DateTimeOffset ? expiresAt )
68109 {
110+ // optional: normalize already-expired value? keep as-is; domain policy can decide.
111+ if ( ExpiresAt == expiresAt )
112+ return this ;
113+
69114 return new CredentialSecurityState (
70115 revokedAt : RevokedAt ,
71116 lockedUntil : LockedUntil ,
72117 expiresAt : expiresAt ,
73118 resetRequestedAt : ResetRequestedAt ,
74- securityStamp : SecurityStamp ) ;
119+ resetExpiresAt : ResetExpiresAt ,
120+ resetConsumedAt : ResetConsumedAt ,
121+ failedAttemptCount : FailedAttemptCount ,
122+ lastFailedAt : LastFailedAt ,
123+ securityStamp : EnsureStamp ( SecurityStamp )
124+ ) ;
75125 }
76126
77- public CredentialSecurityState BeginReset ( DateTimeOffset now , bool rotateStamp = true )
78- => new (
79- revokedAt : RevokedAt ,
80- lockedUntil : LockedUntil ,
81- expiresAt : ExpiresAt ,
82- resetRequestedAt : now ,
83- securityStamp : rotateStamp ? Guid . NewGuid ( ) : SecurityStamp
84- ) ;
85-
86- public CredentialSecurityState CompleteReset ( bool rotateStamp = true )
87- => new (
127+ private static Guid EnsureStamp ( Guid stamp ) => stamp == Guid . Empty ? Guid . NewGuid ( ) : stamp ;
128+
129+ public CredentialSecurityState RotateStamp ( )
130+ {
131+ return new CredentialSecurityState (
88132 revokedAt : RevokedAt ,
89133 lockedUntil : LockedUntil ,
90134 expiresAt : ExpiresAt ,
91- resetRequestedAt : null ,
92- securityStamp : rotateStamp ? Guid . NewGuid ( ) : SecurityStamp
135+ resetRequestedAt : ResetRequestedAt ,
136+ resetExpiresAt : ResetExpiresAt ,
137+ resetConsumedAt : ResetConsumedAt ,
138+ failedAttemptCount : FailedAttemptCount ,
139+ lastFailedAt : LastFailedAt ,
140+ securityStamp : Guid . NewGuid ( )
93141 ) ;
142+ }
94143
95- public CredentialSecurityState RotateStamp ( )
144+ public CredentialSecurityState RegisterSuccessfulAuthentication ( )
96145 {
97146 return new CredentialSecurityState (
98147 revokedAt : RevokedAt ,
99- lockedUntil : LockedUntil ,
148+ lockedUntil : null ,
149+ expiresAt : ExpiresAt ,
150+ resetRequestedAt : ResetRequestedAt ,
151+ resetExpiresAt : ResetExpiresAt ,
152+ resetConsumedAt : ResetConsumedAt ,
153+ failedAttemptCount : 0 ,
154+ lastFailedAt : null ,
155+ securityStamp : EnsureStamp ( SecurityStamp )
156+ ) ;
157+ }
158+
159+ public CredentialSecurityState RegisterFailedAttempt ( DateTimeOffset now , int threshold , TimeSpan lockoutDuration )
160+ {
161+ if ( threshold <= 0 )
162+ throw new UAuthValidationException ( nameof ( threshold ) ) ;
163+
164+ var failed = FailedAttemptCount + 1 ;
165+
166+ var newLockedUntil = LockedUntil ;
167+
168+ if ( failed >= threshold )
169+ {
170+ var candidate = now . Add ( lockoutDuration ) ;
171+
172+ if ( LockedUntil is null || candidate > LockedUntil )
173+ newLockedUntil = candidate ;
174+ }
175+
176+ return new CredentialSecurityState (
177+ revokedAt : RevokedAt ,
178+ lockedUntil : newLockedUntil ,
100179 expiresAt : ExpiresAt ,
101180 resetRequestedAt : ResetRequestedAt ,
102- securityStamp : Guid . NewGuid ( ) ) ;
181+ resetExpiresAt : ResetExpiresAt ,
182+ resetConsumedAt : ResetConsumedAt ,
183+ failedAttemptCount : failed ,
184+ lastFailedAt : now ,
185+ securityStamp : EnsureStamp ( SecurityStamp )
186+ ) ;
187+ }
188+
189+ public CredentialSecurityState BeginReset ( DateTimeOffset now , TimeSpan validity )
190+ {
191+ if ( validity <= TimeSpan . Zero )
192+ throw new UAuthValidationException ( "credential_lockout_threshold_invalid" ) ;
193+
194+ return new CredentialSecurityState (
195+ revokedAt : RevokedAt ,
196+ lockedUntil : LockedUntil ,
197+ expiresAt : ExpiresAt ,
198+ resetRequestedAt : now ,
199+ resetExpiresAt : now . Add ( validity ) ,
200+ resetConsumedAt : null ,
201+ failedAttemptCount : FailedAttemptCount ,
202+ lastFailedAt : LastFailedAt ,
203+ securityStamp : Guid . NewGuid ( )
204+ ) ;
205+ }
206+
207+ public CredentialSecurityState CompleteReset ( DateTimeOffset now , bool rotateStamp = true )
208+ {
209+ if ( ResetRequestedAt is null )
210+ throw new UAuthValidationException ( "reset_not_requested" ) ;
211+
212+ if ( ResetConsumedAt is not null )
213+ throw new UAuthValidationException ( "reset_already_consumed" ) ;
214+
215+ if ( ResetExpiresAt is not null && ResetExpiresAt <= now )
216+ throw new UAuthValidationException ( "reset_expired" ) ;
217+
218+ return new CredentialSecurityState (
219+ revokedAt : RevokedAt ,
220+ lockedUntil : null ,
221+ expiresAt : ExpiresAt ,
222+ resetRequestedAt : null ,
223+ resetExpiresAt : null ,
224+ resetConsumedAt : now ,
225+ failedAttemptCount : 0 ,
226+ lastFailedAt : null ,
227+ securityStamp : rotateStamp ? Guid . NewGuid ( ) : EnsureStamp ( SecurityStamp )
228+ ) ;
103229 }
104230}
0 commit comments