You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core登出重定向至signin-arcgis引发OAuth state无效错误

问题背景

我们有一个基于SPA+ASP.NET Core 6的Web应用,使用通过SAML配置Microsoft Entra ID的ArcGIS Portal进行认证,ASP.NET Core借助AspNet.Security.OAuth.Providers中的ArcGIS提供程序处理登录与登出流程。需求是登出应用时同时登出身份提供商(IdP),但目前遇到以下问题:

  • 登出时会错误重定向至内部中间件路由app/signin-arcgis,抛出「The oauth state was missing or invalid」错误。
  • 用户完成登录后会被重定向至app/error而非app/index.html,需手动移除/error路径才能访问应用。

具体登出流程

  1. 用户点击“登出”
  2. 重定向至app/logout
  3. 服务端调用HttpContext.SignoutAsync删除自身认证Cookie
  4. 客户端提交隐藏表单向IdP(https://login.microsoftonline.us/{tenant}/saml2)发送LogoutRequest
  5. IdP完成登出后重定向至/portal/sharing/rest/oauth2/saml/signout
  6. 该页面删除Portal Cookie后返回Location头为app/signin-arcgis
  7. 用户无参数重定向至app/signin-arcgis,引发上述错误
  8. 后续被重定向至app/login?ReturnUrl=%2Fapp%2Ferror,登录后仍跳转至app/error

疑问

  1. 为何会重定向至app/signin-arcgis?
  2. 如何修改为重定向至app/index.html?
  3. 实现该需求是否有更优方案?

附代码

Program.cs

builder.services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddCookie(options =>
{
    options.LoginPath = "/login";
    options.LogoutPath = "/logout";
    options.Cookie.Name = authOptions.CookieName;

    options.Events.OnSigningOut = async ctx =>
    {
        // code to invalidate refresh_token removed for clarity
    };

}).AddArcGIS(options =>
{
    options.ClientId = authOptions.ClientId;
    options.ClientSecret = authOptions.ClientSecret;

    var baseUri = configuration[_PortalUrlKey];
    options.AuthorizationEndpoint = $"{baseUri}/sharing/rest/oauth2/authorize";
    options.TokenEndpoint = $"{baseUri}/sharing/rest/oauth2/token";
    options.UserInformationEndpoint = $"{baseUri}/sharing/rest/community/self";
    options.SaveTokens = true;
});

builder.Services.AddAuthorization(options =>
{
   options.FallbackPolicy = new AuthorizationPolicyBuilder()
   .RequireAuthenticatedUser()
   .Build();
});

AccountController.cs

public class AccountController : Controller
{
    private readonly string _portalUrl;
    private readonly OAuthOptions _oAuthOptions;

    public AccountController(
        IOptionsSnapshot<OAuthOptions> oAuthOptions,
        IOptionsSnapshot<PortalUrlOptions> portalUrlOptions
    )
    {
        _oAuthOptions = oAuthOptions.Value;
        _portalUrl = portalUrlOptions.Value.PortalUrl;
    }

    [Route("/login")]
    [AllowAnonymous]
    public IActionResult LogIn(string returnUrl)
    {
        var props = new AuthenticationProperties { RedirectUri = returnUrl ?? "/" };
        return new ChallengeResult("ArcGIS", props);
    }

    [Route("/logout")]
    public async Task<IActionResult> LogOut()
    {
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

        var content = BuildPageContent(HttpContext);
        return Content(content, "text/html"); // Redirect(redirTo);
    }

    private static string BuildPageContent (HttpContext context)
    {
        var saml = BuildSamlRequest(context);

        var cookies = context.Request.Cookies;
        var relayState = cookies.ContainsKey("RelayState") 
            ? cookies["RelayState"] 
            : "";

        var signOutPage = $@"<!DOCTYPE html>
        <html lang=""en"">
          <head>
            <meta charset=""UTF-8"" />
            <meta name=""viewport"" content=""width=device-width, initial-scale=1.0"" />
            <title>Signing out</title>
            <script language=""javascript"">
              window.onload = function (e) {{
                document.forms[0].submit();
              }};
            </script>
          </head>
          <body>
            Bye bye bye!
            <form name=""f"" method=""post"" action=""https://login.microsoftonline.us/{tenant}/saml2"">
                <input type=""hidden"" name=""SAMLRequest"" value=""{saml}"/>
                <input type=""hidden"" name=""post_logout_redirect_uri"" value=""https://localhost/app""/>
            <form>
          </body>
        </html>
        ";

        return signOutPage;
    }

    private static string BuildSamlRequest(HttpContext context)
    {
        string id = $"id{Guid.NewGuid()}"; // id must not begin with a number 
        string instant = DateTime.Now.ToUniversalTime().ToString("o"); // round trip format
        string destination = "https://login.microsoftonline.us/{tenant}";
        string issuer = "the_issuer";
        string email = context.User.FindFirst(ClaimTypes.Email).Value;

        string request = $@"
            <samlp:LogoutRequest 
                    xmlns:samlp=""urn:oasis:names:tc:SAML:2.0:protocol"" 
                    xmlns:saml=""urn:oasis:names:tc:SAML:2.0:assertion"" 
                    Version=""2.0"" 
                    ID=""{id}"" 
                    IssueInstant=""{instant}"" 
                    Destination=""{destination}"">
                <saml:Issuer>{issuer}</saml:Issuer>
                <saml:NameID>{email}</saml:NameID>
            </samlp:LogoutRequest>";

        var bytes = Encoding.UTF8.GetBytes(request);
        return System.Convert.ToBase64String(bytes);
    }
}
问题解答

1. 为何会重定向至app/signin-arcgis?

这是因为ArcGIS Portal的SAML登出页面(/portal/sharing/rest/oauth2/saml/signout)默认会重定向到OAuth认证的回调地址,也就是你配置的ArcGIS OAuth提供程序的默认回调路径signin-arcgis。当IdP完成登出后跳转到Portal的这个登出端点,它会自动触发这个默认重定向,而此时你的应用已经清除了认证Cookie,且没有携带OAuth所需的state参数,所以访问signin-arcgis时会抛出「The oauth state was missing or invalid」错误。

另外,登录后跳转到app/error是因为登出流程中产生的错误路径被作为ReturnUrl传递给了登录接口,登录完成后会自动跳转到这个错误的ReturnUrl。

2. 如何修改为重定向至app/index.html?

需要从三个关键环节调整:

(1)修改ArcGIS Portal的登出重定向目标

在ArcGIS Portal管理界面的SAML认证设置中,找到“登出URL”配置项,将其设置为你的应用首页https://localhost/app/index.html。这样Portal的/portal/sharing/rest/oauth2/saml/signout页面处理完登出操作后,会直接跳转到指定的首页,而非默认的signin-arcgis。

(2)过滤无效的ReturnUrl

在登录接口中,拦截包含/error的无效ReturnUrl,强制替换为应用首页:

[Route("/login")]
[AllowAnonymous]
public IActionResult LogIn(string returnUrl)
{
    var validReturnUrl = !string.IsNullOrEmpty(returnUrl) && !returnUrl.Contains("/error") 
        ? returnUrl 
        : "/app/index.html";
    var props = new AuthenticationProperties { RedirectUri = validReturnUrl };
    return new ChallengeResult("ArcGIS", props);
}

(3)优化SAML登出请求的重定向参数

修改BuildPageContent中的post_logout_redirect_uri,使其指向Portal的SAML登出端点,而非直接指向应用:

var signOutPage = $@"<!DOCTYPE html>
<html lang=""en"">
  <head>
    <meta charset=""UTF-8"" />
    <meta name=""viewport"" content=""width=device-width, initial-scale=1.0"" />
    <title>Signing out</title>
    <script language=""javascript"">
      window.onload = function (e) {{
        document.forms[0].submit();
      }};
    </script>
  </head>
  <body>
    Bye bye bye!
    <form name=""f"" method=""post"" action=""https://login.microsoftonline.us/{tenant}/saml2"">
        <input type=""hidden"" name=""SAMLRequest"" value=""{saml}"/>
        <input type=""hidden"" name=""post_logout_redirect_uri"" value=""{_portalUrl}/sharing/rest/oauth2/saml/signout""/>
    </form>
  </body>
</html>";

3. 实现该需求是否有更优方案?

有两种更简洁可靠的方案:

方案一:直接对接Entra ID的SAML服务

移除AspNet.Security.OAuth.Providers的ArcGIS提供程序,改用ITfoxtec.Identity.Saml2.MvcCore这类SAML中间件直接对接Microsoft Entra ID。利用中间件自带的单点登出功能,只需配置SignedOutCallbackPath和PostLogoutRedirectUri,即可实现登出后自动跳转首页,无需手动构建SAML请求。

方案二:使用ArcGIS Portal的OAuth登出端点

放弃手动构建SAML请求,直接调用ArcGIS Portal的OAuth登出端点,并指定最终重定向地址:

[Route("/logout")]
public async Task<IActionResult> LogOut()
{
    await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    var logoutUrl = $"{_portalUrl}/sharing/rest/oauth2/signout?redirect_uri=https://localhost/app/index.html";
    return Redirect(logoutUrl);
}

这种方式由Portal负责与Entra ID的SAML登出交互,流程更简洁,且能直接控制最终的重定向目标。


内容的提问来源于stack exchange,提问作者Ryan Taylor

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 02:25:56