Issue with Azure B2C Reset Password user flow - asp.net-mvc

I have recently developed an ASP.net MVC web application which uses Azure B2C to authenticate users.
I have been asked to enable the Reset Password User flow to enable users to reset via self-service.
I created the user flow within the portal (using the correct identity provider and setting Reset password using email address) and added the code from the microsoft example here however every time I click reset password, it just directs me back to the login screen and it never reaches the reset password page.
When I click the forgot password link the method below is called , it steps through the code fine, but then loads the login page.
Reset Password code
public void ResetPassword(string redirectUrl)
{
// Let the middleware know you are trying to use the reset password policy (see OnRedirectToIdentityProvider in Startup.Auth.cs)
HttpContext.GetOwinContext().Set("Policy", Startup.PasswordResetPolicyId);
// Set the page to redirect to after changing passwords
var authenticationProperties = new AuthenticationProperties { RedirectUri = "/" };
HttpContext.GetOwinContext().Authentication.Challenge(authenticationProperties);
return;
}
The policy ID is correct in both azure and in the code as I step through and the values are all pulling through correctly (see below):
Policy ID string (as used above)
public static string PasswordResetPolicyId = ConfigurationManager.AppSettings["ida:ResetPasswordPolicyId"];
In Web.config where the policy is defined
<add key="ida:ResetPasswordPolicyId" value="B2C_1_UserApp_ResetPassword" />
I have provided all the code samples I have added for the reset function to work, the rest of the code is all included in the Microsoft Web App example.
Has anyone else experienced something similar? As I said previously, when you click the forgot password link it does exactly as it should and goes to the correct controller/method, but then goes back to the login screen.

Searching through my code, I found that the line
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(PasswordResetPolicyId));
was missing from ConfigureAuth. Once added this has fixed the issue.
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
CookieManager = new SystemWebCookieManager()
});
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(SignInPolicyId));
/////////////////
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(PasswordResetPolicyId));
}

Related

Aspnet core cookie [Authorize] not redirecting on ajax calls

In an asp.net core 3.1 web app with cookie-based authorization I have created a custom validator which executes on the cookie authorization's OnValidatePrincipal event. The validator does a few things, one of those is check in the backend if the user has been blocked. If the user has been blocked, The CookieValidatePrincipalContext.RejectPrincipal() method is executed and the user is signed out using the CookieValidatePrincipalContext.HttpContext.SignOutAsyn(...) method, as per the MS docs.
Here is the relevant code for the validator:
public static async Task ValidateAsync(CookieValidatePrincipalContext cookieValidatePrincipalContext)
{
var userPrincipal = cookieValidatePrincipalContext.Principal;
var userService = cookieValidatePrincipalContext.GetUserService();
var databaseUser = await userService.GetUserBySidAsync(userPrincipal.GetSidAsByteArray());
if (IsUserInvalidOrBlocked(databaseUser))
{
await RejectUser(cookieValidatePrincipalContext);
return;
}
else if (IsUserPrincipalOutdated(userPrincipal, databaseUser))
{
var updatedUserPrincipal = await CreateUpdatedUserPrincipal(userPrincipal, userService);
cookieValidatePrincipalContext.ReplacePrincipal(updatedUserPrincipal);
cookieValidatePrincipalContext.ShouldRenew = true;
}
}
private static bool IsUserInvalidOrBlocked(User user)
=> user is null || user.IsBlocked;
private static async Task RejectUser(CookieValidatePrincipalContext context)
{
context.RejectPrincipal();
await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
And here is the setup for the cookie-based authorization:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(co =>
{
co.LoginPath = #$"/{ControllerHelpers.GetControllerName<AuthenticationController>()}/{nameof(AuthenticationController.Login)}";
co.LogoutPath = #$"/{ControllerHelpers.GetControllerName<AuthenticationController>()}/{nameof(AuthenticationController.Logout)}";
co.ExpireTimeSpan = TimeSpan.FromDays(30);
co.Cookie.SameSite = SameSiteMode.Strict;
co.Cookie.Name = "GioBQADashboard";
co.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = UserPrincipalValidator.ValidateAsync
};
co.Validate();
});
This actually gets called and executed as expected and redirects the user to the login page when they navigate to a new page after having been blocked.
Most of the views have ajax calls to api methods that execute on a timer every 10 seconds. For those calls the credentials also get validated and the user gets signed out. However, after the user has been signed out, a popup asking for user credentials appears on the page:
If the user doesn't enter their credentials and navigate to another page, they get taken to the login page as expected.
If they do enter their credentials, they stay logged in, but their identity appears to be their windows identity...
What is going on here? What I would really want to achieve is for users to be taken to the login page for any request made after they have been signed out.
I have obviously misconfigured something, so that the cookie-based authorization doesn't work properly for ajax requests, but I cannot figure out what it is.
Or is it the Authorization attribute which does not work the way I'm expecting it to?
The code lines look good to me.
This login dialog seems to be the default one for Windows Authentication. Usually, it comes from the iisSettings within the launchSettings.json file. Within Visual Studio you'll find find the latter within your Project > Properties > launchSettings.json
There set the windowsAuthentication to false.
{
"iisSettings": {
"windowsAuthentication": false,
}
}

MVC Windows Authentication causing querystring too long error

I have an MVC web site that I’d like to modify so instead of using forms to log the user in, it picks up their windows id then passes this to a local Active Directory using LDAP.
However, when I change IIS from Anonymous to Windows Authentication and change the code in Start.Auth.cs with the following (where LoginUser is the script that picks up the user and connects to AD)…
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/LoginUser")
});
…it causes a querystring is too long browser error …
http://localhost:80/UserAuthentication/Account/LoginUser?ReturnUrl=%2FUserAuthentication%2FAccount%2FLoginUser%3FReturnUrl%3D%252FUserAuthentication%252FAccount%252FLoginUser%253FReturnUrl%253D%25252FUserAuthentication%25252FAccount%25252FLoginUser%25253FReturnUrl%25253D%2525252FUserAuthentication%2525252FAccount%2525252FLoginUser%2525253FReturnUrl%2525253D%252525252FUserAuthentication%252525252FAccount%252525252FLoginUser%252525253FReturnUrl%252525253D% (etc)
I have placed [AllowAnonymous] above the LoginUser script so I’m really not sure why it won’t go into it (as this what looks like is happening again and again).
Any help would really really be most appreciated.
Updated to include LoginUser script:
I've included a stripped down version of it that just logs a user in.
[AllowAnonymous]
public async Task<ActionResult> LoginUser()
{
var status = await SignInManager.PasswordSignInAsync("ATHORNE", "Something123!", false, false);
return View();
}
I am using PasswordSignInAsync with a constant password because there would not be a password for Windows Authentication. If there is a better way, please let me know!
The View is the blank default view at the moment.
Typically I would override OnActionExcuting(ActionExecutingContext filterContext)
You can do this in a base Controller class or in an Action Filter
If IIS is set to deny Anonymous Access and enable Windows authentication then within the filter context you can access the users' identity via HttpContext.User.Identity.Name;
To be a bit safer you could write some code like
var user = (filterContext.HttpContext.User.Identity != null
&& filterContext.HttpContext.User.Identity.IsAuthenticated) ?
HttpContext.User.Identity.Name : null;
If username is not null then you have the currently logged on authenticated user, in the form domain\username. You can query Active Directory based on this to get any other information you need.

How to add parameters to redirect_uri in WebApi Oauth Owin authentication process?

I'm creating a webapi project with oauth bearer token authenthication and external login providers (google, twitter, facebook etc.). I started with the basic VS 2013 template and got everything to work fine!
However, after a user successfully logs is, the owin infrastructure creates a redirect with the folllowing structure:
http://some.url/#access_token=<the access token>&token_type=bearer&expires_in=1209600
In my server code I want to add an additional parameter to this redirect because in the registration process of my app, a new user needs to first confirm and accept the usage license before he/she is registered as a user. Therefore I want to add the parameter "requiresConfirmation=true" to the redirect. However, I've no clue about how to do this. I tried setting AuthenticationResponseChallenge.Properties.RedirectUri of the AuthenticationManager but this doesn't seem to have any affect.
Any suggestions would be greatly appreciated!
It should be relatively easy with the AuthorizationEndpointResponse notification:
In your custom OAuthAuthorizationServerProvider implementation, simply override AuthorizationEndpointResponse to extract your extra parameter from the ambient response grant, which is created when you call IOwinContext.Authentication.SignIn(properties, identity).
You can then add a custom requiresConfirmation parameter to AdditionalResponseParameters: it will be automatically added to the callback URL (i.e in the fragment when using the implicit flow):
public override Task AuthorizationEndpointResponse(OAuthAuthorizationEndpointResponseContext context) {
var requiresConfirmation = bool.Parse(context.OwinContext.Authentication.AuthenticationResponseGrant.Properties.Dictionary["requiresConfirmation"]);
if (requiresConfirmation) {
context.AdditionalResponseParameters.Add("requiresConfirmation", true);
}
return Task.FromResult<object>(null);
}
In your code calling SignIn, determine whether the user is registered or not and add requiresConfirmation to the AuthenticationProperties container:
var properties = new AuthenticationProperties();
properties.Dictionary.Add("requiresConfirmation", "true"/"false");
context.Authentication.SignIn(properties, identity);
Feel free to ping me if you need more details.

ASP.net Identity stops handing out External Cookie after removing external account

I have a site setup with four 3rd-party login services, Microsoft, VS, Github, and Linkedin. Everything seems to work great, I can log in/out, add/remove external accounts with no problem.
Randomly however, it seems to stop working. When I try to login using any of the 3rd-party services, it just kicks me back to the login page.
Looking at the ExternalLoginCallback it appears that the AuthenticateResult.Identity is null and it can't get the external login info. Looking at it on the client-side it looks like they never got the external signin cookie.
I still can't consistently reproduce this error, so it's really hard to determine what might be happening. Any help would be great.
Update 1: I was able to identify the steps to reproduce:
Login to an account with more than 1 associated login
Remove one of the logins
In a new browser or a private session, try to log in with any of the 3rd-party accounts and you will be returned to login without an external cookie.
After hitting the error it won't hand out a cookie to any new sessions until IIS is restarted.
Update 2: Looks like it has something to do with setting a Session variable.
On the removeLogin action I was adding a value to the session. I'm not sure why but when I stopped doing that, I stopped having my problem. Time to figure out why... Update 3: Looks like this problem has been reported to the Katana Team
Update 4: Looks like someone else already ran into this problem. Stackoverflow post. They didn't give all of the code you needed to solve it, so I'll include that here as an answer.
Startup.Auth.cs
public void ConfigureAuth(IAppBuilder app) {
// Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(appContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions {
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
AuthenticationMode = AuthenticationMode.Active,
LoginPath = new PathString("/Login"),
LogoutPath = new PathString("/Logout"),
Provider = new CookieAuthenticationProvider {
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, User, int>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentityCallback: (manager, user) => user.GenerateUserIdentityAsync(manager),
getUserIdCallback: (id) => (Int32.Parse(id.GetUserId()))
)
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
// Enables the application to remember the second login verification factor such as phone or email.
// Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
// This is similar to the RememberMe option when you log in.
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
// Uncomment the following lines to enable logging in with third party login providers
app.UseMicrosoftAccountAuthentication(new MicrosoftAccountAuthenticationOptions{
ClientId = ConfigurationManager.AppSettings["MSA:Id"],
ClientSecret = ConfigurationManager.AppSettings["MSA:Secret"],
Caption = "Microsoft"
});
app.UseVisualStudioAuthentication(new VisualStudioAuthenticationOptions(){
AppId = ConfigurationManager.AppSettings["VSO:Id"],
AppSecret = ConfigurationManager.AppSettings["VSO:Secret"],
Provider = new VisualStudioAuthenticationProvider(){
OnAuthenticated = (context) =>{
context.Identity.AddClaim(new Claim("urn:vso:access_token", context.AccessToken, XmlSchemaString, "VisualStudio"));
context.Identity.AddClaim(new Claim("urn:vso:refresh_token", context.RefreshToken, XmlSchemaString, "VisualStudio"));
return Task.FromResult(0);
}
},
Caption = "Visual Studio"
});
app.UseGitHubAuthentication(new GitHubAuthenticationOptions{
ClientId = ConfigurationManager.AppSettings["GH:Id"],
ClientSecret = ConfigurationManager.AppSettings["GH:Secret"],
Caption = "Github"
});
app.UseLinkedInAuthentication(new LinkedInAuthenticationOptions {
ClientId = ConfigurationManager.AppSettings["LI:Id"],
ClientSecret = ConfigurationManager.AppSettings["LI:Secret"],
Caption = "LinkedIn"
});
}
OWIN and asp.net handle cookies/session differently. If you authorize with OWIN before you initialize a session, anyone after a session is initialized will not be able to login.
Workaround: Add the following to your Global.asax
// Fix for OWIN session bug
protected void Application_AcquireRequestState() {
Session["Workaround"] = 0;
}
}
Long term: The way OWIN and asp.net handle sessions/cookies will be merged in vNext, use the work around until then...

External Cookie for External Login in ASP.NET OWIN

We have a legacy system which is built on ASP.NET Mvc 4, now we would like to support Signal Sign On via Azure Active Directory for current users as well as new users. Since we have managed our own authentication workflow, ASP.NET Identity definitely does not fit in our case.
I have managed to build a demo which is working on OWIN OpenIdConnect middleware passive mode without using ASP.NET Identity. The below code works correctly:
app.SetDefaultSignInAsAuthenticationType("ExternalCookie");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "ExternalCookie",
AuthenticationMode = AuthenticationMode.Passive,
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Passive,
ClientId = ClientId,
Authority = Authority
// More code
});
And in ExternalLoginCallback action:
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var authManager = Request.GetOwinContext().Authentication;
var result = await authManager.AuthenticateAsync("ExternalCookie");
authManager.SignOut("ExternalCookie");
//More code to convert to local identity
}
This case is really common even using other providers like Google, Facebook or Twitter. One thing I have not much clear is ExternalCookie, maybe I have missed the whole thing. My understanding is when external login is successfully, external cookie is used to store the external claim identity. And then we call:
var result = await authManager.AuthenticateAsync("ExternalCookie");
authManager.SignOut("ExternalCookie");
In order to get the external claim identity and then convert external identity to local identity. I have a little bit confusion why we have to call SignOut external cookie in this case.
Also, I'm not sure whether External Cookie is a must when using external login, or do we have other ways around without using External Cookie.
Please someone give an explanation on this point.
To answer your last question, you change the name of cookie in startup.auth file where you configure external cookie -
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
You can use a string instead of DefaultAuthenticationTypes enum and directly specify the name of the cookie like -
app.UseExternalSignInCookie("myExternalCookie");

Resources