授权回调端点继续重定向到用户交互登录页面IdentityServer

时间:2020-08-28 20:20:15

标签: c# asp.net-core asp.net-identity identityserver4

我正在使用SPA Web应用程序,并且我正在使用IdentityServer4代码流来处理授权。因此,我具有以下组件:

  • Angular客户端应用https://localhost:5001
  • https://localhost:5001上运行的Asp.net 3 Web Api项目
  • http://localhost:8100上运行的使用JWT身份验证并使用API​​的本机移动应用程序

Rightnow正在尝试验证移动应用程序用户,但是用户交互登录屏幕不断重定向,并且正在获取login_required

回拨电话,这就是我得到的:

  • 移动应用在网络视图中调用/connect/authorize端点-检查
  • 我已重定向到我的SPA应用程序登录路径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 -检查
  • 我输入用户名/密码,并在login中呼叫AccountController.cs端点-检查
  • im重定向到IdentityServer回调处理程序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 -检查
  • 而不是使用授权码重定向到redirect_uri,而是再次将其重定向到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-这里的问题 IdentityServer无法检测到用户现在已登录。调试终端中的login_required错误。

这是我的设置:

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 -我正在使用EF表,但是它是从以下种子中植入的:

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 -Web组件中的函数,该函数将触发对login端点的调用并执行重定向:

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');
                    }
                }
            );
    }

不知道为什么它不起作用。我读到我应该在HttpContent.SignInAsync Web api端点中使用login,但是我已经在使用var result = await signInManager.PasswordSignInAsync(model.email, model.password, isPersistent: true, lockoutOnFailure: false);,所以我认为就足够了。

我不确定其他选择是否正确,例如使用AddIdentityCore而不是AddIdentity。我应该添加AddAspNetIdentity<ApplicationUser>()吗?

2 个答案:

答案 0 :(得分:0)

按照您提供的跟踪,由于有关浏览器Cookie的新规则,这种情况可能会发生。

在新的ASP.NET Core应用程序上会自动添加samesite=none属性,但是浏览器也要求您也指定Secure属性,否则Set-Cookie将被阻止。

要配置IdentityServer,请在Configure类的Startup方法内添加以下代码块:

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

使用安全属性时,必须使用HTTPS,否则它将失败

另一个与该主题有关的问题:Session cookie set `SameSite=None; Secure;` does not work

Microsoft提供的有关ASP.NET Core的一些信息:https://docs.microsoft.com/pt-br/aspnet/core/security/samesite?view=aspnetcore-5.0

答案 1 :(得分:0)

当我注销并重新登录时,我的 SPA 遇到了类似的问题。该问题似乎是由 Visual Studio 更新引起的。如果您使用命令提示符导航到项目文件夹并使用 dotnet run ' 或更好的 dotnet watch run(以便您可以在运行时更改 .net 核心代码),您可以绕过该问题;购买不通过 IIS 在 Visual Studio 中运行代码。

如果这对您有用,请将 Properties Launch 更新为 Project(也使用 dotnet run),然后您也可以进行调试。