11

I'm working on an SPA web app, and im using IdentityServer4 code flow to handle the authorization. So I have the following components:

  • The Angular Client Side App, https://localhost:5001
  • Asp.net 3 Web Api project, running on https://localhost:5001
  • A native mobile application consuming the API, with JWT authentication, running on http://localhost:8100

Rightnow im trying to authenticate the mobile app users, however the user interaction login screen keeps redirecting, and im getting login_required.

Tracing back the calls, this is what im getting:

  • the mobile app calls the /connect/authorize endpoint in a webview - Check
  • I'm redirected to my SPA app login path https://localhost:5001/auth/login?returnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fredirect_uri%3Dhttp%253A%252F%252Flocalhost%253A8100%252Fauth%252Fcallback%26client_id%3Dcharla-mobile%26response_type%3Dcode%26state%3Dpq1nokeuVj%26scope%3Dcharla-api%2520openid%2520profile%2520offline_access%26code_challenge%3DJxDVsm2YnMAbvOuemWWXjYLLt-Mi1TpHoO7zhDkCWSI%26code_challenge_method%3DS256 - Check
  • I enter the username/password and call the login endpoint in the AccountController.cs - Check
  • im redirected to the IdentityServer callback handler https://localhost:5001/auth/login?returnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fredirect_uri%3Dhttp%253A%252F%252Flocalhost%253A8100%252Fauth%252Fcallback%26client_id%3Dcharla-mobile%26response_type%3Dcode%26state%3Dpq1nokeuVj%26scope%3Dcharla-api%2520openid%2520profile%2520offline_access%26code_challenge%3DJxDVsm2YnMAbvOuemWWXjYLLt-Mi1TpHoO7zhDkCWSI%26code_challenge_method%3DS256 - Check
  • Instead of being redirected to the redirect_uri with the authorization code, im redirected again to https://localhost:5001/auth/login?returnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fredirect_uri%3Dhttp%253A%252F%252Flocalhost%253A8100%252Fauth%252Fcallback%26client_id%3Dcharla-mobile%26response_type%3Dcode%26state%3Dpbx8alT61z%26scope%3Dcharla-api%2520openid%2520profile%2520offline_access%26code_challenge%3DrGappKbnVpUNzlNHst4t5RlHephWFfJTVXuwtpQ8tZI%26code_challenge_method%3DS256 - Problem here IdentityServer can't sense that the user is now signed in. I keep getting the login_required error in the debug terminal.

So here is my setup:

Startup.cs


namespace Charla
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {


        services.AddIdentityCore<ApplicationUser>(options => { });
        new IdentityBuilder(typeof(ApplicationUser), typeof(IdentityRole), services)
            .AddRoleManager<RoleManager<IdentityRole>>()
            .AddSignInManager<SignInManager<ApplicationUser>>()
            .AddEntityFrameworkStores<ConverseContext>();

            /*services.AddIdentity<ApplicationUser, IdentityRole>()
                 .AddRoleManager<RoleManager<IdentityRole>>()
                .AddSignInManager<SignInManager<ApplicationUser>>()
                .AddEntityFrameworkStores<ConverseContext>()
                .AddDefaultTokenProviders();*/

            var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
            var builder = services
                .AddIdentityServer(SetupIdentityServer)
                .AddDeveloperSigningCredential()
                .AddConfigurationStore(options =>
                {
                    options.ConfigureDbContext = b => b.UseMySql(Configuration.GetConnectionString("DefaultConnection"),
                        sqloptions => {
                            sqloptions.ServerVersion(new Version(10, 1, 37), ServerType.MariaDb); // replace with your Server Version and Type
                            sqloptions.MigrationsAssembly(migrationsAssembly);
                        });
                })
                .AddOperationalStore(options =>
                {
                    options.ConfigureDbContext = b => b.UseMySql(Configuration.GetConnectionString("DefaultConnection"),
                        sqloptions => {
                            sqloptions.ServerVersion(new Version(10, 1, 37), ServerType.MariaDb); // replace with your Server Version and Type
                            sqloptions.MigrationsAssembly(migrationsAssembly);
                        });
                });
                

            /*services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
                    .AddIdentityServerAuthentication(options => {
                        options.Authority = Configuration.GetValue<string>("IdentityServer:Jwt:Authority");
                        options.RequireHttpsMetadata = false;

                        options.ApiName = "charla-api";
            });*/

           services.AddAuthentication(opt => {
                opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(jwt =>{
                jwt.Authority = Configuration.GetValue<string>("IdentityServer:Jwt:Authority");
                jwt.RequireHttpsMetadata = false;
                jwt.TokenValidationParameters.ValidateAudience = false;
                jwt.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
            });



              
        }


        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
        {
       
            
            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseSpaStaticFiles();

            app.UseRouting();

            app.UseAuthentication();
            app.UseIdentityServer();
            app.UseAuthorization();

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

        private static void SetupIdentityServer(IdentityServerOptions options)
        {
            options.UserInteraction.LoginUrl = "/auth/login";
            options.UserInteraction.LoginReturnUrlParameter = "returnUrl";
            options.UserInteraction.LogoutUrl = "/logout";
            options.UserInteraction.ErrorUrl= "/error/identity";

            options.Events.RaiseErrorEvents = true;
            options.Events.RaiseInformationEvents = true;
            options.Events.RaiseFailureEvents = true;
            options.Events.RaiseSuccessEvents = true;
            
            // see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
            //options.EmitStaticAudienceClaim = true;
            //identityServerOptions.Authentication.CookieLifetime = TimeSpan.FromDays(1);
        }

 
        }

    }
}

AccountController.cs


    [Route("api/[controller]")]
    [ApiController]
    [AllowAnonymous]
    public class AccountController : ControllerBase
    {

        [HttpPost("login")] 
        public async Task<IActionResult> Login(UserResource model) { 
                   
            var result = await signInManager.PasswordSignInAsync(model.email, model.password, isPersistent: true, lockoutOnFailure: false);

            var context = await interaction.GetAuthorizationContextAsync(model.return_url);

            if (result.Succeeded) { 

                var uo = db.Users.Include(q => q.UserOrganization).Single( q => q.Email == model.email ).UserOrganization.First();
                uo.LastLogin = DateTime.UtcNow;
                await db.SaveChangesAsync();

                // let identity server know that we loggedin
                await identityEvents.RaiseAsync(new UserLoginSuccessEvent(
                     model.email, uo.UserId, model.email, clientId: context?.Client.ClientId
                ));


                //return Redirect(model.return_url);
                return Ok( new{
                    email = model.email,
                    return_url = context.RedirectUri
                } );
            }

            await identityEvents.RaiseAsync(new UserLoginFailureEvent(model.email, "invalid credentials", clientId:context?.Client.ClientId));
            return NotFound(new {});

        }

IdentityConfig.cs - I'm using EF tables, but it was seeded from the below:

using IdentityServer4;
using IdentityServer4.Models;
using System.Collections.Generic;

namespace Charla
{
    public static class IdentityConfig
    {
        public static IEnumerable<IdentityResource> IdentityResources =>
                   new IdentityResource[]
                   {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                   };

        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource("charla-api", "Charla API Resource")
            };
        }

        public static IEnumerable<ApiScope> ApiScopes =>
            new ApiScope[]
            {
                new ApiScope("charla-api", "Charla API Scope")
            };

        public static IEnumerable<Client> Clients =>
            new Client[]
            {
                // charla web app
                new Client
                {
                    ClientId = "charla-spa",
                    ClientName = "Charla Web App",
                    RequireClientSecret = false,
                    AllowOfflineAccess = true,
                    AllowAccessTokensViaBrowser = true,

                    AllowedGrantTypes = GrantTypes.Code,
                    RedirectUris = { 
                        "https://localhost:5001/authentication/login-callback",
                        "https://app-dev.getcharla.com/authentication/login-callback",
                        "https://app.getcharla.com/authentication/login-callback"
                        },
                    //FrontChannelLogoutUri = "https://localhost:5001/authentication/logout-callback",
                    PostLogoutRedirectUris = { 
                        "https://localhost:5001/authentication/logout-callback",
                        "https://app-dev.getcharla.com/authentication/logout-callback",
                        "https://app.getcharla.com/authentication/logout-callback"
                    },

                    
                    AllowedScopes = { 
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        IdentityServerConstants.StandardScopes.OfflineAccess,
                        "charla-api"
                     }
                },

                // mobile client
                new Client
                {
                    ClientId = "charla-mobile",
                    ClientName = "Charla Mobile Apps",
                    RequireClientSecret = false,
                    AllowedGrantTypes = GrantTypes.Code,
                    AllowAccessTokensViaBrowser = true,
                    AllowOfflineAccess = true,

                    RedirectUris = { 
                        "https://getcharla.com/ios_redirect",
                        "http://localhost:8100/auth/callback"
                    },
                    PostLogoutRedirectUris = { 
                        "https://getcharla.com/ios_redirect_endsession",
                        "http://localhost:8100/auth/endsession"
                    },

                    AllowedScopes = { 
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        IdentityServerConstants.StandardScopes.OfflineAccess,
                        "charla-api"
                     },

                    AllowedCorsOrigins = { "http://localhost:8100", "https://localhost:5001", "http://localhost:5000" }
                },


            };
    }
}

login.component.ts - The function in the web component that fires the call to the login endpoint and that is doing the redirection:

enter image description here


    ngOnInit(): void {

        this.route.queryParams.subscribe(params => {
            this.returnUrl = params['returnUrl'] || '/';
        });
    }

    /**
     * Form Submit
     */
    submit() {
        const controls = this.loginForm.controls;

        const authData = {
            email: controls['email'].value,
            password: controls['password'].value
        };
        this.auth
            .login(authData.email, authData.password, this.returnUrl)
            .pipe(
                finalize(() => {
                    this.loading = false;
                    this.cdr.markForCheck();
                })
            )
            .subscribe(
                result => {
                    window.location = this.returnUrl;
                    this.router.navigateByUrl(this.returnUrl); // Main page
                },
                err => {
                    console.log(err);

                    if (( 'status' in err) && ( err.status === 404)){
                        this.authNoticeService.setNotice(this.translate.instant('AUTH.VALIDATION.INVALID_LOGIN'), 'danger');
                    }
                }
            );
    }

Have no clue why its not working. I read I should be using HttpContent.SignInAsync in the login web api endpoint, but im already using var result = await signInManager.PasswordSignInAsync(model.email, model.password, isPersistent: true, lockoutOnFailure: false); so I assume thats enough.

Other choices I made I'm not sure are right, like using AddIdentityCore instead of AddIdentity. Should I be adding AddAspNetIdentity<ApplicationUser>()?

2
  • I am fighting with this same issue. I've set up about 5 or 6 apps in the past with IDS4, and for some reason this is something I'm really struggling with. Have you had any luck with this? Commented Nov 16, 2020 at 6:13
  • Does this answer your question? Identity server does not redirect after sucessfull login Commented Dec 5, 2020 at 22:59

3 Answers 3

11

By following the trace you provided that could be happening because of the new rules around Cookies on the Browsers.

On new ASP.NET Core applications the samesite=none attribute is automatically added, but the Browsers require that you specify the Secure attribute too otherwise the Set-Cookie will be blocked.

To configure your IdentityServer add the following block of code inside the Configure method in the Startup class:

app.UseCookiePolicy(new CookiePolicyOptions
{
     HttpOnly = HttpOnlyPolicy.None,
     MinimumSameSitePolicy = SameSiteMode.None,
     Secure = CookieSecurePolicy.Always
});

When using the secure attribute HTTPS must be used otherwise it will fail

Another SO question regarding the topic: Session cookie set `SameSite=None; Secure;` does not work

And some information by Microsoft on ASP.NET Core: https://learn.microsoft.com/pt-br/aspnet/core/security/samesite?view=aspnetcore-5.0

Sign up to request clarification or add additional context in comments.

2 Comments

Well this got me past the spinning login, but now I'm getting: "Correlation failed. Unknown location" and what I'm reading is that you HAVE to use https even locally. Ugh.
This worked for me even while using http on localhost. The browser I'm using is Safari. Regardless, can you point me to the documentation in Identity Server where it mentions that this is what you need to do to be able to test redirection when running locally? Does it exist?
0

I experienced a similar issue with my SPA, when logging out and logging back in again. It seems the issue was caused by a Visual Studio update. If you navigate to the project folder using the command prompt and usedotnet run ' or even better dotnet watch run (so you can make changes to the .net core code while it's running) you can circumnavigate the issue; buy not running the code through IIS in visual studio.

If this works for you update the Properties Launch to be Project (which also uses dotnet run) and you can then debug as well.

Comments

0

To work with both http and https, I configured cookie policy as follows:

app.UseCookiePolicy(new CookiePolicyOptions
{
     HttpOnly = HttpOnlyPolicy.None,
     MinimumSameSitePolicy = SameSiteMode.Lax,
     Secure = CookieSecurePolicy.SameAsRequest
});

This configuration corrected Chrome/Edge's redirect to the login page.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.