在两个Web API项目之间共享OAuth令牌

9
我创建了一个带有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);
    }
2个回答

8

我在自己的研究中偶然发现这个问题。简单来说,Token是使用machine.config文件中的machineKey属性生成的:如果要在多个服务器上托管,则需要覆盖此设置。

可以在web.config文件中覆盖MachineKey设置:

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

机器密钥应该在本地生成 - 使用在线服务是不安全的。有关生成密钥的KB文章,请参见此处
所有这些的原始参考资料请见此处

关于这个问题,有一个小建议。如果你已经有了一个生产应用程序,并且更改密钥会导致当前登录失效的情况下,你可以从app1中获取现有密钥,然后为app1和app2进行配置:https://dev59.com/hHI-5IYBdhLWcg3woJ9J#52569714 - Daniel

0

如果您不想使用MachineKey,并且希望它跨不同的服务器和用户具有唯一的MachineKey,那么这可能会有点棘手。

在Asp.NET Core和Framework中实现数据保护提供程序(生成密码重置链接)

我开始使用DataProtectionTokenProvider.cs来实现自己的ValidateAsync。这个类真的帮助我找到了解决方案。

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

使用DataProtectorTokenProvider<TUser, TKey>时,令牌是从SecurityStamp生成的,但深入了解很困难。考虑到如果在单个服务器上更改了应用程序池标识,则验证将失败,这表明实际的保护机制可能如下所示:

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

假设所有站点都使用相同的应用程序池标识,则此方法有效。它也可以是具有protectionDescriptor "LOCAL=user"DataProtectionProvider

new DataProtectionProvider("LOCAL=user")

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

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

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

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

用于提供从数据保护API派生的数据保护服务。当您的应用程序未由ASP.NET托管且所有进程都以相同的域标识运行时,它是数据保护的最佳选择。

创建方法的目的如下所述:

使用额外的熵来确保受保护的数据只能出于正确的目的而被解除保护。

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

鉴于这些信息,我认为使用 Microsoft 提供的普通类没有前进的方法。

最终我实现了自己的 IUserTokenProvider<TUser, TKey>IDataProtectionProviderIDataProtector 来解决问题。

我选择使用证书来实现 IDataProtector,因为我可以相对容易地在服务器之间传输它们。我还可以使用运行网站的 应用程序池身份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<TUser, TKey> 的工作原理,以下是更多信息:

https://dev59.com/qYXca4cB1Zd3GeqPHFuc#53390287


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接