How to disable / invalidate JWT tokens? - .net-core

This is how I create JWT tokens for my .NET Core API and it's working perfectly fine, but I'd like to implement the possibility to revoke, disable or invalidate JWT tokens when an HTTP request comes asking for it, with the token in the header.
I can think of a way where I would store the token in my database and have a boolean column indicating whether the token is active or not, but is there a way to do it without storing the token in the database at all?
public UserAuthenticationResponse CreateToken(UserAuthenticationRequest userAuth)
{
var user = // try to find user in database...
if (user == null)
{
return null;
}
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim("user_id", userAuth.Id),
}),
Expires = DateTime.UtcNow.AddMinutes(5),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var authenticatedUser = new UserAuthenticationResponse();
authenticatedUser.Id = user.Id;
authenticatedUser.Token = tokenHandler.WriteToken(token);
return authenticatedUser;
}

What I did in the end was create middleware which I put at the top of my pipeline. I held a List<string> that represented a list of blacklisted tokens (which I later clean up once they expire).
When I were validating the token myself, I was checking the BlacklistToken(token, timeout) method. If it returns true, I can blacklist the token and the next time the user tries to access something with that token, it won't let him do it. Then, sometime later, I call CleanupTokens(tokenProvider) which gets all tokens, checks if they're expired (thanks to the tokenProvider that gets the token expiration date) and if so, it removes them from the list.
public class TokenPair
{
[JsonProperty(PropertyName = "token")]
public string Token { get; set; }
[JsonProperty(PropertyName = "userId")]
public string UserID { get; set; }
}
public interface ITokenLocker
{
bool BlacklistToken(string token, int timeout);
void CleanupTokens(ITokenProvider tokenProvider);
bool IsBlacklisted(string token);
}
public class TokenLocker : ITokenLocker
{
private List<string> _blacklistedTokens;
public TokenLocker()
{
_blacklistedTokens = new List<string>();
}
public bool BlacklistToken(string token, int timeout)
{
lock (_blacklistedTokens)
{
if (!_blacklistedTokens.Any(x => x == token))
{
_blacklistedTokens.Add(token);
return true;
}
}
Thread.Sleep(timeout);
lock (_blacklistedTokens)
{
if (!_blacklistedTokens.Any(x => x == token))
{
_blacklistedTokens.Add(token);
return true;
}
else
return false;
}
}
public void CleanupTokens(ITokenProvider tokenProvider)
{
lock (_blacklistedTokens)
{
for (int i = 0; i < _blacklistedTokens.Count; i++)
{
var item = _blacklistedTokens[i];
DateTime expiration = tokenProvider.GetExpiration(item);
if (expiration < DateTime.UtcNow)
{
_blacklistedTokens.Remove(item);
i--;
}
}
}
}
public bool IsBlacklisted(string token)
{
return _blacklistedTokens.Any(tok => tok == token);
}
}
public class TokenMiddleware
{
private readonly RequestDelegate _next;
public TokenMiddleware(RequestDelegate next)
{
_next = next;
}
private string GetToken(HttpContext ctx)
{
ctx.Request.EnableBuffering();
using (var reader = new StreamReader(ctx.Request.Body, Encoding.UTF8, true, 1024, true))
{
var jsonBody = reader.ReadToEnd();
var body = JsonConvert.DeserializeObject<TokenPair>(jsonBody);
ctx.Request.Body.Position = 0;
if (body != null && body.Token != null)
{
return body.Token;
}
}
return string.Empty;
}
public async Task InvokeAsync(HttpContext context,
ITokenLocker tokenLocker
)
{
var ctx = context;
if (tokenLocker.IsBlacklisted(GetToken(ctx)))
{
int statusCode = (int)HttpStatusCode.Unauthorized;
ctx.Response.StatusCode = statusCode;
var response = FaziHttpResponse.Create(statusCode, "Unauthorized: Invalid / Expired token");
await context.Response.WriteAsync(JsonConvert.SerializeObject(response));
return;
}
await _next(context);
}
}

Related

Net Core 3.1 API with Identity Server 4 custom password validation

I'm building an API using identity server and I need to use an existing database. The users password are stored with a custom hash password.
I use FindClientByIdAsync to validate user and password, but as the password is encrypted in a non-standard algorithm I get invalid_client error message. If I change in execution time (with breakpoint) the value of password for an unencrypted value the authentication works.
It's possible change the client_secret validation for FindClientByIdAsync?
Custom ClientStore class
public class ClientStore : IClientStore
{
private readonly IMyUserRepository myUserRepository;
public ClientStore(IMyUserRepository myUserRepository)
{
this.myUserRepository = myUserRepository;
}
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId()
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("My_API", "My API")
};
}
public async Task<Client> FindClientByIdAsync(string client)
{
var user = await myUserRepository.GetUserByEmailAsync(client);
if (user == null)
return null;
return new Client()
{
ClientId = client,
AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
ClientSecrets =
{
new Secret(user.Password.Sha256()) //if I change to unencrypted works, but the value in database is hashed
},
AllowedScopes = { "GOLACO_API", IdentityServerConstants.StandardScopes.OpenId }
};
}
}
Identity server configuration in Startup class
services.AddIdentityServer(options =>
{
options.Events.RaiseSuccessEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseErrorEvents = true;
})
.AddSigningCredential(GetSigningCredential()) // here I just read the private.key file
.AddInMemoryIdentityResources(ClientStore.GetIdentityResources())
.AddInMemoryApiResources(ClientStore.GetApiResources())
.AddClientStore<ClientStore>();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = configuration["Configuration"];
options.ApiName = "My_API";
options.RequireHttpsMetadata = false;
});
services.AddAuthentication()
.AddFacebook("Facebook", options =>
{
options.AppId = "1234";
options.AppSecret = "1234567890";
});
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
You have to implement IResourceOwnerPasswordValidator as Damien showed in his blog
public class CustomResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
private readonly IUserRepository _userRepository;
public CustomResourceOwnerPasswordValidator(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
if (_userRepository.ValidateCredentials(context.UserName, context.Password))
{
var user = _userRepository.FindByUsername(context.UserName);
context.Result = new GrantValidationResult(user.SubjectId, OidcConstants.AuthenticationMethods.Password);
}
return Task.FromResult(0);
}
}
And add builder.AddResourceOwnerValidator<CustomResourceOwnerPasswordValidator>(); in the startup file.

HttpContext.Current is null when using IAuthenticationFilter

I am uploading files using ng-file-upload and having some abnormal problem as the HttpContext.Current is null when using the IAuthenticationFilter. While everything working correctly when I comment the authentication filter in WebApiConfig.
Controller to Test
[HttpPost]
public IHttpActionResult Upload()
{
var current = HttpContext.Current;
if (current == null)
{
return Content(HttpStatusCode.BadRequest, Logger.Error("HttpContext.Current is null"));
}
if (current.Request != null && current.Request.Files != null)
{
var file = current.Request.Files.Count > 0 ? current.Request.Files[0] : null;
if (file != null)
{
file.SaveAs(#"C:\Temp\test.csv");
}
}
return Content(HttpStatusCode.BadRequest, Logger.Error("Should not reach here"));
}
IAuthenticationFilter
public class KeyAuthentication : Attribute, IAuthenticationFilter
{
// we only want to apply our authentication filter once on a controller or action method so return false:
public bool AllowMultiple
{
get { return false; }
}
// Authenticate the user by apiKey
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
HttpRequestMessage request = context.Request;
string apiKey = ExtractApiKey(request);
bool IsValidCustomer = await ValidateKey(apiKey);
if (IsValidCustomer)
{
var currentPrincipal = new GenericPrincipal(new GenericIdentity(apiKey), null);
context.Principal = principal;
}
else
{
context.ErrorResult = new ErrorMessageResult("Missing API Key");
}
}
// We don't want to add challange as I am using keys authenticaiton
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
return Task.FromResult(0);
}
}
Extract API Key
public static string ExtractApiKey(HttpRequestMessage request)
{
if (!request.Headers.TryGetValues("x-api-key", out IEnumerable<string> keys))
return string.Empty;
return keys.First();
}
The solution was to include "targetFramework=4.5" in the web.config as commented by #Alfredo and more details in https://stackoverflow.com/a/32338414/3973463

External Authentication ASP.NET Web API

I have Web API and the client that calls the web API in separate solutions. I need to authenticate users using local authentication with existing user data and also authenticate with external authentication like Google and Facebook.
Local Authentication works fine. But while authenticating external logins, I'm getting unauthorized response when calling api/Account/UserInfo. Please note that I'm able to generate the token from google account correctly.
Here's my AccountController.cs file.
[Authorize]
[RoutePrefix("api/Account")]
[EnableCors("*","*","*")]
public class AccountController : ApiController
{
private const string LocalLoginProvider = "Local";
private ApplicationUserManager _userManager;
public AccountController()
{
}
public AccountController(ApplicationUserManager userManager,
ISecureDataFormat<AuthenticationTicket> accessTokenFormat)
{
UserManager = userManager;
AccessTokenFormat = accessTokenFormat;
}
public ApplicationUserManager UserManager
{
get
{
return _userManager ?? Request.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
private set
{
_userManager = value;
}
}
public ISecureDataFormat<AuthenticationTicket> AccessTokenFormat { get; private set; }
// GET api/Account/UserInfo
[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
//[AllowAnonymous]
[Route("UserInfo")]
public UserInfoViewModel GetUserInfo()
{
ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
return new UserInfoViewModel
{
Email = User.Identity.GetUserName(),
HasRegistered = externalLogin == null,
LoginProvider = externalLogin != null ? externalLogin.LoginProvider : null
};
}
// POST api/Account/Logout
[Route("Logout")]
public IHttpActionResult Logout()
{
Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
return Ok();
}
// GET api/Account/ManageInfo?returnUrl=%2F&generateState=true
[Route("ManageInfo")]
public async Task<ManageInfoViewModel> GetManageInfo(string returnUrl, bool generateState = false)
{
IdentityUser user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
if (user == null)
{
return null;
}
List<UserLoginInfoViewModel> logins = new List<UserLoginInfoViewModel>();
foreach (IdentityUserLogin linkedAccount in user.Logins)
{
logins.Add(new UserLoginInfoViewModel
{
LoginProvider = linkedAccount.LoginProvider,
ProviderKey = linkedAccount.ProviderKey
});
}
if (user.PasswordHash != null)
{
logins.Add(new UserLoginInfoViewModel
{
LoginProvider = LocalLoginProvider,
ProviderKey = user.UserName,
});
}
return new ManageInfoViewModel
{
LocalLoginProvider = LocalLoginProvider,
Email = user.UserName,
Logins = logins,
ExternalLoginProviders = GetExternalLogins(returnUrl, generateState)
};
}
// POST api/Account/ChangePassword
[Route("ChangePassword")]
public async Task<IHttpActionResult> ChangePassword(ChangePasswordBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result = await UserManager.ChangePasswordAsync(User.Identity.GetUserId(), model.OldPassword,
model.NewPassword);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// POST api/Account/SetPassword
[Route("SetPassword")]
public async Task<IHttpActionResult> SetPassword(SetPasswordBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result = await UserManager.AddPasswordAsync(User.Identity.GetUserId(), model.NewPassword);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// POST api/Account/AddExternalLogin
[Route("AddExternalLogin")]
public async Task<IHttpActionResult> AddExternalLogin(AddExternalLoginBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
AuthenticationTicket ticket = AccessTokenFormat.Unprotect(model.ExternalAccessToken);
if (ticket == null || ticket.Identity == null || (ticket.Properties != null
&& ticket.Properties.ExpiresUtc.HasValue
&& ticket.Properties.ExpiresUtc.Value < DateTimeOffset.UtcNow))
{
return BadRequest("External login failure.");
}
ExternalLoginData externalData = ExternalLoginData.FromIdentity(ticket.Identity);
if (externalData == null)
{
return BadRequest("The external login is already associated with an account.");
}
IdentityResult result = await UserManager.AddLoginAsync(User.Identity.GetUserId(),
new UserLoginInfo(externalData.LoginProvider, externalData.ProviderKey));
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// POST api/Account/RemoveLogin
[Route("RemoveLogin")]
public async Task<IHttpActionResult> RemoveLogin(RemoveLoginBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result;
if (model.LoginProvider == LocalLoginProvider)
{
result = await UserManager.RemovePasswordAsync(User.Identity.GetUserId());
}
else
{
result = await UserManager.RemoveLoginAsync(User.Identity.GetUserId(),
new UserLoginInfo(model.LoginProvider, model.ProviderKey));
}
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// GET api/Account/ExternalLogin
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)]
[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
[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);
}
ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
if (externalLogin == null)
{
return InternalServerError();
}
if (externalLogin.LoginProvider != provider)
{
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
return new ChallengeResult(provider, this);
}
ApplicationUser user = await UserManager.FindAsync(new UserLoginInfo(externalLogin.LoginProvider,
externalLogin.ProviderKey));
bool hasRegistered = user != null;
if (hasRegistered)
{
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(UserManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookieIdentity = await user.GenerateUserIdentityAsync(UserManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = ApplicationOAuthProvider.CreateProperties(user.UserName);
Authentication.SignIn(properties, oAuthIdentity, cookieIdentity);
}
else
{
IEnumerable<Claim> claims = externalLogin.GetClaims();
ClaimsIdentity identity = new ClaimsIdentity(claims, OAuthDefaults.AuthenticationType);
Authentication.SignIn(identity);
}
return Ok();
}
// GET api/Account/ExternalLogins?returnUrl=%2F&generateState=true
[AllowAnonymous]
[Route("ExternalLogins")]
public IEnumerable<ExternalLoginViewModel> GetExternalLogins(string returnUrl, bool generateState = false)
{
IEnumerable<AuthenticationDescription> descriptions = Authentication.GetExternalAuthenticationTypes();
List<ExternalLoginViewModel> logins = new List<ExternalLoginViewModel>();
string state;
if (generateState)
{
const int strengthInBits = 256;
state = RandomOAuthStateGenerator.Generate(strengthInBits);
}
else
{
state = null;
}
foreach (AuthenticationDescription description in descriptions)
{
ExternalLoginViewModel login = new ExternalLoginViewModel
{
Name = description.Caption,
Url = Url.Route("ExternalLogin", new
{
provider = description.AuthenticationType,
response_type = "token",
client_id = Startup.PublicClientId,
redirect_uri = new Uri(Request.RequestUri, returnUrl).AbsoluteUri,
state = state
}),
State = state
};
logins.Add(login);
}
return logins;
}
// POST api/Account/Register
[AllowAnonymous]
[Route("Register")]
public async Task<IHttpActionResult> Register(RegisterBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var user = new ApplicationUser()
{
UserName = model.Email,
Email = model.Email,
FirstName = model.FirstName,
LastName = model.LastName,
CellNumber = model.CellNumber,
CompanyName = model.CompanyName,
Address = model.Address,
City = model.City,
Country = model.Country,
State = model.State,
Zip = model.Zip,
EmailConfirmed = true
};
IdentityResult result = await UserManager.CreateAsync(user, model.Password);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok("User Registered Successfully!");
}
// POST api/Account/RegisterExternal
[OverrideAuthentication]
//[AllowAnonymous]
[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
[Route("RegisterExternal")]
public async Task<IHttpActionResult> RegisterExternal()
{
var info = await Authentication.GetExternalLoginInfoAsync();
if (info == null)
{
return InternalServerError();
}
var user = new ApplicationUser() { UserName = info.Email, Email = info.Email };
IdentityResult result = await UserManager.CreateAsync(user);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
result = await UserManager.AddLoginAsync(user.Id, info.Login);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
protected override void Dispose(bool disposing)
{
if (disposing && _userManager != null)
{
_userManager.Dispose();
_userManager = null;
}
base.Dispose(disposing);
}
#region Helpers
private IAuthenticationManager Authentication
{
get { return Request.GetOwinContext().Authentication; }
}
private IHttpActionResult GetErrorResult(IdentityResult result)
{
if (result == null)
{
return InternalServerError();
}
if (!result.Succeeded)
{
if (result.Errors != null)
{
foreach (string error in result.Errors)
{
ModelState.AddModelError("", error);
}
}
if (ModelState.IsValid)
{
// No ModelState errors are available to send, so just return an empty BadRequest.
return BadRequest();
}
return BadRequest(ModelState);
}
return null;
}
private class ExternalLoginData
{
public string LoginProvider { get; set; }
public string ProviderKey { get; set; }
public string UserName { get; set; }
public IList<Claim> GetClaims()
{
IList<Claim> claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, ProviderKey, null, LoginProvider));
if (UserName != null)
{
claims.Add(new Claim(ClaimTypes.Name, UserName, null, LoginProvider));
}
return claims;
}
public static ExternalLoginData FromIdentity(ClaimsIdentity identity)
{
if (identity == null)
{
return null;
}
Claim providerKeyClaim = identity.FindFirst(ClaimTypes.NameIdentifier);
if (providerKeyClaim == null || String.IsNullOrEmpty(providerKeyClaim.Issuer)
|| String.IsNullOrEmpty(providerKeyClaim.Value))
{
return null;
}
if (providerKeyClaim.Issuer == ClaimsIdentity.DefaultIssuer)
{
return null;
}
return new ExternalLoginData
{
LoginProvider = providerKeyClaim.Issuer,
ProviderKey = providerKeyClaim.Value,
UserName = identity.FindFirstValue(ClaimTypes.Name)
};
}
}
private static class RandomOAuthStateGenerator
{
private static RandomNumberGenerator _random = new RNGCryptoServiceProvider();
public static string Generate(int strengthInBits)
{
const int bitsPerByte = 8;
if (strengthInBits % bitsPerByte != 0)
{
throw new ArgumentException("strengthInBits must be evenly divisible by 8.", "strengthInBits");
}
int strengthInBytes = strengthInBits / bitsPerByte;
byte[] data = new byte[strengthInBytes];
_random.GetBytes(data);
return HttpServerUtility.UrlTokenEncode(data);
}
}
#endregion
}
Here's the ConfigureAuth method in StartUp.Auth.cs file
public void ConfigureAuth(IAppBuilder app)
{
// Configure the db context and user manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.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
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
// In production mode set AllowInsecureHttp = false
AllowInsecureHttp = true
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
// Uncomment the following lines to enable logging in with third party login providers
//app.UseMicrosoftAccountAuthentication(
// clientId: "",
// clientSecret: "");
//app.UseTwitterAuthentication(
// consumerKey: "",
// consumerSecret: "");
var facebookoptions = new FacebookAuthenticationOptions()
{
AppId = "",
AppSecret = "",
BackchannelHttpHandler = new FaceBookBackChannelHandler(),
UserInformationEndpoint = "https://graph.facebook.com/v2.4/me?fields=email,first_name,last_name,gender"
};
facebookoptions.Scope.Add("email");
app.UseFacebookAuthentication(facebookoptions);
//app.UseFacebookAuthentication(
// appId: "",
// appSecret: "");
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
{
ClientId = "",
ClientSecret = ""
});
}
Also here's the jQuery methods that call the API endpoints
function getAccessToken() {
if (location.hash) {
if (location.hash.split('access_token=')) {
var accessToken = location.hash.split('access_token=')[1].split('&')[0];
if (accessToken) {
isUserRegistered(accessToken);
}
}
}
}
function isUserRegistered(accessToken) {
$.ajax({
url: 'http://localhostXXXX/api/Account/UserInfo',
method: 'GET',
cors:
{
headers: {
'content-type': 'application/JSON',
'Authorization': 'Bearer ' + accessToken
}
},
success: function (response) {
if (response.HasRegistered) {
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('userName', response.Email);
window.location.href = "Data.aspx";
}
else {
signupExternalUser(accessToken);
}
}
});
}
function signupExternalUser(accessToken) {
$.ajax({
url: 'http://localhost:XXXXX/api/Account/RegisterExternal',
method: 'POST',
cors:
{
headers: {
'content-type': 'application/json',
'Authorization': 'Bearer ' + accessToken
}
},
success: function () {
window.location.href = "";
}
});
}
Here is the code which i used for getting my informations. its work fine:
// check if authorized
if (Request["code"] == null)
{
Response.Redirect(string.Format(
"https://graph.facebook.com/oauth/authorize?client_id={0}&redirect_uri={1}&scope={2}",
app_id, Request.Url.AbsoluteUri, scope));
}
else
{
FacebookAccessToken token = new FacebookAccessToken();
FacebookUser user = new FacebookUser();
//Requesting for access token
string url = string.Format("https://graph.facebook.com/oauth/access_token?client_id={0}&redirect_uri={1}&scope={2}&code={3}&client_secret={4}",
app_id, Request.Url.AbsoluteUri, scope, Request["code"].ToString(), app_secret);
HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
{
StreamReader reader = new StreamReader(response.GetResponseStream());
string vals = reader.ReadToEnd();
// Deserialize json object
token = JsonConvert.DeserializeObject<FacebookAccessToken>(data);
}
//Getting user info
url = string.Format("https://graph.facebook.com/v2.8/me?access_token={0}", token.AccessToken);
request = WebRequest.Create(url) as HttpWebRequest;
using (var client = request.GetResponse() as HttpWebResponse)
{
StreamReader reader = new StreamReader(client.GetResponseStream());
string data = reader.ReadToEnd();
// data:"{\"name\":\"Er Vatsal D Patel\",\"id\":\"13168723650*****\"}";
user = JsonConvert.DeserializeObject<FacebookUser>(data);
}
return "done";
}

Identity Framework test if confirm email token is expired

Is it possible to test whether a confirm email token is expired using Identity Framework's UserManager? No matter what the error is, from the following:
var result = await UserManager.ConfirmEmailAsync(userId, code);
I get a generic "Invalid Token" error.
I found a way to parse the token for the date issued, which you can then check to see if is within the allowed timespan (default of 24hours if not specified).
Identity.cs
ApplicationUserManager
public IDataProtector Protector { get; set; }
public TimeSpan TokenLifespan { get; set; }
ApplicationUserManager Create()
// Explicitly set token expiration to 24 hours.
manager.TokenLifespan = TimeSpan.FromHours(24);
var dataProtectionProvider = options.DataProtectionProvider;
manager.Protector = dataProtectionProvider.Create("ASP.NET Identity");
if (dataProtectionProvider != null)
{
manager.UserTokenProvider =
new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"))
{
TokenLifespan = manager.TokenLifespan
};
}
AccountController.cs
public async Task<ActionResult> ConfirmEmail(string Code, string UserId)
{
// Try/catch, validation, etc.
var tokenExpired = false;
var unprotectedData = UserManager.Protector.Unprotect(Convert.FromBase64String(Code));
var ms = new MemoryStream(unprotectedData);
using (BinaryReader reader = new BinaryReader(ms))
{
var creationTime = new DateTimeOffset(reader.ReadInt64(), TimeSpan.Zero);
var expirationTime = creationTime + UserManager.TokenLifespan;
if (expirationTime < DateTimeOffset.UtcNow)
{
tokenExpired = true;
}
}
// Do something if token is expired, else continue with confirmation
}
I found this blog post and Nkosi's answer to be extremely helpful, and if you want to go through the Identity source code, Microsoft has it here (The previous versions of Identity for MVC5 and lower here). Also, I apologize if its in poor form to answer a question that you, yourself put a bounty on, but I couldn't help but continue looking for a better solution.
I get around this by keeping/storing a copy of the generated token
public class ApplicationUser : IdentityUser {
public string EmailConfirmationToken { get; set; }
public string ResetPasswordToken { get; set; }
}
and associating it with the user in derived UserManager<ApplicationUser>.
public override async System.Threading.Tasks.Task<string> GenerateEmailConfirmationTokenAsync(string userId) {
/* NOTE:
* The default UserTokenProvider generates tokens based on the users's SecurityStamp, so until that changes
* (like when the user's password changes), the tokens will always be the same, and remain valid.
* So if you want to simply invalidate old tokens, just call manager.UpdateSecurityStampAsync().
*/
//await base.UpdateSecurityStampAsync(userId);
var token = await base.GenerateEmailConfirmationTokenAsync(userId);
if (!string.IsNullOrEmpty(token)) {
var user = await FindByIdAsync(userId);
user.EmailConfirmationToken = token; //<<< Last issued token
//Note: If a token is generated then the current email is no longer confirmed.
user.EmailConfirmed = false;
await UpdateAsync(user);
}
return token;
}
When the token is provided for confirmation, a search for the user via the token is done.
public static class ApplicationUserManagerExtension {
public static Task<string> FindIdByEmailConfirmationTokenAsync(this UserManager<ApplicationUser> manager, string confirmationToken) {
string result = null;
ApplicationUser user = manager.Users.SingleOrDefault(u => u.EmailConfirmationToken != null && u.EmailConfirmationToken == confirmationToken);
if (user != null) {
result = user.Id;
}
return Task.FromResult(result);
}
}
If the token matches a known user that indicates that it was a validly issued token.
Will then attempt to confirm token with User manager.
If confirmation fails then token has expired and an appropriate action is taken.
Else if the token confirmed, it is removed from associated user and thus invalidating the reuse of that token.
public override async System.Threading.Tasks.Task<IdentityResult> ConfirmEmailAsync(string userId, string token) {
var user = await FindByIdAsync(userId);
if (user == null) {
return IdentityResult.Failed("User Id Not Found");
}
var result = await base.ConfirmEmailAsync(userId, token);
if (result.Succeeded) {
user.EmailConfirmationToken = null;
return await UpdateAsync(user);
} else if (user.EmailConfirmationToken == token) {
//Previously Issued Token expired
result = IdentityResult.Failed("Expired Token");
}
return result;
}
A similar approach was implemented for password reset as well.
Here comes an .NET Core 2.1 adaption of the solution provided by #Nkosi :
ApplicationUser class
public class ApplicationUser : IdentityUser
{
public string EmailConfirmationToken { get; set; }
public string ResetPasswordToken { get; set; }
}
Derived UserManager class
public class CustomUserManager : UserManager<ApplicationUser>
{
public CustomUserManager(IUserStore<ApplicationUser> store,
IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<ApplicationUser> passwordHasher,
IEnumerable<IUserValidator<ApplicationUser>> userValidators,
IEnumerable<IPasswordValidator<ApplicationUser>> passwordValidators,
ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger<UserManager<ApplicationUser>> logger)
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
{
}
public override async Task<string> GenerateEmailConfirmationTokenAsync(ApplicationUser user)
{
/* NOTE:
* The default UserTokenProvider generates tokens based on the users's SecurityStamp, so until that changes
* (like when the user's password changes), the tokens will always be the same, and remain valid.
* So if you want to simply invalidate old tokens, just call manager.UpdateSecurityStampAsync().
*/
//await base.UpdateSecurityStampAsync(userId);
var token = await base.GenerateEmailConfirmationTokenAsync(user);
if (!string.IsNullOrEmpty(token))
{
user.EmailConfirmationToken = token; //<<< Last issued token
//Note: If a token is generated then the current email is no longer confirmed.
user.EmailConfirmed = false;
await UpdateAsync(user);
}
return token;
}
public override async Task<IdentityResult> ConfirmEmailAsync(ApplicationUser user, string token)
{
if (user == null)
{
return IdentityResult.Failed(new IdentityError {Description = "User not found."});
}
var result = await base.ConfirmEmailAsync(user, token);
if (result.Succeeded)
{
user.EmailConfirmationToken = null;
return await UpdateAsync(user);
}
else if (user.EmailConfirmationToken == token)
{
//Previously Issued Token expired
result = IdentityResult.Failed(new IdentityError { Description = "Expired token." });
}
return result;
}
}
UserManager Extension
public static class ApplicationUserManagerExtension
{
public static Task<string> FindIdByEmailConfirmationTokenAsync(this UserManager<ApplicationUser> manager, string confirmationToken)
{
string result = null;
ApplicationUser user = manager.Users
.SingleOrDefault(u => u.EmailConfirmationToken != null && u.EmailConfirmationToken == confirmationToken);
if (user != null)
{
result = user.Id;
}
return Task.FromResult(result);
}
}
Update:
The CustomUserManager has to be added to services in Startup.cs in ConfigureServices Method.
services.AddTransient<CustomUserManager>();
Without this, DependencyInjection fails.
You can use my controller.It's working mate.
public IActionResult ForgotPassword()
{
return View();
}
[HttpPost]
public async Task<IActionResult> ForgotPassword(string Email)
{
if (string.IsNullOrEmpty(Email))
{
return View();
}
var user = await _userManager.FindByEmailAsync(Email);
if (user == null)
{
return View();
}
var code =await _userManager.GeneratePasswordResetTokenAsync(user);
var callback = Url.Action("ResetPassword", "Account", new
{
token=code,
},Request.Scheme);
// send email
await _emailSender.SendEmailAsync(Email, "Confirm Password Reset", $"<a href='{callback}'>If you want to reset your password click please !</a>");
return RedirectToAction("ForgotPasswordConfirmation", "Account");
}
public IActionResult ForgotPasswordConfirmation() => View();
public IActionResult ResetPassword(string token)
{
if (token == null)
{
return View();
}
var model = new ResetPasswordModel()
{
Token = token,
};
return View(model);
}
[HttpPost]
public async Task<IActionResult> ResetPassword(ResetPasswordModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null)
{
return RedirectToAction("Index", "Home");
}
var result = await _userManager.ResetPasswordAsync(user, model.Token, model.Password);
if (result.Succeeded)
{
return RedirectToAction("ResetPasswordConfirmation", "Account");
}
return View(model);
}
public ActionResult ResetPasswordConfirmation() => View();

OAuth 2 refresh token invalid_grant

Following Taiseer Joudeh excellent article
Enable OAuth Refresh Tokens in AngularJS App using ASP .NET Web API 2, and Owin (http://bitoftech.net/2014/07/16/enable-oauth-refresh-tokens-angularjs-app-using-asp-net-web-api-2-owin/) currently I am creating a Token based authentication with refresh token option.
My Startup class code is as follows:
public class Startup
{
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
ConfigureOAuth(app);
WebApiConfig.Register(config);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
}
public void ConfigureOAuth(IAppBuilder app)
{
OAuthAuthorizationServerOptions oAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),
Provider = new SimpleAuthorizationServerProvider(),
RefreshTokenProvider = new SimpleRefreshTokenProvider()
};
// Token Generation
app.UseOAuthAuthorizationServer(oAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
My SimpleAuthorizationServerProvider class code is as follows:
public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientId = string.Empty;
string clientSecret = string.Empty;
Client client = null;
if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
{
context.TryGetFormCredentials(out clientId, out clientSecret);
}
if (context.ClientId == null)
{
//Remove the comments from the below line context.SetError, and invalidate context
//if you want to force sending clientId/secrects once obtain access tokens.
context.Validated();
//context.SetError("invalid_clientId", "ClientId should be sent.");
return Task.FromResult<object>(null);
}
using (AuthRepository _repo = new AuthRepository())
{
client = _repo.FindClient(context.ClientId);
}
if (client == null)
{
context.SetError("invalid_clientId", string.Format("Client '{0}' is not registered in the system.", context.ClientId));
return Task.FromResult<object>(null);
}
if (client.ApplicationType == Models.ApplicationTypes.NativeConfidential)
{
if (string.IsNullOrWhiteSpace(clientSecret))
{
context.SetError("invalid_clientId", "Client secret should be sent.");
return Task.FromResult<object>(null);
}
else
{
if (client.Secret != Helper.GetHash(clientSecret))
{
context.SetError("invalid_clientId", "Client secret is invalid.");
return Task.FromResult<object>(null);
}
}
}
if (!client.Active)
{
context.SetError("invalid_clientId", "Client is inactive.");
return Task.FromResult<object>(null);
}
context.OwinContext.Set<string>("as:clientAllowedOrigin", client.AllowedOrigin);
context.OwinContext.Set<string>("as:clientRefreshTokenLifeTime", client.RefreshTokenLifeTime.ToString());
context.Validated();
return Task.FromResult<object>(null);
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");
if (allowedOrigin == null) allowedOrigin = "*";
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
//var id = "";
using (AuthRepository _repo = new AuthRepository())
{
IdentityUser user = await _repo.FindUser(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
//Here set User.Identity.Id = RavenUserId, So rest of the user will be able to get it
//id = (user == null ? "0" : user.RavenUserId.ToString());
}
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
//So when we will call User.Identity.Id we will be able to get Raven User Id
// identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, id));
identity.AddClaim(new Claim("sub", context.UserName));
identity.AddClaim(new Claim("role", "user"));
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{
"as:client_id", (context.ClientId == null) ? string.Empty : context.ClientId
},
{
"userName", context.UserName
}
});
var ticket = new AuthenticationTicket(identity, props);
context.Validated(ticket);
}
public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
{
var originalClient = context.Ticket.Properties.Dictionary["as:client_id"];
var currentClient = context.ClientId;
if (originalClient != currentClient)
{
context.SetError("invalid_clientId", "Refresh token is issued to a different clientId.");
return Task.FromResult<object>(null);
}
// Change auth ticket for refresh token requests
var newIdentity = new ClaimsIdentity(context.Ticket.Identity);
var newClaim = newIdentity.Claims.Where(c => c.Type == "newClaim").FirstOrDefault();
if (newClaim != null)
{
newIdentity.RemoveClaim(newClaim);
}
newIdentity.AddClaim(new Claim("newClaim", "newValue"));
var newTicket = new AuthenticationTicket(newIdentity, context.Ticket.Properties);
context.Validated(newTicket);
return Task.FromResult<object>(null);
}
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}
}
My SimpleRefreshTokenProvider class code is as follows:
public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider
{
public async Task CreateAsync(AuthenticationTokenCreateContext context)
{
var clientid = context.Ticket.Properties.Dictionary["as:client_id"];
if (string.IsNullOrEmpty(clientid))
{
return;
}
var refreshTokenId = Guid.NewGuid().ToString("n");
using (var _repo = new AuthRepository())
{
var refreshTokenLifeTime = context.OwinContext.Get<string>("as:clientRefreshTokenLifeTime");
var token = new RefreshToken()
{
Id = Helper.GetHash(refreshTokenId),
ClientId = clientid,
Subject = context.Ticket.Identity.Name,
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.AddMinutes(Convert.ToDouble(refreshTokenLifeTime))
};
context.Ticket.Properties.IssuedUtc = token.IssuedUtc;
context.Ticket.Properties.ExpiresUtc = token.ExpiresUtc;
token.ProtectedTicket = context.SerializeTicket();
var result = await _repo.AddRefreshToken(token);
if (result)
{
context.SetToken(refreshTokenId);
}
}
}
public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
string hashedTokenId = Helper.GetHash(context.Token);
using (var _repo = new AuthRepository())
{
var refreshToken = await _repo.FindRefreshToken(hashedTokenId);
if (refreshToken != null)
{
//Get protectedTicket from refreshToken class
context.DeserializeTicket(refreshToken.ProtectedTicket);
var result = await _repo.RemoveRefreshToken(hashedTokenId);
}
}
}
public void Create(AuthenticationTokenCreateContext context)
{
throw new NotImplementedException();
}
public void Receive(AuthenticationTokenReceiveContext context)
{
throw new NotImplementedException();
}
}
The AuthRepository class code is as follows:
public class AuthRepository : IDisposable
{
private AuthContext _ctx;
private UserManager<IdentityUser> _userManager;
public AuthRepository()
{
_ctx = new AuthContext();
_userManager = new UserManager<IdentityUser>(new UserStore<IdentityUser>(_ctx));
}
public async Task<IdentityResult> RegisterUser(UserModel userModel)
{
IdentityUser user = new IdentityUser
{
UserName = userModel.UserName
};
var result = await _userManager.CreateAsync(user, userModel.Password);
return result;
}
public async Task<IdentityUser> FindUser(string userName, string password)
{
IdentityUser user = await _userManager.FindAsync(userName, password);
return user;
}
public Client FindClient(string clientId)
{
var client = _ctx.Clients.Find(clientId);
//var clients = _ctx.Clients;
//var client = _ctx.Clients.FirstOrDefault(x => x.Id==clientId);
return client;
}
public async Task<bool> AddRefreshToken(RefreshToken token)
{
var existingToken = _ctx.RefreshTokens.Where(r => r.Subject == token.Subject && r.ClientId == token.ClientId).SingleOrDefault();
if (existingToken != null)
{
var result = await RemoveRefreshToken(existingToken);
}
_ctx.RefreshTokens.Add(token);
return await _ctx.SaveChangesAsync() > 0;
}
public async Task<bool> RemoveRefreshToken(string refreshTokenId)
{
var refreshToken = await _ctx.RefreshTokens.FindAsync(refreshTokenId);
if (refreshToken != null)
{
_ctx.RefreshTokens.Remove(refreshToken);
return await _ctx.SaveChangesAsync() > 0;
}
return false;
}
public async Task<bool> RemoveRefreshToken(RefreshToken refreshToken)
{
_ctx.RefreshTokens.Remove(refreshToken);
return await _ctx.SaveChangesAsync() > 0;
}
public async Task<RefreshToken> FindRefreshToken(string refreshTokenId)
{
var refreshToken = await _ctx.RefreshTokens.FindAsync(refreshTokenId);
return refreshToken;
}
public List<RefreshToken> GetAllRefreshTokens()
{
return _ctx.RefreshTokens.ToList();
}
public void Dispose()
{
_ctx.Dispose();
_userManager.Dispose();
}
}
And the ajax code is:
$("#refresh").click(function () {
var token = sessionStorage.getItem(tokenKey);
var refresh = sessionStorage.getItem('isRefreshToken');
var refreshToken = sessionStorage.getItem('refreshToken');
if (refresh) {
var refreshdata = "grant_type=refresh_token&refresh_token=" + refreshToken + "&client_id=TokenBasedAuthentication";
console.log(refreshdata);
sessionStorage.setItem(tokenKey, '');
sessionStorage.setItem(isRefreshToken, '');
sessionStorage.setItem(refreshToken, '');
$.ajax({
url: '/token',
type: 'POST',
data: refreshdata,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
success: function (data) {
sessionStorage.setItem(tokenKey, data.access_token);
sessionStorage.setItem(isRefreshToken, true);
sessionStorage.setItem(refreshToken, data.refresh_token);
},
error: function (xhr) {
alert(xhr.status + ': ' + xhr.statusText);
}
});
}
});
Finally when I click on Refresh it returns me following error
error: "invalid_grant"
Last two days I tried to figure out but failed.
I had a problem where I was always receiving a invalid_grant error even though I knew the refresh_token was valid. Granted there are a lot of reasons why there might be a invalid_grant error, but after debugging through the code I discovered that my issue was in the CreateAsync method. The refreshTokenLifetime variable was null. Thus, when the RefreshToken is created, the ExpiresUtc value is already expired, causing the invalid_grant error. To resolve this I verified that I had a valid value for the refreshTokenLifetime variable.
var refreshTokenLifetime = context.OwinContext.Get<string>("as:RefreshTokenLifetime") ?? "60";
Try this.
Remove this line of code newIdentity.AddClaim(new Claim("newClaim", "newValue")); from your GrantRefreshToken function of SimpleAuthorizationServerProvider class. As this line is of no use.
It is duplicating the claim when you request for new refresh token. So it is opposing you.
In ReceiveAsync method in SimpleRefreshTokenProvider class:
var refreshToken = await _repo.FindRefreshToken(hashedTokenId);
inspect refreshToken object to make sure all of its attributes have valid values.
In my case ProtectedTicket property has invalid value (Date value instead of string) and it causes this error.

Resources