【问题标题】:Where does OAuthAuthorizationServerProvider check Refresh Token Expiry?OAuthAuthorizationServerProvider 在哪里检查 Refresh Token Expiry?
【发布时间】:2016-07-06 17:51:59
【问题描述】:

我按照文章实现了一个授权服务器

http://bitoftech.net/2014/07/16/enable-oauth-refresh-tokens-angularjs-app-using-asp-net-web-api-2-owin/

我已经实现了它几乎与文章一样,但我看不到身份验证服务器如何知道刷新令牌已过期。事实上,我已经测试过,当呈现过期的刷新令牌时,服务器不会授予访问令牌,但我在我的身份验证服务器中看不到这个逻辑。此外,当我使用过期的刷新令牌请求访问令牌时,我的 OAuthAuthorizationServerProvider 子类根本不会被调用,实际上,当我使用过期的访问令牌请求新的访问令牌时,我的 OAuthAuthorizationServerProvider 派生类或 IAuthenticationTokenProvider 实现中的任何方法都不会被调用刷新令牌。任何帮助表示赞赏。这是我所拥有的

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);
    }

}

再次,当我提出请求时

并且刷新令牌已过期,上述类\方法均未命中。此外,传递给 API 的刷新令牌只不过是我在SmartCardOAuthAuthenticationTokenProvider.CreateAsync 中生成的 GUID,它不包含有关到期的信息。如果通过刷新请求访问时上述方法均未命中,并且在通过刷新请求新访问令牌时请求没有传递(看起来什么都没有),那么服务器如何知道刷新令牌已过期?

在我看来就像魔法一样。

更新 1 - 添加启动代码

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());
    }
}

更新 - 响应 cmets

上述响应中的访问令牌包含声明信息和访问令牌的到期信息。所有这些信息都由服务器序列化到上面的访问令牌中。因此,我可以看到如何检查访问令牌的到期时间。但是,刷新令牌呢?当使用刷新令牌请求访问令牌时:

在上面的请求中,刷新令牌是好的,因此通过刷新令牌对访问令牌的请求被授予,但如果刷新令牌已过期,OAuthAuthorizationServerProvider 将如何检查刷新令牌是否过期?

再次,我检查了上面提供的代码确实检查了过期的刷新令牌,并且如果刷新令牌已过期,则不会授予访问令牌,但它是如何知道的?我没有写任何东西来检查我的 OAuthAuthorizationServerProvider 派生类中的刷新令牌到期。那怎么办????

更新

奇迹发生在 IAuthenticationTokenProvider 实现的 ReceiveAsync 方法中。

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);
            }
        }
    }

尤其是context.DeserializeTicket(refreshToken.ProtectedTicket); 这行代码很神奇。这将设置 context.Ticket 属性。在 ReceiveAsync 方法完成后。无需手动检查任何东西 OWIN,在幕后某处,知道票已过期。

【问题讨论】:

  • 您能否显示将您的提供程序连接到 OAuthServerOptions 的代码?我怀疑您在创建服务器选项时没有指向刷新令牌提供程序。

标签: asp.net-mvc asp.net-web-api owin


【解决方案1】:

奇迹发生在 IAuthenticationTokenProvider 实现的 ReceiveAsync 方法中。

尤其是行 context.DeserializeTicket(refreshToken.ProtectedTicket);是我所缺少的魔法。这将设置 context.Ticket 属性。在 ReceiveAsync 方法完成后。无需手动检查任何东西 OWIN,在幕后某处,知道票已过期。

【讨论】:

    【解决方案2】:

    我已经实现了它几乎与文章完全一样,但我看不出身份验证服务器如何知道刷新令牌已过期。

    您必须将该信息保存在某处,例如在 sql 表中。当您收到使用刷新令牌发出新令牌的请求时,您必须根据您在本地事实来源中的信息检查提交的刷新令牌是否仍然有效。

    例如,这是我实现的:

        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);
            }
        }
    

    这是查询处理程序:

    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)
                };
            }
    

    如你所见,如果不存在,或者令牌过期点已过,我返回null; null 表示没有令牌,因此ReceiveAsync(AuthenticationTokenReceiveContext context) 将不返回任何内容,并且将发出401 Unauthorized

    【讨论】:

    • 感谢您的回答。但是,我没有实现您在我的 ReceiveAsync 方法中建议的逻辑,并且令牌到期仍然有效。我发布的代码确实检测到过期的令牌,但我无法弄清楚如何,因为我没有实现任何这样的代码。这是我不明白的。
    • 当您说“令牌”时,您是指持有人吗?因为该令牌内部具有到期日期(代码 AccessTokenExpireTimeSpan = TimeSpan.FromDays(1))代表什么。另一个只是“个人”代码(此处为 GUID),因此它没有任何类型的过期,它就像一把钥匙。
    • 访问令牌已经过期,但是刷新令牌呢?我添加了一个新的屏幕截图,它显示了从身份验证服务器发回的两个令牌。有一个访问令牌(是的,这个字符串带有声明、到期和其他信息),还有一个刷新令牌(这个字符串是一个 GUID,指向数据库中的刷新令牌记录)。服务器可以轻松验证访问令牌,因为令牌携带正确的信息,但是当使用刷新令牌请求新访问令牌时,服务器如何验证刷新令牌?这就是问题。
    • 系统不会验证令牌。观点。刷新令牌验证由您的代码负责。这就是为什么令牌可以是任何东西。例如,在我的系统中,令牌只是表的主键,仅此而已。那里有我需要检查的到期时间。也许,我说也许,你在这里进行验证: var refreshToken = await _repo.FindRefreshTokenAsync(hashedTokenId);也许在那个方法中你有一个日期时间检查。如果不是,那么刷新令牌被认为是始终有效的,并且在您使用刷新令牌请求它时,您将始终获得一个新令牌。
    • 当我传递过期的刷新令牌时,没有命中服务器代码并且未授予新的访问令牌。公共异步任务 FindRefreshTokenAsync(string refreshTokenId){ var refreshToken = await ctx.RefreshTokens.FindAsync(refreshTokenId);return refreshToken;}。同样,我的实现遵循文章:bitoftech.net/2014/07/16/…,这篇文章被广泛关注,我在文章中你不会找到刷新令牌过期检查。我会再看一遍。
    猜你喜欢
    • 2017-12-30
    • 2015-12-29
    • 2019-02-18
    • 1970-01-01
    • 2020-10-24
    • 2011-07-27
    • 1970-01-01
    • 1970-01-01
    • 2023-03-26
    相关资源
    最近更新 更多