Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions Tests/Fixtures/MultichallengeEnrollFixtures.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
namespace Tests.Fixtures
{
/// <summary>
/// Captured response bodies for the enroll-via-multichallenge flow. Each constant is
/// a representative server response; the parser only inspects JSON structure, so PNG
/// payloads and long otpauth secrets are abbreviated to keep the strings readable.
/// </summary>
internal static class MultichallengeEnrollFixtures
{
/// HOTP enroll-via-multichallenge CHALLENGE — mandatory enrollment.
public const string HotpEnrollChallenge = @"{
""detail"": {
""client_mode"": ""interactive"",
""enroll_via_multichallenge"": true,
""enroll_via_multichallenge_optional"": false,
""image"": ""data:image/png;base64,STUB"",
""link"": ""otpauth://hotp/OATH0000AC38?secret=ABCDEFG"",
""message"": ""Please scan the QR code and enter the OTP value!"",
""multi_challenge"": [{
""client_mode"": ""interactive"",
""image"": ""data:image/png;base64,STUB"",
""link"": ""otpauth://hotp/OATH0000AC38?secret=ABCDEFG"",
""message"": ""Please scan the QR code and enter the OTP value!"",
""serial"": ""OATH0000AC38"",
""transaction_id"": ""18249856845542401525"",
""type"": ""hotp""
}],
""serial"": ""OATH0000AC38"",
""transaction_id"": ""18249856845542401525"",
""type"": ""hotp""
},
""result"": {""authentication"": ""CHALLENGE"", ""status"": true, ""value"": false}
}";

/// Same shape as HotpEnrollChallenge but with enroll_via_multichallenge_optional=true,
/// which is what makes the "Not Now" path eligible.
public const string HotpEnrollChallengeOptional = @"{
""detail"": {
""client_mode"": ""interactive"",
""enroll_via_multichallenge"": true,
""enroll_via_multichallenge_optional"": true,
""image"": ""data:image/png;base64,STUB"",
""link"": ""otpauth://hotp/OATH0000F6AF?secret=ABCDEFG"",
""message"": ""Please scan the QR code and enter the OTP value!"",
""multi_challenge"": [{
""client_mode"": ""interactive"",
""image"": ""data:image/png;base64,STUB"",
""link"": ""otpauth://hotp/OATH0000F6AF?secret=ABCDEFG"",
""message"": ""Please scan the QR code and enter the OTP value!"",
""serial"": ""OATH0000F6AF"",
""transaction_id"": ""08062584491116057815"",
""type"": ""hotp""
}],
""serial"": ""OATH0000F6AF"",
""transaction_id"": ""08062584491116057815"",
""type"": ""hotp""
},
""result"": {""authentication"": ""CHALLENGE"", ""status"": true, ""value"": false}
}";

/// Email enrollment, first step — server asks for the user's email address. No
/// image; the prompt itself is the entire challenge.
public const string EmailEnrollAskForAddress = @"{
""detail"": {
""client_mode"": ""interactive"",
""enroll_via_multichallenge"": true,
""enroll_via_multichallenge_optional"": false,
""image"": null,
""message"": ""Please enter your new email address!"",
""multi_challenge"": [{
""client_mode"": ""interactive"",
""image"": null,
""message"": ""Please enter your new email address!"",
""serial"": ""PIEM0000733A"",
""transaction_id"": ""02615784748381378184"",
""type"": ""email""
}],
""serial"": ""PIEM0000733A"",
""transaction_id"": ""02615784748381378184"",
""type"": ""email""
},
""result"": {""authentication"": ""CHALLENGE"", ""status"": true, ""value"": false}
}";

/// Smartphone container enrollment — uses client_mode=poll and a pia:// link.
public const string SmartphoneEnrollChallenge = @"{
""detail"": {
""client_mode"": ""poll"",
""enroll_via_multichallenge"": true,
""enroll_via_multichallenge_optional"": false,
""image"": ""data:image/png;base64,STUB"",
""link"": ""pia://container/SMPH0000D847?issuer=privacyIDEA&ttl=10"",
""message"": ""Please scan the QR code to register the container."",
""multi_challenge"": [{
""client_mode"": ""poll"",
""image"": ""data:image/png;base64,STUB"",
""link"": ""pia://container/SMPH0000D847?issuer=privacyIDEA&ttl=10"",
""message"": ""Please scan the QR code to register the container."",
""serial"": ""SMPH0000D847"",
""transaction_id"": ""17359662976761378280"",
""type"": ""smartphone""
}],
""serial"": ""SMPH0000D847"",
""transaction_id"": ""17359662976761378280"",
""type"": ""smartphone""
},
""result"": {""authentication"": ""CHALLENGE"", ""status"": true, ""value"": false}
}";

/// Server ACK after a successful cancel_enrollment=True on an optional enrollment.
public const string CancelEnrollmentAccept = @"{
""detail"": {""message"": ""Cancelled enrollment via multichallenge""},
""result"": {""authentication"": ""ACCEPT"", ""status"": true, ""value"": true}
}";

/// Server response when cancel_enrollment=True is sent against a non-optional
/// enrollment — treated as a wrong OTP, REJECT.
public const string CancelEnrollmentReject = @"{
""detail"": {""message"": ""Failed to cancel enrollment via multichallenge""},
""result"": {""authentication"": ""REJECT"", ""status"": true, ""value"": false}
}";
}
}
98 changes: 98 additions & 0 deletions Tests/Fixtures/OtpFixtures.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
namespace Tests.Fixtures
{
/// <summary>
/// Response bodies for the plain OTP / passthru / error paths of /validate/check.
/// </summary>
internal static class OtpFixtures
{
/// Successful TOTP authentication, no challenge involved.
public const string SimpleAccept = @"{
""detail"": {
""message"": ""matching 1 tokens"",
""otplen"": 6,
""serial"": ""TOTP00001234"",
""type"": ""totp""
},
""result"": {""authentication"": ""ACCEPT"", ""status"": true, ""value"": true}
}";

/// Wrong OTP value — REJECT, no transaction id.
public const string SimpleReject = @"{
""detail"": {""message"": ""wrong otp value""},
""result"": {""authentication"": ""REJECT"", ""status"": true, ""value"": false}
}";

/// CHALLENGE response from validate/check: a token type that requires
/// challenge-response (HOTP), no enrollment image.
public const string OtpChallenge = @"{
""detail"": {
""client_mode"": ""interactive"",
""message"": ""Please enter the OTP"",
""multi_challenge"": [{
""client_mode"": ""interactive"",
""message"": ""Please enter the OTP"",
""serial"": ""OATH0000DEAD"",
""transaction_id"": ""11111111111111111111"",
""type"": ""hotp""
}],
""serial"": ""OATH0000DEAD"",
""transaction_id"": ""11111111111111111111"",
""type"": ""hotp""
},
""result"": {""authentication"": ""CHALLENGE"", ""status"": true, ""value"": false}
}";

/// Server-side error: status=false with a populated error.code / error.message.
/// Returned with HTTP 400 by the server, but the parser only sees the body.
public const string ServerError = @"{
""detail"": null,
""result"": {
""error"": {""code"": 904, ""message"": ""ERR904: User not found""},
""status"": false
}
}";

/// Final ACCEPT after answering an outstanding challenge.
public const string ChallengeCompletionAccept = @"{
""detail"": {""message"": ""Found matching challenge"", ""serial"": ""OATH0000DEAD""},
""result"": {""authentication"": ""ACCEPT"", ""status"": true, ""value"": true}
}";

/// preferred_client_mode=interactive should be translated to "otp" by the parser
/// (the field rewriting is part of PIResponse.FromJSON).
public const string PreferredClientModeInteractive = @"{
""detail"": {
""message"": ""Please enter the OTP"",
""preferred_client_mode"": ""interactive"",
""multi_challenge"": [{
""client_mode"": ""interactive"",
""message"": ""Please enter the OTP"",
""serial"": ""OATH0000DEAD"",
""transaction_id"": ""22222222222222222222"",
""type"": ""hotp""
}],
""transaction_id"": ""22222222222222222222"",
""type"": ""hotp""
},
""result"": {""authentication"": ""CHALLENGE"", ""status"": true, ""value"": false}
}";

/// preferred_client_mode=poll should be translated to "push".
public const string PreferredClientModePoll = @"{
""detail"": {
""message"": ""Please confirm on your device"",
""preferred_client_mode"": ""poll"",
""multi_challenge"": [{
""client_mode"": ""poll"",
""message"": ""Please confirm on your device"",
""serial"": ""PIPU000012AB"",
""transaction_id"": ""33333333333333333333"",
""type"": ""push""
}],
""transaction_id"": ""33333333333333333333"",
""type"": ""push""
},
""result"": {""authentication"": ""CHALLENGE"", ""status"": true, ""value"": false}
}";
}
}
72 changes: 72 additions & 0 deletions Tests/Fixtures/PasskeyFixtures.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
namespace Tests.Fixtures
{
/// <summary>
/// Response bodies for passkey authentication and passkey registration via
/// enroll_via_multichallenge.
/// </summary>
internal static class PasskeyFixtures
{
/// /validate/initialize response that starts a usernameless passkey login.
/// Carries a `passkey` object in detail with its own transaction_id.
public const string PasskeyInitChallenge = @"{
""detail"": {
""passkey"": {
""challenge"": ""challenge-bytes-go-here"",
""rpId"": ""sso.example.com"",
""transaction_id"": ""44444444444444444444"",
""user_verification"": ""preferred""
}
},
""result"": {""authentication"": ""CHALLENGE"", ""status"": true, ""value"": false}
}";

/// /validate/check ACCEPT after a passkey sign response. detail.username is the
/// user that was actually authenticated — the client uses this to guard against
/// a passkey from someone other than the user trying to log in.
public const string PasskeyAuthAccept = @"{
""detail"": {
""username"": ""alice"",
""message"": ""Authentication successful""
},
""result"": {""authentication"": ""ACCEPT"", ""status"": true, ""value"": true}
}";

/// Same ACCEPT but for a different user, for testing the "wrong user" guard.
public const string PasskeyAuthAcceptDifferentUser = @"{
""detail"": {
""username"": ""bob"",
""message"": ""Authentication successful""
},
""result"": {""authentication"": ""ACCEPT"", ""status"": true, ""value"": true}
}";

/// CHALLENGE that asks the user to register a passkey
/// (enroll_via_multichallenge=PASSKEY). The `passkey_registration` field
/// carries the full CredentialCreationOptions payload as a JSON string.
public const string PasskeyRegistrationChallenge = @"{
""detail"": {
""client_mode"": ""webauthn"",
""enroll_via_multichallenge"": true,
""enroll_via_multichallenge_optional"": false,
""message"": ""Please confirm the registration with your passkey!"",
""multi_challenge"": [{
""client_mode"": ""webauthn"",
""message"": ""Please confirm the registration with your passkey!"",
""passkey_registration"": {
""rp"": {""id"": ""sso.example.com"", ""name"": ""privacyIDEA""},
""user"": {""id"": ""dXNlcg"", ""name"": ""alice"", ""displayName"": ""Alice""},
""challenge"": ""regchallenge"",
""pubKeyCredParams"": [{""alg"": -7, ""type"": ""public-key""}]
},
""serial"": ""PIPK00001234"",
""transaction_id"": ""55555555555555555555"",
""type"": ""passkey""
}],
""serial"": ""PIPK00001234"",
""transaction_id"": ""55555555555555555555"",
""type"": ""passkey""
},
""result"": {""authentication"": ""CHALLENGE"", ""status"": true, ""value"": false}
}";
}
}
47 changes: 47 additions & 0 deletions Tests/Fixtures/PushFixtures.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
namespace Tests.Fixtures
{
/// <summary>
/// Response bodies for push authentication: the initial challenge, the poll states
/// (pending vs accepted), and the final ACCEPT after the smartphone confirms.
/// </summary>
internal static class PushFixtures
{
/// Push challenge — triggered by /validate/triggerchallenge or by /validate/check
/// against a push token. client_mode=poll signals the client to poll.
public const string PushChallenge = @"{
""detail"": {
""client_mode"": ""poll"",
""message"": ""Please confirm the authentication on your mobile device!"",
""multi_challenge"": [{
""client_mode"": ""poll"",
""message"": ""Please confirm the authentication on your mobile device!"",
""serial"": ""PIPU0001F75E"",
""transaction_id"": ""02659936574063359702"",
""type"": ""push""
}],
""serial"": ""PIPU0001F75E"",
""transaction_id"": ""02659936574063359702"",
""type"": ""push""
},
""result"": {""authentication"": ""CHALLENGE"", ""status"": true, ""value"": false}
}";

/// /validate/polltransaction response while the user has not yet confirmed.
public const string PollPending = @"{
""detail"": {""challenge_status"": ""pending""},
""result"": {""status"": true, ""value"": false}
}";

/// /validate/polltransaction response once the smartphone has confirmed.
public const string PollAccepted = @"{
""detail"": {""challenge_status"": ""accept""},
""result"": {""status"": true, ""value"": true}
}";

/// Final /validate/check ACCEPT after a confirmed push (empty pass + transaction id).
public const string PushFinalAccept = @"{
""detail"": {""message"": ""Found matching challenge"", ""serial"": ""PIPU0001F75E""},
""result"": {""authentication"": ""ACCEPT"", ""status"": true, ""value"": true}
}";
}
}
Loading