5

Background

We developed an application in 2016 that authenticated using WS-Federation, to grab claims from the on-premises AD. The direction of the IT strategy has changed, and is moving toward Azure AD (currently hosting a hybrid environment).

We're in the process of migrating the authentication from WS-Fed, to AAD, using OpenIDConnect. Getting the user signed in and authenticated with the new method was surprisingly straightforward - do the config properly, and issue the authenticate challenge, and Robert is your mother's brother.

The Problem

Please correct me if I'm getting my terminology wrong here; we need to grab some attributes from Active Directory that aren't accessible (as far as I can tell) via the default JWT. So, we need to pass the JWT to the Graph API, via HTTP, to get the attributes we want from active directory.

I know that a properly formatted and authenticated request can pull the necessary data, because I've managed to see it using the graph explorer (the AAD one, not the Microsoft Graph one).

The Question

If my understanding above is correct, how do I pull the JWT from the HttpContext in ASP.Net? If I've grasped all this lower level HTTP stuff correctly, I need to include the JWT in the request header for the Graph API request, and I should get the JSON document I need as a response.

(Edit, for the benefit of future readers: You actually need to acquire a new token for the specific service you're trying to access, in this case Azure AD. You can do this using the on-behalf-of flow, or using the as-an-application flow).

Request.Headers["IdToken"] is returning null, so I'm wondering what's going wrong here.

The Code Here's our Authentication config that runs on server startup:

    public void Configuration(IAppBuilder app)
    {
        AntiForgeryConfig.SuppressIdentityHeuristicChecks = true;
        //ConfigureAuth(app); //Old WsFed Auth Code

        //start the quartz task scheduler
        //RCHTaskScheduler.Start();

        //Azure AD Configuration
        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
        app.UseCookieAuthentication(new CookieAuthenticationOptions());


        app.UseOpenIdConnectAuthentication(
            new OpenIdConnectAuthenticationOptions
            {
                //sets client ID, authority, and RedirectUri as obtained from web config
                ClientId = clientId,
                ClientSecret = appKey,
                Authority = authority,
                RedirectUri = redirectUrl,

                //page that users are redirected to on logout
                PostLogoutRedirectUri = redirectUrl,

                //scope - the claims that the app will make
                Scope = OpenIdConnectScope.OpenIdProfile,
                ResponseType = OpenIdConnectResponseType.IdToken,

                //setup multi-tennant support here, or set ValidateIssuer = true to config for single tennancy
                TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidateIssuer = true,
                    SaveSigninToken = true
                },
                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    AuthenticationFailed = OnAuthenticationFailed
                }

            }
            );
    }

Here's my partially complete code for crafting the GraphAPI request:

        public static async Task<int> getEmployeeNumber(HttpContextBase context)
        {

            string token;
            int employeeId = -1;
            string path = "https://graph.windows.net/<domain>/users/<AAD_USER_ID>?api-version=1.6";


            HttpWebRequest request = null;
            request = (HttpWebRequest)HttpWebRequest.Create(path);
            request.Method = "GET";
            request.Headers.Add(context.GetOwinContext().Request.Headers["IdToken"]);
            WebResponse response = await request.GetResponseAsync();
            throw new NotImplementedException();

        }
Scuba Steve
  • 1,541
  • 1
  • 19
  • 47
  • Following this: https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-authentication-scenarios The token should be present in the 'Authorization' header, but looking through OwinContext.Request.Headers, there's no such token present. Do I need to do an authenticate challenge to get the token? – Scuba Steve Jul 25 '18 at 00:30
  • The Graph API will not accept your token. You need to acquire a token for the Graph API using the authorization code you can get after login. Here is a sample project: https://github.com/Azure-Samples/active-directory-dotnet-webapp-multitenant-openidconnect. – juunas Jul 25 '18 at 05:52
  • Hi Juunas, I've looked at that sample, but it's really unclear as to how to acquire the token. The Microsoft samples are obfuscated with overly complex use-cases. I really just need to understand how the token is being acquired. – Scuba Steve Jul 25 '18 at 17:49
  • I did reach that conclusion as well though, I'm going to edit with my attempt to grab the token. – Scuba Steve Jul 25 '18 at 17:51
  • Alright, there is a callback that you can add to the OpenId Connect middleware, which is called when an authorization code is received. There you can exchange the code for a token (or more than one if you want). I'll try to find a good sample for OWIN. – juunas Jul 25 '18 at 18:03
  • 1
    Here is one: https://github.com/Azure-Samples/active-directory-dotnet-webapp-webapi-openidconnect/blob/master/TodoListWebApp/App_Start/Startup.Auth.cs#L92 – juunas Jul 25 '18 at 18:14
  • @juunas - Okay, so that sample is using a custom token cache. This is one of the things that hung me up on MS samples - they were using all kinds of custom middleware, and then some of the dependencies for that middleware was autogenerated, by which method I have no idea. Do I need to use the custom token cache? Will an out of the box handler do the job? – Scuba Steve Jul 25 '18 at 18:54
  • Ohh okay. I see what's going on here. The custom token cache is storing the token for that user, by user ID. Is this the best way to do this? Our application can have multiple instances running at any given time (no guarantee that a user accesses the same instance at any given time AFAIK). – Scuba Steve Jul 25 '18 at 18:58
  • 1
    A custom token cache is pretty mandatory in Web scenarios. Storing the data per user in a central store is a good way. The data that is stored can actually contain more than one token, the TokenCache class kind of abstracts that away so you just need to store the bytes it gives you and get them from the data store when requested. – juunas Jul 25 '18 at 19:00
  • Amazing. Thank you! I'm going to take this away and work on it. I might have some followup questions. – Scuba Steve Jul 25 '18 at 19:03
  • Well the session cache example in those code samples doesn't seem to work for me. I'm not sure what I'm doing wrong, but I'm getting a: " System.Web.HttpContext.Current.get returned null." when the constructor calls load. I've specified the 'OnAuthorizationCodeReceived' method in startup, and I'm calling the session cache the same way it's done in that sample code. – Scuba Steve Aug 01 '18 at 18:23
  • Okay that is pretty weird. You should probably create a new question for that. – juunas Aug 01 '18 at 18:41
  • I implemented the 'IRequiresSessionState' interface, but it doesn't seem to have done the trick. – Scuba Steve Aug 01 '18 at 18:45
  • Okay so here's the problem: this.Deserialize((byte[])HttpContext.Current.Session[CacheId]); Calling HttpContext.Current.Session is fine. But calling for the CacheId (exactly as defined in the microsoft sample) flat out does not work. – Scuba Steve Aug 01 '18 at 19:58
  • Ahh so that token cache uses the session to store the data. Interesting. You could switch it store the bytes in a database instead, or a distributed cache. – juunas Aug 01 '18 at 19:59
  • Well I found the culprit. The instructions here fixed the issue. Apparently await.ConfigureAwait is always false in some version of the library: https://github.com/Azure-Samples/active-directory-dotnet-webapp-webapi-openidconnect/issues/25 – Scuba Steve Aug 01 '18 at 20:05
  • So now my 'AcquireTokenSilentAsync' call is hanging on an infinite load on the microsoft page. Progress! – Scuba Steve Aug 01 '18 at 20:06
  • So here's the line of code it hangs on: " AuthenticationResult result = await auth.AcquireTokenSilentAsync("https://graph.windows.net", ConfigurationManager.AppSettings["ClientId"], new UserIdentifier(userObjectId,UserIdentifierType.UniqueId));" – Scuba Steve Aug 01 '18 at 20:19
  • It's an async await deadlock probably. Using ConfigureAwait(false) can help. – juunas Aug 01 '18 at 20:20
  • Oh that helped! I'm getting a 'failed to acquire token silently call acquiretoken'. So I'm going to try{ silent} catch(Exception) and then acquire – Scuba Steve Aug 01 '18 at 20:35
  • So yeah, I can't see any clear way to get the 'UserAssertion' to be able to use 'AquireTokenAsync'. The microsoft code sample doesn't do this, so I suspect my token cache isn't actually storing a token. – Scuba Steve Aug 01 '18 at 21:42
  • Okay I think I found part of the problem. My 'OnAuthorizationCodeReceived' method isn't being called. – Scuba Steve Aug 01 '18 at 23:00
  • 1
    Yep that was part of it. Needed to set "ResponseType = OpenIdConnectResponseType.CodeIdToken" in the OpenIdConnect Options. – Scuba Steve Aug 01 '18 at 23:05
  • 1
    Right, you gotta ask for the code :) Then if it now populates the cache correctly, AcquireTokenSilent should work. Just remember that if silent acquisition fails from now, it means the tokens have expired etc, and you will need to trigger authentication. – juunas Aug 02 '18 at 04:42
  • Yeah I'm having trouble doing the authenticate challenge in the utility class, because HttpContext resolves to null after a call to await (by design for reasons beyond me, probably because they can't know which context it's referring to after some kinds of calls). AcquireTokenSilentAsyc requires an await call, so there's that. I did break the authentication with the CodeIdToken change. It's calling OnAuthenticationCodeReceived, then calling OnAuthenticationFailed in an endless loop, so gotta figure out what's going on there. – Scuba Steve Aug 02 '18 at 19:33
  • Well the auth loop thing was a null ClientSecret due to a typo. AcquireTokenSilent is still failing though. – Scuba Steve Aug 02 '18 at 19:53
  • Oh it succeeded on a rebuild actually. I think I'm in business here. – Scuba Steve Aug 02 '18 at 20:01

1 Answers1

5

Okay it took me a few days to work out (and some pointers from Juunas), but this is definitely doable with some slight modifications to the code here. The aforementioned being the OpenId guide from Microsoft.

I would definitely recommend reading up on your specific authentication scenario, and having a look at the relevant samples.

The above will get you in the door, but to get a JWT from the Graph API, (not to be confused with Microsoft Graph), you need to get an authentication code when you authenticate, and store it in a token cache.

You can get a usable token cache out of this sample from Microsoft (MIT License). Now, personally, I find those samples to be overly obfuscated with complicated use-cases, when really they should be outlining the basics, but that's just me. Nevertheless, these are enough to get you close.

Now for some code. Allow me to draw your attention to the 'ResponseType= CodeIdToken'.

public void ConfigureAuth(IAppBuilder app)
        {
            //Azure AD Configuration
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
            app.UseCookieAuthentication(new CookieAuthenticationOptions());


            app.UseOpenIdConnectAuthentication(
                new OpenIdConnectAuthenticationOptions
                {
                    //sets client ID, authority, and RedirectUri as obtained from web config
                    ClientId = clientId,
                    ClientSecret = appKey,
                    Authority = authority,
                    RedirectUri = redirectUrl,


                    //page that users are redirected to on logout
                    PostLogoutRedirectUri = redirectUrl,

                    //scope - the claims that the app will make
                    Scope = OpenIdConnectScope.OpenIdProfile,
                    ResponseType = OpenIdConnectResponseType.CodeIdToken,

                    //setup multi-tennant support here, or set ValidateIssuer = true to config for single tennancy
                    TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidateIssuer = true,
                        //SaveSigninToken = true
                    },
                    Notifications = new OpenIdConnectAuthenticationNotifications
                    {
                        AuthenticationFailed = OnAuthenticationFailed,
                        AuthorizationCodeReceived = OnAuthorizationCodeReceived,
                    }

                }
                );
        }

When the above parameter is supplied, the following code will run when you authenticate:

        private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification context)
    {
        var code = context.Code;
        ClientCredential cred = new ClientCredential(clientId, appKey);
        string userObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
        AuthenticationContext authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId));

        // If you create the redirectUri this way, it will contain a trailing slash.  
        // Make sure you've registered the same exact Uri in the Azure Portal (including the slash).
        Uri uri = new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path));
        AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(code, uri, cred, "https://graph.windows.net");
    }

This will supply your token cache with a code that you can pass to the Graph API. From here, we can attempt to authenticate with the Graph API.

 string path = "https://graph.windows.net/me?api-version=1.6";
            string tenant = System.Configuration.ConfigurationManager.AppSettings["Tenant"];
            string userObjectId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
            string resource = "https://graph.windows.net";
            AuthenticationResult result = null;  
            string authority = String.Format(System.Globalization.CultureInfo.InvariantCulture, System.Configuration.ConfigurationManager.AppSettings["Authority"], tenant);
            ClientCredential cc = new ClientCredential(ConfigurationManager.AppSettings["ClientId"], ConfigurationManager.AppSettings["ClientSecret"]);
            AuthenticationContext auth = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId));
            try
            {
                result = await auth.AcquireTokenSilentAsync(resource,
                                                            ConfigurationManager.AppSettings["ClientId"],
                                                            new UserIdentifier(userObjectId, UserIdentifierType.UniqueId)).ConfigureAwait(false);
            }
            catch (AdalSilentTokenAcquisitionException e)
            {
                result = await auth.AcquireTokenAsync(resource, cc, new UserAssertion(userObjectId));
                
            }

Once you have the authentication token, you can pass it to the Graph API via Http Request (this is the easy part).

    HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(path);
    request.Method = "GET";
    request.Headers.Set(HttpRequestHeader.Authorization, "Bearer " + result.AccessToken);
    WebResponse response = request.GetResponse();

    System.IO.Stream dataStream = response.GetResponseStream();

From here, you have a datastream that you can pass into a stream reader, get the JSON out of, and do whatever you want with. In my case, I'm simply looking for user data that's in the directory, but is not contained in the default claims that come out of Azure AD Authentication. So in my case, the URL I'm calling is

"https://graph.windows.net/me?api-version=1.6"

If you need to do a deeper dive on your directory, I'd recommend playing with the Graph Explorer. That will help you structure your API calls. Now again, I find the Microsoft documentation a little obtuse (go look at the Twilio API if you want to see something slick). But it's actually not that bad once you figure it out.

EDIT: This question has since gotten a 'notable question' badge from Stack Overflow. Please note, this addresses the ADAL implementation for Azure AD Auth in this scenario. You should be using MSAL, as ADAL is now deprecated! It's mostly the same but there are some key differences in the implementation.

Scuba Steve
  • 1,541
  • 1
  • 19
  • 47
  • I see this question/answer getting upvoted. I just want to point out that we've done something funny in our 'on-behalf-of' flow which caused failures under load (worked fine in test). I think the access token should be serialized and stored to database, rather than storing the authorization code. Unfortunately I don't have time to experiment on this item. – Scuba Steve Feb 15 '19 at 18:22