Skip to content

Commit 25a3013

Browse files
Hintonaseigler
andauthored
Support Multiple Origins (#237)
* Add support for using multiple origins * Improve error message by converting the lists to strings * Changed demo to use origins. * Change Origins to HashSet * Fix conformance test controller, update tests to use origins instead of origin Co-authored-by: Alex Seigler <[email protected]>
1 parent 1ea74bc commit 25a3013

12 files changed

+122
-72
lines changed

Demo/Startup.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using Microsoft.AspNetCore.Builder;
34
using Microsoft.AspNetCore.Hosting;
45
using Microsoft.AspNetCore.Http;
@@ -45,7 +46,7 @@ public void ConfigureServices(IServiceCollection services)
4546
{
4647
options.ServerDomain = Configuration["fido2:serverDomain"];
4748
options.ServerName = "FIDO2 Test";
48-
options.Origin = Configuration["fido2:origin"];
49+
options.Origins = new HashSet<string> { Configuration["fido2:origin"] };
4950
options.TimestampDriftTolerance = Configuration.GetValue<int>("fido2:timestampDriftTolerance");
5051
options.MDSCacheDirPath = Configuration["fido2:MDSCacheDirPath"];
5152
})

Demo/TestController.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ public class TestController : Controller
2222

2323
public TestController(IOptions<Fido2Configuration> fido2Configuration)
2424
{
25-
_origin = fido2Configuration.Value.Origin;
25+
_origin = fido2Configuration.Value.FullyQualifiedOrigins.FirstOrDefault();
2626

2727
_fido2 = new Fido2(new Fido2Configuration
2828
{
2929
ServerDomain = fido2Configuration.Value.ServerDomain,
3030
ServerName = fido2Configuration.Value.ServerName,
31-
Origin = _origin,
31+
Origins = fido2Configuration.Value.FullyQualifiedOrigins,
3232
},
3333
ConformanceTesting.MetadataServiceInstance(
3434
System.IO.Path.Combine(fido2Configuration.Value.MDSCacheDirPath, @"Conformance"), _origin)

Src/Fido2.Models/Fido2Configuration.cs

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
using System.Net.Http;
2-
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
35
namespace Fido2NetLib
46
{
57
public class Fido2Configuration
@@ -38,18 +40,44 @@ public class Fido2Configuration
3840
/// <summary>
3941
/// Server origin, including protocol host and port.
4042
/// </summary>
41-
public string Origin { get; set; }
42-
43-
/// <summary>
44-
/// MDSCacheDirPath
45-
/// </summary>
43+
[Obsolete("This property is obsolete. Use Origins instead.")]
44+
public string Origin { get; set; }
45+
46+
/// <summary>
47+
/// Server origins, including protocol host and port.
48+
/// </summary>
49+
public HashSet<string> Origins
50+
{
51+
get => _origins ?? new HashSet<string> { Origin };
52+
set
53+
{
54+
_origins = value;
55+
_fullyQualifiedOrigins = new HashSet<string>(value.Select(o => o.ToFullyQualifiedOrigin()), StringComparer.OrdinalIgnoreCase);
56+
}
57+
}
58+
59+
/// <summary>
60+
/// Fully Qualified Server origins, generated automatically from Origins.
61+
/// </summary>
62+
public HashSet<string> FullyQualifiedOrigins
63+
{
64+
get => _fullyQualifiedOrigins ?? new HashSet<string> { Origin?.ToFullyQualifiedOrigin() };
65+
private set => _fullyQualifiedOrigins = value;
66+
}
67+
68+
/// <summary>
69+
/// MDSCacheDirPath
70+
/// </summary>
4671
public string MDSCacheDirPath { get; set; }
4772

4873
/// <summary>
4974
/// Create the configuration for Fido2
5075
/// </summary>
5176
public Fido2Configuration()
5277
{
53-
}
78+
}
79+
80+
private HashSet<string> _origins;
81+
private HashSet<string> _fullyQualifiedOrigins;
5482
}
5583
}

Src/Fido2.Models/StringExtensions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System;
2+
3+
namespace Fido2NetLib
4+
{
5+
public static class StringExtensions
6+
{
7+
public static string ToFullyQualifiedOrigin(this string origin)
8+
{
9+
var uri = new Uri(origin);
10+
11+
if (UriHostNameType.Unknown != uri.HostNameType)
12+
return uri.IsDefaultPort ? $"{uri.Scheme}://{uri.Host}" : $"{uri.Scheme}://{uri.Host}:{uri.Port}";
13+
14+
return origin;
15+
}
16+
}
17+
}

Src/Fido2/AuthenticatorAssertionResponse.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
using System;
2+
using System.Collections.Generic;
3+
14
#nullable disable
25

3-
using System;
46
using System.Linq;
57
using System.Security.Cryptography;
68
using System.Text;
@@ -44,22 +46,22 @@ public static AuthenticatorAssertionResponse Parse(AuthenticatorAssertionRawResp
4446
/// Implements alghoritm from https://www.w3.org/TR/webauthn/#verifying-assertion
4547
/// </summary>
4648
/// <param name="options">The assertionoptions that was sent to the client</param>
47-
/// <param name="expectedOrigin">
48-
/// The expected server origin, used to verify that the signature is sent to the expected server
49+
/// <param name="fullyQualifiedExpectedOrigins">
50+
/// The expected fully qualified server origins, used to verify that the signature is sent to the expected server
4951
/// </param>
5052
/// <param name="storedPublicKey">The stored public key for this CredentialId</param>
5153
/// <param name="storedSignatureCounter">The stored counter value for this CredentialId</param>
5254
/// <param name="isUserHandleOwnerOfCredId">A function that returns <see langword="true"/> if user handle is owned by the credential ID</param>
5355
/// <param name="requestTokenBindingId"></param>
5456
public async Task<AssertionVerificationResult> VerifyAsync(
5557
AssertionOptions options,
56-
string expectedOrigin,
58+
HashSet<string> fullyQualifiedExpectedOrigins,
5759
byte[] storedPublicKey,
5860
uint storedSignatureCounter,
5961
IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredId,
6062
byte[] requestTokenBindingId)
6163
{
62-
BaseVerify(expectedOrigin, options.Challenge, requestTokenBindingId);
64+
BaseVerify(fullyQualifiedExpectedOrigins, options.Challenge, requestTokenBindingId);
6365

6466
if (Raw.Type != PublicKeyCredentialType.PublicKey)
6567
throw new Fido2VerificationException("AssertionResponse Type is not set to public-key");

Src/Fido2/AuthenticatorAttestationResponse.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public async Task<AttestationVerificationSuccess> VerifyAsync(CredentialCreateOp
8686
// 5. Verify that the value of C.origin matches the Relying Party's origin.
8787
// 6. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the assertion was obtained.
8888
// If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection.
89-
BaseVerify(config.Origin, originalOptions.Challenge, requestTokenBindingId);
89+
BaseVerify(config.FullyQualifiedOrigins, originalOptions.Challenge, requestTokenBindingId);
9090

9191
if (Raw.Id is null || Raw.Id.Length == 0)
9292
throw new Fido2VerificationException("AttestationResponse is missing Id");

Src/Fido2/AuthenticatorResponse.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using System.Text.Json;
45
using System.Text.Json.Serialization;
@@ -44,6 +45,8 @@ protected AuthenticatorResponse(ReadOnlySpan<byte> utf8EncodedJson)
4445
}
4546
#nullable enable
4647

48+
public const int MAX_ORIGINS_TO_PRINT = 5;
49+
4750
[JsonPropertyName("type")]
4851
public string Type { get; set; }
4952

@@ -56,7 +59,7 @@ protected AuthenticatorResponse(ReadOnlySpan<byte> utf8EncodedJson)
5659

5760
// todo: add TokenBinding https://www.w3.org/TR/webauthn/#dictdef-tokenbinding
5861

59-
protected void BaseVerify(string expectedOrigin, ReadOnlySpan<byte> originalChallenge, ReadOnlySpan<byte> requestTokenBindingId)
62+
protected void BaseVerify(HashSet<string> fullyQualifiedExpectedOrigins, ReadOnlySpan<byte> originalChallenge, ReadOnlySpan<byte> requestTokenBindingId)
6063
{
6164
if (Type is not "webauthn.create" && Type is not "webauthn.get")
6265
throw new Fido2VerificationException($"Type not equal to 'webauthn.create' or 'webauthn.get'. Was: '{Type}'");
@@ -68,12 +71,11 @@ protected void BaseVerify(string expectedOrigin, ReadOnlySpan<byte> originalChal
6871
if (!Challenge.AsSpan().SequenceEqual(originalChallenge))
6972
throw new Fido2VerificationException("Challenge not equal to original challenge");
7073

71-
var fullyQualifiedOrigin = FullyQualifiedOrigin(Origin);
72-
var fullyQualifiedExpectedOrigin = FullyQualifiedOrigin(expectedOrigin);
74+
var fullyQualifiedOrigin = Origin.ToFullyQualifiedOrigin();
7375

7476
// 5. Verify that the value of C.origin matches the Relying Party's origin.
75-
if (!string.Equals(fullyQualifiedOrigin, fullyQualifiedExpectedOrigin, StringComparison.OrdinalIgnoreCase))
76-
throw new Fido2VerificationException($"Fully qualified origin {fullyQualifiedOrigin} of {Origin} not equal to fully qualified original origin {fullyQualifiedExpectedOrigin} of {expectedOrigin}");
77+
if (!fullyQualifiedExpectedOrigins.Contains(fullyQualifiedOrigin))
78+
throw new Fido2VerificationException($"Fully qualified origin {fullyQualifiedOrigin} of {Origin} not equal to fully qualified original origin {string.Join(", ", fullyQualifiedExpectedOrigins.Take(MAX_ORIGINS_TO_PRINT))} ({fullyQualifiedExpectedOrigins.Count})");
7779

7880
}
7981

Src/Fido2/Fido2NetLib.cs

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using System.Security.Cryptography;
33
using System.Threading.Tasks;
44
using Fido2NetLib.Objects;
5-
5+
66
namespace Fido2NetLib
77
{
88
/// <summary>
@@ -14,21 +14,21 @@ public partial class Fido2 : IFido2
1414
private readonly IMetadataService? _metadataService;
1515

1616
public Fido2(
17-
Fido2Configuration config,
17+
Fido2Configuration config,
1818
IMetadataService? metadataService = null)
1919
{
2020
_config = config;
2121
_metadataService = metadataService;
22-
}
22+
}
2323

2424
/// <summary>
2525
/// Returns CredentialCreateOptions including a challenge to be sent to the browser/authr to create new credentials
2626
/// </summary>
2727
/// <returns></returns>
2828
/// <param name="excludeCredentials">Recommended. This member is intended for use by Relying Parties that wish to limit the creation of multiple credentials for the same account on a single authenticator.The client is requested to return an error if the new credential would be created on an authenticator that also contains one of the credentials enumerated in this parameter.</param>
29-
public CredentialCreateOptions RequestNewCredential(
30-
Fido2User user,
31-
List<PublicKeyCredentialDescriptor> excludeCredentials,
29+
public CredentialCreateOptions RequestNewCredential(
30+
Fido2User user,
31+
List<PublicKeyCredentialDescriptor> excludeCredentials,
3232
AuthenticationExtensionsClientInputs? extensions = null)
3333
{
3434
return RequestNewCredential(user, excludeCredentials, AuthenticatorSelection.Default, AttestationConveyancePreference.None, extensions);
@@ -40,11 +40,11 @@ public CredentialCreateOptions RequestNewCredential(
4040
/// <returns></returns>
4141
/// <param name="attestationPreference">This member is intended for use by Relying Parties that wish to express their preference for attestation conveyance. The default is none.</param>
4242
/// <param name="excludeCredentials">Recommended. This member is intended for use by Relying Parties that wish to limit the creation of multiple credentials for the same account on a single authenticator.The client is requested to return an error if the new credential would be created on an authenticator that also contains one of the credentials enumerated in this parameter.</param>
43-
public CredentialCreateOptions RequestNewCredential(
44-
Fido2User user,
45-
List<PublicKeyCredentialDescriptor> excludeCredentials,
46-
AuthenticatorSelection authenticatorSelection,
47-
AttestationConveyancePreference attestationPreference,
43+
public CredentialCreateOptions RequestNewCredential(
44+
Fido2User user,
45+
List<PublicKeyCredentialDescriptor> excludeCredentials,
46+
AuthenticatorSelection authenticatorSelection,
47+
AttestationConveyancePreference attestationPreference,
4848
AuthenticationExtensionsClientInputs? extensions = null)
4949
{
5050
var challenge = new byte[_config.ChallengeSize];
@@ -60,10 +60,10 @@ public CredentialCreateOptions RequestNewCredential(
6060
/// <param name="attestationResponse"></param>
6161
/// <param name="origChallenge"></param>
6262
/// <returns></returns>
63-
public async Task<CredentialMakeResult> MakeNewCredentialAsync(
64-
AuthenticatorAttestationRawResponse attestationResponse,
65-
CredentialCreateOptions origChallenge,
66-
IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser,
63+
public async Task<CredentialMakeResult> MakeNewCredentialAsync(
64+
AuthenticatorAttestationRawResponse attestationResponse,
65+
CredentialCreateOptions origChallenge,
66+
IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser,
6767
byte[]? requestTokenBindingId = null)
6868
{
6969
var parsedResponse = AuthenticatorAttestationResponse.Parse(attestationResponse);
@@ -81,9 +81,9 @@ public async Task<CredentialMakeResult> MakeNewCredentialAsync(
8181
/// Returns AssertionOptions including a challenge to the browser/authr to assert existing credentials and authenticate a user.
8282
/// </summary>
8383
/// <returns></returns>
84-
public AssertionOptions GetAssertionOptions(
85-
IEnumerable<PublicKeyCredentialDescriptor> allowedCredentials,
86-
UserVerificationRequirement? userVerification,
84+
public AssertionOptions GetAssertionOptions(
85+
IEnumerable<PublicKeyCredentialDescriptor> allowedCredentials,
86+
UserVerificationRequirement? userVerification,
8787
AuthenticationExtensionsClientInputs? extensions = null)
8888
{
8989
var challenge = new byte[_config.ChallengeSize];
@@ -97,21 +97,21 @@ public AssertionOptions GetAssertionOptions(
9797
/// Verifies the assertion response from the browser/authr to assert existing credentials and authenticate a user.
9898
/// </summary>
9999
/// <returns></returns>
100-
public async Task<AssertionVerificationResult> MakeAssertionAsync(
101-
AuthenticatorAssertionRawResponse assertionResponse,
102-
AssertionOptions originalOptions,
103-
byte[] storedPublicKey,
104-
uint storedSignatureCounter,
105-
IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredentialIdCallback,
100+
public async Task<AssertionVerificationResult> MakeAssertionAsync(
101+
AuthenticatorAssertionRawResponse assertionResponse,
102+
AssertionOptions originalOptions,
103+
byte[] storedPublicKey,
104+
uint storedSignatureCounter,
105+
IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredentialIdCallback,
106106
byte[]? requestTokenBindingId = null)
107107
{
108108
var parsedResponse = AuthenticatorAssertionResponse.Parse(assertionResponse);
109109

110-
var result = await parsedResponse.VerifyAsync(originalOptions,
111-
_config.Origin,
112-
storedPublicKey,
113-
storedSignatureCounter,
114-
isUserHandleOwnerOfCredentialIdCallback,
110+
var result = await parsedResponse.VerifyAsync(originalOptions,
111+
_config.FullyQualifiedOrigins,
112+
storedPublicKey,
113+
storedSignatureCounter,
114+
isUserHandleOwnerOfCredentialIdCallback,
115115
requestTokenBindingId);
116116

117117
return result;
@@ -122,11 +122,11 @@ public async Task<AssertionVerificationResult> MakeAssertionAsync(
122122
/// </summary>
123123
public sealed class CredentialMakeResult : Fido2ResponseBase
124124
{
125-
public CredentialMakeResult(string status, string errorMessage, AttestationVerificationSuccess result)
126-
{
127-
Status = status;
128-
ErrorMessage = errorMessage;
129-
Result = result;
125+
public CredentialMakeResult(string status, string errorMessage, AttestationVerificationSuccess result)
126+
{
127+
Status = status;
128+
ErrorMessage = errorMessage;
129+
Result = result;
130130
}
131131

132132
public AttestationVerificationSuccess Result { get; }

Test/Attestation/Apple.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ public void TestApplePublicKeyMismatch()
213213
{
214214
ServerDomain = "6cc3c9e7967a.ngrok.io",
215215
ServerName = "6cc3c9e7967a.ngrok.io",
216-
Origin = "6cc3c9e7967a.ngrok.io",
216+
Origins = new HashSet<string> { "https://6cc3c9e7967a.ngrok.io" },
217217
});
218218

219219
var credentialMakeResult = lib.MakeNewCredentialAsync(attestationResponse, origChallenge, callback);

0 commit comments

Comments
 (0)