| | | 1 | | using ArturRios.Data.Relational.Core.Entities; |
| | | 2 | | |
| | | 3 | | namespace ArturRios.Heimdall.Domain.Entities; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// A person's two-factor authentication configuration (UC-36 – UC-40, FR-2F-01…FR-2F-12). At |
| | | 7 | | /// most one row per person. <see cref="IsActive" /> distinguishes a confirmed configuration from |
| | | 8 | | /// one still pending confirmation (UC-36 created it, UC-37 has not yet activated it). Uses only |
| | | 9 | | /// an internal <c>Id</c> — never addressed by ID in a path; a person's own configuration is |
| | | 10 | | /// reached implicitly through their authenticated identity (see §4.1 of the System Requirements |
| | | 11 | | /// Document). |
| | | 12 | | /// </summary> |
| | | 13 | | public class TwoFactorAuth : Entity<long> |
| | | 14 | | { |
| | | 15 | | /// <summary>Foreign key to the owning <see cref="Person" /> (internal Id). Required, unique.</summary> |
| | 462 | 16 | | public long PersonId { get; set; } |
| | | 17 | | |
| | | 18 | | /// <summary>Whether the authenticator-app method is configured (FR-2F-02).</summary> |
| | 560 | 19 | | public bool AppEnabled { get; set; } |
| | | 20 | | |
| | | 21 | | /// <summary>Whether the email method is configured (FR-2F-03).</summary> |
| | 507 | 22 | | public bool EmailEnabled { get; set; } |
| | | 23 | | |
| | | 24 | | /// <summary> |
| | | 25 | | /// The TOTP secret, encrypted at rest via <c>ITotpSecretProtector</c>. Present only when |
| | | 26 | | /// <see cref="AppEnabled" /> is <c>true</c>; the plaintext secret is never stored. |
| | | 27 | | /// </summary> |
| | 426 | 28 | | public byte[]? TotpSecretEncrypted { get; set; } |
| | | 29 | | |
| | | 30 | | /// <summary> |
| | | 31 | | /// Set <c>true</c> only once every method selected at setup has been confirmed (UC-37, |
| | | 32 | | /// FR-2F-04). <c>false</c> with a row present means setup was initiated (UC-36) but not yet |
| | | 33 | | /// confirmed. |
| | | 34 | | /// </summary> |
| | 527 | 35 | | public bool IsActive { get; set; } |
| | | 36 | | |
| | | 37 | | /// <summary> |
| | | 38 | | /// The TOTP time step (30-second counter since the Unix epoch) of the most recently accepted |
| | | 39 | | /// authenticator-app code, or <c>null</c> while none has been accepted. A code is refused |
| | | 40 | | /// unless its step is strictly greater, which is what makes an app code single-use: without |
| | | 41 | | /// it the same six digits verify for as long as they are current, and the verification |
| | | 42 | | /// window widens that to a step either side (FR-2F-14, RFC 6238 §5.2). |
| | | 43 | | /// </summary> |
| | 99 | 44 | | public long? LastTotpTimeStepUsed { get; set; } |
| | | 45 | | |
| | | 46 | | /// <summary> |
| | | 47 | | /// How many times the current challenge has had its email code reissued (UC-46, FR-2F-13). |
| | | 48 | | /// Reset to zero every time UC-11 issues a challenge, so the cap counts reissues per |
| | | 49 | | /// authentication attempt rather than per account — a fresh budget costs a fresh password |
| | | 50 | | /// check, which is the price FR-2F-13 charged before a resend existed. |
| | | 51 | | /// </summary> |
| | | 52 | | /// <remarks> |
| | | 53 | | /// Kept here rather than as a claim on the challenge token because UC-46 must never return a |
| | | 54 | | /// new token: one could only be returned for a genuine challenge, so returning it would tell |
| | | 55 | | /// an anonymous caller that the challenge they presented was real. A claim that cannot be |
| | | 56 | | /// reissued cannot be incremented, which leaves the count on the server. |
| | | 57 | | /// </remarks> |
| | 125 | 58 | | public int EmailCodeReissueCount { get; set; } |
| | | 59 | | |
| | | 60 | | /// <summary> |
| | | 61 | | /// Identifies the one challenge UC-11 most recently issued for this configuration and UC-38 has |
| | | 62 | | /// not yet redeemed, or <c>null</c> when none is outstanding. The challenge token carries the |
| | | 63 | | /// same value as a claim, and is honoured only while the two agree (FR-2F-10). |
| | | 64 | | /// </summary> |
| | | 65 | | /// <remarks> |
| | | 66 | | /// A signature and an expiry say a challenge token is genuine and recent; neither says it has |
| | | 67 | | /// not already been spent. Without this a redeemed challenge stayed redeemable for the rest of |
| | | 68 | | /// its ten minutes, so whoever held it could trade any further factor for another full token |
| | | 69 | | /// without the password. Redeeming clears it, and a new login replaces it, so only the latest |
| | | 70 | | /// unredeemed challenge is ever accepted — the same "only the newest is live" rule the email |
| | | 71 | | /// codes issued alongside it already follow. |
| | | 72 | | /// </remarks> |
| | 438 | 73 | | public Guid? ChallengeId { get; set; } |
| | | 74 | | |
| | | 75 | | /// <summary> |
| | | 76 | | /// Guesses made at the outstanding challenge with an authenticator-app code or a recovery code. |
| | | 77 | | /// Reset with <see cref="ChallengeId" /> whenever UC-11 issues a challenge; at the cap the |
| | | 78 | | /// challenge is cleared, so further guessing costs a fresh password check. |
| | | 79 | | /// </summary> |
| | | 80 | | /// <remarks> |
| | | 81 | | /// The email code has always had its own cap (FR-2F-13): five wrong guesses retire it. An app |
| | | 82 | | /// code and a recovery code had none — only the per-address rate limit — so a challenge could |
| | | 83 | | /// be guessed at for its whole ten minutes from as many addresses as an attacker could muster. |
| | | 84 | | /// This gives them the same five, counted on the challenge because neither has a row of its |
| | | 85 | | /// own to count on: a TOTP code is computed, and a guessed recovery code matches no row. |
| | | 86 | | /// </remarks> |
| | 275 | 87 | | public int ChallengeAttempts { get; set; } |
| | | 88 | | |
| | | 89 | | /// <summary>Creation timestamp.</summary> |
| | 35 | 90 | | public DateTime CreatedAt { get; set; } |
| | | 91 | | |
| | | 92 | | /// <summary>Last update timestamp.</summary> |
| | 65 | 93 | | public DateTime UpdatedAt { get; set; } |
| | | 94 | | |
| | | 95 | | // Navigation properties |
| | | 96 | | |
| | | 97 | | /// <summary>The person this configuration belongs to.</summary> |
| | 34 | 98 | | public Person Person { get; set; } = null!; |
| | | 99 | | |
| | | 100 | | /// <summary>Outstanding email codes issued for this configuration (UC-36 step 4, UC-37 step 2).</summary> |
| | 478 | 101 | | public ICollection<TwoFactorEmailCode> EmailCodes { get; set; } = new List<TwoFactorEmailCode>(); |
| | | 102 | | |
| | | 103 | | /// <summary>Recovery codes issued for this configuration (UC-37 step 4, UC-38, UC-40).</summary> |
| | 476 | 104 | | public ICollection<TwoFactorRecoveryCode> RecoveryCodes { get; set; } = new List<TwoFactorRecoveryCode>(); |
| | | 105 | | } |