Sustainsys / Saml2

Saml2 Authentication services for ASP.NET
Other
960 stars 603 forks source link

How to support multiple Okta accounts #356

Closed biddlem closed 8 years ago

biddlem commented 8 years ago

I have a working example to integrate with an Okta account following this example(https://github.com/KentorIT/authservices/blob/master/doc/IdentityServer3Okta.md). However, we need to support the ability to integrate with n number of Okta accounts(company 1's okta, company 2's okta, etc).

Here is what I've tried:

        var firstCompanyOktaOptions = new KentorAuthServicesAuthenticationOptions(false)
        {
            SPOptions = new SPOptions
            {
                EntityId = new EntityId(ConfigurationManager.AppSettings.Get("EntityId")), // from (B) above
                ReturnUrl = new Uri(ConfigurationManager.AppSettings.Get("ReturnUrl"))
            },
            SignInAsAuthenticationType = signInAsType,
            AuthenticationType = "firstCompany_okta", // this is the "idp" - identity provider - that you can refer to throughout identity server
            Caption = "FirstCompany Okta",  // this is the caption for the button or option that a user might see to prompt them for this login option             
        };

        firstCompanyOktaOptions.IdentityProviders.Add(new IdentityProvider(new EntityId(ConfigurationManager.AppSettings.Get("firstCompanyOktaEntityId")), firstCompanyOktaOptions.SPOptions)  // from (F) above
        {
            LoadMetadata = true,
            MetadataUrl = new Uri(ConfigurationManager.AppSettings.Get("firstCompanyOktaMetadataUrl")), // see Metadata note above
            AllowUnsolicitedAuthnResponse = true
        });

        app.UseKentorAuthServicesAuthentication(firstCompanyOktaOptions);

        var secondCompanyOktaOptions = new KentorAuthServicesAuthenticationOptions(false)
        {
            SPOptions = new SPOptions
            {
                EntityId = new EntityId(ConfigurationManager.AppSettings.Get("EntityId")), // from (B) above
                ReturnUrl = new Uri(ConfigurationManager.AppSettings.Get("ReturnUrl"))
            },
            SignInAsAuthenticationType = signInAsType,
            AuthenticationType = "secondCompany_okta", // this is the "idp" - identity provider - that you can refer to throughout identity server
            Caption = "SecondCompany Okta",  // this is the caption for the button or option that a user might see to prompt them for this login option             
        };

        secondCompanyOktaOptions.IdentityProviders.Add(new IdentityProvider(new EntityId(ConfigurationManager.AppSettings.Get("secondCompanyOktaEntityId")), secondCompanyOktaOptions.SPOptions)  // from (F) above
        {
            LoadMetadata = true,
            MetadataUrl = new Uri(ConfigurationManager.AppSettings.Get("secondCompanyOktaMetadataUrl")), // see Metadata note above
            AllowUnsolicitedAuthnResponse = true
        });

        app.UseKentorAuthServicesAuthentication(secondCompanyOktaOptions);

I've also tried:

        var oktaOptions = new KentorAuthServicesAuthenticationOptions(false)
        {
            SPOptions = new SPOptions
            {
                EntityId = new EntityId(ConfigurationManager.AppSettings.Get("EntityId")), // from (B) above
                ReturnUrl = new Uri(ConfigurationManager.AppSettings.Get("ReturnUrl"))
            },
            SignInAsAuthenticationType = signInAsType,
            AuthenticationType = "okta", // this is the "idp" - identity provider - that you can refer to throughout identity server
            Caption = "Okta",  // this is the caption for the button or option that a user might see to prompt them for this login option             
        };

        oktaOptions.IdentityProviders.Add(new IdentityProvider(new EntityId(ConfigurationManager.AppSettings.Get("firstCompanyOktaEntityId")), oktaOptions.SPOptions)  // from (F) above
        {
            LoadMetadata = true,
            MetadataUrl = new Uri(ConfigurationManager.AppSettings.Get("firstCompanyOktaMetadataUrl")), // see Metadata note above
            AllowUnsolicitedAuthnResponse = true
        });

        oktaOptions.IdentityProviders.Add(new IdentityProvider(new EntityId(ConfigurationManager.AppSettings.Get("secondCompanyOktaEntityId")), oktaOptions.SPOptions)  // from (F) above
        {
            LoadMetadata = true,
            MetadataUrl = new Uri(ConfigurationManager.AppSettings.Get("secondCompanyOktaMetadataUrl")), // see Metadata note above
            AllowUnsolicitedAuthnResponse = true
        });

        app.UseKentorAuthServicesAuthentication(oktaOptions);

Please let me know what the correct approach is.

AndersAbel commented 8 years ago

I'd go with the second approach, where you just have one middleware instance. The first is also possible, but then you would need to manually set the modulePath for at least one instance, so that they don't collide.

When using the second approach, you have to make it possible to select the different identity providers. If you can control the login buttons, you can use one of the methods described in the owin docs to select idp.

Another option is to use a discovery service to handle the idp selection.

dahlsailrunner commented 8 years ago

As it happens, I'm just now trying to do the same thing and was wondering if you (@AndersAbel) could elaborate just a bit on the above two options. For the first option -- how does one set the modulePath for an instance of the middleware? Is it just something like this, where the authServicesOptions are the custom-per-client item?

app.Map("AuthServices/newpath" idApp => 
       { 
           idApp.UseKentorAuthServicesAuthentication(authServicesOptions); 
       });

And for the second option, I'm not sure where we would inject code to do something like that. My "main" login page is not on id server -- but rather my app site. When the user provides a username, if they are an okta user I create a full id-server authorize url for them specifying the idp in the acr_values. Identity server takes over at that point at routes the request. I could have a unique idp value in the authorize url, but am unsure how to get it properly translated into the selection within identity server.

When we (this group) come up with a good and final approach I can update the docs to reflect the multi-client stuff. And I haven't forgotten about the improved documentation setup. :)

-Erik

albinsunnanbo commented 8 years ago

As an alternative to putting the idp in the Owin dictionary you can pass it as a query string parameter to the SignIn method, see https://github.com/KentorIT/authservices/blob/4ffe5c54f8b2100a37a6e4cf7269f65a40b0798e/SampleMvcApplication/Views/Home/Index.cshtml#L23

biddlem commented 8 years ago

I was able to successfully get it to work with option 1 (with two middleware instances) by just assigning a ModulePath:

    var secondCompanyOktaOptions = new KentorAuthServicesAuthenticationOptions(false)
    {
        SPOptions = new SPOptions
        {
            EntityId = new EntityId(ConfigurationManager.AppSettings.Get("EntityId")), // from (B) above
            ReturnUrl = new Uri(ConfigurationManager.AppSettings.Get("ReturnUrl")),
            ModulePath = "/AuthServicesSecondCompany"
        },
        SignInAsAuthenticationType = signInAsType,
        AuthenticationType = "secondCompany_okta", // this is the "idp" - identity provider - that you can refer to throughout identity server
        Caption = "SecondCompany Okta",  // this is the caption for the button or option that a user might see to prompt them for this login option             
    };

However, option 2 fits better into my end goal.

My end goal is to have my website's application on the Okta platform and support Idp Initiated scenarios for an unlimited number of clients.

If the Single sign on URL for my Okta application's settings is "https://myidentityserver.com/AuthServices/Acs", I'm trying to understand the following:

1) What unique identifier will come with that request that will tell me which IdentityProvider I am to authenticate against

2) If I have one middleware instance setup with two identityproviders, I still don't know how to tell it which of the two to use based on the unsolicited request. I know you can choose which idp to use by passing in the querystring idp=yourchoice, and that seems to match up against the AuthenticationType setting in KentorAuthServicesAuthenticationOptions. If I have one authentication type of "Okta", and it has two identity providers (one for Company A's okta and one for Company B's okta), that's where I get lost on how to be able to tell it which one to go against. It looks like for me right now it's always just using the first one in the collection because it's set to the default.

So to summarize the work-flow (which follows the Idp Initiated example in the docs):

1) Client A clicks on my public application in Okta 2) Okta sends an unsolicited request to https://myidentityserver.com/AuthServices/Acs 3) My SPOptions ReturnUrl would hit https://coolsite.com/Idp_InitiatedRedirect.aspx?idp=firstCompanyOkta and then eventually end up on our website's homepage

dahlsailrunner commented 8 years ago

Ok -- I got the same thing working using the "multiple middleware" approach as well. I was curious about how we might truly attack the single middleware and believe that the real answer may involve a change to the AuthServices package.

As @Christfigure has noted -- the real question is how to determine the real idp (or returnUrl, to be precise) with an unsolicited SAML request. I see two options on the surface.

Option 1: Provide a ReturnUrl configuration option on the IdentityProvider class. This way the returnUrl would be set at the Idp level, not the middleware level. Or the IDP return URL would / could override anything set at the MW level.

Option 2. Provide some kind of event that could get fired in the AcsCommand .ProcessResponse method that would allow us to see the saml response, and evaluate the Issuer of it and set a return url based on that that would override the one set at the middleware level.

From what I can see, those are the best options, but the bad news is that they imply a change to the package. For now, I think I'll just be going down the "multiple middleware" path. @AndersAbel or @albinsunnanbo please let me know if you think I'm off base or have other ideas that could support the single MW approach.

AndersAbel commented 8 years ago

Sorry for being a bit late to the party, here's my thoughts on the subject:

I've tried to answer as much as I can of the discussion above. Please post a follow up comment if there is something I missed.

svetlichniymax commented 8 years ago

Hi, I have the same problem, I need to configure identity server for work with different okta Apps, also with one login Apps. Here is my config for okta and one login: image I set ReturnUrl = new Uri("https://localhost:44319/Home/SAMLRequest") And I really need to pass params or claims to may SAML_Request action for determine who create this request, I need to pass name of user or email for automatic authorize users who clicked on App in okta or one login.

I configured tested App for okta and one login, and I've got redirect to my page https://localhost:44319/Home/SAMLRequest

But I cant to get any info who clicked to my App, I tied to get it from HttpContext.GetOwinContext().Authentication but there is no any useful info for me.

Please, Help me with my issue.

AndersAbel commented 8 years ago

@svetlichniymax Looks like you would like help with a complete design of your login implementation. That's the kind of stuff I do as consultancy services. On the issue tracker here we can only answer specific questions for free. Please mail me if you're interested in consultancy services.

yvanin commented 8 years ago

How can I select an IdP for the request in the old versions of ASP.NET (pre-OWIN)? I am able to do this by loading options on every request and making the identity provider of choice a default one (the only one) in the collection but is there a better way?

AndersAbel commented 8 years ago

@yvanin Yes, there is a better way. You can supply the Idp entity Id as a querystring param to the SignIn command: http://whatever.com/AuthServices/SignIn?idp=http%3A%2F%2Fstubidp.kentor.se%2Fmetadata

sharkyu commented 8 years ago

@AndersAbel I followed your suggestion that use multiple middleware instances in identityserver3, but the application crashed when tried to add 500+ instances. Switch to use one MW instance with multi identity providers, but cannot find a way to select the proper IDP when login through identityserver 3, do you have any doc about how to do it?

dahlsailrunner commented 8 years ago

I'm not aware of docs, but I think I have done something similar to what you need (haven't tried 500+ instances of okta, though, so not sure about that one). As far as selecting proper idp, I have an API called "getidp" that I've written that takes a username, does a database lookup for which idp they use and returns the value that is fed into the idp: acr_value setting of the authorize query string.

The way it works is this: You have a custom view service (docs on that) and in the login view you add a javascript call onblur for the username field. When the user has gotten to the identity server login page, you have a signin message that has all of the information we might need EXCEPT which idp (if external) they might need to use. But the OTHER INFO, like return url, state, nonce, client_id, etc are all in the original signin message.

I put the original authorize url in a hidden field on the page with the view service (login page load), then make my api call on the blur event of the username field. If the getidp api call returns an external idp, then set the window.location to the original authorize request (from your hidden field) PLUS the idp setting from the API call.

NOTE that if this javascript code (to call the API) is directly in your login.html view, you will need to adjust the content security policy (CSP to include inline and (if your API is not in the identity server itself) to include the connect address of your API). There are also docs on setting the CSP in identity server.

svetlichniymax commented 7 years ago

Hi guys, Have a question about supporting multiply SAML accounts. For example I can register 3 different identity providers with their metadata in Startup class: image

Is it possible to add provider metadata dynamically without restarting/recompile identity server app? Thanks.

sharkyu commented 7 years ago

@svetlichniymax you can try to hook the SelectIdentityProvider event in KentorAuthServicesAuthenticationOptions.

sharkyu commented 7 years ago

@dahlsailrunner thanks for your information. my situation is a little bit different. I am using WS-FED to integrate with TT3 which uses KentorIT to federate with external IDPs. I am trying to use single Kentor MiddleWare with multi identity providers (i.e. @Christfigure 's Option 2) because Option 1 will be crashed if 500+ Idps are added. the challenge with option 2 is how to let TT3 tell Kentor which identity provider to use. TT3 only setup authentication type when handle Authentication.Challenge(authProp, provider), but persist the original signin messsage in a cookie, the signin message has all the information Kentor needs, e.g.: idp, and acr. after reading the source code of TT3 and KentorIT, I found A solution (may not be perfect). hopefully it can help other people with the same issue. the code is based on ws-fed protocol.

  1. TT3 setup (copy from @Christfigure example)
  // tt3 setup
    var oktaOptions = new KentorAuthServicesAuthenticationOptions(false)
        {
            SPOptions = new SPOptions
            {
                EntityId = new EntityId(ConfigurationManager.AppSettings.Get("EntityId")), 
                ReturnUrl = new Uri(ConfigurationManager.AppSettings.Get("ReturnUrl"))
            },
            SignInAsAuthenticationType = signInAsType,
            AuthenticationType = "okta", // this is the "idp" - identity provider - that you can refer to throughout identity server
            Caption = "Okta",  // this is the caption for the button or option that a user might see to prompt them for this login option             
        };

        oktaOptions.IdentityProviders.Add(new IdentityProvider(new EntityId(ConfigurationManager.AppSettings.Get("firstCompanyOktaEntityId")), oktaOptions.SPOptions)  
        {
            LoadMetadata = true,
            MetadataUrl = new Uri(ConfigurationManager.AppSettings.Get("firstCompanyOktaMetadataUrl")), // see Metadata note above
            AllowUnsolicitedAuthnResponse = true
        });

        oktaOptions.IdentityProviders.Add(new IdentityProvider(new EntityId(ConfigurationManager.AppSettings.Get("secondCompanyOktaEntityId")), oktaOptions.SPOptions)  
        {
            LoadMetadata = true,
            MetadataUrl = new Uri(ConfigurationManager.AppSettings.Get("secondCompanyOktaMetadataUrl")), // see Metadata note above
            AllowUnsolicitedAuthnResponse = true
        });

        app.UseKentorAuthServicesAuthentication(oktaOptions);
  1. sp-initialize
    • client side needs to tell TT3 which identity provider to use
    protected void WSFederationAuthenticationModule_RedirectingToIdentityProvider(object sender, RedirectingToIdentityProviderEventArgs e)
        {
            e.SignInRequestMessage.HomeRealm = "okta";    // must match your authentication type
            e.SignInRequestMessage.Federation = ""firstCompanyOktaEntityId";  // can be any idp entity id 
        }

TT3 will match HomeRealm to IdentityProvider, and Add Federation into acr.

    // hook selectIdentityProvider event to return the proper identity provider
     SelectIdentityProvider = (id, dictionary) =>
                {
                    var signinid = dictionary[Constants.Authentication.SigninId];
                    var signInMessage = HttpContext.Current.Request.GetOwinContext().Environment.GetSignInMessage(signinid);
                    var acr = signInMessage.AcrValues.FirstOrDefault();
                    if (acr != null)
                    {
                        var entityId = new EntityId(acr);
                        IdentityProvider idp;
                        // find the proper idp
                        if (authServicesOptions.IdentityProviders.TryGetValue(entityId, out idp))
                            return idp;
                    }
                    return null;
                },   
  1. Idp-initialize

    The challenge for idp-initialize is the ReturnUrl is configured in the Kentor AuthService Middleware level, not idp level. to modify it, you have to hook the AcsCommandResultCreated event. sample is below:

        AcsCommandResultCreated = (result, response) =>
               {
                   if (result.Location.IsAbsoluteUri)
                   {
                       var uriBuilder = new UriBuilder(result.Location);
                       var query = HttpUtility.ParseQueryString(uriBuilder.Query);
                       query["idp"] = "okta";
                       query["federation"] = response.Issuer.Id;   
                       uriBuilder.Query = query.ToString();
    
                       result.Location = uriBuilder.Uri;
                   }
               }

    the trick here is your idp and federation info will be appended as query string and return to client side, the client side can check the query string to figure out which idp initialize the sso session and integrate with its own business logic.

svetlichniymax commented 7 years ago

Thanks for replay @sharkyu, Yes, your situation is different, I don't need to identify IdP, maybe it will need to me in the future, thanks for advice anyway.

So, for now, I have to dynamically add new Identity provider in existing configuration. For example I have 3 static configurations with provider metadata for OneLogin (It can be also Okta, NetIQ ....):

image

I want to add another dynamically from client code without rebuilding IdentityServer. Because static configuration is not flexible, every time when new customer want to use SSO SAML I need to rebuild IdentityServer for apply new metadata configuration.

Any advice for my problem?

Thanks.

dahlsailrunner commented 7 years ago

I'm going to be doing something similar with ours. We host the production identity server in a web farm, and use the Entity Framework module for the operational data to not require server affinity. This way, when we add configuration to the database and need to recycle the identity server app pools to pick up the changes, we can roll the recycles without affecting availability -- or at least that's the plan. :)

HTH

svetlichniymax commented 7 years ago

Yes, it can be as part of continuous integration and continuous delivery process, but really it is a simple things and we need to process it with a tricky way :(.

sharkyu commented 7 years ago

@svetlichniymax you still can hook SelectIdentityProvider to lazy load identity server. I test locally and work well.

the idea is KentorIT auth service will call SelectIdentityProvider to resolve proper identity provider(https://github.com/KentorIT/authservices/blob/25a7ec8c1afeacfe681a19b5001da86f39d91cf6/Kentor.AuthServices/WebSSO/SignInCommand.cs#L75)

assuming your client and identity server share the same data source of identity server configuration,e.g. database

  1. client add a new identity server to your identity server db
  2. your identity server app hook SelectIdentityProvider event, pseudo code below
SelectIdentityProvider = (idpEntityId, dictionary) =>
                {
                       if (!authServicesOptions.IdentityProviders.TryGetValue(idpEntityId, out idp))
                        {
                             read identity server configuration from your data source by idpEntityId

                            var idp = new IdentityProvider(idpEntityId, authServicesOptions.SPOptions)
                            {
                                LoadMetadata = true,
                                MetadataLocation =  ...
                            };
                            authServicesOptions.IdentityProviders.Add(idp)
                        }
                        return idp;
                    }
svetlichniymax commented 7 years ago

Hi @sharkyu , Could you provide sample code how to make request to IdentityServer from the client (API action) to fire SelectIdentityProvider event? I really can't figure out with that.

sharkyu commented 7 years ago

@svetlichniymax I am using WS-FED, not API directly.

public override void Init()
{
    base.Init();
    FederatedAuthentication.FederationConfigurationCreated += FederatedAuthentication_OnServiceConfigurationCreated;
}

void FederatedAuthentication_OnServiceConfigurationCreated(object sender, FederationConfigurationCreatedEventArgs e)
{
    var fam = FederatedAuthentication.WSFederationAuthenticationModule;
    fam.RedirectingToIdentityProvider += WSFederationAuthenticationModule_RedirectingToIdentityProvider;
}

protected void WSFederationAuthenticationModule_RedirectingToIdentityProvider(object sender, RedirectingToIdentityProviderEventArgs e)
{
    e.SignInRequestMessage.HomeRealm = "okta";    // must match your authentication type
    e.SignInRequestMessage.Federation = ""firstCompanyOktaEntityId";  // can be any idp entity id 
}   
AndersAbel commented 7 years ago

@sharkyu If you're using the WS-Fed module, you're in the wrong place to ask question. This issue tracker is for the Kentor.AuthServices SAML2P packages only.

svetlichniymax commented 7 years ago

Hi @AndersAbel I don't use WS-Fed module. I'm trying to figured out with my problem to add Identiry Provider dynamically. Is it possible to do it dynamically like create specific request to Identity Server and handle this request to add info about new provider in KentorAuthServicesAuthenticationOptions IdentityProvider Dictionary?

AndersAbel commented 7 years ago

@svetlichniymax Please open a new issue. This one is old and closed.

And if your code contains stuff like this, you're obviously using ws-fed module. Those things are not related to AuthServices.


var fam = FederatedAuthentication.WSFederationAuthenticationModule;
fam.RedirectingToIdentityProvider += WSFederationAuthenticationModule_RedirectingToIdentityProvider;
sharkyu commented 7 years ago

@AndersAbel , I did use Kentor.AuthServices SAML2P packages to integrate with TT3. I figured out a way how to use single AutheServices instance to support multi external IDPs by hooking SelectIdentityProvider and AcsCommandResultCreated . it works perfectly. I shared the idea here.

The reason I posted the WS-FED code is because @svetlichniymax is asking for the sample code to trigger the workflow. since my RPs use ws-fed to talk with TT3, i just gave some sample code for him. The ws-fed code is only for client side, not for token service side (tt3 + authservice), and I am not asking the question about ws-fed.