6

I've successfully build simple registration system using ASP.NET Identity 2.2.1.
I only needed API so I've build simple controller that allows creating account using route account/create, when account is created user receives SMS message with token needed to confirm his phone, same time I'm generating link that is needed to verify email address and I'm sending it to user email address. Then user need to input token from SMS (do request to account/confirm_phone) and click link in email he receives.
These two actions are needed to activate account, this part works fine.

I have requirement to change email link to email token, similar to one used to confirm phone number, so instead of clicking link user will have to input that token (request to account/confirm_email)

Method GenerateEmailConfirmationTokenAsync returns very long code that becomes part of link send in email, I'd like it to return 6 digit token.

I can't reuse GeneratePhoneConfirmationTokenAsync because this will generate same token as send via SMS.

I've searched over the internet and I wasn't able to find any information how to customize token generated by GenerateEmailConfirmationTokenAsync method.

Is it possible to configure that token generator so it will return 6 (or any configurable length) digit code that I can use to confirm user email address.

I'm aware that link send to user is better option, but as I wrote this is requirement I got and I can't change that.

Misiu
  • 4,738
  • 21
  • 94
  • 198
  • So you are sending confirmation to both email and a mobile? – trailmax Oct 26 '16 at 13:57
  • @trailmax yes, but these shoul be different codes. One for confirming phone number, second for email address. – Misiu Oct 26 '16 at 14:13
  • I see. In one of the systems I've done similar thing. But I've stored a confirmation code with expiry time. And then compared user-provided to the stored one. You can do the same - store both codes with expiry timestamp and generate the codes yourself (not with Identity). – trailmax Oct 26 '16 at 14:16
  • @trailmax thanks for hint. I was thinking about similar thing, but I've looked at source code of Identity and I found code responsible for generating phone number tokens. I was thinking about using similar thing but with email as modifier instead of phone number. I' like to avoid those two fields in database and generate that token from salt and email address, but I don't know how to set lifetime of that nee token. – Misiu Oct 26 '16 at 14:23
  • 1
    You are on the good track then. I recall I've seen lifetime property somewhere in token generation code. Keep digging. I can have a look on that tonight if you still need it – trailmax Oct 26 '16 at 14:36
  • UserManager uses Rfc6238AuthenticationService to generate and validate code, but I can't figure out how to specify lifetime of that token. Ideally I'd like to be specify different token times, for example 5 minutes for token send via SMS and 24 hours to one send via email. – Misiu Oct 26 '16 at 14:52
  • What about generating your own code and using it as a lookup to the code generated by the Identity Framework? This will allow you to use the identity framework as is and with out altering any of the features/behavior around codes. It does not have to know that the code it is presented is actually looked up based on another code you send your user. – Igor Oct 26 '16 at 16:37
  • @Igor You're suggesting to store original token generated by Identity Framework in database and assign random short token to it and then serch for original token when user inputs short token ? This way I must store tokems in database which I'd like to avoid. As I wrote before I want to generate token based in salt and email address. Problem is lifetime and number of tries user has to enter right token. – Misiu Oct 26 '16 at 16:43
  • @Misiu - I am not sure how you will get around it but I will keep my eyes on your question to see if you (or someone else) figures out a good solution. As to `generate token based in salt and email address`, do it on `SecurityStamp` instead. This changes any time the users credentials change (username and password) and is used when generating a login token which forces all existing cached tokens to be invalidated. If you want to use email as username (or something like that) then include the email address with the SecurityStamp. – Igor Oct 26 '16 at 17:13
  • @Igor sorry, it should be `SecurityStamp` instead of `salt`. For now I've implemented custom `Rfc6238AuthenticationService` that allows specifying timespan, but this has some limitations. if time step is large in some cases token can be valid too long. For example if I specify 1 hour and user will generate token one second after full hour that token will be valid current hour + next one. I think it will be impossible to generate such short code and have it valid exactly 1 hour without storing some kind of hash in db. Maybe there is a way, but I didn't figure it out yet – Misiu Oct 27 '16 at 08:10

2 Answers2

2

I've had a good careful look on this and I'm afraid you are out of luck.

ASP.Net Identity framework (v2.2.x) has 2 token providers: DataProtectorTokenProvider and EmailTokenProvider.

DataProtectorTokenProvider gives you a very long token that is no good for manual copy-paste. But it allows to set a timespan: DataProtectorTokenProvider.TokenLifespan. By default it is set to 1 day - see constractor of that class. Token generated by this provider contains current UTC time, userId, security stamp and additional string purpose which can be anything, but on validation this must be the same as on generation. This is usually is "email" or "phone". Then all that data is encrypted by IDataProtector. Hence you get very long and nasty looking string.

You can have variable life span for different tokens, but you'll have to do a magic dance around setting correct value in DataProtectorTokenProvider.TokenLifespan before validating every particular token.

Another option EmailTokenProvider - this class is inherited from TotpSecurityStampBasedTokenProvider. This token provider generates One Time Password (OTP) based on Rfc6238. This token is time-sensitive and becomes invalid once the time-window is passed. See section 5.2 for more details. Default time-window for identity is set to be 3 minutes. See line:

private static readonly TimeSpan _timestep = TimeSpan.FromMinutes(3);

in Rfc6238AuthenticationService. But Rfc6238 says that implementation must allow for more than 1 time-window. Given implementation allows total of 6 minutes token lifespan. And there is no way to change that without implementing your own Rfc6238.

So Identity does not provide you the means to implement your requirement - you'll have to generate tokens yourself and store them with the timestamp. Perhaps only one of them - short token with longer lifespan. The default SMS implementation is already short-lived and short for manual entry.

Community
  • 1
  • 1
trailmax
  • 34,305
  • 22
  • 140
  • 234
  • Thank You for Your investigation. I was thinking about using similar approach as `GenerateChangePhoneNumberTokenAsync` and `VerifyChangePhoneNumberTokenAsync` in `UserManager`, but this would give me 3 minute tokens, so I'll probably copy `Rfc6238AuthenticationService` code and modify it to be able to specify timestep. One think I'm not sure is if I change `_timespan` from 3 minutes to lets say 4 yours token will be valid 4 hours from now or 8 hours? It's bit confusing in Your answer. Could You please clarify that a bit? – Misiu Oct 27 '16 at 06:17
  • I've modified that rfc6238 authentication service you mentioned. I've added parameter for specifying time step, but because this all is quite new to me I'm not sure if I did everything correctly. Could You please take a look at it? https://gist.github.com/Misiu/c2822de165e347ede71750ab20b6c12a – Misiu Oct 27 '16 at 06:37
  • @Misiu Sorry, I don't know enough about this algorithm. [Here](https://gist.github.com/Misiu/c2822de165e347ede71750ab20b6c12a#file-myrfc6238authenticationservice-cs-L75) it is actually validated, but I've no idea what happens there with -2..+2. The only way to check would be to write unit tests and replace `DateTime.UtcNow` [here](https://gist.github.com/Misiu/c2822de165e347ede71750ab20b6c12a#file-myrfc6238authenticationservice-cs-L45) with a parameter that you can change to simulate changed time. – trailmax Oct 27 '16 at 14:46
  • @Misiu I've used code from this [answer](http://stackoverflow.com/a/2425739/809357) to do unit tests where `DateTime.UtcNow` was used. [Part of my project](https://github.com/AMVSoftware/NSaga/blob/f2b32ce13f9ae378cf69acee86fd6af98f67cfd0/src/NSaga/Implementations/TimeProvider.cs) - for reference. And [this is how it is used in tests](https://github.com/AMVSoftware/NSaga/blob/0826c75974d4128e7a45984fe8697017e70a05dd/src/Tests/PipelineHook/MetadataPipelineHookConsumingTests.cs#L25). – trailmax Oct 27 '16 at 14:51
  • Thank You for Your time and help. I've tested my token implementation. Problem is algorithm - it use time windows, not specific time as expiration date. If I generate token just couple of minutes after midnight and set it validity as 24 hours it will be valid 2 days (till end of current day and next), same with 1 hour tokens, they are valid till end of current hour + one hour. I'll probably will edit my user store and add 3 extra fields, one for token hash and second for token expiration date and third for number to ties. This way I'll be sure expiration time is correct. – Misiu Oct 28 '16 at 09:59
  • @misiu yes, indeed the time window works like that. And yes, it will be easier to have the tokens stored in a way with number of retries and expiration date. – trailmax Oct 28 '16 at 10:10
  • @Misiu I have the same requirement. Could you elaborate on your solution. Maybe you could answer your question with some code or edit your original question with the solution you have applied. – Mounhim Nov 25 '18 at 20:42
  • @Mounhim I dig thru the code and here are some tips. I've built custom UserStore and custom UserManager classes. My code is based on phone number confirmation mechanism, I dig thru source code of ASP Identity. I had to add 3 fields to my Users table - `EmailTokenHash, EmailTokenExpirationDateUtc, EmailTokenFailedCount`. Basically, I'm using `RNGCryptoServiceProvider` to generate a random string and store hash in DB, I set token expiration and number of allowed tries. Then when checking if the token is valid I check the expiration date, the number of tries and I compare hashes. – Misiu Nov 26 '18 at 08:16
  • Thank you. Sorry to bother you again, but if you could provide some sample code I would be even more grateful. – Mounhim Nov 29 '18 at 10:32
  • You can bypass the `DataProtectorTokenProvider` and configure to use the `EmailTokenProvider`: https://stackoverflow.com/a/69735988/129269 – riezebosch Oct 28 '21 at 06:33
  • @riezebosch you link to answer for Asp.Net Core. This QA is about Identity 2.2 which is .Net Framework. Your answer is not applicable here. – trailmax Oct 28 '21 at 14:31
0

This may be not so easy, but the way to do it would be to write your own IUserTokenProvider implementation.

public class CustomTokenProvider : IUserTokenProvider<ApplicationUser, string>
{
    public Task<string> GenerateAsync(string purpose, UserManager<ApplicationUser, string> manager, ApplicationUser user)
    {
        ???
    }

    public Task<bool> IsValidProviderForUserAsync(UserManager<ApplicationUser, string> manager, ApplicationUser user)
    {
        ???
    }

    public Task NotifyAsync(string token, UserManager<ApplicationUser, string> manager, ApplicationUser user)
    {
        ???
    }

    public Task<bool> ValidateAsync(string purpose, string token, UserManager<ApplicationUser, string> manager, ApplicationUser user)
    {
        ???
    }
}

and then in your ApplicationUserManager's Create method:

manager.UserTokenProvider = new CustomTokenProvider<ApplicationUser, string>();
patrykgliwinski
  • 316
  • 1
  • 8
  • Thank You for answer, but I don't want to change UserTokenProvider, because I'd like to leave reset password functionality as it is. I only need other way of email confirmation - via token. – Misiu Oct 26 '16 at 16:37