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

4

9

I am using OWIN Authentication for a Multi-Tenant ASP.NET MVC application.

The application and authentication sits on one server in a single application but can be accessed via many domains and subdomains. For instance:

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

I would like to allow a user to login on any of these domains and have their authentication cookie work regardless of which domain is used to access the application.

This is how I have my authentication setup:

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

I have also explicitly set a Machine Key for my application in the root web.config of my application:

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

Update

This setup works as expected when I navigate between domain.com and site1.domain.com, but now it is not letting me login to differentdomain.com.

I understand that cookies are tied to a single domain. But what is the easiest way of persisting a login across multiple domains? Is there a way for me to read a cookie from a different domain, decrypt it, and recreate a new cookie for the differentdomain.com?

Standup answered 21/4, 2016 at 17:26 Comment(17)
So when you inspect your requests\responses, which cookies do you see (if you inspect with something like firebug)?Transpierce
Are you sure you set up your machine keys correctly? Are all your sites running on different servers? If so, you need to use single machine key for all of them. Otherwise, if they are running on the same server, ensure that machine keys used by each App are the same (see here msdn.microsoft.com/en-us/library/w8h3skw9%28v=vs.100%29.aspx). Note AutoGenerate, IsolateApps settings at link above: if it is set (default) - one app cannot decrypt token of another app.Transpierce
@Transpierce - Everything is on a single server, so it is is all using the same machine key. They key here is not SSO over multiple Apps - it's SSO in a single App that is reachable via multiple domains.Standup
@Transpierce - In terms of cookies - if i go to the site and login via subdomain1.cooldomain.com I can see that Firefox has created a cookie with that domain noted. Then if I go to subdomain2.cooldomain.com the site acts as though I have not yet logged in - and it appears like Firefox has created a second cookie for the second domain even though both point to the same IP/server.Standup
"With domain noted", which one? .domain.com or subdomain.domain.com?Transpierce
I've just verified that authentication setup you provided works fine when single app is reachable by multiple subdomains. After log in site1.domain.com, I'm already logged in when I open site2.domain.com. So you need to provide more details.Transpierce
William, please clarify you statement "Everything is on a single server, so it is is all using the same machine key"... I hope you realize the machinekey is not automatically the same for every app on a single server... Are you using a root web.config? If so, have you confirmed it is taking precedence?Demicanton
@DaveAlperovich - Hi Dave - there is only a single app, running on a single instance of IIS, on a single physical server. So yes - it is using the same machinekey. Right now I am running the site on my development machine so that I can get this working, but the production is hosted on Azure.Standup
William, a single app working from different sub domains DOES NOT automatically share the same MachineKey. I know this sounds like a misnomer, but it would be more accurate to call a Domain-Key, because, unless you specify a MachineKey in your Web.config, each domain will use a different Key...Demicanton
@DaveAlperovich how can you confirm this? I don't think that is true - machine key may be isolated only per application, not per domain. I tested OP set up (using multiple domains) and it is working fine, my machine key config is AutoGenerate,IsolateApps for both encryption and validation keys.Transpierce
@Evk, I very tried this with forms Auth and confirmed it. If you have different experience with Identity cookies, I could be wrong. Again, my experience was with a single app accessed from domain and sub-domain.Demicanton
@DaveAlperovich - Hi Dave, I actually did not realize that. But in my web.config, I explicitly set the mahineKey - I will update the question above with the exact information.Standup
Hi @Transpierce - okay, so now it seems to be working with subdomains. The only issue remaining is for using domains that are not subdomains - such as domain1.com and domain2.com - how would I go about getting them to share a cookie across multiple domains? Alternatively, would I be able to search for a cookie that belongs to the root domain and then 'adopt' the contained information to create a new cookie for the second domain?Standup
@Standup you cannot do that using single cookie. Browser will never pass cookie of one domain to request to another domain. You have to make one site eligible for authentication and redirect all other sites to that single site for log in, then redirect back when it's done. I can describe how you can do this (redirect to single "main" domain for log in, then immediatly redirect back if user is already logged in there - so user won't need to type his password twice), if that is what you need.Transpierce
What about using auth tokens? they are not domain dependantToffey
@Toffey - how would I implement that?Standup
@Standup - you should only configure your server application to has its own token server. Both webapi and MVC supports it. When you trying to access some resource on your site, as authentication result you generate the token with your principal data and send it to user. User should save it. Then when user will try to access to your site you will only validate this token (if it is present in request headers). Generally - this is a JWT token based authorization. Does it make sense for your task?Toffey
T
9

Since you need something simple, consider this. In your particular setup, where you really have just one app accessible by multiple domain names, you can make simple "single sign on". First you have to choose single domain name which is responsible for initial authentication. Let's say that is auth.domain.com (remember it's just domain name - all your domains still point to single application). Then:

  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.

So there is a bunch of redirects but at least user have to type his password just once.

Update to answer your questions.

  1. Yes if you find that user is authenticated on domain1.com you redirect to auth.domain.com. But after auth.domain.com redirects back with token - you create a cookie at domain1.com as usual and user becomes logged-in a domain1.com. So this redirect happens just once per user (just as with usual log in).

  2. You can make request to auth.domain.com with javascript (XmlHttpRequest, or just jquery.get\post methods). But note you have to configure CORS to allow that (see here for example). What is CORS in short? When siteB is requested via javascript from siteA (another domain) - browser will first ask siteB if it trusts siteA to make such requests. It does so with adding special headers to request and it wants to see some special headers in response. Those headers you need to add to allow domain1.com to request auth.domain.com via javascript. When this is done - make such request from domain1.com javascript to auth.domain.com and if logged in - auth.domain.com will return you token as described above. Then make a query (again with javascript) to domain1.com with that token so that domain1.com can set a cookie in response. Now you are logged in at domain1.com with cookie and can continue. Why we need all this at all, even if we have one application just reachable from different domains? Because browser does not know that and treats them completely different. In addition to that - http protocol is stateless and every request is not related to any other, so our server also needs confirmation that request A and B made by the same user, hence those tokens.

  3. Yes, HttpServerUtility.UrlTokenEncode is perfectly fine to use here, even better than just Convert.ToBase64String, because you need to url encode it anyway (you pass it in query string). But if you will not pass token in query string (for example you would use javascript way above - you won't need to url encode it, so don't use HttpServerUtility.UrlTokenEncode in that case.

Transpierce answered 8/5, 2016 at 9:6 Comment(7)
This is great - two questions come up: 1) Every time an unauthenticated user views a page on domain1.com, would I always do a redirect to check if they are logged in on the auth.domain.com domain? 2) Is there anyways of 'communicating' to the auth.domain.com domain without doing an 'actual' redirect?Standup
Also - in the past I have used HttpServerUtility.UrlTokenEncode for getting a base64 string to put as a query string - do I also need to use Convert.ToBase64String or will HttpServerUtility.UrlTokenEncode suffice?Standup
I'm getting an error when the page attempts to redirected with the serialized, encrypted object because it is too long (it ends up being 343 characters). Is there a way to make the serialized object shorter?Standup
343 is not really too long.Which error exactly you receive? Try to increase request size limits in IIS.Transpierce
Bad Request - Invalid URL HTTP Error 400. The request URL is invalid.Standup
I assumed that it was because the URL was too long - I read somewhere that while the overall URL can be over 2000 characters, each Query can only be 250 usually...Standup
@Transpierce Hi. Could you please guide me how to implement the system which contain several apart project, But I need to login to the main project, and then I want to tell the other project that: " this is an authenticated user without sending the user to login page". For more detail, Assume you login as user1 on domain1.com, then you click on an item of menu which redirect you to another domain: domain2.com. but because you are authenticated on domain1, there is no need to check the validity of user on domain2. How can I do that? That is enough for me to tell the keywords I can search about it.Marashio
T
1

You are right on how cookie works, but that it not how OWIN works.

Don't override the cookie domain of the Auth Server(auth.domain.com).

You may override the cookie domain of the individual sites to "site1.domain.com" and "site2.domain.com".

In your SSO page, let's say someone lands on site1.domain.com and since is unauthenticated is taken to your auth server. The auth server takes the login credentials and sends a code to site1.domain.com on the registered URI(eg: /oauthcallback). This endpoint on site1.domain.com will get an access token from the code and SignIn(automatically write the cookie). So 2 cookies are written one on auth.domain.com and second on site1.domain.com

Now, same user visits site2.domain.com and finds a cookie of logged in user on "auth.domain.com". This means that the user is logged in and a new cookie is created with same claims on "site2.domain.com"

User is now logged into both site.

You don't manually write the cookie. Use OwinContext.Signin and the cookie will be saved / created.

Tautology answered 22/4, 2016 at 9:33 Comment(2)
But what if I don't have a separate auth server? The authentication and web server is all running on the same application - so it is SSO within a single application that can be reached over multiple domains. So then do I really need to do an OAuth callback? Isn't there someway of creating a cookie that will work across multiple domains? Or maybe I could create a cookie for my root domain and then when you visit the site on a subdomain, the site will look for that cookie?Standup
Do you have any more information on how to implement this/Standup
R
1

To answer the question on your update, there is no way of sharing cookies across different domains.

You could possibly use some query strings parameters and some server side logic to handle this particular case, but this could raise some security concerns.

Se this suggestion: https://mcmap.net/q/552413/-how-do-i-use-cookies-across-two-different-domains

Update

Following your comment, here are the details:

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

Bonus:

The mechanism in use today is a bit different, and simpler, than what is discribed in the first two links above.

If you look at the network requests when you login on StackOverflow, you will see that it logs you in individually to each site on the network.

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

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

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

etc, etc...

Rightist answered 7/5, 2016 at 17:5 Comment(1)
How does Stackoverflow do it? Right now I am authenticated with stackoverflow.com. I navigate to askubuntu.com and I click "Join this community". It already somehow knows that I am logged into stackexchange.com and when I press the button, it confirms my new membership. How does this process work? It's basically what I want to replicate -Standup
C
0

William,

I understand that cookies are tied to a single domain.

Yes and there is no way you can manipulate it on the client side. The browsers never send a cookie of one domain to another.

But what is the easiest way of persisting a login across multiple domains?

External Identity Provider or Security Token Service (STS) is the easiest way to achieve this. In this setup all the domains site1.com. site2.com etc will trust the STS as the identity provider. In this federated solution, the user authenticates with the STS and the federated identity is used across all the domains. Here is a great resource on this topic from an expert.

Is there a way for me to read a cookie from a different domain, decrypt it, and recreate a new cookie for the differentdomain.com?

With some tweaks you may achieve this federated solution with your current setup. FYI, this is not recommended or an in-use approach, but an idea to help you achieve the goal.

Lets say you have multiple domains 1, 2, 3 pointing to a single application. I will create another domain STS pointing to the same application but deals only with cookie creation and validation. Create a custom middleware that utilizes the asp.net cookie authentication middleware under the wrap. This gets executed only if the requests are for STS domain. This can be achieved with a simple if condition on the domain/ host or by using the Map on IAppBuilder interface.

Lets look at the flow:

a. The user tries to access a protected resource using domain 1

b. Since he is not authenticated, he will be redirected to domain STS, with a query parameter for domain1 (for STS to identify which domain he is accessing the resource from) and the url for the protected resource on domain1

c. As the request is for STS domain, the custom middleware kicks in and authenticates the user. And sends two cookies one for STS and the second one for whatever the domain (in this case 1) he is trying.

d. Now the user will be redirected to the protected resource on domain1

e. If he tries to access protected resource on domain 2, he is not autheticated hence will be redirected to STS.

f. Since he had an authentication cookie for STS that will be attached with this request to STS by the browser. The STS middleware can validate the cookie and can authenticate the user. If authenticate, issues another cookie for domain 2 and redirects him to the protected resource on domain2.

If you closely look at the flow it is similar to what we do when we have an external STS, but in our case the STS is our application. I hope this makes sense.

If I had to do this task, I would use an external STS sitting on the same host (IIS). IdentityServer, an opensource implementation of OpenID Connect standard, is what I would use as STS. It is extremely flexible in terms of usage and can be co-hosted with our application (which I think is great deal in your case). Here are links Identity server, Video

I hope that this is helpful. Please let me know if you have any questions.

Thank you, Soma.

Colotomy answered 3/5, 2016 at 22:9 Comment(4)
Please refer to this link for further reading [#27821508 owin dientity cookie).Colotomy
Hi, if it is okay, please explain what I got wrong? May be I missing something. Thank you.Colotomy
I don't believe that using a wildcard is valid here -Standup
Please read my updated answer and comment with your opinion. Thank you.Colotomy

© 2022 - 2024 — McMap. All rights reserved.