diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 96be437d..dbe78984 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "7.0.11",
+ "version": "7.0.13",
"commands": [
"dotnet-ef"
]
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 95071074..be31cc2f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,12 +4,13 @@
+
-
+
-
+
@@ -21,17 +22,17 @@
-
-
-
+
+
+
-
+
-
+
@@ -43,6 +44,6 @@
-
+
\ No newline at end of file
diff --git a/Udap.Client/Client/Extensions/HttpClientTokenRequestExtensions.cs b/Udap.Client/Client/Extensions/HttpClientTokenRequestExtensions.cs
index a27eb7ec..f0981d0b 100644
--- a/Udap.Client/Client/Extensions/HttpClientTokenRequestExtensions.cs
+++ b/Udap.Client/Client/Extensions/HttpClientTokenRequestExtensions.cs
@@ -110,12 +110,12 @@ internal static async Task RequestTokenAsync(this HttpMessageInvo
///
/// The client.
/// The request.
- /// The cancellation token.
+ /// The cancellation token.
///
public static async Task ExchangeCodeForAuthTokenResponse(
this HttpMessageInvoker client,
AuthorizationCodeTokenRequest request,
- CancellationToken tokenRequest = default)
+ CancellationToken cancellationToken = default)
{
var clone = request.Clone();
@@ -136,9 +136,9 @@ public static async Task ExchangeCodeForAuthTokenResponse(
clone.Prepare();
clone.Method = HttpMethod.Post;
- var response = await client.SendAsync(clone, tokenRequest);
+ var response = await client.SendAsync(clone, cancellationToken);
- var body = await response.Content.ReadAsStringAsync(tokenRequest);
+ var body = await response.Content.ReadAsStringAsync(cancellationToken);
return response.IsSuccessStatusCode switch
{
diff --git a/Udap.Client/Client/IUdapClient.cs b/Udap.Client/Client/IUdapClient.cs
index 0799f825..3fb1dde0 100644
--- a/Udap.Client/Client/IUdapClient.cs
+++ b/Udap.Client/Client/IUdapClient.cs
@@ -25,21 +25,24 @@ public interface IUdapClient : IUdapClientEvents
Task ValidateResource(
string baseUrl,
string? community = null,
- DiscoveryPolicy? discoveryPolicy = null);
+ DiscoveryPolicy? discoveryPolicy = null,
+ CancellationToken token = default);
Task ValidateResource(
string baseUrl,
ITrustAnchorStore? trustAnchorStore,
string? community = null,
- DiscoveryPolicy? discoveryPolicy = null);
+ DiscoveryPolicy? discoveryPolicy = null,
+ CancellationToken token = default);
UdapMetadata? UdapDynamicClientRegistrationDocument { get; set; }
UdapMetadata? UdapServerMetaData { get; set; }
-
+
///
/// Register a TieredClient in the Authorization Server.
- /// Currently it is not SAN and Community aware. It picks the first SAN.
+ /// Currently it is not SAN aware. It picks the first SAN.
+ /// To pick a different community the client can add a community query parameter to the .
///
///
///
@@ -51,7 +54,79 @@ Task RegisterTieredClient(string redirect
string scopes,
CancellationToken token = default);
+ ///
+ /// Register a UdapClient in the Authorization Server with authorization_code flow.
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// If issuer is supplied it will match try to match to a valid URI based subject alternative name from the X509Certificate
+ ///
+ ///
+ Task RegisterAuthCodeClient(
+ IEnumerable certificates,
+ string scopes,
+ string logo,
+ ICollection redirectUrl,
+ string? issuer = null,
+ CancellationToken token = default);
+
+ ///
+ /// Register a UdapClient in the Authorization Server with authorization_code flow.
+ ///
+ ///
+ ///
+ /// optional
+ ///
+ /// If issuer is supplied it will match try to match to a valid URI based subject alternative name from the X509Certificate
+ ///
+ ///
+ Task RegisterAuthCodeClient(
+ X509Certificate2 certificate,
+ string scopes,
+ string logo,
+ ICollection redirectUrl,
+ string? issuer = null,
+ CancellationToken token = default);
+
+ ///
+ /// Register a UdapClient in the Authorization Server with client_credentials flow.
+ ///
+ ///
+ ///
+ ///
+ /// If issuer is supplied it will match try to match to a valid URI based subject alternative name from the X509Certificate
+ ///
+ ///
+ Task RegisterClientCredentialsClient(
+ IEnumerable certificates,
+ string scopes,
+ string? issuer = null,
+ string? logo = null,
+ CancellationToken token = default);
+
+ ///
+ /// Register a UdapClient in the Authorization Server with client_credentials flow.
+ ///
+ ///
+ ///
+ /// optional
+ /// If issuer is supplied it will match try to match to a valid URI based subject alternative name from the X509Certificate
+ ///
+ ///
+ Task RegisterClientCredentialsClient(
+ X509Certificate2 certificate,
+ string scopes,
+ string? issuer = null,
+ string? logo = null,
+ CancellationToken token = default);
+
+ Task ExchangeCodeForTokenResponse(UdapAuthorizationCodeTokenRequest tokenRequest, CancellationToken token = default);
+
Task ExchangeCodeForAuthTokenResponse(UdapAuthorizationCodeTokenRequest tokenRequest, CancellationToken token = default);
Task?> ResolveJwtKeys(DiscoveryDocumentRequest? request = null, CancellationToken cancellationToken = default);
+
+ Task ResolveOpenIdConfig(DiscoveryDocumentRequest? request = null, CancellationToken cancellationToken = default);
}
\ No newline at end of file
diff --git a/Udap.Client/Client/UdapClient.cs b/Udap.Client/Client/UdapClient.cs
index 64bdc655..fbe01eb4 100644
--- a/Udap.Client/Client/UdapClient.cs
+++ b/Udap.Client/Client/UdapClient.cs
@@ -8,18 +8,12 @@
#endregion
using System.Net;
-using System.Net.Http;
-using System.Text.Json.Serialization;
-using System.Text.Json;
-using System.Threading.Tasks;
-
-using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using IdentityModel.Client;
-// using Microsoft.AspNetCore.Authentication.OAuth;
+using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
@@ -32,7 +26,10 @@
using Udap.Model.Access;
using Udap.Model.Registration;
using Udap.Model.Statement;
-using Microsoft.AspNetCore.Authentication.OAuth;
+#if NET7_0_OR_GREATER
+using System.Net.Http.Headers;
+#endif
+// using Microsoft.AspNetCore.Authentication.OAuth;
namespace Udap.Client.Client
{
@@ -95,87 +92,127 @@ public async Task RegisterTieredClient(st
{
if (this.UdapServerMetaData == null)
{
- throw new Exception("Tiered OAuth: UdapServerMetaData is null. Call ValidateResource first.");
+ throw new Exception("Tiered OAuth Client: UdapServerMetaData is null. Call ValidateResource first.");
}
try
{
- var x509Certificates = certificates.ToList();
- if (certificates == null || !x509Certificates.Any())
+ var resultDocument = await RegisterAuthCodeFlow(certificates, scopes, _udapClientOptions.TieredOAuthClientLogo, new List{ redirectUrl }, null, token);
+
+ if(string.IsNullOrEmpty(resultDocument.GetError()))
{
- throw new Exception("Tiered OAuth: No client certificates provided.");
+ _logger.LogWarning("Tiered OAuth Client: Unable to register client to {RegistrationEndpoint}", this.UdapServerMetaData?.RegistrationEndpoint);
}
- foreach (var clientCert in x509Certificates)
+ return resultDocument;
+ }
+ catch(Exception ex)
+ {
+ _logger.LogError(ex, "Tiered OAuth Client: Unable to register client to {RegistrationEndpoint}",
+ this.UdapServerMetaData?.RegistrationEndpoint);
+ throw;
+ }
+ }
+
+ ///
+ public async Task RegisterAuthCodeClient(
+ IEnumerable certificates,
+ string scopes,
+ string logo,
+ ICollection redirectUrl,
+ string? issuer,
+ CancellationToken token = default)
+ {
+ if (this.UdapServerMetaData == null)
+ {
+ throw new Exception("UdapClient: UdapServerMetaData is null. Call ValidateResource first.");
+ }
+
+ try
+ {
+ var resultDocument = await RegisterAuthCodeFlow(certificates, scopes, logo, redirectUrl, issuer, token);
+
+ if (string.IsNullOrEmpty(resultDocument.GetError()))
{
- _logger.LogDebug($"Using certificate {clientCert.SubjectName.Name} [ {clientCert.Thumbprint} ]");
-
- var document = UdapDcrBuilderForAuthorizationCode
- .Create(clientCert)
- .WithAudience(this.UdapServerMetaData?.RegistrationEndpoint)
- .WithExpiration(TimeSpan.FromMinutes(5))
- .WithJwtId()
- .WithClientName(_udapClientOptions.ClientName)
- //Todo get logo from client registration, maybe Client object. But still nee to retain logo in clientproperties during registration
- .WithLogoUri("https://udaped.fhirlabs.net/images/udap-dotnet-auth-server.png")
- .WithContacts(_udapClientOptions.Contacts)
- .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues
- .TokenEndpointAuthMethodValue)
- .WithScope(scopes)
- .WithResponseTypes(new List { "code" })
- .WithRedirectUrls(new List { redirectUrl })
- .Build();
-
-
- var signedSoftwareStatement =
- SignedSoftwareStatementBuilder
- .Create(clientCert, document)
- .Build();
-
- var requestBody = new UdapRegisterRequest
- (
- signedSoftwareStatement,
- UdapConstants.UdapVersionsSupportedValue
- // new string[] { }
- );
-
- // New StringContent constructor taking a MediaTypeHeaderValue to ensure CharSet can be controlled
- // by the caller.
- // Good historical conversations.
- // https://github.com/dotnet/runtime/pull/63231
- // https://github.com/dotnet/runtime/issues/17036
- //
-#if NET7_0_OR_GREATER
- var content = new StringContent(
- JsonSerializer.Serialize(requestBody),
- new MediaTypeHeaderValue("application/json") );
-#else
- var content = new StringContent(JsonSerializer.Serialize(requestBody), null, "application/json");
- content.Headers.ContentType!.CharSet = string.Empty;
- #endif
+ _logger.LogWarning("UdapClient: Unable to register authorization_code client to {RegistrationEndpoint}", this.UdapServerMetaData?.RegistrationEndpoint);
+ }
+
+ return resultDocument;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "UdapClient: Unable to register authorization_code client to {RegistrationEndpoint}",
+ this.UdapServerMetaData?.RegistrationEndpoint);
+ throw;
+ }
+ }
- var response = await _httpClient.PostAsync(this.UdapServerMetaData?.RegistrationEndpoint, content, token);
+ ///
+ public async Task RegisterAuthCodeClient(
+ X509Certificate2 certificate,
+ string scopes,
+ string logo,
+ ICollection redirectUrl,
+ string? issuer,
+ CancellationToken token = default)
+ {
+ return await this.RegisterAuthCodeClient(
+ new List { certificate },
+ scopes,
+ logo,
+ redirectUrl,
+ issuer,
+ token
+ );
+ }
- if (((int)response.StatusCode) < 500)
- {
- var resultDocument =
- await response.Content.ReadFromJsonAsync(cancellationToken: token);
+ ///
+ public async Task RegisterClientCredentialsClient(
+ IEnumerable certificates,
+ string scopes,
+ string? issuer,
+ string? logo,
+ CancellationToken token = default)
+ {
+ if (this.UdapServerMetaData == null)
+ {
+ throw new Exception("UdapClient: UdapServerMetaData is null. Call ValidateResource first.");
+ }
- return resultDocument;
- }
+ try
+ {
+ var resultDocument = await RegisterClientCredFlow(certificates, scopes, logo, issuer, token);
+
+ if (string.IsNullOrEmpty(resultDocument.GetError()))
+ {
+ _logger.LogWarning("UdapClient: Unable to register client_credentials client to {RegistrationEndpoint}", this.UdapServerMetaData?.RegistrationEndpoint);
}
+
+ return resultDocument;
}
- catch(Exception ex)
+ catch (Exception ex)
{
- _logger.LogError(ex, "Tiered OAuth: Unable to register client to {RegistrationEndpoint}",
- this.UdapServerMetaData?.RegistrationEndpoint);
+ _logger.LogError(ex, "UdapClient: Unable to register client_credentials client to {RegistrationEndpoint}",
+ this.UdapServerMetaData?.RegistrationEndpoint);
throw;
}
+ }
-
- _logger.LogWarning("Tiered OAuth: Unable to register client to {RegistrationEndpoint}", this.UdapServerMetaData?.RegistrationEndpoint);
- // Todo: typed exception? or null return etc...
- throw new Exception($"Tiered OAuth: Unable to register client to {this.UdapServerMetaData?.RegistrationEndpoint}");
+ ////
+ public async Task RegisterClientCredentialsClient(
+ X509Certificate2 certificate,
+ string scopes,
+ string? issuer,
+ string? logo,
+ CancellationToken token = default)
+ {
+ return await this.RegisterClientCredentialsClient(
+ new List { certificate },
+ scopes,
+ logo,
+ issuer,
+ token
+ );
}
///
@@ -183,7 +220,7 @@ public async Task RegisterTieredClient(st
///
/// The request.
/// The cancellation token.
- ///
+ ///
public async Task ExchangeCodeForTokenResponse(
UdapAuthorizationCodeTokenRequest tokenRequest,
CancellationToken token = default)
@@ -218,14 +255,16 @@ public async Task ExchangeCodeForAuthTokenResponse(
///
///
///
+ ///
///
public Task ValidateResource(
string baseUrl,
ITrustAnchorStore? trustAnchorStore,
string? community = null,
- DiscoveryPolicy? discoveryPolicy = null)
+ DiscoveryPolicy? discoveryPolicy = null,
+ CancellationToken token = default)
{
- return InternalValidateResource(baseUrl, trustAnchorStore, community, discoveryPolicy);
+ return InternalValidateResource(baseUrl, trustAnchorStore, community, discoveryPolicy, token);
}
///
@@ -234,22 +273,25 @@ public Task ValidateResource(
///
///
///
+ ///
///
///
public async Task ValidateResource(
string baseUrl,
string? community,
- DiscoveryPolicy? discoveryPolicy)
+ DiscoveryPolicy? discoveryPolicy,
+ CancellationToken token = default)
{
- return await InternalValidateResource(baseUrl, null, community, discoveryPolicy);
+ return await InternalValidateResource(baseUrl, null, community, discoveryPolicy, token);
}
private async Task InternalValidateResource(
string baseUrl,
ITrustAnchorStore? trustAnchorStore,
string? community,
- DiscoveryPolicy? discoveryPolicy)
- {
+ DiscoveryPolicy? discoveryPolicy,
+ CancellationToken token = default)
+ {
baseUrl.AssertUri();
@@ -267,7 +309,7 @@ private async Task InternalValidateResource(
Address = baseUrl,
Community = community,
Policy = _discoveryPolicy
- });
+ }, cancellationToken: token);
if (disco.HttpStatusCode == HttpStatusCode.OK && !disco.IsError)
{
@@ -296,7 +338,7 @@ private async Task InternalValidateResource(
_logger.LogError(ex, "Failed validating resource metadata");
return ProtocolResponse.FromException(ex);
}
- }
+ }
public async Task?> ResolveJwtKeys(DiscoveryDocumentRequest? request = null, CancellationToken cancellationToken = default)
@@ -329,6 +371,21 @@ private async Task InternalValidateResource(
return keys;
}
+ public async Task ResolveOpenIdConfig(DiscoveryDocumentRequest? request = null, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(request);
+
+ //TODO: Cache Discovery Document?
+ var disco = await _httpClient.GetDiscoveryDocumentAsync(request, cancellationToken: cancellationToken);
+
+ if (disco.HttpStatusCode != HttpStatusCode.OK || disco.IsError)
+ {
+ throw new Exception("Failed to retrieve discovery document: " + disco.Error);
+ }
+
+ return disco;
+ }
+
private void NotifyTokenError(string message)
{
@@ -346,6 +403,209 @@ private void NotifyTokenError(string message)
}
}
}
+
+ private async Task RegisterAuthCodeFlow(
+ IEnumerable certificates,
+ string scopes,
+ string logoUrl,
+ ICollection? redirectUrls,
+ string? issuer,
+ CancellationToken token)
+ {
+ var x509Certificates = certificates.ToList();
+ if (certificates == null || !x509Certificates.Any())
+ {
+ throw new Exception("Tiered OAuth: No client certificates provided.");
+ }
+
+ if (string.IsNullOrEmpty(_udapClientOptions.ClientName))
+ {
+ throw new ArgumentException("UdapClientOptions.ClientName is empty");
+ }
+
+ UdapDynamicClientRegistrationDocument? resultDocument;
+
+ foreach (var clientCert in x509Certificates)
+ {
+ _logger.LogDebug($"Using certificate {clientCert.SubjectName.Name} [ {clientCert.Thumbprint} ]");
+
+ var builder = UdapDcrBuilderForAuthorizationCode
+ .Create(clientCert)
+ .WithAudience(this.UdapServerMetaData?.RegistrationEndpoint)
+ .WithExpiration(TimeSpan.FromMinutes(5))
+ .WithJwtId()
+ .WithClientName(_udapClientOptions.ClientName)
+ .WithLogoUri(logoUrl)
+ .WithContacts(_udapClientOptions.Contacts)
+ .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue)
+ .WithScope(scopes)
+ .WithResponseTypes(new List { "code" })
+ .WithRedirectUrls(redirectUrls);
+
+ if (!string.IsNullOrEmpty(issuer))
+ {
+ builder.WithIssuer(new Uri(issuer));
+ }
+
+ var document = builder.Build();
+
+ var signedSoftwareStatement =
+ SignedSoftwareStatementBuilder
+ .Create(clientCert, document)
+ .Build();
+
+ var requestBody = new UdapRegisterRequest
+ (
+ signedSoftwareStatement,
+ UdapConstants.UdapVersionsSupportedValue
+ // new string[] { }
+ );
+
+ // New StringContent constructor taking a MediaTypeHeaderValue to ensure CharSet can be controlled
+ // by the caller.
+ // Good historical conversations.
+ // https://github.com/dotnet/runtime/pull/63231
+ // https://github.com/dotnet/runtime/issues/17036
+ //
+#if NET7_0_OR_GREATER
+ var content = new StringContent(
+ JsonSerializer.Serialize(requestBody),
+ new MediaTypeHeaderValue("application/json"));
+#else
+ var content = new StringContent(JsonSerializer.Serialize(requestBody), null, "application/json");
+ content.Headers.ContentType!.CharSet = string.Empty;
+#endif
+
+ var response = await _httpClient.PostAsync(this.UdapServerMetaData?.RegistrationEndpoint, content, token);
+
+ if (((int)response.StatusCode) < 500)
+ {
+ resultDocument =
+ await response.Content.ReadFromJsonAsync(
+ cancellationToken: token);
+
+ if (resultDocument == null)
+ {
+ resultDocument = new UdapDynamicClientRegistrationDocument
+ {
+ { "error", "Unknown error" },
+ { "error_description", response.StatusCode }
+ };
+ }
+
+ return resultDocument;
+ }
+ }
+
+ resultDocument = new UdapDynamicClientRegistrationDocument
+ {
+ { "error", "Unknown error" },
+ { "error_description", "Failed to register with all client certificates" }
+ };
+
+ return resultDocument;
+ }
+
+
+ private async Task RegisterClientCredFlow(
+ IEnumerable certificates,
+ string scopes,
+ string? issuer,
+ string? logoUrl,
+ CancellationToken token)
+ {
+ var x509Certificates = certificates.ToList();
+ if (certificates == null || !x509Certificates.Any())
+ {
+ throw new Exception("Tiered OAuth: No client certificates provided.");
+ }
+
+ if (string.IsNullOrEmpty(_udapClientOptions.ClientName))
+ {
+ throw new ArgumentException("UdapClientOptions.ClientName is empty");
+ }
+
+ UdapDynamicClientRegistrationDocument? resultDocument;
+
+ foreach (var clientCert in x509Certificates)
+ {
+ _logger.LogDebug($"Using certificate {clientCert.SubjectName.Name} [ {clientCert.Thumbprint} ]");
+
+ var builder = UdapDcrBuilderForClientCredentials
+ .Create(clientCert)
+ .WithAudience(this.UdapServerMetaData?.RegistrationEndpoint)
+ .WithExpiration(TimeSpan.FromMinutes(5))
+ .WithJwtId()
+ .WithClientName(_udapClientOptions.ClientName)
+ .WithLogoUri(logoUrl)
+ .WithContacts(_udapClientOptions.Contacts)
+ .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue)
+ .WithScope(scopes);
+
+ if (!string.IsNullOrEmpty(issuer))
+ {
+ builder.WithIssuer(new Uri(issuer));
+ }
+
+ var document = builder.Build();
+
+ var signedSoftwareStatement =
+ SignedSoftwareStatementBuilder
+ .Create(clientCert, document)
+ .Build();
+
+ var requestBody = new UdapRegisterRequest
+ (
+ signedSoftwareStatement,
+ UdapConstants.UdapVersionsSupportedValue
+ // new string[] { }
+ );
+
+ // New StringContent constructor taking a MediaTypeHeaderValue to ensure CharSet can be controlled
+ // by the caller.
+ // Good historical conversations.
+ // https://github.com/dotnet/runtime/pull/63231
+ // https://github.com/dotnet/runtime/issues/17036
+ //
+#if NET7_0_OR_GREATER
+ var content = new StringContent(
+ JsonSerializer.Serialize(requestBody),
+ new MediaTypeHeaderValue("application/json"));
+#else
+ var content = new StringContent(JsonSerializer.Serialize(requestBody), null, "application/json");
+ content.Headers.ContentType!.CharSet = string.Empty;
+#endif
+
+ var response = await _httpClient.PostAsync(this.UdapServerMetaData?.RegistrationEndpoint, content, token);
+
+ if (((int)response.StatusCode) < 500)
+ {
+ resultDocument =
+ await response.Content.ReadFromJsonAsync(
+ cancellationToken: token);
+
+ if (resultDocument == null)
+ {
+ resultDocument = new UdapDynamicClientRegistrationDocument
+ {
+ { "error", "Unknown error" },
+ { "error_description", response.StatusCode }
+ };
+ }
+
+ return resultDocument;
+ }
+ }
+
+ resultDocument = new UdapDynamicClientRegistrationDocument
+ {
+ { "error", "Unknown error" },
+ { "error_description", "Failed to register with all client certificates" }
+ };
+
+ return resultDocument;
+ }
+
}
-
+
}
diff --git a/Udap.Client/Configuration/UdapClientOptions.cs b/Udap.Client/Configuration/UdapClientOptions.cs
index 7b699a16..39aa5184 100644
--- a/Udap.Client/Configuration/UdapClientOptions.cs
+++ b/Udap.Client/Configuration/UdapClientOptions.cs
@@ -20,4 +20,7 @@ public class UdapClientOptions
[JsonPropertyName("Headers")]
public Dictionary? Headers { get; set; }
+
+ [JsonPropertyName("TieredOAuthClientLogo")]
+ public string TieredOAuthClientLogo { get; set; }
}
diff --git a/Udap.Common/Certificates/IssuedCertificateStore.cs b/Udap.Common/Certificates/IssuedCertificateStore.cs
index c729c164..1d892bf3 100644
--- a/Udap.Common/Certificates/IssuedCertificateStore.cs
+++ b/Udap.Common/Certificates/IssuedCertificateStore.cs
@@ -65,7 +65,7 @@ private void LoadCertificates(UdapFileCertStoreManifest manifestCurrentValue)
foreach (var community in communities)
{
- _logger.LogInformation($"Loading Community:: Name: '{community.Name}' IdPBaseUrl: '{community.IdPBaseUrl}'");
+ _logger.LogInformation($"Loading Community:: Name: '{community.Name}'");
foreach (var communityIssuer in community.IssuedCerts)
{
@@ -97,7 +97,6 @@ is X509BasicConstraintsExtension extension &&
IssuedCertificates.Add(new IssuedCertificate
{
Community = community.Name,
- IdPBaseUrl = community.IdPBaseUrl,
Certificate = x509Cert,
Thumbprint = x509Cert.Thumbprint
});
diff --git a/Udap.Common/Metadata/Community.cs b/Udap.Common/Metadata/Community.cs
index 41be4156..a6f7cfde 100644
--- a/Udap.Common/Metadata/Community.cs
+++ b/Udap.Common/Metadata/Community.cs
@@ -16,11 +16,6 @@ public class Community
///
public string Name { get; set; } = "Default";
- ///
- /// Used to map an IdP url to the client certificate when registering with the Idp
- ///
- public string? IdPBaseUrl { get; set; }
-
///
/// Remote Idp community projection
///
diff --git a/Udap.Common/Models/IssuedCertificate.cs b/Udap.Common/Models/IssuedCertificate.cs
index ce276458..a8dc7224 100644
--- a/Udap.Common/Models/IssuedCertificate.cs
+++ b/Udap.Common/Models/IssuedCertificate.cs
@@ -16,7 +16,6 @@ public class IssuedCertificate
public int Id { get; set; }
public bool Enabled { get; set; }
// public string Name { get; set; } = string.Empty;
- public string? IdPBaseUrl { get; set; }
public string Community { get; set; } = string.Empty;
public X509Certificate2 Certificate { get; set; } = default!;
diff --git a/Udap.Common/Models/TieredClient.cs b/Udap.Common/Models/TieredClient.cs
index 3edff823..8203a0e8 100644
--- a/Udap.Common/Models/TieredClient.cs
+++ b/Udap.Common/Models/TieredClient.cs
@@ -12,4 +12,6 @@ public class TieredClient
public int CommunityId { get; set; }
public bool Enabled { get; set; }
+
+ public string TokenEndpoint { get; set; }
}
\ No newline at end of file
diff --git a/Udap.Metadata.Server/Configuration/DependencyInjection/ServiceCollectionExtensions.cs b/Udap.Metadata.Server/Configuration/DependencyInjection/ServiceCollectionExtensions.cs
index 5b28e5db..172b1847 100644
--- a/Udap.Metadata.Server/Configuration/DependencyInjection/ServiceCollectionExtensions.cs
+++ b/Udap.Metadata.Server/Configuration/DependencyInjection/ServiceCollectionExtensions.cs
@@ -96,10 +96,10 @@ public static IServiceCollection AddUdapMetadataServer(
//TODO: this could use some DI work...
var udapMetadata = new UdapMetadata(
udapMetadataOptions!,
- Hl7ModelInfoExtensions
- .BuildHl7FhirV1AndV2Scopes(new List { "patient", "user", "system" })
- .Where(s => s.Contains("/*")) //Just show the wild card
- );
+ new List
+ {
+ "openid", "patient/*.read", "user/*.read", "system/*.read", "patient/*.rs", "user/*.rs", "system/*.rs"
+ });
services.AddSingleton(udapMetadata);
services.TryAddScoped();
diff --git a/Udap.Model/Access/AccessTokenRequestForClientCredentialsBuilder.cs b/Udap.Model/Access/AccessTokenRequestForClientCredentialsBuilder.cs
index 74b75c2a..3df21b85 100644
--- a/Udap.Model/Access/AccessTokenRequestForClientCredentialsBuilder.cs
+++ b/Udap.Model/Access/AccessTokenRequestForClientCredentialsBuilder.cs
@@ -9,7 +9,6 @@
using System;
using System.Collections.Generic;
-using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
diff --git a/Udap.Model/Registration/UdapDcrBuilderForAuthorizationCode.cs b/Udap.Model/Registration/UdapDcrBuilderForAuthorizationCode.cs
index d777418e..268fbdac 100644
--- a/Udap.Model/Registration/UdapDcrBuilderForAuthorizationCode.cs
+++ b/Udap.Model/Registration/UdapDcrBuilderForAuthorizationCode.cs
@@ -14,6 +14,7 @@
using IdentityModel;
using Microsoft.IdentityModel.Tokens;
using Udap.Model.Statement;
+using Udap.Util.Extensions;
namespace Udap.Model.Registration;
@@ -72,22 +73,10 @@ public static UdapDcrBuilderForAuthorizationCode Create(X509Certificate2 cert)
{
return new UdapDcrBuilderForAuthorizationCode(cert, false);
}
-
- //TODO: Safe for multi SubjectAltName scenarios
- ///
- /// Register or update an existing registration by subjectAltName
- ///
- ///
- ///
- public static UdapDcrBuilderForAuthorizationCode Create(X509Certificate2 cert, string subjectAltName)
- {
- return new UdapDcrBuilderForAuthorizationCode(cert, false);
- }
-
+
///
/// Register or update an existing registration
///
- ///
///
public static UdapDcrBuilderForAuthorizationCode Create()
{
@@ -104,18 +93,6 @@ public static UdapDcrBuilderForAuthorizationCode Cancel(X509Certificate2 cert)
return new UdapDcrBuilderForAuthorizationCode(cert, true);
}
- //TODO: Safe for multi SubjectAltName scenarios
- ///
- /// Cancel an existing registration by subject alt name.
- ///
- ///
- ///
- ///
- public static UdapDcrBuilderForAuthorizationCode Cancel(X509Certificate2 cert, string subjectAltName)
- {
- return new UdapDcrBuilderForAuthorizationCode(cert, true);
- }
-
///
/// Cancel an existing registration.
///
@@ -142,6 +119,27 @@ public UdapDcrBuilderForAuthorizationCode WithGrantType(string grantType)
return this;
}
+#if NET6_0_OR_GREATER
+ ///
+ /// If the certificate has more than one uniformResourceIdentifier in the Subject Alternative Name
+ /// extension of the client certificate then this will allow one to be picked.
+ ///
+ ///
+ ///
+ public UdapDcrBuilderForAuthorizationCode WithIssuer(Uri issuer)
+ {
+ var uriNames = _certificate!.GetSubjectAltNames(n => n.TagNo == (int)X509Extensions.GeneralNameType.URI);
+ if (!uriNames.Select(u => u.Item2).Contains(issuer.AbsoluteUri))
+ {
+ throw new Exception($"Certificate does not contain a URI Subject Alternative Name of, {issuer.AbsoluteUri}");
+ }
+ _document.Issuer = issuer.AbsoluteUri;
+ _document.Subject = issuer.AbsoluteUri;
+ return this;
+ }
+
+#endif
+
public UdapDcrBuilderForAuthorizationCode WithAudience(string? audience)
{
_document.Audience = audience;
@@ -182,7 +180,7 @@ public UdapDcrBuilderForAuthorizationCode WithJwtId(string? jwtId = null)
return this;
}
- public UdapDcrBuilderForAuthorizationCode WithClientName(string? clientName)
+ public UdapDcrBuilderForAuthorizationCode WithClientName(string clientName)
{
_document.ClientName = clientName;
return this;
@@ -218,15 +216,13 @@ public UdapDcrBuilderForAuthorizationCode WithRedirectUrls(ICollection?
return this;
}
- public UdapDcrBuilderForAuthorizationCode WithLogoUri(string? logoUri)
+ public UdapDcrBuilderForAuthorizationCode WithLogoUri(string logoUri)
{
- //TODO: Testing. And better technique.
_ = new Uri(logoUri);
_document.LogoUri = logoUri;
return this;
}
- //TODO: should be able to build with all certs in path.
public UdapDcrBuilderForAuthorizationCode WithCertificate(X509Certificate2 certificate)
{
_certificate = certificate;
diff --git a/Udap.Model/Registration/UdapDcrBuilderForClientCredentials.cs b/Udap.Model/Registration/UdapDcrBuilderForClientCredentials.cs
index 406941b1..4f0f2d0c 100644
--- a/Udap.Model/Registration/UdapDcrBuilderForClientCredentials.cs
+++ b/Udap.Model/Registration/UdapDcrBuilderForClientCredentials.cs
@@ -25,7 +25,7 @@ namespace Udap.Model.Registration;
///
public class UdapDcrBuilderForClientCredentials
{
- private DateTime _now;
+ private readonly DateTime _now;
private UdapDynamicClientRegistrationDocument _document;
private X509Certificate2? _certificate;
@@ -75,21 +75,10 @@ public static UdapDcrBuilderForClientCredentials Create(X509Certificate2 cert)
return new UdapDcrBuilderForClientCredentials(cert, false);
}
- //TODO: Safe for multi SubjectAltName scenarios
- ///
- /// Register or update an existing registration by subjectAltName
- ///
- ///
- ///
- public static UdapDcrBuilderForClientCredentials Create(X509Certificate2 cert, string subjectAltName)
- {
- return new UdapDcrBuilderForClientCredentials(cert, false);
- }
-
+
///
/// Register or update an existing registration
///
- ///
///
public static UdapDcrBuilderForClientCredentials Create()
{
@@ -106,18 +95,7 @@ public static UdapDcrBuilderForClientCredentials Cancel(X509Certificate2 cert)
return new UdapDcrBuilderForClientCredentials(cert, true);
}
- //TODO: Safe for multi SubjectAltName scenarios
- ///
- /// Cancel an existing registration by subject alt name.
- ///
- ///
- ///
- ///
- public static UdapDcrBuilderForClientCredentials Cancel(X509Certificate2 cert, string subjectAltName)
- {
- return new UdapDcrBuilderForClientCredentials(cert, true);
- }
-
+
///
/// Cancel an existing registration.
///
@@ -131,12 +109,7 @@ public static UdapDcrBuilderForClientCredentials Cancel()
///
/// Set at construction time.
///
- public DateTime Now {
- get
- {
- return _now;
- }
- }
+ public DateTime Now => _now;
#if NET6_0_OR_GREATER
///
@@ -205,7 +178,7 @@ public UdapDcrBuilderForClientCredentials WithClientName(string clientName)
return this;
}
- public UdapDcrBuilderForClientCredentials WithContacts(ICollection contacts)
+ public UdapDcrBuilderForClientCredentials WithContacts(ICollection? contacts)
{
_document.Contacts = contacts;
return this;
@@ -223,14 +196,18 @@ public UdapDcrBuilderForClientCredentials WithScope(string? scope)
return this;
}
- public UdapDcrBuilderForClientCredentials WithLogoUri(string logoUri)
+ public UdapDcrBuilderForClientCredentials WithLogoUri(string? logoUri)
{
- //TODO: Testing. And better technique.
+ if (string.IsNullOrEmpty(logoUri))
+ {
+ return this;
+ }
+
_ = new Uri(logoUri);
+ _document.LogoUri = logoUri;
return this;
}
- //TODO: should be able to build with all certs in path.
public UdapDcrBuilderForClientCredentials WithCertificate(X509Certificate2 certificate)
{
_certificate = certificate;
diff --git a/Udap.Model/Registration/UdapDynamicClientRegistrationDocument.cs b/Udap.Model/Registration/UdapDynamicClientRegistrationDocument.cs
index 585eae81..7ab53c1e 100644
--- a/Udap.Model/Registration/UdapDynamicClientRegistrationDocument.cs
+++ b/Udap.Model/Registration/UdapDynamicClientRegistrationDocument.cs
@@ -44,7 +44,6 @@ public class UdapDynamicClientRegistrationDocument : Dictionary,
private long _issuedAt;
private string? _jwtId;
private string? _clientName;
- private Uri? _clientUri;
private ICollection? _redirectUris = new List();
private string? _logoUri;
private ICollection? _contacts = new HashSet();
@@ -257,29 +256,34 @@ public string? ClientName
}
}
- ///
- /// Web page providing information about the client.
- /// See
- ///
- [JsonPropertyName(UdapConstants.RegistrationDocumentValues.ClientUri)]
- public Uri? ClientUri {
- get
- {
- if (_clientUri == null)
- {
- if (Uri.TryCreate(GetStandardClaim(UdapConstants.RegistrationDocumentValues.ClientUri), UriKind.Absolute, out var value ))
- {
- _clientUri = value;
- }
- }
- return _clientUri;
- }
- set
- {
- _clientUri = value;
- if (value != null) this[UdapConstants.RegistrationDocumentValues.ClientUri] = value;
- }
- }
+
+ //
+ // Dropping this for now.
+ // If I bring it back read and follow instructions here: https://www.rfc-editor.org/rfc/rfc7591#section-5
+ //
+ // ///
+ // /// Web page providing information about the client.
+ // /// See
+ // ///
+ // [JsonPropertyName(UdapConstants.RegistrationDocumentValues.ClientUri)]
+ // public Uri? ClientUri {
+ // get
+ // {
+ // if (_clientUri == null)
+ // {
+ // if (Uri.TryCreate(GetStandardClaim(UdapConstants.RegistrationDocumentValues.ClientUri), UriKind.Absolute, out var value ))
+ // {
+ // _clientUri = value;
+ // }
+ // }
+ // return _clientUri;
+ // }
+ // set
+ // {
+ // _clientUri = value;
+ // if (value != null) this[UdapConstants.RegistrationDocumentValues.ClientUri] = value;
+ // }
+ // }
///
@@ -499,6 +503,16 @@ public string? Scope
}
}
+ public string? GetError()
+ {
+ return GetStandardClaim("error");
+ }
+
+ public string? GetErrorDescription()
+ {
+ return GetStandardClaim("error_description");
+ }
+
///
/// Similar to .
/// Adds a number of to the .
@@ -529,13 +543,8 @@ public void AddClaims(IEnumerable claims)
foreach (Claim claim in claims)
{
- if (claim == null)
- {
- continue;
- }
-
- string jsonClaimType = claim.Type;
- object jsonClaimValue = claim.ValueType.Equals(ClaimValueTypes.String) ? claim.Value : GetClaimValueUsingValueType(claim);
+ var jsonClaimType = claim.Type;
+ var jsonClaimValue = claim.ValueType.Equals(ClaimValueTypes.String) ? claim.Value : GetClaimValueUsingValueType(claim);
// If there is an existing value, append to it.
// What to do if the 'ClaimValueType' is not the same.
@@ -695,11 +704,6 @@ internal static object GetClaimValueUsingValueType(Claim claim)
if (claim.ValueType == JsonClaimValueTypes.JsonNull)
return string.Empty;
- if (claim.Value == null)
- {
- return string.Empty;
- }
-
if (claim.ValueType == JsonClaimValueTypes.Json)
return JsonNode.Parse(claim.Value)!;
diff --git a/Udap.Model/Registration/UdapDynamicClientRegistrationErrors.cs b/Udap.Model/Registration/UdapDynamicClientRegistrationErrors.cs
index dd6a3c41..86a7e3ac 100644
--- a/Udap.Model/Registration/UdapDynamicClientRegistrationErrors.cs
+++ b/Udap.Model/Registration/UdapDynamicClientRegistrationErrors.cs
@@ -38,8 +38,9 @@ public static class UdapDynamicClientRegistrationErrorDescriptions
public const string ClientNameMissing = "client_name is missing";
public const string LogoMissing = "logo_uri is missing";
public const string LogoInvalidUri = "logo_uri is not a valid uri";
- public const string LogoInvalidFileType = "logo_uri is not a valid file type";
+ public const string LogoInvalidContentType = "logo_uri is not a valid content-type";
public const string LogoInvalidScheme = "logo_uri is not a valid https schema";
+ public const string LogoCannotBeResolved = "logo_uri cannot be resolved";
public const string GrantTypeMissing = "grant_types is missing";
public const string ResponseTypesMissing = "invalid_client_metadata response_types is missing";
diff --git a/Udap.Model/Statement/SignedSoftwareStatementBuilder.cs b/Udap.Model/Statement/SignedSoftwareStatementBuilder.cs
index 78a092df..f17a1c1c 100644
--- a/Udap.Model/Statement/SignedSoftwareStatementBuilder.cs
+++ b/Udap.Model/Statement/SignedSoftwareStatementBuilder.cs
@@ -9,13 +9,11 @@
using System;
using System.IdentityModel.Tokens.Jwt;
-using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
-using Org.BouncyCastle.Math.EC;
using Udap.Model.Registration;
using ECCurve = System.Security.Cryptography.ECCurve;
@@ -94,15 +92,14 @@ public string BuildECDSA(string? algorithm = UdapConstants.SupportedAlgorithm.ES
// https://github.com/dotnet/runtime/issues/77590#issuecomment-1325896560
// https://stackoverflow.com/a/57330499/6115838
//
- byte[] encryptedPrivKeyBytes = key.ExportEncryptedPkcs8PrivateKey(
+ var encryptedPrivateKeyBytes = key?.ExportEncryptedPkcs8PrivateKey(
"ILikePasswords",
new PbeParameters(
PbeEncryptionAlgorithm.Aes256Cbc,
HashAlgorithmName.SHA256,
iterationCount: 100_000));
- ecdsa.ImportEncryptedPkcs8PrivateKey("ILikePasswords".AsSpan(), encryptedPrivKeyBytes.AsSpan(),
- out int bytesRead);
+ ecdsa.ImportEncryptedPkcs8PrivateKey("ILikePasswords".AsSpan(), encryptedPrivateKeyBytes.AsSpan(), out _);
}
else
{
diff --git a/Udap.Model/Udap.Model.csproj b/Udap.Model/Udap.Model.csproj
index 51db96cd..cd2d11e2 100644
--- a/Udap.Model/Udap.Model.csproj
+++ b/Udap.Model/Udap.Model.csproj
@@ -27,9 +27,9 @@
-
+
-
+
@@ -44,4 +44,21 @@
+
+
+
+ ..\..\..\..\DuendeSoftware\IdentityServer\src\Storage\bin\Debug\net7.0\Duende.IdentityServer.Storage.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Udap.Model/UdapConstants.cs b/Udap.Model/UdapConstants.cs
index d4ed3715..8a3400ca 100644
--- a/Udap.Model/UdapConstants.cs
+++ b/Udap.Model/UdapConstants.cs
@@ -7,8 +7,6 @@
// */
#endregion
-using Udap.Model.UdapAuthenticationExtensions;
-
namespace Udap.Model;
///
diff --git a/Udap.Server/Configuration/DependencyInjection/Additional.cs b/Udap.Server/Configuration/DependencyInjection/Additional.cs
index 6ad3f0e9..fbbbb0db 100644
--- a/Udap.Server/Configuration/DependencyInjection/Additional.cs
+++ b/Udap.Server/Configuration/DependencyInjection/Additional.cs
@@ -7,7 +7,9 @@
// */
#endregion
+using Duende.IdentityServer.Hosting.DynamicProviders;
using Duende.IdentityServer.Services;
+using Duende.IdentityServer.Stores;
using Duende.IdentityServer.Validation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
diff --git a/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs b/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs
index 1a88fa1f..80673fed 100644
--- a/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs
+++ b/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs
@@ -15,20 +15,26 @@
//
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Hosting;
+using Duende.IdentityServer.Models;
using Duende.IdentityServer.ResponseHandling;
using IdentityModel;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Udap.Client.Client;
using Udap.Common;
using Udap.Common.Certificates;
using Udap.Common.Extensions;
+using Udap.Model;
using Udap.Server;
using Udap.Server.Configuration.DependencyInjection;
using Udap.Server.DbContexts;
+using Udap.Server.Hosting.DynamicProviders.Oidc;
using Udap.Server.Options;
using Udap.Server.ResponseHandling;
+using Udap.Server.Security.Authentication.TieredOAuth;
using Udap.Server.Stores;
using Udap.Server.Validation;
using Constants = Udap.Server.Constants;
@@ -95,7 +101,7 @@ public static IUdapServiceBuilder AddUdapConfigurationStore(
public static IUdapServiceBuilder AddSmartV2Expander(this IUdapServiceBuilder builder)
{
- builder.Services.AddScoped();
+ builder.Services.AddScoped();
return builder;
}
@@ -118,4 +124,4 @@ public static IUdapServiceBuilder AddPrivateFileStore(this IUdapServiceBuilder b
return builder;
}
-}
+}
\ No newline at end of file
diff --git a/Udap.Server/Configuration/ServerSettings.cs b/Udap.Server/Configuration/ServerSettings.cs
index 37e36afa..cabd7b25 100644
--- a/Udap.Server/Configuration/ServerSettings.cs
+++ b/Udap.Server/Configuration/ServerSettings.cs
@@ -34,9 +34,7 @@ public class ServerSettings
///
[JsonPropertyName("ForceStateParamOnAuthorizationCode")]
public bool ForceStateParamOnAuthorizationCode { get; set; } = false;
-
- public ICollection? IdPMappings { get; set; }
-
+
[JsonPropertyName("LogoRequired")]
public bool LogoRequired { get; set; } = true;
@@ -49,11 +47,6 @@ public class ServerSettings
public bool AlwaysIncludeUserClaimsInIdToken { get; set; }
}
-public class IdPMapping
-{
- public string? Scheme { get; set; }
- public string? IdpBaseUrl { get; set; }
-}
public enum ServerSupport
{
diff --git a/Udap.Server/Entities/TieredClient.cs b/Udap.Server/Entities/TieredClient.cs
index 3cbc9328..a28ad016 100644
--- a/Udap.Server/Entities/TieredClient.cs
+++ b/Udap.Server/Entities/TieredClient.cs
@@ -23,4 +23,6 @@ public class TieredClient
public int CommunityId { get; set; }
public bool Enabled { get; set; }
+
+ public string TokenEndpoint { get; set; }
}
\ No newline at end of file
diff --git a/Udap.Server/Hosting/DynamicProviders/Oidc/IdentityServerBuilderExtensions.cs b/Udap.Server/Hosting/DynamicProviders/Oidc/IdentityServerBuilderExtensions.cs
new file mode 100644
index 00000000..80683334
--- /dev/null
+++ b/Udap.Server/Hosting/DynamicProviders/Oidc/IdentityServerBuilderExtensions.cs
@@ -0,0 +1,28 @@
+#region (c) 2023 Joseph Shook. All rights reserved.
+// /*
+// Authors:
+// Joseph Shook Joseph.Shook@Surescripts.com
+//
+// See LICENSE in the project root for license information.
+// */
+#endregion
+
+using Duende.IdentityServer.Models;
+using Microsoft.Extensions.DependencyInjection;
+using Udap.Server.Hosting.DynamicProviders.Store;
+
+namespace Udap.Server.Hosting.DynamicProviders.Oidc;
+public static class IdentityServerBuilderExtensions
+{
+
+
+ public static IIdentityServerBuilder AddInMemorIdentityProviders(
+ this IIdentityServerBuilder builder, IEnumerable identityProviders)
+ {
+ builder.Services.AddSingleton(identityProviders);
+ builder.AddIdentityProviderStore();
+
+ return builder;
+ }
+
+}
diff --git a/Udap.Server/Hosting/DynamicProviders/Store/UdapInMemoryIdentityProviderStore.cs b/Udap.Server/Hosting/DynamicProviders/Store/UdapInMemoryIdentityProviderStore.cs
new file mode 100644
index 00000000..cb34edc5
--- /dev/null
+++ b/Udap.Server/Hosting/DynamicProviders/Store/UdapInMemoryIdentityProviderStore.cs
@@ -0,0 +1,45 @@
+#region (c) 2023 Joseph Shook. All rights reserved.
+// /*
+// Authors:
+// Joseph Shook Joseph.Shook@Surescripts.com
+//
+// See LICENSE in the project root for license information.
+// */
+#endregion
+
+using Duende.IdentityServer.Models;
+using Duende.IdentityServer.Stores;
+using Udap.Common;
+
+namespace Udap.Server.Hosting.DynamicProviders.Store;
+internal class UdapInMemoryIdentityProviderStore : IIdentityProviderStore
+{
+ private readonly IEnumerable _providers;
+
+ public UdapInMemoryIdentityProviderStore(IEnumerable providers)
+ {
+ _providers = providers;
+ }
+
+ public Task> GetAllSchemeNamesAsync()
+ {
+ using var activity = Tracing.StoreActivitySource.StartActivity("UdapInMemoryIdentityProviderStore.GetAllSchemeNames");
+
+ var items = _providers.Select(x => new IdentityProviderName
+ {
+ Enabled = x.Enabled,
+ DisplayName = x.DisplayName,
+ Scheme = x.Scheme
+ });
+
+ return Task.FromResult(items);
+ }
+
+ public Task GetBySchemeAsync(string scheme)
+ {
+ using var activity = Tracing.StoreActivitySource.StartActivity("UdapInMemoryIdentityProviderStore.GetByScheme");
+
+ var item = _providers.FirstOrDefault(x => x.Scheme == scheme);
+ return Task.FromResult(item);
+ }
+}
\ No newline at end of file
diff --git a/Udap.Server/Registration/UdapDynamicClientRegistrationValidationResult.cs b/Udap.Server/Registration/UdapDynamicClientRegistrationValidationResult.cs
index bc655674..7ef0c3a9 100644
--- a/Udap.Server/Registration/UdapDynamicClientRegistrationValidationResult.cs
+++ b/Udap.Server/Registration/UdapDynamicClientRegistrationValidationResult.cs
@@ -8,6 +8,7 @@
#endregion
using Udap.Model.Registration;
+using Udap.Util.Extensions;
namespace Udap.Server.Registration;
diff --git a/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs b/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs
index a1a83fc8..fc6b3196 100644
--- a/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs
+++ b/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs
@@ -16,16 +16,21 @@
//
using System.IdentityModel.Tokens.Jwt;
+using System.Net.Http;
+using System.Net.Mime;
+using System.Reflection.Metadata;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;
+using Duende.IdentityServer.Stores;
using IdentityModel;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
+using Udap.Client.Client;
using Udap.Common;
using Udap.Common.Certificates;
using Udap.Common.Extensions;
@@ -44,27 +49,33 @@ namespace Udap.Server.Registration;
public class UdapDynamicClientRegistrationValidator : IUdapDynamicClientRegistrationValidator
{
private readonly TrustChainValidator _trustChainValidator;
+ private readonly HttpClient _httpClient;
private readonly IReplayCache _replayCache;
private readonly ILogger _logger;
private readonly ServerSettings _serverSettings;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IScopeExpander _scopeExpander;
+ private readonly IResourceStore _resourceStore;
private const string Purpose = nameof(UdapDynamicClientRegistrationValidator);
public UdapDynamicClientRegistrationValidator(
TrustChainValidator trustChainValidator,
+ HttpClient httpClient,
IReplayCache replayCache,
ServerSettings serverSettings,
IHttpContextAccessor httpContextAccessor,
IScopeExpander scopeExpander,
+ IResourceStore resourceStore, //TODO use CachingResourceStore
ILogger logger)
{
_trustChainValidator = trustChainValidator;
+ _httpClient = httpClient;
_replayCache = replayCache;
_serverSettings = serverSettings;
_httpContextAccessor = httpContextAccessor;
_scopeExpander = scopeExpander;
+ _resourceStore = resourceStore;
_logger = logger;
}
@@ -367,7 +378,8 @@ IEnumerable anchors
{
if (_serverSettings.LogoRequired)
{
- if ( ! ValidateLogoUri(document, out UdapDynamicClientRegistrationValidationResult? errorResult))
+ var (successFlag, errorResult) = await ValidateLogoUri(document);
+ if (!successFlag)
{
return errorResult!;
}
@@ -481,13 +493,31 @@ IEnumerable anchors
{
var scopes = document.Scope.Split(' ', StringSplitOptions.RemoveEmptyEntries);
// todo: ideally scope names get checked against configuration store?
+
+ var resources = await _resourceStore.GetAllEnabledResourcesAsync();
+ var expandedScopes = _scopeExpander.Expand(scopes).ToList();
+ var explodedScopes = _scopeExpander.WildCardExpand(expandedScopes, resources.ApiScopes.Select(a => a.Name).ToList()).ToList();
+ var allowedApiScopes = resources.ApiScopes.Where(s => explodedScopes.Contains(s.Name));
- var expandedScopes = _scopeExpander.Expand(scopes);
+ foreach (var scope in allowedApiScopes)
+ {
+ client?.AllowedScopes.Add(scope.Name);
+ }
+
+ var allowedResourceScopes = resources.IdentityResources.Where(s => explodedScopes.Contains(s.Name));
- foreach (var scope in expandedScopes)
+ foreach (var scope in allowedResourceScopes.Where(s => s.Enabled).Select(s => s.Name))
{
client?.AllowedScopes.Add(scope);
}
+
+ //
+ // Present scopes in aggregate form
+ //
+ if (client?.AllowedScopes != null)
+ {
+ document.Scope = _scopeExpander.Aggregate(client.AllowedScopes).OrderBy(s => s).ToSpaceSeparatedString();
+ }
}
@@ -503,10 +533,9 @@ IEnumerable anchors
return await Task.FromResult(new UdapDynamicClientRegistrationValidationResult(client, document));
}
- public bool ValidateLogoUri(UdapDynamicClientRegistrationDocument document,
- out UdapDynamicClientRegistrationValidationResult? errorResult)
+ public async Task<(bool, UdapDynamicClientRegistrationValidationResult?)> ValidateLogoUri(UdapDynamicClientRegistrationDocument document)
{
- errorResult = null;
+ UdapDynamicClientRegistrationValidationResult? errorResult;
if (string.IsNullOrEmpty(document.LogoUri))
{
@@ -514,32 +543,44 @@ public bool ValidateLogoUri(UdapDynamicClientRegistrationDocument document,
UdapDynamicClientRegistrationErrors.InvalidClientMetadata,
UdapDynamicClientRegistrationErrorDescriptions.LogoMissing);
- return false;
+ return (false, errorResult);
}
if (Uri.TryCreate(document.LogoUri, UriKind.Absolute, out var logoUri))
{
- if (!logoUri.OriginalString.EndsWith("png", StringComparison.OrdinalIgnoreCase) &&
- !logoUri.OriginalString.EndsWith("jpg", StringComparison.OrdinalIgnoreCase) &&
- !logoUri.OriginalString.EndsWith("gif", StringComparison.OrdinalIgnoreCase))
+ if (!logoUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
{
errorResult = new UdapDynamicClientRegistrationValidationResult(
UdapDynamicClientRegistrationErrors.InvalidClientMetadata,
- UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidFileType);
+ UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidScheme);
- return false;
+ return (false, errorResult);
}
+
+ var response = await _httpClient.GetAsync(logoUri.OriginalString);
+ response.Content.Headers.TryGetValues("Content-Type", out var contentTypes);
+ var contentType = contentTypes?.FirstOrDefault();
- if (!logoUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
+ if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
errorResult = new UdapDynamicClientRegistrationValidationResult(
- UdapDynamicClientRegistrationErrors.InvalidClientMetadata,
- UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidScheme);
+ UdapDynamicClientRegistrationErrors.InvalidClientMetadata,
+ UdapDynamicClientRegistrationErrorDescriptions.LogoCannotBeResolved);
- return false;
+ return (false, errorResult);
}
+
+ if (contentType == null ||
+ !contentType.Equals("image/png", StringComparison.OrdinalIgnoreCase) &&
+ !contentType.Equals(MediaTypeNames.Image.Jpeg, StringComparison.OrdinalIgnoreCase) &&
+ !contentType.Equals(MediaTypeNames.Image.Gif, StringComparison.OrdinalIgnoreCase))
+ {
+ errorResult = new UdapDynamicClientRegistrationValidationResult(
+ UdapDynamicClientRegistrationErrors.InvalidClientMetadata,
+ UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidContentType);
-
+ return (false, errorResult);
+ }
}
else
{
@@ -547,10 +588,10 @@ public bool ValidateLogoUri(UdapDynamicClientRegistrationDocument document,
UdapDynamicClientRegistrationErrors.InvalidClientMetadata,
UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidUri);
- return false;
+ return (false, errorResult);
}
- return true;
+ return (true, null);
}
public async Task ValidateJti(
diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationDefaults.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationDefaults.cs
index 4747b794..c5602992 100644
--- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationDefaults.cs
+++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationDefaults.cs
@@ -21,11 +21,7 @@ public static class TieredOAuthAuthenticationDefaults
///
/// Default value for .
///
- public static readonly string DisplayName = "UDAP Tiered OAuth (DOTNET-Provider1)";
+ public static readonly string DisplayName = "Launch Tiered OAuth";
- public static readonly string CallbackPath = "/signin-tieredoauth";
-
- // public static readonly string AuthorizationEndpoint = "https://localhost:5055/connect/authorize";
- //
- // public static readonly string TokenEndpoint = "https://localhost:5055/connect/token";
+ public static readonly string CallbackPath = "/federation/udap-tiered/signin";
}
\ No newline at end of file
diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationExtensions.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationExtensions.cs
index 53af7340..86a65967 100644
--- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationExtensions.cs
+++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationExtensions.cs
@@ -78,7 +78,7 @@ public static AuthenticationBuilder AddTieredOAuth(
Action configuration)
{
- builder.Services.AddTransient();
+ builder.Services.TryAddTransient();
builder.Services.AddHttpClient().AddHttpMessageHandler();
builder.Services.TryAddSingleton(sp =>
@@ -91,7 +91,7 @@ public static AuthenticationBuilder AddTieredOAuth(
return handler;
});
-
+
builder.Services.TryAddSingleton, TieredOAuthPostConfigureOptions>();
return builder.AddOAuth(scheme, caption, configuration);
}
diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs
index 518d5561..3fdcd748 100644
--- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs
+++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs
@@ -11,19 +11,17 @@
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
+using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Web;
-using Duende.IdentityServer.Models;
-using Duende.IdentityServer.Stores;
using IdentityModel;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
-using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
@@ -31,21 +29,20 @@
using Microsoft.IdentityModel.Tokens;
using Udap.Client.Client;
using Udap.Common.Certificates;
-using Udap.Common.Extensions;
using Udap.Common.Models;
+using Udap.Model;
using Udap.Model.Access;
-using Udap.Model.Registration;
using Udap.Server.Storage.Stores;
using Udap.Util.Extensions;
-using static IdentityModel.ClaimComparer;
namespace Udap.Server.Security.Authentication.TieredOAuth;
+
public class TieredOAuthAuthenticationHandler : OAuthHandler
{
private readonly IUdapClient _udapClient;
private readonly IPrivateCertificateStore _certificateStore;
- private readonly IServiceScopeFactory _scopeFactory;
+ private readonly IUdapClientRegistrationStore _udapClientRegistrationStore;
///
/// Initializes a new instance of .
@@ -58,12 +55,13 @@ public TieredOAuthAuthenticationHandler(
ISystemClock clock,
IUdapClient udapClient,
IPrivateCertificateStore certificateStore,
- IServiceScopeFactory scopeFactory) :
+ IUdapClientRegistrationStore udapClientRegistrationStore
+ ) :
base(options, logger, encoder, clock)
{
_udapClient = udapClient;
_certificateStore = certificateStore;
- _scopeFactory = scopeFactory;
+ _udapClientRegistrationStore = udapClientRegistrationStore;
}
/// Constructs the OAuth challenge url.
@@ -83,10 +81,39 @@ protected override string BuildChallengeUrl(AuthenticationProperties properties,
var state = Options.StateDataFormat.Protect(properties);
queryStrings.Add("state", state);
- var authorizationEndpoint = QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, queryStrings!);
-
- return authorizationEndpoint;
+
+ // Static configured Options
+ // if (!Options.AuthorizationEndpoint.IsNullOrEmpty())
+ // {
+ // return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, queryStrings!);
+ // }
+
+ var authEndpoint = properties.Parameters[UdapConstants.Discovery.AuthorizationEndpoint] as string ??
+ throw new InvalidOperationException("Missing IdP authorization endpoint.");
+
+ var community = properties.GetParameter(UdapConstants.Community);
+ if (!community.IsNullOrEmpty())
+ {
+ queryStrings.Add(UdapConstants.Community, community!);
+ }
+
+ var tokenEndpoint = properties.GetParameter(UdapConstants.Discovery.TokenEndpoint);
+
+ if (!tokenEndpoint.IsNullOrEmpty())
+ {
+ Options.TokenEndpoint = tokenEndpoint!;
+ }
+
+ var idpBaseUrl = properties.GetParameter("idpBaseUrl");
+
+ if (!idpBaseUrl.IsNullOrEmpty())
+ {
+ Options.IdPBaseUrl = idpBaseUrl;
+ }
+
+ // Dynamic options
+ return QueryHelpers.AddQueryString(authEndpoint, queryStrings!);
}
@@ -225,11 +252,14 @@ protected override async Task HandleRemoteAuthenticateAsync
var validationParameters = Options.TokenValidationParameters.Clone();
+ var clientId = properties.Items["client_id"] ?? throw new InvalidOperationException($"ClientId not found in properties");
+ var tieredClient = await _udapClientRegistrationStore.FindTieredClientById(clientId) ?? throw new InvalidOperationException($"ClientId not found in registration store: {clientId}");
+
// TODO: pre installed keys check?
var request = new DiscoveryDocumentRequest
{
- Address = Options.IdPBaseUrl,
+ Address = tieredClient.IdPBaseUrl,
Policy = new IdentityModel.Client.DiscoveryPolicy()
{
//TODO: Promote to TieredOAuthOptions. Maybe even injectable for advanced use cases.
@@ -240,7 +270,7 @@ protected override async Task HandleRemoteAuthenticateAsync
var keys = await _udapClient.ResolveJwtKeys(request);
validationParameters.IssuerSigningKeys = keys;
- var tokenEndpointUser = ValidateToken(idToken, properties, validationParameters, out var tokenEndpointJwt);
+ var tokenEndpointUser = ValidateToken(idToken, tieredClient.IdPBaseUrl, clientId, validationParameters, out var tokenEndpointJwt);
// nonce = tokenEndpointJwt.Payload.Nonce;
// if (!string.IsNullOrEmpty(nonce))
@@ -272,7 +302,8 @@ protected override async Task HandleRemoteAuthenticateAsync
// Note this modifies properties if Options.UseTokenLifetime
private ClaimsPrincipal ValidateToken(
string idToken,
- AuthenticationProperties properties,
+ string idpBaseUrl,
+ string clientId,
TokenValidationParameters validationParameters,
out JwtSecurityToken jwt)
{
@@ -293,8 +324,7 @@ private ClaimsPrincipal ValidateToken(
// no need to validate signature when token is received using "code flow" as per spec
// [http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation].
- validationParameters.ValidIssuer = Options.IdPBaseUrl;
- properties.Items.TryGetValue("client_id", out var clientId);
+ validationParameters.ValidIssuers = new List{ idpBaseUrl , idpBaseUrl.TrimEnd('/')};
validationParameters.ValidAudience = clientId;
validationParameters.ValidateIssuerSigningKey = false;
@@ -380,28 +410,33 @@ protected override async Task ExchangeCodeAsync([NotNull] OA
var originalRequestParams = HttpUtility.ParseQueryString(context.Properties.Items["returnUrl"] ?? "~/");
var idp = (originalRequestParams.GetValues("idp") ?? throw new InvalidOperationException()).Last();
+ var idpUri = new Uri(idp);
+ var communityParam = (HttpUtility.ParseQueryString(idpUri.Query).GetValues("community") ?? Array.Empty()).LastOrDefault();
+
var clientId = context.Properties.Items["client_id"];
var resourceHolderRedirectUrl =
- $"{Context.Request.Scheme}://{Context.Request.Host}{Context.Request.PathBase}{Options.CallbackPath}";
+ $"{Context.Request.Scheme}{Uri.SchemeDelimiter}{Context.Request.Host}{Context.Request.PathBase}{Options.CallbackPath}";
var requestParams = Context.Request.Query;
var code = requestParams["code"];
-
- using var serviceScope = _scopeFactory.CreateScope();
- var clientStore = serviceScope.ServiceProvider.GetRequiredService();
- var idpClient = await clientStore.FindTieredClientById(clientId);
- var idpClientId = idpClient.ClientId;
-
+ var tieredClient = await _udapClientRegistrationStore.FindTieredClientById(clientId) ?? throw new InvalidOperationException($"ClientId not found: {clientId}");
+ var tieredClientId = tieredClient.ClientId;
+
await _certificateStore.Resolve();
// Sign request for token
var tokenRequestBuilder = AccessTokenRequestForAuthorizationCodeBuilder.Create(
- idpClientId,
- Options.TokenEndpoint,
- _certificateStore.IssuedCertificates.Where(ic => ic.IdPBaseUrl == idp)
+ tieredClientId,
+ tieredClient.TokenEndpoint,
+ communityParam == null
+ ?
+ _certificateStore.IssuedCertificates.First().Certificate
+ :
+ _certificateStore.IssuedCertificates.Where(ic => ic.Community == communityParam)
//TODO: multiple certs or latest cert?
- .Select(ic => ic.Certificate).First(),
+ .Select(ic => ic.Certificate).First(),
+
resourceHolderRedirectUrl,
code);
@@ -416,22 +451,35 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop
{
var requestParams = HttpUtility.ParseQueryString(properties.Items["returnUrl"] ?? "~/");
- var idp = (requestParams.GetValues("idp") ?? throw new InvalidOperationException()).Last();
+ var idpParam = (requestParams.GetValues("idp") ?? throw new InvalidOperationException()).Last();
var scope = (requestParams.GetValues("scope") ?? throw new InvalidOperationException()).First();
var clientRedirectUrl = (requestParams.GetValues("redirect_uri") ?? throw new InvalidOperationException()).Last();
var updateRegistration = requestParams.GetValues("update_registration")?.Last();
// Validate idp Server;
- var community = idp.GetCommunityFromQueryParams();
-
+ var idpUri = new Uri(idpParam);
+ var communityParam = (HttpUtility.ParseQueryString(idpUri.Query).GetValues("community") ?? Array.Empty()).LastOrDefault();
+ var idp = idpUri.OriginalString;
+ if (communityParam != null)
+ {
+ if (idp.Contains($":{idpUri.Port}"))
+ {
+ idp = $"{idpUri.Scheme}{Uri.SchemeDelimiter}{idpUri.Host}:{idpUri.Port}{idpUri.LocalPath}";
+ }
+ else
+ {
+ idp = $"{idpUri.Scheme}{Uri.SchemeDelimiter}{idpUri.Host}{idpUri.LocalPath}";
+ }
+ }
+
_udapClient.Problem += element => properties.Parameters.Add("Problem", element.ChainElementStatus.Summarize(TrustChainValidator.DefaultProblemFlags));
_udapClient.Untrusted += certificate2 => properties.Parameters.Add("Untrusted", certificate2.Subject);
_udapClient.TokenError += message => properties.Parameters.Add("TokenError", message);
- var response = await _udapClient.ValidateResource(idp, community);
+ var response = await _udapClient.ValidateResource(idp, communityParam);
var resourceHolderRedirectUrl =
- $"{Context.Request.Scheme}://{Context.Request.Host}{Options.CallbackPath}";
+ $"{Context.Request.Scheme}{Uri.SchemeDelimiter}{Context.Request.Host}{Options.CallbackPath}";
if (response.IsError)
{
@@ -463,9 +511,7 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop
// if not registered with IdP, then register.
//
- using var serviceScope = _scopeFactory.CreateScope();
- var clientStore = serviceScope.ServiceProvider.GetRequiredService();
- var idpClient = await clientStore.FindTieredClientById(idp);
+ var idpClient = await _udapClientRegistrationStore.FindTieredClientById(idp);
var idpClientId = null as string;
@@ -479,17 +525,8 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop
if (idpClient == null || !idpClient.Enabled || updateRegistration == "true")
{
await _certificateStore.Resolve();
-
-
-
- // What does this code even accomplish? Shouldn't it look for more than the first community?
- // Maybe I am still tied to one community. Lots more work here.
-
- var communityName = _certificateStore.IssuedCertificates.First().Community;
- var registrationStore = serviceScope.ServiceProvider.GetRequiredService();
-
- var communityId =
- await registrationStore.GetCommunityId(communityName, Context.RequestAborted);
+ var communityName = communityParam ?? _certificateStore.IssuedCertificates.First().Community; //TODO: query by
+ var communityId = await _udapClientRegistrationStore.GetCommunityId(communityName, Context.RequestAborted);
if (communityId == null)
{
@@ -499,18 +536,27 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop
//Todo: return strategy?
return;
}
-
-
- //TODO: RegisterClient should be typed to the two builders
- // UdapDcrBuilderForAuthorizationCode or UdapDcrBuilderForClientCredentials
var document = await _udapClient.RegisterTieredClient(
resourceHolderRedirectUrl,
- _certificateStore.IssuedCertificates.Where(ic => ic.IdPBaseUrl == idp)
- .Select(ic => ic.Certificate),
+
+ communityParam == null
+ ?
+ new List(){ _certificateStore.IssuedCertificates.First().Certificate }
+ :
+ _certificateStore.IssuedCertificates.Where(ic => ic.Community == communityParam)
+ .Select(ic => ic.Certificate),
+
OptionsMonitor.CurrentValue.Scope.ToSpaceSeparatedString(),
Context.RequestAborted);
+ if (document.GetError() != null)
+ {
+ Logger.LogWarning(document.GetError() + ": " + document.GetErrorDescription());
+ await base.HandleChallengeAsync(properties);
+ return;
+ }
+
if (idpClient == null)
{
idpClientId = document.ClientId;
@@ -528,12 +574,15 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop
RedirectUri = clientRedirectUrl,
ClientUriSan = publicCert.GetSubjectAltNames().First().Item2, //TODO: can a AuthServer register multiple times per community?
CommunityId = communityId.Value,
- Enabled = true
+ Enabled = true,
+ TokenEndpoint = properties.Parameters[UdapConstants.Discovery.TokenEndpoint] as string ?? throw new InvalidOperationException("Missing IdP token endpoint."),
};
- await registrationStore.UpsertTieredClient(tieredClient, Context.RequestAborted);
+ await _udapClientRegistrationStore.UpsertTieredClient(tieredClient, Context.RequestAborted);
}
+
+
properties.SetString("client_id", idpClientId);
await base.HandleChallengeAsync(properties);
diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs
index a7df3c8c..1f53d9a2 100644
--- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs
+++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs
@@ -11,30 +11,39 @@
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using IdentityModel;
-using Udap.Model;
namespace Udap.Server.Security.Authentication.TieredOAuth;
-public class TieredOAuthAuthenticationOptions : OAuthOptions{
+public class TieredOAuthAuthenticationOptions : OAuthOptions
+{
private readonly JwtSecurityTokenHandler _defaultHandler = new JwtSecurityTokenHandler();
public TieredOAuthAuthenticationOptions()
{
- CallbackPath = TieredOAuthAuthenticationDefaults.CallbackPath;
- ClientId = "dynamic";
- ClientSecret = "signed metadata";
- // AuthorizationEndpoint = TieredOAuthAuthenticationDefaults.AuthorizationEndpoint;
- // TokenEndpoint = TieredOAuthAuthenticationDefaults.TokenEndpoint;
SignInScheme = TieredOAuthAuthenticationDefaults.AuthenticationScheme;
-
- // TODO: configurable.
+
+ // TODO: configurable for the non-dynamic AddTieredOAuthForTests call.
Scope.Add(OidcConstants.StandardScopes.OpenId);
- // Scope.Add(UdapConstants.StandardScopes.FhirUser);
Scope.Add(OidcConstants.StandardScopes.Email);
Scope.Add(OidcConstants.StandardScopes.Profile);
SecurityTokenValidator = _defaultHandler;
+
+ //
+ // Properties below are required to survive Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions.Validate(String scheme)
+ //
+ // AuthorizationEndpoint and TokenEndpoint are placed them in the AuthenticationProperties.Parameters
+ // and set during the GET /externallogin/challenge
+ //
+ // ClientSecret is not used
+ // ClientId is set after dynamic registration
+ //
+ AuthorizationEndpoint = "/connect/authorize";
+ TokenEndpoint = "/connect/token";
+ ClientSecret = "signed metadata";
+ ClientId = "temporary";
+ CallbackPath = TieredOAuthAuthenticationDefaults.CallbackPath;
}
///
@@ -54,4 +63,6 @@ public TieredOAuthAuthenticationOptions()
///
///
public string IdPBaseUrl { get; set; }
+
+ public string Community { get; set; }
}
\ No newline at end of file
diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthHelpers.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthHelpers.cs
new file mode 100644
index 00000000..819ceade
--- /dev/null
+++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthHelpers.cs
@@ -0,0 +1,86 @@
+#region (c) 2023 Joseph Shook. All rights reserved.
+// /*
+// Authors:
+// Joseph Shook Joseph.Shook@Surescripts.com
+//
+// See LICENSE in the project root for license information.
+// */
+#endregion
+
+using System.Web;
+using Duende.IdentityServer.Services;
+using IdentityModel;
+using IdentityModel.Client;
+using Microsoft.AspNetCore.Authentication;
+using Udap.Client.Client;
+using Udap.Model;
+
+namespace Udap.Server.Security.Authentication.TieredOAuth;
+public static class TieredOAuthHelpers
+{
+ public static async Task BuildDynamicTieredOAuthOptions(
+ IIdentityServerInteractionService interactionService,
+ IUdapClient udapClient,
+ string scheme,
+ string redirectUri,
+ string returnUrl)
+ {
+ if (interactionService.IsValidReturnUrl(returnUrl) == false)
+ {
+ throw new Exception("invalid return URL");
+ }
+
+ var props = new AuthenticationProperties
+ {
+ RedirectUri = redirectUri,
+
+ Items =
+ {
+ { "returnUrl", returnUrl },
+ { "scheme", scheme },
+ }
+ };
+
+
+ var originalRequestParams = HttpUtility.ParseQueryString(returnUrl);
+ var idp = (originalRequestParams.GetValues("idp") ?? throw new InvalidOperationException()).Last();
+
+ var parts = idp.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries);
+
+ if (parts.Length > 1)
+ {
+ props.Parameters.Add(UdapConstants.Community, parts[1]);
+ }
+
+ var idpUri = new Uri(idp);
+ string idpBaseUrl;
+
+ if (idp.Contains($":{idpUri.Port}"))
+ {
+ idpBaseUrl = $"{idpUri.Scheme}{Uri.SchemeDelimiter}{idpUri.Host}:{idpUri.Port}{idpUri.LocalPath}";
+ }
+ else
+ {
+ idpBaseUrl = $"{idpUri.Scheme}{Uri.SchemeDelimiter}{idpUri.Host}{idpUri.LocalPath}";
+ }
+
+ var request = new DiscoveryDocumentRequest
+ {
+ Address = idpBaseUrl,
+ Policy = new IdentityModel.Client.DiscoveryPolicy()
+ {
+ EndpointValidationExcludeList = new List { OidcConstants.Discovery.RegistrationEndpoint }
+ }
+ };
+
+ var openIdConfig = await udapClient.ResolveOpenIdConfig(request);
+
+ // TODO: Properties will be protected in state in the BuildchallengeUrl. Need to trim out some of these
+ // during the protect process.
+ props.Parameters.Add(UdapConstants.Discovery.AuthorizationEndpoint, openIdConfig.AuthorizeEndpoint);
+ props.Parameters.Add(UdapConstants.Discovery.TokenEndpoint, openIdConfig.TokenEndpoint);
+ props.Parameters.Add("idpBaseUrl", idpBaseUrl.TrimEnd('/'));
+
+ return props;
+ }
+}
diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthPostConfigureOptions.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthPostConfigureOptions.cs
index e1e2413b..12ce9429 100644
--- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthPostConfigureOptions.cs
+++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthPostConfigureOptions.cs
@@ -7,6 +7,8 @@
// */
#endregion
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Options;
using Udap.Client.Client;
@@ -16,18 +18,21 @@ public class TieredOAuthPostConfigureOptions : IPostConfigureOptions
/// Initializes a new instance of the class.
///
///
- public TieredOAuthPostConfigureOptions(UdapClientMessageHandler udapClientMessageHandler)
+ ///
+ public TieredOAuthPostConfigureOptions(UdapClientMessageHandler udapClientMessageHandler, IDataProtectionProvider dataProtection)
{
_udapClientMessageHandler = udapClientMessageHandler;
+ _dataProtection = dataProtection;
}
///
- /// Invoked to configure a instance.
+ /// Invoked to configure a instance.
///
/// The name of the options instance being configured.
/// The options instance to configured.
@@ -36,6 +41,36 @@ public void PostConfigure(string? name, TieredOAuthAuthenticationOptions options
//TODO Register _udapClientMessageHandler events for logging
options.BackchannelHttpHandler = _udapClientMessageHandler;
- options.SignInScheme = options.SignInScheme;
+ options.DataProtectionProvider ??= _dataProtection;
+
+ if (options.StateDataFormat == null)
+ {
+ var dataProtector = options.DataProtectionProvider.CreateProtector(
+ typeof(TieredOAuthAuthenticationHandler).FullName!, name ?? throw new ArgumentNullException(nameof(name)));
+ options.StateDataFormat = new PropertiesDataFormat(dataProtector);
+ }
+
+
+
+ //
+ // If I go down the path of OpendIdConnectHandler remember to visit the OpenIdConnectPostConfigureOptions source for guidance
+ //
+ // if (options.StateDataFormat == null)
+ // {
+ // var dataProtector = options.DataProtectionProvider.CreateProtector(
+ // typeof(OpenIdConnectHandler).FullName!, name, "v1");
+ // options.StateDataFormat = new PropertiesDataFormat(dataProtector);
+ // }
+ //
+ // if (options.StringDataFormat == null)
+ // {
+ // var dataProtector = options.DataProtectionProvider.CreateProtector(
+ // typeof(OpenIdConnectHandler).FullName!,
+ // typeof(string).FullName!,
+ // name,
+ // "v1");
+ //
+ // options.StringDataFormat = new SecureDataFormat(new StringSerializer(), dataProtector);
+ // }
}
}
\ No newline at end of file
diff --git a/Udap.Server/Udap.Server.csproj b/Udap.Server/Udap.Server.csproj
index cb16ff0a..54ed11da 100644
--- a/Udap.Server/Udap.Server.csproj
+++ b/Udap.Server/Udap.Server.csproj
@@ -31,15 +31,7 @@
-
-
-
-
-
-
-
-
-
+
diff --git a/Udap.Server/Validation/Default/DefaultScopeExpander.cs b/Udap.Server/Validation/Default/DefaultScopeExpander.cs
index 549bf32f..ee5b466d 100644
--- a/Udap.Server/Validation/Default/DefaultScopeExpander.cs
+++ b/Udap.Server/Validation/Default/DefaultScopeExpander.cs
@@ -7,9 +7,12 @@
// */
#endregion
+using Duende.IdentityServer.Models;
+
namespace Udap.Server.Validation.Default;
public class DefaultScopeExpander : IScopeExpander
{
+
///
/// Default implementation of IScopeExpander. It does nothing.
///
@@ -20,12 +23,23 @@ public IEnumerable Expand(IEnumerable scopes)
return scopes;
}
+ ///
+ /// If the a wildcard is present such as a * then the implementation should expand accordingly.
+ ///
+ ///
+ ///
+ ///
+ public IEnumerable WildCardExpand(ICollection clientScopes, ICollection apiScopes)
+ {
+ return clientScopes;
+ }
+
///
/// Shrinks scope parameters.
///
///
///
- public IEnumerable Shrink(IEnumerable scopes)
+ public IEnumerable Aggregate(IEnumerable scopes)
{
return scopes;
}
diff --git a/Udap.Server/Validation/HL7SmartScopeExpander.cs b/Udap.Server/Validation/HL7SmartScopeExpander.cs
new file mode 100644
index 00000000..0ed12ad9
--- /dev/null
+++ b/Udap.Server/Validation/HL7SmartScopeExpander.cs
@@ -0,0 +1,205 @@
+#region (c) 2023 Joseph Shook. All rights reserved.
+// /*
+// Authors:
+// Joseph Shook Joseph.Shook@Surescripts.com
+//
+// See LICENSE in the project root for license information.
+// */
+#endregion
+
+using System.Text.RegularExpressions;
+using Duende.IdentityServer.Models;
+using Udap.Common.Extensions;
+
+namespace Udap.Server.Validation;
+
+///
+/// Implements rules to expand scopes where the scope parameter part may represent an encoded set of scopes.
+///
+/// From HL7 FHIR SMART v2 the parameters portion of system/Patient.crud can be expanded to discrete scopes. For example::
+///
+/// system/Patient.c
+/// system/Patient.r
+/// system/Patient.u
+/// system/Patient.d
+///
+///
+public class HL7SmartScopeExpander : IScopeExpander
+{
+
+ ///
+ /// Expands scope parameters to a set of discrete scopes.
+ /// Implement logic to determine if a scope represents a pattern that can be expanded.
+ ///
+ /// The scope parameter value.
+ /// A set of discrete scopes.
+ public IEnumerable Expand(IEnumerable scopes)
+ {
+ var expandedScopes = new HashSet();
+
+ foreach (var scope in scopes.ToList())
+ {
+ var smartV2Regex = new Regex(@"^(system|user|patient)[\/].*\.[cruds]+$");
+ var smartV2Matches = smartV2Regex.Matches(scope);
+
+ var smartV1Regex = new Regex(@"^(system|user|patient)[\/].*\.(read|write)$");
+ var smartV1Matches = smartV1Regex.Matches(scope);
+
+ if (smartV2Matches.Any()) // Expand SMART V2 Scopes
+ {
+ foreach (Match match in smartV2Matches)
+ {
+ var value = match.Value;
+ var parts = value.Split('.');
+
+ var combinations = ScopeExtensions.GenerateCombinations(parts[1]);
+
+ foreach (var parameter in combinations)
+ {
+ expandedScopes.Add($"{parts[0]}.{parameter}");
+ }
+ }
+ }
+ else if (smartV1Matches.Any()) // Just keep SMART V1 scopes
+ {
+ expandedScopes.Add(scope);
+ }
+ else
+ {
+ if (!scope.Contains('*'))
+ {
+ expandedScopes.Add(scope);
+ }
+ }
+ }
+
+ return expandedScopes;
+ }
+
+ public IEnumerable ExpandToApiScopes(string scope)
+ {
+ var expandedScopes = Expand(new List { scope.Trim() });
+
+ return expandedScopes.Select(s => new ApiScope(s));
+ }
+
+ ///
+ /// Expand * resources.
+ ///
+ ///
+ ///
+ ///
+ public IEnumerable WildCardExpand(ICollection clientScopes, ICollection apiScopes)
+ {
+ var expandedScopes = new HashSet();
+
+ foreach (var scope in clientScopes.Where(s => s.Contains('*')))
+ {
+ var smartV2Regex = new Regex(@"^(system|user|patient)\/\*\.[cruds]+$");
+ var smartV2Matches = smartV2Regex.Matches(scope);
+
+ var smartV1Regex = new Regex(@"^(system|user|patient)\/\*\.(read|write)$");
+ var smartV1Matches = smartV1Regex.Matches(scope);
+
+ if (smartV2Matches.Any())
+ {
+ foreach (Match match in smartV2Matches)
+ {
+ var value = match.Value;
+ var parts = value.Split('*');
+
+ var specificScopes = apiScopes.Where(s => s.StartsWith(parts[0]) &&
+ s.EndsWith(parts[1]));
+
+ foreach (var specificScope in specificScopes)
+ {
+ expandedScopes.Add(specificScope);
+ }
+ }
+ }
+ else if (smartV1Matches.Any())
+ {
+ foreach (Match match in smartV1Matches)
+ {
+ var value = match.Value;
+ var parts = value.Split('*');
+
+ var specificScopes = apiScopes.
+ Where(s => s.StartsWith(parts[0]) &&
+ s.EndsWith(parts[1]));
+
+ foreach (var specificScope in specificScopes)
+ {
+ expandedScopes.Add(specificScope);
+ }
+ }
+ }
+ else
+ {
+ expandedScopes.Add(scope);
+ }
+ }
+
+ foreach (var scope in clientScopes.Where(s => !s.Contains('*')))
+ {
+ expandedScopes.Add(scope);
+ }
+
+ return expandedScopes;
+ }
+
+ ///
+ /// Shrinks scope parameters.
+ ///
+ ///
+ ///
+ public IEnumerable Aggregate(IEnumerable scopes)
+ {
+ var matchedScopes = new HashSet();
+ var unmatchedScopes = new HashSet();
+ //todo cache this
+ var fixedOrder = new List { "c", "r", "u", "d", "s" };
+
+ foreach (var scope in scopes)
+ {
+ var regex = new Regex("^(system|user|patient)[\\/].*\\.[c|r|u|d|s]+$");
+ if (regex.IsMatch(scope))
+ {
+ matchedScopes.Add(scope);
+ }
+ else
+ {
+ unmatchedScopes.Add(scope);
+ }
+ }
+
+ var scopeGroups = matchedScopes
+ .Select(s =>
+ {
+ var parts = s.Split('.');
+
+ return (parts[0], parts[1]);
+ })
+ .GroupBy((g) => (g.Item1, g.Item2))
+ ;
+
+ var shrunkScopes = new HashSet();
+
+ foreach (var scopeGroup in scopeGroups
+ .GroupBy(sc => sc.Key.Item1)
+ .Select(sc => sc))
+ {
+ string groupingPrefix = scopeGroup.First().Key.Item1;
+ string groupingSuffix = scopeGroup.OrderByDescending(g => g.Key.Item2.Length).First().Key.Item2;
+
+ shrunkScopes.Add($"{groupingPrefix}.{groupingSuffix}");
+ }
+
+ foreach (var unmatchedScope in unmatchedScopes)
+ {
+ shrunkScopes.Add(unmatchedScope);
+ }
+
+ return shrunkScopes.OrderBy(s => s);
+ }
+}
\ No newline at end of file
diff --git a/Udap.Server/Validation/IScopeExpander.cs b/Udap.Server/Validation/IScopeExpander.cs
index 371d2ec8..54bd713a 100644
--- a/Udap.Server/Validation/IScopeExpander.cs
+++ b/Udap.Server/Validation/IScopeExpander.cs
@@ -27,10 +27,20 @@ public interface IScopeExpander
/// A set of discrete scopes.
IEnumerable Expand(IEnumerable scopes);
+
+ ///
+ /// If the a wildcard is present such as a * then the implementation should expand accordingly.
+ ///
+ /// Client requested scopes
+ /// Scopes supported by server
+ ///
+ IEnumerable WildCardExpand(ICollection clientScopes, ICollection apiScopes);
+
+
///
/// Shrinks scope parameters.
///
///
///
- IEnumerable Shrink(IEnumerable scopes);
+ IEnumerable Aggregate(IEnumerable scopes);
}
diff --git a/Udap.Server/Validation/SmartV2Expander.cs b/Udap.Server/Validation/SmartV2Expander.cs
deleted file mode 100644
index b99c3504..00000000
--- a/Udap.Server/Validation/SmartV2Expander.cs
+++ /dev/null
@@ -1,138 +0,0 @@
-#region (c) 2023 Joseph Shook. All rights reserved.
-// /*
-// Authors:
-// Joseph Shook Joseph.Shook@Surescripts.com
-//
-// See LICENSE in the project root for license information.
-// */
-#endregion
-
-using System.Text.RegularExpressions;
-using Duende.IdentityServer.Models;
-using Udap.Common.Extensions;
-
-namespace Udap.Server.Validation;
-
-///
-/// Implements rules to expand scopes where the scope parameter part may represent an encoded set of scopes.
-///
-/// From HL7 FHIR SMART v2 the parameters portion of system/Patient.crud can be expanded to discrete scopes. For example::
-///
-/// system/Patient.c
-/// system/Patient.r
-/// system/Patient.u
-/// system/Patient.d
-///
-///
-public class SmartV2Expander : IScopeExpander
-{
-
- ///
- /// Expands scope parameters to a set of discrete scopes.
- /// Implement logic to determine if a scope represents a pattern that can be expanded.
- ///
- /// The scope parameter value.
- /// A set of discrete scopes.
- public IEnumerable Expand(IEnumerable scopes)
- {
- var expandedScopes = new HashSet();
-
- foreach (var scope in scopes.ToList())
- {
- var regex = new Regex("^(system|user|patient)[\\/].*\\.[cruds]+$");
- var matches = regex.Matches(scope);
-
- if (matches.Any())
- {
- foreach (Match match in matches)
- {
- var value = match.Value;
- var parts = value.Split('.');
-
- var combinations = ScopeExtensions.GenerateCombinations(parts[1]);
-
- foreach (var parameter in combinations)
- {
- expandedScopes.Add($"{parts[0]}.{parameter}");
- }
- }
- }
- else
- {
- expandedScopes.Add(scope);
- }
- }
-
- return expandedScopes;
- }
-
- public IEnumerable ExpandToApiScopes(string scope)
- {
- var expandedScopes = Expand(new List { scope });
-
- return expandedScopes.Select(s => new ApiScope(s));
- }
-
-
- ///
- /// Shrinks scope parameters.
- ///
- ///
- ///
- public IEnumerable Shrink(IEnumerable scopes)
- {
- var matchedScopes = new HashSet();
- var unmatchedScopes = new HashSet();
- //todo cache this
- var fixedOrder = new List { "c", "r", "u", "d", "s" };
-
- foreach (var scope in scopes)
- {
- var regex = new Regex("^(system|user|patient)[\\/].*\\.[c|r|u|d|s]+$");
- if (regex.IsMatch(scope))
- {
- matchedScopes.Add(scope);
- }
- else
- {
- unmatchedScopes.Add(scope);
- }
- }
-
- var scopeGroups = matchedScopes
- .Select(s =>
- {
- var parts = s.Split('.');
-
- return (parts[0], parts[1]);
- })
- .GroupBy((g) => (g.Item1, g.Item2))
- ;
-
- var shrunkScopes = new HashSet();
-
- foreach (var scopeGroup in scopeGroups
- .GroupBy(sc => sc.Key.Item1)
- .Select(sc => sc))
- {
- string groupingPrefix = scopeGroup.First().Key.Item1;
- string groupingSuffix = "";
-
- foreach (var item in scopeGroup
- .OrderBy(sg =>
- {
- var index = fixedOrder.IndexOf(sg.Key.Item2);
- return index == -1 ? int.MaxValue : index;
- }))
- {
- groupingSuffix += item.Key.Item2;
- }
-
- shrunkScopes.Add($"{groupingPrefix}.{groupingSuffix}");
- }
-
- shrunkScopes.ToList().AddRange(unmatchedScopes.ToList());
-
- return shrunkScopes;
- }
-}
\ No newline at end of file
diff --git a/_tests/Directory.Packages.props b/_tests/Directory.Packages.props
index dc983d1d..05ace284 100644
--- a/_tests/Directory.Packages.props
+++ b/_tests/Directory.Packages.props
@@ -12,23 +12,23 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
-
-
+
+
-
+
\ No newline at end of file
diff --git a/_tests/Udap.PKI.Generator/BuildTestCerts.cs b/_tests/Udap.PKI.Generator/BuildTestCerts.cs
index 8b9ecdc3..a8e7f80f 100644
--- a/_tests/Udap.PKI.Generator/BuildTestCerts.cs
+++ b/_tests/Udap.PKI.Generator/BuildTestCerts.cs
@@ -1175,6 +1175,23 @@ public void MakeCaWithIntermediateUdapForLocalhostCommunity(
$"http://host.docker.internal:5033/certs/{intermediateName}.cer"
);
}
+
+ if (issuedName == "fhirLabsApiClientLocalhostCert2")
+ {
+ BuildClientCertificate(
+ intermediateCert,
+ caCert,
+ intermediate,
+ "CN=idpserver2", //issuedDistinguishedName
+ new List
+ {
+ "https://idpserver2",
+ },
+ $"{LocalhostUdapIssued}/idpserver2",
+ $"{LocalhostCdp}/{intermediateName}.crl",
+ $"http://host.docker.internal:5033/certs/{intermediateName}.cer"
+ );
+ }
}
@@ -1271,13 +1288,14 @@ public void MakeCaWithIntermediateUdapForLocalhostCommunity(
$"{BaseDir}/../../examples/Udap.Identity.Provider/CertStore/issued/{issuedName}.pfx",
true);
- // Udap.Server.Tests :: Identity Provider
+ // Udap.Server.Tests :: Identity Provider 1
if (issuedName == "fhirLabsApiClientLocalhostCert")
{
File.Copy($"{LocalhostUdapIssued}/idpserver.pfx",
$"{BaseDir}/../../_tests/UdapServer.Tests/CertStore/issued/idpserver.pfx",
true);
+
File.Copy($"{communityStorePath}/{anchorName}.cer",
$"{BaseDir}/../../_tests/UdapServer.Tests/CertStore/anchors/{anchorName}.cer",
true);
@@ -1286,6 +1304,20 @@ public void MakeCaWithIntermediateUdapForLocalhostCommunity(
true);
}
+ // Udap.Server.Tests :: Identity Provider 2
+ if (issuedName == "fhirLabsApiClientLocalhostCert2")
+ {
+ File.Copy($"{LocalhostUdapIssued}/idpserver2.pfx",
+ $"{BaseDir}/../../_tests/UdapServer.Tests/CertStore/issued/idpserver2.pfx",
+ true);
+
+ File.Copy($"{communityStorePath}/{anchorName}.cer",
+ $"{BaseDir}/../../_tests/UdapServer.Tests/CertStore/anchors/{anchorName}.cer",
+ true);
+ File.Copy($"{LocalhostUdapIntermediates}/{intermediateName}.cer",
+ $"{BaseDir}/../../_tests/UdapServer.Tests/CertStore/intermediates/{intermediateName}.cer",
+ true);
+ }
// Udap.Identity.Provider.2 :: Second Identity Provider
if (issuedName == "fhirLabsApiClientLocalhostCert2")
diff --git a/_tests/UdapServer.Tests/Common/TestExtensions.cs b/_tests/UdapServer.Tests/Common/TestExtensions.cs
index 13a058d5..7ea592db 100644
--- a/_tests/UdapServer.Tests/Common/TestExtensions.cs
+++ b/_tests/UdapServer.Tests/Common/TestExtensions.cs
@@ -1,13 +1,21 @@
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.Extensions.Options;
+#region (c) 2023 Joseph Shook. All rights reserved.
+// /*
+// Authors:
+// Joseph Shook Joseph.Shook@Surescripts.com
+//
+// See LICENSE in the project root for license information.
+// */
+#endregion
+
+using System.Diagnostics;
+using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
-using Udap.Client.Client;
-using Udap.Server.Security.Authentication.TieredOAuth;
-using Udap.Common.Certificates;
using Microsoft.Extensions.Logging;
-using FluentAssertions.Common;
+using Microsoft.Extensions.Options;
+using Udap.Client.Client;
using Udap.Client.Configuration;
+using Udap.Server.Security.Authentication.TieredOAuth;
namespace UdapServer.Tests.Common;
public static class TestExtensions
@@ -18,19 +26,47 @@ public static class TestExtensions
///
/// The authentication builder.
/// The delegate used to configure the Tiered OAuth options.
- /// Inject delegating handler attached to HttpClient
+ /// Wire httpClient to WebHostBuilder test harness
+ /// Wire httpClient to WebHostBuilder test harness
/// The .
public static AuthenticationBuilder AddTieredOAuthForTests(
this AuthenticationBuilder builder,
Action configuration,
- UdapIdentityServerPipeline pipeline)
+ UdapAuthServerPipeline authServerPipeline,
+ UdapIdentityServerPipeline pipelineIdp1,
+ UdapIdentityServerPipeline pipelineIdp2)
{
builder.Services.AddScoped(sp =>
- new UdapClient(
- pipeline.BrowserClient,
+ {
+ var dynamicIdp = sp.GetRequiredService();
+
+ if (dynamicIdp.Name == "https://idpserver")
+ {
+ Debug.Assert(pipelineIdp1.BackChannelClient != null, "pipelineIdp1.BackChannelClient != null");
+ return new UdapClient(
+ pipelineIdp1.BackChannelClient,
+ sp.GetRequiredService(),
+ sp.GetRequiredService>(),
+ sp.GetRequiredService>());
+ }
+
+ if (dynamicIdp.Name == "https://idpserver2")
+ {
+ Debug.Assert(pipelineIdp2.BackChannelClient != null, "pipelineIdp2.BackChannelClient != null");
+ return new UdapClient(
+ pipelineIdp2.BackChannelClient,
+ sp.GetRequiredService(),
+ sp.GetRequiredService>(),
+ sp.GetRequiredService>());
+ }
+
+ return new UdapClient(
+ authServerPipeline.BrowserClient,
sp.GetRequiredService(),
sp.GetRequiredService>(),
- sp.GetRequiredService>()));
+ sp.GetRequiredService>());
+
+ });
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
diff --git a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs
index f28452ca..958f9b7a 100644
--- a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs
+++ b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs
@@ -17,7 +17,6 @@
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Events;
using Duende.IdentityServer.Models;
-using Duende.IdentityServer.ResponseHandling;
using Duende.IdentityServer.Services;
using Duende.IdentityServer.Test;
using FluentAssertions;
@@ -35,15 +34,14 @@
using Microsoft.Extensions.Options;
using Moq;
using Udap.Auth.Server.Pages;
+using Udap.Client.Client;
using Udap.Common;
using Udap.Common.Certificates;
using Udap.Common.Models;
using Udap.Server.Configuration.DependencyInjection;
using Udap.Server.Registration;
-using Udap.Server.ResponseHandling;
using Udap.Server.Security.Authentication.TieredOAuth;
using UnitTests.Common;
-using AuthorizeResponse = IdentityModel.Client.AuthorizeResponse;
using Constants = Udap.Server.Constants;
namespace UdapServer.Tests.Common;
@@ -89,19 +87,19 @@ public class UdapAuthServerPipeline
public BrowserClient BrowserClient { get; set; }
public HttpClient BackChannelClient { get; set; }
- public MockMessageHandler BackChannelMessageHandler { get; set; } = new MockMessageHandler();
- public MockMessageHandler JwtRequestMessageHandler { get; set; } = new MockMessageHandler();
+ public MockMessageHandler BackChannelMessageHandler { get; set; } = new();
+ public MockMessageHandler JwtRequestMessageHandler { get; set; } = new();
public TestEventService EventService = new TestEventService();
- public event Action OnPreConfigureServices = (ctx, services) => { };
- public event Action OnPostConfigureServices = services => { };
- public event Action OnPreConfigure = app => { };
- public event Action OnPostConfigure = app => { };
+ public event Action OnPreConfigureServices = (_, _) => { };
+ public event Action OnPostConfigureServices = _ => { };
+ public event Action OnPreConfigure = _ => { };
+ public event Action OnPostConfigure = _ => { };
- public Func> OnFederatedSignout;
+ public Func>? OnFederatedSignOut;
- public void Initialize(string basePath = null, bool enableLogging = false)
+ public void Initialize(string? basePath = null, bool enableLogging = false)
{
var builder = new WebHostBuilder();
builder.ConfigureServices(ConfigureServices);
@@ -120,11 +118,11 @@ public void Initialize(string basePath = null, bool enableLogging = false)
}
});
- builder.ConfigureAppConfiguration(configure => configure.AddJsonFile("appsettings.json"));
+ builder.ConfigureAppConfiguration(configure => configure.AddJsonFile("appsettings.Auth.json"));
if (enableLogging)
{
- builder.ConfigureLogging((ctx, b) =>
+ builder.ConfigureLogging((_, b) =>
{
b.AddConsole(c => c.LogToStandardErrorThreshold = LogLevel.Debug);
b.SetMinimumLevel(LogLevel.Trace);
@@ -146,6 +144,8 @@ public void ConfigureServices(WebHostBuilderContext builder, IServiceCollection
OnPreConfigureServices(builder, services);
+ services.AddSingleton();
+
// services.AddAuthentication(opts =>
// {
// opts.AddScheme("external", scheme =>
@@ -157,21 +157,22 @@ public void ConfigureServices(WebHostBuilderContext builder, IServiceCollection
services.AddTransient(svcs =>
{
var handler = new MockExternalAuthenticationHandler(svcs.GetRequiredService());
- if (OnFederatedSignout != null) handler.OnFederatedSignout = OnFederatedSignout;
+ if (OnFederatedSignOut != null) handler.OnFederatedSignout = OnFederatedSignOut;
return handler;
});
services.AddSingleton(sp =>
new TrustAnchorFileStore(
sp.GetRequiredService>(),
- new Mock>().Object));
-
+ new Mock>().Object));
+
services.AddUdapServer(BaseUrl, "FhirLabsApi")
.AddUdapInMemoryApiScopes(ApiScopes)
.AddInMemoryUdapCertificates(Communities)
- .AddUdapResponseGenerators()
- .AddSmartV2Expander();
+ .AddUdapResponseGenerators();
+ // .AddTieredOAuthDynamicProvider();
+ //.AddSmartV2Expander();
services.AddIdentityServer(options =>
@@ -268,13 +269,25 @@ public void ConfigureApp(IApplicationBuilder app)
path.Run(async ctx => await OnExternalLoginCallback(ctx, new Mock().Object));
});
+ app.Map("/udap.logo.48x48.png", path =>
+ {
+ path.Run(ctx => OnLogo(ctx));
+ });
+
OnPostConfigure(app);
}
-
+
+ private Task OnLogo(HttpContext context)
+ {
+ context.Response.ContentType = "image/png";
+ context.Response.StatusCode = (int)HttpStatusCode.OK;
+ return Task.CompletedTask;
+ }
+
public bool LoginWasCalled { get; set; }
public string LoginReturnUrl { get; set; }
public AuthorizationRequest LoginRequest { get; set; }
- public ClaimsPrincipal Subject { get; set; }
+ public ClaimsPrincipal? Subject { get; set; }
private async Task OnRegister(HttpContext ctx)
{
@@ -292,28 +305,20 @@ private async Task OnExternalLoginChallenge(HttpContext ctx)
//TODO: factor this code into library code and share with the Challenge.cshtml.cs file
var interactionService = ctx.RequestServices.GetRequiredService();
var returnUrl = ctx.Request.Query["returnUrl"].FirstOrDefault();
-
- if (interactionService.IsValidReturnUrl(returnUrl) == false)
- {
- throw new Exception("invalid return URL");
- }
+ var scheme = ctx.Request.Query["scheme"];
+ var udapClient = ctx.RequestServices.GetService();
- var scheme = ctx.Request.Query["scheme"].FirstOrDefault();
- ;
- var props = new AuthenticationProperties
- {
- RedirectUri = "/externallogin/callback",
-
- Items =
- {
- { "returnUrl", returnUrl },
- { "scheme", scheme },
- }
- };
+ var props = await TieredOAuthHelpers.BuildDynamicTieredOAuthOptions(
+ interactionService,
+ udapClient,
+ scheme.ToString(),
+ "/externallogin/callback",
+ returnUrl);
// When calling ChallengeAsync your handler will be called if it is registered.
- await ctx.ChallengeAsync(TieredOAuthAuthenticationDefaults.AuthenticationScheme, props);
+ await ctx.ChallengeAsync(scheme, props);
}
+
private async Task OnExternalLoginCallback(HttpContext ctx, ILogger logger)
{
@@ -324,7 +329,7 @@ private async Task OnExternalLoginCallback(HttpContext ctx, ILogger logger)
// read external identity from the temporary cookie
var result = await ctx.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
- if (result?.Succeeded != true)
+ if (result.Succeeded != true)
{
throw new Exception("External authentication error");
}
@@ -647,4 +652,9 @@ public T Resolve()
queryParams.TryGetValue("state", out var state);
return state.SingleOrDefault();
}
+}
+
+public class DynamicIdp
+{
+ public string? Name { get; set; }
}
\ No newline at end of file
diff --git a/_tests/UdapServer.Tests/Common/UdapIdentityServerPipeline.cs b/_tests/UdapServer.Tests/Common/UdapIdentityServerPipeline.cs
index ffbd7328..ba3ef017 100644
--- a/_tests/UdapServer.Tests/Common/UdapIdentityServerPipeline.cs
+++ b/_tests/UdapServer.Tests/Common/UdapIdentityServerPipeline.cs
@@ -12,24 +12,19 @@
// See LICENSE in the project root for license information.
-
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
-using System.Security.Policy;
using System.Text.Json;
using Duende.IdentityServer;
using Duende.IdentityServer.Configuration;
-using Duende.IdentityServer.Extensions;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;
using Duende.IdentityServer.Test;
using FluentAssertions;
-using Google.Api;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
@@ -39,63 +34,93 @@
using Microsoft.Extensions.Logging;
using Udap.Common.Certificates;
using Udap.Common.Models;
-using Udap.Model;
using Udap.Server;
using Udap.Server.Configuration.DependencyInjection;
using Udap.Server.Registration;
using Udap.Server.Security.Authentication.TieredOAuth;
-using Udap.Server.Stores.InMemory;
namespace UdapServer.Tests.Common;
public class UdapIdentityServerPipeline
{
- public const string BaseUrl = "https://idpserver";
- public const string LoginPage = BaseUrl + "/account/login";
- public const string LogoutPage = BaseUrl + "/account/logout";
- public const string ConsentPage = BaseUrl + "/account/consent";
- public const string ErrorPage = BaseUrl + "/home/error";
-
- public const string DeviceAuthorization = BaseUrl + "/connect/deviceauthorization";
- public const string DiscoveryEndpoint = BaseUrl + "/.well-known/openid-configuration";
- public const string DiscoveryKeysEndpoint = BaseUrl + "/.well-known/openid-configuration/jwks";
- public const string AuthorizeEndpoint = BaseUrl + "/connect/authorize";
- public const string BackchannelAuthenticationEndpoint = BaseUrl + "/connect/ciba";
- public const string TokenEndpoint = BaseUrl + "/connect/token";
- public const string RevocationEndpoint = BaseUrl + "/connect/revocation";
- public const string UserInfoEndpoint = BaseUrl + "/connect/userinfo";
- public const string IntrospectionEndpoint = BaseUrl + "/connect/introspect";
- public const string IdentityTokenValidationEndpoint = BaseUrl + "/connect/identityTokenValidation";
- public const string EndSessionEndpoint = BaseUrl + "/connect/endsession";
- public const string EndSessionCallbackEndpoint = BaseUrl + "/connect/endsession/callback";
- public const string CheckSessionEndpoint = BaseUrl + "/connect/checksession";
- public const string RegistrationEndpoint = BaseUrl + "/connect/register";
-
- public const string FederatedSignOutPath = "/signout-oidc";
- public const string FederatedSignOutUrl = BaseUrl + FederatedSignOutPath;
-
- public IdentityServerOptions Options { get; set; }
+ public string BaseUrl => _baseUrl;
+ private readonly string _baseUrl = "https://idpserver";
+ private readonly string? _appSettingsFile;
+
+ public readonly string LoginPage;
+ public readonly string LogoutPage;
+ public readonly string ConsentPage;
+ public readonly string ErrorPage;
+ public readonly string DeviceAuthorization;
+ public readonly string DiscoveryEndpoint;
+ public readonly string DiscoveryKeysEndpoint;
+ public readonly string AuthorizeEndpoint;
+ public readonly string BackchannelAuthenticationEndpoint;
+ public readonly string TokenEndpoint;
+ public readonly string RevocationEndpoint;
+ public readonly string UserInfoEndpoint;
+ public readonly string IntrospectionEndpoint;
+ public readonly string IdentityTokenValidationEndpoint;
+ public readonly string EndSessionEndpoint;
+ public readonly string EndSessionCallbackEndpoint;
+ public readonly string CheckSessionEndpoint;
+ public readonly string RegistrationEndpoint;
+ public readonly string FederatedSignOutPath;
+ public readonly string FederatedSignOutUrl;
+
+ public UdapIdentityServerPipeline(string? baseUrl = null, string? appSettingsFile = null)
+ {
+ _baseUrl = baseUrl ?? _baseUrl;
+ _appSettingsFile = appSettingsFile;
+
+ LoginPage = BaseUrl + "/account/login";
+ LogoutPage = BaseUrl + "/account/logout";
+ ConsentPage = BaseUrl + "/account/consent";
+ ErrorPage = BaseUrl + "/home/error";
+
+ DeviceAuthorization = BaseUrl + "/connect/deviceauthorization";
+ DiscoveryEndpoint = BaseUrl + "/.well-known/openid-configuration";
+ DiscoveryKeysEndpoint = BaseUrl + "/.well-known/openid-configuration/jwks";
+ AuthorizeEndpoint = BaseUrl + "/connect/authorize";
+ BackchannelAuthenticationEndpoint = BaseUrl + "/connect/ciba";
+ TokenEndpoint = BaseUrl + "/connect/token";
+ RevocationEndpoint = BaseUrl + "/connect/revocation";
+ UserInfoEndpoint = BaseUrl + "/connect/userinfo";
+ IntrospectionEndpoint = BaseUrl + "/connect/introspect";
+ IdentityTokenValidationEndpoint = BaseUrl + "/connect/identityTokenValidation";
+ EndSessionEndpoint = BaseUrl + "/connect/endsession";
+ EndSessionCallbackEndpoint = BaseUrl + "/connect/endsession/callback";
+ CheckSessionEndpoint = BaseUrl + "/connect/checksession";
+ RegistrationEndpoint = BaseUrl + "/connect/register";
+ FederatedSignOutPath = "/signout-oidc";
+ FederatedSignOutUrl = BaseUrl + FederatedSignOutPath;
+ }
+
+
+
+ public IdentityServerOptions? Options { get; set; }
public List Clients { get; set; } = new List();
public List IdentityScopes { get; set; } = new List();
public List ApiResources { get; set; } = new List();
public List ApiScopes { get; set; } = new List();
public List Users { get; set; } = new List();
public List Communities { get; set; } = new List();
- public TestServer Server { get; set; }
- public HttpMessageHandler Handler { get; set; }
+ public TestServer? Server { get; set; }
+ public HttpMessageHandler? Handler { get; set; }
- public BrowserClient BrowserClient { get; set; }
- public HttpClient BackChannelClient { get; set; }
+ public BrowserClient? BrowserClient { get; set; }
+ public HttpClient? BackChannelClient { get; set; }
public MockMessageHandler BackChannelMessageHandler { get; set; } = new MockMessageHandler();
public MockMessageHandler JwtRequestMessageHandler { get; set; } = new MockMessageHandler();
- public event Action OnPreConfigureServices = (ctx, services) => { };
- public event Action OnPostConfigureServices = services => { };
- public event Action OnPreConfigure = app => { };
- public event Action OnPostConfigure = app => { };
+ public event Action OnPreConfigureServices = (_, _) => { };
+ public event Action OnPostConfigureServices = _ => { };
+ public event Action OnPreConfigure = _ => { };
+ public event Action OnPostConfigure = _ => { };
- public Func> OnFederatedSignout;
+ public Func>? OnFederatedSignout;
+
public void Initialize(string basePath = null, bool enableLogging = false)
{
@@ -116,7 +141,7 @@ public void Initialize(string basePath = null, bool enableLogging = false)
}
});
- builder.ConfigureAppConfiguration(configure => configure.AddJsonFile("appsettings.json"));
+ builder.ConfigureAppConfiguration(configure => configure.AddJsonFile(_appSettingsFile ?? "appsettings.Idp1.json"));
if (enableLogging)
{
@@ -298,9 +323,9 @@ public void ConfigureApp(IApplicationBuilder app)
}
public bool LoginWasCalled { get; set; }
- public string LoginReturnUrl { get; set; }
- public AuthorizationRequest LoginRequest { get; set; }
- public ClaimsPrincipal Subject { get; set; }
+ public string? LoginReturnUrl { get; set; }
+ public AuthorizationRequest? LoginRequest { get; set; }
+ public ClaimsPrincipal? Subject { get; set; }
private async Task OnRegister(HttpContext ctx)
{
@@ -371,7 +396,7 @@ private async Task IssueLoginCookie(HttpContext ctx)
}
public bool LogoutWasCalled { get; set; }
- public LogoutRequest LogoutRequest { get; set; }
+ public LogoutRequest? LogoutRequest { get; set; }
private async Task OnLogout(HttpContext ctx)
{
@@ -387,8 +412,8 @@ private async Task ReadLogoutRequest(HttpContext ctx)
}
public bool ConsentWasCalled { get; set; }
- public AuthorizationRequest ConsentRequest { get; set; }
- public ConsentResponse ConsentResponse { get; set; }
+ public AuthorizationRequest? ConsentRequest { get; set; }
+ public ConsentResponse? ConsentResponse { get; set; }
private async Task OnConsent(HttpContext ctx)
{
@@ -418,7 +443,7 @@ private async Task CreateConsentResponse(HttpContext ctx)
}
public bool CustomWasCalled { get; set; }
- public AuthorizationRequest CustomRequest { get; set; }
+ public AuthorizationRequest? CustomRequest { get; set; }
private async Task OnCustom(HttpContext ctx)
{
@@ -428,13 +453,13 @@ private async Task OnCustom(HttpContext ctx)
}
public bool ErrorWasCalled { get; set; }
- public ErrorMessage ErrorMessage { get; set; }
- public IServiceProvider ApplicationServices { get; private set; }
+ public ErrorMessage? ErrorMessage { get; set; }
+ public IServiceProvider? ApplicationServices { get; private set; }
///
/// Record the backchannel Identity Token during Tiered OAuth
///
- public JwtSecurityToken IdToken { get; set; }
+ public JwtSecurityToken? IdToken { get; set; }
private async Task OnError(HttpContext ctx)
{
diff --git a/_tests/UdapServer.Tests/Conformance/Basic/ClientCredentialsUdapModeTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/ClientCredentialsUdapModeTests.cs
index 5f4e9a96..d98b72cd 100644
--- a/_tests/UdapServer.Tests/Conformance/Basic/ClientCredentialsUdapModeTests.cs
+++ b/_tests/UdapServer.Tests/Conformance/Basic/ClientCredentialsUdapModeTests.cs
@@ -18,7 +18,11 @@
using IdentityModel;
using IdentityModel.Client;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
+using Moq;
+using Udap.Client.Client;
using Udap.Client.Client.Extensions;
using Udap.Client.Configuration;
using Udap.Common.Models;
@@ -50,20 +54,33 @@ public ClientCredentialsUdapModeTests(ITestOutputHelper testOutputHelper)
var anchorCommunity2 = new X509Certificate2("CertStore/anchors/caLocalhostCert2.cer");
var intermediateCommunity2 = new X509Certificate2("CertStore/intermediates/intermediateLocalhostCert2.cer");
- _mockPipeline.OnPostConfigureServices += s =>
+ _mockPipeline.OnPostConfigureServices += services =>
{
- s.AddSingleton(new ServerSettings
+ services.AddSingleton(new ServerSettings
{
ServerSupport = ServerSupport.UDAP,
DefaultUserScopes = "udap",
DefaultSystemScopes = "udap"
});
- s.AddSingleton(new UdapClientOptions
- {
- ClientName = "Mock Client",
- Contacts = new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" }
- });
+ services.AddSingleton>(new OptionsMonitorForTests(
+ new UdapClientOptions
+ {
+ ClientName = "Mock Client",
+ Contacts = new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" }
+
+ })
+ );
+
+ //
+ // Ensure IUdapClient uses the TestServer's HttpMessageHandler
+ // Wired up via _mockPipeline.BrowserClient
+ //
+ services.AddScoped(sp => new UdapClient(
+ _mockPipeline.BrowserClient,
+ sp.GetRequiredService(),
+ sp.GetRequiredService>(),
+ sp.GetRequiredService>()));
};
@@ -145,8 +162,8 @@ public ClientCredentialsUdapModeTests(ITestOutputHelper testOutputHelper)
_mockPipeline.IdentityScopes.Add(new IdentityResources.OpenId());
_mockPipeline.IdentityScopes.Add(new IdentityResources.Profile());
- _mockPipeline.ApiScopes.AddRange(new SmartV2Expander().ExpandToApiScopes("system/Patient.rs"));
- _mockPipeline.ApiScopes.AddRange(new SmartV2Expander().ExpandToApiScopes(" system/Appointment.rs"));
+ _mockPipeline.ApiScopes.AddRange(new HL7SmartScopeExpander().ExpandToApiScopes("system/Patient.rs"));
+ _mockPipeline.ApiScopes.AddRange(new HL7SmartScopeExpander().ExpandToApiScopes(" system/Appointment.rs"));
}
[Fact]
@@ -163,48 +180,26 @@ public async Task GetAccessToken()
{
var clientCert = new X509Certificate2("CertStore/issued/fhirlabs.net.client.pfx", "udap-test");
- var document = UdapDcrBuilderForClientCredentials
- .Create(clientCert)
- .WithAudience(UdapAuthServerPipeline.RegistrationEndpoint)
- .WithExpiration(TimeSpan.FromMinutes(5))
- .WithJwtId()
- .WithClientName("mock test")
- .WithContacts(new HashSet
- {
- "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com"
- })
- .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue)
- .WithScope("system/Patient.rs")
- .Build();
-
- var signedSoftwareStatement =
- SignedSoftwareStatementBuilder
- .Create(clientCert, document)
- .Build();
-
- var requestBody = new UdapRegisterRequest
- (
- signedSoftwareStatement,
- UdapConstants.UdapVersionsSupportedValue,
- new string[] { }
- );
-
- var regResponse = await _mockPipeline.BrowserClient.PostAsync(
- UdapAuthServerPipeline.RegistrationEndpoint,
- new StringContent(JsonSerializer.Serialize(requestBody), new MediaTypeHeaderValue("application/json")));
-
- regResponse.StatusCode.Should().Be(HttpStatusCode.Created);
- var regDocumentResult = await regResponse.Content.ReadFromJsonAsync();
+ var udapClient = _mockPipeline.Resolve();
+ //
+ // Typically the client would validate a server before proceeding to registration.
+ //
+ udapClient.UdapServerMetaData = new UdapMetadata(new Mock().Object, new Mock>().Object)
+ { RegistrationEndpoint = UdapAuthServerPipeline.RegistrationEndpoint };
+ var regDocumentResult = await udapClient.RegisterClientCredentialsClient(
+ clientCert,
+ "system/Patient.rs");
+ regDocumentResult.GetError().Should().BeNull();
//
// Get Access Token
//
var now = DateTime.UtcNow;
var jwtPayload = new JwtPayLoadExtension(
- regDocumentResult!.ClientId,
+ regDocumentResult.ClientId,
IdentityServerPipeline.TokenEndpoint,
new List()
{
@@ -248,40 +243,19 @@ public async Task GetAccessTokenECDSA()
var clientCert = new X509Certificate2("CertStore/issued/fhirlabs.net.ecdsa.client.pfx", "udap-test",
X509KeyStorageFlags.Exportable);
- var document = UdapDcrBuilderForClientCredentials
- .Create(clientCert)
- .WithAudience(UdapAuthServerPipeline.RegistrationEndpoint)
- .WithExpiration(TimeSpan.FromMinutes(5))
- .WithJwtId()
- .WithClientName("mock test")
- .WithContacts(new HashSet
- {
- "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com"
- })
- .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue)
- .WithScope("system/Patient.rs")
- .Build();
-
- var signedSoftwareStatement =
- SignedSoftwareStatementBuilder
- .Create(clientCert, document)
- .BuildECDSA();
-
- var requestBody = new UdapRegisterRequest
- (
- signedSoftwareStatement,
- UdapConstants.UdapVersionsSupportedValue,
- new string[] { }
- );
-
- var regResponse = await _mockPipeline.BrowserClient.PostAsync(
- UdapAuthServerPipeline.RegistrationEndpoint,
- new StringContent(JsonSerializer.Serialize(requestBody), new MediaTypeHeaderValue("application/json")));
+ var udapClient = _mockPipeline.Resolve();
- regResponse.StatusCode.Should().Be(HttpStatusCode.Created);
- var regDocumentResult = await regResponse.Content.ReadFromJsonAsync();
+ //
+ // Typically the client would validate a server before proceeding to registration.
+ //
+ udapClient.UdapServerMetaData = new UdapMetadata(new Mock().Object, new Mock>().Object)
+ { RegistrationEndpoint = UdapAuthServerPipeline.RegistrationEndpoint };
+ var regDocumentResult = await udapClient.RegisterClientCredentialsClient(
+ clientCert,
+ "system/Patient.rs");
+ regDocumentResult.GetError().Should().BeNull();
//
@@ -335,38 +309,19 @@ public async Task UpdateRegistration()
//
// First Registration
//
- var document = UdapDcrBuilderForClientCredentials
- .Create(clientCert)
- .WithAudience(UdapAuthServerPipeline.RegistrationEndpoint)
- .WithExpiration(TimeSpan.FromMinutes(5))
- .WithJwtId()
- .WithClientName("mock test")
- .WithContacts(new HashSet
- {
- "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com"
- })
- .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue)
- .WithScope("system/Patient.rs")
- .Build();
+ var udapClient = _mockPipeline.Resolve();
- var signedSoftwareStatement =
- SignedSoftwareStatementBuilder
- .Create(clientCert, document)
- .Build();
-
- var requestBody = new UdapRegisterRequest
- (
- signedSoftwareStatement,
- UdapConstants.UdapVersionsSupportedValue,
- new string[] { }
- );
+ //
+ // Typically the client would validate a server before proceeding to registration.
+ //
+ udapClient.UdapServerMetaData = new UdapMetadata(new Mock().Object, new Mock>().Object)
+ { RegistrationEndpoint = UdapAuthServerPipeline.RegistrationEndpoint };
- var regResponse = await _mockPipeline.BrowserClient.PostAsync(
- UdapAuthServerPipeline.RegistrationEndpoint,
- new StringContent(JsonSerializer.Serialize(requestBody), new MediaTypeHeaderValue("application/json")));
+ var regDocumentResult = await udapClient.RegisterClientCredentialsClient(
+ clientCert,
+ "system/Patient.rs");
- regResponse.StatusCode.Should().Be(HttpStatusCode.Created);
- var regDocumentResult = await regResponse.Content.ReadFromJsonAsync();
+ regDocumentResult.GetError().Should().BeNull();
regDocumentResult!.Scope.Should().Be("system/Patient.rs");
var clientIdWithDefaultSubAltName = regDocumentResult.ClientId;
@@ -374,82 +329,25 @@ public async Task UpdateRegistration()
//
// Second Registration
//
- document = UdapDcrBuilderForClientCredentials
- .Create(clientCert)
- .WithAudience(UdapAuthServerPipeline.RegistrationEndpoint)
- .WithExpiration(TimeSpan.FromMinutes(5))
- .WithJwtId()
- .WithClientName("mock test")
- .WithContacts(new HashSet
- {
- "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com"
- })
- .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue)
- .WithScope("system/Patient.rs system/Appointment.rs")
- .Build();
+ regDocumentResult = await udapClient.RegisterClientCredentialsClient(
+ clientCert,
+ "system/Patient.rs system/Appointment.rs");
- signedSoftwareStatement =
- SignedSoftwareStatementBuilder
- .Create(clientCert, document)
- .Build();
+ regDocumentResult.GetError().Should().BeNull();
+ regDocumentResult!.Scope.Should().Be("system/Appointment.rs system/Patient.rs");
- requestBody = new UdapRegisterRequest
- (
- signedSoftwareStatement,
- UdapConstants.UdapVersionsSupportedValue,
- new string[] { }
- );
-
- regResponse = await _mockPipeline.BrowserClient.PostAsync(
- UdapAuthServerPipeline.RegistrationEndpoint,
- new StringContent(JsonSerializer.Serialize(requestBody), new MediaTypeHeaderValue("application/json")));
-
- regResponse.StatusCode.Should().Be(HttpStatusCode.OK);
- regDocumentResult = await regResponse.Content.ReadFromJsonAsync();
- regDocumentResult!.Scope.Should().Be("system/Patient.rs system/Appointment.rs");
-
- regDocumentResult!.ClientId.Should().Be(clientIdWithDefaultSubAltName);
+ regDocumentResult.ClientId.Should().Be(clientIdWithDefaultSubAltName);
//
// Third Registration with different Uri Subject Alt Name from same client certificate
// expect 201 created because I changed the SAN selected by calling WithIssuer
//
-
- document = UdapDcrBuilderForClientCredentials
- .Create(clientCert)
- .WithIssuer(new Uri("https://fhirlabs.net:7016/fhir/r4"))
- .WithAudience(UdapAuthServerPipeline.RegistrationEndpoint)
- .WithExpiration(TimeSpan.FromMinutes(5))
- .WithJwtId()
- .WithClientName("mock test")
- .WithContacts(new HashSet
- {
- "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com"
- })
- .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue)
- .WithScope("system/Patient.rs system/Appointment.rs")
- .Build();
-
- signedSoftwareStatement =
- SignedSoftwareStatementBuilder
- .Create(clientCert, document)
- .Build();
-
- requestBody = new UdapRegisterRequest
- (
- signedSoftwareStatement,
- UdapConstants.UdapVersionsSupportedValue,
- new string[] { }
- );
-
- regResponse = await _mockPipeline.BrowserClient.PostAsync(
- UdapAuthServerPipeline.RegistrationEndpoint,
- new StringContent(JsonSerializer.Serialize(requestBody), new MediaTypeHeaderValue("application/json")));
-
- regResponse.StatusCode.Should().Be(HttpStatusCode.Created, await regResponse.Content.ReadAsStringAsync());
- var regDocumentResultForSelectedSubAltName =
- await regResponse.Content.ReadFromJsonAsync();
- regDocumentResultForSelectedSubAltName!.Scope.Should().Be("system/Patient.rs system/Appointment.rs");
+ var regDocumentResultForSelectedSubAltName = await udapClient.RegisterClientCredentialsClient(
+ clientCert,
+ "system/Patient.rs system/Appointment.rs",
+ "https://fhirlabs.net:7016/fhir/r4");
+
+ regDocumentResultForSelectedSubAltName!.Scope.Should().Be("system/Appointment.rs system/Patient.rs");
var clientIdWithSelectedSubAltName = regDocumentResultForSelectedSubAltName.ClientId;
clientIdWithSelectedSubAltName.Should().NotBe(clientIdWithDefaultSubAltName);
@@ -457,48 +355,16 @@ public async Task UpdateRegistration()
// Fourth Registration with Uri Subject Alt Name from third registration
// expect 200 OK because I changed scope
//
- document = UdapDcrBuilderForClientCredentials
- .Create(clientCert)
- .WithIssuer(new Uri("https://fhirlabs.net:7016/fhir/r4"))
- .WithAudience(UdapAuthServerPipeline.RegistrationEndpoint)
- .WithExpiration(TimeSpan.FromMinutes(5))
- .WithJwtId()
- .WithClientName("mock test")
- .WithContacts(new HashSet
- {
- "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com"
- })
- .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue)
- .WithScope("system/Patient.rs")
- .Build();
-
- signedSoftwareStatement =
- SignedSoftwareStatementBuilder
- .Create(clientCert, document)
- .Build();
-
- requestBody = new UdapRegisterRequest
- (
- signedSoftwareStatement,
- UdapConstants.UdapVersionsSupportedValue,
- new string[] { }
- );
-
- regResponse = await _mockPipeline.BrowserClient.PostAsync(
- UdapAuthServerPipeline.RegistrationEndpoint,
- new StringContent(JsonSerializer.Serialize(requestBody), new MediaTypeHeaderValue("application/json")));
-
- regResponse.StatusCode.Should().Be(HttpStatusCode.OK);
- var regDocumentResultForSelectedSubAltNameSecond =
- await regResponse.Content.ReadFromJsonAsync();
+ var regDocumentResultForSelectedSubAltNameSecond = await udapClient.RegisterClientCredentialsClient(
+ clientCert,
+ "system/Patient.rs",
+ "https://fhirlabs.net:7016/fhir/r4");
+
regDocumentResultForSelectedSubAltNameSecond!.Scope.Should().Be("system/Patient.rs");
-
- regDocumentResultForSelectedSubAltNameSecond!.ClientId.Should().Be(clientIdWithSelectedSubAltName);
+ regDocumentResultForSelectedSubAltNameSecond.ClientId.Should().Be(clientIdWithSelectedSubAltName);
}
-
-
[Fact]
public async Task CancelRegistration()
@@ -636,7 +502,7 @@ public async Task CancelRegistration()
regResponse.StatusCode.Should().Be(HttpStatusCode.Created, await regResponse.Content.ReadAsStringAsync());
var regDocumentResultForSelectedSubAltName =
await regResponse.Content.ReadFromJsonAsync();
- regDocumentResultForSelectedSubAltName!.Scope.Should().Be("system/Patient.rs system/Appointment.rs");
+ regDocumentResultForSelectedSubAltName!.Scope.Should().Be("system/Appointment.rs system/Patient.rs");
var clientIdWithSelectedSubAltName = regDocumentResultForSelectedSubAltName.ClientId;
clientIdWithSelectedSubAltName.Should().NotBe(clientIdWithDefaultSubAltName);
@@ -696,14 +562,14 @@ public async Task CancelRegistration()
[Fact]
public async Task RegisterTwoCommunitiesWithSameISS_AndCancelOne()
{
- var clientCert_1 = new X509Certificate2("CertStore/issued/fhirlabs.net.client.pfx", "udap-test");
- var clientCert_2 = new X509Certificate2("CertStore/issued/fhirLabsApiClientLocalhostCert2.pfx", "udap-test");
+ var clientCert1 = new X509Certificate2("CertStore/issued/fhirlabs.net.client.pfx", "udap-test");
+ var clientCert2 = new X509Certificate2("CertStore/issued/fhirLabsApiClientLocalhostCert2.pfx", "udap-test");
//
// Register Client 1 from community "udap://fhirlabs.net"
//
var document = UdapDcrBuilderForClientCredentials
- .Create(clientCert_1)
+ .Create(clientCert1)
.WithAudience(UdapAuthServerPipeline.RegistrationEndpoint)
.WithExpiration(TimeSpan.FromMinutes(5))
.WithJwtId()
@@ -718,7 +584,7 @@ public async Task RegisterTwoCommunitiesWithSameISS_AndCancelOne()
var signedSoftwareStatement =
SignedSoftwareStatementBuilder
- .Create(clientCert_1, document)
+ .Create(clientCert1, document)
.Build();
var requestBody = new UdapRegisterRequest
@@ -741,7 +607,7 @@ public async Task RegisterTwoCommunitiesWithSameISS_AndCancelOne()
// Register Client 2 from community "localhost_fhirlabs_community2"
//
document = UdapDcrBuilderForClientCredentials
- .Create(clientCert_2)
+ .Create(clientCert2)
.WithAudience(UdapAuthServerPipeline.RegistrationEndpoint)
.WithExpiration(TimeSpan.FromMinutes(5))
.WithJwtId()
@@ -756,7 +622,7 @@ public async Task RegisterTwoCommunitiesWithSameISS_AndCancelOne()
signedSoftwareStatement =
SignedSoftwareStatementBuilder
- .Create(clientCert_2, document)
+ .Create(clientCert2, document)
.Build();
requestBody = new UdapRegisterRequest
@@ -780,7 +646,7 @@ public async Task RegisterTwoCommunitiesWithSameISS_AndCancelOne()
// Cancel Registration from community "udap://fhirlabs.net"
//
document = UdapDcrBuilderForClientCredentials
- .Cancel(clientCert_1)
+ .Cancel(clientCert1)
.WithAudience(UdapAuthServerPipeline.RegistrationEndpoint)
.WithExpiration(TimeSpan.FromMinutes(5))
.WithJwtId()
@@ -795,7 +661,7 @@ public async Task RegisterTwoCommunitiesWithSameISS_AndCancelOne()
signedSoftwareStatement =
SignedSoftwareStatementBuilder
- .Create(clientCert_1, document)
+ .Create(clientCert1, document)
.Build();
requestBody = new UdapRegisterRequest
@@ -956,7 +822,7 @@ public async Task Missing_grant_types_RegistrationResultsIn_invalid_client()
regResponse.StatusCode.Should().Be(HttpStatusCode.Created, await regResponse.Content.ReadAsStringAsync());
var regDocumentResultForSelectedSubAltName =
await regResponse.Content.ReadFromJsonAsync();
- regDocumentResultForSelectedSubAltName!.Scope.Should().Be("system/Patient.rs system/Appointment.rs");
+ regDocumentResultForSelectedSubAltName!.Scope.Should().Be("system/Appointment.rs system/Patient.rs");
var clientIdWithSelectedSubAltName = regDocumentResultForSelectedSubAltName.ClientId;
clientIdWithSelectedSubAltName.Should().NotBe(clientIdWithDefaultSubAltName);
diff --git a/_tests/UdapServer.Tests/Conformance/Basic/RegistrationAndChangeRegistrationTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/RegistrationAndChangeRegistrationTests.cs
index ed1bf577..4aa423c8 100644
--- a/_tests/UdapServer.Tests/Conformance/Basic/RegistrationAndChangeRegistrationTests.cs
+++ b/_tests/UdapServer.Tests/Conformance/Basic/RegistrationAndChangeRegistrationTests.cs
@@ -142,7 +142,7 @@ public RegistrationAndChangeRegistrationTests(ITestOutputHelper testOutputHelper
_mockPipeline.IdentityScopes.Add(new IdentityResources.OpenId());
_mockPipeline.IdentityScopes.Add(new IdentityResources.Profile());
_mockPipeline.ApiScopes.Add(new ApiScope("system/Patient.rs"));
- _mockPipeline.ApiScopes.Add(new ApiScope(" system/Appointment.rs"));
+ _mockPipeline.ApiScopes.Add(new ApiScope("system/Appointment.rs"));
}
@@ -203,7 +203,7 @@ public async Task RegisterClientCredentialsThenRegisterAuthorizationCode()
.WithExpiration(TimeSpan.FromMinutes(5))
.WithJwtId()
.WithClientName("mock test")
- .WithLogoUri("https://example.com/logo.png")
+ .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4")
.WithContacts(new HashSet
{
"mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com"
@@ -234,7 +234,7 @@ public async Task RegisterClientCredentialsThenRegisterAuthorizationCode()
regResponse.StatusCode.Should().Be(HttpStatusCode.OK, await regResponse.Content.ReadAsStringAsync());
regDocumentResult = await regResponse.Content.ReadFromJsonAsync();
- regDocumentResult!.Scope.Should().Be("system/Patient.rs system/Appointment.rs");
+ regDocumentResult!.Scope.Should().Be("system/Appointment.rs system/Patient.rs");
regDocumentResult!.ClientId.Should().Be(clientId);
_mockPipeline.Clients.Single().AllowedGrantTypes.Should().NotContain(OidcConstants.GrantTypes.ClientCredentials);
diff --git a/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs
index 90bf7ce8..dc874e6b 100644
--- a/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs
+++ b/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs
@@ -14,28 +14,27 @@
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using Duende.IdentityServer.Models;
+using Duende.IdentityServer.Test;
using FluentAssertions;
-using IdentityModel.Client;
using IdentityModel;
+using IdentityModel.Client;
+using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Udap.Client.Client.Extensions;
+using Udap.Client.Configuration;
+using Udap.Common.Extensions;
using Udap.Common.Models;
using Udap.Model;
using Udap.Model.Access;
using Udap.Model.Registration;
using Udap.Model.Statement;
using Udap.Server.Configuration;
+using Udap.Server.Models;
+using Udap.Server.Validation;
using Udap.Util.Extensions;
using UdapServer.Tests.Common;
using Xunit.Abstractions;
-using Microsoft.AspNetCore.WebUtilities;
-using Udap.Server.Models;
-using Duende.IdentityServer.Test;
-using System.Text;
-using Udap.Client.Configuration;
-using Udap.Common.Extensions;
-using Udap.Server.Validation;
namespace UdapServer.Tests.Conformance.Basic;
@@ -44,7 +43,7 @@ namespace UdapServer.Tests.Conformance.Basic;
public class ScopeExpansionTests
{
private readonly ITestOutputHelper _testOutputHelper;
- private UdapAuthServerPipeline _mockPipeline = new UdapAuthServerPipeline();
+ private readonly UdapAuthServerPipeline _mockPipeline = new();
public ScopeExpansionTests(ITestOutputHelper testOutputHelper)
{
@@ -55,19 +54,20 @@ public ScopeExpansionTests(ITestOutputHelper testOutputHelper)
_mockPipeline.OnPostConfigureServices += s =>
{
- s.AddSingleton(new ServerSettings
+ s.AddSingleton(new ServerSettings
{
ServerSupport = ServerSupport.UDAP,
DefaultUserScopes = "udap",
DefaultSystemScopes = "udap"
});
- s.AddSingleton(new UdapClientOptions
+ s.AddSingleton(new UdapClientOptions
{
ClientName = "Mock Client",
Contacts = new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" }
});
-
+
+ s.AddScoped();
};
_mockPipeline.OnPreConfigureServices += (_, s) =>
@@ -99,7 +99,7 @@ public ScopeExpansionTests(ITestOutputHelper testOutputHelper)
Enabled = true,
Intermediates = new List()
{
- new Intermediate
+ new()
{
BeginDate = intermediateCert.NotBefore.ToUniversalTime(),
EndDate = intermediateCert.NotAfter.ToUniversalTime(),
@@ -112,9 +112,13 @@ public ScopeExpansionTests(ITestOutputHelper testOutputHelper)
}
}
});
- _mockPipeline.ApiScopes.AddRange(new SmartV2Expander().ExpandToApiScopes("system/Patient.cruds"));
- _mockPipeline.ApiScopes.AddRange(new SmartV2Expander().ExpandToApiScopes("system/Encounter.r"));
- _mockPipeline.ApiScopes.AddRange(new SmartV2Expander().ExpandToApiScopes("system/Condition.s"));
+
+ _mockPipeline.ApiScopes.AddRange(new HL7SmartScopeExpander().ExpandToApiScopes("system/Patient.cruds"));
+ _mockPipeline.ApiScopes.AddRange(new HL7SmartScopeExpander().ExpandToApiScopes("system/Encounter.r"));
+ _mockPipeline.ApiScopes.AddRange(new HL7SmartScopeExpander().ExpandToApiScopes("system/Condition.s"));
+ _mockPipeline.ApiScopes.Add( new ApiScope("system/Practitioner.read"));
+
+
_mockPipeline.IdentityScopes.Add(new IdentityResources.OpenId());
_mockPipeline.IdentityScopes.Add(new UdapIdentityResources.Udap());
@@ -122,7 +126,7 @@ public ScopeExpansionTests(ITestOutputHelper testOutputHelper)
{
SubjectId = "bob",
Username = "bob",
- Claims = new Claim[]
+ Claims = new[]
{
new Claim("name", "Bob Loblaw"),
new Claim("email", "bob@loblaw.com"),
@@ -144,7 +148,7 @@ public void GenerateCombinations_ReturnsUniqueStringCombinationsInGivenOrder(str
}
}
-
+
[Fact]
public async Task ScopeV2WithClientCredentialsTest()
{
@@ -158,13 +162,13 @@ public async Task ScopeV2WithClientCredentialsTest()
//
var now = DateTime.UtcNow;
var jwtPayload = new JwtPayLoadExtension(
- resultDocument!.ClientId,
+ resultDocument.ClientId,
IdentityServerPipeline.TokenEndpoint,
new List()
{
- new Claim(JwtClaimTypes.Subject, resultDocument.ClientId!),
- new Claim(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer),
- new Claim(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()),
+ new(JwtClaimTypes.Subject, resultDocument.ClientId!),
+ new(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer),
+ new(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()),
// new Claim(UdapConstants.JwtClaimTypes.Extensions, BuildHl7B2BExtensions() ) //see http://hl7.org/fhir/us/udap-security/b2b.html#constructing-authentication-token
},
now.ToUniversalTime(),
@@ -199,13 +203,13 @@ public async Task ScopeV2WithClientCredentialsTest()
//
jwtPayload = new JwtPayLoadExtension(
- resultDocument!.ClientId,
+ resultDocument.ClientId,
IdentityServerPipeline.TokenEndpoint,
new List()
{
- new Claim(JwtClaimTypes.Subject, resultDocument.ClientId!),
- new Claim(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer),
- new Claim(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()),
+ new(JwtClaimTypes.Subject, resultDocument.ClientId!),
+ new(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer),
+ new(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()),
// new Claim(UdapConstants.JwtClaimTypes.Extensions, BuildHl7B2BExtensions() ) //see http://hl7.org/fhir/us/udap-security/b2b.html#constructing-authentication-token
},
now.ToUniversalTime(),
@@ -241,13 +245,13 @@ public async Task ScopeV2WithClientCredentialsTest()
//
jwtPayload = new JwtPayLoadExtension(
- resultDocument!.ClientId,
+ resultDocument.ClientId,
IdentityServerPipeline.TokenEndpoint,
new List()
{
- new Claim(JwtClaimTypes.Subject, resultDocument.ClientId!),
- new Claim(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer),
- new Claim(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()),
+ new(JwtClaimTypes.Subject, resultDocument.ClientId!),
+ new(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer),
+ new(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()),
// new Claim(UdapConstants.JwtClaimTypes.Extensions, BuildHl7B2BExtensions() ) //see http://hl7.org/fhir/us/udap-security/b2b.html#constructing-authentication-token
},
now.ToUniversalTime(),
@@ -277,59 +281,19 @@ public async Task ScopeV2WithClientCredentialsTest()
tokenResponse.Scope.Should().Be("system/Patient.rs", tokenResponse.Raw);
- //
- // Again wild card expansion: TODO
- //
-
- // jwtPayload = new JwtPayLoadExtension(
- // resultDocument!.ClientId,
- // IdentityServerPipeline.TokenEndpoint,
- // new List()
- // {
- // new Claim(JwtClaimTypes.Subject, resultDocument.ClientId!),
- // new Claim(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer),
- // new Claim(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()),
- // // new Claim(UdapConstants.JwtClaimTypes.Extensions, BuildHl7B2BExtensions() ) //see http://hl7.org/fhir/us/udap-security/b2b.html#constructing-authentication-token
- // },
- // now.ToUniversalTime(),
- // now.AddMinutes(5).ToUniversalTime()
- // );
- //
- // clientAssertion =
- // SignedSoftwareStatementBuilder
- // .Create(clientCert, jwtPayload)
- // .Build("RS384");
- //
- //
- // clientRequest = new UdapClientCredentialsTokenRequest
- // {
- // Address = IdentityServerPipeline.TokenEndpoint,
- // //ClientId = result.ClientId, we use Implicit ClientId in the iss claim
- // ClientAssertion = new ClientAssertion()
- // {
- // Type = OidcConstants.ClientAssertionTypes.JwtBearer,
- // Value = clientAssertion
- // },
- // Udap = UdapConstants.UdapVersionsSupportedValue,
- // Scope = "system/Patient.*"
- // };
- //
- // tokenResponse = await _mockPipeline.BackChannelClient.UdapRequestClientCredentialsTokenAsync(clientRequest);
- //
- // tokenResponse.Scope.Should().Be("system/Patient.rs", tokenResponse.Raw);
-
+
//
// Again negative
//
jwtPayload = new JwtPayLoadExtension(
- resultDocument!.ClientId,
+ resultDocument.ClientId,
IdentityServerPipeline.TokenEndpoint,
new List()
{
- new Claim(JwtClaimTypes.Subject, resultDocument.ClientId!),
- new Claim(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer),
- new Claim(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()),
+ new(JwtClaimTypes.Subject, resultDocument.ClientId!),
+ new(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer),
+ new(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()),
// new Claim(UdapConstants.JwtClaimTypes.Extensions, BuildHl7B2BExtensions() ) //see http://hl7.org/fhir/us/udap-security/b2b.html#constructing-authentication-token
},
now.ToUniversalTime(),
@@ -361,6 +325,150 @@ public async Task ScopeV2WithClientCredentialsTest()
tokenResponse.Error.Should().Be("invalid_scope");
}
+ [Fact]
+ public async Task ScopeV2WithClientCredentialsExtendedTest()
+ {
+ var clientCert = new X509Certificate2("CertStore/issued/fhirlabs.net.client.pfx", "udap-test");
+ var resultDocument = await RegisterClientWithAuthServer("system/Patient.rs", clientCert);
+ resultDocument.Should().NotBeNull();
+ resultDocument!.ClientId.Should().NotBeNull();
+
+ resultDocument.Scope.Should().Be("system/Patient.rs");
+ _mockPipeline.Clients[0].AllowedScopes.Count.Should().Be(3);
+
+ var now = DateTime.UtcNow;
+ var jwtPayload = new JwtPayLoadExtension(
+ resultDocument.ClientId,
+ IdentityServerPipeline.TokenEndpoint,
+ new List()
+ {
+ new(JwtClaimTypes.Subject, resultDocument.ClientId!),
+ new(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer),
+ new(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()),
+ // new Claim(UdapConstants.JwtClaimTypes.Extensions, BuildHl7B2BExtensions() ) //see http://hl7.org/fhir/us/udap-security/b2b.html#constructing-authentication-token
+ },
+ now.ToUniversalTime(),
+ now.AddMinutes(5).ToUniversalTime()
+ );
+
+ var clientAssertion =
+ SignedSoftwareStatementBuilder
+ .Create(clientCert, jwtPayload)
+ .Build("RS384");
+
+
+ var clientRequest = new UdapClientCredentialsTokenRequest
+ {
+ Address = IdentityServerPipeline.TokenEndpoint,
+ //ClientId = result.ClientId, we use Implicit ClientId in the iss claim
+ ClientAssertion = new ClientAssertion()
+ {
+ Type = OidcConstants.ClientAssertionTypes.JwtBearer,
+ Value = clientAssertion
+ },
+ Udap = UdapConstants.UdapVersionsSupportedValue,
+ Scope = "system/Patient.rs system/Patient.r system/Patient.s"
+ };
+
+ var tokenResponse = await _mockPipeline.BackChannelClient.UdapRequestClientCredentialsTokenAsync(clientRequest);
+ tokenResponse.Scope.Should().Be("system/Patient.r system/Patient.rs system/Patient.s", tokenResponse.Raw);
+ }
+
+ [Fact]
+ public async Task ScopeV2WithClientCredentialsExtended2Test()
+ {
+ var clientCert = new X509Certificate2("CertStore/issued/fhirlabs.net.client.pfx", "udap-test");
+ var resultDocument = await RegisterClientWithAuthServer("system/*.rs", clientCert);
+ resultDocument.Should().NotBeNull();
+ resultDocument!.ClientId.Should().NotBeNull();
+
+ resultDocument.Scope.Should().Be("system/Condition.s system/Encounter.r system/Patient.rs");
+
+ var now = DateTime.UtcNow;
+ var jwtPayload = new JwtPayLoadExtension(
+ resultDocument.ClientId,
+ IdentityServerPipeline.TokenEndpoint,
+ new List()
+ {
+ new(JwtClaimTypes.Subject, resultDocument.ClientId!),
+ new(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer),
+ new(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()),
+ // new Claim(UdapConstants.JwtClaimTypes.Extensions, BuildHl7B2BExtensions() ) //see http://hl7.org/fhir/us/udap-security/b2b.html#constructing-authentication-token
+ },
+ now.ToUniversalTime(),
+ now.AddMinutes(5).ToUniversalTime()
+ );
+
+ var clientAssertion =
+ SignedSoftwareStatementBuilder
+ .Create(clientCert, jwtPayload)
+ .Build("RS384");
+
+
+ var clientRequest = new UdapClientCredentialsTokenRequest
+ {
+ Address = IdentityServerPipeline.TokenEndpoint,
+ //ClientId = result.ClientId, we use Implicit ClientId in the iss claim
+ ClientAssertion = new ClientAssertion()
+ {
+ Type = OidcConstants.ClientAssertionTypes.JwtBearer,
+ Value = clientAssertion
+ },
+ Udap = UdapConstants.UdapVersionsSupportedValue,
+ Scope = "system/Condition.s system/Encounter.r system/Patient.rs"
+ };
+
+ var tokenResponse = await _mockPipeline.BackChannelClient.UdapRequestClientCredentialsTokenAsync(clientRequest);
+ tokenResponse.Scope.Should().Be("system/Condition.s system/Encounter.r system/Patient.rs", tokenResponse.Raw);
+ }
+
+ [Fact]
+ public async Task ScopeV2WithClientCredentialsWildcardTest()
+ {
+ var clientCert = new X509Certificate2("CertStore/issued/fhirlabs.net.client.pfx", "udap-test");
+ var resultDocument = await RegisterClientWithAuthServer("system/*.read", clientCert);
+ resultDocument.Should().NotBeNull();
+ resultDocument!.ClientId.Should().NotBeNull();
+
+ resultDocument.Scope.Should().Be("system/Practitioner.read");
+
+ var now = DateTime.UtcNow;
+ var jwtPayload = new JwtPayLoadExtension(
+ resultDocument.ClientId,
+ IdentityServerPipeline.TokenEndpoint,
+ new List()
+ {
+ new(JwtClaimTypes.Subject, resultDocument.ClientId!),
+ new(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer),
+ new(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()),
+ // new Claim(UdapConstants.JwtClaimTypes.Extensions, BuildHl7B2BExtensions() ) //see http://hl7.org/fhir/us/udap-security/b2b.html#constructing-authentication-token
+ },
+ now.ToUniversalTime(),
+ now.AddMinutes(5).ToUniversalTime()
+ );
+
+ var clientAssertion =
+ SignedSoftwareStatementBuilder
+ .Create(clientCert, jwtPayload)
+ .Build("RS384");
+
+
+ var clientRequest = new UdapClientCredentialsTokenRequest
+ {
+ Address = IdentityServerPipeline.TokenEndpoint,
+ //ClientId = result.ClientId, we use Implicit ClientId in the iss claim
+ ClientAssertion = new ClientAssertion()
+ {
+ Type = OidcConstants.ClientAssertionTypes.JwtBearer,
+ Value = clientAssertion
+ },
+ Udap = UdapConstants.UdapVersionsSupportedValue,
+ Scope = "system/Practitioner.read"
+ };
+
+ var tokenResponse = await _mockPipeline.BackChannelClient.UdapRequestClientCredentialsTokenAsync(clientRequest);
+ tokenResponse.Scope.Should().Be("system/Practitioner.read", tokenResponse.Raw);
+ }
[Fact]
public async Task ScopeV2WithAuthCodeTest()
{
@@ -372,7 +480,7 @@ public async Task ScopeV2WithAuthCodeTest()
.WithExpiration(TimeSpan.FromMinutes(5))
.WithJwtId()
.WithClientName("mock test")
- .WithLogoUri("https://example.com/logo.png")
+ .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4")
.WithContacts(new HashSet
{
"mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com"
@@ -414,7 +522,7 @@ public async Task ScopeV2WithAuthCodeTest()
await _mockPipeline.LoginAsync("bob");
var url = _mockPipeline.CreateAuthorizeUrl(
- clientId: resultDocument!.ClientId!,
+ clientId: resultDocument.ClientId!,
responseType: "code",
scope: "openid system/Patient.rs",
redirectUri: "https://code_client/callback",
@@ -427,8 +535,7 @@ public async Task ScopeV2WithAuthCodeTest()
response.StatusCode.Should().Be(HttpStatusCode.Redirect, await response.Content.ReadAsStringAsync());
response.Headers.Location.Should().NotBeNull();
- var redirectUri = response.Headers.Location!.AbsoluteUri;
- response.Headers.Location!.AbsoluteUri.Should().Contain("https://code_client/callback");
+ response.Headers.Location!.AbsoluteUri.Should().Contain("https://code_client/callback");
// _testOutputHelper.WriteLine(response.Headers.Location!.AbsoluteUri);
var queryParams = QueryHelpers.ParseQuery(response.Headers.Location.Query);
queryParams.Should().Contain(p => p.Key == "code");
diff --git a/_tests/UdapServer.Tests/Conformance/Basic/UdapForceStateParamFalseTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/UdapForceStateParamFalseTests.cs
index df01a602..2656682a 100644
--- a/_tests/UdapServer.Tests/Conformance/Basic/UdapForceStateParamFalseTests.cs
+++ b/_tests/UdapServer.Tests/Conformance/Basic/UdapForceStateParamFalseTests.cs
@@ -146,7 +146,7 @@ public async Task Request_state_missing_results_in_success()
.WithExpiration(TimeSpan.FromMinutes(5))
.WithJwtId()
.WithClientName("mock test")
- .WithLogoUri("https://example.com/logo.png")
+ .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4")
.WithContacts(new HashSet
{
"mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com"
diff --git a/_tests/UdapServer.Tests/Conformance/Basic/UdapResponseTypeResponseModeTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/UdapResponseTypeResponseModeTests.cs
index a063fa49..a4eaf80d 100644
--- a/_tests/UdapServer.Tests/Conformance/Basic/UdapResponseTypeResponseModeTests.cs
+++ b/_tests/UdapServer.Tests/Conformance/Basic/UdapResponseTypeResponseModeTests.cs
@@ -144,7 +144,7 @@ public async Task Request_response_type_missing_results_in_unsupported_response_
.WithExpiration(TimeSpan.FromMinutes(5))
.WithJwtId()
.WithClientName("mock test")
- .WithLogoUri("https://example.com/logo.png")
+ .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4")
.WithContacts(new HashSet
{
"mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com"
@@ -223,7 +223,7 @@ public async Task Request_state_missing_results_in_unsupported_response_type()
.WithExpiration(TimeSpan.FromMinutes(5))
.WithJwtId()
.WithClientName("mock test")
- .WithLogoUri("https://example.com/logo.png")
+ .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4")
.WithContacts(new HashSet
{
"mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com"
@@ -296,7 +296,7 @@ public async Task Request_response_type_invalid_results_in_unsupported_response_
.WithExpiration(TimeSpan.FromMinutes(5))
.WithJwtId()
.WithClientName("mock test")
- .WithLogoUri("https://example.com/logo.png")
+ .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4")
.WithContacts(new HashSet
{
"mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com"
@@ -370,7 +370,7 @@ public async Task Request_client_id_missing_results_in_invalid_request()
.WithExpiration(TimeSpan.FromMinutes(5))
.WithJwtId()
.WithClientName("mock test")
- .WithLogoUri("https://example.com/logo.png")
+ .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4")
.WithContacts(new HashSet
{
"mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com"
@@ -439,7 +439,7 @@ public async Task Request_client_id_invalid_results_in_unauthorized_client()
.WithExpiration(TimeSpan.FromMinutes(5))
.WithJwtId()
.WithClientName("mock test")
- .WithLogoUri("https://example.com/logo.png")
+ .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4")
.WithContacts(new HashSet
{
"mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com"
@@ -505,7 +505,7 @@ public async Task Request_accepted()
.WithExpiration(TimeSpan.FromMinutes(5))
.WithJwtId()
.WithClientName("mock test")
- .WithLogoUri("https://example.com/logo.png")
+ .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4")
.WithContacts(new HashSet
{
"mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com"
@@ -583,7 +583,7 @@ public async Task Request_accepted_RegisterWithDifferentRedirectUrl()
.WithExpiration(TimeSpan.FromMinutes(5))
.WithJwtId()
.WithClientName("mock test")
- .WithLogoUri("https://example.com/logo.png")
+ .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4")
.WithContacts(new HashSet
{
"mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com"
@@ -724,7 +724,7 @@ public async Task Request_accepted_URI_HostOnly()
.WithExpiration(TimeSpan.FromMinutes(5))
.WithJwtId()
.WithClientName("mock test")
- .WithLogoUri("https://example.com/logo.png")
+ .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4")
.WithContacts(new HashSet
{
"mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com"
diff --git a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs
index 0877b919..7619b18e 100644
--- a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs
+++ b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs
@@ -7,10 +7,9 @@
// */
#endregion
+using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
using System.Net;
-using System.Net.Http.Headers;
-using System.Net.Http.Json;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Text;
@@ -20,12 +19,13 @@
using Duende.IdentityServer.Test;
using FluentAssertions;
using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
+using Moq;
using Udap.Client.Client;
using Udap.Client.Configuration;
using Udap.Common;
@@ -34,7 +34,6 @@
using Udap.Model;
using Udap.Model.Access;
using Udap.Model.Registration;
-using Udap.Model.Statement;
using Udap.Server.Configuration;
using Udap.Server.Models;
using Udap.Server.Security.Authentication.TieredOAuth;
@@ -49,51 +48,54 @@ public class TieredOauthTests
{
private readonly ITestOutputHelper _testOutputHelper;
- private UdapAuthServerPipeline _mockAuthorServerPipeline = new UdapAuthServerPipeline();
- private UdapIdentityServerPipeline _mockIdPPipeline = new UdapIdentityServerPipeline();
+ private readonly UdapAuthServerPipeline _mockAuthorServerPipeline = new();
+ private readonly UdapIdentityServerPipeline _mockIdPPipeline = new();
+ private readonly UdapIdentityServerPipeline _mockIdPPipeline2 = new("https://idpserver2", "appsettings.Idp2.json");
- private IAuthenticationSchemeProvider _schemeProvider;
+ private readonly X509Certificate2 _community1Anchor;
+ private readonly X509Certificate2 _community1IntermediateCert;
+ private readonly X509Certificate2 _community2Anchor;
+ private readonly X509Certificate2 _community2IntermediateCert;
public TieredOauthTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
- var sureFhirLabsAnchor = new X509Certificate2("CertStore/anchors/caLocalhostCert.cer");
- var intermediateCert = new X509Certificate2("CertStore/intermediates/intermediateLocalhostCert.cer");
+ _community1Anchor = new X509Certificate2("CertStore/anchors/caLocalhostCert.cer");
+ _community1IntermediateCert = new X509Certificate2("CertStore/intermediates/intermediateLocalhostCert.cer");
- var idpAnchor1 = new X509Certificate2("CertStore/anchors/caLocalhostCert.cer");
- var idpIntermediate1 = new X509Certificate2("CertStore/intermediates/intermediateLocalhostCert.cer");
-
- BuildUdapAuthorizationServer(sureFhirLabsAnchor, intermediateCert);
- BuildUdapIdentityProvider(idpAnchor1, idpIntermediate1);
+ _community2Anchor = new X509Certificate2("CertStore/anchors/caLocalhostCert2.cer");
+ _community2IntermediateCert = new X509Certificate2("CertStore/intermediates/intermediateLocalhostCert2.cer");
+
+ BuildUdapAuthorizationServer();
+ BuildUdapIdentityProvider1();
+ BuildUdapIdentityProvider2();
}
- private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X509Certificate2 intermediateCert)
+ private void BuildUdapAuthorizationServer()
{
- _mockAuthorServerPipeline.OnPostConfigureServices += s =>
+ _mockAuthorServerPipeline.OnPostConfigureServices += services =>
{
- s.AddSingleton(new ServerSettings
+ services.AddSingleton(new ServerSettings
{
ServerSupport = ServerSupport.Hl7SecurityIG,
- IdPMappings = new List
- {
- new IdPMapping()
- {
- Scheme = "TieredOAuth", // default name
- IdpBaseUrl = "https://idpserver"
- }
- }
// DefaultUserScopes = "udap",
// DefaultSystemScopes = "udap"
// ForceStateParamOnAuthorizationCode = false (default)
});
- s.AddSingleton>(new OptionsMonitorForTests(
+ services.AddSingleton>(new OptionsMonitorForTests(
new UdapClientOptions
{
ClientName = "AuthServer Client",
- Contacts = new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" }
+ Contacts = new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" },
+ TieredOAuthClientLogo = "https://server/udap.logo.48x48.png"
})
);
+
+ //
+ // Allow logo resolve back to udap.auth server
+ //
+ services.AddSingleton(_ => _mockAuthorServerPipeline.BrowserClient);
};
_mockAuthorServerPipeline.OnPreConfigureServices += (builderContext, services) =>
@@ -103,28 +105,33 @@ private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X
// It was not intended to work with the concept of a dynamic client registration.
services.AddSingleton(_mockAuthorServerPipeline.Clients);
- services.Configure(builderContext.Configuration.GetSection(Udap.Common.Constants.UDAP_FILE_STORE_MANIFEST));
+ services.Configure(builderContext.Configuration.GetSection(Constants.UDAP_FILE_STORE_MANIFEST));
services.AddAuthentication()
- //
- // By convention the scheme name should match the community name in UdapFileCertStoreManifest
- // to allow discovery of the IdPBaseUrl
- //
- .AddTieredOAuthForTests(options =>
- {
- options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
- options.AuthorizationEndpoint = "https://idpserver/connect/authorize";
- options.TokenEndpoint = "https://idpserver/connect/token";
- options.IdPBaseUrl = "https://idpserver";
- }, _mockIdPPipeline); // point backchannel to the IdP
+ //
+ // By convention the scheme name should match the community name in UdapFileCertStoreManifest
+ // to allow discovery of the IdPBaseUrl
+ //
+ .AddTieredOAuthForTests(options =>
+ {
+ options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
+ },
+ _mockAuthorServerPipeline,
+ _mockIdPPipeline,
+ _mockIdPPipeline2);
+
-
services.AddAuthorization(); // required for TieredOAuth Testing
+
+ services.ConfigureAll(options =>
+ {
+ options.BackchannelHttpHandler = _mockIdPPipeline2.Server?.CreateHandler();
+ });
+
+
using var serviceProvider = services.BuildServiceProvider();
- _schemeProvider = serviceProvider.GetRequiredService();
-
};
_mockAuthorServerPipeline.OnPostConfigure += app =>
@@ -140,29 +147,30 @@ private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X
_mockAuthorServerPipeline.Communities.Add(new Community
{
- Name = "udap://fhirlabs.net",
+ Id = 0,
+ Name = "https://idpserver",
Enabled = true,
Default = true,
Anchors = new[]
{
new Anchor
{
- BeginDate = sureFhirLabsAnchor.NotBefore.ToUniversalTime(),
- EndDate = sureFhirLabsAnchor.NotAfter.ToUniversalTime(),
- Name = sureFhirLabsAnchor.Subject,
- Community = "udap://fhirlabs.net",
- Certificate = sureFhirLabsAnchor.ToPemFormat(),
- Thumbprint = sureFhirLabsAnchor.Thumbprint,
+ BeginDate = _community1Anchor.NotBefore.ToUniversalTime(),
+ EndDate = _community1Anchor.NotAfter.ToUniversalTime(),
+ Name = _community1Anchor.Subject,
+ Community = "https://idpserver",
+ Certificate = _community1Anchor.ToPemFormat(),
+ Thumbprint = _community1Anchor.Thumbprint,
Enabled = true,
Intermediates = new List()
{
- new Intermediate
+ new()
{
- BeginDate = intermediateCert.NotBefore.ToUniversalTime(),
- EndDate = intermediateCert.NotAfter.ToUniversalTime(),
- Name = intermediateCert.Subject,
- Certificate = intermediateCert.ToPemFormat(),
- Thumbprint = intermediateCert.Thumbprint,
+ BeginDate = _community1IntermediateCert.NotBefore.ToUniversalTime(),
+ EndDate = _community1IntermediateCert.NotAfter.ToUniversalTime(),
+ Name = _community1IntermediateCert.Subject,
+ Certificate = _community1IntermediateCert.ToPemFormat(),
+ Thumbprint = _community1IntermediateCert.Thumbprint,
Enabled = true
}
}
@@ -170,10 +178,42 @@ private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X
}
});
-
- // _mockAuthorServerPipeline.
+ _mockAuthorServerPipeline.Communities.Add(new Community
+ {
+ Id = 1,
+ Name = "udap://idp-community-2",
+ Enabled = true,
+ Default = true,
+ Anchors = new[]
+ {
+ new Anchor
+ {
+ BeginDate = _community2Anchor.NotBefore.ToUniversalTime(),
+ EndDate = _community2Anchor.NotAfter.ToUniversalTime(),
+ Name = _community2Anchor.Subject,
+ Community = "udap://idp-community-2",
+ Certificate = _community2Anchor.ToPemFormat(),
+ Thumbprint = _community2Anchor.Thumbprint,
+ Enabled = true,
+ Intermediates = new List()
+ {
+ new()
+ {
+ BeginDate = _community2IntermediateCert.NotBefore.ToUniversalTime(),
+ EndDate = _community2IntermediateCert.NotAfter.ToUniversalTime(),
+ Name = _community2IntermediateCert.Subject,
+ Certificate = _community2IntermediateCert.ToPemFormat(),
+ Thumbprint = _community2IntermediateCert.Thumbprint,
+ Enabled = true
+ }
+ }
+ }
+ }
+ });
+ // _mockAuthorServerPipeline.
+
_mockAuthorServerPipeline.IdentityScopes.Add(new IdentityResources.OpenId());
_mockAuthorServerPipeline.IdentityScopes.Add(new IdentityResources.Profile());
@@ -185,7 +225,7 @@ private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X
{
SubjectId = "bob",
Username = "bob",
- Claims = new Claim[]
+ Claims = new[]
{
new Claim("name", "Bob Loblaw"),
new Claim("email", "bob@loblaw.com"),
@@ -196,11 +236,11 @@ private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X
_mockAuthorServerPipeline.UserStore = new TestUserStore(_mockAuthorServerPipeline.Users);
}
- private void BuildUdapIdentityProvider(X509Certificate2 sureFhirLabsAnchor, X509Certificate2 intermediateCert)
+ private void BuildUdapIdentityProvider1()
{
- _mockIdPPipeline.OnPostConfigureServices += s =>
+ _mockIdPPipeline.OnPostConfigureServices += services =>
{
- s.AddSingleton(new ServerSettings
+ services.AddSingleton(new ServerSettings
{
ServerSupport = ServerSupport.UDAP,
DefaultUserScopes = "udap",
@@ -208,44 +248,48 @@ private void BuildUdapIdentityProvider(X509Certificate2 sureFhirLabsAnchor, X509
// ForceStateParamOnAuthorizationCode = false (default)
AlwaysIncludeUserClaimsInIdToken = true
});
- };
- _mockIdPPipeline.OnPreConfigureServices += (builderContext, services) =>
- {
// This registers Clients as List so downstream I can pick it up in InMemoryUdapClientRegistrationStore
// Duende's AddInMemoryClients extension registers as IEnumerable and is used in InMemoryClientStore as readonly.
// It was not intended to work with the concept of a dynamic client registration.
services.AddSingleton(_mockIdPPipeline.Clients);
+
+ //
+ // Allow logo resolve back to udap.auth server
+ //
+ services.AddSingleton(sp => _mockAuthorServerPipeline.BrowserClient);
};
+
_mockIdPPipeline.Initialize(enableLogging: true);
+ Debug.Assert(_mockIdPPipeline.BrowserClient != null, "_mockIdPPipeline.BrowserClient != null");
_mockIdPPipeline.BrowserClient.AllowAutoRedirect = false;
_mockIdPPipeline.Communities.Add(new Community
{
- Name = "udap://fhirlabs.net",
+ Name = "udap://idp-community-1",
Enabled = true,
Default = true,
Anchors = new[]
{
new Anchor
{
- BeginDate = sureFhirLabsAnchor.NotBefore.ToUniversalTime(),
- EndDate = sureFhirLabsAnchor.NotAfter.ToUniversalTime(),
- Name = sureFhirLabsAnchor.Subject,
- Community = "udap://fhirlabs.net",
- Certificate = sureFhirLabsAnchor.ToPemFormat(),
- Thumbprint = sureFhirLabsAnchor.Thumbprint,
+ BeginDate = _community1Anchor.NotBefore.ToUniversalTime(),
+ EndDate = _community1Anchor.NotAfter.ToUniversalTime(),
+ Name = _community1Anchor.Subject,
+ Community = "udap://idp-community-1",
+ Certificate = _community1Anchor.ToPemFormat(),
+ Thumbprint = _community1Anchor.Thumbprint,
Enabled = true,
Intermediates = new List()
{
- new Intermediate
+ new()
{
- BeginDate = intermediateCert.NotBefore.ToUniversalTime(),
- EndDate = intermediateCert.NotAfter.ToUniversalTime(),
- Name = intermediateCert.Subject,
- Certificate = intermediateCert.ToPemFormat(),
- Thumbprint = intermediateCert.Thumbprint,
+ BeginDate = _community1IntermediateCert.NotBefore.ToUniversalTime(),
+ EndDate = _community1IntermediateCert.NotAfter.ToUniversalTime(),
+ Name = _community1IntermediateCert.Subject,
+ Certificate = _community1IntermediateCert.ToPemFormat(),
+ Thumbprint = _community1IntermediateCert.Thumbprint,
Enabled = true
}
}
@@ -263,7 +307,7 @@ private void BuildUdapIdentityProvider(X509Certificate2 sureFhirLabsAnchor, X509
{
SubjectId = "bob",
Username = "bob",
- Claims = new Claim[]
+ Claims = new[]
{
new Claim("name", "Bob Loblaw"),
new Claim("email", "bob@loblaw.com"),
@@ -276,6 +320,91 @@ private void BuildUdapIdentityProvider(X509Certificate2 sureFhirLabsAnchor, X509
_mockIdPPipeline.Subject = new IdentityServerUser("bob").CreatePrincipal();
}
+ private void BuildUdapIdentityProvider2()
+ {
+ _mockIdPPipeline2.OnPostConfigureServices += services =>
+ {
+ services.AddSingleton(new ServerSettings
+ {
+ ServerSupport = ServerSupport.UDAP,
+ DefaultUserScopes = "udap",
+ DefaultSystemScopes = "udap",
+ // ForceStateParamOnAuthorizationCode = false (default)
+ AlwaysIncludeUserClaimsInIdToken = true
+ });
+
+ // This registers Clients as List so downstream I can pick it up in InMemoryUdapClientRegistrationStore
+ // Duende's AddInMemoryClients extension registers as IEnumerable and is used in InMemoryClientStore as readonly.
+ // It was not intended to work with the concept of a dynamic client registration.
+ services.AddSingleton(_mockIdPPipeline2.Clients);
+
+ //
+ // Allow logo resolve back to udap.auth server
+ //
+ services.AddSingleton(sp => _mockAuthorServerPipeline.BrowserClient);
+ };
+
+
+
+ _mockIdPPipeline2.Initialize(enableLogging: true);
+ Debug.Assert(_mockIdPPipeline2.BrowserClient != null, "_mockIdPPipeline2.BrowserClient != null");
+ _mockIdPPipeline2.BrowserClient.AllowAutoRedirect = false;
+
+ _mockIdPPipeline2.Communities.Add(new Community
+ {
+ Name = "udap://idp-community-2",
+ Enabled = true,
+ Default = true,
+ Anchors = new[]
+ {
+ new Anchor
+ {
+ BeginDate = _community2Anchor.NotBefore.ToUniversalTime(),
+ EndDate = _community2Anchor.NotAfter.ToUniversalTime(),
+ Name = _community2Anchor.Subject,
+ Community = "udap://idp-community-2",
+ Certificate = _community2Anchor.ToPemFormat(),
+ Thumbprint = _community2Anchor.Thumbprint,
+ Enabled = true,
+ Intermediates = new List()
+ {
+ new()
+ {
+ BeginDate = _community2IntermediateCert.NotBefore.ToUniversalTime(),
+ EndDate = _community2IntermediateCert.NotAfter.ToUniversalTime(),
+ Name = _community2IntermediateCert.Subject,
+ Certificate = _community2IntermediateCert.ToPemFormat(),
+ Thumbprint = _community2IntermediateCert.Thumbprint,
+ Enabled = true
+ }
+ }
+ }
+ }
+ });
+
+ _mockIdPPipeline2.IdentityScopes.Add(new IdentityResources.OpenId());
+ _mockIdPPipeline2.IdentityScopes.Add(new UdapIdentityResources.Profile());
+ _mockIdPPipeline2.IdentityScopes.Add(new UdapIdentityResources.Udap());
+ _mockIdPPipeline2.IdentityScopes.Add(new IdentityResources.Email());
+ _mockIdPPipeline2.IdentityScopes.Add(new UdapIdentityResources.FhirUser());
+
+ _mockIdPPipeline2.Users.Add(new TestUser
+ {
+ SubjectId = "bob",
+ Username = "bob",
+ Claims = new[]
+ {
+ new Claim("name", "Bob Loblaw"),
+ new Claim("email", "bob@loblaw.com"),
+ new Claim("role", "Attorney"),
+ new Claim("hl7_identifier", "123")
+ }
+ });
+
+ // Allow pipeline to sign in during Login
+ _mockIdPPipeline2.Subject = new IdentityServerUser("bob").CreatePrincipal();
+ }
+
///
///
///
@@ -292,11 +421,13 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli
var clientId = resultDocument.ClientId!;
+ var dynamicIdp = _mockAuthorServerPipeline.ApplicationServices.GetRequiredService();
+ dynamicIdp.Name = _mockIdPPipeline.BaseUrl;
//////////////////////
// ClientAuthorize
//////////////////////
-
+
// Data Holder's Auth Server validates Identity Provider's Server software statement
var clientState = Guid.NewGuid().ToString();
@@ -335,7 +466,7 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli
queryParams.Single(q => q.Key == "state").Value.Should().BeEquivalentTo(clientState);
queryParams.Single(q => q.Key == "idp").Value.Should().BeEquivalentTo("https://idpserver");
- var schemes = await _schemeProvider.GetAllSchemesAsync();
+ var schemes = await _mockAuthorServerPipeline.Resolve().GetAllSchemesAsync();
var sb = new StringBuilder();
sb.Append("https://server/externallogin/challenge?"); // built in UdapAccount/Login/Index.cshtml.cs
@@ -377,24 +508,25 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli
// response after discovery and registration
_mockAuthorServerPipeline.BrowserClient.AllowCookies = true; // Need to set the idsrv cookie so calls to /authorize will succeed
- _mockAuthorServerPipeline.BrowserClient.GetXsrfCookie("https://server/signin-tieredoauth", new TieredOAuthAuthenticationOptions().CorrelationCookie.Name).Should().BeNull();
+ _mockAuthorServerPipeline.BrowserClient.GetXsrfCookie("https://server/federation/udap-tiered/signin", new TieredOAuthAuthenticationOptions().CorrelationCookie.Name!).Should().BeNull();
var backChannelChallengeResponse = await _mockAuthorServerPipeline.BrowserClient.GetAsync(clientAuthorizeUrl);
- _mockAuthorServerPipeline.BrowserClient.GetXsrfCookie("https://server/signin-tieredoauth", new TieredOAuthAuthenticationOptions().CorrelationCookie.Name).Should().NotBeNull();
+ _mockAuthorServerPipeline.BrowserClient.GetXsrfCookie("https://server/federation/udap-tiered/signin", new TieredOAuthAuthenticationOptions().CorrelationCookie.Name!).Should().NotBeNull();
backChannelChallengeResponse.StatusCode.Should().Be(HttpStatusCode.Redirect, await backChannelChallengeResponse.Content.ReadAsStringAsync());
backChannelChallengeResponse.Headers.Location.Should().NotBeNull();
backChannelChallengeResponse.Headers.Location!.AbsoluteUri.Should().StartWith("https://idpserver/connect/authorize");
// _testOutputHelper.WriteLine(backChannelChallengeResponse.Headers.Location!.AbsoluteUri);
- var backChannelClientId = QueryHelpers.ParseQuery(backChannelChallengeResponse.Headers.Location.Query).Single(p => p.Key == "client_id").Value.ToString();
+ QueryHelpers.ParseQuery(backChannelChallengeResponse.Headers.Location.Query).Single(p => p.Key == "client_id").Value.Should().NotBeEmpty();
var backChannelState = QueryHelpers.ParseQuery(backChannelChallengeResponse.Headers.Location.Query).Single(p => p.Key == "state").Value.ToString();
backChannelState.Should().NotBeNullOrEmpty();
var idpClient = _mockIdPPipeline.Clients.Single(c => c.ClientName == "AuthServer Client");
idpClient.AlwaysIncludeUserClaimsInIdToken.Should().BeTrue();
-
+
+ Debug.Assert(_mockIdPPipeline.BrowserClient != null, "_mockIdPPipeline.BrowserClient != null");
var backChannelAuthResult = await _mockIdPPipeline.BrowserClient.GetAsync(backChannelChallengeResponse.Headers.Location);
@@ -414,9 +546,9 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli
// _testOutputHelper.WriteLine(authorizeCallbackResult.Headers.Location!.OriginalString);
authorizeCallbackResult.StatusCode.Should().Be(HttpStatusCode.Redirect, await authorizeCallbackResult.Content.ReadAsStringAsync());
authorizeCallbackResult.Headers.Location.Should().NotBeNull();
- authorizeCallbackResult.Headers.Location!.AbsoluteUri.Should().StartWith("https://server/signin-tieredoauth?");
+ authorizeCallbackResult.Headers.Location!.AbsoluteUri.Should().StartWith("https://server/federation/udap-tiered/signin?");
- var backChannelCode = QueryHelpers.ParseQuery(authorizeCallbackResult.Headers.Location.Query).Single(p => p.Key == "code").Value.ToString();
+ QueryHelpers.ParseQuery(authorizeCallbackResult.Headers.Location.Query).Single(p => p.Key == "code").Value.Should().NotBeEmpty();
//
// Validate backchannel state is the same
@@ -431,7 +563,7 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli
_mockAuthorServerPipeline.GetSessionCookie().Should().BeNull();
_mockAuthorServerPipeline.BrowserClient.GetCookie("https://server", "idsrv").Should().BeNull();
- // Run Auth Server /signin-tieredoauth This is the Registered scheme callback endpoint
+ // Run Auth Server /federation/udap-tiered/signin This is the Registered scheme callback endpoint
// Allow one redirect to run /connect/token.
// Sets Cookies: idsrv.external idsrv.session, and idsrv
// Backchannel calls:
@@ -447,7 +579,7 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli
_mockAuthorServerPipeline.BrowserClient.AllowCookies = true;
- // "https://server/signin-tieredoauth?..."
+ // "https://server/federation/udap-tiered/signin?..."
var schemeCallbackResult = await _mockAuthorServerPipeline.BrowserClient.GetAsync(authorizeCallbackResult.Headers.Location!.AbsoluteUri);
@@ -465,8 +597,8 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli
// Check the IdToken in the back channel. Ensure the HL7_Identifier is in the claims
//
// _testOutputHelper.WriteLine(_mockIdPPipeline.IdToken.ToString());
-
- _mockIdPPipeline.IdToken.Claims.Should().Contain(c => c.Type == "hl7_identifier");
+ _mockIdPPipeline.IdToken.Should().NotBeNull();
+ _mockIdPPipeline.IdToken!.Claims.Should().Contain(c => c.Type == "hl7_identifier");
_mockIdPPipeline.IdToken.Claims.Single(c => c.Type == "hl7_identifier").Value.Should().Be("123");
// Run the authServer https://server/connect/authorize/callback
@@ -507,19 +639,15 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli
.Build();
- var udapClient = new UdapClient(
- _mockAuthorServerPipeline.BrowserClient,
- _mockAuthorServerPipeline.Resolve(),
- _mockAuthorServerPipeline.Resolve>(),
- _mockAuthorServerPipeline.Resolve>());
+ dynamicIdp.Name = null; // Influence UdapClient resolution in AddTieredOAuthForTests.
+ var udapClient = _mockAuthorServerPipeline.Resolve();
+
var accessToken = await udapClient.ExchangeCodeForTokenResponse(tokenRequest);
accessToken.Should().NotBeNull();
accessToken.IdentityToken.Should().NotBeNull();
var jwt = new JwtSecurityToken(accessToken.IdentityToken);
-
- var at = new JwtSecurityToken(accessToken.AccessToken);
-
+ new JwtSecurityToken(accessToken.AccessToken).Should().NotBeNull();
using var jsonDocument = JsonDocument.Parse(jwt.Payload.SerializeToJson());
var formattedStatement = JsonSerializer.Serialize(
@@ -571,48 +699,311 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli
*/
}
- private async Task RegisterClientWithAuthServer()
+ [Fact] //(Skip = "Dynamic Tiered OAuth Provider WIP")]
+ public async Task Tiered_OAuth_With_DynamicProvider()
{
- var clientCert = new X509Certificate2("CertStore/issued/fhirLabsApiClientLocalhostCert.pfx", "udap-test");
+ // Register client with auth server
+ var resultDocument = await RegisterClientWithAuthServer();
+ _mockAuthorServerPipeline.RemoveSessionCookie();
+ _mockAuthorServerPipeline.RemoveLoginCookie();
+ resultDocument.Should().NotBeNull();
+ resultDocument!.ClientId.Should().NotBeNull();
+
+ var clientId = resultDocument.ClientId!;
- // await _mockAuthorServerPipeline.LoginAsync("bob");
+ var dynamicIdp = _mockAuthorServerPipeline.ApplicationServices.GetRequiredService();
+ dynamicIdp.Name = _mockIdPPipeline2.BaseUrl;
+
+ //////////////////////
+ // ClientAuthorize
+ //////////////////////
+
+ // Data Holder's Auth Server validates Identity Provider's Server software statement
- var document = UdapDcrBuilderForAuthorizationCode
- .Create(clientCert)
- .WithAudience(UdapAuthServerPipeline.RegistrationEndpoint)
- .WithExpiration(TimeSpan.FromMinutes(5))
- .WithJwtId()
- .WithClientName("mock tiered test")
- .WithLogoUri("https://example.com/logo.png")
- .WithContacts(new HashSet
+ var clientState = Guid.NewGuid().ToString();
+
+ var clientAuthorizeUrl = _mockAuthorServerPipeline.CreateAuthorizeUrl(
+ clientId: clientId,
+ responseType: "code",
+ scope: "udap openid user/*.read",
+ redirectUri: "https://code_client/callback",
+ state: clientState,
+ extra: new
{
- "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com"
- })
- .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue)
- .WithScope("udap openid user/*.read")
- .WithResponseTypes(new List { "code" })
- .WithRedirectUrls(new List { "https://code_client/callback" })
- .Build();
+ idp = "https://idpserver2?community=udap://idp-community-2"
+ });
+
+ _mockAuthorServerPipeline.BrowserClient.AllowAutoRedirect = false;
+ // The BrowserHandler.cs will normally set the cookie to indicate user signed in.
+ // We want to skip that and get a redirect to the login page
+ _mockAuthorServerPipeline.BrowserClient.AllowCookies = false;
+ var response = await _mockAuthorServerPipeline.BrowserClient.GetAsync(clientAuthorizeUrl);
+ response.StatusCode.Should().Be(HttpStatusCode.Redirect, await response.Content.ReadAsStringAsync());
+ response.Headers.Location.Should().NotBeNull();
+ response.Headers.Location!.AbsoluteUri.Should().Contain("https://server/Account/Login");
+ // _testOutputHelper.WriteLine(response.Headers.Location!.AbsoluteUri);
+ var queryParams = QueryHelpers.ParseQuery(response.Headers.Location.Query);
+ queryParams.Should().Contain(p => p.Key == "ReturnUrl");
+ queryParams.Should().NotContain(p => p.Key == "code");
+ queryParams.Should().NotContain(p => p.Key == "state");
+
+
+ // Pull the inner query params from the ReturnUrl
+ var returnUrl = queryParams.Single(p => p.Key == "ReturnUrl").Value.ToString();
+ returnUrl.Should().StartWith("/connect/authorize/callback?");
+ queryParams = QueryHelpers.ParseQuery(returnUrl);
+ queryParams.Single(q => q.Key == "scope").Value.ToString().Should().Contain("udap openid user/*.read");
+ queryParams.Single(q => q.Key == "state").Value.Should().BeEquivalentTo(clientState);
+ queryParams.Single(q => q.Key == "idp").Value.Should().BeEquivalentTo("https://idpserver2?community=udap://idp-community-2");
+
+ // var schemes = await _mockAuthorServerPipeline.Resolve().GetAllSchemeNamesAsync();
+
+
+ var sb = new StringBuilder();
+ sb.Append("https://server/externallogin/challenge?"); // built in UdapAccount/Login/Index.cshtml.cs
+ sb.Append("scheme=").Append(TieredOAuthAuthenticationDefaults.AuthenticationScheme);
+ sb.Append("&returnUrl=").Append(Uri.EscapeDataString(returnUrl));
+ clientAuthorizeUrl = sb.ToString();
+
+ //////////////////////////////////
+ //
+ // IdPDiscovery
+ // IdPRegistration
+ // IdPAuthAccess
+ //
+ //////////////////////////////////
+
+
+ // Auto Dynamic registration between Auth Server and Identity Provider happens here.
+ // /Challenge?
+ // ctx.ChallengeAsync -> launch registered scheme. In this case the TieredOauthAuthenticationHandler
+ // see: OnExternalLoginChallenge and Challenge(props, scheme) in ExternalLogin/Challenge.cshtml.cs or UdapTieredLogin/Challenge.cshtml.cs
+ // Backchannel
+ // Discovery
+ // Auto registration
+ // externalloging/challenge or in the Udap implementation it is the UdapAccount/Login/Index.cshtml.cs. XSRF cookie is set here.
+
+ // *** We are here after the request to the IdPs /authorize call. If the client is registered already then Discovery and Reg is skipped ***
+ //
+ // Authentication request (/authorize?)
+ // User logs in at IdP
+ // Authentication response
+ // Token request
+ // Data Holder incorporates user input into authorization decision
+ //
+
+
+
+ // response after discovery and registration
+ _mockAuthorServerPipeline.BrowserClient.AllowCookies = true; // Need to set the idsrv cookie so calls to /authorize will succeed
- var signedSoftwareStatement =
- SignedSoftwareStatementBuilder
- .Create(clientCert, document)
- .Build();
-
- var requestBody = new UdapRegisterRequest
- (
- signedSoftwareStatement,
- UdapConstants.UdapVersionsSupportedValue,
- new string[] { }
+ _mockAuthorServerPipeline.BrowserClient.GetXsrfCookie("https://server/federation/udap-tiered/signin", new TieredOAuthAuthenticationOptions().CorrelationCookie.Name!).Should().BeNull();
+ var backChannelChallengeResponse = await _mockAuthorServerPipeline.BrowserClient.GetAsync(clientAuthorizeUrl);
+ _mockAuthorServerPipeline.BrowserClient.GetXsrfCookie("https://server/federation/udap-tiered/signin", new TieredOAuthAuthenticationOptions().CorrelationCookie.Name!).Should().NotBeNull();
+
+ backChannelChallengeResponse.StatusCode.Should().Be(HttpStatusCode.Redirect, await backChannelChallengeResponse.Content.ReadAsStringAsync());
+ backChannelChallengeResponse.Headers.Location.Should().NotBeNull();
+ backChannelChallengeResponse.Headers.Location!.AbsoluteUri.Should().StartWith("https://idpserver2/connect/authorize");
+
+ // _testOutputHelper.WriteLine(backChannelChallengeResponse.Headers.Location!.AbsoluteUri);
+ QueryHelpers.ParseQuery(backChannelChallengeResponse.Headers.Location.Query).Single(p => p.Key == "client_id").Value.Should().NotBeEmpty();
+ var backChannelState = QueryHelpers.ParseQuery(backChannelChallengeResponse.Headers.Location.Query).Single(p => p.Key == "state").Value.ToString();
+ backChannelState.Should().NotBeNullOrEmpty();
+
+
+ var idpClient = _mockIdPPipeline2.Clients.Single(c => c.ClientName == "AuthServer Client");
+ idpClient.AlwaysIncludeUserClaimsInIdToken.Should().BeTrue();
+
+ _mockIdPPipeline2.BrowserClient.Should().NotBeNull();
+ var backChannelAuthResult = await _mockIdPPipeline2.BrowserClient!.GetAsync(backChannelChallengeResponse.Headers.Location);
+
+
+ backChannelAuthResult.StatusCode.Should().Be(HttpStatusCode.Redirect, await backChannelAuthResult.Content.ReadAsStringAsync());
+ // _testOutputHelper.WriteLine(backChannelAuthResult.Headers.Location!.AbsoluteUri);
+ backChannelAuthResult.Headers.Location!.AbsoluteUri.Should().StartWith("https://idpserver2/Account/Login");
+
+ // Run IdP /Account/Login
+ var loginCallbackResult = await _mockIdPPipeline2.BrowserClient.GetAsync(backChannelAuthResult.Headers.Location!.AbsoluteUri);
+ loginCallbackResult.StatusCode.Should().Be(HttpStatusCode.Redirect, await backChannelAuthResult.Content.ReadAsStringAsync());
+ // _testOutputHelper.WriteLine(loginCallbackResult.Headers.Location!.OriginalString);
+ loginCallbackResult.Headers.Location!.OriginalString.Should().StartWith("/connect/authorize/callback?");
+
+ // Run IdP /connect/authorize/callback
+ var authorizeCallbackResult = await _mockIdPPipeline2.BrowserClient.GetAsync(
+ $"https://idpserver2{loginCallbackResult.Headers.Location!.OriginalString}");
+ // _testOutputHelper.WriteLine(authorizeCallbackResult.Headers.Location!.OriginalString);
+ authorizeCallbackResult.StatusCode.Should().Be(HttpStatusCode.Redirect, await authorizeCallbackResult.Content.ReadAsStringAsync());
+ authorizeCallbackResult.Headers.Location.Should().NotBeNull();
+ authorizeCallbackResult.Headers.Location!.AbsoluteUri.Should().StartWith("https://server/federation/udap-tiered/signin?");
+
+ var backChannelCode = QueryHelpers.ParseQuery(authorizeCallbackResult.Headers.Location.Query).Single(p => p.Key == "code").Value.ToString();
+ backChannelCode.Should().NotBeEmpty();
+
+ //
+ // Validate backchannel state is the same
+ //
+ backChannelState.Should().BeEquivalentTo(_mockAuthorServerPipeline.GetClientState(authorizeCallbackResult));
+
+ //
+ // Ensure client state and back channel state never become the same.
+ //
+ clientState.Should().NotBeEquivalentTo(backChannelState);
+
+ _mockAuthorServerPipeline.GetSessionCookie().Should().BeNull();
+ _mockAuthorServerPipeline.BrowserClient.GetCookie("https://server", "idsrv").Should().BeNull();
+
+ // Run Auth Server /federation/idpserver2/signin This is the Registered scheme callback endpoint
+ // Allow one redirect to run /connect/token.
+ // Sets Cookies: idsrv.external idsrv.session, and idsrv
+ // Backchannel calls:
+ // POST https://idpserver2/connect/token
+ // GET https://idpserver2/.well-known/openid-configuration
+ // GET https://idpserver2/.well-known/openid-configuration/jwks
+ //
+ // Redirects to https://server/externallogin/callback
+ //
+
+ _mockAuthorServerPipeline.BrowserClient.AllowAutoRedirect = true;
+ _mockAuthorServerPipeline.BrowserClient.StopRedirectingAfter = 1;
+ _mockAuthorServerPipeline.BrowserClient.AllowCookies = true;
+
+
+ // "https://server/federation/idpserver2/signin?..."
+ var schemeCallbackResult = await _mockAuthorServerPipeline.BrowserClient.GetAsync(authorizeCallbackResult.Headers.Location!.AbsoluteUri);
+
+
+ schemeCallbackResult.StatusCode.Should().Be(HttpStatusCode.Redirect, await schemeCallbackResult.Content.ReadAsStringAsync());
+ schemeCallbackResult.Headers.Location.Should().NotBeNull();
+ schemeCallbackResult.Headers.Location!.OriginalString.Should().StartWith("/connect/authorize/callback?");
+ // _testOutputHelper.WriteLine(schemeCallbackResult.Headers.Location!.OriginalString);
+ // Validate Cookies
+ _mockAuthorServerPipeline.GetSessionCookie().Should().NotBeNull();
+ // _testOutputHelper.WriteLine(_mockAuthorServerPipeline.GetSessionCookie()!.Value);
+ // _mockAuthorServerPipeline.BrowserClient.GetCookie("https://server", "idsrv").Should().NotBeNull();
+ //TODO assert match State and nonce between Auth Server and IdP
+
+ //
+ // Check the IdToken in the back channel. Ensure the HL7_Identifier is in the claims
+ //
+ // _testOutputHelper.WriteLine(_mockIdPPipeline2.IdToken.ToString());
+
+ _mockIdPPipeline2.IdToken.Should().NotBeNull();
+ _mockIdPPipeline2.IdToken!.Claims.Should().Contain(c => c.Type == "hl7_identifier");
+ _mockIdPPipeline2.IdToken.Claims.Single(c => c.Type == "hl7_identifier").Value.Should().Be("123");
+
+ // Run the authServer https://server/connect/authorize/callback
+ _mockAuthorServerPipeline.BrowserClient.AllowAutoRedirect = false;
+
+ var clientCallbackResult = await _mockAuthorServerPipeline.BrowserClient.GetAsync(
+ $"https://server{schemeCallbackResult.Headers.Location!.OriginalString}");
+
+ clientCallbackResult.StatusCode.Should().Be(HttpStatusCode.Redirect, await clientCallbackResult.Content.ReadAsStringAsync());
+ clientCallbackResult.Headers.Location.Should().NotBeNull();
+ clientCallbackResult.Headers.Location!.AbsoluteUri.Should().StartWith("https://code_client/callback?");
+ // _testOutputHelper.WriteLine(clientCallbackResult.Headers.Location!.AbsoluteUri);
+
+
+ // Assert match state and nonce between User and Auth Server
+ clientState.Should().BeEquivalentTo(_mockAuthorServerPipeline.GetClientState(clientCallbackResult));
+
+ queryParams = QueryHelpers.ParseQuery(clientCallbackResult.Headers.Location.Query);
+ queryParams.Should().Contain(p => p.Key == "code");
+ var code = queryParams.Single(p => p.Key == "code").Value.ToString();
+ // _testOutputHelper.WriteLine($"Code: {code}");
+ ////////////////////////////
+ //
+ // ClientAuthAccess
+ //
+ ///////////////////////////
+
+ // Get a Access Token (Cash in the code)
+
+ var privateCerts = _mockAuthorServerPipeline.Resolve();
+
+ var tokenRequest = AccessTokenRequestForAuthorizationCodeBuilder.Create(
+ clientId,
+ "https://server/connect/token",
+ privateCerts.IssuedCertificates.Select(ic => ic.Certificate).First(),
+ "https://code_client/callback",
+ code)
+ .Build();
+
+
+ dynamicIdp.Name = null; // Influence UdapClient resolution in AddTieredOAuthForTests.
+ var udapClient = _mockAuthorServerPipeline.Resolve();
+
+ var accessToken = await udapClient.ExchangeCodeForTokenResponse(tokenRequest);
+ accessToken.Should().NotBeNull();
+ accessToken.IdentityToken.Should().NotBeNull();
+ var jwt = new JwtSecurityToken(accessToken.IdentityToken);
+ new JwtSecurityToken(accessToken.AccessToken).Should().NotBeNull();
+
+
+ using var jsonDocument = JsonDocument.Parse(jwt.Payload.SerializeToJson());
+ var formattedStatement = JsonSerializer.Serialize(
+ jsonDocument,
+ new JsonSerializerOptions { WriteIndented = true }
);
- var response = await _mockAuthorServerPipeline.BrowserClient.PostAsync(
- UdapAuthServerPipeline.RegistrationEndpoint,
- new StringContent(JsonSerializer.Serialize(requestBody), new MediaTypeHeaderValue("application/json")));
+ var formattedHeader = Base64UrlEncoder.Decode(jwt.EncodedHeader);
- response.StatusCode.Should().Be(HttpStatusCode.Created);
- var resultDocument = await response.Content.ReadFromJsonAsync();
+ _testOutputHelper.WriteLine(formattedHeader);
+ _testOutputHelper.WriteLine(formattedStatement);
+
+
+
+ // udap.org Tiered 4.3
+ // aud: client_id of Resource Holder (matches client_id in Resource Holder request in Step 3.4)
+ jwt.Claims.Should().Contain(c => c.Type == "aud");
+ jwt.Claims.Single(c => c.Type == "aud").Value.Should().Be(clientId);
+
+ // iss: IdP’s unique identifying URI (matches idp parameter from Step 2)
+ jwt.Claims.Should().Contain(c => c.Type == "iss");
+ jwt.Claims.Single(c => c.Type == "iss").Value.Should().Be(UdapAuthServerPipeline.BaseUrl);
- return resultDocument;
+ jwt.Claims.Should().Contain(c => c.Type == "hl7_identifier");
+ jwt.Claims.Single(c => c.Type == "hl7_identifier").Value.Should().Be("123");
+
+
+
+
+ // sub: unique identifier for user in namespace of issuer, i.e. iss + sub is globally unique
+
+ // TODO: Currently the sub is the code given at access time. Maybe that is OK? I could put the clientId in from
+ // backchannel. But I am not sure I want to show that. After all it is still globally unique.
+ // jwt.Claims.Should().Contain(c => c.Type == "sub");
+ // jwt.Claims.Single(c => c.Type == "sub").Value.Should().Be(backChannelClientId);
+
+ // jwt.Claims.Should().Contain(c => c.Type == "sub");
+ // jwt.Claims.Single(c => c.Type == "sub").Value.Should().Be(backChannelCode);
+
+ // Todo: Nonce
+ // Todo: Validate claims. Like missing name and other identity claims. Maybe add a hl7_identifier
+ // Why is idp:TieredOAuth in the returned claims?
+
+ }
+
+ private async Task RegisterClientWithAuthServer()
+ {
+ var clientCert = new X509Certificate2("CertStore/issued/fhirLabsApiClientLocalhostCert.pfx", "udap-test");
+
+ var udapClient = _mockAuthorServerPipeline.Resolve();
+
+ //
+ // Typically the client would validate a server before proceeding to registration.
+ //
+ udapClient.UdapServerMetaData = new UdapMetadata(new Mock().Object, new Mock>().Object)
+ { RegistrationEndpoint = UdapAuthServerPipeline.RegistrationEndpoint };
+
+
+ var documentResponse = await udapClient.RegisterAuthCodeClient(
+ clientCert,
+ "udap openid user/*.read",
+ "https://server/udap.logo.48x48.png",
+ new List { "https://code_client/callback" });
+
+ documentResponse.GetError().Should().BeNull();
+
+ return documentResponse;
}
-}
+}
\ No newline at end of file
diff --git a/_tests/UdapServer.Tests/Hl7RegistrationTests.cs b/_tests/UdapServer.Tests/Hl7RegistrationTests.cs
index f5f4363b..cf52f34e 100644
--- a/_tests/UdapServer.Tests/Hl7RegistrationTests.cs
+++ b/_tests/UdapServer.Tests/Hl7RegistrationTests.cs
@@ -191,7 +191,7 @@ public async Task RegistrationSuccess_authorization_code_Test()
IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()),
JwtId = jwtId,
ClientName = "udapTestClient",
- LogoUri = "https://example.com/logo.png",
+ LogoUri = "https://avatars.githubusercontent.com/u/77421324?s=48&v=4",
Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" },
GrantTypes = new HashSet { "authorization_code", "refresh_token" },
ResponseTypes = new HashSet { "code" },
@@ -1246,7 +1246,7 @@ public async Task RegistrationInvalidClientMetadata_Invalid_GrantType_Test()
IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()),
JwtId = jwtId,
ClientName = "udapTestClient",
- LogoUri = "https://example.com/logo.png",
+ LogoUri = "https://avatars.githubusercontent.com/u/77421324?s=48&v=4",
Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" },
GrantTypes = new HashSet { "authorization_code", "refresh_bad" },
ResponseTypes = new HashSet { "code" },
@@ -1289,7 +1289,7 @@ public async Task RegistrationInvalidClientMetadata_Invalid_GrantType_Test()
IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()),
JwtId = jwtId,
ClientName = "udapTestClient",
- LogoUri = "https://example.com/logo.png",
+ LogoUri = "https://avatars.githubusercontent.com/u/77421324?s=48&v=4",
Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" },
GrantTypes = new HashSet { "refresh_bad" },
ResponseTypes = new HashSet { "code" },
@@ -1350,7 +1350,7 @@ public async Task RegistrationInvalidClientMetadata_responseTypesMissing_Test()
IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()),
JwtId = jwtId,
ClientName = "udapTestClient",
- LogoUri = "https://example.com/logo.png",
+ LogoUri = "https://avatars.githubusercontent.com/u/77421324?s=48&v=4",
Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" },
GrantTypes = new HashSet { "authorization_code" },
TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue,
diff --git a/_tests/UdapServer.Tests/IntegrationRegistrationTests.cs b/_tests/UdapServer.Tests/IntegrationRegistrationTests.cs
index 66ffc06a..fd6cfa1f 100644
--- a/_tests/UdapServer.Tests/IntegrationRegistrationTests.cs
+++ b/_tests/UdapServer.Tests/IntegrationRegistrationTests.cs
@@ -30,6 +30,7 @@
using Udap.Model.Registration;
using Udap.Model.Statement;
using Udap.Server.Configuration;
+using Udap.Server.Configuration.DependencyInjection;
using Udap.Server.DbContexts;
using Udap.Server.Options;
using Udap.Server.Registration;
@@ -313,7 +314,8 @@ public async Task GoodIUdapClientRegistrationStore()
var builder = services.AddUdapServerBuilder();
- builder.AddUdapServerConfiguration();
+ builder.AddUdapServerConfiguration()
+ .AddUdapInMemoryApiScopes(new List(){new ApiScope("system/Practitioner.read")});
services.AddIdentityServer();
@@ -366,7 +368,7 @@ public async Task GoodIUdapClientRegistrationStore()
GrantTypes = new HashSet { "client_credentials" },
ResponseTypes = new HashSet { "authorization_code" },
TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue,
- Scope = "system/Patient.* system/Practitioner.read"
+ Scope = "system/Practitioner.read"
};
diff --git a/_tests/UdapServer.Tests/UdapServer.Tests.csproj b/_tests/UdapServer.Tests/UdapServer.Tests.csproj
index c399fd72..2d60d880 100644
--- a/_tests/UdapServer.Tests/UdapServer.Tests.csproj
+++ b/_tests/UdapServer.Tests/UdapServer.Tests.csproj
@@ -1,7 +1,7 @@

- net7.0
+ net7.0
enable
enable
@@ -16,11 +16,23 @@
-
+
+
+
-
+
+ PreserveNewest
+ true
+ PreserveNewest
+
+
+ PreserveNewest
+ true
+ PreserveNewest
+
+
PreserveNewest
true
PreserveNewest
@@ -106,18 +118,27 @@
Always
+
+ Always
+
Always
Always
+
+ Always
+
PreserveNewest
Always
+
+ Always
+
diff --git a/_tests/UdapServer.Tests/Validators/UdapDCRValidatorTests.cs b/_tests/UdapServer.Tests/Validators/UdapDCRValidatorTests.cs
index e712f34e..3c20d1f0 100644
--- a/_tests/UdapServer.Tests/Validators/UdapDCRValidatorTests.cs
+++ b/_tests/UdapServer.Tests/Validators/UdapDCRValidatorTests.cs
@@ -1,10 +1,21 @@
-using FluentAssertions;
+#region (c) 2023 Joseph Shook. All rights reserved.
+// /*
+// Authors:
+// Joseph Shook Joseph.Shook@Surescripts.com
+//
+// See LICENSE in the project root for license information.
+// */
+#endregion
+
+using System.Net;
+using Duende.IdentityServer.Stores;
+using FluentAssertions;
using IdentityModel;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
-using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using Moq;
+using Moq.Protected;
using Udap.Common.Certificates;
using Udap.Model;
using Udap.Model.Registration;
@@ -32,34 +43,44 @@ public DateTime UtcNow
[Fact]
public async Task ValidateLogo_Missing()
{
- var document = BuildUdapDcrValidator(out var validator);
-
- validator.ValidateLogoUri(document, out var errorResponse).Should().BeFalse();
-
+ var document = BuildUdapDcrValidator(GetHttpClientForLogo("image/png"), out var validator);
+ var (successFlag, errorResponse) = await validator.ValidateLogoUri(document);
+ successFlag.Should().BeFalse();
errorResponse.Should().NotBeNull();
errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidClientMetadata);
errorResponse.ErrorDescription.Should().Be($"{UdapDynamicClientRegistrationErrorDescriptions.LogoMissing}");
}
[Fact]
- public async Task ValidateLogo_InvalidFileType()
+ public async Task ValidateLogo_ValidContentType()
{
- var document = BuildUdapDcrValidator(out var validator);
- document.LogoUri = "https://localhost/logo";
- validator.ValidateLogoUri(document, out var errorResponse).Should().BeFalse();
+ var document = BuildUdapDcrValidator(GetHttpClientForLogo("image/png"), out var validator);
+ document.LogoUri = "https://avatars.githubusercontent.com/u/77421324?s=48&v=4";
+ var (successFlag, errorResponse) = await validator.ValidateLogoUri(document);
+ successFlag.Should().BeTrue();
+ errorResponse.Should().BeNull();
+ }
+
+ [Fact]
+ public async Task ValidateLogo_InvalidContentType()
+ {
+ var document = BuildUdapDcrValidator(GetHttpClientForLogo("image/tiff"), out var validator);
+ document.LogoUri = "https://localhost/logo";
+ var (successFlag, errorResponse) = await validator.ValidateLogoUri(document);
+ successFlag.Should().BeFalse();
errorResponse.Should().NotBeNull();
errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidClientMetadata);
- errorResponse.ErrorDescription.Should().Be($"{UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidFileType}");
+ errorResponse.ErrorDescription.Should().Be($"{UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidContentType}");
}
[Fact]
public async Task ValidateLogo_InvalidScheme()
{
- var document = BuildUdapDcrValidator(out var validator);
+ var document = BuildUdapDcrValidator(GetHttpClientForLogo("image/png"), out var validator);
document.LogoUri = "http://localhost/logo.png";
- validator.ValidateLogoUri(document, out var errorResponse).Should().BeFalse();
-
+ var (successFlag, errorResponse) = await validator.ValidateLogoUri(document);
+ successFlag.Should().BeFalse();
errorResponse.Should().NotBeNull();
errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidClientMetadata);
errorResponse.ErrorDescription.Should().Be($"{UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidScheme}");
@@ -68,10 +89,10 @@ public async Task ValidateLogo_InvalidScheme()
[Fact]
public async Task ValidateLogo_InvalidUri()
{
- var document = BuildUdapDcrValidator(out var validator);
+ var document = BuildUdapDcrValidator(GetHttpClientForLogo("image/png"), out var validator);
document.LogoUri = "http:/localhost/logo.png"; // missing a slash
- validator.ValidateLogoUri(document, out var errorResponse).Should().BeFalse();
-
+ var (successFlag, errorResponse) = await validator.ValidateLogoUri(document);
+ successFlag.Should().BeFalse();
errorResponse.Should().NotBeNull();
errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidClientMetadata);
errorResponse.ErrorDescription.Should().Be($"{UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidUri}");
@@ -110,12 +131,30 @@ public async Task ValidateJti_And_ReplayTest()
_clock.UtcNowFunc = () => UtcNow;
+ var mockHandler = new Mock(MockBehavior.Strict);
+ mockHandler.Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny()
+ )
+ .ReturnsAsync((HttpRequestMessage request, CancellationToken token) =>
+ {
+ HttpResponseMessage response = new HttpResponseMessage();
+
+ return response;
+ });
+
+ var httpClient = new HttpClient(mockHandler.Object);
+
var validator = new UdapDynamicClientRegistrationValidator(
new Mock(new Mock>().Object).Object,
+ httpClient,
new TestReplayCache(_clock),
serverSettings,
mockHttpContextAccessor.Object,
new DefaultScopeExpander(),
+ new Mock().Object,
new Mock>().Object);
@@ -158,6 +197,7 @@ public async Task ValidateJti_And_ReplayTest()
private static UdapDynamicClientRegistrationDocument BuildUdapDcrValidator(
+ HttpClient httpClient,
out UdapDynamicClientRegistrationValidator validator)
{
var _clock = new StubClock();
@@ -189,16 +229,41 @@ private static UdapDynamicClientRegistrationDocument BuildUdapDcrValidator(
context.Request.Host = new HostString("localhost:5001");
context.Request.Path = "/connect/register";
mockHttpContextAccessor.Setup(_ => _.HttpContext).Returns(context);
-
+
validator = new UdapDynamicClientRegistrationValidator(
new Mock(new Mock>().Object).Object,
+ httpClient,
new TestReplayCache(_clock),
serverSettings,
mockHttpContextAccessor.Object,
new DefaultScopeExpander(),
+ new Mock().Object,
new Mock>().Object);
return document;
}
+
+ private HttpClient GetHttpClientForLogo(string? contentType)
+ {
+ var mockHandler = new Mock(MockBehavior.Strict);
+ mockHandler.Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny