ASP.NET Identity is returning an 'Invalid token.' response when resetting a password for users.
I've tried the following:
URL Encode the code before sending email
URL Encode & Decode the code before and after
Copying the code to make sure it matches what was sent
Ensured my user email is confirmed (I heard that could be a problem)
Created a custom UserManager/Store etc.
This is my email code:
var user = await UserManager.FindByNameAsync(model.Email);
var code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
var callbackUrl = Url.Action("ResetPassword", "Account", new { code }, "http");
var body = string.Format("Click here to reset your password: {0}", callbackUrl);
await UserManager.SendEmailAsync(user.Id, "Reset Password", body);
return View("~/Views/Account/Login.cshtml", model);
The generated URL:
http://localhost/Account/ResetPassword?code=XTMg3fBDDR77LRptnRpg7r7oDxz%2FcvGscq5Pm3HMe8RJgX0KVx6YbOeqflvVUINipVcXcDDq1phuj0GCmieCuawdgfQzhoG0FUH4BoLi1TxY2kMljGp1deN60krGYaJMV6rbkrDivKa43UEarBHawQ%3D%3D
Finally my reset code:
if (!ModelState.IsValid)
{
return View(model);
}
var user = await UserManager.FindByNameAsync(model.Email);
if (user == null)
{
// Don't reveal that the user does not exist
return RedirectToAction("ResetPasswordConfirmation", "Account");
}
var result = await UserManager.ResetPasswordAsync(user.Id, model.Code, model.Password);
if (result.Succeeded)
{
return RedirectToAction("ResetPasswordConfirmation", "Account");
}
ModelState.AddModelError("","Invalid Password Please Try Again");
return View();
Inside the result is 1 error, Invalid token.
My create UserManager method:
public static CustomerUserManager Create(IdentityFactoryOptions<CustomerUserManager> options, IOwinContext context)
{
var manager = new CustomerUserManager(new CustomerUserStore(context.Get<CustomerDbContext>()));
// Configure validation logic for usernames
manager.UserValidator = new UserValidator<Customer>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};
// Configure validation logic for passwords
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = 6,
RequireNonLetterOrDigit = true,
RequireDigit = true,
RequireLowercase = true,
RequireUppercase = true,
};
manager.EmailService = new EmailService();
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider = new DataProtectorTokenProvider<Customer, string>(dataProtectionProvider.Create("ASP.NET Identity"));
}
return manager;
}
My Startup.Auth config:
app.CreatePerOwinContext(CustomerDbContext.Create);
app.CreatePerOwinContext<CustomerUserManager>(CustomerUserManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity =
SecurityStampValidator.OnValidateIdentity<CustomerUserManager, Customer, string>
(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentityCallback: (manager, user) => user.GenerateUserIdentityAsync(manager),
getUserIdCallback: (id) => (id.GetUserId())
)
}
});
List of tried solutions:
ASP.NET Identity 2 - UserManager.ConfirmEmail Fails In Production
aspnet identity invalid token on confirmation email
http://www.asp.net/mvc/overview/security/create-an-aspnet-mvc-5-web-app-with-email-confirmation-and-password-reset#reset
Asp.NET - Identity 2 - Invalid Token Error
aspnet identity invalid token on confirmation email
https://aspnetidentity.codeplex.com/discussions/544368
Thanks for any help with this problem.
You can try this code.
I shared this link: aspnet identity invalid token on confirmation email
var encodedCode= code.Base64ForUrlEncode();
var decodedCode= encodedCode.Base64ForUrlDecode();
public static class UrlEncoding
{
public static string Base64ForUrlEncode(this string str)
{
byte[] encbuff = Encoding.UTF8.GetBytes(str);
return HttpServerUtility.UrlTokenEncode(encbuff);
}
public static string Base64ForUrlDecode(this string str)
{
byte[] decbuff = HttpServerUtility.UrlTokenDecode(str);
return Encoding.UTF8.GetString(decbuff);
}
}
Related
There is some problem in my code not sure where I am getting wrong. Earlier same code used to work properly. Now I am trying to log in to application in both the ways i.e through ClaimPrincipal and Claim Identity. In both the ways Sometime data is null. Not sure where is the issue.
Below is my code
StartUp.cs
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
Authority = authority,
RedirectUri = redirectUri,
//PostLogoutRedirectUri = redirectUri,
Scope = OpenIdConnectScope.OpenIdProfile,
ResponseType = OpenIdConnectResponseType.IdToken,
TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = false // This is a simplification
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed
}
}
);
SignIn Method
[AllowAnonymous]
public void SignIn(string ReturnUrl = "/", string loginType = "")
{
HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/Account/Office365LoginCallback" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
RedirectURL Code
[AllowAnonymous]
[ExceptionHandler]
public async Task<ActionResult> Office365LoginCallback(string code)
{
var userClaims = User.Identity as System.Security.Claims.ClaimsIdentity;
string userName = userClaims?.FindFirst("name")?.Value;
string userEmail = userClaims?.FindFirst("preferred_username")?.Value;
string userId = userClaims?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
ViewBag.TenantId = userClaims?.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid")?.Value;
return Redirect("~/");
}
catch (Exception ex)
{
throw ex;
}
}
SignOut Method
public ActionResult LogOff()
{
//AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
//HttpContext.GetOwinContext().Authentication.SignOut();
FormsAuthentication.SignOut();
//HttpContext.GetOwinContext().Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
//HttpContext.Session[AppConstants.UserEmail] = null;
//HttpContext.Session[AppConstants.UserUpload] = null;
//HttpContext.Session[AppConstants.UserImage] = null;
//HttpContext.Session[AppConstants.CurrentRole] = null;
//HttpContext.Session[AppConstants.Users] = null;
//Session.Clear();
//Session.Abandon();
return RedirectToAction("Login", "Account");
}
Any Help will be appreciated. FYI I have tried same code in new project there is working fine but here in my old application its not working once clicked on Logout
I'm trying to implement asp.net core cookie-based authentication. So I have added the code below to my startup.cs
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.AccessDeniedPath = new PathString("/login");
options.LoginPath = new PathString("/login");
options.SlidingExpiration = true;
});
and I'm sign-in using the code below
[HttpPost]
[ValidateAntiForgeryToken]
[Route("login")]
public async Task<IActionResult> Login(AuthViewModel authView)
{
if (ModelState.IsValid)
{
var (status, message, SigninUser) = await authentication.Authenticate(new User()
{
email = authView.Email,
pwd = authView.Password
});
if (status)
{
List<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.Name, "App Member"),
new Claim(ClaimTypes.Email, SigninUser.email)
};
ClaimsIdentity identity = new ClaimsIdentity(claims, "cookie");
ClaimsPrincipal principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(
scheme: CookieAuthenticationDefaults.AuthenticationScheme,
principal: principal,
properties: new AuthenticationProperties
{
IsPersistent = authView.RememberMe,
ExpiresUtc = DateTime.UtcNow.AddYears(1)
});
HttpContext.Session.Set<User>("session_user", SigninUser);
if (Url.IsLocalUrl(authView.returnUrl))
return Redirect(authView.returnUrl);
else
return RedirectToAction("Index");
}
else
{
authView.Status = false;
authView.Message = message;
}
}
else
{
string message = string.Join(" | ", ModelState.Values.SelectMany(e => e.Errors).Select(v => v.ErrorMessage));
authView.Status = false;
authView.Message = message;
}
return View(authView);
}
This works fine. But when I keep the browser IDLE for like 30 minutes the "session_user" session variable get expired and the user still gets authenticated. How can I resolve this?
Also using cookie-based authentication can have a performance penalty?
Thanks
I want to sign my users in using their email address from Facebook. I have configured my Facebook authentication:
var facebookAuthenticationOptions = new FacebookAuthenticationOptions
{
AppId = facebookId,
AppSecret = facebookSecret,
Provider = new FacebookProvider()
};
app.UseFacebookAuthentication(facebookAuthenticationOptions);
I have overridden the Facebook provider to also return the email address:
public class FacebookProvider : FacebookAuthenticationProvider
{
public override Task Authenticated(FacebookAuthenticatedContext context)
{
var accessTokenClaim = new Claim("ExternalAccessToken", context.AccessToken, "urn:facebook:access_token");
context.Identity.AddClaim(accessTokenClaim);
var extraClaims = GetAdditionalFacebookClaims(accessTokenClaim);
context.Identity.AddClaim(new Claim(ClaimTypes.Email, extraClaims.First(k => k.Key == "email").Value.ToString()));
context.Identity.AddClaim(new Claim("Provider", context.Identity.AuthenticationType));
context.Identity.AddClaim(new Claim(ClaimTypes.Name, context.Identity.FindFirstValue(ClaimTypes.Name)));
var userDetail = context.User;
var link = userDetail.Value<string>("link") ?? string.Empty;
context.Identity.AddClaim(new Claim("link", link));
context.Identity.AddClaim(new Claim("FacebookId", userDetail.Value<string>("id")));
return Task.FromResult(0);
}
private static JsonObject GetAdditionalFacebookClaims(Claim accessToken)
{
var fb = new FacebookClient(accessToken.Value);
return fb.Get("me", new { fields = new[] { "email" } }) as JsonObject;
}
Everything works fine in MVC - in the LoginOwinCallback function, I am able to retrieve the user's email address as returned from Facebook. I am trying to achieve the same thing in WebApi using token authentication instead of external cookies. However, although I can see my provider adding the email claim to the response, when I call the AuthenticateAsync method in the following routine, the Email claim is not included.
private async Task<ExternalLoginInfo> GetExternalLoginInfoAsync()
{
var result = await Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ExternalBearer);
if (result == null || result.Identity == null) return null;
var idClaim = result.Identity.FindFirst(ClaimTypes.NameIdentifier);
if (idClaim != null)
{
return new ExternalLoginInfo()
{
DefaultUserName = result.Identity.Name == null ? "" : result.Identity.Name.Replace(" ", ""),
ExternalIdentity = result.Identity,
Login = new UserLoginInfo(idClaim.Issuer, idClaim.Value)
};
}
return null;
}
Any ideas what I am doing wrong?
For anyone else facing the same problem, the Email claim is available - just a bit hidden.
Firstly, to correctly retrieve the user's email address from Facebook, the authentication set-up in the ConfigureAuth method of Startup.Auth.cs should be:
app.UseFacebookAuthentication(new FacebookAuthenticationOptions
{
AppId = facebookId,
AppSecret = facebookSecret,
Scope= {"email"},
UserInformationEndpoint = "https://graph.facebook.com/v2.4/me?fields=email"
});
In the LoginOwinCallback method of the MVC AccountController class, the email address is found in the Email property of the ExternalLoginInfo object returned by var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();.
To retrieve the email address in the WebAPI AccountController class, you will need to cast the User.Identity object to the ClaimsIdentity type.
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)]
[AllowAnonymous]
[Route("ExternalLogin", Name = "ExternalLogin")]
public async Task<IHttpActionResult> GetExternalLogin(string provider, string error = null)
{
if (error != null)
{
return Redirect(Url.Content("~/") + "#error=" + Uri.EscapeDataString(error));
}
if (!User.Identity.IsAuthenticated)
{
return new ChallengeResult(provider, this);
}
// get all of the claims
var claimsIdentity = User.Identity as ClaimsIdentity; // this cast exposes all of the claims returned by Facebook
// get the external login details
var externalLogin = ExternalLoginData.FromIdentity(claimsIdentity);
if (externalLogin == null)
{
return InternalServerError();
}
if (externalLogin.LoginProvider != provider)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
return new ChallengeResult(provider, this);
}
var user = await UserManager.FindAsync(new UserLoginInfo(externalLogin.LoginProvider,
externalLogin.ProviderKey));
var hasRegistered = user != null;
if (hasRegistered)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var oAuthIdentity = await user.GenerateUserIdentityAsync(UserManager,
OAuthDefaults.AuthenticationType);
var cookieIdentity = await user.GenerateUserIdentityAsync(UserManager,
CookieAuthenticationDefaults.AuthenticationType);
var properties = ApplicationOAuthProvider.CreateProperties(user.UserName);
AuthenticationManager.SignIn(properties, oAuthIdentity, cookieIdentity);
}
else
{
var claims = claimsIdentity?.Claims ?? externalLogin.GetClaims();
var identity = new ClaimsIdentity(claims, OAuthDefaults.AuthenticationType);
AuthenticationManager.SignIn(identity);
}
return Ok();
}
This means you can then use the FindFirst method on the Identity to find the email claim returned by Facebook.
i am new to mvc and i am trying to make confirmation email on registration in mvc5. i have followed sometutorials. lots of them use sendgrid but i want to use smtp. i have done the tutorials step by step by i cant get email after registration. i cant find the problem becuase i have done what i was read in tutorials.
any help is appritiated.
in identityConfig.cs
public class EmailService : IIdentityMessageService
{
public Task SendAsync(IdentityMessage message)
{
// Plug in your email service here to send an email.
//return Task.FromResult(0);
// Credentials:
var credentialUserName = "[myEmail]";
var sentFrom = "[myEmail]";
var pwd = "[myPassword]";
// Configure the client:
System.Net.Mail.SmtpClient client =
new System.Net.Mail.SmtpClient("smtp.gmail.com");
client.Port = 587;
client.DeliveryMethod = System.Net.Mail.SmtpDeliveryMethod.Network;
client.UseDefaultCredentials = false;
// Create the credentials:
System.Net.NetworkCredential credentials =
new System.Net.NetworkCredential(credentialUserName, pwd);
client.EnableSsl = true;
client.Credentials = credentials;
// Create the message:
var mail =
new System.Net.Mail.MailMessage(sentFrom, message.Destination);
mail.Subject = message.Subject;
mail.Body = message.Body;
// Send:
return client.SendMailAsync(mail);
}
}
public ApplicationUserManager(IUserStore<ApplicationUser> store)
: base(store)
{
this.UserTokenProvider = new TotpSecurityStampBasedTokenProvider<ApplicationUser, string>();
this.EmailService = new EmailService();
}
and in my AccontController.cs
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);
// For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771
// Send an email with this link
string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please confirm your account by clicking here");
return RedirectToAction("Index", "Home");
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
and i havnt change anything else. am i missing something?
thank you very much.
I am kind of successful by doing this in the Startup.Auth.cs file
// Configure the db context and user manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.Properties["Microsoft.Owin.Security.Constants.DefaultSignInAsAuthenticationType"] = "ExternalCookie";
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri
});
The challenge I have is, when a user is signed out,and tries to hit a non-login page like say http://mywebsite/users/management rather than http://mywebsite/account/login the application redirects to the Azure AD sign-in page automatically, which is not right. Because there could be users who do not have account on Azure AD at all. Even if we give a proper userid and password in the AD sign in page and click sign-in, it keeps redirecting between different urls within http://login.windows.net and never goes to our website at all.
Here is the logout code -
AuthenticationManager.SignOut(new string[] { DefaultAuthenticationTypes.ExternalCookie, DefaultAuthenticationTypes.ApplicationCookie, OpenIdConnectAuthenticationDefaults.AuthenticationType });
return RedirectToAction("Login", "Account");
I am not sure what I'm doing wrong here.
Edit 1
My ExternalLoginCallback method
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Sid, "Office365"));
// Sign in the user with this external login provider if the user already has a login
var user = await UserManager.FindByEmailAsync(loginInfo.ExternalIdentity.Name);
if (user != null && user.IsActive == true && user.EmailConfirmed == true)
{
var result = await UserManager.AddLoginAsync(user.Id, loginInfo.Login);
if (result.Succeeded)
{
if (claims != null)
{
var userIdentity = await user.GenerateUserIdentityAsync(UserManager);
userIdentity.AddClaims(claims);
}
}
await SignInAsync(user, isPersistent: true);
Session[AppConstants.General.UserID] = user.Id;
string fullName = string.Format("{0} {1}",user.FirstName,user.LastName);
Session[AppConstants.General.UserFullName] = fullName;
return RedirectToLocal(returnUrl);
}
else
{
// If the user does not have an account, tell that to the user.
ViewBag.ReturnUrl = returnUrl;
ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email });
}
}
Try this
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = ClientId,
Authority = Authority,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
RedirectToIdentityProvider = (context) =>
{
if (context.Request.Path.Value == "/Account/ExternalLogin" || (context.Request.Path.Value == "/Account/LogOff" && context.Request.User.Identity.IsExternalUser()))
{
// This ensures that the address used for sign in and sign out is picked up dynamically from the request
// this allows you to deploy your app (to Azure Web Sites, for example)without having to change settings
// Remember that the base URL of the address used here must be provisioned in Azure AD beforehand.
string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
context.ProtocolMessage.RedirectUri = appBaseUrl + "/";
context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
}
else
{
//This is to avoid being redirected to the microsoft login page when deep linking and not logged in
context.State = Microsoft.Owin.Security.Notifications.NotificationResultState.Skipped;
context.HandleResponse();
}
return Task.FromResult(0);
},
}
});
EDIT:
Forgot this extension method
public static class IdentityExtensions
{
public static bool IsExternalUser(this IIdentity identity)
{
ClaimsIdentity ci = identity as ClaimsIdentity;
if (ci != null && ci.IsAuthenticated == true)
{
var value = ci.FindFirstValue(ClaimTypes.Sid);
if (value != null && value == "Office365")
{
return true;
}
}
return false;
}
}
EDIT 2:
You have to have some custom logic in the ExternalLoginCallback (AccountController) e.g. add the Sid claim. In this case there is also logic to check if the user allows external login.
// GET: /Account/ExternalLoginCallback
[AllowAnonymous]
public async Task<ActionResult> ExternalLoginCallback(string returnUrl, string urlHash)
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Sid, "Office365"));
// Sign in the user with this external login provider if the user already has a login
var user = await UserManager.FindAsync(loginInfo.Login);
if (user == null)
{
user = await UserManager.FindByNameAsync(loginInfo.DefaultUserName);
if (user != null)
{
if(user.AllowExternalLogin == false)
{
ModelState.AddModelError("", String.Format("User {0} not allowed to authenticate with Office 365.", loginInfo.DefaultUserName));
return View("Login");
}
var result = await UserManager.AddLoginAsync(user.Id, loginInfo.Login);
if (result.Succeeded)
{
if (claims != null)
{
var userIdentity = await user.GenerateUserIdentityAsync(UserManager);
userIdentity.AddClaims(claims);
}
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
}
return RedirectToLocal(returnUrl);
}
else
{
ModelState.AddModelError("", String.Format("User {0} not found.", loginInfo.DefaultUserName));
return View("Login");
}
}
else
{
if (user.AllowExternalLogin == false)
{
ModelState.AddModelError("", String.Format("User {0} not allowed to authenticate with Office 365.", loginInfo.DefaultUserName));
return View("Login");
}
if (claims != null)
{
var userIdentity = await user.GenerateUserIdentityAsync(UserManager);
userIdentity.AddClaims(claims);
}
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
return RedirectToLocal(returnUrl);
}
}