diff --git a/CHANGELOG.md b/CHANGELOG.md index 307977c93..21c3e4ace 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ Running changelog of releases since `3.1.1` +## v5.2.2 + +### Features + +- Add ability to use pre-requested access tokens for authentication. #508 + + ## v5.2.1 ### Update diff --git a/README.md b/README.md index 13ffeb487..58cbd18b8 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,28 @@ var clientConfiguration = new OktaClientConfiguration var client = new OktaClient(clientConfiguration); ``` +It is possible to use an access token you retrieved outside of the SDK for authentication. For that, set `OktaClientConfiguration.AuthorizationMode` configuration property to `AuthorizationMode.BearerToken` and `OktaClientConfiguration.BearerToken` to the token string. +In addition to passing the token via configuration, you can inject your own implementation of the `IOAuthTokenProvider` interface via the OktaClient constructor. This strategy is useful when you want to refresh a token that has been expired. + +You can provide a value for `OktaClientConfiguration.BearerToken` option along with a custom `IOAuthTokenProvider` implementation. In this case, the SDK will try to use the `OktaClientConfiguration.BearerToken` first and if the request fails (for example when the token expires) then the SDK will retry with the custom token provider. + + + See [Get an access token and make a request](https://developer.okta.com/docs/guides/implement-oauth-for-okta/request-access-token/) for additional information. + + +```csharp + // myOAuthTokenProvider implements the Okta.Sdk.Internal.IOAuthTokenProvider interface + + var client = new OktaClient(new OktaClientConfiguration + { + ClientId = "{{clientId}}", + AuthorizationMode = AuthorizationMode.BearerToken, + BearerToken = "{{preRequestedAccessToken}}", + }, + oAuthTokenProvider: myOAuthTokenProvider + ); +``` ## Usage guide These examples will help you understand how to use this library. You can also browse the full [API reference documentation][dotnetdocs]. diff --git a/src/Okta.Sdk.UnitTests/DefaultBearerTokenProviderShould.cs b/src/Okta.Sdk.UnitTests/DefaultBearerTokenProviderShould.cs new file mode 100644 index 000000000..5127034e8 --- /dev/null +++ b/src/Okta.Sdk.UnitTests/DefaultBearerTokenProviderShould.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) 2020 - 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.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Okta.Sdk.Configuration; +using Okta.Sdk.Internal; +using Okta.Sdk.UnitTests.Internal; +using Xunit; + +namespace Okta.Sdk.UnitTests +{ + public class DefaultBearerTokenProviderShould + { + [Fact] + public async Task ReturnAccessTokenWhenRequestSuccess() + { + var tokenProvider = new DefaultBearerTokenProvider("DefaultExternallyGeneratedToken", default); + var token = await tokenProvider.GetAccessTokenAsync(); + token.Should().Be("DefaultExternallyGeneratedToken"); + } + + [Fact] + public async Task ReturnNewAccessTokenWhenOldIsNotAuthorized() + { + var configuration = new OktaClientConfiguration + { + OktaDomain = "https://myOktaDomain.oktapreview.com", + AuthorizationMode = AuthorizationMode.BearerToken, + ClientId = "foo", + BearerToken = "token", + Scopes = new List { "foo" }, + }; + + var requestMessageHandler = new MockHttpMessageHandler(string.Empty, HttpStatusCode.Unauthorized); + var httpClientRequest = new HttpClient(requestMessageHandler); + var testableCustomTokenProvider = new TestableCustomTokenProvider(); + var mockLogger = Substitute.For(); + var externalTokenProvider = new DefaultBearerTokenProvider("oldAccessToken", testableCustomTokenProvider); + var requestExecutor = new DefaultRequestExecutor(configuration, httpClientRequest, mockLogger, oAuthTokenProvider: externalTokenProvider); + + testableCustomTokenProvider.TokenRefreshed.Should().BeFalse(); + await requestExecutor.GetAsync("https://myOktaDomain.oktapreview.com/v1/users", default, default).ConfigureAwait(false); + testableCustomTokenProvider.TokenRefreshed.Should().BeTrue(); + } + } +} diff --git a/src/Okta.Sdk.UnitTests/OktaClientValidatorShould.cs b/src/Okta.Sdk.UnitTests/OktaClientValidatorShould.cs index 279378535..80c5bc503 100644 --- a/src/Okta.Sdk.UnitTests/OktaClientValidatorShould.cs +++ b/src/Okta.Sdk.UnitTests/OktaClientValidatorShould.cs @@ -195,5 +195,36 @@ public void NotFailWhenValidConfigWhenAuthorizationModeIsPrivateKey() Action action = () => OktaClientConfigurationValidator.Validate(configuration); action.Should().NotThrow(); } + + [Fact] + public void FailWhenAccessTokenNotProvidedAndAuthorizationModeIsOAuthAccessToken() + { + var configuration = new OktaClientConfiguration + { + OktaDomain = "https://myOktaDomain.oktapreview.com", + AuthorizationMode = AuthorizationMode.BearerToken, + ClientId = "foo", + Scopes = new List { "foo" }, + }; + + Action action = () => OktaClientConfigurationValidator.Validate(configuration); + action.Should().Throw(); + } + + [Fact] + public void NotFailWhenAccessTokenProvidedAndAuthorizationModeIsOAuthAccessToken() + { + var configuration = new OktaClientConfiguration + { + OktaDomain = "https://myOktaDomain.oktapreview.com", + AuthorizationMode = AuthorizationMode.BearerToken, + BearerToken = "AnyToken", + ClientId = "foo", + Scopes = new List { "foo" }, + }; + + Action action = () => OktaClientConfigurationValidator.Validate(configuration); + action.Should().NotThrow(); + } } } diff --git a/src/Okta.Sdk.UnitTests/TestableCustomTokenProvider.cs b/src/Okta.Sdk.UnitTests/TestableCustomTokenProvider.cs new file mode 100644 index 000000000..75821b30e --- /dev/null +++ b/src/Okta.Sdk.UnitTests/TestableCustomTokenProvider.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) 2020 - 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.Threading.Tasks; +using Okta.Sdk.Internal; + +namespace Okta.Sdk.UnitTests +{ + internal class TestableCustomTokenProvider : IOAuthTokenProvider + { + public bool TokenRefreshed { get; set; } + + public Task GetAccessTokenAsync(bool forceRenew = false) + { + TokenRefreshed = true; + return Task.FromResult("NewAccessToken"); + } + } +} diff --git a/src/Okta.Sdk/Configuration/AuthorizationMode.cs b/src/Okta.Sdk/Configuration/AuthorizationMode.cs index b7d35f78c..df9b40a4b 100644 --- a/src/Okta.Sdk/Configuration/AuthorizationMode.cs +++ b/src/Okta.Sdk/Configuration/AuthorizationMode.cs @@ -11,7 +11,7 @@ namespace Okta.Sdk.Configuration public enum AuthorizationMode { /// - /// Indicates that the SDK will send a SSWS token in the authorozation header when making calls. + /// Indicates that the SDK will send a SSWS token in the authorization header when making calls. /// SSWS, @@ -19,5 +19,10 @@ public enum AuthorizationMode /// Indicates that the SDK will request and send an access token in the authorization header when making calls. /// PrivateKey, + + /// + /// Indicates that the SDK will use the provided access token when making calls. + /// + BearerToken, } } diff --git a/src/Okta.Sdk/Configuration/OktaClientConfiguration.cs b/src/Okta.Sdk/Configuration/OktaClientConfiguration.cs index 93221686d..5fc46f03b 100644 --- a/src/Okta.Sdk/Configuration/OktaClientConfiguration.cs +++ b/src/Okta.Sdk/Configuration/OktaClientConfiguration.cs @@ -3,7 +3,6 @@ // 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.Diagnostics; using Okta.Sdk.Internal; @@ -126,11 +125,16 @@ public bool DisableHttpsCheck /// public List Scopes { get; set; } + /// + /// Gets or sets the OAuth Access Token to use. For AuthorizationMode set to . + /// + public string BearerToken { get; set; } + /// public OktaClientConfiguration DeepClone() => new OktaClientConfiguration { - ConnectionTimeout = ConnectionTimeout, + ConnectionTimeout = this.ConnectionTimeout, OktaDomain = this.OktaDomain, Token = this.Token, Proxy = this.Proxy?.DeepClone(), @@ -141,6 +145,7 @@ public OktaClientConfiguration DeepClone() PrivateKey = this.PrivateKey, ClientId = this.ClientId, Scopes = this.Scopes, + BearerToken = this.BearerToken, }; } } diff --git a/src/Okta.Sdk/Internal/DefaultBearerTokenProvider.cs b/src/Okta.Sdk/Internal/DefaultBearerTokenProvider.cs new file mode 100644 index 000000000..2c70d53e0 --- /dev/null +++ b/src/Okta.Sdk/Internal/DefaultBearerTokenProvider.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) 2014 - 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.Threading.Tasks; + +namespace Okta.Sdk.Internal +{ + /// + /// External Token Provider. + /// + public class DefaultBearerTokenProvider : IOAuthTokenProvider + { + private readonly IOAuthTokenProvider _thirdPartyTokenProvider; + private string _bearerToken; + + /// + /// Initializes a new instance of the class. + /// + /// The token. + /// Optional custom token provider. + public DefaultBearerTokenProvider(string bearerToken, IOAuthTokenProvider tokenProvider) + { + if (string.IsNullOrEmpty(bearerToken) && tokenProvider == default) + { + throw new ArgumentException("The token is not set.", nameof(bearerToken)); + } + + _bearerToken = bearerToken; + _thirdPartyTokenProvider = tokenProvider; + } + + /// + public async Task GetAccessTokenAsync(bool forceRenew = false) + { + if (forceRenew || string.IsNullOrEmpty(_bearerToken)) + { + if (_thirdPartyTokenProvider != null) + { + _bearerToken = await _thirdPartyTokenProvider.GetAccessTokenAsync(forceRenew).ConfigureAwait(false); + } + else + { + _bearerToken = string.Empty; + } + } + + return _bearerToken; + } + } +} diff --git a/src/Okta.Sdk/Internal/DefaultRequestExecutor.cs b/src/Okta.Sdk/Internal/DefaultRequestExecutor.cs index 6249b7bd2..9f32e631b 100644 --- a/src/Okta.Sdk/Internal/DefaultRequestExecutor.cs +++ b/src/Okta.Sdk/Internal/DefaultRequestExecutor.cs @@ -3,14 +3,14 @@ // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. // -using Microsoft.Extensions.Logging; -using Okta.Sdk.Configuration; using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Okta.Sdk.Configuration; namespace Okta.Sdk.Internal { @@ -74,7 +74,10 @@ private static void ApplyDefaultClientSettings(HttpClient client, string oktaDom private async Task ApplyOAuthHeaderAsync(bool forceTokenRenew = false) { var token = await _oAuthTokenProvider.GetAccessTokenAsync(forceTokenRenew).ConfigureAwait(false); - _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + if (!string.IsNullOrEmpty(token)) + { + _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + } } private string EnsureRelativeUrl(string uri) @@ -119,10 +122,7 @@ private async Task> SendAsync( { _logger.LogTrace($"{request.Method} {request.RequestUri}"); - if (_oktaConfiguration.AuthorizationMode == AuthorizationMode.PrivateKey) - { - await ApplyOAuthHeaderAsync().ConfigureAwait(false); - } + await ApplyOAuthHeaderAsync().ConfigureAwait(false); Func> operation = async (x, y) => await _httpClient.SendAsync(x, y).ConfigureAwait(false); HttpResponseMessage response = null; @@ -132,7 +132,9 @@ private async Task> SendAsync( _logger.LogTrace($"{(int)response.StatusCode} {request.RequestUri.PathAndQuery}"); // If OAuth token expired, get a new token and retry - if ((int)response.StatusCode == 401 && _oktaConfiguration.AuthorizationMode == AuthorizationMode.PrivateKey) + if ((int)response.StatusCode == 401 && + (_oktaConfiguration.AuthorizationMode == AuthorizationMode.PrivateKey || + _oktaConfiguration.AuthorizationMode == AuthorizationMode.BearerToken)) { await ApplyOAuthHeaderAsync(true).ConfigureAwait(false); diff --git a/src/Okta.Sdk/Internal/NullOAuthTokenProvider.cs b/src/Okta.Sdk/Internal/NullOAuthTokenProvider.cs index 6c8ffa0f5..f2fbd3e09 100644 --- a/src/Okta.Sdk/Internal/NullOAuthTokenProvider.cs +++ b/src/Okta.Sdk/Internal/NullOAuthTokenProvider.cs @@ -37,7 +37,7 @@ public static IOAuthTokenProvider Instance /// public Task GetAccessTokenAsync(bool forceRenew = false) { - return new Task(null); + return Task.FromResult(null); } } } diff --git a/src/Okta.Sdk/Internal/OktaClientConfigurationValidator.cs b/src/Okta.Sdk/Internal/OktaClientConfigurationValidator.cs index 78faad15a..fc2a5dbf1 100644 --- a/src/Okta.Sdk/Internal/OktaClientConfigurationValidator.cs +++ b/src/Okta.Sdk/Internal/OktaClientConfigurationValidator.cs @@ -18,7 +18,8 @@ public class OktaClientConfigurationValidator /// Validates the OktaClient configuration /// /// The configuration to be validated - public static void Validate(OktaClientConfiguration configuration) + /// If there is an external token provider to be used to retrieve BearerToken. + public static void Validate(OktaClientConfiguration configuration, bool isExternalTokenProviderDefined = false) { if (string.IsNullOrEmpty(configuration.OktaDomain)) { @@ -91,6 +92,16 @@ public static void Validate(OktaClientConfiguration configuration) throw new ArgumentNullException(nameof(configuration.Scopes), "Scopes cannot be null or empty."); } } + + if (configuration.AuthorizationMode == AuthorizationMode.BearerToken) + { + if (string.IsNullOrEmpty(configuration.BearerToken) && !isExternalTokenProviderDefined) + { + throw new ArgumentNullException( + nameof(configuration.AuthorizationMode), + $"{nameof(configuration.BearerToken)} configuration property cannot be null when AuthorizationMode.BearerToken is selected."); + } + } } /// diff --git a/src/Okta.Sdk/Okta.Sdk.csproj b/src/Okta.Sdk/Okta.Sdk.csproj index 24d489d30..621afba96 100644 --- a/src/Okta.Sdk/Okta.Sdk.csproj +++ b/src/Okta.Sdk/Okta.Sdk.csproj @@ -2,14 +2,13 @@ netstandard2.0;netstandard2.1;netcoreapp3.1 - 5.2.1 + 5.2.2 7.3 - diff --git a/src/Okta.Sdk/OktaClient.cs b/src/Okta.Sdk/OktaClient.cs index 40a25a123..a31f2cf6f 100644 --- a/src/Okta.Sdk/OktaClient.cs +++ b/src/Okta.Sdk/OktaClient.cs @@ -4,10 +4,8 @@ // using System; -using System.Collections.Generic; using System.IO; using System.Net.Http; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; @@ -47,25 +45,28 @@ public partial class OktaClient : IOktaClient /// configuration from an okta.yaml file or environment variables. /// /// The logging interface to use, if any. + /// The HTTP client to use for requests to the Okta API. + /// The retry strategy interface to use, if any. /// The JSON serializer to use, if any. Using the DefaultSerializer is still strongly recommended since it has all the behavior this SDK needs to work properly. /// If a custom serializer is used, the developer is responsible to add the required logic for this SDK to continue working properly. See to check out what can be configured. - /// - public OktaClient(OktaClientConfiguration apiClientConfiguration = null, ILogger logger = null, ISerializer serializer = null) + /// The custom token provider to renew an access token when it expires. Only works with . + protected OktaClient(OktaClientConfiguration apiClientConfiguration, IOAuthTokenProvider oAuthTokenProvider, HttpClient httpClient, ILogger logger, IRetryStrategy retryStrategy, ISerializer serializer) { Configuration = GetConfigurationOrDefault(apiClientConfiguration); - OktaClientConfigurationValidator.Validate(Configuration); + OktaClientConfigurationValidator.Validate(Configuration, oAuthTokenProvider != default); logger = logger ?? NullLogger.Instance; serializer = serializer ?? new DefaultSerializer(); + var resourceFactory = new ResourceFactory(this, logger); - var defaultClient = DefaultHttpClient.Create( - Configuration.ConnectionTimeout, - Configuration.Proxy, - logger); + var defaultHttpClient = httpClient ?? + DefaultHttpClient.Create( + Configuration.ConnectionTimeout, + Configuration.Proxy, + logger); - var resourceFactory = new ResourceFactory(this, logger); - IOAuthTokenProvider oAuthTokenProvider = (Configuration.AuthorizationMode == AuthorizationMode.PrivateKey) ? new DefaultOAuthTokenProvider(Configuration, resourceFactory, logger: logger) : NullOAuthTokenProvider.Instance; - var requestExecutor = new DefaultRequestExecutor(Configuration, defaultClient, logger, oAuthTokenProvider: oAuthTokenProvider); + var tokenProvider = GetTokenProvider(Configuration, logger, resourceFactory, oAuthTokenProvider); + var requestExecutor = new DefaultRequestExecutor(Configuration, defaultHttpClient, logger, retryStrategy, tokenProvider); _dataStore = new DefaultDataStore( requestExecutor, @@ -78,6 +79,38 @@ public OktaClient(OktaClientConfiguration apiClientConfiguration = null, ILogger PayloadHandler.TryRegister(); } + /// + /// Initializes a new instance of the class. + /// + /// + /// The client configuration. If null, the library will attempt to load + /// configuration from an okta.yaml file or environment variables. + /// + /// The logging interface to use, if any. + /// The JSON serializer to use, if any. Using the DefaultSerializer is still strongly recommended since it has all the behavior this SDK needs to work properly. + /// If a custom serializer is used, the developer is responsible to add the required logic for this SDK to continue working properly. See to check out what can be configured. + /// The custom token provider to renew an access token when it expires. Only works with . + /// + public OktaClient(OktaClientConfiguration apiClientConfiguration = null, ILogger logger = null, ISerializer serializer = null, IOAuthTokenProvider oAuthTokenProvider = null) + : this (apiClientConfiguration, oAuthTokenProvider, httpClient: default, logger, retryStrategy: default, serializer) + { + } + + private IOAuthTokenProvider GetTokenProvider(OktaClientConfiguration configuration, ILogger logger, ResourceFactory resourceFactory, IOAuthTokenProvider customTokenProvider) + { + switch (configuration.AuthorizationMode) + { + case AuthorizationMode.PrivateKey: + return new DefaultOAuthTokenProvider(configuration, resourceFactory, logger: logger); + case AuthorizationMode.SSWS: + return NullOAuthTokenProvider.Instance; + case AuthorizationMode.BearerToken: + return new DefaultBearerTokenProvider(configuration.BearerToken, customTokenProvider); + default: + throw new ArgumentException($"Token provider is not implemented for AuthorizationMode={configuration.AuthorizationMode}", nameof(configuration)); + } + } + /// /// Initializes a new instance of the class using the specified . /// @@ -91,27 +124,10 @@ public OktaClient(OktaClientConfiguration apiClientConfiguration = null, ILogger /// The JSON serializer to use, if any. Using the DefaultSerializer is still strongly recommended since it has all the behavior this SDK needs to work properly. /// If a custom serializer is used, the developer is responsible to add the required logic for this SDK to continue working properly. See to check out what settings can be configured. /// - public OktaClient(OktaClientConfiguration apiClientConfiguration, HttpClient httpClient, ILogger logger = null, IRetryStrategy retryStrategy = null, ISerializer serializer = null) + /// The custom token provider to renew an access token when it expires. Only works with . + public OktaClient(OktaClientConfiguration apiClientConfiguration, HttpClient httpClient, ILogger logger = null, IRetryStrategy retryStrategy = null, ISerializer serializer = null, IOAuthTokenProvider oAuthTokenProvider = null) + : this(apiClientConfiguration, oAuthTokenProvider, httpClient, logger, retryStrategy, serializer) { - Configuration = GetConfigurationOrDefault(apiClientConfiguration); - OktaClientConfigurationValidator.Validate(Configuration); - - logger = logger ?? NullLogger.Instance; - serializer = serializer ?? new DefaultSerializer(); - - var resourceFactory = new ResourceFactory(this, logger); - IOAuthTokenProvider oAuthTokenProvider = (Configuration.AuthorizationMode == AuthorizationMode.PrivateKey) ? new DefaultOAuthTokenProvider(Configuration, resourceFactory, logger: logger) : NullOAuthTokenProvider.Instance; - var requestExecutor = new DefaultRequestExecutor(Configuration, httpClient, logger, retryStrategy, oAuthTokenProvider); - - _dataStore = new DefaultDataStore( - requestExecutor, - serializer, - resourceFactory, - logger); - - PayloadHandler.TryRegister(); - PayloadHandler.TryRegister(); - PayloadHandler.TryRegister(); } ///