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.
The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.
Step 1 − First, the client authenticates with the authorization server by giving the authorization grant. Step 2 − Next, the authorization server authenticates the client, validates the authorization grant and issues the access token and refresh token to the client, if valid.
There are two ways to fix the error: (RECOMMENDED) Change the application signature algorithm to RS256 instead of HS256. Change the value of your responseType parameter to token id_token (instead of the default), so that you receive an access token in the response.
By default, access tokens are valid for 60 days and programmatic refresh tokens are valid for a year. The member must reauthorize your application when refresh tokens expire.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With