IdentityServer / IdentityServer3

OpenID Connect Provider and OAuth 2.0 Authorization Server Framework for ASP.NET 4.x/Katana
https://identityserver.github.io/Documentation/
Apache License 2.0
2.01k stars 764 forks source link

IdSrv Self host on Azure Web App #1710

Closed dcinzona closed 9 years ago

dcinzona commented 9 years ago

I submitted this question to the gitter forum, but it might be better suited here. We are having an issue where we are unable to maintain a logged in session on multiple page refreshes and can manually trigger a log out by switching between azure web app instances (this is done by changing the value of the ARRAFFINITY cookie to the ID of the other instance. We have the machine key configured under <system.web> and have an entry for specifying Identity Model use the machine key:

  <system.identityModel>
    <identityConfiguration>
      <securityTokenHandlers>
        <remove type="System.IdentityModel.Tokens.SessionSecurityTokenHandler, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
        <add type="System.IdentityModel.Services.Tokens.MachineKeySessionSecurityTokenHandler, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089">
          <sessionTokenRequirement lifetime="00:30:00"></sessionTokenRequirement>
        </add>
      </securityTokenHandlers>
    </identityConfiguration>
  </system.identityModel>

Our machine key entry (cleared for security):

       <system.web>
<!-- Other stuff -->
        <machineKey decryption="AES" decryptionKey="clearedForSec" validation="SHA1" validationKey="clearedForSec" />
    </system.web>

Errors I'm seeing:

w3wp.exe Warning: 0 : [Thinktecture.IdentityServer.Core.Configuration.Hosting.AntiForgeryToken]: 8/12/2015 12:24:26 PM +00:00 -- Problem unprotecting cookie; Issuing new cookie. Error message: Key not valid for use in specified state.

w3wp.exe Error: 0 : [Thinktecture.IdentityServer.Core.Configuration.Hosting.AntiForgeryToken]: 8/12/2015 12:24:26 PM +00:00 -- AntiForgeryTokenValidator validating token
System.Security.Cryptography.CryptographicException: Key not valid for use in specified state.

and

w3wp.exe Information: 0 : [Thinktecture.IdentityServer.WsFederation.WsFederationController]: 8/12/2015 12:26:52 PM +00:00 -- Redirecting to login page
Debug: [Thinktecture.IdentityServer.Core.Configuration.Hosting.MessageCookie`1]: 8/12/2015 12:26:52 PM +00:00 -- Unable to decrypt cookie SignInMessage.e4f611d46d6f67aced95de10425092be: Key not valid for use in specified state.

Debug: [Thinktecture.IdentityServer.Core.Configuration.Hosting.MessageCookie`1]: 8/12/2015 12:26:52 PM +00:00 -- Unable to decrypt cookie SignInMessage.d826313fcf1292e2d19d02f8f07f17cc: Key not valid for use in specified state.

Debug: [Thinktecture.IdentityServer.Core.Configuration.Hosting.MessageCookie`1]: 8/12/2015 12:26:52 PM +00:00 -- Unable to decrypt cookie SignInMessage.572f7504c031e37189c3f638da87ffc2: Key not valid for use in specified state.
dcinzona commented 9 years ago

Startup.cs

using System.Collections.Generic;
using System.Linq;
using Custom.Web.Module.IdentityServer.Config;
using Custom.Web.Module.IdentityServer.Services.IdentityServer;
using Orchard;
using Orchard.Logging;
using Orchard.Owin;
using Owin;
using Thinktecture.IdentityServer.Core.Configuration;
using Thinktecture.IdentityServer.Core.Logging;
using Thinktecture.IdentityServer.Core.Models;
using Thinktecture.IdentityServer.Core.Services;
using Thinktecture.IdentityServer.EntityFramework;
using Thinktecture.IdentityServer.WsFederation.Configuration;
using Thinktecture.IdentityServer.WsFederation.Models;
using Thinktecture.IdentityServer.WsFederation.Services;

namespace Custom.Web.Module.IdentityServer
{
    public class Startup : IOwinMiddlewareProvider
    {
        private readonly IWorkContextAccessor _wca;
        public ILogger Logger { get; set; }

        public Startup(
            IWorkContextAccessor wca)
        {
            _wca = wca;
            Logger = NullLogger.Instance;
        }

        public IEnumerable<OwinMiddlewareRegistration> GetOwinMiddlewares() {
            var middleWare = new List<OwinMiddlewareRegistration> {
                new OwinMiddlewareRegistration {
                    Priority = "100",
                    Configure = app => app.Map("/identity", coreApp => {

                        var efConfig = new EntityFrameworkServiceOptions
                        {
                            ConnectionString = "Website",
                            Schema = "IdentityServer"
                        };

                        // Copy InMememory Clients to EF
                        ConfigureClients(Clients.Get(), efConfig);
                        ConfigureScopes(Scopes.Get(), efConfig);

                        var factory = new IdentityServerServiceFactory();

                        // Will log to trace provider for Debugging IdentityServer
                        LogProvider.SetCurrentLogProvider(new DiagnosticsTraceLogProvider());

                        // Register Orchard User UserService, pass in IWorkContextAccessor so it can access Orchard Pipeline
                        var userService = new UserService(_wca);
                        factory.UserService = new Registration<IUserService>(resolver => userService);

                        // Entity Framework Stores
                        factory.RegisterOperationalServices(efConfig);

                        factory.RegisterClientStore(efConfig);
                        factory.RegisterScopeStore(efConfig);

                        // Custom View service to override front-end
                        factory.ViewService = new Registration<IViewService>(typeof(ViewService));

                        // Set options and register Identity Server with OWIN
                        var options = new IdentityServerOptions {
                            SiteName = "Website Single Sign-On",
                            EnableWelcomePage = false,
                            SigningCertificate = Certificate.Get(),
                            Factory = factory,
                            PluginConfiguration = ConfigurePlugins,  // WS-Fed
                            CorsPolicy = CorsPolicy.AllowAll,
                            AuthenticationOptions = new AuthenticationOptions
                            {
                                IdentityProviders = ConfigureIdentityProviders,
                                EnableSignOutPrompt = true
                            }
                        };
                        coreApp.UseIdentityServer(options);
                    })
                }
            };

            return middleWare;

        }

        public static void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
        {
            // TODO: Add Staff config here when WS-Federation enpoint is available
            // https://identityserver.github.io/Documentation/docs/configuration/identityProviders.html

        }

        private void ConfigurePlugins(IAppBuilder pluginApp, IdentityServerOptions options)
        {
            var wsFedOptions = new WsFederationPluginOptions(options);

            // data sources for in-memory services
            wsFedOptions.Factory.Register(new Registration<IEnumerable<RelyingParty>>(RelyingParties.Get()));
            wsFedOptions.Factory.RelyingPartyService = new Registration<IRelyingPartyService>(typeof(InMemoryRelyingPartyService));

            pluginApp.UseWsFederationPlugin(wsFedOptions);
        }

        public static void ConfigureClients(IEnumerable<Client> clients, EntityFrameworkServiceOptions options)
        {
            using (var db = new ClientConfigurationDbContext(options.ConnectionString, options.Schema))
            {
                if (!db.Clients.Any())
                {
                    foreach (var c in clients)
                    {
                        var e = c.ToEntity();
                        db.Clients.Add(e);
                    }
                    db.SaveChanges();
                }
            }
        }

        public static void ConfigureScopes(IEnumerable<Scope> scopes, EntityFrameworkServiceOptions options)
        {
            using (var db = new ScopeConfigurationDbContext(options.ConnectionString, options.Schema))
            {
                if (!db.Scopes.Any())
                {
                    foreach (var s in scopes)
                    {
                        var e = s.ToEntity();
                        db.Scopes.Add(e);
                    }
                    db.SaveChanges();
                }
            }
        }

    }
}
brockallen commented 9 years ago

I'm a little confused. <system.identityModel> has nothing to do with IdentityServer3's sessions -- we use the cookie authentication middleware. It does rely upon the host's protection (e.g. <machineKey>) for the cookies, though.

dcinzona commented 9 years ago

The <system.identityModel> was something that we were trying out to see if it would resolve the issue, but it obviously didn't. We are also quite a bit confused as to why switching between Azure Web App instances would break the session... Does the machine key value look ok? Any ideas as to why this would be breaking when swapping between instances? I can trigger this 100%, just by changing the ARRAffinity cookie and refreshing the page.

brockallen commented 9 years ago

I don't know Azure very well, so I cant' speak to how it works. But our authentication cookie relies upon the host's machineKey -- so if you're switching servers and they have different keys, then it would explain it.

dcinzona commented 9 years ago

They have the same web.config and machine key. Azure Web Apps use a shared file system, so those are exactly the same in multiple instances. I can verify this by hitting reports, which also uses the machine key to decrypt a signing token, and I can bounce between instances without issue.

Here's an SO describing the Azure Web Apps (formerly known as Azure Websites) file system: http://stackoverflow.com/questions/23136706/is-the-file-system-shared-across-multiple-azure-websites

dcinzona commented 9 years ago

BTW, we are using the WS Federation module...not sure if that makes a difference.

dcinzona commented 9 years ago

So we've been experimenting some and something odd came up. I tested changing the machine key and refreshing the /identity/permissions page. Changing the machine key in the web.config file prompted a app pool restart. After refreshing that page, my session stayed active even with a different machine key. I verified that the machine key changed because I am displaying it. At this point, I don't know how it's decrypting the cookie when the site restarted with a completely different machine key.

Just to be sure this was a full site restart, I actually went into the Azure Management portal and told it to restart the site that way as well. This did not affect my session and I remained logged in. But when I swap instances, my session breaks. Is there a setting somewhere to specify auth cookie encryption using the machine key? I'm guessing it's the default, but I'm wondering if we changed it inadvertently somehow.

dcinzona commented 9 years ago

This is resolved! Had to specify the use of IdentityServer's x509CertificateDataProtector and use that as the machine key was not being used at all. Works like a charm!

var options = new IdentityServerOptions {
    SiteName = "QA Single Sign-On",
    EnableWelcomePage = false,
    SigningCertificate = Certificate.Get(),
    Factory = factory,
    PluginConfiguration = ConfigurePlugins,  // WS-Fed
    //CorsPolicy = CorsPolicy.AllowAll, //Handled by CorsPolicyService
    AuthenticationOptions = new AuthenticationOptions
    {
        IdentityProviders = ConfigureIdentityProviders,
        EnableSignOutPrompt = true,
        CookieOptions = new CookieOptions {
            SecureMode = CookieSecureMode.Always
        }
    },
    DataProtector = new X509CertificateDataProtector(Certificate.Get())
};
coreApp.UseIdentityServer(options);
brockallen commented 9 years ago

So azure wasn't providing one? Or was it orchard?

dcinzona commented 9 years ago

Azure runs the instances under different profiles and this was causing the DPAPI to be unique. It was an azure issue since running orchard locally seemed to work fine (although, hard to tell since I can't run multiple instances locally :P )

brockallen commented 9 years ago

Ok, so the short of it is that in azure you needed to set the data protector. Ok, thanks for the info/

dcinzona commented 9 years ago

I believe this is only the case when deploying to Azure Web Apps, not cloud services, as those are independent VMs, where you do have control of IIS, etc. (however, I have not tested this on cloud services, so can't be certain - but given the architecture differences between the two, I think this is the case)

dcinzona commented 9 years ago

Actually, upon further review, it appears this has to do more specifically with Orchard CMS. Orchard uses System.Web.Hosting, not Microsoft.Owin.Host.SystemWeb - so from my understanding this falls back to DpapiDataProtector instead of machine key usage. Similar to what was posted here https://github.com/IdentityServer/IdentityServer3/issues/1029#issuecomment-77358100