The missing .NET Core & ASP.NET Core request signing library. Generate links that is valid with specific parameters for specific duration and optinally that are valid for only once!
The generated URL contains at least a signature as query parameter, but it may optinally contain expiration time and allowed HTTP method(s).
For example,
/reset?email=buraktamturk@gmail.com&code=8295&method=GET,POST&exp=1545888427&sig=c23752ab1f08385285d85ce8224604e477b17f7cf3356ec0e4bf4db5b9a7415a
This URL is generated by SignRequest function and the user must submit the same path and parameters (parameter order does not matter, as ordering made internally before signature checking!). Good thing is request body can have different content, so you can send signed e-mail reset links that is valid for 10 minutes without storing anything on your database.
This library provides a way to generate URLs with hidden query parameters. This means you can send a random code using SMS to verify the users phone number. And you can return this URL in the AJAX call.
/reset?email=buraktamturk@gmail.com&method=GET,POST&exp=1545888427&sig=c23752ab1f08385285d85ce8224604e477b17f7cf3356ec0e4bf4db5b9a7415a
Since the signature generated with "code=8295", the returned url will not work by default. The client has to send the correct code parameter in order to call this function, which verifies the user got the valid code ;)
This assembly provides basic functionality that is used to sign and validate requests. Also, it comes with InMemory hash-table for revoking URLs even before their expiration time.
This assembly provides extension methods for ASP.NET Core classes to the Tamturk.RequestSigning assembly.
Sample web project is included in the samples folder.
- Call AddRequestSigning and optionally AddInMemoryRevokedHashTable on your startup class.
public void ConfigureServices(IServiceCollection services) {
services
.AddRequestSigning(Configuration["signingKey"]) // use HMACSHA256 with this key that is taken from appconfig
.AddInMemoryRevokedHashTable() // use in memory table to store revoked tokens (optional!)
//
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
- Inject RequestSigning class on your controller.
public class MyController : Controller {
private RequestSigning requestSigning;
public MyController(Tamturk.RequestSigning requestSigning) {
this.requestSigning = requestSigning;
}
// methods
}
- Generate URLs with expiration and send them over email, and generate URLs with hidden content that user needs to supply. (this example refers forget password action)
[HttpGet("forgotpw")]
public dynamic get(string email) {
if (email == null) {
throw new Exception("Please enter an e-mail");
}
int randomNumber = new Random().Next(1000, 9999);
// sign request for 10 minutes hash it with hidden query string values and make
// client estimate correct code in 10 minutes
var expire_date = DateTimeOffset.UtcNow.AddMinutes(10);
var linkWithoutCode = requestSigning.SignRequest(
"GET,POST",
"/reset",
new Dictionary<string, string>() {
{ "email", email }
},
expire_date,
new Dictionary<string, string>() {
{ "code", randomNumber.ToString() }
}
);
// example return /reset?email=buraktamturk@gmail.com&method=GET,POST&exp=1545888427&sig=c23752ab1f08385285d85ce8224604e477b17f7cf3356ec0e4bf4db5b9a7415a
// sign request for 10 minutes, include the code here also, and sent to
// user e-mail so he can go there and reset
var linkWithCode = requestSigning.SignRequest(
"GET,POST",
"/reset",
new Dictionary<string, string>() {
{ "email", email },
{ "code", randomNumber.ToString() }
},
expire_date
);
// example return /reset?email=buraktamturk@gmail.com&code=8295&method=GET,POST&exp=1545888427&sig=c23752ab1f08385285d85ce8224604e477b17f7cf3356ec0e4bf4db5b9a7415a
// we do not have to store code, or this request, anywhere else!
return new {
message = "Assume 'has_sent' was sent to e-mail address. And has_returned returned to the client. These links valid only for 10 minutes!",
has_sent = new {
code = randomNumber,
link = linkWithCode
},
has_returned = linkWithoutCode
};
}
- Right now, you have a signed URL that you can send to the email, along with an optional code. If all of the query strings that is included in linkWithCode variable is not passed, validation fails.
[HttpGet("reset")]
public bool reset(string email) {
// is any of the query parameters (incl. code) invalid?
if (!Request.TryValidateRequest()) {
return false;
}
// or you can use this instead, which will throw exception
// Request.ValidateRequest();
// this link used before?
if (Request.IsRevoked()) {
return false;
}
// or use Request.ThrowIfRevoked();
return true; // congrats, code is correct and not used, so redirect user to ask new password page.
}
- Reset user password and Revoke token, Revoking invalidates the url and makes it expire before the expiration date, by using in-memory hash table.
// we can also return void because we can return 2xx on success 4xx or 5xx on failure.
[HttpPost("reset")]
public async Task<string> reset(
// we can get email as plain text from query string, because of the hash, the user
// can not alter the email address that we set it from forgetpw endpoint
[FromQuery] string email,
[FromBody] ResetPasswordModel model) {
Request.ValidateRequest(); // if code is incorrect, or link is timeout, this will throw exception
// you may check your password strength here
// if you check it later,
// use will not be able to submit with a new password
await Request.RevokeAsync();
// this will throw exception if the same link used for password reset twice
// you may also use Sync version of this (Revoke),
// some backends may take advantage of async functions but all of them can be called synchronously also
/*
* If you omit calling Revoke function (which is optional anyway), the same link
* may be used to reset password as many times
* till the expiration date that is set in
* forgotpw endpoint (10 minutes)
*/
// update user in db and save hashed & salted password to db.
return "YOUR PASSWORD OF " + email + " IS SUCCESSFULLY RESET.";
}
- So you can basically send secure URLs without storing tokens anywhere. Revoking is complately optional and it makes user not be able to reset password twice during the lifespan on the URL. Expiration time is put on the URL before the signature is generated. So you can make it expire in 10 minutes and forget about revoking it.
Pull requests and issues are welcome!
© 2018 Burak Tamturk
Released under the MIT LICENSE