diff --git a/AspNet.Security.OAuth.Providers.sln b/AspNet.Security.OAuth.Providers.sln
index 5ecce9b56..0dc6761d6 100644
--- a/AspNet.Security.OAuth.Providers.sln
+++ b/AspNet.Security.OAuth.Providers.sln
@@ -295,6 +295,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.Smart
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.Huawei", "src\AspNet.Security.OAuth.Huawei\AspNet.Security.OAuth.Huawei.csproj", "{E3CF7FFC-56A0-4033-87A9-BB3080CF030E}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.PingOne", "src\AspNet.Security.OAuth.PingOne\AspNet.Security.OAuth.PingOne.csproj", "{CF8C4235-6AE6-404E-B572-4FF4E85AB5FF}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -677,6 +679,10 @@ Global
{E3CF7FFC-56A0-4033-87A9-BB3080CF030E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E3CF7FFC-56A0-4033-87A9-BB3080CF030E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E3CF7FFC-56A0-4033-87A9-BB3080CF030E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CF8C4235-6AE6-404E-B572-4FF4E85AB5FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CF8C4235-6AE6-404E-B572-4FF4E85AB5FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CF8C4235-6AE6-404E-B572-4FF4E85AB5FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CF8C4235-6AE6-404E-B572-4FF4E85AB5FF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -782,6 +788,7 @@ Global
{8E42EF81-A630-4BDB-B642-3F20C863F9BE} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
{68862DC5-65B7-4517-B909-AB334F9FCF6E} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
{E3CF7FFC-56A0-4033-87A9-BB3080CF030E} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
+ {CF8C4235-6AE6-404E-B572-4FF4E85AB5FF} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C7B54DE2-6407-4802-AD9C-CE54BF414C8C}
diff --git a/README.md b/README.md
index 19886a6e5..c3c2c4172 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,7 @@ We would love it if you could help contributing to this repository.
* [CoCo Lin](https://github.com/linmasaki)
* [Dave Timmins](https://github.com/davetimmins)
* [Dmitry Popov](https://github.com/justdmitry)
+* [Drew Killion](https://github.com/drewkill32)
* [Elan Hasson](https://github.com/ElanHasson)
* [Eric Green](https://github.com/ericgreenmix)
* [Ethan Celletti](https://github.com/Gekctek)
@@ -180,6 +181,7 @@ If a provider you're looking for does not exist, consider making a PR to add one
| Onshape | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Onshape?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Onshape/ "Download AspNet.Security.OAuth.Onshape from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Onshape?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Onshape "Download AspNet.Security.OAuth.Onshape from MyGet.org") | N/A |
| Patreon | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Patreon?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Patreon/ "Download AspNet.Security.OAuth.Patreon from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Patreon?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Patreon "Download AspNet.Security.OAuth.Patreon from MyGet.org") | [Documentation](https://docs.patreon.com/#oauth "Patreon developer documentation") |
| Paypal | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Paypal?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Paypal/ "Download AspNet.Security.OAuth.Paypal from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Paypal?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Paypal "Download AspNet.Security.OAuth.Paypal from MyGet.org") | [Documentation](https://developer.paypal.com/docs/api-basics/#oauth-20-authorization-protocol "Paypal developer documentation") |
+| PingOne | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.PingOne?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.PingOne/ "Download AspNet.Security.OAuth.PingOne from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.PingOne?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.PingOne "Download AspNet.Security.OAuth.PingOne from MyGet.org") | [Documentation](https://apidocs.pingidentity.com/pingone/platform/v1/api/#openid-connectoauth-2 "PingOne developer documentation") |
| QQ | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.QQ?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.QQ/ "Download AspNet.Security.OAuth.QQ from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.QQ?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.QQ "Download AspNet.Security.OAuth.QQ from MyGet.org") | [Documentation](https://developers.e.qq.com/docs/apilist/auth/oauth2 "QQ developer documentation") |
| QuickBooks | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.QuickBooks?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.QuickBooks/ "Download AspNet.Security.OAuth.QuickBooks from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.QuickBooks?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.QuickBooks "Download AspNet.Security.OAuth.QuickBooks from MyGet.org") | [Documentation](https://www.developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-2.0-playground "QuickBooks developer documentation") |
| Reddit | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Reddit?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Reddit/ "Download AspNet.Security.OAuth.Reddit from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Reddit?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Reddit "Download AspNet.Security.OAuth.Reddit from MyGet.org") | [Documentation](https://github.com/reddit-archive/reddit/wiki/oauth2 "Reddit developer documentation") |
diff --git a/docs/README.md b/docs/README.md
index 8f6fac04a..fabdb074e 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -61,6 +61,7 @@ covered by the section above.
| Odnoklassniki | _Optional_ | [Documentation](odnoklassniki.md "Odnoklassniki provider documentation") |
| Okta | **Required** | [Documentation](okta.md "Okta provider documentation") |
| Patreon | _Optional_ | [Documentation](patreon.md "Patreon provider documentation") |
+| PingOne | _Optional_ | [Documentation](pingone.md "PingOne provider documentation") |
| QQ | _Optional_ | [Documentation](qq.md "QQ provider documentation") |
| Reddit | _Optional_ | [Documentation](reddit.md "Reddit provider documentation") |
| Salesforce | _Optional_ | [Documentation](salesforce.md "Salesforce provider documentation") |
diff --git a/docs/pingone.md b/docs/pingone.md
new file mode 100644
index 000000000..15546c0e2
--- /dev/null
+++ b/docs/pingone.md
@@ -0,0 +1,26 @@
+# Integrating the PingOne Provider
+
+## Example
+
+```csharp
+services.AddAuthentication(options => /* Auth configuration */)
+ .AddPingOne(options =>
+ {
+ options.ClientId = "my-client-id";
+ options.ClientSecret = "my-client-secret";
+ options.EnvironmentId = "63e9d5c3-5bb8-462d-8f71-8e6b2592e516";
+ });
+```
+
+## Required Additional Settings
+
+| Property Name | Property Type | Description | Default Value |
+|:--|:--|:--|:--|
+| `EnvironmentId` | `string` | The PingOne EnvironmentId to use for authentication. This can be found on the `environment.properties` page of the PingOne admin portal for your account. | `""` |
+
+## Optional Settings
+
+| Property Name | Property Type | Description | Default Value |
+|:--|:--|:--|:--|
+| `Domain` | `string?` | The PingOne domain to use for authentication. Can be a custom domain configured in PingOne or one of the following: `auth.pingone.com` for the United States region, `auth.pingone.ca` for the Canada region, `auth.pingone.eu` for the European Union region, or `auth.pingone.asia` for the Asia-Pacific region. | `"auth.pingone.com"` |
+
diff --git a/src/AspNet.Security.OAuth.PingOne/AspNet.Security.OAuth.PingOne.csproj b/src/AspNet.Security.OAuth.PingOne/AspNet.Security.OAuth.PingOne.csproj
new file mode 100644
index 000000000..774a3e23d
--- /dev/null
+++ b/src/AspNet.Security.OAuth.PingOne/AspNet.Security.OAuth.PingOne.csproj
@@ -0,0 +1,25 @@
+
+
+
+ $(DefaultNetCoreTargetFramework)
+
+
+
+
+
+ true
+ 7.0.1
+
+
+
+ ASP.NET Core security provider enabling PingOne authentication.
+ Drew Killion
+ pingone;aspnetcore;authentication;oauth;security
+
+
+
+
+
+
+
+
diff --git a/src/AspNet.Security.OAuth.PingOne/PingOneAuthenticationDefaults.cs b/src/AspNet.Security.OAuth.PingOne/PingOneAuthenticationDefaults.cs
new file mode 100644
index 000000000..c3d680be7
--- /dev/null
+++ b/src/AspNet.Security.OAuth.PingOne/PingOneAuthenticationDefaults.cs
@@ -0,0 +1,53 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
+ * for more information concerning the license and the contributors participating to this project.
+ */
+
+namespace AspNet.Security.OAuth.PingOne;
+
+///
+/// Default values used by the PingOne authentication provider.
+///
+public static class PingOneAuthenticationDefaults
+{
+ ///
+ /// Default value for .
+ ///
+ public const string AuthenticationScheme = "PingOne";
+
+ ///
+ /// Default value for .
+ ///
+ public static readonly string DisplayName = "PingOne";
+
+ ///
+ /// Default value for .
+ ///
+ public static readonly string Issuer = "PingOne";
+
+ ///
+ /// Default value for PingOne domain.
+ ///
+ public static readonly string Domain = "auth.pingone.com";
+
+ ///
+ /// Default value for .
+ ///
+ public static readonly string CallbackPath = "/signin-pingone";
+
+ ///
+ /// Default path format to use for .
+ ///
+ public static readonly string AuthorizationEndpointPathFormat = "/{0}/as/authorize";
+
+ ///
+ /// Default path format to use for .
+ ///
+ public static readonly string TokenEndpointPathFormat = "/{0}/as/token";
+
+ ///
+ /// Default path format to use for .
+ ///
+ public static readonly string UserInformationEndpointPathFormat = "/{0}/as/userinfo";
+}
diff --git a/src/AspNet.Security.OAuth.PingOne/PingOneAuthenticationExtensions.cs b/src/AspNet.Security.OAuth.PingOne/PingOneAuthenticationExtensions.cs
new file mode 100644
index 000000000..00e1150a1
--- /dev/null
+++ b/src/AspNet.Security.OAuth.PingOne/PingOneAuthenticationExtensions.cs
@@ -0,0 +1,77 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
+ * for more information concerning the license and the contributors participating to this project.
+ */
+
+using AspNet.Security.OAuth.PingOne;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+///
+/// Extension methods to add PingOne authentication capabilities to an HTTP application pipeline.
+///
+public static class PingOneAuthenticationExtensions
+{
+ ///
+ /// Adds to the specified
+ /// , which enables PingOne authentication capabilities.
+ ///
+ /// The authentication builder.
+ /// The .
+ public static AuthenticationBuilder AddPingOne([NotNull] this AuthenticationBuilder builder)
+ {
+ return builder.AddPingOne(PingOneAuthenticationDefaults.AuthenticationScheme, options => { });
+ }
+
+ ///
+ /// Adds to the specified
+ /// , which enables PingOne authentication capabilities.
+ ///
+ /// The authentication builder.
+ /// The delegate used to configure the PingOne options.
+ /// The .
+ public static AuthenticationBuilder AddPingOne(
+ [NotNull] this AuthenticationBuilder builder,
+ [NotNull] Action configuration)
+ {
+ return builder.AddPingOne(PingOneAuthenticationDefaults.AuthenticationScheme, configuration);
+ }
+
+ ///
+ /// Adds to the specified
+ /// , which enables PingOne authentication capabilities.
+ ///
+ /// The authentication builder.
+ /// The authentication scheme associated with this instance.
+ /// The delegate used to configure the PingOne options.
+ /// The .
+ public static AuthenticationBuilder AddPingOne(
+ [NotNull] this AuthenticationBuilder builder,
+ [NotNull] string scheme,
+ [NotNull] Action configuration)
+ {
+ return builder.AddPingOne(scheme, PingOneAuthenticationDefaults.DisplayName, configuration);
+ }
+
+ ///
+ /// Adds to the specified
+ /// , which enables PingOne authentication capabilities.
+ ///
+ /// The authentication builder.
+ /// The authentication scheme associated with this instance.
+ /// The optional display name associated with this instance.
+ /// The delegate used to configure the PingOne options.
+ /// The .
+ public static AuthenticationBuilder AddPingOne(
+ [NotNull] this AuthenticationBuilder builder,
+ [NotNull] string scheme,
+ [CanBeNull] string caption,
+ [NotNull] Action configuration)
+ {
+ builder.Services.TryAddSingleton, PingOnePostConfigureOptions>();
+ return builder.AddOAuth(scheme, caption, configuration);
+ }
+}
diff --git a/src/AspNet.Security.OAuth.PingOne/PingOneAuthenticationHandler.cs b/src/AspNet.Security.OAuth.PingOne/PingOneAuthenticationHandler.cs
new file mode 100644
index 000000000..5a0e40d77
--- /dev/null
+++ b/src/AspNet.Security.OAuth.PingOne/PingOneAuthenticationHandler.cs
@@ -0,0 +1,84 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
+ * for more information concerning the license and the contributors participating to this project.
+ */
+
+using System.Net.Http.Headers;
+using System.Security.Claims;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace AspNet.Security.OAuth.PingOne;
+
+///
+/// Defines a handler for authentication using PingOne.
+///
+public partial class PingOneAuthenticationHandler : OAuthHandler
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The authentication options.
+ /// The logger to use.
+ /// The URL encoder to use.
+ /// The system clock to use.
+ public PingOneAuthenticationHandler(
+ [NotNull] IOptionsMonitor options,
+ [NotNull] ILoggerFactory logger,
+ [NotNull] UrlEncoder encoder,
+ [NotNull] ISystemClock clock)
+ : base(options, logger, encoder, clock)
+ {
+ }
+
+ ///
+ protected override async Task CreateTicketAsync(
+ [NotNull] ClaimsIdentity identity,
+ [NotNull] AuthenticationProperties properties,
+ [NotNull] OAuthTokenResponse tokens)
+ {
+ string endpoint = Options.UserInformationEndpoint;
+
+ using var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
+
+ using var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted);
+ if (!response.IsSuccessStatusCode)
+ {
+ await Log.UserProfileErrorAsync(Logger, response, Context.RequestAborted);
+ throw new HttpRequestException("An error occurred while retrieving the user profile from PingOne.");
+ }
+
+ using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync(Context.RequestAborted));
+
+ var principal = new ClaimsPrincipal(identity);
+ var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement);
+ context.RunClaimActions();
+
+ await Events.CreatingTicket(context);
+ return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name);
+ }
+
+ private static partial class Log
+ {
+ internal static async Task UserProfileErrorAsync(ILogger logger, HttpResponseMessage response, CancellationToken cancellationToken)
+ {
+ UserProfileError(
+ logger,
+ response.StatusCode,
+ response.Headers.ToString(),
+ await response.Content.ReadAsStringAsync(cancellationToken));
+ }
+
+ [LoggerMessage(1, LogLevel.Error, "An error occurred while retrieving the user profile: the remote server returned a {Status} response with the following payload: {Headers} {Body}.")]
+ private static partial void UserProfileError(
+ ILogger logger,
+ System.Net.HttpStatusCode status,
+ string headers,
+ string body);
+ }
+}
diff --git a/src/AspNet.Security.OAuth.PingOne/PingOneAuthenticationOptions.cs b/src/AspNet.Security.OAuth.PingOne/PingOneAuthenticationOptions.cs
new file mode 100644
index 000000000..2ea094437
--- /dev/null
+++ b/src/AspNet.Security.OAuth.PingOne/PingOneAuthenticationOptions.cs
@@ -0,0 +1,76 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
+ * for more information concerning the license and the contributors participating to this project.
+ */
+
+using System.Security.Claims;
+
+namespace AspNet.Security.OAuth.PingOne;
+
+///
+/// Defines a set of options used by .
+///
+public class PingOneAuthenticationOptions : OAuthOptions
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PingOneAuthenticationOptions()
+ {
+ ClaimsIssuer = PingOneAuthenticationDefaults.Issuer;
+ CallbackPath = PingOneAuthenticationDefaults.CallbackPath;
+ Domain = PingOneAuthenticationDefaults.Domain;
+ EnvironmentId = string.Empty;
+
+ Scope.Add("openid");
+ Scope.Add("profile");
+ Scope.Add("email");
+
+ ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
+ ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name");
+ ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
+ ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub");
+ ClaimActions.MapJsonKey(ClaimTypes.Surname, "family_name");
+ }
+
+ ///
+ /// Gets or sets the PingOne domain to use for authentication.
+ ///
+ ///
+ /// The default value is .
+ ///
+ public string Domain { get; set; }
+
+ ///
+ /// Gets or sets the PingOne environment Id to use for authentication.
+ ///
+ public string EnvironmentId { get; set; }
+
+ ///
+ public override void Validate()
+ {
+ base.Validate();
+
+ if (!Uri.TryCreate(AuthorizationEndpoint, UriKind.Absolute, out _))
+ {
+ throw new ArgumentException(
+ $"The '{nameof(AuthorizationEndpoint)}' option must be set to a valid URI.",
+ nameof(AuthorizationEndpoint));
+ }
+
+ if (!Uri.TryCreate(TokenEndpoint, UriKind.Absolute, out _))
+ {
+ throw new ArgumentException(
+ $"The '{nameof(TokenEndpoint)}' option must be set to a valid URI.",
+ nameof(TokenEndpoint));
+ }
+
+ if (!Uri.TryCreate(UserInformationEndpoint, UriKind.Absolute, out _))
+ {
+ throw new ArgumentException(
+ $"The '{nameof(UserInformationEndpoint)}' option must be set to a valid URI.",
+ nameof(UserInformationEndpoint));
+ }
+ }
+}
diff --git a/src/AspNet.Security.OAuth.PingOne/PingOnePostConfigureOptions.cs b/src/AspNet.Security.OAuth.PingOne/PingOnePostConfigureOptions.cs
new file mode 100644
index 000000000..492a14e0d
--- /dev/null
+++ b/src/AspNet.Security.OAuth.PingOne/PingOnePostConfigureOptions.cs
@@ -0,0 +1,51 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
+ * for more information concerning the license and the contributors participating to this project.
+ */
+
+using System.Globalization;
+using Microsoft.Extensions.Options;
+
+namespace AspNet.Security.OAuth.PingOne;
+
+///
+/// A class used to setup defaults for all .
+///
+public class PingOnePostConfigureOptions : IPostConfigureOptions
+{
+ ///
+ public void PostConfigure(
+ string? name,
+ [NotNull] PingOneAuthenticationOptions options)
+ {
+ if (string.IsNullOrWhiteSpace(options.Domain))
+ {
+ throw new ArgumentException("No PingOne domain configured.", nameof(options));
+ }
+
+ if (string.IsNullOrWhiteSpace(options.EnvironmentId))
+ {
+ throw new ArgumentException("No PingOne environment Id configured.", nameof(options));
+ }
+
+ options.AuthorizationEndpoint = CreateUrl(options.Domain, PingOneAuthenticationDefaults.AuthorizationEndpointPathFormat, options.EnvironmentId);
+ options.TokenEndpoint = CreateUrl(options.Domain, PingOneAuthenticationDefaults.TokenEndpointPathFormat, options.EnvironmentId);
+ options.UserInformationEndpoint = CreateUrl(options.Domain, PingOneAuthenticationDefaults.UserInformationEndpointPathFormat, options.EnvironmentId);
+ }
+
+ private static string CreateUrl(string domain, string pathFormat, params object[] args)
+ {
+ var path = string.Format(CultureInfo.InvariantCulture, pathFormat, args);
+
+ // Enforce use of HTTPS
+ var builder = new UriBuilder(domain)
+ {
+ Path = path,
+ Port = -1,
+ Scheme = Uri.UriSchemeHttps,
+ };
+
+ return builder.Uri.ToString();
+ }
+}
diff --git a/test/AspNet.Security.OAuth.Providers.Tests/PingOne/PingOneAuthenticationOptionsTests.cs b/test/AspNet.Security.OAuth.Providers.Tests/PingOne/PingOneAuthenticationOptionsTests.cs
new file mode 100644
index 000000000..e22ac42a5
--- /dev/null
+++ b/test/AspNet.Security.OAuth.Providers.Tests/PingOne/PingOneAuthenticationOptionsTests.cs
@@ -0,0 +1,75 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
+ * for more information concerning the license and the contributors participating to this project.
+ */
+
+namespace AspNet.Security.OAuth.PingOne;
+
+public static class PingOneAuthenticationOptionsTests
+{
+ [Fact]
+ public static void Validate_Throws_If_AuthorizationEndpoint_Not_Set()
+ {
+ // Arrange
+ var options = new PingOneAuthenticationOptions()
+ {
+ ClientId = "ClientId",
+ ClientSecret = "ClientSecret",
+ TokenEndpoint = "https://auth.pingone.local",
+ UserInformationEndpoint = "https://auth.pingone.local",
+ };
+
+ // Act and Assert
+ Assert.Throws("AuthorizationEndpoint", () => options.Validate());
+ }
+
+ [Fact]
+ public static void Validate_Throws_If_TokenEndpoint_Not_Set()
+ {
+ // Arrange
+ var options = new PingOneAuthenticationOptions()
+ {
+ AuthorizationEndpoint = "https://auth.pingone.local",
+ ClientId = "ClientId",
+ ClientSecret = "ClientSecret",
+ UserInformationEndpoint = "https://auth.pingone.local",
+ };
+
+ // Act and Assert
+ Assert.Throws("TokenEndpoint", () => options.Validate());
+ }
+
+ [Fact]
+ public static void Validate_Throws_If_UserInformationEndpoint_Not_Set()
+ {
+ // Arrange
+ var options = new PingOneAuthenticationOptions()
+ {
+ AuthorizationEndpoint = "https://auth.pingone.local",
+ ClientId = "ClientId",
+ ClientSecret = "ClientSecret",
+ TokenEndpoint = "https://auth.pingone.local",
+ };
+
+ // Act and Assert
+ Assert.Throws("UserInformationEndpoint", () => options.Validate());
+ }
+
+ [Fact]
+ public static void Validate_Does_Not_Throw_If_Uris_Are_Valid()
+ {
+ // Arrange
+ var options = new PingOneAuthenticationOptions()
+ {
+ AuthorizationEndpoint = "https://auth.pingone.local",
+ ClientId = "ClientId",
+ ClientSecret = "ClientSecret",
+ TokenEndpoint = "https://auth.pingone.local",
+ UserInformationEndpoint = "https://auth.pingone.local",
+ };
+
+ // Act (no Assert)
+ options.Validate();
+ }
+}
diff --git a/test/AspNet.Security.OAuth.Providers.Tests/PingOne/PingOnePostConfigureOptionsTests.cs b/test/AspNet.Security.OAuth.Providers.Tests/PingOne/PingOnePostConfigureOptionsTests.cs
new file mode 100644
index 000000000..2bedc17c6
--- /dev/null
+++ b/test/AspNet.Security.OAuth.Providers.Tests/PingOne/PingOnePostConfigureOptionsTests.cs
@@ -0,0 +1,102 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
+ * for more information concerning the license and the contributors participating to this project.
+ */
+
+namespace AspNet.Security.OAuth.PingOne;
+
+public static class PingOnePostConfigureOptionsTests
+{
+ [Fact]
+ public static void PostConfigure_Configures_Valid_Endpoints()
+ {
+ // Arrange
+ string name = "PingOne";
+ var target = new PingOnePostConfigureOptions();
+
+ var options = new PingOneAuthenticationOptions()
+ {
+ EnvironmentId = "b775aadd-a2f3-4d88-a768-b7c85dd1af47"
+ };
+
+ // Act
+ target.PostConfigure(name, options);
+
+ // Assert
+ options.AuthorizationEndpoint.ShouldStartWith("https://auth.pingone.com/b775aadd-a2f3-4d88-a768-b7c85dd1af47/");
+ Uri.TryCreate(options.AuthorizationEndpoint, UriKind.Absolute, out _).ShouldBeTrue();
+
+ options.TokenEndpoint.ShouldStartWith("https://auth.pingone.com/b775aadd-a2f3-4d88-a768-b7c85dd1af47/");
+ Uri.TryCreate(options.TokenEndpoint, UriKind.Absolute, out _).ShouldBeTrue();
+
+ options.UserInformationEndpoint.ShouldStartWith("https://auth.pingone.com/b775aadd-a2f3-4d88-a768-b7c85dd1af47/");
+ Uri.TryCreate(options.UserInformationEndpoint, UriKind.Absolute, out _).ShouldBeTrue();
+ }
+
+ [Fact]
+ public static void PostConfigure_Configures_Valid_Endpoints_With_Custom_Domain()
+ {
+ // Arrange
+ string name = "PingOne";
+ var target = new PingOnePostConfigureOptions();
+
+ var options = new PingOneAuthenticationOptions()
+ {
+ EnvironmentId = "b775aadd-a2f3-4d88-a768-b7c85dd1af47",
+ Domain = "auth.pingone.local"
+ };
+
+ // Act
+ target.PostConfigure(name, options);
+
+ // Assert
+ options.AuthorizationEndpoint.ShouldStartWith("https://auth.pingone.local/b775aadd-a2f3-4d88-a768-b7c85dd1af47/");
+ Uri.TryCreate(options.AuthorizationEndpoint, UriKind.Absolute, out _).ShouldBeTrue();
+
+ options.TokenEndpoint.ShouldStartWith("https://auth.pingone.local/b775aadd-a2f3-4d88-a768-b7c85dd1af47/");
+ Uri.TryCreate(options.TokenEndpoint, UriKind.Absolute, out _).ShouldBeTrue();
+
+ options.UserInformationEndpoint.ShouldStartWith("https://auth.pingone.local/b775aadd-a2f3-4d88-a768-b7c85dd1af47/");
+ Uri.TryCreate(options.UserInformationEndpoint, UriKind.Absolute, out _).ShouldBeTrue();
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public static void PostConfigure_Throws_If_Domain_Is_Invalid(string value)
+ {
+ // Arrange
+ string name = "PingOne";
+ var target = new PingOnePostConfigureOptions();
+
+ var options = new PingOneAuthenticationOptions()
+ {
+ Domain = value,
+ EnvironmentId = "b775aadd-a2f3-4d88-a768-b7c85dd1af47",
+ };
+
+ // Act and Assert
+ Assert.Throws("options", () => target.PostConfigure(name, options));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public static void PostConfigure_Throws_If_EnvironmentId_Is_Invalid(string value)
+ {
+ // Arrange
+ string name = "PingOne";
+ var target = new PingOnePostConfigureOptions();
+
+ var options = new PingOneAuthenticationOptions()
+ {
+ EnvironmentId = value,
+ };
+
+ // Act and Assert
+ Assert.Throws("options", () => target.PostConfigure(name, options));
+ }
+}
diff --git a/test/AspNet.Security.OAuth.Providers.Tests/PingOne/PingOneTests.cs b/test/AspNet.Security.OAuth.Providers.Tests/PingOne/PingOneTests.cs
new file mode 100644
index 000000000..7ad2bff69
--- /dev/null
+++ b/test/AspNet.Security.OAuth.Providers.Tests/PingOne/PingOneTests.cs
@@ -0,0 +1,70 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
+ * for more information concerning the license and the contributors participating to this project.
+ */
+
+namespace AspNet.Security.OAuth.PingOne;
+
+public class PingOneTests : OAuthTests
+{
+ public PingOneTests(ITestOutputHelper outputHelper)
+ {
+ OutputHelper = outputHelper;
+ }
+
+ public override string DefaultScheme => PingOneAuthenticationDefaults.AuthenticationScheme;
+
+ protected internal override void RegisterAuthentication(AuthenticationBuilder builder)
+ {
+ builder.AddPingOne(options =>
+ {
+ ConfigureDefaults(builder, options);
+ options.EnvironmentId = "b775aadd-a2f3-4d88-a768-b7c85dd1af47";
+ });
+ }
+
+ [Theory]
+ [InlineData(ClaimTypes.Email, "john.doe@example.com")]
+ [InlineData(ClaimTypes.GivenName, "John")]
+ [InlineData(ClaimTypes.Name, "John Doe")]
+ [InlineData(ClaimTypes.NameIdentifier, "00uid4BxXw6I6TV4m0g3")]
+ [InlineData(ClaimTypes.Surname, "Doe")]
+ public async Task Can_Sign_In_Using_PingOne(string claimType, string claimValue)
+ {
+ // Arrange
+ using var server = CreateTestServer();
+
+ // Act
+ var claims = await AuthenticateUserAsync(server);
+
+ // Assert
+ AssertClaim(claims, claimType, claimValue);
+ }
+
+ [Theory]
+ [InlineData(ClaimTypes.Email, "jane.doe@example.com")]
+ [InlineData(ClaimTypes.GivenName, "Jane")]
+ [InlineData(ClaimTypes.Name, "Jane Doe")]
+ [InlineData(ClaimTypes.NameIdentifier, "00uid4BxXw6I6TV4m0g4")]
+ [InlineData(ClaimTypes.Surname, "Doe")]
+ public async Task Can_Sign_In_Using_PingOne_With_Custom_Domain(string claimType, string claimValue)
+ {
+ // Arrange
+ static void ConfigureServices(IServiceCollection services)
+ {
+ services.Configure("PingOne", (options) =>
+ {
+ options.Domain = "auth.pingone.local";
+ });
+ }
+
+ using var server = CreateTestServer(ConfigureServices);
+
+ // Act
+ var claims = await AuthenticateUserAsync(server);
+
+ // Assert
+ AssertClaim(claims, claimType, claimValue);
+ }
+}
diff --git a/test/AspNet.Security.OAuth.Providers.Tests/PingOne/bundle.json b/test/AspNet.Security.OAuth.Providers.Tests/PingOne/bundle.json
new file mode 100644
index 000000000..1049573da
--- /dev/null
+++ b/test/AspNet.Security.OAuth.Providers.Tests/PingOne/bundle.json
@@ -0,0 +1,87 @@
+{
+ "$schema": "https://raw.githubusercontent.com/justeat/httpclient-interception/master/src/HttpClientInterception/Bundles/http-request-bundle-schema.json",
+ "items": [
+ {
+ "comment": "https://apidocs.pingidentity.com/pingone/platform/v1/api/#token",
+ "uri": "https://auth.pingone.com/b775aadd-a2f3-4d88-a768-b7c85dd1af47/as/token",
+ "method": "POST",
+ "contentFormat": "json",
+ "contentJson": {
+ "access_token": "secret-access-token",
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ "scope": "openid email",
+ "refresh_token": "secret-refresh-token",
+ "id_token": "secret-id-token"
+ }
+ },
+ {
+ "comment": "https://apidocs.pingidentity.com/pingone/platform/v1/api/#post-userinfo",
+ "uri": "https://auth.pingone.com/b775aadd-a2f3-4d88-a768-b7c85dd1af47/as/userinfo",
+ "contentFormat": "json",
+ "contentJson": {
+ "sub": "00uid4BxXw6I6TV4m0g3",
+ "name": "John Doe",
+ "nickname": "Jimmy",
+ "given_name": "John",
+ "middle_name": "James",
+ "family_name": "Doe",
+ "profile": "https://example.com/john.doe",
+ "zoneinfo": "America/Los_Angeles",
+ "locale": "en-US",
+ "updated_at": 1311280970,
+ "email": "john.doe@example.com",
+ "email_verified": true,
+ "address": {
+ "street_address": "123 Hollywood Blvd.",
+ "locality": "Los Angeles",
+ "region": "CA",
+ "postal_code": "90210",
+ "country": "US"
+ },
+ "phone_number": "+1 (425) 555-1212"
+ }
+ },
+ {
+ "comment": "https://apidocs.pingidentity.com/pingone/platform/v1/api/#token",
+ "uri": "https://auth.pingone.local/b775aadd-a2f3-4d88-a768-b7c85dd1af47/as/token",
+ "method": "POST",
+ "contentFormat": "json",
+ "contentJson": {
+ "access_token": "secret-access-token",
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ "scope": "openid email",
+ "refresh_token": "secret-refresh-token",
+ "id_token": "secret-id-token"
+ }
+ },
+ {
+ "comment": "https://apidocs.pingidentity.com/pingone/platform/v1/api/#post-userinfo",
+ "uri": "https://auth.pingone.local/b775aadd-a2f3-4d88-a768-b7c85dd1af47/as/userinfo",
+ "contentFormat": "json",
+ "contentJson": {
+ "sub": "00uid4BxXw6I6TV4m0g4",
+ "name": "Jane Doe",
+ "nickname": "Jan",
+ "given_name": "Jane",
+ "middle_name": "Jenny",
+ "family_name": "Doe",
+ "profile": "https://example.com/jane.doe",
+ "zoneinfo": "America/Los_Angeles",
+ "locale": "en-US",
+ "updated_at": 1311280970,
+ "email": "jane.doe@example.com",
+ "email_verified": true,
+ "address": {
+ "street_address": "123 Hollywood Blvd.",
+ "locality": "Los Angeles",
+ "region": "CA",
+ "postal_code": "90210",
+ "country": "US"
+ },
+ "phone_number": "+1 (425) 555-1212"
+ }
+ }
+ ]
+}