带有 Azure AD 的身份服务器 4 - "We couldn't sign you in. Please try again."

Identity Server 4 with Azure AD - "We couldn't sign you in. Please try again."

我正在使用 .NET Core 3.1 和 Identity Server 4,并通过 OpenIdConnect 连接到 Azure AD。我正在使用 Vue.js 前端和 .NET Core API。 IdentityServer、前端和 API 都托管在同一台服务器(同一域)上。一切都使用 https。我首先使用带有 EF 模型的 Oracle 数据库,以及完全自定义的 IdentityServer 存储和自定义用户存储(我实现了接口)。我正在使用 IdentityServer 的 Quickstart,稍微编辑一下以连接我的自定义用户存储而不是测试用户。我在我的开发环境中 运行 这个。

如果我在 IdentityServer 中输入 url,我将被重定向到 Azure AD,成功登录,并显示此页面:

Grants - successful login

声明正在从 Azure AD 返回并且自动配置成功。成功写入数据库。

通过我的 JS 客户端验证身份服务器,重定向到 Azure AD,我登录,然后它重定向到 IdentityServer 的外部控制器,然后重定向回 Microsoft url,然后继续重复直到它最终失败使用此页面:

Sign-in failure from Azure AD

我猜是我在某处弄乱了重定向 uri。这是我的代码和 IdentityServer 日志:

IdentityServer Log

该记录块重复 6-10 次。最后没有错误或任何不同。

我不得不分解 C# 代码,因为站点无法处理我的长选项行之一。

IdentityServer Startup.cs:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();

        var builder = services.AddIdentityServer(options =>
        {
            options.Events.RaiseErrorEvents = true;
            options.Events.RaiseInformationEvents = true;
            options.Events.RaiseFailureEvents = true;
            options.Events.RaiseSuccessEvents = true;
            options.UserInteraction.LoginUrl = "/Account/Login";
            options.UserInteraction.LogoutUrl = "/Account/Logout";
            options.Authentication = new AuthenticationOptions()
            {
                CookieLifetime = TimeSpan.FromHours(10),
                CookieSlidingExpiration = true
            };
        }).AddClientStore<ClientStore>()
          .AddCorsPolicyService<CorsPolicyService>()
          .AddResourceStore<ResourceStore>()
          .AddPersistedGrantStore<PersistedGrantStore>()
          .AddProfileService<UserProfileService>();

        services.AddScoped<IUserStore, UserStore>();

        if (env.IsDevelopment())
        {
            // not recommended for production
            builder.AddDeveloperSigningCredential();
        }
        else
        {
            // TODO: Load Signing Credentials for Production.
        }

        services.AddAuthentication()
            .AddOpenIdConnect("aad", "Azure AD", options =>
            {
              options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
                options.SignOutScheme = IdentityServerConstants.SignoutScheme;

                options.Authority = "https://login.windows.net/[authority]";
                options.CallbackPath = "/callback-aad";
                options.ClientId = "[ClientId]";
                options.RemoteSignOutPath = "/signout-aad";
                options.RequireHttpsMetadata = true;
                options.ResponseType = OpenIdConnectResponseType.IdToken;
                options.SaveTokens = true;
                options.SignedOutCallbackPath = "/signout-callback-aad";

                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = "name",
                    RoleClaimType = "role"
                };
                options.UsePkce = true;
            });
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }

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

客户端 OIDC 配置:

const oidcSettings = {
    authority: '[IdentityServerUrl]',
    client_id: '[ClientId]',
    post_logout_redirect_uri: '[front-end url]/logout-aad',
    redirect_uri: '[front-end url]/callback-aad',
    response_type: 'code',
    save_tokens: true,
    scope: 'openid profile',
}

正在为 ExternalController 调用回调方法:

    [HttpGet]
    public async Task<IActionResult> Callback()
    {
        // read external identity from the temporary cookie
        var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
        if (result?.Succeeded != true)
        {
            throw new Exception("External authentication error");
        }

        if (_logger.IsEnabled(LogLevel.Debug))
        {
            var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
            _logger.LogDebug("External claims: {@claims}", externalClaims);
        }

        // lookup our user and external provider info
        var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);
        if (user == null)
        {
            // this might be where you might initiate a custom workflow for user registration
            // in this sample we don't show how that would be done, as our sample implementation
            // simply auto-provisions new external user
            user = await AutoProvisionUser(provider, providerUserId, claims);
        }

        // this allows us to collect any additional claims or properties
        // for the specific protocols used and store them in the local auth cookie.
        // this is typically used to store data needed for signout from those protocols.
        var additionalLocalClaims = new List<Claim>();
        var localSignInProps = new AuthenticationProperties();
        ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);

        // issue authentication cookie for user
        var isuser = new IdentityServerUser(user.SubjectId)
        {
            DisplayName = user.Username,
            IdentityProvider = provider,
            AdditionalClaims = additionalLocalClaims
        };

        await HttpContext.SignInAsync(isuser, localSignInProps);

        // delete temporary cookie used during external authentication
        await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);

        // retrieve return URL
        var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";

        // check if external login is in the context of an OIDC request
        var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
        await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, true, context?.Client.ClientId));

        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", returnUrl);
            }
        }

        return Redirect(returnUrl);
    }

Azure AD 配置:

redirect uri: [IdentityServer url]/callback-aad

数据库table数据:

Client table IMG1

Client table IMG2

ClientScopes table

ClientRedirectUris table

如果您需要任何其他信息,请告诉我。谢谢

问题出在我的自定义 UserStore 中。我通过 Azure AD SubjectId 而不是 UserSubjectId 获取用户。所以在 ExternalController 中,ApplicationUser 对象出现为 null。它没有出现异常,而是不断返回到 Azure AD 以尝试再次获取用户,但显然这只会造成无限循环。我没想到要看那里,因为我的用户已成功配置 ID 和声明。