Open Closed

How to persist the granted user tokens from IdentityServer4 after reboot? #2005


0
[email protected] created

I am using docker containers to house my app and redis and reverse proxy in production. But every time I deploy a new build to the server the currently authorized users in my mobile app (and the web-app, but this is less troublesome) lose their authentication.

From looking at the account module code and the tables that are created (ie. AbpUserTokens) and the documentation, I think the default template should be using the IPersistedGrantStore that writes to EF Core database instead of in-memory store. But I cannot figure out how to tell the system to use the persisted store instead of in-memory one. I didn't want to try and separate the IdentityServer4 from the MVC project because of the complexity of doing so. Any directions or hints?

ABP Framework version: v4.2.2 UI type: MVC DB provider: EF Core Tiered (MVC) or Identity Server Separated (Angular): yes, tiered MVC Exception message and stack trace: Steps to reproduce the issue:

  1. Spin up new solution
  2. See the AbpUserTokens table is created
  3. Use OAuth system (ie, with the react-native template) to login a user
  4. See the AbpUserTokens table is empty
  5. Restart the server and lose your authentication

I am also working on trying to get the mobile app to re-up the auth when/if it is lost but this above scenario is bugging me because I THINK it should be straight forward to accomplish and would allow me to only concentrate on getting refresh token instead of checking for broken auth before every API call in the mobile app.


8 Answer(s)
  • 0
    maliming created
    Support Team

    hi

    You should always persist the key to the database or redis, etc. If you use redis, make sure that the key is not cleared;

    https://docs.identityserver.io/en/latest/topics/startup.html#key-material https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-5.0

  • 0
    [email protected] created

    Thank-you for your reply; let me see if I understand what you are saying.

    The app generates cryptographic keys for protecting tokens, etc, when it starts. The app stores this key in memory by default (probably in redis by default with the standard template?) To ensure that the protected tokens can be read after the app restarts, the app must not be allowed to roll the keys; that is, the key must be perisisted to DB or to redis. This way, when the app starts up again, it will re-use the same key from previous and only roll it when it expires.

    Do I have that correct?

    I guess this doesn't explain the AbpUserTokens table or how to persist the tokens, but that wouldn't matter anyway unless the key was persisted. Do you know of any samples/examples of this setup?

    Any idea how to configure this seeing as I am using the account module?

  • 0
    maliming created
    Support Team

    hi

    The AbpUserTokens table has nothing to do with user login. It's represents an authentication token for a user.

    Maybe you can check if tempkey.rsa exists in your docker?

    https://github.com/abpframework/abp/blob/dev/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/tempkey.rsa

  • 0
    [email protected] created

    It is not! Should it be in there?

    What's weird is that both tempkey.rsa and tempkey.jwk are set to "do not copy" yet the tempkey.jwk is found in the docker image.

  • 0
    maliming created
    Support Team

    hi

    Please try to use AddSigningCredential and check your Data Protection.

    https://docs.identityserver.io/en/latest/topics/startup.html#key-material https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-5.0

  • 0
    [email protected] created

    As per your instructions I tried adding AddSigningCredential(..) to startup in ConfigureServices of my Web Module; but I receieve an exception (presumably because AddIdentityServer() is already called by the Identity Module)

    "Decorator already registered for type: IAuthenticationService."

  • 0
    maliming created
    Support Team

    https://docs.abp.io/en/abp/4.4/Modules/IdentityServer#abpidentityserverbuilderoptions

    public override void PreConfigureServices(ServiceConfigurationContext context)
    {
    	PreConfigure<IIdentityServerBuilder>(builder =>
    	{
        	builder.AddSigningCredential(...);	
    	});
    }
    
  • 0
    [email protected] created

    HUZZAH! Thank-you @maliming you have provided the info I needed to solve this.

    For future visitors trying to persist cryptographic key for IdentityServer4 in docker containers that are recreated at each deploy, the trick is to store a pfx cert file on the host that you load on app startup, along with a host mapping folder to store the generated keys (take a step further and encrypt the keys at rest).

    I store the path and secret in ENV variables that are passed through docker-compose

    Some code for you:

    public class YourWebModule : AbpModule
        {
            public override void PreConfigureServices(ServiceConfigurationContext context)
            {
                context.Services.PreConfigure<AbpMvcDataAnnotationsLocalizationOptions>(options =>
                {
                    PreConfigure<IIdentityServerBuilder>(builder =>
                    {
                        ConfigureIdentityCertificate(context, builder);
                    });
    
                    options.AddAssemblyResource(
    ...your assemblies...
                    );
                });
            }
    
            private static void ConfigureIdentityCertificate(ServiceConfigurationContext context, IIdentityServerBuilder builder)
            {
                var configuration = context.Services.GetConfiguration();
                var config = configuration.GetSection("Identity");
                var certSecret = config["Secret"];
                var certFilePath = config["CertificateStorePath"];
    
                if (string.IsNullOrEmpty(certSecret))
                {
                    Console.WriteLine("ERROR: No secret specified for identity cert!");
                    return;
                }
                
                if (string.IsNullOrEmpty(certFilePath) || !Directory.Exists(certFilePath))
                {
                    Console.WriteLine("WARN: No specified CertificateStorePath or not found so falling back to content root.");
                    certFilePath = context.Services.GetHostingEnvironment().ContentRootPath;
                }
    
                var certFile = Path.Combine(certFilePath, "cert.pfx");
    
                if (!File.Exists(certFile))
                {
                    Console.WriteLine($"ERROR: Certificate file '{certFile}' not found on disk");
                    return;
                }
    
                X509Certificate2 cert;
                try
                {
                    cert = new X509Certificate2(
                        certFile,
                        certSecret,
                        X509KeyStorageFlags.MachineKeySet |
                        X509KeyStorageFlags.PersistKeySet |
                        X509KeyStorageFlags.Exportable
                    );
                }
                catch (Exception exception)
                {
                    Console.WriteLine($"ERROR: Could not parse the identity cert! Message={exception.Message}");
                    return;
                }
    
                builder.AddSigningCredential(cert);
                context.Services.AddDataProtection()
                    .PersistKeysToFileSystem(new DirectoryInfo(certFilePath));
            }
    

    Create a cert using the following commands (*nix):

    openssl genrsa 2048 openssl req -x509 -days 3650 -new -key privatekey.pem -out publickey.pem openssl pkcs12 -export -in publickey.pem -inkey privatekey.pem -out cert.pfx -password pass:$PASSWD <-- $PASSWD var in .sh script