Skip to content

Commit

Permalink
Delegate on the fly (#14)
Browse files Browse the repository at this point in the history
Adds the ability to request access token using delegation
  • Loading branch information
zapodot authored Sep 30, 2020
1 parent 509386c commit c44d3bf
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 33 deletions.
11 changes: 6 additions & 5 deletions KS.Fiks.Maskinporten.Client.Tests/Cache/TokenCacheTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using FluentAssertions;
using Ks.Fiks.Maskinporten.Client.Cache;
using Xunit;

namespace Ks.Fiks.Maskinporten.Client.Tests.Cache
Expand All @@ -21,7 +22,7 @@ public async Task ReturnsValueFromGetterAtFirstCall()

var expectedValue = _fixture.GetRandomToken();

var actualValue = await sut.GetToken("key", () => Task.FromResult(expectedValue)).ConfigureAwait(false);
var actualValue = await sut.GetToken(new TokenRequest { Scopes = "key"}, () => Task.FromResult(expectedValue)).ConfigureAwait(false);

actualValue.Should().Be(expectedValue);
}
Expand All @@ -34,9 +35,9 @@ public async Task ReturnsValueFromCacheInSecondCallIfWithinTokenTimeLimit()
var expectedValue = _fixture.GetRandomToken(10);
var otherValue = _fixture.GetRandomToken(10);

var firstValue = await sut.GetToken("key", () => Task.FromResult(expectedValue)).ConfigureAwait(false);
var firstValue = await sut.GetToken(new TokenRequest { Scopes = "key"}, () => Task.FromResult(expectedValue)).ConfigureAwait(false);
await Task.Delay(TimeSpan.FromMilliseconds(100)).ConfigureAwait(false);
var secondValue = await sut.GetToken("key", () => Task.FromResult(otherValue)).ConfigureAwait(false);
var secondValue = await sut.GetToken(new TokenRequest { Scopes = "key"}, () => Task.FromResult(otherValue)).ConfigureAwait(false);

secondValue.Should().Be(expectedValue);
}
Expand All @@ -49,9 +50,9 @@ public async Task ReturnsNewValueIfCallIsOutsideTokenTimeLimit()
var expectedValue = _fixture.GetRandomToken(1);
var otherValue = _fixture.GetRandomToken(1);

var firstValue = await sut.GetToken("key", () => Task.FromResult(otherValue)).ConfigureAwait(false);
var firstValue = await sut.GetToken(new TokenRequest { Scopes = "key"}, () => Task.FromResult(otherValue)).ConfigureAwait(false);
await Task.Delay(TimeSpan.FromMilliseconds(1500)).ConfigureAwait(false);
var secondValue = await sut.GetToken("key", () => Task.FromResult(expectedValue)).ConfigureAwait(false);
var secondValue = await sut.GetToken(new TokenRequest { Scopes = "key"}, () => Task.FromResult(expectedValue)).ConfigureAwait(false);

secondValue.Should().Be(expectedValue);
}
Expand Down
22 changes: 20 additions & 2 deletions KS.Fiks.Maskinporten.Client.Tests/MaskinportenClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ public async Task DoesNotSendRequestTwiceIfSecondCallIsWithinTimelimit()
ItExpr.IsAny<CancellationToken>());
}

[Fact]
public async Task DoesNotSendDelegatedAccessTokenRequestTwiceIfSecondCallIsWithinTimelimit()
{
const string consumerOrg = "999888999";
var sut = _fixture.CreateSut();

var token1 = await sut.GetDelegatedAccessToken(consumerOrg, _fixture.DefaultScopes).ConfigureAwait(false);
await Task.Delay(TimeSpan.FromMilliseconds(100)).ConfigureAwait(false);
var token2 = await sut.GetDelegatedAccessToken(consumerOrg, _fixture.DefaultScopes).ConfigureAwait(false);

token1.Should().Be(token2);
_fixture.HttpMessageHandleMock.Protected().Verify(
"SendAsync",
Times.Exactly(1),
ItExpr.Is<HttpRequestMessage>(req => true),
ItExpr.IsAny<CancellationToken>());
}

[Fact]
public async Task SendsRequestTwiceIfSecondCallIsOutsideTimelimit()
{
Expand Down Expand Up @@ -294,7 +312,7 @@ public async Task SendsHeaderCharsetUtf8()
req.Content.Headers.GetValues("Charset").Contains("utf-8")),
ItExpr.IsAny<CancellationToken>());
}

[Fact]
public async Task SendsHeaderConsumerOrgIfSet()
{
Expand All @@ -310,7 +328,7 @@ public async Task SendsHeaderConsumerOrgIfSet()
req.Content.Headers.GetValues("consumer_org").Contains(consumerOrg)),
ItExpr.IsAny<CancellationToken>());
}

[Fact]
public async Task DoesNotSendHeaderConsumerOrgIfNotSet()
{
Expand Down
2 changes: 1 addition & 1 deletion KS.Fiks.Maskinporten.Client/Cache/ITokenCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ namespace Ks.Fiks.Maskinporten.Client.Cache
{
public interface ITokenCache
{
Task<MaskinportenToken> GetToken(string tokenKey, Func<Task<MaskinportenToken>> tokenGetter);
Task<MaskinportenToken> GetToken(TokenRequest tokenRequest, Func<Task<MaskinportenToken>> tokenGetter);
}
}
27 changes: 14 additions & 13 deletions KS.Fiks.Maskinporten.Client/Cache/TokenCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,23 @@ namespace Ks.Fiks.Maskinporten.Client.Cache
{
public class TokenCache : ITokenCache, IDisposable
{
private readonly Dictionary<string, MaskinportenToken> _cacheDictionary;
private readonly Dictionary<TokenRequest, MaskinportenToken> _cacheDictionary;
private readonly SemaphoreSlim _mutex;

public TokenCache()
{
_cacheDictionary = new Dictionary<string, MaskinportenToken>();
_cacheDictionary = new Dictionary<TokenRequest, MaskinportenToken>();
_mutex = new SemaphoreSlim(1);
}

public async Task<MaskinportenToken> GetToken(string tokenKey, Func<Task<MaskinportenToken>> tokenFactory)
public async Task<MaskinportenToken> GetToken(TokenRequest tokenRequest, Func<Task<MaskinportenToken>> tokenFactory)
{
await _mutex.WaitAsync().ConfigureAwait(false);
try
{
return HasValidEntry(tokenKey)
? _cacheDictionary[tokenKey]
: await UpdateOrAddToken(tokenKey, tokenFactory).ConfigureAwait(false);
return HasValidEntry(tokenRequest)
? _cacheDictionary[tokenRequest]
: await UpdateOrAddToken(tokenRequest, tokenFactory).ConfigureAwait(false);
}
finally
{
Expand All @@ -46,31 +46,32 @@ protected virtual void Dispose(bool disposing)
}
}

private bool HasValidEntry(string tokenKey)
private bool HasValidEntry(TokenRequest tokenRequest)
{
if (!_cacheDictionary.ContainsKey(tokenKey))
if (!_cacheDictionary.ContainsKey(tokenRequest))
{
return false;
}

return !_cacheDictionary[tokenKey].IsExpiring();
return !_cacheDictionary[tokenRequest].IsExpiring();
}

private async Task<MaskinportenToken> UpdateOrAddToken(
string tokenKey,
TokenRequest tokenRequest,
Func<Task<MaskinportenToken>> tokenFactory)
{
var newToken = await tokenFactory().ConfigureAwait(false);
if (_cacheDictionary.ContainsKey(tokenKey))
if (_cacheDictionary.ContainsKey(tokenRequest))
{
_cacheDictionary[tokenKey] = newToken;
_cacheDictionary[tokenRequest] = newToken;
}
else
{
_cacheDictionary.Add(tokenKey, newToken);
_cacheDictionary.Add(tokenRequest, newToken);
}

return newToken;
}
}

}
3 changes: 3 additions & 0 deletions KS.Fiks.Maskinporten.Client/IMaskinportenClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@ public interface IMaskinportenClient
Task<MaskinportenToken> GetAccessToken(IEnumerable<string> scopes);

Task<MaskinportenToken> GetAccessToken(string scopes);

Task<MaskinportenToken> GetDelegatedAccessToken(string consumerOrg, IEnumerable<string> scopes);
Task<MaskinportenToken> GetDelegatedAccessToken(string consumerOrg, string scopes);
}
}
47 changes: 35 additions & 12 deletions KS.Fiks.Maskinporten.Client/MaskinportenClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,32 @@ public async Task<MaskinportenToken> GetAccessToken(IEnumerable<string> scopes)

public async Task<MaskinportenToken> GetAccessToken(string scopes)
{
return await _tokenCache.GetToken(
scopes,
async () => await GetNewAccessToken(scopes).ConfigureAwait(false))
.ConfigureAwait(false);
var tokenRequest = new TokenRequest
{
Scopes = scopes
};
return await GetAccessTokenForRequest(tokenRequest);
}

public async Task<MaskinportenToken> GetDelegatedAccessToken(string consumerOrg, IEnumerable<string> scopes)
{
return await GetDelegatedAccessToken(consumerOrg, ScopesAsString(scopes)).ConfigureAwait(false);
}

public async Task<MaskinportenToken> GetDelegatedAccessToken(string consumerOrg, string scopes)
{
return await GetAccessTokenForRequest(new TokenRequest
{
Scopes = scopes,
ConsumerOrg = consumerOrg
}).ConfigureAwait(false);
}

private async Task<MaskinportenToken> GetAccessTokenForRequest(TokenRequest tokenRequest)
{
return await this._tokenCache.GetToken(tokenRequest,
async () => await GetNewAccessToken(tokenRequest).ConfigureAwait(false)
).ConfigureAwait(false);
}

private static async Task<MaskinportenResponse> ReadResponse(HttpResponseMessage responseMessage)
Expand All @@ -56,15 +78,15 @@ private static async Task<MaskinportenResponse> ReadResponse(HttpResponseMessage
return JsonConvert.DeserializeObject<MaskinportenResponse>(responseAsJson);
}

private string ScopesAsString(IEnumerable<string> scopes)
private static string ScopesAsString(IEnumerable<string> scopes)
{
return string.Join(" ", scopes);
}

private async Task<MaskinportenToken> GetNewAccessToken(string scopes)
private async Task<MaskinportenToken> GetNewAccessToken(TokenRequest tokenRequest)
{
SetRequestHeaders();
var requestContent = CreateRequestContent(scopes);
var requestContent = CreateRequestContent(tokenRequest);

var tokenUri = new Uri(_configuration.TokenEndpoint);
var response = await _httpClient.PostAsync(tokenUri, requestContent).ConfigureAwait(false);
Expand All @@ -82,19 +104,20 @@ private void SetRequestHeaders()
};
}

private FormUrlEncodedContent CreateRequestContent(string scopes)
private FormUrlEncodedContent CreateRequestContent(TokenRequest tokenRequest)
{
var content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("grant_type", GrantType),
new KeyValuePair<string, string>("assertion", _tokenGenerator.CreateEncodedJwt(scopes, _configuration))
new KeyValuePair<string, string>("assertion", _tokenGenerator.CreateEncodedJwt(tokenRequest.Scopes, _configuration))
});

if (_configuration.ConsumerOrg != null)
var consumerOrg = tokenRequest.ConsumerOrg ?? this._configuration.ConsumerOrg;
if (consumerOrg != null)
{
content.Headers.Add("consumer_org", _configuration.ConsumerOrg);
content.Headers.Add("consumer_org", consumerOrg);
}

content.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeFromUrl);
content.Headers.Add("Charset", CharsetUtf8);

Expand Down
36 changes: 36 additions & 0 deletions KS.Fiks.Maskinporten.Client/TokenRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;

namespace Ks.Fiks.Maskinporten.Client.Cache
{
public class TokenRequest
{
public string Scopes { get; set; }

public string ConsumerOrg { get; set; }

public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}

if (ReferenceEquals(this, obj))
{
return true;
}

return obj.GetType() == GetType() && Equals((TokenRequest) obj);
}

public override int GetHashCode()
{
return HashCode.Combine(Scopes, ConsumerOrg);
}

private bool Equals(TokenRequest other)
{
return Scopes == other.Scopes && ConsumerOrg == other.ConsumerOrg;
}
}
}

0 comments on commit c44d3bf

Please sign in to comment.