Sustainsys / Saml2

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

Idp-Init and SP-Init flows using IdentityServer and AuthServices supporting multiple identity providers #748

Closed designsinnovate closed 6 years ago

designsinnovate commented 7 years ago

First, Thank you for this great library. I came across many posts around these two SAML flows and integrating them with IdentityServer, thought I should contribute back with some documentation and consolidation of my findings.

Learned about this library from https://github.com/IdentityServer/IdentityServer3/issues/833, that eventually led me to this documentation IdentityServer3Okta.md, which was great and very easy to follow.

How to support multiple providers and manage them dynamically when integrated with IdentityServer?

This is a hot topic with many discussions - these are just a few ... Is there a sample using multiple IdPs? #724 Register Identity Provider after App Startup #540 How to support multiple Okta accounts #356 Question: Dynamic configuration for OWIN middleware? #287

To use single middleware instance or multiple middleware instances for each external provider

Why not multiple middleware instances for each provider? Because trying to change OWIN pipeline dynamically once it is configured during startup is not so simple - and I ran into issues as soon as I tried to put IdentityServer in a dynamic pipeline and there aren't many examples out there on this topic, at least none that I could find.

Use multiple middleware instances when there are finite and known number of external providers.

Single middleware instance works best with multiple SAML providers when providers need to be managed dynamically CRUD style. And Providing notification hooks shows some great thought by the author that makes this process a breeze.

So, how to select which Identity Provider to redirect the user to?

I could be wrong, but I think this suggestion for Selecting IdP assumes that AuthServices is installed as a library within an existing OWIN based application, not a separate OWIN application that uses UseOpenIdConnectAuthentication middleware to authenticate with IdentityServer + AuthServices.

App > IdentityServer > AuthService. I didn't try this but I think RedirectToIdentityProvider notification might be a good place to manipulate signin message to IdentityServer by passing idp query string that could potentially get to AuthService notification SelectIdentityProvider.

Notifications = new OpenIdConnectAuthenticationNotifications {
 RedirectToIdentityProvider = n => {
  if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.AuthenticationRequest) {
    // manipulate signin message or query string to pass idp to IdentityServer to  AuthServices
   }
  }
  return Task.FromResult(0);
 }
}

First approach - use cookies

KentorAuthOptions = new KentorAuthServicesAuthenticationOptions(false) {
  SPOptions = new SPOptions {
   AuthenticateRequestSigningBehavior = SigningBehavior.Always,
   ReturnUrl = new Uri("https://.../samples/mvcapp/Home/Idp_InitiatedRedirect?idp=saml"),
  },
  Notifications = new KentorAuthServicesNotifications {
   MessageUnbound = result => {
     if (Regex.IsMatch(result.Data.Name, "saml.*Response")) {
      var idpUrl = result.Data["Issuer", Saml2Namespaces.Saml2Name] ? .InnerText.Trim();
      HttpContext.Current.Response.Cookies.Add(new HttpCookie("authidp", idpUrl));
     }
    },

    SelectIdentityProvider = (id, dictionary) => {
     IdentityProvider idp = null;
     var idPCookie = HttpContext.Current.Request.Cookies.Get("authidp");
     KentorAuthOptions.SPOptions.ServiceCertificates.Clear();

     if (!string.IsNullOrEmpty(idPCookie ? .Value)) {
      idp = KentorAuthOptions.IdentityProviders[new EntityId(idPCookie.Value)];
     }

     if (idp == null || idp.EntityId.Id.Contains("stubidp.kentor.se")) {
      KentorAuthOptions.SPOptions.ServiceCertificates.Add(Certificate.Load());
     } else if (idp.EntityId.Id.Contains("adfs.site.com")) {
      KentorAuthOptions.SPOptions.ServiceCertificates.Add(Certificate.LoadPersonalCertificate("adfs.pfx", ""));
     }
     return idp;
    },

    LogoutCommandResultCreated = result => {
     var idPCookie = HttpContext.Current.Request.Cookies.Get("authidp");
     if (!string.IsNullOrEmpty(idPCookie ? .Value)) {
      idPCookie.Expires = DateTime.Now.AddDays(-10);
      idPCookie.Value = null;
      HttpContext.Current.Response.Cookies.Add(idPCookie);
     }
    }
  }
};

Second approach without cookies (and better certificate management)

inspired by this https://github.com/KentorIT/authservices/issues/356#issuecomment-259473970

KentorAuthOptions = new KentorAuthServicesAuthenticationOptions(false) {
  SPOptions = new SPOptions {
   AuthenticateRequestSigningBehavior = SigningBehavior.Always,
   ReturnUrl = new Uri("https://.../samples/mvcapp/Home/Idp_InitiatedRedirect?idp=saml"),
  },
  Notifications = new KentorAuthServicesNotifications {
   AcsCommandResultCreated = (cr, r) => {
     // Idp-Init flow
     if (r.InResponseTo == null && cr.Location.IsAbsoluteUri) {
      UriBuilder uriBuilder = new UriBuilder(cr.Location);
      var query = HttpUtility.ParseQueryString(uriBuilder.Query);
      query["idpurl"] = r.Issuer.Id;
      uriBuilder.Query = query.ToString();

      cr.Location = uriBuilder.Uri;
     }
    },

    SelectIdentityProvider = (id, dictionary) => {
     IdentityProvider idp = null;
     var signinid = dictionary["signinid"];
     var msg = HttpContext.Current.GetOwinContext().Environment.GetSignInMessage(signinid);
     UriBuilder url = new UriBuilder(msg.ReturnUrl);
     var query = HttpUtility.ParseQueryString(url.Query);
     var idpUrl = query.Get("idpurl");

     if (!string.IsNullOrEmpty(idpUrl)) {
      idp = KentorAuthOptions.IdentityProviders[new EntityId(idpUrl)];
     }

     return idp;
    },

    AuthenticationRequestCreated = (request, provider, dictionary) => {
     var certs = KentorAuthOptions.SPOptions.ServiceCertificates;
     if (provider.EntityId.Id.Contains("stubidp.kentor.se")) {
      request.SigningCertificate = certs.Select(c => c.Certificate).FirstOrDefault();
     } else if (provider.EntityId.Id.Contains("adfs.site.com")) {
      request.SigningCertificate = certs.Select(c => c.Certificate).ElementAt(1);
     }
    }
  }
};

Not sure which options is better, but I guess anything to avoid another cookie is a good thing ...?

SPI-Init: application calls IdentityServer /authorize endpoint

var idp = Request.QueryString["idp"];
var idpurl = Request.QueryString["idpurl"];
var extraParams = new Dictionary < string,
 string > ();
if (!string.IsNullOrEmpty(idpurl)) {
 extraParams.Add("idpurl", idpurl);
}
var request = new AuthorizeRequest("https://.../identity/core/connect/authorize");
var url = request.CreateAuthorizeUrl(
 "mvc",
 "id_token token",
 "openid profilei",
 responseMode: "form_post",
 redirectUri: "https://.../samples/mvcapp/Home/About",
 state: CryptoRandom.CreateUniqueId(),
 nonce: CryptoRandom.CreateUniqueId(),
 acrValues: string.Format("idp:{0}", idp),
 extra: extraParams);

return Redirect(url);

IdP-Init case: SAML assertion comes in, the response is used to figure out IdP to use, append it to Idp_InitiatedRedirect as a query string parameter, which then starts starts the SPI-Init flow.

Certificate Management

In above code, assertions are encrypted where each IdP has its own certificate. This library picks the first certificate to sign the assertions, the second approach highlighted above illustrates picking an appropriate certificate for an IdentityProvider.

Also, if an unsolicited SAML assertion (Idp-Init) comes in and AuthServices is unable to validate it perhaps due to a decryption failure, AuthService will send a query string parameter error=access_denied to the Idp_InitiatedRedirect.

In the second approach above, this parameter is ignored to start the SPI-Init flow. Since SAML assertion failed to validate, we have no clue where the assertion came from, so SPI-Init flow falls back to the default / first identity provider.

I feel this is where cookie based approach works better since it looks at raw XML message in the MessageUnbound notification.

Once again, thank you for sharing this library!

explunit commented 7 years ago

Thank you for contributing back your findings. Is there something further you would suggest about adding to the official documentation? If so, perhaps you could create a version of the SampleIdentityServer3 application in your fork so that your suggestion can be analyzed in more detail.

AndersAbel commented 6 years ago

Old issue without recent activity, closing.