Azure AD part 4 – minimal approach to authentication

Following up on my previous blog posts on Azure AD, I got the idea in my head to see what the minimal approach would be to implement Azure AD authentication in a DotNet based web application. Yes, there are componants you should use, like OWIN, ADAL, MSAL, etc, but how do they really work and just how big task is it to implement yourself if you really wanted – that was the question I wanted to answer.

The simple web application

I created an empty WebForms application with just a Default.aspx page. I deliberately choose WebForms since all modern examples of AAD auth are MVC/OWIN based. In the end I just added the Global.asax page, a page called Login.aspx and a C# class with just a few static methods and I was able to integrate with azure AD for authentication.

aaddotnet-website-1When the user press the “Azure AD Login” menu item, it goes to the Login page with action=login.

Azure AD authentication via OAuth and OpenID

There are numerous flow charts out on the internet explaining the interaction between the browser, your web application and the Identity Provider (IDP) and to be honest, it can look a bit complex. Azure AD supports OAuth and OpenID and in Microsofts documentation of OpenID and important detail about OpenID is explained

“OpenID Connect 1.0 in Azure Active Directory (Azure AD) enables you to use the OAuth 2.0 protocol for single sign-on. OAuth 2.0 is an authorization protocol, but OpenID Connect extends OAuth 2.0 for use as an authentication protocol. A primary feature of the OpenID Connect protocol is that it returns an id_token, which is used to authenticate the user.” (see refs)

So, by redirecting from my web application to Azure AD, asking for an OAuth authorization AND asking for a response_type=id_token, we can do a authorization/authentication in one call.

Login Sequence

By using Fiddler, we can get a clear understanding of what is happening here. Clicking on the menu item in the browser calls the Login.aspx page (frame 31) which responds with a redirection url to Azure AD. The browser redirects to Azure AD in frame 33 asking for an authentication. In the query parameter, we pass identification of the web application (redirect_uri, clientID and clientSecret) which are items we have registered in Azure AD.

aaddotnet-fiddler-1

The important part is responseType=id_token, which tells Azure AD to return a JWT Token on a successfull authentication. Azure AD responds with yet a redirection request to the browser and this time to the destination we supplied in the callbackURL parameter. You can see the browser making this call in frame 35.

aaddotnet-fiddler-2

Code that handles clicking on the login link and that returns the redirection url to Azure AD

            string nonce = System.Guid.NewGuid().ToString();
            string authUrl = "https://login.microsoftonline.com/common/oauth2/authorize";
            string clientID = ConfigurationManager.AppSettings["aad.clientid"];
            if (string.IsNullOrEmpty(clientID))
                throw new ArgumentNullException("clientID", "ClientID must be specified in Web.Config as aad.clientid under AppSettings");
            string clientSecret = ConfigurationManager.AppSettings["aad.clientsecret"];
            if (string.IsNullOrEmpty(clientSecret))
                throw new ArgumentNullException("clientSecret", "clientSecret must be specified in Web.Config as aad.clientsecret under AppSettings");
            string appIdUri = ConfigurationManager.AppSettings["aad.appiduri"];
            if (string.IsNullOrEmpty(appIdUri))
                throw new ArgumentNullException("appIdUri", "clientSecret must be specified in Web.Config as aad.appiduri under AppSettings");
            redirectUri = GetRedirectUrl(Request, redirectUri);
            if (string.IsNullOrEmpty(redirectUri))
                throw new ArgumentNullException("redirectUri", "redirectUri must be specified in Web.Config as aad.appiduri under AppSettings");
            // build url for AAD auth and redirect to ourself 
            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("{0}?", authUrl);
            sb.AppendFormat("redirect_uri={0}", Uri.EscapeDataString(redirectUri));
            sb.AppendFormat("&nonce={0}", nonce);
            sb.AppendFormat("&authorizationURL={0}", Uri.EscapeDataString(authUrl));
            sb.AppendFormat("&callbackURL={0}", Uri.EscapeDataString(redirectUri));
            sb.AppendFormat("&clientID={0}", clientID);
            sb.AppendFormat("&clientSecret={0}", Uri.EscapeDataString(clientSecret));
            sb.AppendFormat("&identifierField=openid_identifier");
            sb.AppendFormat("&oidcIssuer={0}", Uri.EscapeDataString("https://sts.windows.net/{tenantid}/"));
            sb.AppendFormat("&responseType=id_token");
            sb.AppendFormat("&revocationURL={0}", Uri.EscapeDataString("https://login.microsoftonline.com/common/oauth2/logout"));
            sb.AppendFormat("&scopeSeparator=%20");
            sb.AppendFormat("&tokenInfoURL=");
            sb.AppendFormat("&tokenURL={0}", Uri.EscapeDataString("https://login.microsoftonline.com/common/oauth2/token"));
            sb.AppendFormat("&userInfoURL={0}", Uri.EscapeDataString("https://login.microsoftonline.com/common/openid/userinfo"));
            sb.AppendFormat("&response_mode=form_post");
            sb.AppendFormat("&response_type=id_token");
            sb.AppendFormat("&scope=openid");
            sb.AppendFormat("&client_id={0}", clientID);
            sb.AppendFormat("&state={0}", nonce);
            // redirect to auth via AAD (and then redirect back to ourself)
            Response.Redirect(sb.ToString(), true);

We’re authenticated, now what?

The callback to Login.aspx then has to decode the JWT token and covert them to claims and setting up a claims identity. But that is not all. In order keep this on a session level we need to create a cookie on the callback processing and for each subsequent request, we need to read this cookie and recreate/reapply the claims identity. Simple, right?

Step 1 – Handling the Callback

First, we shouldn’t do anything if we are already authenticated or ir the callback is missing the id_token in it’s post body. Second, decode the JWT token into JSON and create a ClaimsPrincipal. Third, create the FormsAuthenticationTicket where we store the JWT token in the UserData section so that we always have it available. Fourth, create the cookie using the well known name ASPXAUTH (FormsCookieName) and redirect ourselves to wherever we should go after authentication is complete

            // we shouln't already be Auth'd and we need the "id_token" part in the body
            if (Request.IsAuthenticated) return;
            if (!Request.Form.AllKeys.Contains("id_token")) return;
            
            // decode shit
            string value = Request.Form.Get("id_token");
            JObject id_token = JwtDecode(value);
            // UserPrincipalNme, ie a fancy word for the original e-mail address you have in ActiveDirectory
            string upn = id_token.GetValue("upn").ToString();
            DateTime expireTime = GetExpireTime(id_token);
            SetUserPrincipal(id_token);

            // create the cookie and store the JWT token in the UserData attrribute so we can pick it up 
            FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, upn, DateTime.UtcNow, expireTime, false, id_token.ToString(), FormsAuthentication.FormsCookiePath);
            string encryptedCookie = FormsAuthentication.Encrypt(ticket);
            HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedCookie);
            cookie.Expires = expireTime;
            Response.Cookies.Add(cookie);

            // redirect to ourself
            redirectUri = GetRedirectUrl(Request, redirectUri);
            Response.Redirect(redirectUri, true);

Creating the ClaimsPrincipal is straight forward. Setting the HttpContext.Current.User makes the boolean flag Request.IsAuthentication flip from false to true, which is important since that is how we check in code if we are dealing with an anonymous or authenticated user. Setting Thread.CurrentPrincipal makes the claims available since the static method ClaimsPrincipal.Current actually uses this value.

private static void SetUserPrincipal( JObject id_token )
{
    string upn = id_token.GetValue("upn").ToString();
    List<Claim> claims = new List<Claim>
                {
                      new Claim(ClaimTypes.Email, upn )
                    , new Claim(ClaimTypes.Upn, upn )
                    , new Claim( "http://schemas.microsoft.com/identity/claims/objectidentifier", id_token.GetValue("oid").ToString() )
                    , new Claim(ClaimTypes.Surname, id_token.GetValue("family_name").ToString() )
                    , new Claim(ClaimTypes.GivenName, id_token.GetValue("given_name").ToString() )
                    , new Claim(ClaimTypes.Name, id_token.GetValue("unique_name").ToString() )
                    , new Claim("name", id_token.GetValue("name").ToString() )
                    , new Claim("iss", id_token.GetValue("iss").ToString() )
                    , new Claim("nbf", id_token.GetValue("nbf").ToString() )
                    , new Claim("exp", id_token.GetValue("exp").ToString() )
                    , new Claim("aud", id_token.GetValue("aud").ToString() )
                    , new Claim(ClaimTypes.NameIdentifier, id_token.GetValue("sub").ToString() )
                    , new Claim("ipaddr", id_token.GetValue("ipaddr").ToString() )
                    , new Claim("http://schemas.microsoft.com/identity/claims/tenantid", id_token.GetValue("tid").ToString() )
                    , new Claim("ver", id_token.GetValue("ver").ToString() )
                };
    ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, "Cookies");
    ClaimsPrincipal principal = new ClaimsPrincipal(claimsIdentity);
    HttpContext.Current.User = principal;
    Thread.CurrentPrincipal = principal; // updates ClaimsPrincipal.Current
}

 

aaddotnet-cookie-1

Step 2 – Reapplying the cookie on each subsequent Request

If we do nothing on the following request, the flag Request.IsAuthenticated will be false, so we need to recreate the ClaimsPrincipal and reapply on each subsequent request. This is the only way it can work if your web applicaiton is hosted on multiple servers.

In Global.asax there is a method that is there just for this and it’s called AuthenticateRequest

public class Global : System.Web.HttpApplication
{
    protected void Application_AuthenticateRequest(object sender, EventArgs e)
    {
        Tiny.AzureAD.OAuthHandler.GlobalApplication_AuthenticateRequest(Request, Response);
    }
}

 

public static void GlobalApplication_AuthenticateRequest(HttpRequest Request, HttpResponse Response)
{
    // if not already auth'd and we have the aspnet auth cookie, hook up the principal if cookie hasn't expired
    if (!Request.IsAuthenticated && Request.Cookies.AllKeys.Contains(FormsAuthentication.FormsCookieName))
    {
        HttpCookie cookie = Request.Cookies.Get(FormsAuthentication.FormsCookieName);
        if (cookie != null)
        {
            FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);
            var id_token = JObject.Parse(ticket.UserData);
            DateTime expireTime = GetExpireTime(id_token);
            if (DateTime.UtcNow < expireTime)
            {
                SetUserPrincipal(id_token);
            }
        }
    }
}

The end result is a web page that behaves just like you expect and that can show the claim values we created.

aaddotnet-website-3

Logoff

Logging off is basically the same process but simpler. It’s a redirect to Azure AD again asking it to logoff and at the same time making sure the cookie we have is set to expired.

Summary

This excersie is about showing you that you can roll your own implementation of integrating Azure AD authentication in your web application. In doing so I hope I gave you an understanding of how easy it is and how it really works behind the covers. However, with Identity you should NEVER roll your own solutions and should ALWAYS use componants that are tested and maintained by bigger players.

References

OpenID 1.0 Connect
https://msdn.microsoft.com/en-us/library/azure/dn645541.aspx

OpenID 1.0 Specifications
http://openid.net/developers/specs/

Authorize webapps with OAuth and Azure AD
https://azure.microsoft.com/en-us/documentation/articles/active-directory-protocols-oauth-code/

Azure AD Developer’s Guide
https://azure.microsoft.com/en-us/documentation/articles/active-directory-developers-guide/

Sources

Available on github
https://github.com/cljung/azwebaadtiny