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();
}
///