Sharing OAuth Tokens Across Two Web API projects

前端 未结 2 1488
感动是毒
感动是毒 2020-12-16 03:28

I have created a Web API application with OAuth token authentication. This worked without issue when the token server was running on the same application as the service. How

相关标签:
2条回答
  • 2020-12-16 04:00

    This turned out to be a bit tricky if you do not wan't to use MachineKey and I wanted it to be across different servers and users with unique MachineKey per server.

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

    I started with implementing my own ValidateAsync with the help from DataProtectionTokenProvider.cs for ASP.NET Core Identity. This Class really helped me find a solution.

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

    Tokens are generated from the SecurityStamp when using DataProtectorTokenProvider<TUser, TKey> but its hard to dig deeper. Given that the verification will fail if the Application Pool Identity is changed on a single server points to that the actual protection mechanism would look something like this:

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

    Given that it works if all sites use the same Application Pool Identity points to this as well. It could also be DataProtectionProvider with 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

    When reading about DpapiDataProtectionProvider(DPAPI stands for Data Protection Application Programming Interface) the description says:

    Used to provide the data protection services that are derived from the Data Protection API. It is the best choice of data protection when you application is not hosted by ASP.NET and all processes are running as the same domain identity.

    The Create method purposes are described as:

    Additional entropy used to ensure protected data may only be unprotected for the correct purposes.

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

    Given this information I saw no way forward in trying to use the normal classes provided by Microsoft.

    I ended up implementing my own IUserTokenProvider<TUser, TKey>, IDataProtectionProvider and IDataProtector to get it right instead.

    I choose to implement IDataProtector with certificates since I can relatively easy transfer these between servers. I can also pick it up from the X509Store with the Application Pool Identity that runs the website so no keys are stored in the application itself.

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

    Customer website reset:

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

    Back Office:

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

    A bit more information about how the normal DataProtectorTokenProvider<TUser, TKey> works:

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

    0 讨论(0)
  • 2020-12-16 04:04

    Just stumbled across this Q whilst researching this myself. The TL;DR answer is that Tokens are generated using the machineKey properties in the machine.config file: if you want to host on multiple servers you need to override this.

    The MachineKey can be overridden in web.config :

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

    Machine Keys should be generated locally - using an online service is not secure. KB Article for generating keys

    Orginal ref for all this here http://bitoftech.net/2014/09/24/decouple-owin-authorization-server-resource-server-oauth-2-0-web-api

    0 讨论(0)
提交回复
热议问题