Skip to content

Commit

Permalink
OKTA-434102 Add ability to use OIDC access tokens. #508 (#517)
Browse files Browse the repository at this point in the history
  • Loading branch information
andriizhegurov-okta authored Nov 18, 2021
1 parent 05c7aa2 commit 1ea6de9
Show file tree
Hide file tree
Showing 13 changed files with 274 additions and 47 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down
55 changes: 55 additions & 0 deletions src/Okta.Sdk.UnitTests/DefaultBearerTokenProviderShould.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// <copyright file="DefaultBearerTokenProviderShould.cs" company="Okta, Inc">
// 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.
// </copyright>

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<string> { "foo" },
};

var requestMessageHandler = new MockHttpMessageHandler(string.Empty, HttpStatusCode.Unauthorized);
var httpClientRequest = new HttpClient(requestMessageHandler);
var testableCustomTokenProvider = new TestableCustomTokenProvider();
var mockLogger = Substitute.For<ILogger>();
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();
}
}
}
31 changes: 31 additions & 0 deletions src/Okta.Sdk.UnitTests/OktaClientValidatorShould.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> { "foo" },
};

Action action = () => OktaClientConfigurationValidator.Validate(configuration);
action.Should().Throw<ArgumentNullException>();
}

[Fact]
public void NotFailWhenAccessTokenProvidedAndAuthorizationModeIsOAuthAccessToken()
{
var configuration = new OktaClientConfiguration
{
OktaDomain = "https://myOktaDomain.oktapreview.com",
AuthorizationMode = AuthorizationMode.BearerToken,
BearerToken = "AnyToken",
ClientId = "foo",
Scopes = new List<string> { "foo" },
};

Action action = () => OktaClientConfigurationValidator.Validate(configuration);
action.Should().NotThrow();
}
}
}
22 changes: 22 additions & 0 deletions src/Okta.Sdk.UnitTests/TestableCustomTokenProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// <copyright file="TestableCustomTokenProvider.cs" company="Okta, Inc">
// 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.
// </copyright>

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<string> GetAccessTokenAsync(bool forceRenew = false)
{
TokenRefreshed = true;
return Task.FromResult("NewAccessToken");
}
}
}
7 changes: 6 additions & 1 deletion src/Okta.Sdk/Configuration/AuthorizationMode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ namespace Okta.Sdk.Configuration
public enum AuthorizationMode
{
/// <summary>
/// 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.
/// </summary>
SSWS,

/// <summary>
/// Indicates that the SDK will request and send an access token in the authorization header when making calls.
/// </summary>
PrivateKey,

/// <summary>
/// Indicates that the SDK will use the provided access token <see cref="OktaClientConfiguration.BearerToken"/> when making calls.
/// </summary>
BearerToken,
}
}
9 changes: 7 additions & 2 deletions src/Okta.Sdk/Configuration/OktaClientConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
// </copyright>

using System;
using System.Collections.Generic;
using System.Diagnostics;
using Okta.Sdk.Internal;
Expand Down Expand Up @@ -126,11 +125,16 @@ public bool DisableHttpsCheck
/// </summary>
public List<string> Scopes { get; set; }

/// <summary>
/// Gets or sets the OAuth Access Token to use. For AuthorizationMode set to <see cref="AuthorizationMode.BearerToken"/>.
/// </summary>
public string BearerToken { get; set; }

/// <inheritdoc/>
public OktaClientConfiguration DeepClone()
=> new OktaClientConfiguration
{
ConnectionTimeout = ConnectionTimeout,
ConnectionTimeout = this.ConnectionTimeout,
OktaDomain = this.OktaDomain,
Token = this.Token,
Proxy = this.Proxy?.DeepClone(),
Expand All @@ -141,6 +145,7 @@ public OktaClientConfiguration DeepClone()
PrivateKey = this.PrivateKey,
ClientId = this.ClientId,
Scopes = this.Scopes,
BearerToken = this.BearerToken,
};
}
}
53 changes: 53 additions & 0 deletions src/Okta.Sdk/Internal/DefaultBearerTokenProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// <copyright file="DefaultBearerTokenProvider.cs" company="Okta, Inc">
// 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.
// </copyright>

using System;
using System.Threading.Tasks;

namespace Okta.Sdk.Internal
{
/// <summary>
/// External Token Provider.
/// </summary>
public class DefaultBearerTokenProvider : IOAuthTokenProvider
{
private readonly IOAuthTokenProvider _thirdPartyTokenProvider;
private string _bearerToken;

/// <summary>
/// Initializes a new instance of the <see cref="DefaultBearerTokenProvider"/> class.
/// </summary>
/// <param name="bearerToken">The token.</param>
/// <param name="tokenProvider">Optional custom token provider.</param>
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;
}

/// <inheritdoc/>
public async Task<string> 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;
}
}
}
18 changes: 10 additions & 8 deletions src/Okta.Sdk/Internal/DefaultRequestExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
// </copyright>

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
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -119,10 +122,7 @@ private async Task<HttpResponse<string>> SendAsync(
{
_logger.LogTrace($"{request.Method} {request.RequestUri}");

if (_oktaConfiguration.AuthorizationMode == AuthorizationMode.PrivateKey)
{
await ApplyOAuthHeaderAsync().ConfigureAwait(false);
}
await ApplyOAuthHeaderAsync().ConfigureAwait(false);

Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> operation = async (x, y) => await _httpClient.SendAsync(x, y).ConfigureAwait(false);
HttpResponseMessage response = null;
Expand All @@ -132,7 +132,9 @@ private async Task<HttpResponse<string>> 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);

Expand Down
2 changes: 1 addition & 1 deletion src/Okta.Sdk/Internal/NullOAuthTokenProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public static IOAuthTokenProvider Instance
/// <inheritdoc/>
public Task<string> GetAccessTokenAsync(bool forceRenew = false)
{
return new Task<string>(null);
return Task.FromResult<string>(null);
}
}
}
13 changes: 12 additions & 1 deletion src/Okta.Sdk/Internal/OktaClientConfigurationValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public class OktaClientConfigurationValidator
/// Validates the OktaClient configuration
/// </summary>
/// <param name="configuration">The configuration to be validated</param>
public static void Validate(OktaClientConfiguration configuration)
/// <param name="isExternalTokenProviderDefined">If there is an external token provider to be used to retrieve BearerToken.</param>
public static void Validate(OktaClientConfiguration configuration, bool isExternalTokenProviderDefined = false)
{
if (string.IsNullOrEmpty(configuration.OktaDomain))
{
Expand Down Expand Up @@ -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.");
}
}
}

/// <summary>
Expand Down
3 changes: 1 addition & 2 deletions src/Okta.Sdk/Okta.Sdk.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@

<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;netcoreapp3.1</TargetFrameworks>
<Version>5.2.1</Version>
<Version>5.2.2</Version>
<LangVersion>7.3</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Okta.Sdk.Abstractions" Version="4.0.2" />
<PackageReference Include="System.Diagnostics.TraceSource" Version="4.3.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.11.1" />
Expand Down
Loading

0 comments on commit 1ea6de9

Please sign in to comment.