microsoft/healthcare-shared-components

Public

mirrored from https://github.com/microsoft/healthcare-shared-componentsAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
main

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/Microsoft.Health.Client.UnitTests/CredentialProviderTests.cs

214lines · modecode

1// -------------------------------------------------------------------------------------------------
2// Copyright (c) Microsoft Corporation. All rights reserved.
3// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
4// -------------------------------------------------------------------------------------------------
5
6using System;
7using System.IdentityModel.Tokens.Jwt;
8using System.Net;
9using System.Net.Http;
10using System.Reflection;
11using System.Security.Cryptography;
12using System.Security.Cryptography.X509Certificates;
13using System.Threading;
14using System.Threading.Tasks;
15using Microsoft.Extensions.Options;
16using Microsoft.Health.Client.Authentication;
17using Microsoft.Health.Client.Authentication.Exceptions;
18using NSubstitute;
19using Xunit;
20
21namespace Microsoft.Health.Client.UnitTests;
22
23public class CredentialProviderTests
24{
25 [Fact]
26 public async Task GivenAnNonSetToken_WhenGetBearerTokenCalled_ThenBearerTokenFunctionIsCalled()
27 {
28 DateTime expirationTime = DateTime.UtcNow + TimeSpan.FromDays(1);
29
30 var credentialProvider = new TestCredentialProvider(JwtTokenHelpers.GenerateToken(expirationTime));
31 Assert.Null(credentialProvider.Token);
32 Assert.Equal(default, credentialProvider.TokenExpiration);
33
34 var token = await credentialProvider.GetBearerTokenAsync(cancellationToken: default);
35
36 Assert.Equal(token, credentialProvider.Token);
37
38 // JWT token expiration is limited to second precision
39 Assert.InRange(credentialProvider.TokenExpiration, expirationTime.AddSeconds(-1), expirationTime.AddSeconds(1));
40 }
41
42 [Fact]
43 public async Task InvalidOAuth2ClientCredential_RetrieveToken_ShouldThrowError()
44 {
45 using var response = new HttpResponseMessage
46 {
47 StatusCode = HttpStatusCode.BadRequest,
48 Content = new StringContent(@"{""error"": ""This is an error!""}"),
49 };
50
51 HttpMessageHandler mockHandler = GetMockMessageHandler(
52 Arg.Any<HttpRequestMessage>(),
53 response,
54 Arg.Any<CancellationToken>());
55
56 using var httpClient = new HttpClient(mockHandler);
57
58 var credentialConfiguration = new OAuth2ClientCredentialOptions(
59 new Uri("https://fakehost/connect/token"),
60 "invalid resource",
61 "invalid scope",
62 "invalid client id",
63 "invalid client secret");
64
65 var credentialProvider = new OAuth2ClientCredentialProvider(GetOptionsMonitor(credentialConfiguration), httpClient);
66 await Assert.ThrowsAsync<FailToRetrieveTokenException>(() => credentialProvider.GetBearerTokenAsync(cancellationToken: default));
67 }
68
69 [Fact]
70 public async Task InvalidOAuth2UserPasswordCredential_RetrieveToken_ShouldThrowError()
71 {
72 using var response = new HttpResponseMessage
73 {
74 StatusCode = HttpStatusCode.BadRequest,
75 Content = new StringContent(@"{""error"": ""This is an error!""}"),
76 };
77
78 HttpMessageHandler mockHandler = GetMockMessageHandler(
79 Arg.Any<HttpRequestMessage>(),
80 response,
81 Arg.Any<CancellationToken>());
82
83 using var httpClient = new HttpClient(mockHandler);
84
85 var credentialConfiguration = new OAuth2UserPasswordCredentialOptions(
86 new Uri("https://fakehost/connect/token"),
87 "invalid resource",
88 "invalid scope",
89 "invalid client id",
90 "invalid client secret",
91 "invalid username",
92 "invalid password");
93
94 var credentialProvider = new OAuth2UserPasswordCredentialProvider(GetOptionsMonitor(credentialConfiguration), httpClient);
95 await Assert.ThrowsAsync<FailToRetrieveTokenException>(() => credentialProvider.GetBearerTokenAsync(cancellationToken: default));
96 }
97
98 [Fact]
99 public async Task GivenANonExpiredToken_WhenGetBearerTokenCalled_ThenSameBearerTokenIsReturned()
100 {
101 DateTime initialExpiration = DateTime.UtcNow + TimeSpan.FromDays(1);
102 var initialToken = JwtTokenHelpers.GenerateToken(initialExpiration);
103 var credentialProvider = new TestCredentialProvider(initialToken);
104
105 // Returns the initialToken
106 var initialResult = await credentialProvider.GetBearerTokenAsync(cancellationToken: default);
107 Assert.Equal(initialToken, initialResult);
108
109 // Update the token that would be returned if BearerTokenFunction() was called
110 DateTime updatedExpiration = DateTime.UtcNow + TimeSpan.FromDays(1);
111 var secondToken = JwtTokenHelpers.GenerateToken(updatedExpiration);
112 credentialProvider.EncodedToken = secondToken;
113
114 // Should return the initialToken since it is not within the expiration window
115 var secondResult = await credentialProvider.GetBearerTokenAsync(cancellationToken: default);
116
117 Assert.Equal(initialResult, secondResult);
118 }
119
120 [Fact]
121 public async Task GivenAnExpiringToken_WhenGetBearerTokenCalled_ThenNewBearerTokenIsReturned()
122 {
123 DateTime initialExpiration = DateTime.UtcNow + TimeSpan.FromMinutes(4);
124 var initialToken = JwtTokenHelpers.GenerateToken(initialExpiration);
125 var credentialProvider = new TestCredentialProvider(initialToken);
126
127 // Returns the initialToken
128 var initialResult = await credentialProvider.GetBearerTokenAsync(cancellationToken: default);
129 Assert.Equal(initialToken, initialResult);
130
131 // Update the token that will be returned since the initial token is within the expiration window
132 DateTime updatedExpiration = DateTime.UtcNow + TimeSpan.FromDays(1);
133 var secondToken = JwtTokenHelpers.GenerateToken(updatedExpiration);
134 credentialProvider.EncodedToken = secondToken;
135
136 // Should return the initialToken since it is not within the expiration window
137 var secondResult = await credentialProvider.GetBearerTokenAsync(cancellationToken: default);
138
139 Assert.Equal(secondToken, secondResult);
140 Assert.NotEqual(initialResult, secondResult);
141 }
142
143 [Fact]
144 public void GivenACertificateWithAPrivateKey_WhenGeneratingClientAssertion_ThenPrivateKeyNotIncludedInX5c()
145 {
146 string clientId = Guid.NewGuid().ToString();
147 using var certificate = BuildSelfSignedServerPfxCertificate(clientId);
148
149 Assert.True(certificate.HasPrivateKey);
150
151 var assertion = OAuth2ClientCertificateCredentialProvider.GenerateClientAssertion(clientId, certificate, new Uri("https://example.com/token"));
152
153 var handler = new JwtSecurityTokenHandler();
154 var token = handler.ReadToken(assertion) as JwtSecurityToken;
155
156 Assert.NotNull(token?.Header.X5c);
157 byte[] x5CBytes = Convert.FromBase64String(token.Header.X5c);
158
159#if NET9_0_OR_GREATER
160 using X509Certificate2 x5CCertificate = X509CertificateLoader.LoadCertificate(x5CBytes);
161#else
162 using var x5CCertificate = new X509Certificate2(x5CBytes);
163#endif
164
165 Assert.Equal($"CN={clientId}", x5CCertificate.SubjectName.Name);
166 Assert.False(x5CCertificate.HasPrivateKey);
167
168 static X509Certificate2 BuildSelfSignedServerPfxCertificate(string certificateName)
169 {
170 var sanBuilder = new SubjectAlternativeNameBuilder();
171 sanBuilder.AddIpAddress(IPAddress.Loopback);
172 sanBuilder.AddIpAddress(IPAddress.IPv6Loopback);
173 sanBuilder.AddDnsName("example.com");
174 sanBuilder.AddDnsName(Environment.MachineName);
175
176 var distinguishedName = new X500DistinguishedName($"CN={certificateName}");
177
178 using var rsa = RSA.Create(2048);
179 var request = new CertificateRequest(distinguishedName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
180
181 request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false));
182 request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
183 request.CertificateExtensions.Add(sanBuilder.Build());
184
185 X509Certificate2 certificate = request.CreateSelfSigned(new DateTimeOffset(DateTime.UtcNow.AddDays(-1)), new DateTimeOffset(DateTime.UtcNow.AddDays(1)));
186
187#if NET9_0_OR_GREATER
188 return X509CertificateLoader.LoadPkcs12(certificate.Export(X509ContentType.Pfx, "exampleString"), "exampleString", X509KeyStorageFlags.Exportable);
189#else
190 return new X509Certificate2(certificate.Export(X509ContentType.Pfx, "exampleString"), "exampleString", X509KeyStorageFlags.Exportable);
191#endif
192 }
193 }
194
195 private static HttpMessageHandler GetMockMessageHandler(HttpRequestMessage requestMessage, HttpResponseMessage responseMessage, CancellationToken cancellationToken)
196 {
197 HttpMessageHandler mockHandler = Substitute.For<HttpMessageHandler>();
198
199 typeof(HttpMessageHandler)
200 .GetMethod("SendAsync", BindingFlags.Instance | BindingFlags.NonPublic)
201 .Invoke(mockHandler, new object[] { requestMessage, cancellationToken })
202 .Returns(Task.FromResult(responseMessage));
203
204 return mockHandler;
205 }
206
207 private static IOptionsMonitor<T> GetOptionsMonitor<T>(T configuration)
208 {
209 var optionsMonitor = Substitute.For<IOptionsMonitor<T>>();
210 optionsMonitor.CurrentValue.Returns(configuration);
211 optionsMonitor.Get(default).ReturnsForAnyArgs(configuration);
212 return optionsMonitor;
213 }
214}
215