IdentityModel / oidc-client-js

OpenID Connect (OIDC) and OAuth2 protocol support for browser-based JavaScript applications
Apache License 2.0
2.43k stars 840 forks source link

Keep getting session out with OIDC JS client and Identity server 4 Authorization flow with PKCE flow on Azure app services #1278

Closed ghost closed 3 years ago

ghost commented 3 years ago

Account controller

[HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(LoginInputModel model, string button)
        {
            // check if we are in the context of an authorization request
            var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);

            // the user clicked the "cancel" button
            if (button != "login")
            {
                if (context != null)
                {
                    // if the user cancels, send a result back into IdentityServer as if they 
                    // denied the consent (even if this client does not require consent).
                    // this will send back an access denied OIDC error response to the client.
                    await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);

                    // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
                    if (context.IsNativeClient())
                    {
                        // The client is native, so this change in how to
                        // return the response is for better UX for the end user.
                        return this.LoadingPage("Redirect", model.ReturnUrl);
                    }

                    return Redirect(model.ReturnUrl);
                }
                else
                {
                    // since we don't have a valid context, then we just go back to the home page
                    return Redirect("~/");
                }
            }

            if (ModelState.IsValid)
            {
                // validate username/password against in-memory store
                var result = await _signInManager.PasswordSignInAsync(model.Username, model.Password,
                    model.RememberLogin, lockoutOnFailure: true);
                // validate username/password against in-memory store
                if (result.Succeeded)
                {
                    var user = await _userManager.FindByNameAsync(model.Username);
                    await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.Email, clientId: context?.Client.ClientId));

                    // only set explicit expiration here if user chooses "remember me". 
                    // otherwise we rely upon expiration configured in cookie middleware.
                    AuthenticationProperties props = null;
                    if (AccountOptions.AllowRememberLogin && model.RememberLogin)
                    {
                        props = new AuthenticationProperties
                        {
                            IsPersistent = true,
                            ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
                        };
                    };

                    // Add the additional claim to the Identity and token
                    var principal = await _claimsFactory.CreateAsync(user);
                    var claims = principal.Claims.ToList();

                    // issue authentication cookie with subject ID and username
                    var isuser = new IdentityServerUser(user.Id)
                    {
                        DisplayName = user.UserName,
                        AdditionalClaims = claims    
                    };

                    await HttpContext.SignInAsync(isuser, props);

                    if (context != null)
                    {
                        if (context.IsNativeClient())
                        {
                            // The client is native, so this change in how to
                            // return the response is for better UX for the end user.
                            return this.LoadingPage("Redirect", model.ReturnUrl);
                        }

                        // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
                        return Redirect(model.ReturnUrl);
                    }

                    // request for a local page
                    if (Url.IsLocalUrl(model.ReturnUrl))
                    {
                        return Redirect(model.ReturnUrl);
                    }
                    else if (string.IsNullOrEmpty(model.ReturnUrl))
                    {
                        return Redirect("~/");
                    }
                    else
                    {
                        // user might have clicked on a malicious link - should be logged
                        throw new Exception("invalid return URL");
                    }
                }

                await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId:context?.Client.ClientId));
                ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
            }

            // something went wrong, show form with error
            var vm = await BuildLoginViewModelAsync(model);
            return View(vm);
        }

Startup.cs

public static void ConfigureIdentityServer(this IServiceCollection services, IConfiguration configuration)
        {
            var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
            var builder = services.AddIdentityServer(options =>
                {
                    options.Authentication.CookieLifetime = TimeSpan.FromDays(30);
                    options.Authentication.CookieSlidingExpiration = true;
                })
                .AddProfileService<ProfileService>()
                // this adds the config data from DB (clients, resources, CORS)
                .AddConfigurationStore(options =>
                {
                    options.ConfigureDbContext = builder =>
                        builder.UseSqlServer(configuration.GetConnectionString("DefaultConnection"),
                            sql => sql.MigrationsAssembly(migrationsAssembly));
                })
                // this adds the operational data from DB (codes, tokens, consents)
                .AddOperationalStore(options =>
                {
                    options.ConfigureDbContext = builder =>
                        builder.UseSqlServer(configuration.GetConnectionString("DefaultConnection"),
                            sql => sql.MigrationsAssembly(migrationsAssembly));
                    // this enables automatic token cleanup. this is optional.
                    options.EnableTokenCleanup = true;
                });
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    configuration.GetConnectionString("DefaultConnection"),
                    b => b.MigrationsAssembly("Falcon-Identity")));
            services.AddIdentity<ApplicationUser, IdentityRole>(
                    options =>
                    {
                        options.SignIn.RequireConfirmedAccount = true;
                        options.Password.RequireDigit = false;
                        options.Password.RequiredLength = 8;
                        options.Password.RequireLowercase = true;
                        options.Password.RequireUppercase = false;
                    })
                .AddEntityFrameworkStores<ApplicationDbContext>();

            // not recommended for production - you need to store your key material somewhere secure
            builder.AddDeveloperSigningCredential();
            builder.AddInMemoryApiScopes(Config.ApiScopes);

            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);

            // Register the IConfiguration instance which options binds against.
            services.Configure<IdentityServerViewModel>(configuration.GetSection("IdentityServer"));
            // services.AddAuthentication(AuthorizePolicy.TokenSchema)
            //     .AddIdentityServerAuthentication(AuthorizePolicy.TokenSchema, options =>
            //     {
            //         options.ApiName = AuthorizePolicy.apiScope;
            //         options.Authority = configuration.GetSection("IdentityServer:Jwt:Issuer").Value;
            //     });
        }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // this will do the initial DB population
            Config.InitializeDatabase(app,Configuration);

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }
            app.UseSwagger();
            app.UseSwaggerUI(options =>
            {
                options.SwaggerEndpoint("/swagger/v1/swagger.json", "Falcon Identity V1");
                options.OAuthClientId(Configuration.GetSection("IdentityServer:Client:ClientId").Value);
                options.OAuthAppName(Configuration.GetSection("IdentityServer:Client:ClientName").Value);
                options.OAuthUsePkce();
            });
            app.UseHttpsRedirection();
            app.UseCors("CorsPolicy");
            app.UseStaticFiles();
            if (!env.IsDevelopment())
            {
                app.UseSpaStaticFiles();
            }

            app.UseRouting();

            app.UseIdentityServer();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller}/{action=Index}/{id?}");
                endpoints.MapRazorPages();
            });

            app.UseSpa(spa =>
            {
                // To learn more about options for serving an Angular SPA from ASP.NET Core,
                // see https://go.microsoft.com/fwlink/?linkid=864501

                spa.Options.SourcePath = "ClientApp";

                if (env.IsDevelopment())
                {
                    spa.UseProxyToSpaDevelopmentServer("http://localhost:4201/");
                }
            });
        }

Using Identity server 4 with OIDC JS client in every 10 seconds the application reloads, in the network tabs I can see image

The end session URL is always been canceled and application reload frequently

Request URL: https://falconidentityserver.azurewebsites.net/connect/endsession?id_token_hint=eyJhbGciOiJSUzI1NiIsImtpZCI6IjU1RDY5MTZFODVCOUNENTgwRjQ0RTMzNzREMjZFOUFCIiwidHlwIjoiSldUIn0.eyJuYmYiOjE2MTAzNzU0NjQsImV4cCI6MTYxMDM3NTc2NCwiaXNzIjoiaHR0cHM6Ly9mYWxjb25pZGVudGl0eXNlcnZlci5henVyZXdlYnNpdGVzLm5ldCIsImF1ZCI6IkZldGVfQmlyZF9VSSIsImlhdCI6MTYxMDM3NTQ2NCwiYXRfaGFzaCI6IlVDaUt3LXBBdi14aFVINFRZVXk1a1EiLCJzX2hhc2giOiI5clc3UUY5ZEtqRXdubE9lTWpRTTVBIiwic2lkIjoiQjYwNEJBNEU5MTEyOURCQjYzNTJFOEJDNDZFQUM5QkUiLCJzdWIiOiJlNDEzMTIwYS0yYWEzLTQzZTktYTQ1MC1lZWU2NzBjY2EzMjEiLCJhdXRoX3RpbWUiOjE2MTAzNzU0NTEsImlkcCI6ImxvY2FsIiwiYW1yIjpbInB3ZCJdfQ.lmfq1ZJ2ukkrZ-FPjtCPaEsBYM0HXCAF496dNGMH0WP-SBFbbSllLSPGcavpruzzA0n-JQmkshtEqhyMvk5-c81dQLgjblrQ-X5QrzGoRd6fXDMnWwR0dlm2ZC2TcPOcBJXdaW1nfjLChxxrQljHMNLBr1tAmPfTfx0kaG7uvoXg1iY1aNmBsFUs4erdYs24Wd0JPtGzlHrGc3wyXk7aJNS77Ocu9SHUTL5XLsecOPMX0CVqeAX0ibRT6b-VuJ6u0egMKzR6yS8vCx4DNdHIScuX-zyMvisDePiCwqbN_K9VE0iwy2CmNfMKmioPX-aora7V1qZwCdj2-Lp-OSVVwg

image

Identity server client setting

new Client
                {
                    ClientId = "XXXXXXXXXXXXXX",
                    ClientName = "XXXXXXXXXXXXXXXX",
                    AllowedCorsOrigins = CorsUris(configuration),
                    AllowedGrantTypes = GrantTypes.Code,
                    AllowAccessTokensViaBrowser = bool.Parse(configuration.GetSection("IdentityServer:Client:AllowAccessTokensViaBrowser").Value),
                    IdentityTokenLifetime = 300,
                    AccessTokenLifetime = 200,
                    RequireConsent = bool.Parse(configuration.GetSection("IdentityServer:Client:RequireConsent").Value),
                    UpdateAccessTokenClaimsOnRefresh = bool.Parse(configuration.GetSection("IdentityServer:Client:UpdateAccessTokenClaimsOnRefresh").Value),
                    RedirectUris = LocalRedirectUris(configuration),
                    PostLogoutRedirectUris = LocalRedirectUris(configuration),
                    AllowedScopes = AllowedScopes(),
                    AllowOfflineAccess = bool.Parse(configuration.GetSection("IdentityServer:Client:AllowOfflineAccess").Value),
                    AccessTokenType = AccessTokenType.Jwt,
                    RequireClientSecret = bool.Parse(configuration.GetSection("IdentityServer:Client:RequireClientSecret").Value),
                    RequirePkce = bool.Parse(configuration.GetSection("IdentityServer:Client:RequirePkce").Value),
                    //AllowRememberConsent = true
                },

OIDC client setting

openID = {
authority: 'https://falconidentityserver.azurewebsites.net',
client_id: 'xxxxxxxx',
redirect_uri: 'https://localhost:4200/auth-callback',
silent_redirect_uri: 'https://localhost:4200/assets/silent-renew.html',
response_type: 'code',
scope: 'openid profile email',
automaticSilentRenew: true
};

I don't know what I am missing and where is the issue. Can anyone help me with the this issue

I am doing silent reniew and it is working fine as you can see in the network tab.

OIDC usermanager logs

image

brockallen commented 3 years ago

Sorry don't know. Did you ever figure this out?

ghost commented 3 years ago

This issue is related to this https://stackoverflow.com/questions/65669259/keep-getting-session-out-with-oidc-js-client-and-identity-server-4-authorization But I have not found the solution for it.

brockallen commented 3 years ago

the endsession endpoint is due to a call to signoutRedirect -- so find out where your code is calling that?