简体   繁体   中英

Adding SSO OpenId/Azure AD auth to an existing Web Forms app

I have a web forms app currently using either forms authentication (or LDAP which then sets a FormsAuthenticationTicket cookie). I need to add SSO to this project and I'm currently using OpenID/Azure AD to authenticate with. I have the following Startup.cs configured.

     public void Configuration(IAppBuilder app)
    { 
        string appId = "<id here>";
        string aadInstance = "https://login.microsoftonline.com/{0}";
        string tenant = "<tenant here>"; 
        string postLogoutRedirectUri = "https://localhost:21770/";
        string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);

 app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
            app.UseCookieAuthentication(new CookieAuthenticationOptions());

            app.UseOpenIdConnectAuthentication(
             new OpenIdConnectAuthenticationOptions
             {
                 ClientId = appId,
                 Authority = authority,
                 PostLogoutRedirectUri = postLogoutRedirectUri,
                 Notifications = new OpenIdConnectAuthenticationNotifications
                 {
                     SecurityTokenReceived = context =>
                     {
                         System.Diagnostics.Debug.WriteLine("SecurityTokenReceived");
                         return Task.FromResult(0);
                     },

                     SecurityTokenValidated = async n =>
                     {
                         var claims_to_exclude = new[]
                         {
                             "aud", "iss", "nbf", "exp", "nonce", "iat", "at_hash"
                         };

                         var claims_to_keep =
                             n.AuthenticationTicket.Identity.Claims 
                             .Where(x => false == claims_to_exclude.Contains(x.Type)).ToList();
                         claims_to_keep.Add(new Claim("id_token", n.ProtocolMessage.IdToken));

                         if (n.ProtocolMessage.AccessToken != null)
                         {
                             claims_to_keep.Add(new Claim("access_token", n.ProtocolMessage.AccessToken));

                             //var userInfoClient = new UserInfoClient(new Uri("https://localhost:44333/core/connect/userinfo"), n.ProtocolMessage.AccessToken);
                             //var userInfoResponse = await userInfoClient.GetAsync();
                             //var userInfoClaims = userInfoResponse.Claims
                             //    .Where(x => x.Item1 != "sub") // filter sub since we're already getting it from id_token
                             //    .Select(x => new Claim(x.Item1, x.Item2));
                             //claims_to_keep.AddRange(userInfoClaims);
                         }

                         var ci = new ClaimsIdentity(
                             n.AuthenticationTicket.Identity.AuthenticationType,
                             "name", "role");
                         ci.AddClaims(claims_to_keep);

                         n.AuthenticationTicket = new AuthenticationTicket(
                             ci, n.AuthenticationTicket.Properties
                         );
                     },
                     MessageReceived = context =>
                     {
                         System.Diagnostics.Debug.WriteLine("MessageReceived");
                         return Task.FromResult(0);
                     },
                     AuthorizationCodeReceived = context =>
                     {
                         System.Diagnostics.Debug.WriteLine("AuthorizationCodeReceived"); 
                         return Task.FromResult(0);
                     },
                     AuthenticationFailed = context =>
                     {
                         System.Diagnostics.Debug.WriteLine("AuthenticationFailed");
                         context.HandleResponse();
                         context.Response.Write(  context.Exception.Message);
                         return Task.FromResult(0);
                     }
                     ,
                     RedirectToIdentityProvider = (context) =>
                     {
                         System.Diagnostics.Debug.WriteLine("RedirectToIdentityProvider"); 
                         //string currentUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.Path;
                         //context.ProtocolMessage.RedirectUri = currentUrl;

                         return Task.FromResult(0);
                     }
                 }
             }); 
            app.UseStageMarker(PipelineStage.Authenticate);

        }

I have placed this in page Load event of my master (although it never seems to be getting hit - something else must be causing the authentication process to kick off when I navigate to a page requiring authentication.)

   if (!Request.IsAuthenticated)
                {
                    HttpContext.Current.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/Login.aspx" }, OpenIdConnectAuthenticationDefaults.AuthenticationType);
                }

My Azure settings are all correct because I am hitting SecurityTokenValidated and AuthorizationCodeReceived functions - I can see my email I am logged in with in the claims information, but I am not sure what to do next. As is I have a never ending loop of authentication requests. I am assuming this is because I have not translated the claim information I have received back into forms authentication ? I attempted to add a dummy auth ticket to the response in AuthorizationCodeReceived but that didn't appear to change anything - I am still getting the looping authentication requests.

FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1, "<UserName>", DateTime.Now, DateTime.Now.AddMinutes(60), true,"");
String encryptedTicket = FormsAuthentication.Encrypt(authTicket); 
context.Response.Cookies.Append(FormsAuthentication.FormsCookieName, encryptedTicket);

This is not a clear cut answer but it's too big for a comment.

I'm using "Organizational Accounts" (ie O365 email logins) and I had two big problems (both solved).

First Issue

intermittently, when logging in it would go into an endless redirect loop back and forth between two pages (This didn't happen all the time - only after half an hour testing and logging in and out).

If I left it long enough it would say "query string too long". There is a lot of long winded explanation around cookies and stuff but I had difficulties solving it. In the end it was solved simply by forcing https instead of http

I don't think that's your issue as it seems like it does it everytime. Perhaps have a read through this

New Asp.Net MVC5 project produces an infinite loop to login page

One answer says:

Do not call a protected web API (any web API which requires Authorization) from an authorization page such as ~/Account/Login (which, by itself, does NOT do this.). If you do you will enter into an infinite redirect loop on the server-side.

Second Issue

So the next thing was: our existing authorisation system was sitting in a classic login/pwd table in our database (with an unencrypted password field >:| ). So I needed to pick up the login email and match that to a role defined in this table. Which I did thanks to the guy who answered my question:

Capturing login event so I can cache other user information

This answer meant that I could:

  1. Go pick up the users role from the database once upon initial login
  2. Save this role inside the existing native C# security object
  3. Best of all: use the native authorisation annotations in my controller methods without any custom code in the method

I think thats what you are after but the question really is: how are you currently storing roles? In a database table? In Active Directory? In Azure active directory?

So in the hope that it help someone else - this is what I ended up with. In the web.config, the authentication mode is set to 'Forms'. I added the following Startup.cs

  public class Startup
    {
        public void Configuration(IAppBuilder app)
        {

        var appId = ConfigurationCache.GetConfigurationString(TOS_Configuration.KEY_SSO_APPID);
        var authority = ConfigurationCache.GetConfigurationString(TOS_Configuration.KEY_SSO_AUTHORITY);

        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
        app.UseCookieAuthentication(new CookieAuthenticationOptions());

        app.UseOpenIdConnectAuthentication(
         new OpenIdConnectAuthenticationOptions
         {
             ClientId = appId,
             Authority = authority,
             Notifications = new OpenIdConnectAuthenticationNotifications
             {
                 AuthorizationCodeReceived = context =>
                 {

                     string username = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Name).Value; 

                     FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1, username, DateTime.Now, DateTime.Now.AddMinutes(60), true, "");
                     String encryptedTicket = FormsAuthentication.Encrypt(authTicket);
                     context.Response.Cookies.Append(FormsAuthentication.FormsCookieName, encryptedTicket);

                     return Task.FromResult(0);
                 },
                 AuthenticationFailed = context =>
                 {
                     context.HandleResponse();
                     context.Response.Write(context.Exception.Message);
                     return Task.FromResult(0);
                 }
             }
         });

        // This makes any middleware defined above this line run before the Authorization rule is applied in web.config
        app.UseStageMarker(PipelineStage.Authenticate);

    }

}

I did not add any challenge to my site master pages and instead added the following to my login page to trigger the authentication challenge:

if (!Request.IsAuthenticated && AttemptSSO)
{
    ReturnURL = Request.QueryString["ReturnUrl"];
    HttpContext.Current.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/Login.aspx" }, OpenIdConnectAuthenticationDefaults.AuthenticationType);
 }
 else if (Request.IsAuthenticated && AttemptSSO)
 {
     if (!string.IsNullOrEmpty(ReturnURL))
     {
           var url = ReturnURL;
           ReturnURL = "";
           Response.Redirect(ResolveUrl(url));
     }
     else
     {
            Response.Redirect(ResolveUrl("~/Default.aspx"));
     }
 }

This means that if a user arrives at a authenticated page without a valid forms authentication token they get redirected to the login page. The login page takes care of deciding if SSO is set up and handling it appropriately. If anyone has any thoughts as to how to improve it - I'd love to hear them, but for the moment this does work.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM