How do i force audience validation for a client credential flow bearer token validation in identity server 4 - oauth-2.0

I've protected a resource api with client credential flow bearer in IDS4. I can specify audience and if it does not match the name in the apiresource defined in IDS4 the token fails to validate as expected.
Snippet from IDS4
public static IEnumerable<ApiResource> ApiResources =>
new List<ApiResource>
{
new ApiResource("api6")
{
Scopes = { "api6" },
UserClaims =
{
JwtClaimTypes.Audience
}
},
};
Snippet from Resource Api
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:5001";
//options.Audience = "api6"; // works as expected
options.Audience = "api7"; // Fails as expected
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true
};
});
However, if i remove the ValidateAudience = true from the api, it all becomes meaningless unless i can enforce IDS4 to make an audience mandatory.
I'm looking to set a property somewhere in the identity server that's something like AudienceRequired=true;
So when the token is validated, it checks for the presence of the aud claim and it's a relevant registered audience.
IE, how do i stop another API resource using the same creds? (Granted you could hack the audience to be the same as another API.)
It just seems pointless specifying audience validation in the resource if it's not enforced on the identity server and you can turn off in code? Am i missing something?....

Related

Initiate and store multiple OAuth2 external authentication challenges in a ASP.NET Core MVC application?

I can authenticate against two separate OAuth authentication schemes but it seems only one can be active at a time. I'd like to compare data from two separate SaaS applications and therefore I need two separate Bearer tokens. How can I initiate multiple OAuth challenges when the user loads the application and then store the Bearer Tokens for each? (e.g. in the Context.User cookie?)
My Startup.cs is as follows:
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/signin";
options.LogoutPath = "/signout";
})
.AddScheme1 (options =>
{
options.ClientId = Configuration["Scheme1:ClientId"];
options.ClientSecret = Configuration["Scheme1:ClientSecret"];
options.Scope.Add("scope1");
options.SaveTokens = true;
})
.AddScheme2(options =>
{
options.ClientId = Configuration["Scheme2:ClientId"];
options.ClientSecret = Configuration["Scheme2:ClientSecret"];
options.Scope.Add("scope1");
options.SaveTokens = true;
});...
}
The AuthenticationController calls the Challenge overloaded method from the Microsoft.AspNetCore.Mvc.Core assembly that takes a single provider/scheme (passing multiple schemes in the overloaded method seems to be ignored).
[HttpGet("~/signin")]
public async Task<IActionResult> SignIn() => View("SignIn", await HttpContext.GetExternalProvidersAsync());
[HttpPost("~/signin")]
public async Task<IActionResult> SignIn([FromForm] string provider)
{
...
return Challenge(new AuthenticationProperties { RedirectUri = "/" }, provider);
}
Presumably, you'd prompt the user to sign-into one external application, redirect back to the home page, and then prompt them to sign-into the second one, and then allow them to start using the application proper.
If this is possible - e.g. using a "multiple" Auth cookie - how then would I fetch the correct Bearer token and User values for the given scheme? Currently you just seem to fetch the token with a generic "access_token" name and unique user values:
string accessToken = await HttpContext.GetTokenAsync("access_token");
string userID = User.FindFirstValue(ClaimTypes.NameIdentifier);
There does seem to be some information here regarding using a SignInManager but I'm unable to determine if this is applicable to this problem.
I would aim to start with a standard architecture where the user authenticates with the one and only app, and gets only one set of tokens, issued by your own Authorization Server.
SaaS DATA - OPTION 1
Does the user need to get involved in these connections or can you use a back end to back end flow here?
Your C# code could connect to the SaaS provider with the client credentials grant, using the client ID and secret that you reference above. Provider tokens would then be cached in memory, then used by the back end code to return provider data to the UI. This is a simple option to code.
SaaS DATA - OPTION 2
If the user needs to get involved, because the data is owned by them, you might offer UI options like this. After each click the user is redirected again, to get a token for that provider.
View provider 1 data
View provider 2 data
Aim to emulate the embedded token pattern, where the provider tokens are available as a secondary credential. How you represent this could vary, eg you might prefer to store provider tokens in an encrypted cookie.
CODING AND SIMPLICITY
I would not mix up provider tokens with the primary OAuth mechanism of signing into the app and getting tokens via the .NET security framework, which typically implements OpenID Connect. Instead I would aim to code the SaaS connections on demand.
I think you will find it easier to code the SaaS connections with a library approach, such as Identity Model. This will also help you to deal with SaaS provider differences more easily.
I assume you use OIDC schemes.
First, you need to add two cookie schemes, one for each OIDC authentication scheme as their sign in scheme and set their callback path to different values to stop them competing:
services.AddAuthentication()
.AddCookie("Cookie1")
.AddCookie("Cookie2")
.AddOpenIdConnect("OidcScheme1", opt =>
{
opt.SignInScheme = "Cookie1";
opt.CallbackPath = "/signin-oidc-scheme1";
opt.SaveTokens = true;
})
.AddOpenIdConnect("OidcScheme2", opt =>
{
opt.SignInScheme = "Cookie2";
opt.CallbackPath = "/signin-oidc-scheme2";
opt.SaveTokens = true;
});
This will instruct the OIDC handler to authenticate the user from corresponding cookie.
Second, you need a controller action to challenge the user against each OIDC scheme:
[HttpGet]
[Route("login")]
[AllowAnonymous]
public IActionResult Login([FromQuery]string scheme,
[FromQuery]string? returnUrl)
{
return Challenge(new AuthenticationProperties
{
RedirectUri = returnUrl ?? "/"
}, scheme);
}
From your web app, you need to send the user to the Login endpoint twice with different scheme values:
GET /login?scheme=OidcScheme1
GET /login?scheme=OidcScheme2
Or chain them together using the returnUrl:
GET /login?scheme=OidcScheme1&returnUrl=%2Flogin%3Fscheme%3DOidcScheme2
Once signed in, there should be two cookies in the browser window, for example:
To authenticate the user and restore both identities from two cookies, you can use authorization policy:
[HttpGet]
[Authorize(AuthenticationSchemes = "OidcScheme1,OidcScheme2")]
public async Task<IActionResult> SomeOperation()
{
// Two identities, one from each cookie
var userIdentities = User.Identities;
...
}
To get access token from each authentication scheme, use the method you discovered (GetTokenAsync) and specify authentication scheme:
var token1 = await HttpContext.GetTokenAsync("OidcScheme1", "access_token");
var token2 = await HttpContext.GetTokenAsync("OidcScheme2", "access_token");
It is possible that the access token is not returned from the token endpoint depends on the response_type you used. If this is the case, try set the OpenIdConnectionOptions.ResponseType to OpenIdConnectResponseType.Code and make sure the scope is correct.
I encountered a similar problem where we had microservices that are/were shared across multiple products with each product having a separate IDP tenant (essentially a different token issuer). Perhaps a similar approach might work for your scenario...
The following link helped me with a solution - see here.
Basically I defined a smart authentication scheme
var builder = services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = "smart";
//...
});
Then in the smart policy scheme definition, I decode the JWT coming in to work out the issuer from the iss claim in the JWT, so that I can forward to the correct location for JWT bearer authentication.
builder.AddPolicyScheme("smart", "smart", options =>
{
options.ForwardDefaultSelector = context =>
{
var jwtEncodedString = context.Request.Headers["Authorization"].FirstOrDefault()?.Substring(7);
if (string.IsNullOrEmpty(jwtEncodedString))
return settings.Tenants.First().Key; // There's no authorization header, so just return any.
var token = new JwtSecurityToken(jwtEncodedString: jwtEncodedString);
var issuer = token.Claims.First(c => c.Type == "iss").Value?.TrimEnd('/');
var tenant = settings.Tenants
.Where(pair => pair.Value.Issuer.TrimEnd('/') == issuer)
.Select(pair => pair.Key).FirstOrDefault();
if (tenant == null)
throw new AuthorizationException($"Failed to locate authorization tenant with issuer '{issuer}'.");
return tenant;
};
});
Note: settings.Tenants is just an array of whitelisted tenants (from appsettings) that I configure as follows:
foreach (var tenant in settings.Tenants)
builder.AddJwtBearer(tenant.Key, options => Configure(options, tenant.Value, defaultJwtBearerEvents));

OWIN - ResponseType <code> vs <idtoken> on AuthorizationCodeReceived

I`m using from KeyCloak for SSO (Single Sign On). And I have MVC.NET project to use from it.
So I have this for sent request:
new OpenIdConnectAuthenticationOptions
{
ClientId = client_id,
Authority = issuer_url,
RedirectUri = redirect_uri,
PostLogoutRedirectUri = redirect_uri,
Scope = OpenIdConnectScope.OpenIdProfile,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
ClientSecret = secret,
SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed
},
});
=> And I get code OnAuthorizationCodeReceived():
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification n)
{
var client = new HttpClient();
var tokenResponse = await client.RequestAuthorizationCodeTokenAsync(new AuthorizationCodeTokenRequest
{
Code = n.Code,
Address = token_endpoint_url,
ClientId = client_id,
ClientSecret = secret,
RedirectUri = redirect_uri,
});
if (tokenResponse.IsError)
throw new Exception(tokenResponse.Error);
var response = await client.GetUserInfoAsync(new UserInfoRequest
{
Token = tokenResponse.AccessToken,
Address = userinfo_endpoint,
});
if (response.IsError)
throw new Exception(response.Error);
n.AuthenticationTicket.Identity.AddClaims(response.Claims);
}
When I use ResponseType only code, the n.AuthenticationTicketis NULL. and I must set response type as codeIdtoken to get n.AuthenticationTicket and I fill claims. Also I have to enable Hybrid Flow on KeyCloak.
I don`t want to change my code on changing flows (Standard Flow Enabled or Implicit Flow Enabled).
Is it possible that I get n.AuthenticationTicket on ResponseType.Code?
Note: I dont have any problem on get n.codealsotokenResponse.AccessToken` but my problem is only on n.AuthenticationTicket.
If it is not possible, what is the standard solution for filling claims?
if you just use ResponseType code, you will get the access token or authorization code, but they are not related to the AuthenticationTicket.
The AuthenticationTicket is created based on the ID-token, because that contains the details about the user. So you need to use code id_token to get it to work.
For modern application and to follow best practices, you should only aim to use the authorization code flow or the client credentials flow.
Some parts of the standards are no longer considered secure and if you follow OAuth 2.1 then the two flows above are the ones that are recommended.
OAuth 2.1 is a summary of OAuth 2.0 +/- all the best practices that has been established since 2.0 was released.
For the example, the Implicit grant (response_type=token) is omitted from this specification. You can still use it, but it is no longer considered best practice.

how to set up an asp.net mvc app for AD B2C "auth code flow" authentication

It states here
that going forward, we must prefer the auth code flow method since "With the plans for third party cookies to be removed from browsers, the implicit grant flow is no longer a suitable authentication method."
I have set up an asp.net mvc 4 web application according to the sample app here. It works when I set up my app registration in AD B2C directory for Access Tokens (used for implicit grant flows). If I switch to "ID Tokens (for implicit and hybrid flows)" as the documentation recommends, I get error that my application is not setup for it.
As I understand from the documentation, I would have to specifiy separate endpoins for /authorize and/token to fetch a token after authorization. I am not sure from looking at the sample though how exactly I can do this. Below is the ConfigureAuth method as you can see in the sample code on github link provided:
public void ConfigureAuth(IAppBuilder app)
{
// Required for Azure webapps, as by default they force TLS 1.2 and this project attempts 1.0
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
// ASP.NET web host compatible cookie manager
CookieManager = new SystemWebChunkingCookieManager()
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Generate the metadata address using the tenant and policy information
MetadataAddress = String.Format(WellKnownMetadata, Tenant, DefaultPolicy),
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = ClientId,
RedirectUri = RedirectUri,
PostLogoutRedirectUri = RedirectUri,
// Specify the callbacks for each type of notifications
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed,
},
// Specify the claim type that specifies the Name property.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
ValidateIssuer = false
},
// Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
Scope = $"openid profile offline_access {ReadTasksScope} {WriteTasksScope}",
// ASP.NET web host compatible cookie manager
CookieManager = new SystemWebCookieManager()
}
);
}

How to make secure authentication for .NET Core Web API?

I am developing an app with .NET Core Web API, Entity Framework and React. I've been reading a lot recently about possible authentication techniques for my API and I've discovered that plain JWT is not entirely secure, so at first I decided to use OpenID Connect with IdentityServer 4. I understand the idea behind OAuth 2.0 and OpenID Connect is to hide user credentials during login process and to involve external authentication provider in issuing an access token, but I don't want to rely on such services because not everyone have an account on Facebook etc. I consider this as an optional way to login. I want to give users an ability to sign in with just login and password. So what is the best (secure) way to accomplish this in modern web apps?
Having project 1 as Client App, project 2 as API Resources and project 3 as Authorization Service (IdentityServer4), I consider following scenarios:
A user is able to create an account on Authorization Service which is responsible for issuing a token required to get access to API Resources through Client App. Authorization Service is registered as authorization provider only for my Client App.
Get authorization token from Authorization Service using resource owner password grant - this one is not recommended by the specs but in my case since user must provide credentials to Authorization Service anyway and I will be hosting every project I can't see any problem.
Don't bother with OAuth and implement authorization mechanism using ASP.NET Core Identity + bearer token authentication.
Any ideas or recommendations highly apprecieated.
I use the JwtBearer package, wire it up in your Startup.cs Configure method like
.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = new TokenValidationParameters
{
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration["AppSettings:AuthConfig:SecretKey"])),
ValidateIssuer = true,
ValidIssuer = Configuration["AppSettings:AuthConfig:Issuer"],
ValidateAudience = true,
ValidAudience = Configuration["AppSettings:AuthConfig:Audience"],
ValidateLifetime = true,
}
})
and my login action on my User controller looks like
[HttpPost]
public string Post([FromBody]LoginRequest request)
{
var contact = dbContext.Contacts.Where(c => c.Active && c.Email == request.Email).Select(c => new { c.Id, c.PasswordHash }).SingleOrDefault();
if (contact == null || !Security.PasswordHash.ValidatePassword(request.Password, contact.PasswordHash))
{
return string.Empty;
}
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(appSettings.AuthConfig.SecretKey));
var now = DateTime.UtcNow;
var claims = new Claim[]
{
new Claim(JwtRegisteredClaimNames.Sub, contact.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.Now.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64)
};
var jwt = new JwtSecurityToken(
issuer: appSettings.AuthConfig.Issuer,
audience: appSettings.AuthConfig.Audience,
claims: claims,
notBefore: now,
expires: now.AddDays(30),
signingCredentials: new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256));
jwt.Payload.Add("roles", dbContext.ContactRoles.Where(cr => cr.ContactId == contact.Id).Select(ur => ur.Role.Name).ToArray());
return new JwtSecurityTokenHandler().WriteToken(jwt);
}
I use a JWT package for Angular on the client, there may be something similar for React.

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

Resources