I have implemented an authorization server following the article
I've implemented it almost exactly like the article, but I dont see how the auth server knows the refresh token is expired. Indeed I've tested and the server does not grant access tokens when presented expired refresh tokens, but I dont see the logic for this in my auth server. Further, when I request an access token using an expired refresh token the my OAuthAuthorizationServerProvider sub class is never called at all, actually none of my methods in my OAuthAuthorizationServerProvider derived class or my IAuthenticationTokenProvider implementation are called when I request a new access token using an expired refresh token. Any help is appreciated. Here is what I have
public class SmartCardOAuthAuthenticationTokenProvider : IAuthenticationTokenProvider { private IDataAccessFactoryFactory _producesFactoryThatProducesIAuthenticateDataAccess; public SmartCardOAuthAuthenticationTokenProvider(IDataAccessFactoryFactory producesFactoryThatProducesIAuthenticateDataAccess) { _producesFactoryThatProducesIAuthenticateDataAccess = producesFactoryThatProducesIAuthenticateDataAccess; } 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(IDataAccessFactory producesIAuthenticateDataAccess = _producesFactoryThatProducesIAuthenticateDataAccess.GetDataAccessFactory())//using (IAuthorizationDataAccess _repo = new AuthRepository()) { IAuthorizationDataAccess _repo = producesIAuthenticateDataAccess.GetDataAccess<IAuthorizationDataAccess>(); var refreshTokenLifeTime = context.OwinContext.Get<string>("as:clientRefreshTokenLifeTime"); var token = new RefreshToken() { RefreshTokenId = 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.AddRefreshTokenAsync(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 (IAuthorizationDataAccess _repo = new AuthRepository()) //{ using (IDataAccessFactory producesIAuthenticateDataAccess = _producesFactoryThatProducesIAuthenticateDataAccess.GetDataAccessFactory()) //using (AuthRepository _repo = new AuthRepository()) { IAuthorizationDataAccess _repo = producesIAuthenticateDataAccess.GetDataAccess<IAuthorizationDataAccess>(); var refreshToken = await _repo.FindRefreshTokenAsync(hashedTokenId); if (refreshToken != null) { //Get protectedTicket from refreshToken class context.DeserializeTicket(refreshToken.ProtectedTicket); var result = await _repo.RemoveRefreshTokenAsync(hashedTokenId); } } } public void Create(AuthenticationTokenCreateContext context) { throw new NotImplementedException(); } public void Receive(AuthenticationTokenReceiveContext context) { throw new NotImplementedException(); } } public class SmartCardOAuthAuthorizationProvider : OAuthAuthorizationServerProvider { private IDataAccessFactoryFactory _producesFactoryThatProducesIAuthenticateDataAccess; public SmartCardOAuthAuthorizationProvider(IDataAccessFactoryFactory producesFactoryThatProducesIAuthenticateDataAccess) { _producesFactoryThatProducesIAuthenticateDataAccess = producesFactoryThatProducesIAuthenticateDataAccess; } public override System.Threading.Tasks.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 }); if (context.UserName != "onlyOneHardCodedUserForSakeOfExploration" && context.Password!="thePassword") { context.SetError("invalid_grant", "the user name or password is incorrect"); return Task.FromResult<object>(null); ; } ClaimsIdentity identity = new ClaimsIdentity(context.Options.AuthenticationType); identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName)); identity.AddClaim(new Claim("sub", context.UserName)); identity.AddClaim(new Claim(ClaimTypes.Role, "PostVSDebugBreakModeEnterEventArgs")); identity.AddClaim(new Claim(DatawareClaimTypes.SmartCardUserId.ToString(), 1.ToString())); 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); //ticket.Properties.ExpiresUtc = new DateTimeOffset(DateTime.Now.AddDays(2)); context.Validated(ticket); return Task.FromResult<object>(null); } public override System.Threading.Tasks.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); } string[] clientIdClientUnique = context.ClientId.Split(':'); if (clientIdClientUnique == null || clientIdClientUnique.Length <= 1) { context.SetError("invalid_client_unique"); return Task.FromResult<object>(null); } clientId = clientIdClientUnique[0]; string clientUnique = clientIdClientUnique[1]; using (IDataAccessFactory producesIAuthenticateDataAccess = _producesFactoryThatProducesIAuthenticateDataAccess.GetDataAccessFactory()) //using (AuthRepository _repo = new AuthRepository()) { IAuthorizationDataAccess _repo = producesIAuthenticateDataAccess.GetDataAccess<IAuthorizationDataAccess>(); client = _repo.FindClient(clientId);//new Client { Active = true, AllowedOrigin = "*", ApplicationType = ApplicationTypes.DesktopClient, ClientId = context.ClientId, Name = "Visual Studio Event Source", RefreshTokenLifeTimeInMinutes = 14400, Secret = Helper.GetHash(clientSecret) };//_repo.FindClient(context.ClientId); } if (client == null) { //context.SetError("invalid_client_unique"); context.SetError("invalid_clientId", string.Format("Client '{0}' is not registered in the system.", context.ClientId)); return Task.FromResult<object>(null); } 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.RefreshTokenLifeTimeInMinutes.ToString()); context.Validated(); return Task.FromResult<object>(null); } 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); 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); } }
Again, when I make a request
and the refresh token is expired, NONE of the above classes\methods get hit. Further, the refresh token passed to the API is nothing more than the GUID I generated in the SmartCardOAuthAuthenticationTokenProvider.CreateAsync
it contains no information about expiry. If none of the above methods are hit when requesting access by refresh and the request passes nothing(it looks like nothing) when requesting a new access token by refresh then how does the server know the refresh token is expired?
Looks like magic to me.
UPDATE 1 - Add Startup code
public static class OwinStartUpConfig { public static void Configure(HttpConfiguration configFromOwinStartup) { configFromOwinStartup.MapHttpAttributeRoutes(); configFromOwinStartup.Routes.MapHttpRoute("Default", "{controller}/{id}", new { id = RouteParameter.Optional }); var jsonFormatter = configFromOwinStartup.Formatters.OfType<JsonMediaTypeFormatter>().First(); jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); RegiserDependencies(configFromOwinStartup); } public static void RegiserDependencies(HttpConfiguration configFromOwinStartup) { string connectionStringForSmartCardDbCntx = System.Configuration.ConfigurationManager.ConnectionStrings["SmartCardDataContext"].ConnectionString; string projectNameWhenNewProjectCreatedDueToNoMatch = System.Configuration.ConfigurationManager.AppSettings["ProjectNameWhenNewProjectCreatedDueToNoMatch"]; Autofac.ContainerBuilder builderUsedToRegisterDependencies = new Autofac.ContainerBuilder(); builderUsedToRegisterDependencies.RegisterType<DataAccessFactoryFactoryEf>() .As<IDataAccessFactoryFactory>() .WithParameter(new TypedParameter(typeof(string), connectionStringForSmartCardDbCntx)); builderUsedToRegisterDependencies.Register( c => new List<IProjectActivityMatch<VSDebugBreakModeEnterActivity>> { new MatchVSProjectWithMostRecentActivity<VSDebugBreakModeEnterActivity>(), new MatchVSSolutionWithMostRecentActivityActivityMatch<VSDebugBreakModeEnterActivity>(), new MatchMostRecentActivityMatch<VSDebugBreakModeEnterActivity>(), new MatchToNewProjectActivityMatch<VSDebugBreakModeEnterActivity>(projectNameWhenNewProjectCreatedDueToNoMatch) } ).As<IEnumerable<IProjectActivityMatch<VSDebugBreakModeEnterActivity>>>(); builderUsedToRegisterDependencies .RegisterType<MatchDontGiveUpActivityMatch<VSDebugBreakModeEnterActivity>>() //.WithParameter(Autofac.Core.ResolvedParameter.ForNamed<IEnumerable<IProjectActivityMatch<VSDebugBreakModeEnterActivity>>>("VSDebugBreakModeEnterActivityMatchers")) .As<IProjectActivityMatch<VSDebugBreakModeEnterActivity>>(); builderUsedToRegisterDependencies .RegisterType<VSDebugBreakModeEnterEventArgsEventSaver>() .Named<ISaveVisualStudioEvents>("VSDebugBreakModeEnterSaver"); builderUsedToRegisterDependencies .RegisterType<VSDebugBreakModeEnterEventArgsController>() .WithParameter(Autofac.Core.ResolvedParameter.ForNamed<ISaveVisualStudioEvents>("VSDebugBreakModeEnterSaver")); var container = builderUsedToRegisterDependencies.Build(); configFromOwinStartup.DependencyResolver = new AutofacWebApiDependencyResolver(container); } } public static class OAuthStartupConfig { internal static void Configure(IAppBuilder app) { OAuthAuthorizationServerOptions oAuthServerOptions = new OAuthAuthorizationServerOptions { AllowInsecureHttp = false, TokenEndpointPath = new PathString("/token"), AccessTokenExpireTimeSpan = TimeSpan.FromDays(1), //TimeSpan.FromDays(1), Provider = new SmartCardOAuthAuthorizationProvider(new AuthorizationDataAccessFactoryFactory()), RefreshTokenProvider = new SmartCardOAuthAuthenticationTokenProvider(new AuthorizationDataAccessFactoryFactory()) }; app.UseOAuthAuthorizationServer(oAuthServerOptions); app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()); } }
UPDATE - In response to comments
The access token in the above response contains claims information and the expiry information for the access token. All that info is serialized by the server into the access token above. So, I can see how the access token's expiry is checked. However, what about the refresh token? When a request for an access token is make using the refresh token:
In the request above the refresh token is good and so the request for the access token by refresh token is granted, but if the refresh token were expired how would OAuthAuthorizationServerProvider check the refresh token for expiry?
Again, I have checked that the code provided above does indeed check for expired refresh tokens and does not grant an access token if the refresh token is expired, but HOW does it know? I didnt write anything for checking refresh token expiry into my OAuthAuthorizationServerProvider derived class. So then how????
UPDATE
The magic happens in the ReceiveAsync method of the IAuthenticationTokenProvider implementation.
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 (IAuthorizationDataAccess _repo = new AuthRepository()) //{ using (IDataAccessFactory producesIAuthenticateDataAccess = _producesFactoryThatProducesIAuthenticateDataAccess.GetDataAccessFactory()) //using (AuthRepository _repo = new AuthRepository()) { IAuthorizationDataAccess _repo = producesIAuthenticateDataAccess.GetDataAccess<IAuthorizationDataAccess>(); var refreshToken = await _repo.FindRefreshTokenAsync(hashedTokenId); if (refreshToken != null) { //Get protectedTicket from refreshToken class context.DeserializeTicket(refreshToken.ProtectedTicket); var result = await _repo.RemoveRefreshTokenAsync(hashedTokenId); } } }
Particularly the line context.DeserializeTicket(refreshToken.ProtectedTicket);
is the magic. That will set the context.Ticket property. After the method ReceiveAsync has completed its done. No need to manually check anything OWIN, somewhere behind the scenes, knows the ticket is expired.
2 Answers
Answers 1
I've implemented it almost exactly like the article, but I dont see how the auth server knows the refresh token is expired.
You have to save that information somewhere, for example in a sql table. When you receive a request to emit a new token with a refresh token, there you have to check if the submitted refresh token is still valid, based on the information you have on you local source of truth.
For example, this is the I implemented it:
public override async Task ReceiveAsync(AuthenticationTokenReceiveContext context) { var allowedOrigin = context.OwinContext.Get<string>(Constants.PublicAuth.CLIENT_ALLOWED_ORIGIN); context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] {allowedOrigin}); RefreshTokenModel refreshToken = await _mediator.SendAsync(new VerifyRefreshToken(context.Token)); if (refreshToken != null) { context.DeserializeTicket(refreshToken.ProtectedTicket); } }
And this is the query handler:
public async Task<RefreshTokenModel> Handle(VerifyRefreshToken query) { RefreshToken refreshToken = await Context.RefreshTokens .Where(rt => rt.Token == query.Token) .FirstOrDefaultAsync(); if (refreshToken == null || refreshToken.ExpiresUtc < DateTimeOffset.UtcNow) return null; User user = await Context.Users.Where(u => u.Email == refreshToken.Subject).SingleOrDefaultAsync(); if (user == null || (user.LockoutEnabled && user.LockoutEndUtc > DateTimeOffset.UtcNow)) return null; return new RefreshTokenModel { ProtectedTicket = new UTF8Encoding(true).GetString(refreshToken.ProtectedTicket) }; }
As you can see, if the doesn't exists, or the token expiration point has passed, I return null; null means no token, so the ReceiveAsync(AuthenticationTokenReceiveContext context)
will return nothing, and a 401 Unauthorized
will be emitted.
Answers 2
The magic happens in the ReceiveAsync method of the IAuthenticationTokenProvider implementation.
Particularly the line context.DeserializeTicket(refreshToken.ProtectedTicket); is the magic I was missing. That will set the context.Ticket property. After the method ReceiveAsync has completed its done. No need to manually check anything OWIN, somewhere behind the scenes, knows the ticket is expired.
0 comments:
Post a Comment