9

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. However, I'd like to move the authorization service into its own application (VS project) and use it across several Web API projects I am working on. However, when I isolated the authorization logic into it's own project the original service no longer treats the tokens generated as valid. My question is, is it possible for one Web API project to generate a token for another one to validate? Here is my OWIN startup code for both the auth service and the original service

Auth Service:

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

Original Service:

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);
    }
Ochowie
  • 543
  • 2
  • 6
  • 20

2 Answers2

8

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

Ian
  • 4,208
  • 21
  • 33
  • One tip on this. There may be cases where you already have a production app and changing the key would mean invalidating current logins. In that case, you can get the existing key out from app1 and then configure for both app1 and app2: https://stackoverflow.com/questions/1755130/getting-the-current-asp-net-machine-key/52569714#52569714 – Daniel Sep 29 '18 at 15:30
0

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://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

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://learn.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

Ogglas
  • 62,132
  • 37
  • 328
  • 418