diff --git a/KS.Fiks.Maskinporten.Client.Tests/Cache/TokenCacheTests.cs b/KS.Fiks.Maskinporten.Client.Tests/Cache/TokenCacheTests.cs index 6d30953..cc3ba6a 100644 --- a/KS.Fiks.Maskinporten.Client.Tests/Cache/TokenCacheTests.cs +++ b/KS.Fiks.Maskinporten.Client.Tests/Cache/TokenCacheTests.cs @@ -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 @@ -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); } @@ -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); } @@ -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); } diff --git a/KS.Fiks.Maskinporten.Client.Tests/MaskinportenClientTests.cs b/KS.Fiks.Maskinporten.Client.Tests/MaskinportenClientTests.cs index fb2a816..a6e4d3b 100644 --- a/KS.Fiks.Maskinporten.Client.Tests/MaskinportenClientTests.cs +++ b/KS.Fiks.Maskinporten.Client.Tests/MaskinportenClientTests.cs @@ -75,6 +75,24 @@ public async Task DoesNotSendRequestTwiceIfSecondCallIsWithinTimelimit() ItExpr.IsAny()); } + [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(req => true), + ItExpr.IsAny()); + } + [Fact] public async Task SendsRequestTwiceIfSecondCallIsOutsideTimelimit() { @@ -294,7 +312,7 @@ public async Task SendsHeaderCharsetUtf8() req.Content.Headers.GetValues("Charset").Contains("utf-8")), ItExpr.IsAny()); } - + [Fact] public async Task SendsHeaderConsumerOrgIfSet() { @@ -310,7 +328,7 @@ public async Task SendsHeaderConsumerOrgIfSet() req.Content.Headers.GetValues("consumer_org").Contains(consumerOrg)), ItExpr.IsAny()); } - + [Fact] public async Task DoesNotSendHeaderConsumerOrgIfNotSet() { diff --git a/KS.Fiks.Maskinporten.Client/Cache/ITokenCache.cs b/KS.Fiks.Maskinporten.Client/Cache/ITokenCache.cs index c547039..8bc7657 100644 --- a/KS.Fiks.Maskinporten.Client/Cache/ITokenCache.cs +++ b/KS.Fiks.Maskinporten.Client/Cache/ITokenCache.cs @@ -5,6 +5,6 @@ namespace Ks.Fiks.Maskinporten.Client.Cache { public interface ITokenCache { - Task GetToken(string tokenKey, Func> tokenGetter); + Task GetToken(TokenRequest tokenRequest, Func> tokenGetter); } } \ No newline at end of file diff --git a/KS.Fiks.Maskinporten.Client/Cache/TokenCache.cs b/KS.Fiks.Maskinporten.Client/Cache/TokenCache.cs index 7898261..6a89f2c 100644 --- a/KS.Fiks.Maskinporten.Client/Cache/TokenCache.cs +++ b/KS.Fiks.Maskinporten.Client/Cache/TokenCache.cs @@ -8,23 +8,23 @@ namespace Ks.Fiks.Maskinporten.Client.Cache { public class TokenCache : ITokenCache, IDisposable { - private readonly Dictionary _cacheDictionary; + private readonly Dictionary _cacheDictionary; private readonly SemaphoreSlim _mutex; public TokenCache() { - _cacheDictionary = new Dictionary(); + _cacheDictionary = new Dictionary(); _mutex = new SemaphoreSlim(1); } - public async Task GetToken(string tokenKey, Func> tokenFactory) + public async Task GetToken(TokenRequest tokenRequest, Func> 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 { @@ -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 UpdateOrAddToken( - string tokenKey, + TokenRequest tokenRequest, Func> 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; } } + } \ No newline at end of file diff --git a/KS.Fiks.Maskinporten.Client/IMaskinportenClient.cs b/KS.Fiks.Maskinporten.Client/IMaskinportenClient.cs index 4d18cb1..45ed381 100644 --- a/KS.Fiks.Maskinporten.Client/IMaskinportenClient.cs +++ b/KS.Fiks.Maskinporten.Client/IMaskinportenClient.cs @@ -8,5 +8,8 @@ public interface IMaskinportenClient Task GetAccessToken(IEnumerable scopes); Task GetAccessToken(string scopes); + + Task GetDelegatedAccessToken(string consumerOrg, IEnumerable scopes); + Task GetDelegatedAccessToken(string consumerOrg, string scopes); } } \ No newline at end of file diff --git a/KS.Fiks.Maskinporten.Client/MaskinportenClient.cs b/KS.Fiks.Maskinporten.Client/MaskinportenClient.cs index 7b6ebae..ba7b4ef 100644 --- a/KS.Fiks.Maskinporten.Client/MaskinportenClient.cs +++ b/KS.Fiks.Maskinporten.Client/MaskinportenClient.cs @@ -44,10 +44,32 @@ public async Task GetAccessToken(IEnumerable scopes) public async Task 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 GetDelegatedAccessToken(string consumerOrg, IEnumerable scopes) + { + return await GetDelegatedAccessToken(consumerOrg, ScopesAsString(scopes)).ConfigureAwait(false); + } + + public async Task GetDelegatedAccessToken(string consumerOrg, string scopes) + { + return await GetAccessTokenForRequest(new TokenRequest + { + Scopes = scopes, + ConsumerOrg = consumerOrg + }).ConfigureAwait(false); + } + + private async Task GetAccessTokenForRequest(TokenRequest tokenRequest) + { + return await this._tokenCache.GetToken(tokenRequest, + async () => await GetNewAccessToken(tokenRequest).ConfigureAwait(false) + ).ConfigureAwait(false); } private static async Task ReadResponse(HttpResponseMessage responseMessage) @@ -56,15 +78,15 @@ private static async Task ReadResponse(HttpResponseMessage return JsonConvert.DeserializeObject(responseAsJson); } - private string ScopesAsString(IEnumerable scopes) + private static string ScopesAsString(IEnumerable scopes) { return string.Join(" ", scopes); } - private async Task GetNewAccessToken(string scopes) + private async Task 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); @@ -82,19 +104,20 @@ private void SetRequestHeaders() }; } - private FormUrlEncodedContent CreateRequestContent(string scopes) + private FormUrlEncodedContent CreateRequestContent(TokenRequest tokenRequest) { var content = new FormUrlEncodedContent(new List>() { new KeyValuePair("grant_type", GrantType), - new KeyValuePair("assertion", _tokenGenerator.CreateEncodedJwt(scopes, _configuration)) + new KeyValuePair("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); diff --git a/KS.Fiks.Maskinporten.Client/TokenRequest.cs b/KS.Fiks.Maskinporten.Client/TokenRequest.cs new file mode 100644 index 0000000..3861f77 --- /dev/null +++ b/KS.Fiks.Maskinporten.Client/TokenRequest.cs @@ -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; + } + } +} \ No newline at end of file