Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get the JWT (using OpenIdConnect) from HttpContext, and pass to Azure AD Graph API

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();

        }
like image 594
Scuba Steve Avatar asked Jul 25 '18 00:07

Scuba Steve


1 Answers

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.

like image 164
Scuba Steve Avatar answered Oct 05 '22 00:10

Scuba Steve