development

"잘못된 토큰"오류를 제공하는 Asp.NET Identity 2

big-blog 2020. 12. 31. 23:20
반응형

"잘못된 토큰"오류를 제공하는 Asp.NET Identity 2


내가 사용하고 Asp.Net-신원이 나는 법 아래를 사용하여 이메일 확인 코드를 확인하기 위해 노력하고있어. 하지만 "유효하지 않은 토큰" 오류 메시지가 나타납니다.

  • 내 응용 프로그램의 사용자 관리자는 다음과 같습니다.

    public class AppUserManager : UserManager<AppUser>
    {
        public AppUserManager(IUserStore<AppUser> store) : base(store) { }
    
        public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options, IOwinContext context)
        {
            AppIdentityDbContext db = context.Get<AppIdentityDbContext>();
            AppUserManager manager = new AppUserManager(new UserStore<AppUser>(db));
    
            manager.PasswordValidator = new PasswordValidator { 
                RequiredLength = 6,
                RequireNonLetterOrDigit = false,
                RequireDigit = false,
                RequireLowercase = true,
                RequireUppercase = true
            };
    
            manager.UserValidator = new UserValidator<AppUser>(manager)
            {
                AllowOnlyAlphanumericUserNames = true,
                RequireUniqueEmail = true
            };
    
            var dataProtectionProvider = options.DataProtectionProvider;
    
            //token life span is 3 hours
            if (dataProtectionProvider != null)
            {
                manager.UserTokenProvider =
                   new DataProtectorTokenProvider<AppUser>
                      (dataProtectionProvider.Create("ConfirmationToken"))
                   {
                       TokenLifespan = TimeSpan.FromHours(3)
                   };
            }
    
            manager.EmailService = new EmailService();
    
            return manager;
        } //Create
      } //class
    } //namespace
    
  • 토큰을 생성하는 내 작업은 다음과 같습니다 (여기에서 토큰을 확인하더라도 "잘못된 토큰"메시지가 표시됨).

    [AllowAnonymous]
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult ForgotPassword(string email)
    {
        if (ModelState.IsValid)
        {
            AppUser user = UserManager.FindByEmail(email);
            if (user == null || !(UserManager.IsEmailConfirmed(user.Id)))
            {
                // Returning without warning anything wrong...
                return View("../Home/Index");
    
            } //if
    
            string code = UserManager.GeneratePasswordResetToken(user.Id);
            string callbackUrl = Url.Action("ResetPassword", "Admin", new { Id = user.Id, code = HttpUtility.UrlEncode(code) }, protocol: Request.Url.Scheme);
    
            UserManager.SendEmail(user.Id, "Reset password Link", "Use the following  link to reset your password: <a href=\"" + callbackUrl + "\">link</a>");
    
            //This 2 lines I use tho debugger propose. The result is: "Invalid token" (???)
            IdentityResult result;
            result = UserManager.ConfirmEmail(user.Id, code);
        }
    
        // If we got this far, something failed, redisplay form
        return View();
    
    } //ForgotPassword
    
  • 토큰을 확인하는 내 작업은 다음과 같습니다 (여기서는 결과를 확인할 때 항상 "잘못된 토큰"이 표시됨).

    [AllowAnonymous]
    public async Task<ActionResult> ResetPassword(string id, string code)
    {
    
        if (id == null || code == null)
        {
            return View("Error", new string[] { "Invalid params to reset password." });
        }
    
        IdentityResult result;
    
        try
        {
            result = await UserManager.ConfirmEmailAsync(id, code);
        }
        catch (InvalidOperationException ioe)
        {
            // ConfirmEmailAsync throws when the id is not found.
            return View("Error", new string[] { "Error to reset password:<br/><br/><li>" + ioe.Message + "</li>" });
        }
    
        if (result.Succeeded)
        {
            AppUser objUser = await UserManager.FindByIdAsync(id);
            ResetPasswordModel model = new ResetPasswordModel();
    
            model.Id = objUser.Id;
            model.Name = objUser.UserName;
            model.Email = objUser.Email;
    
            return View(model);
        }
    
        // If we got this far, something failed.
        string strErrorMsg = "";
        foreach(string strError in result.Errors)
        {
            strErrorMsg += "<li>" + strError + "</li>";
        } //foreach
    
        return View("Error", new string[] { strErrorMsg });
    
    } //ForgotPasswordConfirmation
    

무엇이 빠졌는지, 무엇이 잘못되었는지 모르겠습니다 ...


여기에서 비밀번호 재설정을위한 토큰을 생성하고 있기 때문에 :

string code = UserManager.GeneratePasswordResetToken(user.Id);

그러나 실제로 이메일에 대한 토큰의 유효성을 검사하려고합니다.

result = await UserManager.ConfirmEmailAsync(id, code);

이들은 2 개의 다른 토큰입니다.

귀하의 질문에 이메일을 확인하려고했지만 귀하의 코드는 비밀번호 재설정을위한 것입니다. 당신은 어느 것을하고 있습니까?

이메일 확인이 필요한 경우 다음을 통해 토큰을 생성하십시오.

var emailConfirmationCode = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);

그것을 통해 확인하십시오

var confirmResult = await UserManager.ConfirmEmailAsync(userId, code);

비밀번호 재설정이 필요한 경우 다음과 같이 토큰을 생성하십시오.

var code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);

다음과 같이 확인하십시오.

var resetResult = await userManager.ResetPasswordAsync(user.Id, code, newPassword);

이 문제가 발생하여 해결했습니다. 몇 가지 가능한 이유가 있습니다.

1. URL 인코딩 문제 (문제가 "임의로"발생하는 경우)

이 문제가 무작위로 발생하면 URL 인코딩 문제가 발생할 수 있습니다. 알 수없는 이유로 토큰은 url-safe 용으로 설계되지 않았습니다. 즉, URL을 통해 전달 될 때 잘못된 문자가 포함될 수 있습니다 (예 : 전자 메일을 통해 전송되는 경우).

이 경우, HttpUtility.UrlEncode(token)HttpUtility.UrlDecode(token)사용되어야한다.

oão Pereira가 그의 의견에서 말했듯이, UrlDecode필요하지 않습니다 (또는 때로는 필요하지 않습니까?). 둘 다 시도하십시오. 감사.

2. 일치하지 않는 방법 (이메일 대 암호 토큰)

예를 들면 :

    var code = await userManager.GenerateEmailConfirmationTokenAsync(user.Id);

    var result = await userManager.ResetPasswordAsync(user.Id, code, newPassword);

email-token-provide가 생성 한 토큰은 reset-password-token-provider가 확인할 수 없습니다.

그러나 우리는 이것이 일어나는 이유의 근본 원인을 볼 것입니다.

3. 토큰 공급자의 다른 인스턴스

다음을 사용하는 경우에도 :

var token = await _userManager.GeneratePasswordResetTokenAsync(user.Id);

와 함께

var result = await _userManager.ResetPasswordAsync(user.Id, HttpUtility.UrlDecode(token), newPassword);

오류가 여전히 발생할 수 있습니다.

내 이전 코드는 이유를 보여줍니다.

public class AccountController : Controller
{
    private readonly UserManager _userManager = UserManager.CreateUserManager(); 

    [AllowAnonymous]
    [HttpPost]
    public async Task<ActionResult> ForgotPassword(FormCollection collection)
    {
        var token = await _userManager.GeneratePasswordResetTokenAsync(user.Id);
        var callbackUrl = Url.Action("ResetPassword", "Account", new { area = "", UserId = user.Id, token = HttpUtility.UrlEncode(token) }, Request.Url.Scheme);

        Mail.Send(...);
    }

과:

public class UserManager : UserManager<IdentityUser>
{
    private static readonly UserStore<IdentityUser> UserStore = new UserStore<IdentityUser>();
    private static readonly UserManager Instance = new UserManager();

    private UserManager()
        : base(UserStore)
    {
    }

    public static UserManager CreateUserManager()
    {
        var dataProtectionProvider = new DpapiDataProtectionProvider();
        Instance.UserTokenProvider = new DataProtectorTokenProvider<IdentityUser>(dataProtectionProvider.Create());

        return Instance;
    }

이 코드에서는 a UserManager가 생성 (또는 new-ed) 될 때마다 new dataProtectionProvider도 생성 된다는 점에 유의하십시오 . 따라서 사용자가 이메일을 받고 링크를 클릭하면 :

public class AccountController : Controller
{
    private readonly UserManager _userManager = UserManager.CreateUserManager();
    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> ResetPassword(string userId, string token, FormCollection collection)
    {
        var result = await _userManager.ResetPasswordAsync(user.Id, HttpUtility.UrlDecode(token), newPassword);
        if (result != IdentityResult.Success)
            return Content(result.Errors.Aggregate("", (current, error) => current + error + "\r\n"));
        return RedirectToAction("Login");
    }

AccountController더 이상 이전하지 않습니다, 그리고없는 둘 _userManager과 토큰 공급자입니다. 따라서 새 토큰 공급자는 메모리에 해당 토큰이 없기 때문에 실패합니다.

따라서 토큰 공급자에 대해 단일 인스턴스를 사용해야합니다. 다음은 내 새 코드이며 잘 작동합니다.

public class UserManager : UserManager<IdentityUser>
{
    private static readonly UserStore<IdentityUser> UserStore = new UserStore<IdentityUser>();
    private static readonly UserManager Instance = new UserManager();

    private UserManager()
        : base(UserStore)
    {
    }

    public static UserManager CreateUserManager()
    {
        //...
        Instance.UserTokenProvider = TokenProvider.Provider;

        return Instance;
    }

과:

public static class TokenProvider
{
    [UsedImplicitly] private static DataProtectorTokenProvider<IdentityUser> _tokenProvider;

    public static DataProtectorTokenProvider<IdentityUser> Provider
    {
        get
        {

            if (_tokenProvider != null)
                return _tokenProvider;
            var dataProtectionProvider = new DpapiDataProtectionProvider();
            _tokenProvider = new DataProtectorTokenProvider<IdentityUser>(dataProtectionProvider.Create());
            return _tokenProvider;
        }
    }
}

우아한 솔루션이라고 할 수는 없지만 뿌리를 내리고 내 문제를 해결했습니다.


다음과 같은 코드에서도 "잘못된 토큰"오류가 발생했습니다.

var emailCode = UserManager.GenerateEmailConfirmationToken(id);
var result = UserManager.ConfirmEmail(id, emailCode);

제 경우에 문제는 제가 사용자를 수동으로 생성하고 그 UserManager.Create(...)방법 을 사용하지 않고 데이터베이스에 추가한다는 입니다. 사용자가 데이터베이스에 있지만 보안 스탬프가 없습니다.

GenerateEmailConfirmationToken보안 스탬프 부족에 대해 불평하지 않고 토큰을 반환 한 것이 흥미롭지 만 그 토큰은 검증 할 수 없습니다.


그 외에는 인코딩되지 않으면 코드 자체가 실패하는 것을 보았습니다.

최근에 다음과 같은 방식으로 인코딩을 시작했습니다.

string code = manager.GeneratePasswordResetToken(user.Id);
code = HttpUtility.UrlEncode(code);

그리고 다시 읽을 준비가되면 :

string code = IdentityHelper.GetCodeFromRequest(Request);
code = HttpUtility.UrlDecode(code);

솔직히 말해서 애초에 제대로 인코딩되지 않은 것이 놀랍습니다.


제 경우에는 AngularJS 앱이 모든 더하기 기호 (+)를 빈 공간 ( "")으로 변환했기 때문에 토큰이 다시 전달되었을 때 실제로 유효하지 않았습니다.

이 문제를 해결하기 위해 AccountController의 ResetPassword 메서드에서 암호를 업데이트하기 전에 간단히 교체를 추가했습니다.

code = code.Replace(" ", "+");
IdentityResult result = await AppUserManager.ResetPasswordAsync(user.Id, code, newPassword);

이것이 웹 API 및 AngularJS에서 Identity로 작업하는 다른 사람에게 도움이되기를 바랍니다.


string code = _userManager.GeneratePasswordResetToken(user.Id);

                code = HttpUtility.UrlEncode(code);

// 나머지 이메일 보내기


코드를 해독하지 마십시오

var result = await _userManager.ResetPasswordAsync(user.Id, model.Code, model.Password); 

내가 한 일은 다음과 같습니다 : URL을 인코딩 한 후 토큰 디코딩 (간단히)

먼저 생성 된 사용자 GenerateEmailConfirmationToken을 인코딩해야했습니다. (위의 표준 조언)

    var token = await userManager.GenerateEmailConfirmationTokenAsync(user);
    var encodedToken = HttpUtility.UrlEncode(token);

컨트롤러의 "확인"작업에서 토큰을 검증하기 전에 해독해야했습니다.

    var decodedCode = HttpUtility.UrlDecode(mViewModel.Token);
    var result = await userManager.ConfirmEmailAsync(user,decodedCode);

tl; dr : MachineKey 보호 대신 AES 암호화를 사용하려면 aspnet 코어 2.2 에 사용자 지정 토큰 공급자를 등록합니다 . 요점 : https://gist.github.com/cyptus/dd9b2f90c190aaed4e807177c45c3c8b

aspnet core 2.2cheny가 토큰 공급자의 인스턴스가 동일해야한다고 지적했듯이 에서 동일한 문제가 발생했습니다 . 이것은 나를 위해 작동하지 않습니다.

  • 나는 different API-projects토큰을 생성하고 암호를 재설정하는 토큰을 받았습니다.
  • API는 different instances가상 머신에서 실행될 수 있으므로 머신 키가 동일하지 않습니다.
  • API는 더 이상 restart토큰이 아니기 때문에 유효하지 않을 수 있습니다.same instance

내가 사용할 수있는 services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo("path"))파일 시스템 및 회피를 다시 시작하고 여러 인스턴스 공유 문제에 토큰을 저장하지만, 각 프로젝트는 자신의 파일을 생성로, 여러 프로젝트와 함께 문제를 주위에 가져올 수 없습니다.

나를위한 해결책은 MachineKey 데이터 보호 로직 AES then HMAC을 시스템, 인스턴스 및 프로젝트간에 공유 할 수있는 내 설정의 키로 토큰을 대칭 암호화하는 데 사용하는 자체 로직으로 대체하는 것입니다. 암호화에서 암호화 논리를 가져와 C #에서 문자열을 해독합니까? (요점 : https://gist.github.com/jbtule/4336842#file-aesthenhmac-cs ) 사용자 지정 TokenProvider를 구현했습니다.

    public class AesDataProtectorTokenProvider<TUser> : DataProtectorTokenProvider<TUser> where TUser : class
    {
        public AesDataProtectorTokenProvider(IOptions<DataProtectionTokenProviderOptions> options, ISettingSupplier settingSupplier)
            : base(new AesProtectionProvider(settingSupplier.Supply()), options)
        {
            var settingsLifetime = settingSupplier.Supply().Encryption.PasswordResetLifetime;

            if (settingsLifetime.TotalSeconds > 1)
            {
                Options.TokenLifespan = settingsLifetime;
            }
        }
    }
    public class AesProtectionProvider : IDataProtectionProvider
    {
        private readonly SystemSettings _settings;

        public AesProtectionProvider(SystemSettings settings)
        {
            _settings = settings;

            if(string.IsNullOrEmpty(_settings.Encryption.AESPasswordResetKey))
                throw new ArgumentNullException("AESPasswordResetKey must be set");
        }

        public IDataProtector CreateProtector(string purpose)
        {
            return new AesDataProtector(purpose, _settings.Encryption.AESPasswordResetKey);
        }
    }
    public class AesDataProtector : IDataProtector
    {
        private readonly string _purpose;
        private readonly SymmetricSecurityKey _key;
        private readonly Encoding _encoding = Encoding.UTF8;

        public AesDataProtector(string purpose, string key)
        {
            _purpose = purpose;
            _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
        }

        public byte[] Protect(byte[] userData)
        {
            return AESThenHMAC.SimpleEncryptWithPassword(userData, _encoding.GetString(_key.Key));
        }

        public byte[] Unprotect(byte[] protectedData)
        {
            return AESThenHMAC.SimpleDecryptWithPassword(protectedData, _encoding.GetString(_key.Key));
        }

        public IDataProtector CreateProtector(string purpose)
        {
            throw new NotSupportedException();
        }
    }

내 설정을 제공하기 위해 내 프로젝트에서 사용하는 SettingsSupplier

    public interface ISettingSupplier
    {
        SystemSettings Supply();
    }

    public class SettingSupplier : ISettingSupplier
    {
        private IConfiguration Configuration { get; }

        public SettingSupplier(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public SystemSettings Supply()
        {
            var settings = new SystemSettings();
            Configuration.Bind("SystemSettings", settings);

            return settings;
        }
    }

    public class SystemSettings
    {
        public EncryptionSettings Encryption { get; set; } = new EncryptionSettings();
    }

    public class EncryptionSettings
    {
        public string AESPasswordResetKey { get; set; }
        public TimeSpan PasswordResetLifetime { get; set; } = new TimeSpan(3, 0, 0, 0);
    }

마지막으로 Startup에 공급자를 등록합니다.

 services
     .AddIdentity<AppUser, AppRole>()
     .AddEntityFrameworkStores<AppDbContext>()
     .AddDefaultTokenProviders()
     .AddTokenProvider<AesDataProtectorTokenProvider<AppUser>>(TokenOptions.DefaultProvider);


 services.AddScoped(typeof(ISettingSupplier), typeof(SettingSupplier));
//AESThenHMAC.cs: See https://gist.github.com/jbtule/4336842#file-aesthenhmac-cs

아마도 이것은 오래된 스레드 일지 모르지만 경우에 따라이 오류가 무작위로 발생하여 머리를 긁적입니다. 나는 모든 스레드를 확인하고 각 제안을 확인했지만 "무효 토큰"으로 반환 된 일부 코드는 무작위로 보였습니다. 사용자 데이터베이스에 대한 몇 가지 쿼리 후에 마침내 사용자 이름의 공백 또는 기타 영숫자가 아닌 문자와 직접 관련된 "유효하지 않은 토큰"오류를 발견했습니다. 해결책은 찾기 쉬웠습니다. 사용자 이름에 해당 문자를 허용하도록 UserManager를 구성하기 만하면됩니다. 이는 사용자 관리자가 이벤트를 생성 한 직후에 수행 할 수 있으며 다음과 같은 방식으로 해당 속성을 false로 설정하는 새 UserValidator 설정을 추가 할 수 있습니다.

 public static UserManager<User> Create(IdentityFactoryOptions<UserManager<User>> options, IOwinContext context)
    {
        var userManager = new UserManager<User>(new UserStore());

        // this is the key 
        userManager.UserValidator = new UserValidator<User>(userManager) { AllowOnlyAlphanumericUserNames = false };


        // other settings here
        userManager.UserLockoutEnabledByDefault = true;
        userManager.MaxFailedAccessAttemptsBeforeLockout = 5;
        userManager.DefaultAccountLockoutTimeSpan = TimeSpan.FromDays(1);

        var dataProtectionProvider = options.DataProtectionProvider;
        if (dataProtectionProvider != null)
        {
            userManager.UserTokenProvider = new DataProtectorTokenProvider<User>(dataProtectionProvider.Create("ASP.NET Identity"))
            {
                TokenLifespan = TimeSpan.FromDays(5)
            };
        }

        return userManager;
    }

이것이 저처럼 "늦은 도착"에 도움이되기를 바랍니다.


생성 할 때 다음을 사용하는지 확인하십시오.

GeneratePasswordResetTokenAsync(user.Id)

그리고 다음을 사용하는지 확인하십시오.

ResetPasswordAsync(user.Id, model.Code, model.Password)

If you make sure you are using the matching methods, but it still doesn't work, please verify that user.Id is the same in both methods. (Sometimes your logic may not be correct because you allow using same email for registry, etc.)


Make sure that the token that you generate doesn't expire rapidly - I had changed it to 10 seconds for testing and it would always return the error.

    if (dataProtectionProvider != null) {
        manager.UserTokenProvider =
           new DataProtectorTokenProvider<AppUser>
              (dataProtectionProvider.Create("ConfirmationToken")) {
               TokenLifespan = TimeSpan.FromHours(3)
               //TokenLifespan = TimeSpan.FromSeconds(10);
           };
    }

We have run into this situation with a set of users where it was all working fine. We have isolated it down to Symantec's email protection system which replaces links in our emails to users with safe links that go to their site for validation and then redirects the user to the original link we sent.

The problem is that they are introducing a decode... they appear to do a URL Encode on the generated link to embed our link as a query parameter to their site but then when the user clicks and clicksafe.symantec.com decodes the url it decodes the first part they needed to encode but also the content of our query string and then the URL that the browser gets redirected to has been decoded and we are back in the state where the special characters mess up the query string handling in the code behind.


Here I've the same problem but after a lot of time I found that in my case the invalid token error was raised by the fact that my custom Account class has the Id property re-declared and overridden.

Like that:

 public class Account : IdentityUser
 {
    [ScaffoldColumn(false)]
    public override string Id { get; set; } 
    //Other properties ....
 }

So to fix it I've just removed that property and generated again the database schema just to be sure.

Removing this solves the problem.


In my case, I just need to do HttpUtility.UrlEncode before sending an email. No HttpUtility.UrlDecode during reset.


My problem was that there was a typo in the email containing the ConfirmationToken:

<p>Please confirm your account by <a href=@ViewBag.CallbackUrl'>clicking here</a>.</p>

This meant the extra apostrophe was appended to the end of the ConfirmationToken.

D'oh!


My issue was that I was missing a <input asp-for="Input.Code" type="hidden" /> control in my Reset Password form

<form role="form" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<input asp-for="Input.Code" type="hidden" />

Related to chenny's 3. Different instances of token providers .

In my case I was passing IDataProtectionProvider.Create a new guid every time it got called, which prevented existing codes from being recognized in subsequent web api calls (each request creates its own user manager).

Making the string static solved it for me.

private static string m_tokenProviderId = "MyApp_" + Guid.NewGuid().ToString();
...
manager.UserTokenProvider =
  new DataProtectorTokenProvider<User>(
  dataProtectionProvider.Create(new string[1] { m_tokenProviderId } ))
  {
      TokenLifespan = TimeSpan.FromMinutes(accessTokenLifespan)
  };

ReferenceURL : https://stackoverflow.com/questions/25405307/asp-net-identity-2-giving-invalid-token-error

반응형