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

9

我正在为一个多租户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之间导航时,此设置按预期工作,但现在它不允许我登录到differentdomain.com。

我知道cookies与单个域相关联。但是,跨多个域保持登录的最简单方法是什么?我是否可以从不同的域中读取cookie,对其进行解密,并为differentdomain.com重新创建一个新的cookie?


William,一款可以在不同子域中运行的单一应用程序 并不会 自动共享相同的MachineKey。我知道听起来很像错误术语,但更准确的称呼应该是Domain-Key,因为除非在Web.config中指定了MachineKey,否则每个域将使用不同的密钥... - Dave Alperovich
1
@DaveAlperovich - 嗨,Dave,我实际上没有意识到这一点。但是在我的web.config中,我明确设置了machineKey - 我将在上面的问题中更新具体信息。 - William
嗨@Evk - 好的,现在似乎子域名已经可以工作了。唯一剩下的问题是如何在使用非子域名的域名时(例如domain1.com和domain2.com),如何让它们在多个域名之间共享cookie?或者,我是否能够搜索属于根域的cookie,然后“采用”其中包含的信息来创建第二个域的新cookie? - William
2
使用身份验证令牌怎么样?它们不依赖于域。 - Andrew
@William - 你应该只配置你的服务器应用程序来拥有自己的令牌服务器。WebAPI和MVC都支持它。当你尝试访问站点上的某些资源时,作为身份验证结果,你会生成带有主体数据的令牌并将其发送给用户。用户应该保存它。然后,当用户尝试访问你的站点时,你只需要验证这个令牌(如果它存在于请求头中)。一般来说,这是基于JWT令牌的授权。对于你的任务有意义吗? - Andrew
显示剩余12条评论
4个回答

9

如果您需要简单的解决方案,请考虑以下内容。在您的特定设置中,您只需要一个应用程序可以通过多个域名访问,您可以实现简单的“单点登录”。首先,您必须选择负责初始身份验证的单个域名。假设这是 auth.domain.com(请记住,这只是域名 - 所有您的域名仍然指向单个应用程序)。然后:

  1. Suppose user is on domain1.com and you found he is not logged-in (no cookie). You direct him to auth.domain.com login page.
  2. Suppose you are logged-in there already. You see that request came from domain1.com (via Referrer header, or you can pass domain explicitly). You verify that is your trusted domain (important), and generate auth token like this:

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

    If you do not use forms authentication - just protect some data with machine key:

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

    So basically you generate your token in the same way asp.net does. Since your sites are all in the same app - no need to bother about different machine keys.

  3. You redirect user back to domain1.com, passing encrypted token in query string. See here for example about security implications of this. Of course I suppose you use https, otherwise no setup (be it "single sign on" or not) is secure anyway. This is in some ways similar to asp.net "cookieless" authentication.

  4. On domain1.com you see that token and verify:

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

    Or with:

    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. You create cookie on domain1.com using information you received in token and redirect user back to the location he came from initially.

因此,有许多重定向,但至少用户只需输入一次密码。

更新以回答您的问题。

  1. 如果您发现用户已在domain1.com上进行了身份验证,则重定向到auth.domain.com。但是,在auth.domain.com返回令牌后,您像往常一样在domain1.com创建cookie,用户变为已登录的domain1.com。因此,此重定向每个用户只会发生一次(就像通常的登录一样)。

  2. 您可以使用JavaScript(XmlHttpRequest或只是jquery.get\post方法)向auth.domain.com发出请求。但请注意,您必须配置CORS以允许此操作(例如,请参见here)。简而言之,CORS是什么?当通过JavaScript从siteA(另一个域)请求siteB时,浏览器将首先询问siteB是否信任siteA进行此类请求。它通过向请求添加特殊标头来执行此操作,并希望在响应中看到一些特殊标头。您需要添加这些标头以允许domain1.com通过JavaScript请求auth.domain.com。完成此操作后,从domain1.com JavaScript发出此类请求到auth.domain.com,如果已登录,则auth.domain.com将向您返回如上所述的令牌。然后使用该令牌向domain1.com发出查询(再次使用JavaScript),以便domain1.com可以在响应中设置cookie。现在,您已使用cookie在domain1.com上登录并可以继续。

  3. 是的,HttpServerUtility.UrlTokenEncode在此处使用非常好,甚至比仅使用Convert.ToBase64String更好,因为您无论如何都需要对其进行url编码(将其传递给查询字符串)。但是,如果您不会在查询字符串中传递令牌(例如,您将使用上面的JavaScript方式),则不需要对其进行url编码,因此请勿在该情况下使用HttpServerUtility.UrlTokenEncode


这很好 - 有两个问题:1)每当未经身份验证的用户查看domain1.com上的页面时,我是否总是要重定向以检查他们是否已在auth.domain.com域上登录?2)是否有任何方法可以在不进行“实际”重定向的情况下“通信”到auth.domain.com域? - William
过去我曾使用HttpServerUtility.UrlTokenEncode来获取一个base64字符串以放在查询字符串中,那么我是否还需要使用Convert.ToBase64String呢?或者仅使用HttpServerUtility.UrlTokenEncode就足够了? - William
343并不是太长。您具体收到了哪个错误?尝试增加IIS中的请求大小限制。 - Evk
我认为这是因为URL太长了 - 我在某个地方读到过,虽然整个URL可以超过2000个字符,但每个查询通常只能有250个字符... - William
@Evk 你好。请问您能指导我如何实现包含多个项目的系统吗?但是我需要登录到主项目,然后告诉其他项目:“这是一个经过身份验证的用户,无需将用户发送到登录页面”。更详细地说,假设您在domain1.com上以user1身份登录,然后单击菜单项,将您重定向到另一个域:domain2.com。但是因为您已在domain1上进行了身份验证,所以无需检查domain2上的用户有效性。我该怎么做呢?这对我来说足够了,可以让我搜索相关关键词了。 - Golnaz Saraji
显示剩余2条评论

1
您在cookie的工作原理方面是正确的,但OWIN的工作方式并非如此。
不要覆盖Auth Server(auth.domain.com)的cookie域。
您可以将各个站点的cookie域覆盖为“site1.domain.com”和“site2.domain.com”。
假设有人登陆到site1.domain.com,由于未经身份验证,因此被带到您的认证服务器上。认证服务器接收登录凭据并向在已注册URI(例如:/oauthcallback)上的site1.domain.com发送代码。此端点在site1.domain.com上将从代码获取访问令牌,并SignIn(自动编写cookie)。因此,会编写两个cookie,一个在auth.domain.com上,另一个在site1.domain.com上。
现在,同一用户访问site2.domain.com,并在“auth.domain.com”上找到已登录用户的cookie。这意味着用户已登录,并且在“site2.domain.com”上创建了具有相同声明的新cookie 用户现在已登录到两个站点。
您不需要手动编写cookie。使用OwinContext.Signin,cookie将被保存/创建。

但是如果我没有单独的认证服务器怎么办?认证和Web服务器都在同一个应用程序中运行 - 因此它是可以通过多个域访问的单个应用程序内的SSO。那么我真的需要进行OAuth回调吗?有没有一种方法可以创建跨多个域工作的cookie?或者我可以为我的根域创建一个cookie,然后当您在子域上访问该站点时,该站点将查找该cookie? - William
你有关于如何实现这个的更多信息吗? - William

1

针对你更新的问题,没有办法在不同的域之间共享cookie。

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

可以参考这个建议:https://dev59.com/N3RC5IYBdhLWcg3wYP-h#315141

更新

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

https://blog.stackoverflow.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

奖金:

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

如果您在登录StackOverflow时查看网络请求,您将看到它会分别登录到网络上的每个站点。

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

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

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

等等...


Stackoverflow是如何做到的?现在我已经通过stackoverflow.com进行了身份验证。我导航到http://askubuntu.com并单击“加入此社区”。它已经以某种方式知道我已经登录到stackexchange.com,当我按下按钮时,它确认了我的新成员资格。这个过程是如何工作的?基本上这就是我想要复制的 - - William

0

威廉,

我知道cookie与单个域名绑定。

是的,在客户端无法操纵它。浏览器从不将一个域的cookie发送到另一个域。

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

外部身份提供者或安全令牌服务(STS)是实现此目的的最简单方法。在这种设置中,所有域site1.com、site2.com等都将信任STS作为身份提供者。在这个联合解决方案中,用户通过STS进行身份验证,并且联合身份在所有域中使用。这里有一篇来自专家的resource关于这个主题的优秀文章。

我能否读取来自不同域的cookie,对其进行解密并重新创建一个新的cookie以供differentdomain.com使用?

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

假设您有多个域1、2、3指向单个应用程序。我将创建另一个指向相同应用程序但仅处理cookie创建和验证的域STS。创建一个自定义中间件,利用asp.net cookie身份验证中间件的包装。只有在请求为STS域时才会执行此操作。这可以通过对域/主机使用简单的if条件或使用IAppBuilder接口上的Map来实现。
让我们看一下流程:
a. 用户尝试使用域1访问受保护的资源
b. 由于他未经过身份验证,他将被重定向到域STS,并带有查询参数domain1(用于STS识别他正在从哪个域访问资源)和域1上受保护资源的URL
c. 由于请求是针对STS域的,因此自定义中间件会启动并对用户进行身份验证。并发送两个cookie,一个用于STS,另一个用于他正在尝试的任何域(在本例中为1)。
d. 现在用户将被重定向到域1上的受保护资源
e. 如果他尝试访问域2上的受保护资源,则未经身份验证,因此将被重定向到STS。

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

如果您仔细查看流程,它与我们在拥有外部 STS 时所做的操作类似,但在我们的情况下,STS 是我们的应用程序。我希望这是有意义的。

如果我要执行此任务,我会使用位于同一主机(IIS)上的外部 STS。IdentityServer 是 OpenID Connect 标准的开源实现,我会将其用作 STS。它在使用方面非常灵活,并且可以与我们的应用程序共同托管(我认为这在您的情况下非常好)。以下是链接 Identity serverVideo

我希望这对您有所帮助。如果您有任何问题,请告诉我。

谢谢, Soma。


请参考此链接以获取更多阅读资料:[http://stackoverflow.com/questions/27821008/sharing-owin-identity-cookie-with-mvc-5](共享OWIN身份验证Cookie)。 - Soma Yarlagadda
你好,如果可以的话,请解释一下我哪里做错了?也许我漏掉了什么。谢谢。 - Soma Yarlagadda
我不相信在这里使用通配符是有效的。 - William
请阅读我的更新答案,并发表您的意见。谢谢。 - Soma Yarlagadda

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接