Skip to content

Commit 47f1fc5

Browse files
authored
Fix bug 68 around issuer validation and introduce unit testing (#117)
* Fix bug 68 and introduce unit testing * Make a few types internal * Revert version bump
1 parent 37239c0 commit 47f1fc5

11 files changed

+374
-99
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
using Microsoft.Identity.Web.Resource;
2+
using Microsoft.IdentityModel.Tokens;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.IdentityModel.Tokens.Jwt;
6+
using System.Security.Claims;
7+
using Xunit;
8+
9+
namespace Microsoft.Identity.Web.Test
10+
{
11+
public class AadIssuerValidatorTests
12+
{
13+
private const string Tid = "9188040d-6c67-4c5b-b112-36a304b66dad";
14+
private static readonly string Iss = $"https://login.microsoftonline.com/{Tid}/v2.0";
15+
private static readonly IEnumerable<string> s_aliases = new[] { "login.microsoftonline.com", "sts.windows.net" };
16+
17+
[Fact]
18+
public void NullArg()
19+
{
20+
// Arrange
21+
AadIssuerValidator validator = new AadIssuerValidator(s_aliases);
22+
var jwtSecurityToken = new JwtSecurityToken();
23+
var validationParams = new TokenValidationParameters();
24+
25+
// Act and Assert
26+
Assert.Throws<ArgumentNullException>(() => validator.Validate(null, jwtSecurityToken, validationParams));
27+
Assert.Throws<ArgumentNullException>(() => validator.Validate("", jwtSecurityToken, validationParams));
28+
Assert.Throws<ArgumentNullException>(() => validator.Validate(Iss, null, validationParams));
29+
Assert.Throws<ArgumentNullException>(() => validator.Validate(Iss, jwtSecurityToken, null));
30+
}
31+
32+
[Fact]
33+
public void PassingValidation()
34+
{
35+
// Arrange
36+
AadIssuerValidator validator = new AadIssuerValidator(s_aliases);
37+
Claim issClaim = new Claim("tid", Tid);
38+
Claim tidClaim = new Claim("iss", Iss);
39+
JwtSecurityToken jwtSecurityToken = new JwtSecurityToken(issuer: Iss, claims: new[] { issClaim, tidClaim });
40+
41+
// Act & Assert
42+
validator.Validate(Iss, jwtSecurityToken,
43+
new TokenValidationParameters() { ValidIssuers = new[] { "https://login.microsoftonline.com/{tenantid}/v2.0" } });
44+
}
45+
46+
47+
[Fact]
48+
public void TokenValidationParameters_ValidIssuer()
49+
{
50+
// Arrange
51+
AadIssuerValidator validator = new AadIssuerValidator(s_aliases);
52+
Claim issClaim = new Claim("tid", Tid);
53+
Claim tidClaim = new Claim("iss", Iss);
54+
JwtSecurityToken jwtSecurityToken = new JwtSecurityToken(issuer: Iss, claims: new[] { issClaim, tidClaim });
55+
56+
// Act & Assert
57+
validator.Validate(Iss, jwtSecurityToken,
58+
new TokenValidationParameters() { ValidIssuer = "https://login.microsoftonline.com/{tenantid}/v2.0" });
59+
}
60+
61+
[Fact]
62+
public void ValidationFails_NoTidClaimInJwt()
63+
{
64+
// Arrange
65+
AadIssuerValidator validator = new AadIssuerValidator(s_aliases);
66+
Claim issClaim = new Claim("iss", Iss);
67+
68+
JwtSecurityToken noTidJwt = new JwtSecurityToken(issuer: Iss, claims: new[] { issClaim });
69+
70+
// Act & Assert
71+
Assert.Throws<SecurityTokenInvalidIssuerException>(() =>
72+
validator.Validate(
73+
Iss,
74+
noTidJwt,
75+
new TokenValidationParameters() { ValidIssuers = new[] { "https://login.microsoftonline.com/{tenantid}/v2.0" } }));
76+
}
77+
78+
[Fact]
79+
public void ValidationFails_BadTidClaimInJwt()
80+
{
81+
// Arrange
82+
AadIssuerValidator validator = new AadIssuerValidator(s_aliases);
83+
Claim issClaim = new Claim("iss", Iss);
84+
Claim tidClaim = new Claim("tid", "9188040d-0000-4c5b-b112-36a304b66dad");
85+
86+
JwtSecurityToken noTidJwt = new JwtSecurityToken(issuer: Iss, claims: new[] { issClaim, tidClaim });
87+
88+
// Act & Assert
89+
Assert.Throws<SecurityTokenInvalidIssuerException>(() =>
90+
validator.Validate(
91+
Iss,
92+
noTidJwt,
93+
new TokenValidationParameters() { ValidIssuers = new[] { "https://login.microsoftonline.com/{tenantid}/v2.0" } }));
94+
}
95+
96+
[Fact]
97+
public void MultipleIssuers_NoneMatch()
98+
{
99+
// Arrange
100+
AadIssuerValidator validator = new AadIssuerValidator(s_aliases);
101+
Claim issClaim = new Claim("iss", Iss);
102+
Claim tidClaim = new Claim("tid", Tid);
103+
104+
JwtSecurityToken noTidJwt = new JwtSecurityToken(issuer: Iss, claims: new[] { issClaim, tidClaim });
105+
106+
// Act & Assert
107+
Assert.Throws<SecurityTokenInvalidIssuerException>(() =>
108+
validator.Validate(
109+
Iss,
110+
noTidJwt,
111+
new TokenValidationParameters()
112+
{
113+
ValidIssuers = new[] {
114+
"https://host1/{tenantid}/v2.0",
115+
"https://host2/{tenantid}/v2.0"
116+
}
117+
}));
118+
}
119+
120+
121+
[Fact] // Regression test for https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2/issues/68
122+
public void ValidationFails_BadIssuerClaimInJwt()
123+
{
124+
// Arrange
125+
string iss = $"https://badissuer/{Tid}/v2.0";
126+
AadIssuerValidator validator = new AadIssuerValidator(s_aliases);
127+
Claim issClaim = new Claim("iss", iss);
128+
Claim tidClaim = new Claim("tid", Tid);
129+
130+
JwtSecurityToken noTidJwt = new JwtSecurityToken(issuer: Iss, claims: new[] { issClaim, tidClaim });
131+
132+
// Act & Assert
133+
Assert.Throws<SecurityTokenInvalidIssuerException>(() =>
134+
validator.Validate(
135+
iss,
136+
noTidJwt,
137+
new TokenValidationParameters() { ValidIssuers = new[] { "https://login.microsoftonline.com/{tenantid}/v2.0" } }));
138+
}
139+
}
140+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netcoreapp2.2</TargetFramework>
5+
6+
<IsPackable>false</IsPackable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
11+
<PackageReference Include="xunit" Version="2.4.0" />
12+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
13+
</ItemGroup>
14+
15+
<ItemGroup>
16+
<ProjectReference Include="..\Microsoft.Identity.Web\Microsoft.Identity.Web.csproj" />
17+
</ItemGroup>
18+
19+
</Project>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/************************************************************************************************
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2015 Microsoft Corporation
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.
23+
***********************************************************************************************/
24+
25+
using System;
26+
using System.Threading;
27+
using System.Threading.Tasks;
28+
using Microsoft.IdentityModel.Protocols;
29+
using Newtonsoft.Json;
30+
31+
namespace Microsoft.Identity.Web.InstanceDiscovery
32+
{
33+
/// <summary>
34+
/// An implementation of IConfigurationRetriever geared towards Azure AD issuers metadata />
35+
/// </summary>
36+
internal class IssuerConfigurationRetriever : IConfigurationRetriever<IssuerMetadata>
37+
{
38+
/// <summary>Retrieves a populated configuration given an address and an <see cref="T:Microsoft.IdentityModel.Protocols.IDocumentRetriever"/>.</summary>
39+
/// <param name="address">Address of the discovery document.</param>
40+
/// <param name="retriever">The <see cref="T:Microsoft.IdentityModel.Protocols.IDocumentRetriever"/> to use to read the discovery document.</param>
41+
/// <param name="cancel">A cancellation token that can be used by other objects or threads to receive notice of cancellation. <see cref="T:System.Threading.CancellationToken"/>.</param>
42+
/// <returns></returns>
43+
/// <exception cref="ArgumentNullException">if <paramref name="address"/> is null or empty.
44+
/// or
45+
/// retriever - No metadata document retriever is provided</exception>
46+
public async Task<IssuerMetadata> GetConfigurationAsync(string address, IDocumentRetriever retriever, CancellationToken cancel)
47+
{
48+
if (string.IsNullOrEmpty(address))
49+
throw new ArgumentNullException(nameof(address), $"Azure AD Issuer metadata address url is required");
50+
51+
if (retriever == null)
52+
throw new ArgumentNullException(nameof(retriever), $"No metadata document retriever is provided");
53+
54+
string doc = await retriever.GetDocumentAsync(address, cancel).ConfigureAwait(false);
55+
return JsonConvert.DeserializeObject<IssuerMetadata>(doc);
56+
}
57+
}
58+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/************************************************************************************************
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2015 Microsoft Corporation
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.
23+
***********************************************************************************************/
24+
25+
using System.Collections.Generic;
26+
using Newtonsoft.Json;
27+
28+
namespace Microsoft.Identity.Web.InstanceDiscovery
29+
{
30+
/// <summary>
31+
/// Model class to hold information parsed from the Azure AD issuer endpoint
32+
/// </summary>
33+
internal class IssuerMetadata
34+
{
35+
[JsonProperty(PropertyName = "tenant_discovery_endpoint")]
36+
public string TenantDiscoveryEndpoint { get; set; }
37+
38+
[JsonProperty(PropertyName = "api-version")]
39+
public string ApiVersion { get; set; }
40+
41+
[JsonProperty(PropertyName = "metadata")]
42+
public List<Metadata> Metadata { get; set; }
43+
}
44+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/************************************************************************************************
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2015 Microsoft Corporation
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.
23+
***********************************************************************************************/
24+
25+
using System.Collections.Generic;
26+
using Newtonsoft.Json;
27+
28+
namespace Microsoft.Identity.Web.InstanceDiscovery
29+
{
30+
/// <summary>
31+
/// Model child class to hold alias information parsed from the Azure AD issuer endpoint.
32+
/// </summary>
33+
internal class Metadata
34+
{
35+
[JsonProperty(PropertyName = "preferred_network")]
36+
public string PreferredNetwork { get; set; }
37+
38+
[JsonProperty(PropertyName = "preferred_cache")]
39+
public string PreferredCache { get; set; }
40+
41+
[JsonProperty(PropertyName = "aliases")]
42+
public List<string> Aliases { get; set; }
43+
}
44+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Runtime.CompilerServices;
5+
6+
[assembly: InternalsVisibleTo("Microsoft.Identity.Web.Test")]

Microsoft.Identity.Web/Microsoft.Identity.Web.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>netcoreapp2.2</TargetFramework>

Microsoft.Identity.Web/Microsoft.Identity.Web.sln

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio 15
4-
VisualStudioVersion = 15.0.28307.539
3+
# Visual Studio Version 16
4+
VisualStudioVersion = 16.0.29009.5
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web", "Microsoft.Identity.Web.csproj", "{BD795319-75B8-4251-AE92-8DD5A807C28E}"
77
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Web.Test", "..\Microsoft.Identity.Web.Test\Microsoft.Identity.Web.Test.csproj", "{9F00FB40-D67A-4E39-8167-C47922DA96A2}"
9+
EndProject
810
Global
911
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1012
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +17,10 @@ Global
1517
{BD795319-75B8-4251-AE92-8DD5A807C28E}.Debug|Any CPU.Build.0 = Debug|Any CPU
1618
{BD795319-75B8-4251-AE92-8DD5A807C28E}.Release|Any CPU.ActiveCfg = Release|Any CPU
1719
{BD795319-75B8-4251-AE92-8DD5A807C28E}.Release|Any CPU.Build.0 = Release|Any CPU
20+
{9F00FB40-D67A-4E39-8167-C47922DA96A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{9F00FB40-D67A-4E39-8167-C47922DA96A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{9F00FB40-D67A-4E39-8167-C47922DA96A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
23+
{9F00FB40-D67A-4E39-8167-C47922DA96A2}.Release|Any CPU.Build.0 = Release|Any CPU
1824
EndGlobalSection
1925
GlobalSection(SolutionProperties) = preSolution
2026
HideSolutionNode = FALSE

0 commit comments

Comments
 (0)