多租户 ASP.NET MVC 应用程序的跨域 OWIN 身份验证

Cross-Domain OWIN Authentication for Multi-Tenanted ASP.NET MVC Application

我正在为多租户 ASP.NET MVC 应用程序使用 OWIN 身份验证。

应用程序和身份验证位于单个应用程序中的一台服务器上,但可以通过许多域和子域访问。例如:

www.domain.com
site1.domain.com
site2.domain.com
site3.domain.com
www.differentdomain.com
site4.differentdomain.com
site5.differentdomain.com
site6.differentdomain.com

我想允许用户登录这些域中的任何一个,并且无论使用哪个域访问应用程序,他们的身份验证 cookie 都有效。

我的身份验证设置如下:

public void ConfigureAuthentication(IAppBuilder Application)
{
    Application.CreatePerOwinContext<RepositoryManager>((x, y) => new RepositoryManager(new SiteDatabase(), x, y));

    Application.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        CookieName = "sso.domain.com",
        CookieDomain = ".domain.com",
        LoginPath = new PathString("/login"),
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,  
        Provider = new CookieAuthenticationProvider
        {
            OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<UserManager, User, int>(
                validateInterval: TimeSpan.FromMinutes(30),
                regenerateIdentityCallback: (manager, user) => user.GenerateClaimsAsync(manager),
                getUserIdCallback: (claim) => int.Parse(claim.GetUserId()))
        }
    });

    Application.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
}

我还在我的应用程序的根 web.config 中为我的应用程序明确设置了机器密钥:

<configuration>
    <system.web>
        <machineKey decryption="AES" decryptionKey="<Redacted>" validation="<Redacted>" validationKey="<Redacted>" />
    </system.web>
</configuration>

更新

当我在 domain.com 和 site1.domain.com 之间导航时,此设置按预期工作,但现在它不允许我登录到不同的 domain.com.

我了解 cookie 与单个域相关联。但是,跨多个域保持登录的最简单方法是什么?有没有办法让我从不同的域读取 cookie,解密它,然后为不同的域重新创建一个新的 cookiedomain.com?

您对 cookie 的工作原理是正确的,但它不是 OWIN 的工作原理。

不要覆盖 Auth 服务器的 cookie 域(auth.domain.com)。

您可以将各个站点的 cookie 域覆盖为 "site1.domain.com" 和 "site2.domain.com"。

在您的 SSO 页面中,假设有人登陆 site1.domain.com,并且由于未经身份验证被带到您的身份验证服务器。 auth 服务器获取登录凭据并向注册 URI 上的 site1.domain.com 发送代码(例如:/oauthcallback)。 site1.domain.com 上的此端点将从代码和登录(自动写入 cookie)中获取访问令牌。所以 2 个 cookie 写在 auth.domain.com 上,第二个写在 site1.domain.com

现在,同一用户访问 site2.domain.com 并在 "auth.domain.com" 上找到登录用户的 cookie。这意味着用户已登录并在 "site2.domain.com"

上创建了一个具有相同声明的新 cookie

用户现在已登录两个站点。

您不手动编写 cookie。使用 OwinContext.Signin 将保存/创建 cookie。

威廉,

我知道 cookie 绑定到一个域。

是的,您无法在客户端对其进行操作。浏览器永远不会将一个域的 cookie 发送到另一个域。

但是跨多个域保持登录的最简单方法是什么?

外部身份提供商或安全令牌服务 (STS) 是实现此目的的最简单方法。在此设置中,所有域 site1.com。 site2.com etc 将信任 STS 作为身份提供者。在此联合解决方案中,用户使用 STS 进行身份验证,并且联合身份在所有域中使用。这是一位专家关于此主题的精彩 resource

有没有办法让我从不同的域读取 cookie,解密它,然后为 differentdomain.com 重新创建一个新的 cookie?

通过一些调整,您可以使用当前设置实现此联合解决方案。仅供参考,这不是推荐的或正在使用的方法,而是一种帮助您实现目标的想法。

假设您有多个域 1、2、3 指向一个应用程序。我将创建另一个域 STS 指向同一个应用程序,但只处理 cookie 创建和验证。创建一个使用 asp.net cookie 身份验证中间件的自定义中间件。仅当请求针对 STS 域时才会执行。这可以通过域/主机上的简单 if 条件或使用 IAppBuilder 界面上的地图来实现。

让我们看看流程:

一个。用户尝试使用域 1

访问受保护的资源

b。由于他未通过身份验证,他将被重定向到域 STS,并使用域 1 的查询参数(用于 STS 识别他正在从哪个域访问资源)和域 1 上受保护资源的 url[=12] =]

c。由于请求是针对 STS 域的,因此自定义中间件会启动并对用户进行身份验证。并发送两个 cookie,一个用于 STS,另一个用于他正在尝试的域(在本例中为 1)。

d.现在用户将被重定向到 domain1

上的受保护资源

e。如果他试图访问域 2 上的受保护资源,则他未通过身份验证,因此将被重定向到 STS。

f。因为他有一个用于 STS 的身份验证 cookie,浏览器会将其附加到 STS 的此请求中。 STS 中间件可以验证 cookie 并可以对用户进行身份验证。如果进行身份验证,则为域 2 发出另一个 cookie,并将其重定向到域 2 上的受保护资源。

如果您仔细观察流程,它与我们在拥有外部 STS 时所做的类似,但在我们的示例中,STS 是我们的应用程序。我希望这是有道理的。

如果我必须执行此任务,我会使用位于同一主机 (IIS) 上的外部 STS。 IdentityServer 是 OpenID Connect 标准的开源实现,我将其用作 STS。它在使用方面非常灵活,可以与我们的应用程序共同托管(我认为这对您来说很重要)。这里有链接 Identity server, Video

希望对您有所帮助。请让我知道,如果你有任何问题。

谢谢, 苏玛.

要回答关于您更新的问题,无法跨不同域共享 cookie。

您可以使用一些查询字符串参数和一些服务器端逻辑来处理这种特殊情况,但这可能会引起一些安全问题。

采纳这个建议:

更新

根据您的评论,以下是详细信息:

https://blog.whosebug.com/2010/09/global-network-auto-login/

https://meta.stackexchange.com/questions/64260/how-does-sos-new-auto-login-feature-work

http://openid.net/specs/openid-connect-session-1_0.html

奖金:

今天使用的机制与上面前两个链接中描述的机制有点不同,也更简单。

如果您在登录 Whosebug 时查看网络请求,您会发现它会单独将您登录到网络上的每个站点。

https://stackexchange.com/users/login/universal.gif?authToken=....

https://serverfault.com/users/login/universal.gif?authToken=...

https://askubuntu.com/users/login/universal.gif?authToken=...

等等等等...

既然您需要简单的东西,请考虑一下。在您的特定设置中,您实际上只有一个应用程序可以通过多个域名访问,您可以使 "single sign on" 变得简单。首先,您必须选择负责初始身份验证的单一域名。假设是 auth.domain.com(记住它只是域名 - 您的所有域仍然指向单个应用程序)。那么:

  1. 假设用户在 domain1.com 而你发现他不在 logged-in(没有 cookie)。您将他引导至 auth.domain.com 登录页面。
  2. 假设您已经logged-in在那里。您会看到该请求来自 domain1.com(通过 Referrer header,或者您可以显式传递域)。您验证这是您的受信任域(重要),并像这样生成身份验证令牌:

    var token = FormsAuthentication.Encrypt(
        new FormsAuthenticationTicket(1, "username", DateTime.Now, DateTime.Now.AddHours(8), true, "some relevant data"));
    

    如果您不使用表单身份验证 - 只需使用机器密钥保护一些数据:

    var myTicket = new MyTicket()
    {
        Username = "username",
        Issued = DateTime.Now,
        Expires = DateTime.Now.AddHours(8),
        TicketExpires = DateTime.Now.AddMinutes(1)
    };
    using (var ms = new MemoryStream()) {
        new BinaryFormatter().Serialize(ms, myTicket);
        var token = Convert.ToBase64String(MachineKey.Protect(ms.ToArray(), "auth"));
    }
    

    所以基本上您生成令牌的方式与 asp.net 相同。由于您的网站都在同一个应用程序中 - 无需担心不同的机器密钥。

  3. 您将用户重定向回 domain1.com,在查询字符串中传递加密令牌。有关此安全隐患的示例,请参见 here。当然,我假设您使用 https,否则任何设置(无论是否 "single sign on")都是安全的。这在某些方面类似于 asp.net "cookieless" 身份验证。

  4. domain1.com 上您看到该令牌并验证:

    var ticket = FormsAuthentication.Decrypt(token);
    var userName = ticket.Name;
    var expires = ticket.Expiration;
    

    或与:

    var unprotected = MachineKey.Unprotect(Convert.FromBase64String(token), "auth");
    using (var ms = new MemoryStream(unprotected)) {
        var ticket = (MyTicket) new BinaryFormatter().Deserialize(ms);
        var user = ticket.Username;
    }
    
  5. 您使用在令牌中收到的信息在 domain1.com 上创建 cookie,并将用户重定向回他最初来自的位置。

所以有一堆重定向,但至少用户只需要输入一次密码。

更新以回答您的问题。

  1. 是的,如果您发现用户在 domain1.com 上通过了身份验证,您将重定向到 auth.domain.com。但是在 auth.domain.com 使用令牌重定向回来后 - 你像往常一样在 domain1.com 创建一个 cookie,用户变成 logged-in a domain1.com。所以这个重定向只发生一次每个用户(就像通常的登录一样)。

  2. 您可以使用 javascript 向 auth.domain.com 发出请求(XmlHttpRequest,或只是 jquery.get\post 方法)。但请注意,您必须配置 CORS 以允许这样做(例如,请参见 here)。简而言之,什么是 CORS?当从 siteA(另一个域)通过 javascript 请求 siteB 时 - 浏览器将首先询问 siteB 是否信任 siteA 发出此类请求。它通过向请求添加特殊的 headers 来实现,并且它希望看到一些特殊的 headers 作为响应。您需要添加那些 header 以允许 domain1.com 通过 javascript 请求 auth.domain.com。完成后 - 从 domain1.com javascript 向 auth.domain.com 发出此类请求,如果登录 - auth.domain.com 将 return 您如上所述进行令牌。然后使用该令牌对 domain1.com 进行查询(再次使用 javascript),以便 domain1.com 可以设置一个 cookie 作为响应。现在您已使用 cookie 在 domain1.com 登录并可以继续。 为什么我们需要所有这些,即使我们有一个应用程序只能从不同的域访问?因为浏览器不知道这一点并且对待它们完全不同。除此之外 - http 协议是无状态的,每个请求都与任何其他请求无关,因此我们的服务器还需要确认同一用户发出的请求 A 和 B,因此需要这些令牌。

  3. 是的,HttpServerUtility.UrlTokenEncode用在这里非常好,甚至比Convert.ToBase64String更好,因为无论如何你都需要url编码(你通过它在查询字符串中)。但是,如果您不在查询字符串中传递令牌(例如,您将使用上面的 javascript 方式 - 您不需要 url 对其进行编码,所以不要在其中使用 HttpServerUtility.UrlTokenEncode case.