diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index fd9a39d8..9d58272e 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "8.0.1", + "version": "8.0.2", "commands": [ "dotnet-ef" ] diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index e6dab09a..25cf0319 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -23,7 +23,6 @@ jobs: source-url: ${{ env.REPOSITORY_URL }} env: NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} - - name: Generate PKI run: dotnet test -c Release _tests/Udap.PKI.Generator/Udap.PKI.Generator.csproj - name: Build and Restore dependencies diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 8c6b64ba..f70bcd7e 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -6,8 +6,7 @@ name: .NET on: push: # branches: [ "main" ] - branches-ignore: - - 'develop' + branches-ignore: ["develop"] pull_request: branches: [ "main" ] @@ -15,7 +14,7 @@ jobs: build: runs-on: ubuntu-latest - timeout-minutes: 7 + timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v3 diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 537c9152..fd324ca9 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -31,7 +31,11 @@ jobs: dotnet pack -v normal -c Release --include-source -p:PackageVersion=${VERSION} ./Udap.Common/Udap.Common.csproj dotnet pack -v normal -c Release --include-source -p:PackageVersion=${VERSION} ./Udap.Metadata.Server/Udap.Metadata.Server.csproj dotnet pack -v normal -c Release --include-source -p:PackageVersion=${VERSION} ./Udap.Server/Udap.Server.csproj - dotnet pack -v normal -c Release --include-source -p:PackageVersion=0${VERSION} ./Udap.Client/Udap.Client.csproj + dotnet pack -v normal -c Release --include-source -p:PackageVersion=0${VERSION} ./Udap.Client/Udap.Client.csproj + dotnet pack -v normal -c Release --include-source -p:PackageVersion=0${VERSION} ./Udap.UI/Udap.UI.csproj + + dotnet pack -v normal -c Release --include-source -p:PackageVersion=${VERSION} ./Udap.Smart.Model/Udap.Smart.Model.csproj + dotnet pack -v normal -c Release --include-source -p:PackageVersion=${VERSION} ./Udap.Smart.Metadata/Udap.Smart.Metadata.csproj - name: Push run: | dotnet nuget push ./Udap.Model/bin/Release/*.symbols.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} @@ -40,4 +44,7 @@ jobs: dotnet nuget push ./Udap.Metadata.Server/bin/Release/*.symbols.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} dotnet nuget push ./Udap.Server/bin/Release/*.symbols.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} dotnet nuget push ./Udap.Client/bin/Release/*.symbols.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} + dotnet nuget push ./Udap.UI/bin/Release/*.symbols.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} + dotnet nuget push ./Udap.Smart.Model/bin/Release/*.symbols.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} + dotnet nuget push ./Udap.Smart.Metadata/bin/Release/*.symbols.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d7b432c..00736b60 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,6 +32,10 @@ jobs: dotnet pack -v normal -c Release --include-source -p:PackageVersion=${VERSION} ./Udap.Metadata.Server/Udap.Metadata.Server.csproj dotnet pack -v normal -c Release --include-source -p:PackageVersion=${VERSION} ./Udap.Server/Udap.Server.csproj dotnet pack -v normal -c Release --include-source -p:PackageVersion=0${VERSION} ./Udap.Client/Udap.Client.csproj + dotnet pack -v normal -c Release --include-source -p:PackageVersion=0${VERSION} ./Udap.UI/Udap.UI.csproj + + dotnet pack -v normal -c Release --include-source -p:PackageVersion=${VERSION} ./Udap.Smart.Model/Udap.Smart.Model.csproj + dotnet pack -v normal -c Release --include-source -p:PackageVersion=${VERSION} ./Udap.Smart.Metadata/Udap.Smart.Metadata.csproj - name: Push run: | dotnet nuget push ./Udap.Model/bin/Release/*.symbols.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} @@ -40,4 +44,10 @@ jobs: dotnet nuget push ./Udap.Metadata.Server/bin/Release/*.symbols.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} dotnet nuget push ./Udap.Server/bin/Release/*.symbols.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} dotnet nuget push ./Udap.Client/bin/Release/*.symbols.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} + dotnet nuget push ./Udap.UI/bin/Release/*.symbols.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} + + dotnet nuget push ./Udap.Smart.Model/bin/Release/*.symbols.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} + dotnet nuget push ./Udap.Smart.Metadata/bin/Release/*.symbols.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} + + diff --git a/.gitignore b/.gitignore index eb803390..aed1e4af 100644 --- a/.gitignore +++ b/.gitignore @@ -222,3 +222,4 @@ _tests/Udap.PKI.Generator/certstores/TEFCA_RCE examples/clients/UdapEd/Client/wwwroot/temp/MudBlazor.min.js examples/clients/UdapEd/Client/wwwroot/temp/MudBlazor.min.css .tye/docker_store +/examples/Udap.Proxy.Server/CertStore/issued/fhirLabsApiClientLocalhostCert.pfx diff --git a/Directory.Packages.props b/Directory.Packages.props index 38890223..4362fc10 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,34 +5,38 @@ - - - + + + - - + + + + + + - - - - + + + + - + - + - + - + @@ -44,6 +48,8 @@ - + + + \ No newline at end of file diff --git a/README.md b/README.md index 54829d22..cd7c7cc7 100644 --- a/README.md +++ b/README.md @@ -19,55 +19,28 @@ The repository contains components and example uses to support the following ite |-------------------------|---|---------------------|--------------------------------------------------------| | Udap.Client |||| ||UDAP Metadata Validation|✔️| Validates JWT and Certificates. See [Udap.Client](Udap.Client/docs/README.md) for usage. | -||Dynamic Client Registration|In process| Functionally DCR exists but it has not been packaged and documented in Udap.Client package.| -||Access Token |In process| Functionally exists and needs to be packaged and documented in Udap.Client packages | +||Dynamic Client Registration|✔️| Functionally DCR exists but it has not been packaged and documented in Udap.Client package.| +||Access Token |✔️| Functionally exists and needs to be packaged and documented in Udap.Client packages | ||[hl7-b2b extension](http://hl7.org/fhir/us/udap-security/b2b.html#b2b-authorization-extension-object)|In process|This is hard coded in the UdapEd tool for illustration and to pass registration against Authorization Servers that require it. It is a required claim when requesting an access token in the client_credentials grant type flow profiles by UDAP Security under HL7 FHIR. I don't know if it stays here as a feature yet. I do want to call it out because it is a very meaningful feature of UDAP in the HL7 FHIR use case. | | [Discovery](http://hl7.org/fhir/us/udap-security/discovery.html): UDAP Metadata for Resource Server|||| -| | Udap.Metadata.Server | ✔️ Including [Multi Trust Communities](http://hl7.org/fhir/us/udap-security/discovery.html#multiple-trust-communities) | Certificate storage is a file strategy. User can implement their own ICertificateStore. May add a Entity Framework example and/or HSM in the future. Checkout the [2023 FHIR® DevDays Tutorial](udap-devdays-2023) to see it in action and the [Udap.Metadata.Server docs](./Udap.Metadata.Server/README.md) | -|| Udap.Metadata.Vonk.Server | Trial status. Including [Multi Trust Communities](http://hl7.org/fhir/us/udap-security/discovery.html#multiple-trust-communities) | This is based on the same components that build ```Udap.Metadata.Server```. It can be used as a plugin for the Firely server. It has been tested on the Community edition. Readme more in the [docs](./Udap.Metadata.Vonk.Server/README.md)| -| [Server Dynamic Registration](http://hl7.org/fhir/us/udap-security/registration.html)|| ✔️ Including [Multi Trust Communities](http://hl7.org/fhir/us/udap-security/discovery.html#multiple-trust-communities).
Notes: Since this development, the Identity Server has Implemented Dynamic Registration. We need to revisit this and try to enable UDAP under the new DCR feature. | Highly Functional. The Deployed example FHIR® Server, "FhirLabsApi" is passing all udap.org Server Tests. I am going to revisit the Client Secrets persistence layer. Packages are dependent on Duende's Identity Server Nuget Packages. | +| | Udap.Metadata.Server | ✔️ Including [Multi Trust Communities](http://hl7.org/fhir/us/udap-security/discovery.html#multiple-trust-communities) | Certificate storage is a file strategy. User can implement their own ICertificateStore. May add a Entity Framework example and/or HSM in the future. Checkout the [2023 FHIR® DevDays Tutorial](udap-devdays-2023) to see it in action and the [Udap.Metadata.Server docs](https://github.com/JoeShook/Udap.Metadata.Vonk.Server/tree/develop) | +|| Udap.Metadata.Vonk.Server | Trial status. Including [Multi Trust Communities](http://hl7.org/fhir/us/udap-security/discovery.html#multiple-trust-communities) | This is based on the same components that build ```Udap.Metadata.Server```. It can be used as a plugin for the Firely server. It has been tested on the Community edition. Readme more in the [docs](https://github.com/JoeShook/Udap.Metadata.Vonk.Server/tree/develop)| +| [Server Dynamic Registration](http://hl7.org/fhir/us/udap-security/registration.html)|| ✔️ Including [Multi Trust Communities](http://hl7.org/fhir/us/udap-security/discovery.html#multiple-trust-communities). | Highly Functional. The Deployed example FHIR® Server, "FhirLabsApi" is passing all udap.org Server Tests. I am going to revisit the Client Secrets persistence layer. Packages are dependent on Duende's Identity Server Nuget Packages.
Notes: Since this development, the Identity Server has Implemented Dynamic Registration. We could revisit this and try to enable UDAP under the new DCR feature. | ||Inclusion of Certifications and Endorsements|Started|Some example certification integration tests included from the client side | Authorization and Authentication | [Consumer-Facing](http://hl7.org/fhir/us/udap-security/consumer.html)|| ✔️ | Functionality same as B2B authorization_code flow. Client would typically register and or request user/* prefixed scopes | | [Business-to-Business](http://hl7.org/fhir/us/udap-security/b2b.html)|| ✔️ | Works with client_credentials and authorization_code flows. | ||JWT Claim Extensions|Started|Some work completed for the B2B Authorization Extension (hl7-b2b) extension within integration tests. } -| [Tiered OAuth for User Authentication](http://hl7.org/fhir/us/udap-security/user.html) || Mechanically functional.
Works with hl7_identifier.

There is a good integration test called [ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_ClientAuthAccess_Test](/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs). This spins up two in memory instances of Identity Server. One plays the role of Authorization Server and the other plays the role of Identity Provider. This test harness is important to quickly test Tiered OAuth without a user interface. I call this test out because going forward we will need to spend some more engineering time deciding if the [TieredOAuthAuthenticationHandler](/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs) is the final design. This handler implements the ASP.NET [OAuthHandler](https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authentication/OAuth/src/OAuthHandler.cs) and registered as a scheme. There is another choice. We could build the handler based on the [OpenIdConnnect](https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs) base class. This is more in line with the Tiered OAuth behavior but a different technique. OpenIdConnect is more of an event based technique. When I build this first implementation, I was inspired by other implementations such as this great collection from the [aspnet-contrib](https://github.com/aspnet-contrib) organization, called [AspNet.Security.OpenId.Providers](https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers). There is another repository at this organization, called [AspNet.Security.OpenId.Providers](https://github.com/aspnet-contrib/AspNet.Security.OpenId.Providers). I have not looked too closely at it yet. One last thing I would like to mention here, the current implementation of adding a trusted IdP to the Authorization Server is static. The goal is to transition too dynamic. For example, this code below represents a sample deployed Auth Server capable of auto registering and federating as to these three IdPs. Duende Identity Server supports "Dynamic Providers" in the Enterprise Server. This licensing is more expensive. So maybe future development can allow for static or dynamic. Remember in Tiered OAuth a client should be able to send an idp parameter in an authorization request, thus initiating a dynamic UDAP relationship between authorization server and IdP server.

Disclaimer, I have not examined this whole code base to see whether parts of the components fit in the different [pricing structures](https://duendesoftware.com/products/identityserver#pricing). The longer I work with this stack of code the more I appreciate the body of work and the enterprise pricing looks very reasonable. | Beta status +| [Tiered OAuth for User Authentication](http://hl7.org/fhir/us/udap-security/user.html) || ✔️ | Simply register Tiered OAuth functionality with the code snippet below.

There is a good integration test called [ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_ClientAuthAccess_Test](/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs). This spins up two in memory instances of Identity Server. One plays the role of Authorization Server and the other plays the role of Identity Provider. This test harness is important to quickly test Tiered OAuth without a user interface. When I built this first implementation, I was inspired by other implementations such as this great collection from the [aspnet-contrib](https://github.com/aspnet-contrib) organization, called [AspNet.Security.OpenId.Providers](https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers). There is another repository at this organization, called [AspNet.Security.OpenId.Providers](https://github.com/aspnet-contrib/AspNet.Security.OpenId.Providers). | ```csharp - builder.Services.AddAuthentication() - // - // By convention the scheme name should match the community name in UdapFileCertStoreManifest - // to allow discovery of the IdPBaseUrl - // - .AddTieredOAuth(options => - { - options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; - options.AuthorizationEndpoint = "https://idp1.securedcontrols.net/connect/authorize"; - options.TokenEndpoint = "https://idp1.securedcontrols.net/connect/token"; - options.IdPBaseUrl = "https://idp1.securedcontrols.net"; - }) - .AddTieredOAuth("TieredOAuthProvider2", "UDAP Tiered OAuth (DOTNET-Provider2)", options => - { - options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; - options.AuthorizationEndpoint = "https://idp2.securedcontrols.net/connect/authorize"; - options.TokenEndpoint = "https://idp2.securedcontrols.net/connect/token"; - options.CallbackPath = "/signin-tieredoauthprovider2"; - options.IdPBaseUrl = "https://idp2.securedcontrols.net"; - }) - .AddTieredOAuth("OktaForUDAP", "UDAP Tiered OAuth Okta", options => - { - options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; - options.AuthorizationEndpoint = "https://udap.zimt.work/oauth2/aus5wvee13EWm169M1d7/v1/authorize"; - options.TokenEndpoint = "https://udap.zimt.work/oauth2/aus5wvee13EWm169M1d7/v1/token"; - options.CallbackPath = "/signin-oktaforudap"; - options.IdPBaseUrl = "https://udap.zimt.work/oauth2/aus5wvee13EWm169M1d7"; - - }); + .AddTieredOAuth(options => + { + options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; + }); ``` - - ## PKI support ### Generate PKI for integration tests @@ -138,3 +111,7 @@ See [Udap.Auth.Server.Admin](./examples/Auth.Server.Admin/) ### UDAP CA UI Tool This is barely implemented. The spirit of it is to create a easy to use CA for experimenting in a lab environment. At this point all the tooling for creating interesting PKI test data for success and failure use cases lives in the Udap.PKI.Generator test project. + +### UDAP Proxy Server + +Turn your FHIR server into a UDAP secured FHIR server with [Udap.Proxy.Server](/examples/Udap.Proxy.Server). diff --git a/Udap.Client/Authentication/AuthTokenResponse.cs b/Udap.Client/Authentication/AuthTokenResponse.cs new file mode 100644 index 00000000..297eb5f0 --- /dev/null +++ b/Udap.Client/Authentication/AuthTokenResponse.cs @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using System.Text.Json; + +namespace Udap.Client.Authentication; + +/// +/// Response from an provider for an OAuth token request. +/// +public class OAuthTokenResponse : IDisposable +{ + /// + /// Initializes a new instance . + /// + /// The received JSON payload. + private OAuthTokenResponse(JsonDocument response) + { + Response = response; + var root = response.RootElement; + AccessToken = root.GetString("access_token"); + TokenType = root.GetString("token_type"); + RefreshToken = root.GetString("refresh_token"); + ExpiresIn = root.GetString("expires_in"); + Error = GetStandardErrorException(response); + } + + private OAuthTokenResponse(Exception error) + { + Error = error; + } + + /// + /// Creates a successful . + /// + /// The received JSON payload. + /// A instance. + public static OAuthTokenResponse Success(JsonDocument response) + { + return new OAuthTokenResponse(response); + } + + /// + /// Creates a failed . + /// + /// The error associated with the failure. + /// A instance. + public static OAuthTokenResponse Failed(Exception error) + { + return new OAuthTokenResponse(error); + } + + /// + public void Dispose() + { + Response?.Dispose(); + } + + /// + /// Gets or sets the received JSON payload. + /// + public JsonDocument? Response { get; set; } + + /// + /// Gets or sets the access token issued by the OAuth provider. + /// + public string? AccessToken { get; set; } + + /// + /// Gets or sets the token type. + /// + /// + /// Typically the string “bearer”. + /// + public string? TokenType { get; set; } + + /// + /// Gets or sets a refresh token that applications can use to obtain another access token if tokens can expire. + /// + public string? RefreshToken { get; set; } + + /// + /// Gets or sets the validatity lifetime of the token in seconds. + /// + public string? ExpiresIn { get; set; } + + /// + /// The exception in the event the response was a failure. + /// + public Exception? Error { get; set; } + + internal static Exception? GetStandardErrorException(JsonDocument response) + { + var root = response.RootElement; + var error = root.GetString("error"); + + if (error is null) + { + return null; + } + + var result = new StringBuilder("OAuth token endpoint failure: "); + result.Append(error); + + if (root.TryGetProperty("error_description", out var errorDescription)) + { + result.Append(";Description="); + result.Append(errorDescription); + } + + if (root.TryGetProperty("error_uri", out var errorUri)) + { + result.Append(";Uri="); + result.Append(errorUri); + } + + var exception = new AuthenticationFailureException(result.ToString()); + exception.Data["error"] = error.ToString(); + exception.Data["error_description"] = errorDescription.ToString(); + exception.Data["error_uri"] = errorUri.ToString(); + + return exception; + } +} diff --git a/Udap.Client/Authentication/AuthenticationFailureException.cs b/Udap.Client/Authentication/AuthenticationFailureException.cs new file mode 100644 index 00000000..859a1e6e --- /dev/null +++ b/Udap.Client/Authentication/AuthenticationFailureException.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + + +/* Unmerged change from project 'Udap.Client (net7.0)' +Before: +namespace Udap.Client.Microsoft.Authentication; +After: +using Udap; +using Udap.Client; +using Udap.Client.Authentication; +using Udap.Client.Microsoft; +using Udap.Client.Microsoft.Authentication; +*/ + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Udap.Client.Authentication; + +/// +/// A generic authentication failure. +/// +public class AuthenticationFailureException : Exception +{ + /// + /// Creates a new instance of + /// with the specified exception . + /// + /// The message that describes the error. + public AuthenticationFailureException(string? message) + : base(message) + { + } + + /// + /// Creates a new instance of + /// with the specified exception and + /// a reference to the inner exception that is the cause of this exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception, or . + public AuthenticationFailureException(string? message, Exception? innerException) + : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/Udap.Client/Authentication/JsonDocumentAuthExtensions.cs b/Udap.Client/Authentication/JsonDocumentAuthExtensions.cs new file mode 100644 index 00000000..2e66edaf --- /dev/null +++ b/Udap.Client/Authentication/JsonDocumentAuthExtensions.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; + +namespace Udap.Client.Authentication; + +/// +/// Authentication extensions to . +/// +public static class JsonDocumentAuthExtensions +{ + /// + /// Gets a string property value from the specified . + /// + /// The . + /// The property name. + /// The property value. + public static string? GetString(this JsonElement element, string key) + { + if (element.TryGetProperty(key, out var property) && property.ValueKind != JsonValueKind.Null) + { + return property.ToString(); + } + + return null; + } +} \ No newline at end of file diff --git a/Udap.Client/Client/Extensions/HttpClientTokenRequestExtensions.cs b/Udap.Client/Client/Extensions/HttpClientTokenRequestExtensions.cs index f0981d0b..c3c7627a 100644 --- a/Udap.Client/Client/Extensions/HttpClientTokenRequestExtensions.cs +++ b/Udap.Client/Client/Extensions/HttpClientTokenRequestExtensions.cs @@ -19,8 +19,7 @@ using System.Text.Json; using IdentityModel; using IdentityModel.Client; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.OAuth; +using Udap.Client.Authentication; using Udap.Model; using Udap.Model.Access; using TokenResponse = IdentityModel.Client.TokenResponse; diff --git a/Udap.Client/Client/IUdapClient.cs b/Udap.Client/Client/IUdapClient.cs index 3fb1dde0..5942bf65 100644 --- a/Udap.Client/Client/IUdapClient.cs +++ b/Udap.Client/Client/IUdapClient.cs @@ -9,8 +9,8 @@ using System.Security.Cryptography.X509Certificates; using IdentityModel.Client; -using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.IdentityModel.Tokens; +using Udap.Client.Authentication; using Udap.Client.Client.Messages; using Udap.Common.Certificates; using Udap.Model; diff --git a/Udap.Client/Client/UdapClient.cs b/Udap.Client/Client/UdapClient.cs index fbe01eb4..9e154c03 100644 --- a/Udap.Client/Client/UdapClient.cs +++ b/Udap.Client/Client/UdapClient.cs @@ -13,10 +13,10 @@ using System.Security.Cryptography.X509Certificates; using System.Text.Json; using IdentityModel.Client; -using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; +using Udap.Client.Authentication; using Udap.Client.Client.Extensions; using Udap.Client.Client.Messages; using Udap.Client.Configuration; @@ -26,6 +26,7 @@ using Udap.Model.Access; using Udap.Model.Registration; using Udap.Model.Statement; + #if NET7_0_OR_GREATER using System.Net.Http.Headers; #endif @@ -84,7 +85,7 @@ public event Action? Error public event Action? TokenError; //TODO the certs include the private key. This needs work. It should be a service or struct that - // allows a an abstraction in "Sign" so that a vault or HSM can sign the metadata. + // allows an abstraction in "Sign" so that a vault or HSM can sign the metadata. public async Task RegisterTieredClient(string redirectUrl, IEnumerable certificates, string scopes, @@ -478,6 +479,15 @@ private async Task RegisterAuthCodeFlow( var response = await _httpClient.PostAsync(this.UdapServerMetaData?.RegistrationEndpoint, content, token); + if (response.StatusCode == HttpStatusCode.NotFound) + { + return new UdapDynamicClientRegistrationDocument + { + { "error", "Not Found(404)" }, + { "error_description", $"Registration endpoint not found {this.UdapServerMetaData?.RegistrationEndpoint}" } + }; + } + if (((int)response.StatusCode) < 500) { resultDocument = diff --git a/Udap.Client/Udap.Client.csproj b/Udap.Client/Udap.Client.csproj index 56108055..41e23e85 100644 --- a/Udap.Client/Udap.Client.csproj +++ b/Udap.Client/Udap.Client.csproj @@ -12,7 +12,7 @@ Copyright © Joseph.Shook@Surescripts.com 2022 MIT true - udap.logo.48x48.jpg + UDAP_Ecosystem_Gears 48X48.jpg UDAP;FHIR;HL7 Package is a part of the UDAP reference implementation for .NET. @@ -27,25 +27,33 @@ - - + + - - + + - - - + + + - + + + + + + + + + \ No newline at end of file diff --git a/Udap.Common/Udap.Common.csproj b/Udap.Common/Udap.Common.csproj index 9d19fc37..a6cebd56 100644 --- a/Udap.Common/Udap.Common.csproj +++ b/Udap.Common/Udap.Common.csproj @@ -13,7 +13,7 @@ Copyright © Joseph.Shook@Surescripts.com 2022 MIT true - udap.logo.48x48.jpg + UDAP_Ecosystem_Gears 48X48.jpg UDAP;FHIR;HL7 Package is a part of the UDAP reference implementation for .NET. @@ -29,7 +29,7 @@ - + diff --git a/Udap.Metadata.Server/Configuration/DependencyInjection/ServiceCollectionExtensions.cs b/Udap.Metadata.Server/Configuration/DependencyInjection/ServiceCollectionExtensions.cs index 172b1847..35c00429 100644 --- a/Udap.Metadata.Server/Configuration/DependencyInjection/ServiceCollectionExtensions.cs +++ b/Udap.Metadata.Server/Configuration/DependencyInjection/ServiceCollectionExtensions.cs @@ -122,11 +122,11 @@ public static IServiceCollection AddUdapMetadataServer( - public static IApplicationBuilder UseUdapMetadataServer(this WebApplication app) + public static IApplicationBuilder UseUdapMetadataServer(this WebApplication app, string? prefixRoute = null) { EnsureMvcControllerUnloads(app); - app.MapGet($"/{UdapConstants.Discovery.DiscoveryEndpoint}", + app.MapGet($"/{prefixRoute}{UdapConstants.Discovery.DiscoveryEndpoint}", async ( [FromServices] UdapMetaDataEndpoint endpoint, HttpContext httpContext, @@ -136,13 +136,13 @@ public static IApplicationBuilder UseUdapMetadataServer(this WebApplication app) .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound); // community doesn't exist - app.MapGet($"/{UdapConstants.Discovery.DiscoveryEndpoint}/communities", + app.MapGet($"/{prefixRoute}{UdapConstants.Discovery.DiscoveryEndpoint}/communities", ([FromServices] UdapMetaDataEndpoint endpoint) => endpoint.GetCommunities()) .AllowAnonymous() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound); // community doesn't exist - app.MapGet($"/{UdapConstants.Discovery.DiscoveryEndpoint}/communities/ashtml", + app.MapGet($"/{prefixRoute}{UdapConstants.Discovery.DiscoveryEndpoint}/communities/ashtml", ( [FromServices] UdapMetaDataEndpoint endpoint, HttpContext httpContext) => endpoint.GetCommunitiesAsHtml(httpContext)) @@ -153,10 +153,10 @@ public static IApplicationBuilder UseUdapMetadataServer(this WebApplication app) return app; } - public static IApplicationBuilder UseUdapMetadataServer(this IApplicationBuilder app) + public static IApplicationBuilder UseUdapMetadataServer(this IApplicationBuilder app, string? prefixRoute = null) { - app.Map($"/{UdapConstants.Discovery.DiscoveryEndpoint}", path => + app.Map($"/{prefixRoute}{UdapConstants.Discovery.DiscoveryEndpoint}", path => { path.Run(async ctx => { diff --git a/Udap.Metadata.Server/README.md b/Udap.Metadata.Server/README.md deleted file mode 100644 index ddc64910..00000000 --- a/Udap.Metadata.Server/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Udap.Metadata.Server - -TODO:: configure for Controller or Endpoint style diff --git a/Udap.Metadata.Server/Udap.Metadata.Server.csproj b/Udap.Metadata.Server/Udap.Metadata.Server.csproj index 18fc7410..6eb684d1 100644 --- a/Udap.Metadata.Server/Udap.Metadata.Server.csproj +++ b/Udap.Metadata.Server/Udap.Metadata.Server.csproj @@ -8,10 +8,10 @@ https://github.com/JoeShook/udap-dotnet README.md Joseph Shook - Copyright © Joseph.Shook@Surescripts.com 2022 + Copyright © Joseph.Shook@Surescripts.com 2022-2024 MIT true - udap.logo.48x48.jpg + UDAP_Ecosystem_Gears 48X48.jpg UDAP;FHIR;HL7 Package is a part of the UDAP reference implementation for .NET. @@ -30,7 +30,7 @@ - + diff --git a/Udap.Model/Registration/UdapDcrBuilderForAuthorizationCode.cs b/Udap.Model/Registration/UdapDcrBuilderForAuthorizationCode.cs index 268fbdac..5c27c39d 100644 --- a/Udap.Model/Registration/UdapDcrBuilderForAuthorizationCode.cs +++ b/Udap.Model/Registration/UdapDcrBuilderForAuthorizationCode.cs @@ -1,4 +1,4 @@ -#region (c) 2023 Joseph Shook. All rights reserved. +#region (c) 2024 Joseph Shook. All rights reserved. // /* // Authors: // Joseph Shook Joseph.Shook@Surescripts.com @@ -9,12 +9,14 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Security.Cryptography.X509Certificates; using IdentityModel; using Microsoft.IdentityModel.Tokens; using Udap.Model.Statement; +#if NET6_0_OR_GREATER using Udap.Util.Extensions; +using System.Linq; +#endif namespace Udap.Model.Registration; @@ -164,13 +166,13 @@ public UdapDcrBuilderForAuthorizationCode WithExpiration(long secondsSinceEpoch) } /// - /// Generally one should just let the constructor set IssuedAt + /// Generally one should just let the constructor set IssuedAt. But clients like UdapEd like to have control over settings to produce negative tests. /// - /// + /// /// - public UdapDcrBuilderForAuthorizationCode OverrideIssuedAt(DateTime issuedAt) + public UdapDcrBuilderForAuthorizationCode WithIssuedAt(long secondsSinceEpoch) { - _document.IssuedAt = EpochTime.GetIntDate(issuedAt.ToUniversalTime()); + _document.IssuedAt = secondsSinceEpoch; return this; } diff --git a/Udap.Model/Registration/UdapDcrBuilderForClientCredentials.cs b/Udap.Model/Registration/UdapDcrBuilderForClientCredentials.cs index 4f0f2d0c..d2f1cec5 100644 --- a/Udap.Model/Registration/UdapDcrBuilderForClientCredentials.cs +++ b/Udap.Model/Registration/UdapDcrBuilderForClientCredentials.cs @@ -1,4 +1,4 @@ -#region (c) 2023 Joseph Shook. All rights reserved. +#region (c) 2024 Joseph Shook. All rights reserved. // /* // Authors: // Joseph Shook Joseph.Shook@Surescripts.com @@ -9,12 +9,14 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Security.Cryptography.X509Certificates; using IdentityModel; using Microsoft.IdentityModel.Tokens; using Udap.Model.Statement; +#if NET6_0_OR_GREATER using Udap.Util.Extensions; +using System.Linq; +#endif namespace Udap.Model.Registration; @@ -156,16 +158,16 @@ public UdapDcrBuilderForClientCredentials WithExpiration(long secondsSinceEpoch) } /// - /// Generally one should just let the constructor set IssuedAt + /// Generally one should just let the constructor set IssuedAt. But clients like UdapEd like to have control over settings to produce negative tests. /// - /// + /// /// - public UdapDcrBuilderForClientCredentials OverrideIssuedAt(DateTime issuedAt) + public UdapDcrBuilderForClientCredentials WithIssuedAt(long secondsSinceEpoch) { - _document.IssuedAt = EpochTime.GetIntDate(issuedAt.ToUniversalTime()); + _document.IssuedAt = secondsSinceEpoch; return this; } - + public UdapDcrBuilderForClientCredentials WithJwtId(string? jwtId = null) { _document.JwtId = jwtId ?? CryptoRandom.CreateUniqueId(); diff --git a/Udap.Model/Udap.Model.csproj b/Udap.Model/Udap.Model.csproj index ece2835d..530fb49e 100644 --- a/Udap.Model/Udap.Model.csproj +++ b/Udap.Model/Udap.Model.csproj @@ -12,7 +12,7 @@ Copyright © Joseph.Shook@Surescripts.com 2022 MIT true - udap.logo.48x48.jpg + UDAP_Ecosystem_Gears 48X48.jpg UDAP;FHIR;HL7 Package is a part of the UDAP reference implementation for .NET. @@ -20,7 +20,7 @@ - + \ true @@ -36,7 +36,7 @@ - + diff --git a/Udap.Model/UdapMetadata.cs b/Udap.Model/UdapMetadata.cs index acc1409a..a0f207b5 100644 --- a/Udap.Model/UdapMetadata.cs +++ b/Udap.Model/UdapMetadata.cs @@ -151,8 +151,16 @@ public UdapMetadata(UdapMetadataOptions udapMetadataOptions, IEnumerable TokenEndpointAuthMethodsSupported = new HashSet { UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue }; //TODO: All of this should be configurable, via config string and builder pattern. - TokenEndpointAuthSigningAlgValuesSupported = new HashSet { UdapConstants.SupportedAlgorithm.RS256, UdapConstants.SupportedAlgorithm.RS384 }; - RegistrationEndpointJwtSigningAlgValuesSupported = new HashSet { UdapConstants.SupportedAlgorithm.RS256, UdapConstants.SupportedAlgorithm.RS384 }; + TokenEndpointAuthSigningAlgValuesSupported = new HashSet + { + UdapConstants.SupportedAlgorithm.RS256, UdapConstants.SupportedAlgorithm.RS384, + UdapConstants.SupportedAlgorithm.ES256, UdapConstants.SupportedAlgorithm.ES384 + }; + RegistrationEndpointJwtSigningAlgValuesSupported = new HashSet + { + UdapConstants.SupportedAlgorithm.RS256, UdapConstants.SupportedAlgorithm.RS384, + UdapConstants.SupportedAlgorithm.ES256, UdapConstants.SupportedAlgorithm.ES384 + }; } private void BuildSupportedProfiles(UdapMetadataOptions udapMetadataOptions) diff --git a/Udap.Server/Hosting/UdapAuthorizationResponseMiddleware.cs b/Udap.Server/Hosting/UdapAuthorizationResponseMiddleware.cs index 7be005c8..102c83a4 100644 --- a/Udap.Server/Hosting/UdapAuthorizationResponseMiddleware.cs +++ b/Udap.Server/Hosting/UdapAuthorizationResponseMiddleware.cs @@ -81,7 +81,7 @@ public async Task Invoke( context.Request.Path.Value.Contains(Constants.ProtocolRoutePaths.Authorize)) { var requestParams = context.Request.Query; - + if (requestParams.Any()) { if (udapServerOptions.ForceStateParamOnAuthorizationCode) @@ -102,41 +102,43 @@ await clients.FindClientByIdAsync( } } } + } - context.Response.OnStarting(async () => + context.Response.OnStarting(async () => + { + if (context.Response.StatusCode == (int)HttpStatusCode.Redirect && + !context.Response.Headers.Location.IsNullOrEmpty() + ) { - if (context.Response.StatusCode == (int)HttpStatusCode.Redirect && - !context.Response.Headers.Location.IsNullOrEmpty() - ) - { - var uri = new Uri(context.Response.Headers.Location!); - var query = uri.Query; - var responseParams = QueryHelpers.ParseQuery(query); + var uri = new Uri(context.Response.Headers.Location!); + var query = uri.Query; + var responseParams = QueryHelpers.ParseQuery(query); - if (responseParams.TryGetValue(_options.UserInteraction.ErrorIdParameter, out var errorId)) - { - var requestParamCollection = context.Request.Query.AsNameValueCollection(); - var client = - await clients.FindClientByIdAsync( - requestParamCollection.Get(AuthorizeRequest.ClientId)); - var scope = requestParamCollection.Get(AuthorizeRequest.Scope); + if (responseParams.TryGetValue(_options.UserInteraction.ErrorIdParameter, out var errorId)) + { + var requestParamCollection = context.Request.Query.AsNameValueCollection(); + var client = + await clients.FindClientByIdAsync( + requestParamCollection.Get(AuthorizeRequest.ClientId)); + var scope = requestParamCollection.Get(AuthorizeRequest.Scope); - if (client == null) - { - await RenderErrorResponse(context, interactionService, errorId); - } + if (client == null) + { + await RenderErrorResponse(context, interactionService, errorId); + return; + } - if (client != null && - client.ClientSecrets.Any(cs => - cs.Type == UdapServerConstants.SecretTypes.UDAP_SAN_URI_ISS_NAME)) - { - await RenderErrorResponse(context, interactionService, errorId); - } + if (client != null && + client.ClientSecrets.Any(cs => + cs.Type == UdapServerConstants.SecretTypes.UDAP_SAN_URI_ISS_NAME)) + { + await RenderErrorResponse(context, interactionService, errorId); + return; } } - }); - } + } + }); } await _next(context); @@ -167,7 +169,9 @@ private async Task RenderErrorResponse( { var errorMessage = await interactionService.GetErrorContextAsync(errorId); - if (errorMessage.Error == AuthorizeErrors.UnsupportedResponseType) + if (errorMessage.Error == AuthorizeErrors.UnsupportedResponseType + || errorMessage is { Error: AuthorizeErrors.InvalidRequest, ErrorDescription: "Missing response_type" } + ) { // // Include error in redirect diff --git a/Udap.Server/Mappers/AuthTokenResponseMapper.cs b/Udap.Server/Mappers/AuthTokenResponseMapper.cs new file mode 100644 index 00000000..214954c0 --- /dev/null +++ b/Udap.Server/Mappers/AuthTokenResponseMapper.cs @@ -0,0 +1,55 @@ +#region (c) 2024 Joseph Shook. All rights reserved. +// /* +// Authors: +// Joseph Shook Joseph.Shook@Surescripts.com +// +// See LICENSE in the project root for license information. +// */ +#endregion + +using AutoMapper; +using Udap.Client.Authentication; + +namespace Udap.Server.Mappers; +public static class AuthTokenResponseMapper +{ + static AuthTokenResponseMapper() + { + Mapper = new MapperConfiguration(cfg => + { + cfg.AddProfile(); + }) + .CreateMapper(); + } + + internal static IMapper Mapper { get; } + + /// + /// Maps a to a . + /// + /// The OAuthTokenResponse. + /// + public static Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse ToMSAuthTokenResponse(this OAuthTokenResponse response) + { + return Mapper.Map(response); + } + + /// + /// Maps a to a . + /// + /// The OAuthTokenResponse. + /// + public static OAuthTokenResponse ToClientAuthTokenResponse(this Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse response) + { + return Mapper.Map(response); + } +} + +public class AuthTokenResponseMapperProfile : Profile +{ + public AuthTokenResponseMapperProfile() + { + CreateMap() + .ReverseMap(); + } +} diff --git a/Udap.Server/Mappers/CommnityMapper.cs b/Udap.Server/Mappers/CommnityMapper.cs index b61e5573..5c8b7190 100644 --- a/Udap.Server/Mappers/CommnityMapper.cs +++ b/Udap.Server/Mappers/CommnityMapper.cs @@ -20,6 +20,7 @@ static CommunityMapper() Mapper = new MapperConfiguration(cfg => { cfg.AddProfile(); + cfg.AddProfile(); }) .CreateMapper(); } @@ -57,25 +58,6 @@ public CommunityMapperProfile() .ReverseMap() ; - CreateMap, ICollection>() - .ConstructUsing(src => - // var dest = new HashSet(); - src.Select(anchor => new Common.Models.Anchor() - { - Id = anchor.Id, - Name = anchor.Name, - Thumbprint = anchor.Thumbprint, - BeginDate = anchor.BeginDate, - EndDate = anchor.EndDate, - Enabled = anchor.Enabled, - Certificate = anchor.X509Certificate, - Community = anchor.Community.Name, - CommunityId = anchor.CommunityId - }).ToHashSet() - ) - .ForAllMembers(opt => opt.Ignore()); - - AllowNullCollections = true; } diff --git a/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs b/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs index fc6b3196..f1e1ca92 100644 --- a/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs +++ b/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs @@ -148,6 +148,8 @@ IEnumerable anchors }); } + _logger.LogDebug($"Is token valid: {validatedToken.IsValid}"); + if (!validatedToken.IsValid) { if (validatedToken.Exception.GetType() == typeof(SecurityTokenNoExpirationException)) @@ -176,7 +178,7 @@ IEnumerable anchors if (_serverSettings.RegistrationJtiRequired) { - var result = await ValidateJti(document, jsonWebToken.ValidTo); + var result = await ValidateJti(document, document.Expiration); if (result.IsError) { @@ -286,7 +288,8 @@ IEnumerable anchors AlwaysIncludeUserClaimsInIdToken = _serverSettings.AlwaysIncludeUserClaimsInIdToken }; - + _logger.LogDebug($"Validating chain for ClientId: {client.ClientId}. x5c {jwtHeader.X5c}"); + if (!ValidateChain(client, jsonWebToken, jwtHeader, intermediateCertificates, anchorCertificates, anchors)) { _logger.LogWarning($"{UdapDynamicClientRegistrationErrors.UnapprovedSoftwareStatement}::" + @@ -310,6 +313,8 @@ IEnumerable anchors UdapDynamicClientRegistrationErrorDescriptions.UntrustedCertificate)); } + _logger.LogDebug($"Chain Validated {client.ClientId}"); + ////////////////////////////// // validate grant_types ////////////////////////////// @@ -335,8 +340,7 @@ IEnumerable anchors } } } - - + if (document.GrantTypes != null && document.GrantTypes.Contains(OidcConstants.GrantTypes.ClientCredentials)) { client.AllowedGrantTypes.Add(OidcConstants.GrantTypes.ClientCredentials); @@ -530,6 +534,8 @@ IEnumerable anchors } // validation successful - return client + _logger.LogDebug($"Validation success for ClientId: {client.ClientId}"); + return await Task.FromResult(new UdapDynamicClientRegistrationValidationResult(client, document)); } @@ -548,6 +554,8 @@ IEnumerable anchors if (Uri.TryCreate(document.LogoUri, UriKind.Absolute, out var logoUri)) { + _logger.LogDebug($"Validating logo: {logoUri.OriginalString}"); + if (!logoUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) { errorResult = new UdapDynamicClientRegistrationValidationResult( @@ -579,6 +587,8 @@ IEnumerable anchors UdapDynamicClientRegistrationErrors.InvalidClientMetadata, UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidContentType); + _logger.LogDebug($"Logo validation failed: {logoUri.OriginalString}"); + return (false, errorResult); } } @@ -588,15 +598,19 @@ IEnumerable anchors UdapDynamicClientRegistrationErrors.InvalidClientMetadata, UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidUri); + _logger.LogDebug($"Logo validation failed: {document.LogoUri}"); + return (false, errorResult); } + _logger.LogDebug($"Logo validation succeeded: {logoUri.OriginalString}"); + return (true, null); } public async Task ValidateJti( UdapDynamicClientRegistrationDocument document, - DateTime validTo) + long exp) { var jti = document.JwtId; @@ -620,10 +634,9 @@ public async Task ValidateJti( } else { - await _replayCache.AddAsync(Purpose, jti, validTo.AddMinutes(5)); + await _replayCache.AddAsync(Purpose, jti, DateTimeOffset.FromUnixTimeSeconds(exp)); } - - + return new UdapDynamicClientRegistrationValidationResult(string.Empty); } @@ -637,7 +650,6 @@ private bool ValidateChain( { var x5cArray = Getx5c(jwtHeader); - // TODO: no test cases for x5c with intermediate certificates. if (x5cArray != null) { @@ -663,14 +675,14 @@ private bool ValidateChain( clientSecrets.Add(new() { - Expiration = chainElements.First().Certificate.NotAfter, + Expiration = chainElements.First().Certificate.NotAfter.ToUniversalTime(), Type = UdapServerConstants.SecretTypes.UDAP_SAN_URI_ISS_NAME, Value = jwtSecurityToken.Issuer }); clientSecrets.Add(new() { - Expiration = chainElements.First().Certificate.NotAfter, + Expiration = chainElements.First().Certificate.NotAfter.ToUniversalTime(), Type = UdapServerConstants.SecretTypes.UDAP_COMMUNITY, Value = communityId.ToString() }); diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs index 3fdcd748..ad7c8e54 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs @@ -32,6 +32,7 @@ using Udap.Common.Models; using Udap.Model; using Udap.Model.Access; +using Udap.Server.Mappers; using Udap.Server.Storage.Stores; using Udap.Util.Extensions; @@ -442,8 +443,10 @@ protected override async Task ExchangeCodeAsync([NotNull] OA //TODO algorithm selectable. var tokenRequest = tokenRequestBuilder.Build(); - - return await _udapClient.ExchangeCodeForAuthTokenResponse(tokenRequest, Context.RequestAborted); + + var authTokenResponse = await _udapClient.ExchangeCodeForAuthTokenResponse(tokenRequest, Context.RequestAborted); + + return authTokenResponse.ToMSAuthTokenResponse(); } /// diff --git a/Udap.Server/Udap.Server.csproj b/Udap.Server/Udap.Server.csproj index ef1fd9b9..acdea429 100644 --- a/Udap.Server/Udap.Server.csproj +++ b/Udap.Server/Udap.Server.csproj @@ -13,7 +13,7 @@ Copyright © Joseph.Shook@Surescripts.com 2022 MIT true - udap.logo.48x48.jpg + UDAP_Ecosystem_Gears 48X48.jpg UDAP;FHIR;HL7 Package is a part of the UDAP reference implementation for .NET. @@ -37,7 +37,7 @@ - + diff --git a/Udap.Smart.Metadata/Configuration/DependencyInjection/ServiceCollectionExtensions.cs b/Udap.Smart.Metadata/Configuration/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..5eed9adf --- /dev/null +++ b/Udap.Smart.Metadata/Configuration/DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,93 @@ +#region (c) 2024 Joseph Shook. All rights reserved. +// /* +// Authors: +// Joseph Shook Joseph.Shook@Surescripts.com +// +// See LICENSE in the project root for license information. +// */ +#endregion + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Udap.Smart.Metadata; +using Udap.Smart.Model; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + /// + /// Extension method used to register a single or a named . + /// + /// + /// Named Option. This feature is anticipated to allow a proxy server implementation to host multiple .well-known/smart-configuration endpoints. + /// + public static IServiceCollection AddSmartMetadata(this IServiceCollection services, string? namedOption = null) + { + services.AddScoped(sp => + new SmartMetadataEndpoint(sp.GetService>(), namedOption)); + return services; + } + + /// + /// Extension method used to register a single or a named . + /// This method will look up SMART Metadata from the "SmartMetadata" configuration section of appsettings. + /// + /// + /// Named Option. This feature is anticipated to allow a proxy server implementation to host multiple .well-known/smart-configuration endpoints. + /// + public static IHostApplicationBuilder AddSmartMetadata(this IHostApplicationBuilder builder, string? namedOption = null) + { + builder.Services.Configure(builder.Configuration.GetRequiredSection("SmartMetadata")); + builder.Services.AddScoped(sp => + new SmartMetadataEndpoint(sp.GetService>(), namedOption)); + + return builder; + } + + public static IApplicationBuilder UseSmartMetadata(this WebApplication app, string? prefixRoute = null) + { + EnsureMvcControllerUnloads(app); + + app.MapGet($"/{prefixRoute}{SmartConstants.Discovery.DiscoveryEndpoint}", + async ([FromServices] SmartMetadataEndpoint endpoint) => await endpoint.Process()) + .AllowAnonymous() + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound); // missing SmartMetadata + + return app; + } + + public static IApplicationBuilder UseSmartMetadata(this IApplicationBuilder app, string? prefixRoute = null) + { + app.Map($"/{prefixRoute}{SmartConstants.Discovery.DiscoveryEndpoint}", path => + { + path.Run(async ctx => + { + var endpoint = ctx.RequestServices.GetRequiredService(); + var result = await endpoint.Process(); + await result.ExecuteAsync(ctx); + }); + }); + + return app; + } + + private static void EnsureMvcControllerUnloads(WebApplication app) + { + if (app.Services.GetService(typeof(ApplicationPartManager)) is ApplicationPartManager appPartManager) + { + var part = appPartManager.ApplicationParts.FirstOrDefault(a => a.Name == "Smart.Metadata.Server"); + if (part != null) + { + appPartManager.ApplicationParts.Remove(part); + } + } + } +} diff --git a/Udap.Smart.Metadata/SmartMetadataEndpoint.cs b/Udap.Smart.Metadata/SmartMetadataEndpoint.cs new file mode 100644 index 00000000..5cd31c74 --- /dev/null +++ b/Udap.Smart.Metadata/SmartMetadataEndpoint.cs @@ -0,0 +1,41 @@ +#region (c) 2024 Joseph Shook. All rights reserved. +// /* +// Authors: +// Joseph Shook Joseph.Shook@Surescripts.com +// +// See LICENSE in the project root for license information. +// */ +#endregion + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Udap.Smart.Model; + +namespace Udap.Smart.Metadata; + +/// +/// See SMART App Launch: Conformance +/// +public class SmartMetadataEndpoint +{ + private readonly IOptionsMonitor? _smartMetadata; + private readonly string? _namedOption; + + public SmartMetadataEndpoint(IOptionsMonitor? smartMetadata, string? namedOption) + { + _smartMetadata = smartMetadata; + _namedOption = namedOption; + } + + public Task Process() + { + if (_smartMetadata == null) + { + return Task.FromResult(Results.NotFound()); + } + + return Task.FromResult(Results.Ok(_namedOption == null ? + _smartMetadata.CurrentValue : + _smartMetadata.Get(_namedOption) )); + } +} \ No newline at end of file diff --git a/Udap.Smart.Metadata/Udap.Smart.Metadata.csproj b/Udap.Smart.Metadata/Udap.Smart.Metadata.csproj new file mode 100644 index 00000000..81ea0e22 --- /dev/null +++ b/Udap.Smart.Metadata/Udap.Smart.Metadata.csproj @@ -0,0 +1,42 @@ + + + + net6.0;net8.0 + enable + enable + git + https://github.com/JoeShook/udap-dotnet + README.md + Joseph Shook + Copyright © Joseph.Shook@Surescripts.com 2022-2024 + MIT + true + UDAP_Ecosystem_Gears 48X48.jpg + SMART;UDAP;FHIR;HL7 + Package is a part of the UDAP reference implementation for .NET. This package includes a SMART endpoint or controller to a ASP.NET application. + + + + + + + + + + + + + + + + \ + true + + + + + + + + + diff --git a/Udap.Smart.Metadata/docs/README.md b/Udap.Smart.Metadata/docs/README.md new file mode 100644 index 00000000..93510b81 --- /dev/null +++ b/Udap.Smart.Metadata/docs/README.md @@ -0,0 +1,7 @@ +# Udap.Smart.Metadata + +![UDAP logo](https://avatars.githubusercontent.com/u/77421324?s=48&v=4) + +## 📦 This package + + diff --git a/Udap.Smart.Model/SmartConstants.cs b/Udap.Smart.Model/SmartConstants.cs new file mode 100644 index 00000000..64f5d66a --- /dev/null +++ b/Udap.Smart.Model/SmartConstants.cs @@ -0,0 +1,18 @@ +#region (c) 2024 Joseph Shook. All rights reserved. +// /* +// Authors: +// Joseph Shook Joseph.Shook@Surescripts.com +// +// See LICENSE in the project root for license information. +// */ +#endregion + +namespace Udap.Smart.Model; + +public static class SmartConstants +{ + public static class Discovery + { + public const string DiscoveryEndpoint = ".well-known/smart-configuration"; + } +} diff --git a/Udap.Smart.Model/SmartMetadata.cs b/Udap.Smart.Model/SmartMetadata.cs new file mode 100644 index 00000000..ab720882 --- /dev/null +++ b/Udap.Smart.Model/SmartMetadata.cs @@ -0,0 +1,98 @@ +namespace Udap.Smart.Model; + +/// +/// FHIR SMART App Launch Metadata Class definition based on HL7 FHIR SMART App Launch Specification v 2.1 +/// https://hl7.org/fhir/smart-app-launch/conformance.html#metadata +/// +public class SmartMetadata +{ + + /// + /// CONDITIONAL, String conveying this system’s OpenID Connect Issuer URL. + /// Required if the server’s capabilities include sso-openid-connect; otherwise, omitted + /// + public string issuer { get; set; } + + /// + /// CONDITIONAL, String conveying this system’s JSON Web Key Set URL. + /// Required if the server’s capabilities include sso-openid-connect; otherwise, optional. + /// + public string jwks_uri { get; set; } + + /// + /// REQUIRED, URL to the OAuth2 authorization endpoint. + /// + public string authorization_endpoint { get; set; } + + /// + /// REQUIRED, Array of grant types supported at the token endpoint. + /// The options are “authorization_code” (when SMART App Launch is supported) and + /// “client_credentials” (when SMART Backend Services is supported). + /// + public ICollection grant_types_supported { get; set; } + + /// + /// REQUIRED, URL to the OAuth2 token endpoint. + /// + public string token_endpoint { get; set; } + + /// + /// OPTIONAL, array of client authentication methods supported by the token endpoint. + /// The options are “client_secret_post”, “client_secret_basic”, and “private_key_jwt”. + /// + public ICollection token_endpoint_auth_methods_supported { get; set; } + + /// + /// OPTIONAL, If available, URL to the OAuth2 dynamic registration endpoint for this FHIR server. + /// + public string registration_endpoint { get; set; } + + /// + /// CONDITIONAL, URL to the EHR’s app state endpoint. SHALL be present when the EHR supports the + /// smart-app-state capability and the endpoint is distinct from the EHR’s primary endpoint. + /// + public string smart_app_state_endpoint { get; set; } + + /// + /// RECOMMENDED, array of scopes a client may request. + /// See scopes and launch context. + /// The server SHALL support all scopes listed here; + /// additional scopes MAY be supported (so clients should not consider this an exhaustive list) + /// + public ICollection scopes_supported { get; set; } + + /// + /// RECOMMENDED, Array of OAuth2 response_type values that are supported. Implementers can + /// refer to response_types defined in OAuth 2.0 (RFC 6749) + /// and in OIDC Core. + /// + public ICollection response_types_supported { get; set; } + + /// + /// RECOMMENDED, URL where an end-user can view which applications currently have + /// access to data and can make adjustments to these access rights. + /// + public string management_endpoint { get; set; } + + /// + /// RECOMMENDED, URL to a server’s introspection endpoint that can be used to validate a token. + /// + public string introspection_endpoint { get; set; } + + /// + /// RECOMMENDED, URL to a server’s revoke endpoint that can be used to revoke a token. + /// + public string revocation_endpoint { get; set; } + + /// + /// REQUIRED, Array of strings representing SMART capabilities + /// (e.g., sso-openid-connect or launch-standalone) that the server supports. + /// + public ICollection capabilities { get; set; } + + /// + /// REQUIRED, Array of PKCE code challenge methods supported. The S256 method + /// SHALL be included in this list, and the plain method SHALL NOT be included in this list. + /// + public ICollection code_challenge_methods_supported { get; set; } +} diff --git a/Udap.Smart.Model/Udap.Smart.Model.csproj b/Udap.Smart.Model/Udap.Smart.Model.csproj new file mode 100644 index 00000000..bcf2b396 --- /dev/null +++ b/Udap.Smart.Model/Udap.Smart.Model.csproj @@ -0,0 +1,28 @@ + + + + netstandard2.0;net6.0;net7.0;net8.0 + enable + enable + git + https://github.com/JoeShook/udap-dotnet + README.md + Joseph Shook + Copyright © Joseph.Shook@Surescripts.com 2022-2024 + MIT + true + UDAP_Ecosystem_Gears 48X48.jpg + SMART;UDAP;FHIR;HL7 + Package is a part of the UDAP reference implementation for .NET. + + + + + + + \ + true + + + + diff --git a/Udap.Smart.Model/docs/README.md b/Udap.Smart.Model/docs/README.md new file mode 100644 index 00000000..6875461a --- /dev/null +++ b/Udap.Smart.Model/docs/README.md @@ -0,0 +1,7 @@ +# Udap.Smart.Model + +![UDAP logo](https://avatars.githubusercontent.com/u/77421324?s=48&v=4) + +## 📦 This package + + diff --git a/examples/Udap.Auth.Server/Pages/Account/AccessDenied.cshtml b/Udap.UI/Pages/Account/AccessDenied.cshtml similarity index 74% rename from examples/Udap.Auth.Server/Pages/Account/AccessDenied.cshtml rename to Udap.UI/Pages/Account/AccessDenied.cshtml index 92aca6d8..a5cfe643 100644 --- a/examples/Udap.Auth.Server/Pages/Account/AccessDenied.cshtml +++ b/Udap.UI/Pages/Account/AccessDenied.cshtml @@ -1,5 +1,5 @@ @page -@model Udap.Auth.Server.Pages.Account.AccessDeniedModel +@model Udap.UI.Pages.Account.AccessDeniedModel @{ }
diff --git a/examples/Udap.Auth.Server/Pages/Account/AccessDenied.cshtml.cs b/Udap.UI/Pages/Account/AccessDenied.cshtml.cs similarity index 75% rename from examples/Udap.Auth.Server/Pages/Account/AccessDenied.cshtml.cs rename to Udap.UI/Pages/Account/AccessDenied.cshtml.cs index aa52e5db..6cfbf7ed 100644 --- a/examples/Udap.Auth.Server/Pages/Account/AccessDenied.cshtml.cs +++ b/Udap.UI/Pages/Account/AccessDenied.cshtml.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc.RazorPages; -namespace Udap.Auth.Server.Pages.Account; +namespace Udap.UI.Pages.Account; public class AccessDeniedModel : PageModel { diff --git a/examples/Udap.Auth.Server/Pages/Account/Login/Index.cshtml b/Udap.UI/Pages/Account/Login/Index.cshtml similarity index 98% rename from examples/Udap.Auth.Server/Pages/Account/Login/Index.cshtml rename to Udap.UI/Pages/Account/Login/Index.cshtml index 9003cb60..332af5af 100644 --- a/examples/Udap.Auth.Server/Pages/Account/Login/Index.cshtml +++ b/Udap.UI/Pages/Account/Login/Index.cshtml @@ -1,5 +1,5 @@ @page -@model Udap.Auth.Server.Pages.Account.Login.Index +@model Udap.UI.Pages.Account.Login.Index