Skip to content

Commit 36fb5f5

Browse files
Add optional login and domain hints to SignIn method (#3330)
* Added new optional paramters in the sign in function. Removed the old public api signatire and added new one. * Modeified the SignIn method. Added some tests to test the changes * Removed unwanted test. Added the project to the solution * Update tests/Microsoft.Identity.Web.UI.Test/Microsoft.Identity.Web.UI.Test.csproj Co-authored-by: Westin Musser <127992899+westin-m@users.noreply.github.com> * Update tests/Microsoft.Identity.Web.UI.Test/Microsoft.Identity.Web.UI.Test.csproj Co-authored-by: Westin Musser <127992899+westin-m@users.noreply.github.com> * Update src/Microsoft.Identity.Web.UI/Areas/MicrosoftIdentity/Controllers/AccountController.cs Co-authored-by: Westin Musser <127992899+westin-m@users.noreply.github.com> * Update src/Microsoft.Identity.Web.UI/Areas/MicrosoftIdentity/Controllers/AccountController.cs Co-authored-by: Westin Musser <127992899+westin-m@users.noreply.github.com> * NIT: removed extra lines --------- Co-authored-by: Westin Musser <127992899+westin-m@users.noreply.github.com>
1 parent ce49726 commit 36fb5f5

6 files changed

Lines changed: 159 additions & 3 deletions

File tree

Microsoft.Identity.Web.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OidcIdpSignedAssertionProvi
162162
EndProject
163163
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Web.OidcFIC", "src\Microsoft.Identity.Web.OidcFIC\Microsoft.Identity.Web.OidcFIC.csproj", "{8DA7A2C6-00D4-4CF1-8145-448D7B7B4E5A}"
164164
EndProject
165+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Web.UI.Test", "tests\Microsoft.Identity.Web.UI.Test\Microsoft.Identity.Web.UI.Test.csproj", "{CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8}"
166+
EndProject
165167
Global
166168
GlobalSection(SolutionConfigurationPlatforms) = preSolution
167169
Debug|Any CPU = Debug|Any CPU
@@ -381,6 +383,10 @@ Global
381383
{8DA7A2C6-00D4-4CF1-8145-448D7B7B4E5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
382384
{8DA7A2C6-00D4-4CF1-8145-448D7B7B4E5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
383385
{8DA7A2C6-00D4-4CF1-8145-448D7B7B4E5A}.Release|Any CPU.Build.0 = Release|Any CPU
386+
{CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
387+
{CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
388+
{CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
389+
{CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8}.Release|Any CPU.Build.0 = Release|Any CPU
384390
EndGlobalSection
385391
GlobalSection(SolutionProperties) = preSolution
386392
HideSolutionNode = FALSE
@@ -454,6 +460,7 @@ Global
454460
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {7786D2DD-9EE4-42E1-B587-740A2E15C41D}
455461
{E927D215-A96C-626C-9A1A-CF99876FE7B4} = {45B20A78-91F8-4DD2-B9AD-F12D3A93536C}
456462
{8DA7A2C6-00D4-4CF1-8145-448D7B7B4E5A} = {1DDE1AAC-5AE6-4725-94B6-A26C58D3423F}
463+
{CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8} = {B4E72F1C-603F-437C-AAA1-153A604CD34A}
457464
EndGlobalSection
458465
GlobalSection(ExtensibilityGlobals) = postSolution
459466
SolutionGuid = {104367F1-CE75-4F40-B32F-F14853973187}

src/Microsoft.Identity.Web.UI/Areas/MicrosoftIdentity/Controllers/AccountController.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,15 @@ public AccountController(IOptionsMonitor<MicrosoftIdentityOptions> microsoftIden
3939
/// </summary>
4040
/// <param name="scheme">Authentication scheme.</param>
4141
/// <param name="redirectUri">Redirect URI.</param>
42+
/// <param name="loginHint">Login hint (user's email address).</param>
43+
/// <param name="domainHint">Domain hint.</param>
4244
/// <returns>Challenge generating a redirect to Azure AD to sign in the user.</returns>
4345
[HttpGet("{scheme?}")]
4446
public IActionResult SignIn(
4547
[FromRoute] string scheme,
46-
[FromQuery] string redirectUri)
48+
[FromQuery] string redirectUri,
49+
[FromQuery] string? loginHint = null,
50+
[FromQuery] string? domainHint = null)
4751
{
4852
scheme ??= OpenIdConnectDefaults.AuthenticationScheme;
4953
string redirect;
@@ -55,9 +59,18 @@ public IActionResult SignIn(
5559
{
5660
redirect = Url.Content("~/")!;
5761
}
62+
var authProps = new AuthenticationProperties { RedirectUri = redirect };
63+
if (!string.IsNullOrEmpty(loginHint))
64+
{
65+
authProps.Parameters[Constants.LoginHint] = loginHint;
66+
}
5867

68+
if (!string.IsNullOrEmpty(domainHint))
69+
{
70+
authProps.Parameters[Constants.DomainHint] = domainHint;
71+
}
5972
return Challenge(
60-
new AuthenticationProperties { RedirectUri = redirect },
73+
authProps,
6174
scheme);
6275
}
6376

src/Microsoft.Identity.Web.UI/PublicAPI.Shipped.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ Microsoft.Identity.Web.UI.Areas.MicrosoftIdentity.Controllers.AccountController.
44
Microsoft.Identity.Web.UI.Areas.MicrosoftIdentity.Controllers.AccountController.Challenge(string! redirectUri, string! scope, string! loginHint, string! domainHint, string! claims, string! policy, string! scheme) -> Microsoft.AspNetCore.Mvc.IActionResult!
55
Microsoft.Identity.Web.UI.Areas.MicrosoftIdentity.Controllers.AccountController.EditProfile(string! scheme) -> System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult!>!
66
Microsoft.Identity.Web.UI.Areas.MicrosoftIdentity.Controllers.AccountController.ResetPassword(string! scheme) -> Microsoft.AspNetCore.Mvc.IActionResult!
7-
Microsoft.Identity.Web.UI.Areas.MicrosoftIdentity.Controllers.AccountController.SignIn(string! scheme, string! redirectUri) -> Microsoft.AspNetCore.Mvc.IActionResult!
87
Microsoft.Identity.Web.UI.Areas.MicrosoftIdentity.Controllers.AccountController.SignOut(string! scheme) -> Microsoft.AspNetCore.Mvc.IActionResult!
98
Microsoft.Identity.Web.UI.Areas.MicrosoftIdentity.Pages.Account.AccessDeniedModel
109
Microsoft.Identity.Web.UI.Areas.MicrosoftIdentity.Pages.Account.AccessDeniedModel.AccessDeniedModel() -> void
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Microsoft.Identity.Web.UI.Areas.MicrosoftIdentity.Controllers.AccountController.SignIn(string! scheme, string! redirectUri, string? loginHint = null, string? domainHint = null) -> Microsoft.AspNetCore.Mvc.IActionResult!
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
using System;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Authentication;
6+
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.AspNetCore.Mvc;
9+
using Microsoft.AspNetCore.Mvc.Routing;
10+
using Microsoft.Extensions.Options;
11+
using Microsoft.Identity.Web.UI.Areas.MicrosoftIdentity.Controllers;
12+
using NSubstitute;
13+
using Xunit;
14+
15+
namespace Microsoft.Identity.Web.UI.Test.Areas.MicrosoftIdentity.Controllers
16+
{
17+
public class AccountControllerTests
18+
{
19+
private readonly IOptionsMonitor<MicrosoftIdentityOptions> _optionsMonitorMock;
20+
private readonly AccountController _accountController;
21+
22+
public AccountControllerTests()
23+
{
24+
_optionsMonitorMock = Substitute.For<IOptionsMonitor<MicrosoftIdentityOptions>>();
25+
_optionsMonitorMock.Get(Arg.Any<string>()).Returns(new MicrosoftIdentityOptions());
26+
_accountController = new AccountController(_optionsMonitorMock);
27+
var urlHelperMock = Substitute.For<IUrlHelper>();
28+
urlHelperMock.IsLocalUrl(Arg.Any<string>()).Returns(true);
29+
urlHelperMock.Content("~/").Returns("/");
30+
_accountController.Url = urlHelperMock;
31+
_accountController.ControllerContext = new ControllerContext
32+
{
33+
HttpContext = new DefaultHttpContext()
34+
};
35+
}
36+
37+
[Fact]
38+
public void SignIn_WithLoginHintAndDomainHint_AddsToAuthProperties()
39+
{
40+
// Arrange
41+
string scheme = OpenIdConnectDefaults.AuthenticationScheme;
42+
string redirectUri = "https://localhost/redirect";
43+
string loginHint = "user@example.com";
44+
string domainHint = "contoso.com";
45+
46+
// Act
47+
var result = _accountController.SignIn(scheme, redirectUri, loginHint, domainHint);
48+
49+
// Assert
50+
var challengeResult = Assert.IsType<ChallengeResult>(result);
51+
Assert.Single(challengeResult.AuthenticationSchemes);
52+
Assert.Equal(scheme, challengeResult.AuthenticationSchemes[0]);
53+
54+
var authProps = challengeResult.Properties;
55+
Assert.NotNull(authProps);
56+
Assert.Equal(redirectUri, authProps.RedirectUri);
57+
Assert.True(authProps.Parameters.ContainsKey(Constants.LoginHint));
58+
Assert.Equal(loginHint, authProps.Parameters[Constants.LoginHint]);
59+
Assert.True(authProps.Parameters.ContainsKey(Constants.DomainHint));
60+
Assert.Equal(domainHint, authProps.Parameters[Constants.DomainHint]);
61+
}
62+
63+
[Fact]
64+
public void SignIn_WithoutLoginHintAndDomainHint_DoesNotAddToAuthProperties()
65+
{
66+
// Arrange
67+
string scheme = OpenIdConnectDefaults.AuthenticationScheme;
68+
string redirectUri = "https://localhost/redirect";
69+
70+
// Act
71+
var result = _accountController.SignIn(scheme, redirectUri);
72+
73+
// Assert
74+
var challengeResult = Assert.IsType<ChallengeResult>(result);
75+
Assert.Single(challengeResult.AuthenticationSchemes);
76+
Assert.Equal(scheme, challengeResult.AuthenticationSchemes[0]);
77+
78+
var authProps = challengeResult.Properties;
79+
Assert.NotNull(authProps);
80+
Assert.Equal(redirectUri, authProps.RedirectUri);
81+
Assert.False(authProps.Parameters.ContainsKey(Constants.LoginHint));
82+
Assert.False(authProps.Parameters.ContainsKey(Constants.DomainHint));
83+
}
84+
85+
[Fact]
86+
public void SignIn_WithInvalidRedirectUri_UsesDefaultRedirectUri()
87+
{
88+
// Arrange
89+
string scheme = OpenIdConnectDefaults.AuthenticationScheme;
90+
string redirectUri = "https://external-site.com";
91+
string loginHint = "user@example.com";
92+
string domainHint = "contoso.com";
93+
94+
var urlHelperMock = Substitute.For<IUrlHelper>();
95+
urlHelperMock.IsLocalUrl(redirectUri).Returns(false);
96+
urlHelperMock.Content("~/").Returns("/");
97+
_accountController.Url = urlHelperMock;
98+
99+
var result = _accountController.SignIn(scheme, redirectUri, loginHint, domainHint);
100+
var challengeResult = Assert.IsType<ChallengeResult>(result);
101+
var authProps = challengeResult.Properties;
102+
Assert.NotNull(authProps); // Ensure authProps is not null
103+
Assert.Equal("/", authProps.RedirectUri);
104+
}
105+
}
106+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
5+
<IsPackable>false</IsPackable>
6+
<LangVersion>latest</LangVersion>
7+
<Nullable>enable</Nullable>
8+
<ImplicitUsings>enable</ImplicitUsings>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
13+
<PackageReference Include="NSubstitute" Version="5.3.0" />
14+
<PackageReference Include="xunit" Version="2.9.3" />
15+
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
16+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
17+
<PrivateAssets>all</PrivateAssets>
18+
</PackageReference>
19+
<PackageReference Include="coverlet.collector" Version="6.0.0">
20+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
21+
<PrivateAssets>all</PrivateAssets>
22+
</PackageReference>
23+
</ItemGroup>
24+
25+
<ItemGroup>
26+
<ProjectReference Include="..\..\src\Microsoft.Identity.Web.UI\Microsoft.Identity.Web.UI.csproj" />
27+
<ProjectReference Include="..\Microsoft.Identity.Web.Test.Common\Microsoft.Identity.Web.Test.Common.csproj" />
28+
</ItemGroup>
29+
30+
</Project>

0 commit comments

Comments
 (0)