This is the second part of the tutorial which will cover Using Azure AD B2C tenant with ASP.NET Web API 2 and various front end clients.
- Azure Active Directory B2C Overview and Policies Management – (Part 1)
- Secure ASP.NET Web API 2 using Azure AD B2C – (This Post)
- Integrate Azure Active Directory B2C with ASP.NET MVC Web App (Part 3)
- Secure Desktop Application using Microsoft Authentication Library (MSAL) and Azure Active Directory B2C (Part 4)
The source code for this tutorial is available on GitHub.
The Web API has been published on Azure App Services, so feel free to try it out using the Base URL (https://aadb2cresourceapi.azurewebsites.net/)
Secure ASP.NET Web API 2 using Azure AD B2C
In the previous post, we have configured all the needed policies in our Azure AD B2C tenant and the Reply URLs. In this Post, we will reconfigure the Web API we have created in the previous post so it relays on the Azure AD B2C IdP we created to secure it and our Web API will only accept and trust tokens issued by our Azure AD BC IdP.
So let’s start implementing those changes on the Web API.
Step 1: Configure Web API to use Azure AD B2C tenant IDs and Policies
Now we need to modify the web.config for our Web API by adding the below keys, so open Web.config and add the below AppSettings keys:
1 2 3 4 5 6 |
<add key="ida:AadInstance" value="https://login.microsoftonline.com/{0}/v2.0/.well-known/openid-configuration?p={1}" /> <add key="ida:Tenant" value="BitofTechDemo.onmicrosoft.com" /> <add key="ida:ClientId" value="bc348057-3c44-42fc-b4df-7ef14b926b78" /> <add key="ida:SignUpPolicyId" value="B2C_1_signup" /> <add key="ida:SignInPolicyId" value="B2C_1_Signin" /> <add key="ida:UserProfilePolicyId" value="B2C_1_Editprofile" /> |
The values for those keys as the following:
- “ida:AadInstance” value contains the metadata discovery endpoint for each policy, this endpoint will be used internally by the middle-wares which we will add in the next steps to validate the JWT tokens.
- “ida:Tenant” value contains the URL for our Azure AD B2C tenant we have already defined in the previous post.
- “ida:ClientId” value contains the App client Id we have already registered with our Azure AD Bb2C tenant.
- “ida:SignUpPolicyId” value contains the name of the signup policy we already created.
- “ida:SignInPolicyId” value contains the name of the Signin policy we already created.
- “ida:UserProfilePolicyId” value contains the name of the Create profile policy we already created.
Step 2: Add the extra NyGet packages to the Web API project
Open NuGet Package Manager Console and install/update the below packages:
1 2 3 4 |
Install-Package Microsoft.Owin.Security.OAuth -Version 3.0.1 Install-Package Microsoft.Owin.Security.Jwt -Version 3.0.1 Update-Package System.IdentityModel.Tokens.Jwt -version 4.0.2.206221351 Install-Package Microsoft.IdentityModel.Protocol.Extensions |
The packages we have installed is responsible for configuring our API to use OAuth bearer token for protection as well we have added the packages which are responsible for validating, parsing and decoding JWT tokens.
I had to downgrade the package “System.IdentityModel.Tokens.Jwt” to the version “4.0.2.206221351” as the newer version of it showed breaking compatibility with other packages, I will keep an eye on it and update the post accordingly if there are new updates.
lastly, we need to add manually using “Add reference” dialog a reference for the assembly “System.IdentityModel” v4, I’m not sure if there is something incorrect with those NuGet packages which forget to include this dependency by default. I will keep an eye on this too and update the post if there is a change.
Step 3: Configure the Startup class
Now we need to configure our API to rely on the Azure AD B2C IdP we already created, this is the most important step in configuring the Web API to trust tokens issued by our Azure AD b2C IdP, our Web API will be able to consume only JWT tokens issued by the trusted IdP and issued for a specific client only (The app we registered in the previous post “Bitoftech Demo App”).
To do so, open the “Startup” class for the Web API and replace all the content with below code, most of the changes has been introduced in method “ConfigureOAuth” which I will describe what happen in the next paragraph.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
public class Startup { // These values are pulled from web.config public static string aadInstance = ConfigurationManager.AppSettings["ida:AadInstance"]; public static string tenant = ConfigurationManager.AppSettings["ida:Tenant"]; public static string clientId = ConfigurationManager.AppSettings["ida:ClientId"]; public static string signUpPolicy = ConfigurationManager.AppSettings["ida:SignUpPolicyId"]; public static string signInPolicy = ConfigurationManager.AppSettings["ida:SignInPolicyId"]; public static string editProfilePolicy = ConfigurationManager.AppSettings["ida:UserProfilePolicyId"]; public void Configuration(IAppBuilder app) { HttpConfiguration config = new HttpConfiguration(); // Web API routes config.MapHttpAttributeRoutes(); ConfigureOAuth(app); app.UseWebApi(config); } public void ConfigureOAuth(IAppBuilder app) { app.UseOAuthBearerAuthentication(CreateBearerOptionsFromPolicy(signUpPolicy)); app.UseOAuthBearerAuthentication(CreateBearerOptionsFromPolicy(signInPolicy)); app.UseOAuthBearerAuthentication(CreateBearerOptionsFromPolicy(editProfilePolicy)); } private OAuthBearerAuthenticationOptions CreateBearerOptionsFromPolicy(string policy) { var metadataEndpoint = string.Format(aadInstance, tenant, policy); TokenValidationParameters tvps = new TokenValidationParameters { // This is where you specify that your API only accepts tokens from its own clients ValidAudience = clientId, AuthenticationType = policy, NameClaimType = "http://schemas.microsoft.com/identity/claims/objectidentifier" }; return new OAuthBearerAuthenticationOptions { // This SecurityTokenProvider fetches the Azure AD B2C metadata & signing keys from the OpenIDConnect metadata endpoint AccessTokenFormat = new JwtFormat(tvps, new OpenIdConnectCachingSecurityTokenProvider(metadataEndpoint)) }; } } |
What we have implemented is the following:
We have configured our API to consume and trust JWT tokens issued by our IdP (“BitofTechDemo.onmicrosoft.com”) for a specific client (“bc348057-3c44-42fc-b4df-7ef14b926b78”) This client represents the app we already registered, as well it will only accept JWT tokens for the three policies we already defined and named “B2C_1_signup”, “B2C_1_Signin”, and “B2C_1_Editprofile”. Any other JWT tokens don’t meet these criteria will be rejected and 401 HTTP response is returned to the requestor.
Back to the Metadata Discovery Endpoint we discussed earlier, this endpoint is auto generated by our IdP and it is very important for our API to obtain the “Signing Tokens Keys” from it; in order to validate the JWT signature and trust this JWT token and send a response to the requestor. This end point is built at run time, for example, the metadata endpoint for the “B2C_1_signin” will be as the follow: https://login.microsoftonline.com/BitofTechDemo.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_signin feel free to click the link and observe the data returned.
A nice trick here that I’ve configured the “NameClaimType” of the “TokenValidationParameters” to use the claim named “objectidentifer” (“oid”) This will facilitate reading the unique user id for the authenticated user inside the controllers, all we need to call now inside the controller is: “User.Identity.Name” instead of querying the claims collection each time.
The last thing we need to add to the “Startup” class is a new class named “OpenIdConnectCachingSecurityTokenProvider”, this class will be responsible for communicating with the “Metadata Discovery Endpoint” and issue HTTP requests to get the signing keys that our API will use to validate signatures from our IdP, those keys exists in the jwks_uri which can read from the discovery endpoint. To add this class create a new class named “OpenIdConnectCachingSecurityTokenProvider” and paste the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
// This class is necessary because the OAuthBearer Middleware does not leverage // the OpenID Connect metadata endpoint exposed by the STS by default. public class OpenIdConnectCachingSecurityTokenProvider : IIssuerSecurityTokenProvider { public ConfigurationManager<OpenIdConnectConfiguration> _configManager; private string _issuer; private IEnumerable<SecurityToken> _tokens; private readonly string _metadataEndpoint; private readonly ReaderWriterLockSlim _synclock = new ReaderWriterLockSlim(); public OpenIdConnectCachingSecurityTokenProvider(string metadataEndpoint) { _metadataEndpoint = metadataEndpoint; _configManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataEndpoint); RetrieveMetadata(); } /// <summary> /// Gets the issuer the credentials are for. /// </summary> /// <value> /// The issuer the credentials are for. /// </value> public string Issuer { get { RetrieveMetadata(); _synclock.EnterReadLock(); try { return _issuer; } finally { _synclock.ExitReadLock(); } } } /// <summary> /// Gets all known security tokens. /// </summary> /// <value> /// All known security tokens. /// </value> public IEnumerable<SecurityToken> SecurityTokens { get { RetrieveMetadata(); _synclock.EnterReadLock(); try { return _tokens; } finally { _synclock.ExitReadLock(); } } } private void RetrieveMetadata() { _synclock.EnterWriteLock(); try { OpenIdConnectConfiguration config = Task.Run(_configManager.GetConfigurationAsync).Result; _issuer = config.Issuer; _tokens = config.SigningTokens; } finally { _synclock.ExitWriteLock(); } } } |
Step 4: Add protected controller for testing
Now we will add an (optional) class which reads all the decoded claims in the JWT token and return them in the response, note that we have protected our controller by the [Authorize] attribute so only valid JWT tokens will be accepted to serve the request and return response, to do so add new controller named “ProtectedController” under folder Controllers and paste the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[Authorize] [RoutePrefix("api/protected")] public class ProtectedController : ApiController { [Route("")] public IEnumerable<object> Get() { var identity = User.Identity as ClaimsIdentity; var userName = identity.Name; return identity.Claims.Select(c => new { Type = c.Type, Value = c.Value }); } } |
We will test the controller in the next steps.
Step 5: Modify the “OrdersController” we created in the previous post
Now we need to do a very little change on the “OrdersController” by adding the [Authorize] attribute on the controller to protect it, as well to read the unique authenticated user id from the claim and store it in Azure Table Storage instead of the fixed value we defined earlier in the previous post. The change is very simple and should be as the below snippet, most of the code is omitted so just add/replace the missing parts as the below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
[Authorize] [RoutePrefix("api/Orders")] public class OrdersController : ApiController { [Route("")] public IHttpActionResult Get() { //This will be read from the access token claims. var userId = User.Identity.Name; // code ommited for brevity } [Route("")] public IHttpActionResult Post (OrderModel order) { //This will be read from the access token claims. var userId = User.Identity.Name; // code ommited for brevity } } |
Step 6: Testing out the protected controllers
Last thing we need to do here is to obtain a JWT token from out IdP by executing one of the policies we have defined, then sending the JWT token in the Authorization header for one of out API endpoints, if all is good we should access the API and return the protected resources.
To do this I have choose the policy Sign in from Azure AD B2C tenant and clicked on “Run now” button, a new window will open and you provide a valid credentials, then a redirect will take place to the defined Reply URL and you can obtain the token manually by copying it, then you need to send the token to the end point “api/protected” or “api/orders” in the authorization header using the bearer scheme. The request will look as the below image:
GET Request to the “Api/orders” endpoint to list all the orders assigned to the user with email “tayseer_joudeh@hotmail.com”
Note: This manual testing demonstrated here is just for testing purposes, in the next post you will see how we will add an MVC application which will be responsible for executing the policies, obtain JWT tokens, create the session for the authenticated users, and access the protected API resources.
TAISEER, Thanks for sharing this series, I’m wondering even the documentation for Azure B2C said SPA applications are out of the scope until know, do you have any workaround to tackle this limitation ?
The Azure team is currently developing the B2C for SPAs, but the (unstable) code for it is out on github already. There are a few caveats you might find along the way if you decide to use it, but for the most part it works just fine.
https://github.com/AzureAD/azure-activedirectory-library-for-js/tree/experimental
Hi Luis and Ben, thanks for your comment. For the meantime, there is a support for SPA when you use Easy Auth which is a feature of Azure App Services. Check out this post But I assume there will be a JS SDK in the near feature such as ADAL for JS to support this cases, users might not want to host their web app on Azure and they need more freedom.
As @Ben stated, ADAL should be used with B2B Azure AD, I’m not sure if it will work with Azure AD B2C.
I could have maybe worded my previous comment better, but the link I posted above is specifically for Azure AD B2C.
Thank you for these amazing tutorials !!!
It is rare to find such “complex” tutorials, which, when followed closely, work like a charm without any problems.
Thanks again, you’ve been a great help.
You are most welcome Bryan, happy to hear that posts are useful and easy to follow 🙂
When I add bearer usage for all three policies like this:
app.UseOAuthBearerAuthentication(CreateBearerOptionsFromPolicy(signUpPolicy));
app.UseOAuthBearerAuthentication(CreateBearerOptionsFromPolicy(signInPolicy));
app.UseOAuthBearerAuthentication(CreateBearerOptionsFromPolicy(editProfilePolicy));
I get three hits on my ValidateIdentity() in my OAuthBearerAuthenticationProvider, which in itself works OK, but then when the stack unwinds I get a “sequence contains more than one element” exception in System.Net.Http.DelegatingHandler.SendAsync(request, cancellationToken).
Not sure why.
If I just enable the signInPolicy and exclude the other two (why would I need them anyway?) then it all works.
Hello Dan,
I’m not sure why is this happening, are you using the same NuGet packages or newer ones?
Hi Taiseer,
How feasible would it be to use your own OAth2 implementation with ADFS as opposed to using Azure? Is this something you would advise against? Is it possible to use ADFS mainly for authentication and OAuth2 piple for authorization? Or does it defeat the purpose of ADFS?
I’m experiencing this exact same issue right now. Just posted a question @ StackOverflow to see if anyone can help.
https://stackoverflow.com/q/57014018/114029
Where do I find ‘OpenIdConnectConfiguration’?
Excellent article!!! It was well written and easy to follow. How would you incorporate roles? I have a role table that is associated with each user. Would you have to create a custom authorization filter or is there a way to inject the role for the user in the current authorization pipeline?
If you already have existing policies in your Azure AD B2C tenant, feel free to re-use those policies in this sample. Follow the instructions at register a Web API with Azure AD B2C to register the ASP.NET Web API sample with your tenant. Registering your Web API allows you to define the scopes that your ASP.NET Web Application will request access tokens for.
Im getting a 401 error when im executing the test api call. I also published this app on my azure ad account and im using the published url to test it. Any clue?