【问题标题】:Sharing OAuth Tokens Across Two Web API projects跨两个 Web API 项目共享 OAuth 令牌
【发布时间】:2014-11-03 04:21:20
【问题描述】:

我创建了一个带有 OAuth 令牌身份验证的 Web API 应用程序。当令牌服务器与服务在同一应用程序上运行时,这没有问题。但是,我想将授权服务移动到它自己的应用程序(VS 项目)中,并在我正在处理的几个 Web API 项目中使用它。但是,当我将授权逻辑隔离到它自己的项目中时,原始服务不再将生成的令牌视为有效。我的问题是,一个 Web API 项目是否可以生成一个令牌供另一个项目进行验证?这是我的身份验证服务和原始服务的 OWIN 启动代码

认证服务:

public void Configuration(IAppBuilder app)
    {
        // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=316888
        HttpConfiguration config = new HttpConfiguration();

        ConfigureOAuth(app);

        WebApiConfig.Register(config);
        app.UseWebApi(config);
        app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
    }

    private void ConfigureOAuth(IAppBuilder app)
    {
        OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
        {
            AllowInsecureHttp = true,
            TokenEndpointPath = new PathString("/token"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
            Provider = new SimpleAuthorizationServerProvider()
        };

        // Token Generation
        app.UseOAuthAuthorizationServer(OAuthServerOptions);
        app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
    }

原始服务:

public void Configuration(IAppBuilder app)
    {
        ConfigureOAuth(app);
        // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=316888
        HttpConfiguration config = new HttpConfiguration();

        config.SuppressDefaultHostAuthentication();
        config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));


        WebApiConfig.Register(config);

        app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
        app.UseWebApi(config);
    }

    public void ConfigureOAuth(IAppBuilder app)
    {
        var oauthBearerOptions = new OAuthBearerAuthenticationOptions();
        app.UseOAuthBearerAuthentication(oauthBearerOptions);
    }

【问题讨论】:

    标签: c# asp.net-web-api oauth


    【解决方案1】:

    我自己研究这个问题时偶然发现了这个问题。 TL;DR 的答案是令牌是使用 machine.config 文件中的 machineKey 属性生成的:如果您想在多个服务器上托管,则需要覆盖它。

    MachineKey 可以在 web.config 中被覆盖:

    <system.web>
    <machineKey validationKey="VALUE GOES HERE" 
                decryptionKey="VALUE GOES HERE" 
                validation="SHA1" 
                decryption="AES"/>
    </system.web>
    

    机器密钥应在本地生成 - 使用在线服务不安全。 KB Article for generating keys

    这里所有这些的原始参考http://bitoftech.net/2014/09/24/decouple-owin-authorization-server-resource-server-oauth-2-0-web-api

    【讨论】:

    • 对此有一个提示。在某些情况下,您已经有一个生产应用程序并且更改密钥意味着使当前登录无效。在这种情况下,您可以从 app1 中获取现有密钥,然后为 app1 和 app2 进行配置:stackoverflow.com/questions/1755130/…
    【解决方案2】:

    如果您不想使用 MachineKey,这会有点棘手,我希望它可以跨不同的服务器和用户,每个服务器具有唯一的 MachineKey。

    Data Protection provider across Asp.NET Core and Framework (generate password reset link)

    在DataProtectionTokenProvider.cs 的帮助下,我开始实现我自己的ValidateAsync ASP.NET Core Identity。这堂课真的帮我找到了解决办法。

    https://github.com/aspnet/Identity/blob/master/src/Identity/DataProtectionTokenProvider.cs

    使用DataProtectorTokenProvider&lt;TUser, TKey&gt; 时,令牌是从SecurityStamp 生成的,但很难深入挖掘。如果在单个服务器上更改Application Pool Identity,则验证将失败,这表明实际的保护机制如下所示:

    System.Security.Cryptography.ProtectedData.Protect(userData, entropy, DataProtectionScope.CurrentUser);
    

    如果所有站点都使用相同的 Application Pool Identity ,那么它也可以工作。也可以是 DataProtectionProvider 和 protectionDescriptor "LOCAL=user"。

    new DataProtectionProvider("LOCAL=user")
    

    https://docs.microsoft.com/en-us/previous-versions/aspnet/dn613280(v%3dvs.108)

    https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.dataprotector?view=netframework-4.7.2

    https://docs.microsoft.com/en-us/uwp/api/windows.security.cryptography.dataprotection.dataprotectionprovider

    当阅读DpapiDataProtectionProvider(DPAPI 代表数据保护应用程序编程接口)时,描述说:

    用于提供源自于 数据保护 API。是您数据保护的最佳选择 应用程序不是由 ASP.NET 托管的,所有进程都运行为 相同的域身份。

    Create 方法的用途描述为:

    用于确保受保护数据的附加熵只能 出于正确目的而不受保护。

    https://docs.microsoft.com/en-us/previous-versions/aspnet/dn253784(v%3dvs.113)

    鉴于此信息,我认为尝试使用 Microsoft 提供的普通类没有任何前进方向。

    我最终实现了自己的 IUserTokenProvider&lt;TUser, TKey&gt;、IDataProtectionProvider 和 IDataProtector 来改正它。

    我选择使用证书实现IDataProtector,因为我可以相对容易地在服务器之间传输这些证书。我还可以使用运行网站的Application Pool Identity 从X509Store 中获取它,因此应用程序本身中不会存储任何密钥。

    public class CertificateProtectorTokenProvider<TUser, TKey> : IUserTokenProvider<TUser, TKey>
        where TUser : class, IUser<TKey>
        where TKey : IEquatable<TKey>
    {
        private IDataProtector protector;
    
        public CertificateProtectorTokenProvider(IDataProtector protector)
        {
            this.protector = protector;
        }
        public virtual async Task<string> GenerateAsync(string purpose, UserManager<TUser, TKey> manager, TUser user)
        {
            if (user == null)
            {
                throw new ArgumentNullException(nameof(user));
            }
            var ms = new MemoryStream();
            using (var writer = new BinaryWriter(ms, new UTF8Encoding(false, true), true))
            {
                writer.Write(DateTimeOffset.UtcNow.UtcTicks);
                writer.Write(Convert.ToInt32(user.Id));
                writer.Write(purpose ?? "");
                string stamp = null;
                if (manager.SupportsUserSecurityStamp)
                {
                    stamp = await manager.GetSecurityStampAsync(user.Id);
                }
                writer.Write(stamp ?? "");
            }
            var protectedBytes = protector.Protect(ms.ToArray());
            return Convert.ToBase64String(protectedBytes);
        }
    
        public virtual async Task<bool> ValidateAsync(string purpose, string token, UserManager<TUser, TKey> manager, TUser user)
        {
            try
            {
                var unprotectedData = protector.Unprotect(Convert.FromBase64String(token));
                var ms = new MemoryStream(unprotectedData);
                using (var reader = new BinaryReader(ms, new UTF8Encoding(false, true), true))
                {
                    var creationTime = new DateTimeOffset(reader.ReadInt64(), TimeSpan.Zero);
                    var expirationTime = creationTime + TimeSpan.FromDays(1);
                    if (expirationTime < DateTimeOffset.UtcNow)
                    {
                        return false;
                    }
    
                    var userId = reader.ReadInt32();
                    var actualUser = await manager.FindByIdAsync(user.Id);
                    var actualUserId = Convert.ToInt32(actualUser.Id);
                    if (userId != actualUserId)
                    {
                        return false;
                    }
                    var purp = reader.ReadString();
                    if (!string.Equals(purp, purpose))
                    {
                        return false;
                    }
                    var stamp = reader.ReadString();
                    if (reader.PeekChar() != -1)
                    {
                        return false;
                    }
    
                    if (manager.SupportsUserSecurityStamp)
                    {
                        return stamp == await manager.GetSecurityStampAsync(user.Id);
                    }
                    return stamp == "";
                }
            }
            catch (Exception e)
            {
                // Do not leak exception
            }
            return false;
        }
    
        public Task NotifyAsync(string token, UserManager<TUser, TKey> manager, TUser user)
        {
            throw new NotImplementedException();
        }
    
        public Task<bool> IsValidProviderForUserAsync(UserManager<TUser, TKey> manager, TUser user)
        {
            throw new NotImplementedException();
        }
    }
    
    public class CertificateProtectionProvider : IDataProtectionProvider
    {
        public IDataProtector Create(params string[] purposes)
        {
            return new CertificateDataProtector(purposes);
        }
    }
    
    public class CertificateDataProtector : IDataProtector
    {
        private readonly string[] _purposes;
    
        private X509Certificate2 cert;
    
        public CertificateDataProtector(string[] purposes)
        {
            _purposes = purposes;
            X509Store store = null;
    
            store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
            store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
    
            var certificateThumbprint = ConfigurationManager.AppSettings["CertificateThumbprint"].ToUpper();
    
            cert = store.Certificates.Cast<X509Certificate2>()
                .FirstOrDefault(x => x.GetCertHashString()
                    .Equals(certificateThumbprint, StringComparison.InvariantCultureIgnoreCase));
        }
    
        public byte[] Protect(byte[] userData)
        {
            using (RSA rsa = cert.GetRSAPrivateKey())
            {
                // OAEP allows for multiple hashing algorithms, what was formermly just "OAEP" is
                // now OAEP-SHA1.
                return rsa.Encrypt(userData, RSAEncryptionPadding.OaepSHA1);
            }
        }
    
        public byte[] Unprotect(byte[] protectedData)
        {
            // GetRSAPrivateKey returns an object with an independent lifetime, so it should be
            // handled via a using statement.
            using (RSA rsa = cert.GetRSAPrivateKey())
            {
                return rsa.Decrypt(protectedData, RSAEncryptionPadding.OaepSHA1);
            }
        }
    }
    

    客户网站重置:

    var provider = new CertificateProtectionProvider();
    var protector = provider.Create("ResetPassword");
    
    userManager.UserTokenProvider = new CertificateProtectorTokenProvider<ApplicationUser, int>(protector);
    
    if (!await userManager.UserTokenProvider.ValidateAsync("ResetPassword", model.Token, UserManager, user))
    {
        return GetErrorResult(IdentityResult.Failed());
    }
    
    var result = await userManager.ResetPasswordAsync(user.Id, model.Token, model.NewPassword);
    

    后台:

    var createdUser = userManager.FindByEmail(newUser.Email);
    
    var provider = new CertificateProtectionProvider();
    var protector = provider.Create("ResetPassword");
    
    userManager.UserTokenProvider = new CertificateProtectorTokenProvider<ApplicationUser, int>(protector);
    var token = userManager.GeneratePasswordResetToken(createdUser.Id);
    

    更多关于普通DataProtectorTokenProvider&lt;TUser, TKey&gt; 工作原理的信息:

    https://stackoverflow.com/a/53390287/3850405

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-06-15
      • 2020-01-31
      • 1970-01-01
      • 2012-02-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多