Search code examples
oauthasp.net-coreopenid-connectaspnet-contrib

.NET Core OpenId Connect Server: Sharing same token across multiple applications


I have two apis written in .NET Core and targeting 4.6.1:

  1. myAuthApi (http://localhost:8496): which verifies credentials and issues tokens to clients. It also has an endpoint /api/values/1 (with an Authorize attribute on this action, used to validate tokens)
  2. myPublicApi(http://localhost:8497): which receives tokens from the client on /api/values/1 (with an Authorize attribute on this action, also used to validate tokens). myPublicApi does not have any tokens endpoint and is meant to be a resource server.

I am using AspNet.Security.OpenIdConnect.Server 1.0.0. Both APIs are Service.Fabric Stateless Services

I can successfully get the token with the following request format to http://localhost:8496/connect/token

client_id=XX&client_secret=XXX&grant_type=password&username=XXX&password=XXX

When validating the token against myAuthApi (http://localhost:8496/api/values/1) it works. However, when using that same token against myPublicApi(http://localhost:8497/api/values/1) it does not.

In both APIs, in the Startup.cs, I have

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        // Connect to Redis database.
        var redis = ConnectionMultiplexer.Connect(ConnectionHelper.GetRedisConnectionString(Configuration));
        services.AddDataProtection()
            .PersistKeysToRedis(redis, "DataProtection-Keys")
            .ProtectKeysWithCertificate(CertificateHandler.GetX509Certificate2(Configuration));

        // Add framework services.
        services.AddMvc().AddJsonOptions(opts =>
        {
            // we set the json serializer to follow camelCaseConventions when 
            // receiving /replying to JSON requests
            opts.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        });
        // we add authentication for the oAuth middleware to be registered in the DI container
        services.AddAuthentication();
    }

In myPublicApi I have:

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {

        // Add a new middleware validating access tokens.
        app.UseOAuthValidation(options =>
        {
            // Automatic authentication must be enabled
            // for SignalR to receive the access token.
            options.AutomaticAuthenticate = true;
            options.Events = new OAuthValidationEvents
            {
                // Note: for SignalR connections, the default Authorization header does not work,
                // because the WebSockets JS API doesn't allow setting custom parameters.
                // To work around this limitation, the access token is retrieved from the query string.
                OnRetrieveToken = context =>
                {
                    // Note: when the token is missing from the query string,
                    // context.Token is null and the JWT bearer middleware will
                    // automatically try to retrieve it from the Authorization header.
                    context.Token = context.Request.Query["access_token"];

                    return Task.FromResult(0);
                }
            };
        });

        app.UseMvc();
    }

In myAuthApi I have:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        // Add a new middleware validating access tokens.
        app.UseOAuthValidation(options =>
        {
            // Automatic authentication must be enabled
            // for SignalR to receive the access token.
            options.AutomaticAuthenticate = true;
            options.Events = new OAuthValidationEvents
            {
                // Note: for SignalR connections, the default Authorization header does not work,
                // because the WebSockets JS API doesn't allow setting custom parameters.
                // To work around this limitation, the access token is retrieved from the query string.
                OnRetrieveToken = context =>
                {
                    // Note: when the token is missing from the query string,
                    // context.Token is null and the JWT bearer middleware will
                    // automatically try to retrieve it from the Authorization header.
                    context.Token = context.Request.Query["access_token"];

                    return Task.FromResult(0);
                }
            };
        });

        // Add a new middleware issuing access tokens.
        app.UseOpenIdConnectServer(options =>
        {
            options.Provider = new AuthenticationProvider();
            // Enable the logout, token and userinfo endpoints.
            options.LogoutEndpointPath = "/connect/logout";
            options.TokenEndpointPath = "/connect/token";
            options.UserinfoEndpointPath = "/connect/userinfo";
            CertificateHandler.SetupCommonAuthServerOptions(options, Configuration);
        });

        app.UseMvc();
    }

As you can see, my data protection provider is storing keys in Redis, and I am protecting keys with a certificate which I am sharing across the 2 applications. The resource server does not have any authentication provider configured and does not have UseOpenIdConnectServer in startup. In asp.net Web API 2, the token decryption used to be managed across the apps using shared machine keys.

How can I successfully validate the token issued by myAuthApi across all other apps using oAuthValidation?

EDIT, some logs can be seen here: https://pastebin.com/ACDz1fam

EDIT2 : After reading the logs thoroughly, I saw that the unprotection of the token was using the same Data Protection Provider, but different purposes:

"Performing unprotect operation to key {4406cfa7-a588-44ba-b73a-e25817d982d9} with purposes ('C:\SfDevCluster\Data\_App\_Node_4\TestMicroServicesType_App22\PublicApiPkg.Code.1.0.1', 'OpenIdConnectServerHandler', 'AccessTokenFormat', 'ASOS')."
"Performing unprotect operation to key {4406cfa7-a588-44ba-b73a-e25817d982d9} with purposes ('C:\SfDevCluster\Data\_App\_Node_3\TestMicroServicesType_App22\AuthApiPkg.Code.1.0.1', 'OpenIdConnectServerHandler', 'AccessTokenFormat', 'ASOS')."

To fix this, @PinpointTownes suggested to setup the data protection provider like so:

    var redis = ConnectionMultiplexer.Connect(ConnectionHelper.GetRedisConnectionString(Configuration));
    services.AddDataProtection()
             // set the application name to a common value in all apps 
             // to have the same purpose and share the token across apps
            .SetApplicationName("MyTestMicroServices")
            .PersistKeysToRedis(redis, "DataProtection-Keys")
            .ProtectKeysWithCertificate(CertificateHandler.GetX509Certificate2(Configuration)); 

Solution

  • Call services.AddDataProtection().SetApplicationName("[your application name]") to ensure your two APIs use the same discriminator (used to derive the crypto keys) and it should work.