From 0435a6d2451c6a2fe1f6539dc0e214bf0807e2ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Rodr=C3=ADguez?= Date: Thu, 9 Dec 2021 12:02:16 -0500 Subject: [PATCH] Request RC-4.0.0 merging to the main branch approval (#192) * Expose OIDC and JWT events (#187) * Expose OIDC events for .NET 4.x and Core MVC projects. * Expose JWT events for .NET 4.x and Core WebApi projects. * Expose OIDC events for .NET Core MVC projects. * Expose JWT events for .NET Core WebApi projects. * Add tests. * Update readme. --- MIGRATING.md | 109 ++++++++++ .../OktaHttpMessageHandlerShould.cs | 2 +- Okta.AspNet.Abstractions/CHANGELOG.md | 18 ++ .../Okta.AspNet.Abstractions.csproj | 2 +- .../OktaHttpMessageHandler.cs | 8 +- Okta.AspNet.Abstractions/OktaWebApiOptions.cs | 4 +- Okta.AspNet.Abstractions/OktaWebOptions.cs | 20 ++ Okta.AspNet.Test/HttpClientBuilderShould.cs | 38 ++++ Okta.AspNet.Test/JwtOptionsBuilderShould.cs | 34 +++ Okta.AspNet.Test/MockHttpClientHandler.cs | 44 ++++ Okta.AspNet.Test/Okta.AspNet.Test.csproj | 2 +- .../OktaHttpMessageHandlerShould.cs | 198 ++++++++++-------- ...nnectAuthenticationOptionsBuilderShould.cs | 9 +- .../MiddlewareShould.cs | 32 +++ .../MockHttpMessageHandler.cs | 30 +++ .../Okta.AspNet.WebApi.IntegrationTest.csproj | 20 +- Okta.AspNet.WebApi.IntegrationTest/Startup.cs | 18 +- Okta.AspNet.WebApi.IntegrationTest/Web.config | 6 +- .../packages.config | 7 +- Okta.AspNet/CHANGELOG.md | 14 ++ Okta.AspNet/HttpClientBuilder.cs | 23 ++ Okta.AspNet/JwtOptionsBuilder.cs | 61 ++++++ Okta.AspNet/Okta.AspNet.csproj | 2 +- Okta.AspNet/OktaMiddlewareExtensions.cs | 35 +--- Okta.AspNet/OktaMvcOptions.cs | 12 +- Okta.AspNet/OktaWebApiOptions.cs | 11 + ...enIdConnectAuthenticationOptionsBuilder.cs | 38 +++- .../OktaMiddlewareShould.cs | 13 ++ .../Startup.cs | 17 ++ .../Okta.AspNetCore.Test.csproj | 18 +- .../OpenIdConnectOptionsHelperShould.cs | 75 +++++-- .../OktaMiddlewareShould.cs | 14 ++ .../Startup.cs | 11 + Okta.AspNetCore/CHANGELOG.md | 14 ++ Okta.AspNetCore/Okta.AspNetCore.csproj | 2 +- .../OktaAuthenticationOptionsExtensions.cs | 40 ++-- Okta.AspNetCore/OktaMvcOptions.cs | 25 +-- Okta.AspNetCore/OktaWebApiOptions.cs | 12 ++ Okta.AspNetCore/OpenIdConnectOptionsHelper.cs | 44 ++-- README.md | 13 +- docs/aspnet4x-mvc.md | 67 +++++- docs/aspnet4x-webapi.md | 53 +++++ docs/aspnetcore-mvc.md | 73 ++++++- docs/aspnetcore-webapi.md | 54 +++++ 44 files changed, 1082 insertions(+), 260 deletions(-) create mode 100644 MIGRATING.md create mode 100644 Okta.AspNet.Test/HttpClientBuilderShould.cs create mode 100644 Okta.AspNet.Test/JwtOptionsBuilderShould.cs create mode 100644 Okta.AspNet.Test/MockHttpClientHandler.cs create mode 100644 Okta.AspNet.WebApi.IntegrationTest/MockHttpMessageHandler.cs create mode 100644 Okta.AspNet/HttpClientBuilder.cs create mode 100644 Okta.AspNet/JwtOptionsBuilder.cs diff --git a/MIGRATING.md b/MIGRATING.md new file mode 100644 index 0000000..e9000d2 --- /dev/null +++ b/MIGRATING.md @@ -0,0 +1,109 @@ +# Okta.AspNet SDK migration guide + +This library uses semantic versioning and follows Okta's [library version policy](https://developer.okta.com/code/library-versions/). In short, we don't make breaking changes unless the major version changes! + +## Migrating from Okta.AspNet 1.x to 2.x + +In previous versions, the `OktaMvcOptions` exposed the `SecurityTokenValidated` and `AuthenticationFailed` events you could hook into. Starting in 2.x series, the `OktaMvcOptions` exposes the `OpenIdConnectEvents` property which allows you to hook into all the events provided by the uderlying OIDC middleware. + +_Before:_ + +```csharp +public class Startup +{ + public void Configuration(IAppBuilder app) + { + app.UseOktaMvc(new OktaMvcOptions() + { + // ... other configuration options removed for brevity ... + AuthenticationFailed = OnAuthenticationFailed, + }); + } + + public async Task OnAuthenticationFailed(AuthenticationFailedNotification notification) + { + await Task.Run(() => + { + notification.Response.Redirect("{YOUR-EXCEPTION-HANDLING-ENDPOINT}?message=" + notification.Exception.Message); + notification.HandleResponse(); + }); + } +} +``` + +_Now:_ + +```csharp + app.UseOktaMvc(new OktaMvcOptions() + { + // ... other configuration options removed for brevity ... + OpenIdConnectEvents = new OpenIdConnectAuthenticationNotifications + { + AuthenticationFailed = OnAuthenticationFailed, + }, + }); +``` +## Migrating from Okta.AspNetCore 3.x to 4.x + +In previous versions, the `OktaMvcOptions` exposed the `OnTokenValidated`, `OnOktaApiFailure`, `OnUserInformationReceived` and `OnAuthenticationFailed` events you could hook into. Starting in 4.x series, the `OktaMvcOptions` exposes the `OpenIdConnectEvents` property which allows you to hook into all the events provided by the uderlying OIDC middleware. + +_Before:_ + +```csharp +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddOktaMvc(new OktaMvcOptions + { + // ... other configuration options removed for brevity ... + OnOktaApiFailure = OnOktaApiFailure, + OnAuthenticationFailed = OnAuthenticationFailed, + }); + } + + public async Task OnOktaApiFailure(RemoteFailureContext context) + { + await Task.Run(() => + { + context.Response.Redirect("{YOUR-EXCEPTION-HANDLING-ENDPOINT}?message=" + context.Failure.Message); + context.HandleResponse(); + }); + } + + public async Task OnAuthenticationFailed(AuthenticationFailedContext context) + { + await Task.Run(() => + { + context.Response.Redirect("{YOUR-EXCEPTION-HANDLING-ENDPOINT}?message=" + context.Exception.Message); + context.HandleResponse(); + }); + } +} +``` + +_Now:_ + +```csharp +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddOktaMvc(new OktaMvcOptions + { + // ... other configuration options removed for brevity ... + OpenIdConnectEvents = new OpenIdConnectEvents + { + OnAuthenticationFailed = OnAuthenticationFailed, + OnRemoteFailure = OnOktaApiFailure, + }, + }); + } +} +``` + +## Getting help + +If you have questions about this library or about the Okta APIs, post a question on our [Developer Forum](https://devforum.okta.com). + +If you find a bug or have a feature request for this library specifically, [post an issue](https://github.com/okta/okta-aspnet/issues) here on GitHub. diff --git a/Okta.AspNet.Abstractions.Test/OktaHttpMessageHandlerShould.cs b/Okta.AspNet.Abstractions.Test/OktaHttpMessageHandlerShould.cs index 8e530b0..422586e 100644 --- a/Okta.AspNet.Abstractions.Test/OktaHttpMessageHandlerShould.cs +++ b/Okta.AspNet.Abstractions.Test/OktaHttpMessageHandlerShould.cs @@ -23,7 +23,7 @@ public async Task BuildUserAgent(string frameworkName) { var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://foo.com"); var version = typeof(OktaHttpMessageHandlerShould).Assembly.GetName().Version; - var handler = new OktaHttpMessageHandler(frameworkName, version) + var handler = new OktaHttpMessageHandler(frameworkName, version, new OktaWebOptions()) { InnerHandler = new TestHandler(), }; diff --git a/Okta.AspNet.Abstractions/CHANGELOG.md b/Okta.AspNet.Abstractions/CHANGELOG.md index 39a6f09..f11957c 100644 --- a/Okta.AspNet.Abstractions/CHANGELOG.md +++ b/Okta.AspNet.Abstractions/CHANGELOG.md @@ -1,6 +1,24 @@ # Changelog Running changelog of releases since `3.0.5` +## v4.0.0 + +### Features + +- Add support for OIDC events configuration in MVC projects. +- Add support for JWT events configuration in Web API projects. +- Add support for BackchannelHttpHandler configuration. +- Add support for BackchannelTimeout configuration. + +### Breaking changes + +- Remove `ClientId` property from `WebApiOptions`. + +### Features + +- Add strong name signature. + + ## v3.2.2 ### Bug Fix diff --git a/Okta.AspNet.Abstractions/Okta.AspNet.Abstractions.csproj b/Okta.AspNet.Abstractions/Okta.AspNet.Abstractions.csproj index 4509b05..78f17a1 100644 --- a/Okta.AspNet.Abstractions/Okta.AspNet.Abstractions.csproj +++ b/Okta.AspNet.Abstractions/Okta.AspNet.Abstractions.csproj @@ -2,7 +2,7 @@ net452;netstandard2.0 - 3.2.2 + 4.0.0 diff --git a/Okta.AspNet.Abstractions/OktaHttpMessageHandler.cs b/Okta.AspNet.Abstractions/OktaHttpMessageHandler.cs index 99b6a17..5df59b4 100644 --- a/Okta.AspNet.Abstractions/OktaHttpMessageHandler.cs +++ b/Okta.AspNet.Abstractions/OktaHttpMessageHandler.cs @@ -18,11 +18,13 @@ public class OktaHttpMessageHandler : DelegatingHandler { private readonly Lazy _userAgent; - public OktaHttpMessageHandler(string frameworkName, Version frameworkVersion, OktaWebOptions oktaWebOptions = null) + public OktaHttpMessageHandler(string frameworkName, Version frameworkVersion, OktaWebOptions oktaWebOptions) { _userAgent = new Lazy(() => new UserAgentBuilder(frameworkName, frameworkVersion).GetUserAgent()); - InnerHandler = new HttpClientHandler(); - if (oktaWebOptions?.Proxy != null) + InnerHandler = oktaWebOptions.BackchannelHttpClientHandler ?? new HttpClientHandler(); + + // If a backchannel handler is provided, then the proxy config is not overwritten + if (oktaWebOptions.BackchannelHttpClientHandler == null && oktaWebOptions.Proxy != null) { ((HttpClientHandler)InnerHandler).Proxy = new DefaultProxy(oktaWebOptions.Proxy); } diff --git a/Okta.AspNet.Abstractions/OktaWebApiOptions.cs b/Okta.AspNet.Abstractions/OktaWebApiOptions.cs index b6ff3a0..b39557a 100644 --- a/Okta.AspNet.Abstractions/OktaWebApiOptions.cs +++ b/Okta.AspNet.Abstractions/OktaWebApiOptions.cs @@ -4,6 +4,7 @@ // using System; +using System.Net.Http; namespace Okta.AspNet.Abstractions { @@ -12,8 +13,5 @@ public class OktaWebApiOptions : OktaWebOptions public static readonly string DefaultAudience = "api://default"; public string Audience { get; set; } = DefaultAudience; - - [Obsolete("ClientId is no longer required, and has no effect. This property will be removed in the next major release.")] - public string ClientId { get; set; } } } diff --git a/Okta.AspNet.Abstractions/OktaWebOptions.cs b/Okta.AspNet.Abstractions/OktaWebOptions.cs index 7dbbf91..875a516 100644 --- a/Okta.AspNet.Abstractions/OktaWebOptions.cs +++ b/Okta.AspNet.Abstractions/OktaWebOptions.cs @@ -4,6 +4,7 @@ // using System; +using System.Net.Http; namespace Okta.AspNet.Abstractions { @@ -40,6 +41,25 @@ public class OktaWebOptions /// /// Gets or sets the URI of your organization's proxy server. The default is null. /// + /// + /// The URI of your organization's proxy server. The default is null. + /// public ProxyConfiguration Proxy { get; set; } + + /// + /// Gets or sets the HttpMessageHandler used to communicate with Okta. + /// + /// + /// The HttpMessageHandler used to communicate with Okta. + /// + public HttpMessageHandler BackchannelHttpClientHandler { get; set; } + + /// + /// Gets or sets timeout value in milliseconds for back channel communications with Okta. + /// + /// + /// Timeout value in milliseconds for back channel communications with Okta. + /// + public TimeSpan BackchannelTimeout { get; set; } = TimeSpan.FromSeconds(60); } } diff --git a/Okta.AspNet.Test/HttpClientBuilderShould.cs b/Okta.AspNet.Test/HttpClientBuilderShould.cs new file mode 100644 index 0000000..b0799f5 --- /dev/null +++ b/Okta.AspNet.Test/HttpClientBuilderShould.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) 2018-present Okta, Inc. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace Okta.AspNet.Test +{ + public class HttpClientBuilderShould + { + [Fact] + public async Task InvokeCustomHandler() + { + var handler = new MockHttpClientHandler(); + + var options = new OktaWebApiOptions(); + options.OktaDomain = "https://test.okta.com"; + options.BackchannelHttpClientHandler = handler; + options.BackchannelTimeout = TimeSpan.FromMinutes(5); + + options.BackchannelHttpClientHandler.Should().NotBeNull(); + + var client = HttpClientBuilder.CreateClient(options); + + var response = await client.GetAsync("http://www.okta.com"); + + handler.NumberOfCalls.Should().BeGreaterThan(0); + client.Timeout.Should().Be(TimeSpan.FromMinutes(5)); + } + } +} diff --git a/Okta.AspNet.Test/JwtOptionsBuilderShould.cs b/Okta.AspNet.Test/JwtOptionsBuilderShould.cs new file mode 100644 index 0000000..5a20f03 --- /dev/null +++ b/Okta.AspNet.Test/JwtOptionsBuilderShould.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) 2018-present Okta, Inc. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. +// + +using System; +using FluentAssertions; +using Microsoft.Owin.Security.OAuth; +using NSubstitute; +using Xunit; + +namespace Okta.AspNet.Test +{ + public class JwtOptionsBuilderShould + { + [Fact] + public void BuildJwtBearerOptions() + { + var mockAuthnProvider = Substitute.For(); + + var oktaWebApiOptions = new OktaWebApiOptions + { + OktaDomain = "http://myoktadomain.com", + BackchannelTimeout = TimeSpan.FromMinutes(5), + BackchannelHttpClientHandler = new MockHttpClientHandler(), + OAuthBearerAuthenticationProvider = mockAuthnProvider, + }; + + var jwtOptions = JwtOptionsBuilder.BuildJwtBearerAuthenticationOptions(oktaWebApiOptions); + jwtOptions.Should().NotBeNull(); + jwtOptions.Provider.Should().Be(mockAuthnProvider); + } + } +} diff --git a/Okta.AspNet.Test/MockHttpClientHandler.cs b/Okta.AspNet.Test/MockHttpClientHandler.cs new file mode 100644 index 0000000..f31af45 --- /dev/null +++ b/Okta.AspNet.Test/MockHttpClientHandler.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) 2018-present Okta, Inc. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. +// + +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Okta.AspNet.Test +{ + public class MockHttpClientHandler : DelegatingHandler + { + private readonly string _response; + private readonly HttpStatusCode _statusCode; + + public string Body { get; private set; } + + public int NumberOfCalls { get; private set; } + + public MockHttpClientHandler(string response = "{}", HttpStatusCode statusCode = HttpStatusCode.OK) + { + _response = response; + _statusCode = statusCode; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) + { + NumberOfCalls++; + + if (request.Content != null) + { + Body = await request.Content.ReadAsStringAsync(); + } + + return new HttpResponseMessage + { + StatusCode = _statusCode, + Content = new StringContent(_response), + }; + } + } +} diff --git a/Okta.AspNet.Test/Okta.AspNet.Test.csproj b/Okta.AspNet.Test/Okta.AspNet.Test.csproj index 1d42c86..59dcf35 100644 --- a/Okta.AspNet.Test/Okta.AspNet.Test.csproj +++ b/Okta.AspNet.Test/Okta.AspNet.Test.csproj @@ -9,7 +9,7 @@ - + diff --git a/Okta.AspNet.Test/OktaHttpMessageHandlerShould.cs b/Okta.AspNet.Test/OktaHttpMessageHandlerShould.cs index 72eda92..c6cd854 100644 --- a/Okta.AspNet.Test/OktaHttpMessageHandlerShould.cs +++ b/Okta.AspNet.Test/OktaHttpMessageHandlerShould.cs @@ -17,114 +17,144 @@ namespace Okta.AspNet.Test public class OktaHttpMessageHandlerShould { [Fact] - public async Task SetInnerHandlerWebProxy() + public void SetInnerHandlerWebProxy() { - await Task.Run(() => + var testProxyAddress = "http://test.cxm/"; + var testFrameworkName = $"{nameof(SetInnerHandlerWebProxy)}_testFrameworkName"; + var version = typeof(OktaHttpMessageHandlerShould).Assembly.GetName().Version; + var oktaHandler = new OktaHttpMessageHandler(testFrameworkName, version, new OktaMvcOptions { - var testProxyAddress = "http://test.cxm/"; - var testFrameworkName = $"{nameof(SetInnerHandlerWebProxy)}_testFrameworkName"; - var version = typeof(OktaHttpMessageHandlerShould).Assembly.GetName().Version; - var oktaHandler = new OktaHttpMessageHandler(testFrameworkName, version, new OktaMvcOptions + Proxy = new ProxyConfiguration { - Proxy = new ProxyConfiguration - { - Host = testProxyAddress, - }, - }); - oktaHandler.InnerHandler.Should().NotBeNull(); - oktaHandler.InnerHandler.Should().BeAssignableTo(); - - var httpClientHandler = (HttpClientHandler)oktaHandler.InnerHandler; - httpClientHandler.Proxy.Should().NotBeNull(); - httpClientHandler.Proxy.Should().BeAssignableTo(); - - var webProxy = (DefaultProxy)httpClientHandler.Proxy; - webProxy.GetProxy(Arg.Any()).ToString().Should().Be(testProxyAddress); - webProxy.Credentials.Should().BeNull(); + Host = testProxyAddress, + }, }); + oktaHandler.InnerHandler.Should().NotBeNull(); + oktaHandler.InnerHandler.Should().BeAssignableTo(); + + var httpClientHandler = (HttpClientHandler)oktaHandler.InnerHandler; + httpClientHandler.Proxy.Should().NotBeNull(); + httpClientHandler.Proxy.Should().BeAssignableTo(); + + var webProxy = (DefaultProxy)httpClientHandler.Proxy; + webProxy.GetProxy(Arg.Any()).ToString().Should().Be(testProxyAddress); + webProxy.Credentials.Should().BeNull(); } [Fact] - public async Task SetInnerHandlerWebProxyPort() + public void SetCustomInnerHandler() { - await Task.Run(() => + var testProxyAddress = "http://test.cxm/"; + var testFrameworkName = $"{nameof(SetInnerHandlerWebProxy)}_testFrameworkName"; + var version = typeof(OktaHttpMessageHandlerShould).Assembly.GetName().Version; + + var oktaHandler = new OktaHttpMessageHandler(testFrameworkName, version, new OktaMvcOptions { - var testProxyBaseUri = "http://test.cxm/"; - var testPort = 8080; - var expectedProxyAddress = "http://test.cxm:8080/"; - var testFrameworkName = $"{nameof(SetInnerHandlerWebProxyPort)}_testFrameworkName"; - var version = typeof(OktaHttpMessageHandlerShould).Assembly.GetName().Version; - var oktaHandler = new OktaHttpMessageHandler(testFrameworkName, version, new OktaMvcOptions + BackchannelHttpClientHandler = new MockHttpClientHandler(), + Proxy = new ProxyConfiguration { - Proxy = new ProxyConfiguration - { - Host = testProxyBaseUri, - Port = testPort, - }, - }); - oktaHandler.InnerHandler.Should().NotBeNull(); - oktaHandler.InnerHandler.Should().BeAssignableTo(); - - var httpClientHandler = (HttpClientHandler)oktaHandler.InnerHandler; - httpClientHandler.Proxy.Should().NotBeNull(); - httpClientHandler.Proxy.Should().BeAssignableTo(); - - var webProxy = (DefaultProxy)httpClientHandler.Proxy; - var proxyUri = webProxy.GetProxy(Arg.Any()); - proxyUri.ToString().Should().Be(expectedProxyAddress); - proxyUri.Port.Should().Be(testPort); - webProxy.Credentials.Should().BeNull(); + Host = testProxyAddress, + }, }); + + oktaHandler.InnerHandler.Should().NotBeNull(); + oktaHandler.InnerHandler.Should().BeAssignableTo(); } [Fact] - public async Task SetInnerHandlerWebProxyCredentials() + public void NotSetProxyWhenCustomInnerHandlerIsProvided() { - await Task.Run(() => + var testProxyAddress = "http://test.cxm/"; + var testFrameworkName = $"{nameof(SetInnerHandlerWebProxy)}_testFrameworkName"; + var version = typeof(OktaHttpMessageHandlerShould).Assembly.GetName().Version; + + var oktaHandler = new OktaHttpMessageHandler(testFrameworkName, version, new OktaMvcOptions { - var testProxyAddress = "http://test.cxm/"; - var testUserName = "testUserName"; - var testPassword = "testPassword"; - var testFrameworkName = $"{nameof(SetInnerHandlerWebProxyCredentials)}_testFrameworkName"; - var version = typeof(OktaHttpMessageHandlerShould).Assembly.GetName().Version; - var oktaHandler = new OktaHttpMessageHandler(testFrameworkName, version, new OktaMvcOptions + BackchannelHttpClientHandler = new MockHttpClientHandler(), + Proxy = new ProxyConfiguration { - Proxy = new ProxyConfiguration - { - Host = testProxyAddress, - Username = testUserName, - Password = testPassword, - }, - }); - oktaHandler.InnerHandler.Should().NotBeNull(); - oktaHandler.InnerHandler.Should().BeAssignableTo(); - - var httpClientHandler = (HttpClientHandler)oktaHandler.InnerHandler; - httpClientHandler.Proxy.Should().NotBeNull(); - httpClientHandler.Proxy.Should().BeAssignableTo(); - - var webProxy = (DefaultProxy)httpClientHandler.Proxy; - webProxy.GetProxy(Arg.Any()).ToString().Should().Be(testProxyAddress); - webProxy.Credentials.Should().NotBeNull(); - webProxy.Credentials.GetCredential(Arg.Any(), string.Empty).UserName.Should().Be(testUserName); - webProxy.Credentials.GetCredential(Arg.Any(), string.Empty).Password.Should().Be(testPassword); + Host = testProxyAddress, + }, }); + + oktaHandler.InnerHandler.Should().NotBeNull(); + oktaHandler.InnerHandler.Should().BeAssignableTo(); + var handler = oktaHandler.InnerHandler as HttpClientHandler; + handler?.Proxy?.Should().BeNull(); } [Fact] - public async Task NotSetInnerHandlerWebProxyIfNotSpecified() + public void SetInnerHandlerWebProxyPort() { - await Task.Run(() => + var testProxyBaseUri = "http://test.cxm/"; + var testPort = 8080; + var expectedProxyAddress = "http://test.cxm:8080/"; + var testFrameworkName = $"{nameof(SetInnerHandlerWebProxyPort)}_testFrameworkName"; + var version = typeof(OktaHttpMessageHandlerShould).Assembly.GetName().Version; + var oktaHandler = new OktaHttpMessageHandler(testFrameworkName, version, new OktaMvcOptions { - var testFrameworkName = $"{nameof(NotSetInnerHandlerWebProxyIfNotSpecified)}_testFrameworkName"; - var version = typeof(OktaHttpMessageHandlerShould).Assembly.GetName().Version; - var oktaHandler = new OktaHttpMessageHandler(testFrameworkName, version, new OktaMvcOptions()); - oktaHandler.InnerHandler.Should().NotBeNull(); - oktaHandler.InnerHandler.Should().BeAssignableTo(); - - var httpClientHandler = (HttpClientHandler)oktaHandler.InnerHandler; - httpClientHandler.Proxy.Should().BeNull(); + Proxy = new ProxyConfiguration + { + Host = testProxyBaseUri, + Port = testPort, + }, }); + oktaHandler.InnerHandler.Should().NotBeNull(); + oktaHandler.InnerHandler.Should().BeAssignableTo(); + + var httpClientHandler = (HttpClientHandler)oktaHandler.InnerHandler; + httpClientHandler.Proxy.Should().NotBeNull(); + httpClientHandler.Proxy.Should().BeAssignableTo(); + + var webProxy = (DefaultProxy)httpClientHandler.Proxy; + var proxyUri = webProxy.GetProxy(Arg.Any()); + proxyUri.ToString().Should().Be(expectedProxyAddress); + proxyUri.Port.Should().Be(testPort); + webProxy.Credentials.Should().BeNull(); + } + + [Fact] + public void SetInnerHandlerWebProxyCredentials() + { + var testProxyAddress = "http://test.cxm/"; + var testUserName = "testUserName"; + var testPassword = "testPassword"; + var testFrameworkName = $"{nameof(SetInnerHandlerWebProxyCredentials)}_testFrameworkName"; + var version = typeof(OktaHttpMessageHandlerShould).Assembly.GetName().Version; + var oktaHandler = new OktaHttpMessageHandler(testFrameworkName, version, new OktaMvcOptions + { + Proxy = new ProxyConfiguration + { + Host = testProxyAddress, + Username = testUserName, + Password = testPassword, + }, + }); + oktaHandler.InnerHandler.Should().NotBeNull(); + oktaHandler.InnerHandler.Should().BeAssignableTo(); + + var httpClientHandler = (HttpClientHandler)oktaHandler.InnerHandler; + httpClientHandler.Proxy.Should().NotBeNull(); + httpClientHandler.Proxy.Should().BeAssignableTo(); + + var webProxy = (DefaultProxy)httpClientHandler.Proxy; + webProxy.GetProxy(Arg.Any()).ToString().Should().Be(testProxyAddress); + webProxy.Credentials.Should().NotBeNull(); + webProxy.Credentials.GetCredential(Arg.Any(), string.Empty).UserName.Should().Be(testUserName); + webProxy.Credentials.GetCredential(Arg.Any(), string.Empty).Password.Should().Be(testPassword); + } + + [Fact] + public void NotSetWebProxyIfNotSpecified() + { + var testFrameworkName = $"{nameof(NotSetWebProxyIfNotSpecified)}_testFrameworkName"; + var version = typeof(OktaHttpMessageHandlerShould).Assembly.GetName().Version; + var oktaHandler = new OktaHttpMessageHandler(testFrameworkName, version, new OktaMvcOptions()); + oktaHandler.InnerHandler.Should().NotBeNull(); + oktaHandler.InnerHandler.Should().BeAssignableTo(); + + var httpClientHandler = (HttpClientHandler)oktaHandler.InnerHandler; + httpClientHandler.Proxy.Should().BeNull(); } } } diff --git a/Okta.AspNet.Test/OpenIdConnectAuthenticationOptionsBuilderShould.cs b/Okta.AspNet.Test/OpenIdConnectAuthenticationOptionsBuilderShould.cs index b7cbfb8..7f30e7d 100644 --- a/Okta.AspNet.Test/OpenIdConnectAuthenticationOptionsBuilderShould.cs +++ b/Okta.AspNet.Test/OpenIdConnectAuthenticationOptionsBuilderShould.cs @@ -34,8 +34,13 @@ public void BuildOpenIdConnectAuthenticationOptionsCorrectly() ClientSecret = "bar", RedirectUri = "/redirectUri", Scope = new List { "openid", "profile", "email" }, - SecurityTokenValidated = mockTokenEvent, + OpenIdConnectEvents = new OpenIdConnectAuthenticationNotifications + { + SecurityTokenValidated = mockTokenEvent, + }, GetClaimsFromUserInfoEndpoint = false, + BackchannelTimeout = TimeSpan.MaxValue, + BackchannelHttpClientHandler = new MockHttpClientHandler(), }; var oidcOptions = new OpenIdConnectAuthenticationOptionsBuilder(oktaMvcOptions).BuildOpenIdConnectAuthenticationOptions(); @@ -44,6 +49,8 @@ public void BuildOpenIdConnectAuthenticationOptionsCorrectly() oidcOptions.ClientSecret.Should().Be(oktaMvcOptions.ClientSecret); oidcOptions.PostLogoutRedirectUri.Should().Be(oktaMvcOptions.PostLogoutRedirectUri); oidcOptions.AuthenticationMode.Should().Be(AuthenticationMode.Active); + oidcOptions.BackchannelTimeout.Should().Be(TimeSpan.MaxValue); + oidcOptions.BackchannelHttpHandler.GetType().Should().Be(typeof(MockHttpClientHandler)); var issuer = UrlHelper.CreateIssuerUrl(oktaMvcOptions.OktaDomain, oktaMvcOptions.AuthorizationServerId); oidcOptions.Authority.Should().Be(issuer); diff --git a/Okta.AspNet.WebApi.IntegrationTest/MiddlewareShould.cs b/Okta.AspNet.WebApi.IntegrationTest/MiddlewareShould.cs index 7ffd0da..8512394 100644 --- a/Okta.AspNet.WebApi.IntegrationTest/MiddlewareShould.cs +++ b/Okta.AspNet.WebApi.IntegrationTest/MiddlewareShould.cs @@ -23,14 +23,18 @@ public class MiddlewareShould : IDisposable private string ProtectedEndpoint { get; set; } + private MockHttpMessageHandler MockHttpHandler { get; set; } + public MiddlewareShould() { BaseUrl = "http://localhost:8080"; ProtectedEndpoint = string.Format("{0}/api/messages", BaseUrl); + MockHttpHandler = new MockHttpMessageHandler(); _server = TestServer.Create(app => { var startup = new Startup(); + startup.HttpMessageHandler = MockHttpHandler; startup.Configuration(app); HttpConfiguration config = new HttpConfiguration(); @@ -66,6 +70,34 @@ public async Task Returns401WhenAccessToProtectedRouteWithInvalidTokenAsync() } } + [Fact] + public async Task InvokeCustomEventsAsync() + { + var accessToken = "thisIsAnInvalidToken"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, ProtectedEndpoint); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + using (var client = new HttpClient(_server.Handler)) + { + var response = await client.SendAsync(request); + Assert.True(response.Headers.Contains("myCustomHeader")); + } + } + + [Fact] + public async Task InvokeCustomHandlerAsync() + { + var accessToken = "thisIsAnInvalidToken"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, ProtectedEndpoint); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + using (var client = new HttpClient(_server.Handler)) + { + var response = await client.SendAsync(request); + Assert.True(MockHttpHandler.NumberOfCalls > 0); + } + } + public void Dispose() { _server.Dispose(); diff --git a/Okta.AspNet.WebApi.IntegrationTest/MockHttpMessageHandler.cs b/Okta.AspNet.WebApi.IntegrationTest/MockHttpMessageHandler.cs new file mode 100644 index 0000000..4b4cafb --- /dev/null +++ b/Okta.AspNet.WebApi.IntegrationTest/MockHttpMessageHandler.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) 2018-present Okta, Inc. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. +// + +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Okta.AspNet.WebApi.IntegrationTest +{ + public class MockHttpMessageHandler : DelegatingHandler + { + public int NumberOfCalls { get; private set; } + + public MockHttpMessageHandler() + { + NumberOfCalls = 0; + } + + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + NumberOfCalls++; + // base.SendAsync calls the inner handler + return await base.SendAsync(request, cancellationToken); + } + } +} diff --git a/Okta.AspNet.WebApi.IntegrationTest/Okta.AspNet.WebApi.IntegrationTest.csproj b/Okta.AspNet.WebApi.IntegrationTest/Okta.AspNet.WebApi.IntegrationTest.csproj index 212dfe8..78d09cb 100644 --- a/Okta.AspNet.WebApi.IntegrationTest/Okta.AspNet.WebApi.IntegrationTest.csproj +++ b/Okta.AspNet.WebApi.IntegrationTest/Okta.AspNet.WebApi.IntegrationTest.csproj @@ -50,14 +50,21 @@ ..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.3.6.0\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll - - ..\packages\Microsoft.Owin.4.1.1\lib\net45\Microsoft.Owin.dll + + ..\packages\Microsoft.Owin.4.2.0\lib\net45\Microsoft.Owin.dll - - ..\packages\Microsoft.Owin.Hosting.4.1.1\lib\net45\Microsoft.Owin.Hosting.dll + + ..\packages\Microsoft.Owin.Hosting.4.2.0\lib\net45\Microsoft.Owin.Hosting.dll - - ..\packages\Microsoft.Owin.Testing.4.1.1\lib\net45\Microsoft.Owin.Testing.dll + + ..\packages\Microsoft.Owin.Security.4.2.0\lib\net45\Microsoft.Owin.Security.dll + + + False + ..\..\..\Users\Laura Rodriguez\.nuget\packages\microsoft.owin.security.oauth\4.1.1\lib\net45\Microsoft.Owin.Security.OAuth.dll + + + ..\packages\Microsoft.Owin.Testing.4.2.0\lib\net45\Microsoft.Owin.Testing.dll ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll @@ -134,6 +141,7 @@ Global.asax + True diff --git a/Okta.AspNet.WebApi.IntegrationTest/Startup.cs b/Okta.AspNet.WebApi.IntegrationTest/Startup.cs index 7b5d240..2aa1fe1 100644 --- a/Okta.AspNet.WebApi.IntegrationTest/Startup.cs +++ b/Okta.AspNet.WebApi.IntegrationTest/Startup.cs @@ -4,7 +4,12 @@ // using System; +using System.Net.Http; +using System.Threading.Tasks; +using System.Web; using Microsoft.Owin; +using Microsoft.Owin.Security.OAuth; +using Microsoft.Owin.Security.Provider; using Owin; [assembly: OwinStartup(typeof(Okta.AspNet.WebApi.IntegrationTest.Startup))] @@ -13,13 +18,24 @@ namespace Okta.AspNet.WebApi.IntegrationTest { public class Startup { + public HttpMessageHandler HttpMessageHandler { get; set; } + public void Configuration(IAppBuilder app) { var oktaDomain = Environment.GetEnvironmentVariable("okta:OktaDomain"); - + var jwtProvider = new OAuthBearerAuthenticationProvider + { + OnApplyChallenge = context => + { + context.OwinContext.Response.Headers.Add("myCustomHeader", new[] { "myCustomValue" }); + return Task.CompletedTask; + }, + }; app.UseOktaWebApi(new OktaWebApiOptions() { OktaDomain = oktaDomain, + OAuthBearerAuthenticationProvider = jwtProvider, + BackchannelHttpClientHandler = HttpMessageHandler, }); } } diff --git a/Okta.AspNet.WebApi.IntegrationTest/Web.config b/Okta.AspNet.WebApi.IntegrationTest/Web.config index 6dec1a7..f37f169 100644 --- a/Okta.AspNet.WebApi.IntegrationTest/Web.config +++ b/Okta.AspNet.WebApi.IntegrationTest/Web.config @@ -34,7 +34,7 @@ - + @@ -44,6 +44,10 @@ + + + + diff --git a/Okta.AspNet.WebApi.IntegrationTest/packages.config b/Okta.AspNet.WebApi.IntegrationTest/packages.config index 0198899..eab3685 100644 --- a/Okta.AspNet.WebApi.IntegrationTest/packages.config +++ b/Okta.AspNet.WebApi.IntegrationTest/packages.config @@ -8,9 +8,10 @@ - - - + + + + diff --git a/Okta.AspNet/CHANGELOG.md b/Okta.AspNet/CHANGELOG.md index 9b53972..a6f7d8a 100644 --- a/Okta.AspNet/CHANGELOG.md +++ b/Okta.AspNet/CHANGELOG.md @@ -1,6 +1,20 @@ # Changelog Running changelog of releases since `1.6.0` +## v2.0.0 + +### Features + +- Add support for OIDC events configuration in MVC projects. +- Add support for JWT events configuration in Web API projects. +- Add support for BackchannelHttpHandler configuration. +- Add support for BackchannelTimeout configuration. + +### Breaking changes + +- Remove `SecurityTokenValidated` and `AuthenticationFailed` events in favor of `OpenIdConnectEvents` (MVC). +- Remove `ClientId` property from `WebApiOptions`. + ## v1.8.2 ### Bug Fix diff --git a/Okta.AspNet/HttpClientBuilder.cs b/Okta.AspNet/HttpClientBuilder.cs new file mode 100644 index 0000000..d4ccc38 --- /dev/null +++ b/Okta.AspNet/HttpClientBuilder.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) 2018-present Okta, Inc. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. +// + +using System.Net.Http; +using Okta.AspNet.Abstractions; + +namespace Okta.AspNet +{ + public class HttpClientBuilder + { + public static HttpClient CreateClient(OktaWebOptions options) + { + var httpClient = new HttpClient(new OktaHttpMessageHandler("okta-aspnet", typeof(OktaMiddlewareExtensions).Assembly.GetName().Version, options)); + + httpClient.Timeout = options.BackchannelTimeout; + httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB + + return httpClient; + } + } +} diff --git a/Okta.AspNet/JwtOptionsBuilder.cs b/Okta.AspNet/JwtOptionsBuilder.cs new file mode 100644 index 0000000..62f860f --- /dev/null +++ b/Okta.AspNet/JwtOptionsBuilder.cs @@ -0,0 +1,61 @@ +// +// Copyright (c) 2018-present Okta, Inc. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Jwt; +using Microsoft.Owin.Security.OAuth; +using Okta.AspNet.Abstractions; + +namespace Okta.AspNet +{ + public class JwtOptionsBuilder + { + /// + /// Builds the JwtBearerAuthenticationOptions object used during the authentication process. + /// + /// The Okta options. + /// An instance of JwtBearerAuthenticationOptions. + public static JwtBearerAuthenticationOptions BuildJwtBearerAuthenticationOptions(OktaWebApiOptions options) + { + var issuer = UrlHelper.CreateIssuerUrl(options.OktaDomain, options.AuthorizationServerId); + + var configurationManager = new ConfigurationManager( + issuer + "/.well-known/openid-configuration", + new OpenIdConnectConfigurationRetriever(), + new HttpDocumentRetriever(HttpClientBuilder.CreateClient(options))); + + // Stop the default behavior of remapping JWT claim names to legacy MS/SOAP claim names + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + + var signingKeyCachingProvider = new DiscoveryDocumentCachingSigningKeyProvider(new DiscoveryDocumentSigningKeyProvider(configurationManager)); + + var tokenValidationParameters = new DefaultTokenValidationParameters(options, issuer) + { + ValidAudience = options.Audience, + IssuerSigningKeyResolver = (token, securityToken, keyId, validationParameters) => + { + return signingKeyCachingProvider.SigningKeys.Where(x => x.KeyId == keyId); + }, + }; + + return new JwtBearerAuthenticationOptions + { + AuthenticationMode = AuthenticationMode.Active, + TokenValidationParameters = tokenValidationParameters, + TokenHandler = new StrictTokenHandler(), + Provider = options.OAuthBearerAuthenticationProvider ?? new OAuthBearerAuthenticationProvider(), + }; + } + } +} diff --git a/Okta.AspNet/Okta.AspNet.csproj b/Okta.AspNet/Okta.AspNet.csproj index 5c2c676..85ff1eb 100644 --- a/Okta.AspNet/Okta.AspNet.csproj +++ b/Okta.AspNet/Okta.AspNet.csproj @@ -3,7 +3,7 @@ Official Okta middleware for ASP.NET 4.5+. Easily add authentication and authorization to ASP.NET applications. (c) 2019 Okta, Inc. - 1.8.2 + 2.0.0 Okta, Inc. net452 Okta.AspNet diff --git a/Okta.AspNet/OktaMiddlewareExtensions.cs b/Okta.AspNet/OktaMiddlewareExtensions.cs index 242b5d6..c50ad84 100644 --- a/Okta.AspNet/OktaMiddlewareExtensions.cs +++ b/Okta.AspNet/OktaMiddlewareExtensions.cs @@ -13,6 +13,7 @@ using Microsoft.Owin.Security; using Microsoft.Owin.Security.Jwt; using Microsoft.Owin.Security.Notifications; +using Microsoft.Owin.Security.OAuth; using Microsoft.Owin.Security.OpenIdConnect; using Okta.AspNet.Abstractions; using Owin; @@ -42,43 +43,11 @@ public static IAppBuilder UseOktaWebApi(this IAppBuilder app, OktaWebApiOptions } new OktaWebApiOptionsValidator().Validate(options); - AddJwtBearerAuthentication(app, options); + app.UseJwtBearerAuthentication(JwtOptionsBuilder.BuildJwtBearerAuthenticationOptions(options)); return app; } - private static void AddJwtBearerAuthentication(IAppBuilder app, OktaWebApiOptions options) - { - var issuer = UrlHelper.CreateIssuerUrl(options.OktaDomain, options.AuthorizationServerId); - var httpClient = new HttpClient(new OktaHttpMessageHandler("okta-aspnet", typeof(OktaMiddlewareExtensions).Assembly.GetName().Version, options)); - - var configurationManager = new ConfigurationManager( - issuer + "/.well-known/openid-configuration", - new OpenIdConnectConfigurationRetriever(), - new HttpDocumentRetriever(httpClient)); - - // Stop the default behavior of remapping JWT claim names to legacy MS/SOAP claim names - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); - - var signingKeyCachingProvider = new DiscoveryDocumentCachingSigningKeyProvider(new DiscoveryDocumentSigningKeyProvider(configurationManager)); - - var tokenValidationParameters = new DefaultTokenValidationParameters(options, issuer) - { - ValidAudience = options.Audience, - IssuerSigningKeyResolver = (token, securityToken, keyId, validationParameters) => - { - return signingKeyCachingProvider.SigningKeys.Where(x => x.KeyId == keyId); - }, - }; - - app.UseJwtBearerAuthentication(new JwtBearerAuthenticationOptions - { - AuthenticationMode = AuthenticationMode.Active, - TokenValidationParameters = tokenValidationParameters, - TokenHandler = new StrictTokenHandler(), - }); - } - private static void AddOpenIdConnectAuthentication(IAppBuilder app, OktaMvcOptions options) { // Stop the default behavior of remapping JWT claim names to legacy MS/SOAP claim names diff --git a/Okta.AspNet/OktaMvcOptions.cs b/Okta.AspNet/OktaMvcOptions.cs index 5d1d743..4405e19 100644 --- a/Okta.AspNet/OktaMvcOptions.cs +++ b/Okta.AspNet/OktaMvcOptions.cs @@ -57,15 +57,9 @@ public class OktaMvcOptions : Abstractions.OktaWebOptions public bool GetClaimsFromUserInfoEndpoint { get; set; } = true; /// - /// Gets or sets the event invoked after the security token has passed validation and a ClaimsIdentity has been generated. + /// Gets or sets the OIDC events which the underlying OpenIdConnectAuthenticationMiddleware invokes to enable developer control over the authentication process. /// - /// The SecurityTokenValidated event. - public Func, Task> SecurityTokenValidated { get; set; } = notification => Task.FromResult(0); - - /// - /// Gets or sets the event invoked if exceptions are thrown during request processing. - /// - /// The AuthenticationFailed event. - public Func, Task> AuthenticationFailed { get; set; } = notification => Task.FromResult(0); + /// + public OpenIdConnectAuthenticationNotifications OpenIdConnectEvents { get; set; } } } diff --git a/Okta.AspNet/OktaWebApiOptions.cs b/Okta.AspNet/OktaWebApiOptions.cs index 9a2b1d7..c509cee 100644 --- a/Okta.AspNet/OktaWebApiOptions.cs +++ b/Okta.AspNet/OktaWebApiOptions.cs @@ -3,9 +3,20 @@ // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. // +using Microsoft.Owin.Security.OAuth; +using Owin; + namespace Okta.AspNet { public sealed class OktaWebApiOptions : Abstractions.OktaWebApiOptions { + /// + /// Gets or sets the authentication provider which specifies callback methods invoked by the underlying authentication middleware to enable developer control over the authentication process. + /// + /// + /// + /// The authentication provider which specifies callback methods invoked by the underlying authentication middleware to enable developer control over the authentication process. + /// + public IOAuthBearerAuthenticationProvider OAuthBearerAuthenticationProvider { get; set; } } } diff --git a/Okta.AspNet/OpenIdConnectAuthenticationOptionsBuilder.cs b/Okta.AspNet/OpenIdConnectAuthenticationOptionsBuilder.cs index ec27d31..a5149aa 100644 --- a/Okta.AspNet/OpenIdConnectAuthenticationOptionsBuilder.cs +++ b/Okta.AspNet/OpenIdConnectAuthenticationOptionsBuilder.cs @@ -3,6 +3,7 @@ // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. // +using System; using System.Linq; using System.Net.Http; using System.Security.Claims; @@ -54,6 +55,17 @@ public OpenIdConnectAuthenticationOptions BuildOpenIdConnectAuthenticationOption var definedScopes = _oktaMvcOptions.Scope?.ToArray() ?? OktaDefaults.Scope; var scopeString = string.Join(" ", definedScopes); + var redirectEvent = _oktaMvcOptions.OpenIdConnectEvents?.RedirectToIdentityProvider; + var tokenEvent = _oktaMvcOptions.OpenIdConnectEvents?.SecurityTokenValidated; + _oktaMvcOptions.OpenIdConnectEvents = + _oktaMvcOptions.OpenIdConnectEvents ?? new OpenIdConnectAuthenticationNotifications(); + + _oktaMvcOptions.OpenIdConnectEvents.RedirectToIdentityProvider = context => + BeforeRedirectToIdentityProviderAsync(context, redirectEvent); + + _oktaMvcOptions.OpenIdConnectEvents.SecurityTokenValidated = + context => SecurityTokenValidatedAsync(context, tokenEvent); + var oidcOptions = new OpenIdConnectAuthenticationOptions { ClientId = _oktaMvcOptions.ClientId, @@ -68,18 +80,18 @@ public OpenIdConnectAuthenticationOptions BuildOpenIdConnectAuthenticationOption SecurityTokenValidator = new StrictSecurityTokenValidator(), AuthenticationMode = (_oktaMvcOptions.LoginMode == LoginMode.SelfHosted) ? AuthenticationMode.Passive : AuthenticationMode.Active, SaveTokens = true, - Notifications = new OpenIdConnectAuthenticationNotifications - { - RedirectToIdentityProvider = BeforeRedirectToIdentityProviderAsync, - SecurityTokenValidated = SecurityTokenValidatedAsync, - AuthenticationFailed = _oktaMvcOptions.AuthenticationFailed, - }, + Notifications = _oktaMvcOptions.OpenIdConnectEvents, + BackchannelHttpHandler = _oktaMvcOptions.BackchannelHttpClientHandler, + BackchannelTimeout = _oktaMvcOptions.BackchannelTimeout, }; return oidcOptions; } - private Task BeforeRedirectToIdentityProviderAsync(RedirectToIdentityProviderNotification redirectToIdentityProviderNotification) + private Task BeforeRedirectToIdentityProviderAsync( + RedirectToIdentityProviderNotification + redirectToIdentityProviderNotification, + Func, Task> redirectEvent) { // If signing out, add the id_token_hint if (redirectToIdentityProviderNotification.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout) @@ -106,10 +118,15 @@ private Task BeforeRedirectToIdentityProviderAsync(RedirectToIdentityProviderNot } } + if (redirectEvent != null) + { + return redirectEvent(redirectToIdentityProviderNotification); + } + return Task.FromResult(false); } - private async Task SecurityTokenValidatedAsync(SecurityTokenValidatedNotification context) + private async Task SecurityTokenValidatedAsync(SecurityTokenValidatedNotification context, Func, Task> tokenEvent) { context.AuthenticationTicket.Identity.AddClaim(new Claim("id_token", context.ProtocolMessage.IdToken)); context.AuthenticationTicket.Identity.AddClaim(new Claim("access_token", context.ProtocolMessage.AccessToken)); @@ -126,7 +143,10 @@ private async Task SecurityTokenValidatedAsync(SecurityTokenValidatedNotificatio await _userInformationProvider.EnrichIdentityViaUserInfoAsync(context.AuthenticationTicket.Identity, context.ProtocolMessage.AccessToken).ConfigureAwait(false); } - await _oktaMvcOptions.SecurityTokenValidated(context).ConfigureAwait(false); + if (tokenEvent != null) + { + await tokenEvent(context).ConfigureAwait(false); + } } /// diff --git a/Okta.AspNetCore.Mvc.IntegrationTest/OktaMiddlewareShould.cs b/Okta.AspNetCore.Mvc.IntegrationTest/OktaMiddlewareShould.cs index db829be..a0f0653 100644 --- a/Okta.AspNetCore.Mvc.IntegrationTest/OktaMiddlewareShould.cs +++ b/Okta.AspNetCore.Mvc.IntegrationTest/OktaMiddlewareShould.cs @@ -67,6 +67,19 @@ public async Task IncludeLoginHintInAuthorizeUrlAsync() } } + [Fact] + public async Task CallCustomRedirectEventAfterInternalEventAsync() + { + var loginWithIdpEndpoint = string.Format("{0}/Account/LoginWithIdp", BaseUrl); + using (var client = _server.CreateClient()) + { + var response = await client.GetAsync(loginWithIdpEndpoint); + Assert.True(response.StatusCode == System.Net.HttpStatusCode.Found); + Assert.Contains("idp=foo", response.Headers.Location.AbsoluteUri); + Assert.Contains("myCustomParamKey=myCustomParamValue", response.Headers.Location.AbsoluteUri); + } + } + public void Dispose() { _server.Dispose(); diff --git a/Okta.AspNetCore.Mvc.IntegrationTest/Startup.cs b/Okta.AspNetCore.Mvc.IntegrationTest/Startup.cs index 0b39af9..4c2c6a8 100644 --- a/Okta.AspNetCore.Mvc.IntegrationTest/Startup.cs +++ b/Okta.AspNetCore.Mvc.IntegrationTest/Startup.cs @@ -1,3 +1,5 @@ +using System; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Builder; @@ -23,6 +25,17 @@ public Startup() // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { + Func myRedirectEvent = context => + { + context.ProtocolMessage.SetParameter("myCustomParamKey", "myCustomParamValue"); + return Task.CompletedTask; + }; + + var events = new OpenIdConnectEvents + { + OnRedirectToIdentityProvider = myRedirectEvent, + }; + services.AddAuthentication(options => { options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; @@ -35,9 +48,13 @@ public void ConfigureServices(IServiceCollection services) ClientId = Configuration["Okta:ClientId"], ClientSecret = Configuration["Okta:ClientSecret"], OktaDomain = Configuration["Okta:OktaDomain"], + OpenIdConnectEvents = events, + }); services.AddAuthorization(); services.AddControllers(); + + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/Okta.AspNetCore.Test/Okta.AspNetCore.Test.csproj b/Okta.AspNetCore.Test/Okta.AspNetCore.Test.csproj index 6d2e450..e4d33d8 100644 --- a/Okta.AspNetCore.Test/Okta.AspNetCore.Test.csproj +++ b/Okta.AspNetCore.Test/Okta.AspNetCore.Test.csproj @@ -7,12 +7,18 @@ - - - - - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Okta.AspNetCore.Test/OpenIdConnectOptionsHelperShould.cs b/Okta.AspNetCore.Test/OpenIdConnectOptionsHelperShould.cs index cdbc21a..957f284 100644 --- a/Okta.AspNetCore.Test/OpenIdConnectOptionsHelperShould.cs +++ b/Okta.AspNetCore.Test/OpenIdConnectOptionsHelperShould.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using Microsoft.AspNetCore.Authentication; @@ -15,6 +16,11 @@ using Xunit; using Microsoft.AspNetCore.Authentication.OAuth.Claims; using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using JwtAuthenticationFailedContext = Microsoft.AspNetCore.Authentication.JwtBearer.AuthenticationFailedContext; +using OpenIdConnectTokenValidatedContext = Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenValidatedContext; +using OpenIdConnectAuthenticationFailedContext = Microsoft.AspNetCore.Authentication.OpenIdConnect.AuthenticationFailedContext; +using JwtTokenValidatedContext = Microsoft.AspNetCore.Authentication.JwtBearer.TokenValidatedContext; namespace Okta.AspNetCore.Test { @@ -23,12 +29,14 @@ public class OpenIdConnectOptionsHelperShould [Theory] [InlineData(true)] [InlineData(false)] - public void SetOpenIdConnectsOptionsCorrectly(bool getClaimsFromUserInfoEndpoint) + public void SetOpenIdConnectsOptions(bool getClaimsFromUserInfoEndpoint) { - var mockTokenValidatedEvent = Substitute.For>(); + var mockTokenValidatedEvent = Substitute.For>(); var mockUserInfoReceivedEvent = Substitute.For>(); var mockOktaExceptionEvent = Substitute.For>(); - var mockAuthenticationFailedEvent = Substitute.For>(); + var mockAuthenticationFailedEvent = Substitute.For>(); + var mockRedirectToIdentityProvider = Substitute.For>(); + var mockHttpHandler = Substitute.For(); var oktaMvcOptions = new OktaMvcOptions { @@ -40,23 +48,29 @@ public void SetOpenIdConnectsOptionsCorrectly(bool getClaimsFromUserInfoEndpoint GetClaimsFromUserInfoEndpoint = getClaimsFromUserInfoEndpoint, CallbackPath = "/somecallbackpath", Scope = new List { "openid", "profile", "email" }, - OnTokenValidated = mockTokenValidatedEvent, - OnUserInformationReceived = mockUserInfoReceivedEvent, - OnAuthenticationFailed = mockAuthenticationFailedEvent, - OnOktaApiFailure = mockOktaExceptionEvent, + BackchannelTimeout = TimeSpan.FromMinutes(5), + BackchannelHttpClientHandler = mockHttpHandler, + OpenIdConnectEvents = new OpenIdConnectEvents + { + OnTokenValidated = mockTokenValidatedEvent, + OnUserInformationReceived = mockUserInfoReceivedEvent, + OnAuthenticationFailed = mockAuthenticationFailedEvent, + OnRemoteFailure = mockOktaExceptionEvent, + OnRedirectToIdentityProvider = mockRedirectToIdentityProvider + } }; - var events = new OpenIdConnectEvents() { OnRedirectToIdentityProvider = null }; - var oidcOptions = new OpenIdConnectOptions(); - OpenIdConnectOptionsHelper.ConfigureOpenIdConnectOptions(oktaMvcOptions, events, oidcOptions); + OpenIdConnectOptionsHelper.ConfigureOpenIdConnectOptions(oktaMvcOptions, oidcOptions); oidcOptions.ClientId.Should().Be(oktaMvcOptions.ClientId); oidcOptions.ClientSecret.Should().Be(oktaMvcOptions.ClientSecret); oidcOptions.SignedOutRedirectUri.Should().Be(oktaMvcOptions.PostLogoutRedirectUri); oidcOptions.GetClaimsFromUserInfoEndpoint.Should().Be(oktaMvcOptions.GetClaimsFromUserInfoEndpoint); oidcOptions.CallbackPath.Value.Should().Be(oktaMvcOptions.CallbackPath); + oidcOptions.BackchannelTimeout.Should().Be(TimeSpan.FromMinutes(5)); + ((DelegatingHandler)oidcOptions.BackchannelHttpHandler).InnerHandler.Should().Be(mockHttpHandler); var jsonClaims = oidcOptions .ClaimActions.Where(ca => ca is JsonKeyClaimAction) @@ -72,8 +86,7 @@ public void SetOpenIdConnectsOptionsCorrectly(bool getClaimsFromUserInfoEndpoint oidcOptions.Scope.ToList().Should().BeEquivalentTo(oktaMvcOptions.Scope); oidcOptions.CallbackPath.Value.Should().Be(oktaMvcOptions.CallbackPath); - oidcOptions.Events.OnRedirectToIdentityProvider.Should().BeNull(); - + // Check the event was call once with a null parameter oidcOptions.Events.OnTokenValidated(null); mockTokenValidatedEvent.Received(1).Invoke(null); @@ -81,6 +94,8 @@ public void SetOpenIdConnectsOptionsCorrectly(bool getClaimsFromUserInfoEndpoint mockAuthenticationFailedEvent.Received(1).Invoke(null); oidcOptions.Events.OnRemoteFailure(null); mockOktaExceptionEvent.Received(1).Invoke(null); + oidcOptions.Events.OnRedirectToIdentityProvider(null); + mockRedirectToIdentityProvider.Received(1).Invoke(null); // UserInfo event is mapped only when GetClaimsFromUserInfoEndpoint = true if (oidcOptions.GetClaimsFromUserInfoEndpoint) @@ -90,5 +105,41 @@ public void SetOpenIdConnectsOptionsCorrectly(bool getClaimsFromUserInfoEndpoint mockUserInfoReceivedEvent.Received(1).Invoke(null); } } + + [Fact] + public void SetJwtBearerOptions() + { + var mockTokenValidatedEvent = Substitute.For>(); + var mockAuthenticationFailedEvent = Substitute.For>(); + var mockHttpHandler = Substitute.For(); + + var oktaWebApiOptions = new OktaWebApiOptions + { + AuthorizationServerId = "bar", + OktaDomain = "http://myoktadomain.com", + Audience = "foo", + BackchannelHttpClientHandler = mockHttpHandler, + BackchannelTimeout = TimeSpan.FromMinutes(5), + JwtBearerEvents = new JwtBearerEvents() + { + OnTokenValidated = mockTokenValidatedEvent, + OnAuthenticationFailed = mockAuthenticationFailedEvent, + } + }; + + var jwtBearerOptions = new JwtBearerOptions(); + + OpenIdConnectOptionsHelper.ConfigureJwtBearerOptions(oktaWebApiOptions, jwtBearerOptions); + var issuer = UrlHelper.CreateIssuerUrl(oktaWebApiOptions.OktaDomain, oktaWebApiOptions.AuthorizationServerId); + jwtBearerOptions.Authority.Should().Be(issuer); + jwtBearerOptions.Audience.Should().Be(oktaWebApiOptions.Audience); + jwtBearerOptions.BackchannelTimeout.Should().Be(TimeSpan.FromMinutes(5)); + ((DelegatingHandler)jwtBearerOptions.BackchannelHttpHandler).InnerHandler.Should().Be(mockHttpHandler); + + jwtBearerOptions.Events.OnTokenValidated(null); + mockTokenValidatedEvent.Received().Invoke(null); + jwtBearerOptions.Events.OnAuthenticationFailed(null); + mockAuthenticationFailedEvent.Received().Invoke(null); + } } } diff --git a/Okta.AspNetCore.WebApi.IntegrationTest/OktaMiddlewareShould.cs b/Okta.AspNetCore.WebApi.IntegrationTest/OktaMiddlewareShould.cs index 2d653c2..e46096f 100644 --- a/Okta.AspNetCore.WebApi.IntegrationTest/OktaMiddlewareShould.cs +++ b/Okta.AspNetCore.WebApi.IntegrationTest/OktaMiddlewareShould.cs @@ -56,6 +56,20 @@ public async Task Returns401WhenAccessToProtectedRouteWithInvalidTokenAsync() } } + [Fact] + public async Task InvokeCustomEventsAsync() + { + var accessToken = "thisIsAnInvalidToken"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, ProtectedEndpoint); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + using (var client = new HttpClient(_server.CreateHandler())) + { + var response = await client.SendAsync(request); + Assert.True(response.Headers.Contains("myCustomHeader")); + } + } + public void Dispose() { _server.Dispose(); diff --git a/Okta.AspNetCore.WebApi.IntegrationTest/Startup.cs b/Okta.AspNetCore.WebApi.IntegrationTest/Startup.cs index 639513a..e0d4b56 100644 --- a/Okta.AspNetCore.WebApi.IntegrationTest/Startup.cs +++ b/Okta.AspNetCore.WebApi.IntegrationTest/Startup.cs @@ -1,6 +1,9 @@ +using System; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -21,6 +24,13 @@ public Startup() // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + JwtBearerEvents events = new JwtBearerEvents(); + events.OnChallenge = context => + { + context.HttpContext.Response.Headers.Add("myCustomHeader", "myCustomValue"); + return Task.CompletedTask; + }; + services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; @@ -30,6 +40,7 @@ public void ConfigureServices(IServiceCollection services) .AddOktaWebApi(new OktaWebApiOptions() { OktaDomain = Configuration["Okta:OktaDomain"], + JwtBearerEvents = events, }); services.AddAuthorization(); diff --git a/Okta.AspNetCore/CHANGELOG.md b/Okta.AspNetCore/CHANGELOG.md index 19a0a2b..5269c82 100644 --- a/Okta.AspNetCore/CHANGELOG.md +++ b/Okta.AspNetCore/CHANGELOG.md @@ -1,6 +1,20 @@ # Changelog Running changelog of releases since `3.2.0` +## v4.0.0 + +### Features + +- Add support for OIDC events configuration in MVC projects. +- Add support for JWT events configuration in Web API projects. +- Add support for BackchannelHttpHandler configuration. +- Add support for BackchannelTimeout configuration. + +### Breaking changes + +- Remove `OnTokenValidated`, `OnUserInformationReceived`, `OnOktaApiFailure` and `OnAuthenticationFailed` events in favor of `OpenIdConnectEvents` (MVC). +- Remove `ClientId` property from `WebApiOptions`. + ## v3.5.1 ### Features diff --git a/Okta.AspNetCore/Okta.AspNetCore.csproj b/Okta.AspNetCore/Okta.AspNetCore.csproj index bd693cb..f989176 100644 --- a/Okta.AspNetCore/Okta.AspNetCore.csproj +++ b/Okta.AspNetCore/Okta.AspNetCore.csproj @@ -7,7 +7,7 @@ Official Okta middleware for ASP.NET Core 3.0+. Easily add authentication and authorization to ASP.NET Core applications. (c) 2020 Okta, Inc. - 3.5.1 + 4.0.0 Okta, Inc. Okta.AspNetCore Okta.AspNetCore diff --git a/Okta.AspNetCore/OktaAuthenticationOptionsExtensions.cs b/Okta.AspNetCore/OktaAuthenticationOptionsExtensions.cs index e9e9e91..53b5cf7 100644 --- a/Okta.AspNetCore/OktaAuthenticationOptionsExtensions.cs +++ b/Okta.AspNetCore/OktaAuthenticationOptionsExtensions.cs @@ -8,6 +8,7 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Extensions.DependencyInjection; using Okta.AspNet.Abstractions; @@ -57,19 +58,18 @@ public static AuthenticationBuilder AddOktaWebApi(this AuthenticationBuilder bui private static AuthenticationBuilder AddCodeFlow(AuthenticationBuilder builder, OktaMvcOptions options) { - var events = new OpenIdConnectEvents - { - OnRedirectToIdentityProvider = BeforeRedirectToIdentityProviderAsync, - }; + Func redirectEvent = options.OpenIdConnectEvents?.OnRedirectToIdentityProvider; + options.OpenIdConnectEvents ??= new OpenIdConnectEvents(); + options.OpenIdConnectEvents.OnRedirectToIdentityProvider = context => BeforeRedirectToIdentityProviderAsync(context, redirectEvent); JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); - builder.AddOpenIdConnect(oidcOptions => OpenIdConnectOptionsHelper.ConfigureOpenIdConnectOptions(options, events, oidcOptions)); + builder.AddOpenIdConnect(oidcOptions => OpenIdConnectOptionsHelper.ConfigureOpenIdConnectOptions(options, oidcOptions)); return builder; } - private static Task BeforeRedirectToIdentityProviderAsync(RedirectContext context) + private static Task BeforeRedirectToIdentityProviderAsync(RedirectContext context, Func redirectEvent) { // Verify if additional well-known params (e.g login-hint, sessionToken, idp, etc.) should be sent in the request. var oktaRequestParamValue = string.Empty; @@ -84,30 +84,14 @@ private static Task BeforeRedirectToIdentityProviderAsync(RedirectContext contex } } - return Task.CompletedTask; - } - - private static AuthenticationBuilder AddJwtValidation(AuthenticationBuilder builder, OktaWebApiOptions options) - { - var issuer = UrlHelper.CreateIssuerUrl(options.OktaDomain, options.AuthorizationServerId); - - var tokenValidationParameters = new DefaultTokenValidationParameters(options, issuer) + if (redirectEvent != null) { - ValidAudience = options.Audience, - }; - - builder.AddJwtBearer(opt => - { - opt.Audience = options.Audience; - opt.Authority = issuer; - opt.TokenValidationParameters = tokenValidationParameters; - opt.BackchannelHttpHandler = new OktaHttpMessageHandler("okta-aspnetcore", typeof(OktaAuthenticationOptionsExtensions).Assembly.GetName().Version, options); - - opt.SecurityTokenValidators.Clear(); - opt.SecurityTokenValidators.Add(new StrictSecurityTokenValidator()); - }); + return redirectEvent(context); + } - return builder; + return Task.CompletedTask; } + + private static AuthenticationBuilder AddJwtValidation(AuthenticationBuilder builder, OktaWebApiOptions options) => builder.AddJwtBearer(opt => OpenIdConnectOptionsHelper.ConfigureJwtBearerOptions(options, opt)); } } diff --git a/Okta.AspNetCore/OktaMvcOptions.cs b/Okta.AspNetCore/OktaMvcOptions.cs index 5ba7c2c..ec2b055 100644 --- a/Okta.AspNetCore/OktaMvcOptions.cs +++ b/Okta.AspNetCore/OktaMvcOptions.cs @@ -11,6 +11,9 @@ namespace Okta.AspNetCore { + /// + /// The configuration options for the underlying OIDC middleware. + /// public class OktaMvcOptions : AspNet.Abstractions.OktaWebOptions { /// @@ -50,25 +53,9 @@ public class OktaMvcOptions : AspNet.Abstractions.OktaWebOptions public bool GetClaimsFromUserInfoEndpoint { get; set; } = true; /// - /// Gets or sets the event invoked when an IdToken has been validated and produced an AuthenticationTicket. + /// Gets or sets the OIDC events. /// - /// The OnTokenValidated event. - public Func OnTokenValidated { get; set; } = context => Task.CompletedTask; - - /// - /// Gets or sets the event invoked when user information is retrieved from the UserInfoEndpoint. The value must be true when using this event. - /// - /// The OnUserInformationReceived event. - public Func OnUserInformationReceived { get; set; } = context => Task.CompletedTask; - - /// - /// Gets or sets the event invoked when a failure occurs within the Okta API. - /// - public Func OnOktaApiFailure { get; set; } = context => Task.CompletedTask; - - /// - /// Gets or sets the event invoked if exceptions are thrown during request processing. - /// - public Func OnAuthenticationFailed { get; set; } = context => Task.CompletedTask; + /// + public OpenIdConnectEvents OpenIdConnectEvents { get; set; } } } diff --git a/Okta.AspNetCore/OktaWebApiOptions.cs b/Okta.AspNetCore/OktaWebApiOptions.cs index dcfadd0..f29ef8a 100644 --- a/Okta.AspNetCore/OktaWebApiOptions.cs +++ b/Okta.AspNetCore/OktaWebApiOptions.cs @@ -3,9 +3,21 @@ // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. // +using System; +using System.Net.Http; +using Microsoft.AspNetCore.Authentication.JwtBearer; + namespace Okta.AspNetCore { + /// + /// Configuration options for the underlying OIDC middleware. + /// public sealed class OktaWebApiOptions : AspNet.Abstractions.OktaWebApiOptions { + /// + /// Gets or sets the JwtBearerEvents. + /// + /// + public JwtBearerEvents JwtBearerEvents { get; set; } } } diff --git a/Okta.AspNetCore/OpenIdConnectOptionsHelper.cs b/Okta.AspNetCore/OpenIdConnectOptionsHelper.cs index 1f92685..02d1a73 100644 --- a/Okta.AspNetCore/OpenIdConnectOptionsHelper.cs +++ b/Okta.AspNetCore/OpenIdConnectOptionsHelper.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Security.Claims; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.OAuth.Claims; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Http; @@ -23,9 +24,8 @@ public class OpenIdConnectOptionsHelper /// Configure an OpenIdConnectOptions object based on user's configuration. /// /// The options. - /// The OpenIdConnect events. /// The OpenIdConnectOptions to configure. - public static void ConfigureOpenIdConnectOptions(OktaMvcOptions oktaMvcOptions, OpenIdConnectEvents events, OpenIdConnectOptions oidcOptions) + public static void ConfigureOpenIdConnectOptions(OktaMvcOptions oktaMvcOptions, OpenIdConnectOptions oidcOptions) { var issuer = UrlHelper.CreateIssuerUrl(oktaMvcOptions.OktaDomain, oktaMvcOptions.AuthorizationServerId); @@ -44,6 +44,7 @@ public static void ConfigureOpenIdConnectOptions(OktaMvcOptions oktaMvcOptions, "okta-aspnetcore", typeof(OktaAuthenticationOptionsExtensions).Assembly.GetName().Version, oktaMvcOptions); + oidcOptions.BackchannelTimeout = oktaMvcOptions.BackchannelTimeout; var hasDefinedScopes = oktaMvcOptions.Scope?.Any() ?? false; if (hasDefinedScopes) @@ -61,16 +62,9 @@ public static void ConfigureOpenIdConnectOptions(OktaMvcOptions oktaMvcOptions, NameClaimType = "name", }; - oidcOptions.Events.OnRedirectToIdentityProvider = events.OnRedirectToIdentityProvider; - - if (oktaMvcOptions.OnTokenValidated != null) - { - oidcOptions.Events.OnTokenValidated = oktaMvcOptions.OnTokenValidated; - } - - if (oktaMvcOptions.GetClaimsFromUserInfoEndpoint && oktaMvcOptions.OnUserInformationReceived != null) + if (oktaMvcOptions.OpenIdConnectEvents != null) { - oidcOptions.Events.OnUserInformationReceived = oktaMvcOptions.OnUserInformationReceived; + oidcOptions.Events = oktaMvcOptions.OpenIdConnectEvents; } if (oktaMvcOptions.GetClaimsFromUserInfoEndpoint) @@ -83,16 +77,30 @@ public static void ConfigureOpenIdConnectOptions(OktaMvcOptions oktaMvcOptions, oidcOptions.ClaimActions.MapJsonKey(ClaimTypes.Name, "name"); oidcOptions.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub"); oidcOptions.ClaimActions.MapJsonKey(ClaimTypes.Surname, "family_name"); + } - if (oktaMvcOptions.OnOktaApiFailure != null) - { - oidcOptions.Events.OnRemoteFailure = oktaMvcOptions.OnOktaApiFailure; - } + /// + /// Configure the JwtBearerOptions based on user's configuration. + /// + /// The options. + /// The jwtBearerOptions to configure. + public static void ConfigureJwtBearerOptions(OktaWebApiOptions oktaWebApiOptions, JwtBearerOptions jwtBearerOptions) + { + var issuer = UrlHelper.CreateIssuerUrl(oktaWebApiOptions.OktaDomain, oktaWebApiOptions.AuthorizationServerId); - if (oktaMvcOptions.OnAuthenticationFailed != null) + var tokenValidationParameters = new DefaultTokenValidationParameters(oktaWebApiOptions, issuer) { - oidcOptions.Events.OnAuthenticationFailed = oktaMvcOptions.OnAuthenticationFailed; - } + ValidAudience = oktaWebApiOptions.Audience, + }; + + jwtBearerOptions.Audience = oktaWebApiOptions.Audience; + jwtBearerOptions.Authority = issuer; + jwtBearerOptions.TokenValidationParameters = tokenValidationParameters; + jwtBearerOptions.BackchannelHttpHandler = new OktaHttpMessageHandler("okta-aspnetcore", typeof(OktaAuthenticationOptionsExtensions).Assembly.GetName().Version, oktaWebApiOptions); + jwtBearerOptions.Events = oktaWebApiOptions.JwtBearerEvents ?? new JwtBearerEvents(); + jwtBearerOptions.SecurityTokenValidators.Clear(); + jwtBearerOptions.SecurityTokenValidators.Add(new StrictSecurityTokenValidator()); + jwtBearerOptions.BackchannelTimeout = oktaWebApiOptions.BackchannelTimeout; } } } diff --git a/README.md b/README.md index 16fad87..bdcf409 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,12 @@ Okta ASP.NET middleware ======================== -* [Release status](#release-status) -* [Need help?](#need-help) -* [What you need](#what-you-need) -* [Getting started](#getting-started) -* [Contributing](#contributing) +- [Okta ASP.NET middleware](#okta-aspnet-middleware) +- [Release status](#release-status) +- [Need help?](#need-help) +- [What you need](#what-you-need) +- [Getting Started](#getting-started) +- [Contributing](#contributing) This package will enable your ASP.NET application to work with Okta via OAuth 2.0/OIDC. You can follow our instructions below, check out our examples on GitHub or [jump to our guides](https://developer.okta.com/docs/guides/sign-into-web-app/aspnet/before-you-begin/) to see how to configure Okta with your ASP.NET applications. @@ -87,6 +88,8 @@ sn.exe -Vr (path-to)\Okta.AspNet.Abstractions.dll sn.exe -Vr (path-to)\Okta.AspNet.Test.dll ``` +You should restart Visual Studio after making these changes. + Check out the [Contributing Guide](https://github.com/okta/okta-aspnet/tree/master/CONTRIBUTING.md). [github-issues]: https://github.com/okta/okta-aspnet/issues diff --git a/docs/aspnet4x-mvc.md b/docs/aspnet4x-mvc.md index 27be6a7..95439a5 100644 --- a/docs/aspnet4x-mvc.md +++ b/docs/aspnet4x-mvc.md @@ -75,6 +75,57 @@ public class Startup } ``` +> Note: The proxy configuration is ignored when an `BackchannelHttpClientHandler` is provided. + +## Configure your own HttpMessageHandler implementation + +Starting in Okta.AspNet 2.0.0/Okta.AspNetCore 4.0.0, you can now provide your own HttpMessageHandler implementation to be used by the uderlying OIDC middleware. This is useful if you want to log all the requests and responses to diagnose problems, or retry failed requests among other use cases. The following example shows how to provide your own logging logic via Http handlers: + +```csharp + +public class Startup +{ + public void Configuration(IAppBuilder app) + { + app.UseOktaMvc(new OktaMvcOptions + { + BackchannelHttpClientHandler = new MyLoggingHandler((logger), + }); + } +} + +public class MyLoggingHandler : DelegatingHandler +{ + private readonly ILogger _logger; + + public MyLoggingHandler(ILogger logger) + { + _logger = logger; + } + + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + _logger.Trace($"Request: {request}"); + + try + { + var response = await base.SendAsync(request, cancellationToken); + _logger.Trace($"Response: {response}"); + + return response; + } + catch (Exception ex) + { + _logger.Error($"Something went wrong: {ex}"); + throw; + } + } +} + +``` + ## Self-Hosted login configuration ```csharp @@ -174,7 +225,11 @@ This example assumes you have a view called `Claim` whose model is of type `Syst ## Handling failures -In the event a failure occurs, the Okta.AspNet library provides the `OnAuthenticationFailed` delegate defined on the `OktaMvcOptions` class. The following is an example of how to use `OnAuthenticationFailed` to handle authentication failures: +This library exposes [OpenIdConnectEvents](https://docs.microsoft.com/en-us/previous-versions/aspnet/mt180963(v=vs.113)) so you can hook into specific events during the authentication process. For more information see [`AuthenticationFailed`](https://docs.microsoft.com/en-us/previous-versions/aspnet/mt180967(v=vs.113)). + + + The following is an example of how to use events to handle failures: + ```csharp public class Startup @@ -184,7 +239,10 @@ public class Startup app.UseOktaMvc(new OktaMvcOptions() { // ... other configuration options removed for brevity ... - AuthenticationFailed = OnAuthenticationFailed, + OpenIdConnectEvents = new OpenIdConnectAuthenticationNotifications + { + AuthenticationFailed = OnAuthenticationFailed, + }, }); } @@ -216,9 +274,10 @@ The `OktaMvcOptions` class configures the Okta middleware. You can see all the a | LoginMode | No | LoginMode controls the login redirect behavior of the middleware. The default value is `OktaHosted`. | | GetClaimsFromUserInfoEndpoint | No | Whether to retrieve additional claims from the UserInfo endpoint after login. The default value is `true`. | | ClockSkew | No | The clock skew allowed when validating tokens. The default value is 2 minutes. | -| SecurityTokenValidated | No | The event invoked after the security token has passed validation and a `ClaimsIdentity` has been generated. | -| OnAuthenticationFailed | No | The event invoked if exceptions are thrown during request processing. | +|OpenIdConnectEvents | No | Specifies the [events](https://docs.microsoft.com/en-us/previous-versions/aspnet/dn800270(v=vs.113)) which the underlying OpenIdConnectHandler invokes to enable developer control over the authentication process.| | Proxy | No | An object describing proxy server configuration. Properties are `Host`, `Port`, `Username` and `Password` | +| BackchannelTimeout | No | Timeout value in milliseconds for back channel communications with Okta. The default value is 1 minute. | +| BackchannelHttpClientHandler | No | The HttpMessageHandler used to communicate with Okta. | You can store these values (except the Token event) in the `Web.config`, but be careful when checking in the client secret to the source control. diff --git a/docs/aspnet4x-webapi.md b/docs/aspnet4x-webapi.md index ee2be50..3b1e005 100644 --- a/docs/aspnet4x-webapi.md +++ b/docs/aspnet4x-webapi.md @@ -56,6 +56,56 @@ public class Startup }); } } +``` +> Note: The proxy configuration is ignored when a `BackchannelHttpClientHandler` is provided. + +## Configure your own HttpMessageHandler implementation + +Starting in Okta.AspNet 2.0.0/Okta.AspNetCore 4.0.0, you can now provide your own HttpMessageHandler implementation to be used by the uderlying OIDC middleware. This is useful if you want to log all the requests and responses to diagnose problems, or retry failed requests among other use cases. The following example shows how to provide your own logging logic via Http handlers: + +```csharp + +public class Startup +{ + public void Configuration(IAppBuilder app) + { + app.UseOktaMvc(new OktaWebApiOptions + { + BackchannelHttpClientHandler = new MyLoggingHandler((logger), + }); + } +} + +public class MyLoggingHandler : DelegatingHandler +{ + private readonly ILogger _logger; + + public MyLoggingHandler(ILogger logger) + { + _logger = logger; + } + + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + _logger.Trace($"Request: {request}"); + + try + { + var response = await base.SendAsync(request, cancellationToken); + _logger.Trace($"Response: {response}"); + + return response; + } + catch (Exception ex) + { + _logger.Error($"Something went wrong: {ex}"); + throw; + } + } +} + ``` # Configuration Reference @@ -70,6 +120,9 @@ The `OktaWebApiOptions` class configures the Okta middleware. You can see all th | Audience | No | The expected audience of incoming tokens. The default value is `api://default`. | | ClockSkew | No | The clock skew allowed when validating tokens. The default value is 2 minutes. | | Proxy | No | An object describing proxy server configuration. Properties are `Host`, `Port`, `Username` and `Password` | + OAuthBearerAuthenticationProvider | No | The [authentication provider](https://docs.microsoft.com/en-us/previous-versions/aspnet/dn253813(v=vs.113)) which specifies callback methods invoked by the underlying authentication middleware to enable developer control over the authentication process. | +| BackchannelTimeout | No | Timeout value in milliseconds for back channel communications with Okta. The default value is 1 minute. | +| BackchannelHttpClientHandler | No | The HttpMessageHandler used to communicate with Okta. | You can store these values in the `Web.config`. diff --git a/docs/aspnetcore-mvc.md b/docs/aspnetcore-mvc.md index 50c141a..cc4bf52 100644 --- a/docs/aspnetcore-mvc.md +++ b/docs/aspnetcore-mvc.md @@ -113,6 +113,56 @@ public void ConfigureServices(IServiceCollection services) } ``` +> Note: The proxy configuration is ignored when a `BackchannelHttpClientHandler` is provided. + +## Configure your own HttpMessageHandler implementation + +Starting in Okta.AspNet 2.0.0/Okta.AspNetCore 4.0.0, you can now provide your own HttpMessageHandler implementation to be used by the uderlying OIDC middleware. This is useful if you want to log all the requests and responses to diagnose problems, or retry failed requests among other use cases. The following example shows how to provide your own logging logic via Http handlers: + +```csharp + +public class Startup +{ + public void Configuration(IAppBuilder app) + { + app.UseOktaMvc(new OktaMvcOptions + { + BackchannelHttpClientHandler = new MyLoggingHandler((logger), + }); + } +} + +public class MyLoggingHandler : DelegatingHandler +{ + private readonly ILogger _logger; + + public MyLoggingHandler(ILogger logger) + { + _logger = logger; + } + + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + _logger.Trace($"Request: {request}"); + + try + { + var response = await base.SendAsync(request, cancellationToken); + _logger.Trace($"Response: {response}"); + + return response; + } + catch (Exception ex) + { + _logger.Error($"Something went wrong: {ex}"); + throw; + } + } +} + +``` ## Configuration for cloud services or load balancers @@ -235,6 +285,7 @@ public IActionResult SignInWithIdp(string idp) return RedirectToAction("Index", "Home"); } ``` +The Okta.AspNetCore library includes your identity provider id in the authorize URL and the user is prompted with the identity provider login. For more information, check out our guides to [add an external identity provider](https://developer.okta.com/docs/guides/add-an-external-idp/). ## Accessing OIDC Tokens @@ -268,7 +319,10 @@ This example assumes you have a view called `OIDCToken` whose model is of type ` ## Handling failures -In the event a failure occurs, the Okta.AspNetCore library provides the `OnOktaApiFailure` and `OnAuthenticationFailed` delegates defined on the `OktaMvcOptions` class. The following is an example of how to use `OnOktaApiFailure` and `OnAuthenticationFailed` to handle failures: +In the event a failure occurs, the Okta.AspNetCore library exposes [OpenIdConnectEvents](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents.onauthenticationfailed) so you can hook into specific events during the authentication process. For more information See [`OnAuthenticationFailed`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents.onauthenticationfailed) or [`OnRemoteFailure`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationevents.onremotefailure). + + + The following is an example of how to use events to handle failures: ```csharp public class Startup @@ -278,8 +332,11 @@ public class Startup services.AddOktaMvc(new OktaMvcOptions { // ... other configuration options removed for brevity ... - OnOktaApiFailure = OnOktaApiFailure, - OnAuthenticationFailed = OnAuthenticationFailed, + OpenIdConnectEvents = new OpenIdConnectEvents + { + OnAuthenticationFailed = OnAuthenticationFailed, + OnRemoteFailure = OnOktaApiFailure, + }, }); } @@ -302,9 +359,6 @@ public class Startup } } ``` - -The Okta.AspNetCore library will include your identity provider id in the authorize URL and the user will prompted with the identity provider login. For more information, check out our guides to [add an external identity provider](https://developer.okta.com/docs/guides/add-an-external-idp/). - # Configuration Reference The `OktaMvcOptions` class configures the Okta middleware. You can see all the available options in the table below: @@ -321,11 +375,10 @@ The `OktaMvcOptions` class configures the Okta middleware. You can see all the a | Scope | No | The OAuth 2.0/OpenID Connect scopes to request when logging in. The default value is `openid profile`. | | GetClaimsFromUserInfoEndpoint | No | Whether to retrieve additional claims from the UserInfo endpoint after login. The default value is `true`. | | ClockSkew | No | The clock skew allowed when validating tokens. The default value is 2 minutes. | -| OnTokenValidated | No | The event invoked after the security token has passed validation and a ClaimsIdentity has been generated. | -| OnUserInformationReceived | No | The event invoked when user information is retrieved from the UserInfoEndpoint. The `GetClaimsFromUserInfoEndpoint` value must be `true` when using this event. | -| OnOktaApiFailure | No | The event invoked when a failure occurs within the Okta API. | -| OnAuthenticationFailed | No | The event invoked if exceptions are thrown during request processing. | | Proxy | No | An object describing proxy server configuration. Properties are `Host`, `Port`, `Username` and `Password` | +| OpenIdConnectEvents | No | Specifies the [events](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents) which the underlying OpenIdConnectHandler invokes to enable developer control over the authentication process.| +| BackchannelTimeout | No | Timeout value in milliseconds for back channel communications with Okta. The default value is 1 minute. | +| BackchannelHttpClientHandler | No | The HttpMessageHandler used to communicate with Okta. | You can store these values (except the events) in the `appsettings.json`, but be careful when checking in the client secret to the source control. diff --git a/docs/aspnetcore-webapi.md b/docs/aspnetcore-webapi.md index 016b763..4612f22 100644 --- a/docs/aspnetcore-webapi.md +++ b/docs/aspnetcore-webapi.md @@ -69,6 +69,57 @@ public void ConfigureServices(IServiceCollection services) } ``` +> Note: The proxy configuration is ignored when a `BackchannelHttpClientHandler` is provided. + +## Configure your own HttpMessageHandler implementation + +Starting in Okta.AspNet 2.0.0/Okta.AspNetCore 4.0.0, you can now provide your own HttpMessageHandler implementation to be used by the uderlying OIDC middleware. This is useful if you want to log all the requests and responses to diagnose problems, or retry failed requests among other use cases. The following example shows how to provide your own logging logic via Http handlers: + +```csharp + +public class Startup +{ + public void Configuration(IAppBuilder app) + { + app.UseOktaMvc(new OktaWebApiOptions + { + BackchannelHttpClientHandler = new MyLoggingHandler((logger), + }); + } +} + +public class MyLoggingHandler : DelegatingHandler +{ + private readonly ILogger _logger; + + public MyLoggingHandler(ILogger logger) + { + _logger = logger; + } + + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + _logger.Trace($"Request: {request}"); + + try + { + var response = await base.SendAsync(request, cancellationToken); + _logger.Trace($"Response: {response}"); + + return response; + } + catch (Exception ex) + { + _logger.Error($"Something went wrong: {ex}"); + throw; + } + } +} + +``` + ## Configuration Reference The `OktaWebApiOptions` class configures the Okta middleware. You can see all the available options in the table below: @@ -79,8 +130,11 @@ The `OktaWebApiOptions` class configures the Okta middleware. You can see all th | ClientId | No | The client ID of your Okta Application. This property is obsolete and will be removed in the next major version. | | AuthorizationServerId | No | The [Okta Custom Authorization Server](https://developer.okta.com/docs/concepts/auth-servers/#custom-authorization-server) to use. The default value is `default`. [The Org Authorization Server](https://developer.okta.com/docs/concepts/auth-servers/#org-authorization-server) is not supported. | | Audience | No | The expected audience of incoming tokens. The default value is `api://default`. | +| JwtBearerEvents | No | Specifies the [events](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbearerevents) which the underlying JwtBearerHandler invokes to enable developer control over the authentication process.| | ClockSkew | No | The clock skew allowed when validating tokens. The default value is 2 minutes. | | Proxy | No | An object describing proxy server configuration. Properties are `Host`, `Port`, `Username` and `Password` | +| BackchannelTimeout | No | Timeout value in milliseconds for back channel communications with Okta. The default value is 1 minute. | +| BackchannelHttpClientHandler | No | The HttpMessageHandler used to communicate with Okta. | You can store these values in the `appsettings.json`.