简体   繁体   中英

Authorize callback endpoint keeps redirecting to the user interaction login page IdentityServer

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:

在此处输入图片说明


    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>() ?

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://docs.microsoft.com/pt-br/aspnet/core/security/samesite?view=aspnetcore-5.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 use dotnet 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM