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

问题描述 投票:0回答:3

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

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

现在我正在尝试对移动应用程序用户进行身份验证,但是用户交互登录屏幕不断重定向,我得到了

login_required

追踪电话,这就是我得到的:

  • 移动应用程序在 Web 视图中调用
    /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
    - Check
    中调用 AccountController.cs
  • 端点
  • 我重定向到 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
端点的调用并执行重定向:


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

c# asp.net-core asp.net-identity identityserver4
3个回答
9
投票

按照您提供的跟踪,由于浏览器上有关 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;安全;`不起作用

以及 Microsoft 关于 ASP.NET Core 的一些信息:https://learn.microsoft.com/pt-br/aspnet/core/security/samesite?view=aspnetcore-5.0


0
投票

注销并重新登录时,我的 SPA 也遇到了类似的问题。该问题似乎是由 Visual Studio 更新引起的。如果您使用命令提示符导航到项目文件夹并使用

dotnet run
' 或什至更好的
dotnet watch run
(这样您就可以在 .net core 代码运行时对其进行更改),您可以解决该问题;购买时不要在 Visual Studio 中通过 IIS 运行代码。

如果这对您有用,请将属性启动更新为项目(也使用 dotnet run),然后您也可以进行调试。


0
投票

为了同时使用 http 和 https,我配置了 cookie 策略,如下所示:

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

此配置更正了 Chrome/Edge 到登录页面的重定向。

© www.soinside.com 2019 - 2024. All rights reserved.