ASP.NET Core, Swagger and seamless integration with Azure B2C

Recently I've made my very first real project using ASP.NET Core and I must say it looks fabulous! Since I was working on a common API, I decided(as always) to introduce an interface via Swagger. There was an additional feature - I had to use Azure B2C as my authentication service. It was more or less painful, yet after all my struggles the whole integration is brilliant. Here is a short receipt to do it on your own(there're different examples, but I find most of them lacking some small details, which make the whole picture). 

ASP.NET project

This part is simple - create a basic ASP.NET Core project using an API template:

Once we have a project created, one more thing is needed - a package, which will generate a Swagger definition. I decided to try out Swashbuckle.AspNetCore:

Now we have to configure it.

Swagger configuration

To configure Swagger, you have to do 2 things:

  • add a Swagger service
  • tell the application to use it

To add(and configure) a Swagger service go to Startup.cs file and find ConfigureServices method. For the basic functionality it should look like this:

/
public void ConfigureServices(IServiceCollection services)
{
	services.AddMvc();
	services.AddSwaggerGen(c =>
	{
		c.SwaggerDoc("v1", new Info { Title = "Test API", Version = "v1" });
	});
}

Now tell the app to use Swagger:

/
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}

	app.UseMvc();
	app.UseSwagger();
}

Let's try to test it. Press F5 and go to /swagger endpoint and...

Bang, it-does-not-work. Let's try to quickly fix this:

/
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}

	app.UseMvc();
	app.UseSwagger();
	app.UseSwaggerUI(options =>
	{
		options.SwaggerEndpoint("/swagger/v1/swagger.json", "Test API");
	});
}

Now when we start an application, going to /swagger endpoint should redirect us to the definition:

Now let's secure our API!

Securing an API

To secure an API we'll add [Authorize] attribute like this:

/
[Authorize]
[Route("api/[controller]")]
public class ValuesController : Controller
{
}

Now when calling an API method, we'll get HTTP 401 response. To enable B2C token validation we need to configure JWT token options like this:

/
public void ConfigureServices(IServiceCollection services)
{
	services.AddAuthentication(options =>
		{
			options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
		})
		.AddJwtBearer(jwtOptions =>
		{
			jwtOptions.Authority = $"https://login.microsoftonline.com/tfp/{Configuration["AzureAdB2C:Tenant"]}/{Configuration["AzureAdB2C:Policy"]}/v2.0/";
			jwtOptions.Audience = Configuration["AzureAdB2C:ClientId"];
		});
	services.AddMvc();
	services.AddSwaggerGen(c =>
	{
		c.SwaggerDoc("v1", new Info { Title = "Test API", Version = "v1" });
	});
}
/
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}

        app.UseAuthentication();
	app.UseMvc();
	app.UseSwagger();
	app.UseSwaggerUI(options =>
	{
		options.SwaggerEndpoint("/swagger/v1/swagger.json", "Test API");
	});
}

Additionally configure your options in appsettings.json:

/
"AzureAdB2C": {
	"Tenant": "tenantname.onmicrosoft.com",
	"ClientId": "client_id",
	"Policy": "policy_name"
}

But how can we automate sending a bearer token which each request requiring authentication in Swagger?

Enabling OAuth2 authentication in Swagger

To enable authentication using OAuth2 in Swagger, we have to change ConfigureServices method a little:

/
public void ConfigureServices(IServiceCollection services)
{
	services.AddAuthentication(options =>
		{
			options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
		})
		.AddJwtBearer(jwtOptions =>
		{
			jwtOptions.Authority = $"https://login.microsoftonline.com/tfp/{Configuration["AzureAdB2C:Tenant"]}/{Configuration["AzureAdB2C:Policy"]}/v2.0/";
			jwtOptions.Audience = Configuration["AzureAdB2C:ClientId"];
		});
	services.AddMvc();
	services.AddSwaggerGen(c =>
	{
		c.SwaggerDoc("v1", new Info { Title = "Test API", Version = "v1" });
		c.AddSecurityDefinition("oauth2", new OAuth2Scheme
		{
			Type = "oauth2",
			Flow = "implicit",
			AuthorizationUrl = $"https://login.microsoftonline.com/{Configuration["AzureAdB2C:Tenant"]}/oauth2/v2.0/authorize?p={Configuration["AzureAdB2C:Policy"]}&response_mode=fragment",
			Scopes = new Dictionary<string, string>
			{
				{"openid", "OpenID"},
				{$"https://{Configuration["AzureAdB2C:Tenant"]}/{Configuration["AzureAdB2C:AppIDURI"]}/read.access", "Access Test API" }
			}
		});
	});
}

As you can see, we've added security definition for OAuth2 in Swagger definition. We defined the type as oauth2 and flow as implicit(as suited for our scenario). AuthorizationUrl is the URL of our B2C endpoint, which defines which policy we'd like to use and what kind of response we expect. The most important thing are the Scopes, which tell us what we can access. When you run your application, you'll see that integration is enabled:

The last thing we need to do is to configure our application in Azure B2C.

Configuring Azure B2C

Once you create an Azure B2C tenant, you have to register an application and define how one can access it. Go to your tenant and create a new application:

Note that port has to match port under which your application runs locally. Now with an application created in Azure B2C we can obtain ObjectId(here called ApplicationId) and test our integration:

Now when I click Authorize, after providing my user and password, I'll get following error:

Auth error

{"error":"invalid_request","error_description":"AADB2C90205:+This+application+does+not+have+sufficient+permissions+against+this+web+resource+to+perform+the+operation.\r\nCorrelation+ID:+c0657981-668b-40c6-a77d-02cb9061956c\r\nTimestamp:+2018-04-09+09:15:09Z\r\n","state":"TW9uIEFwciAwOSAyMDE4IDExOjE1OjA2IEdNVCswMjAwIChDZW50cmFsIEV1cm9wZWFuIERheWxpZ2h0IFRpbWUp"}

We have to grant our application missing permissions. But how can we achieve this?

Missing permission

As you can see in our code, we defined two scopes: openid and read.access. Normally we'd like to use openid, but Azure B2C requires providing both openid and other scope. For now we're missing read.access scope in our application. Let's add it. In Azure Portal go to Published scopes:

Now add a new scope read.access:

To finish it go one section up to API access and add new value:

Let's test our integration now:

However when trying to execute a method in our API, we're still getting HTTP 401. Something's missing. It turns out, that we're missing a security requirement setting, which handles injecting a token into header. Consider following ConfigureServices method:

/
public void ConfigureServices(IServiceCollection services)
{
	services.AddAuthentication(options =>
		{
			options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
		})
		.AddJwtBearer(jwtOptions =>
		{
			jwtOptions.Authority = $"https://login.microsoftonline.com/tfp/{Configuration["AzureAdB2C:Tenant"]}/{Configuration["AzureAdB2C:Policy"]}/v2.0/";
			jwtOptions.Audience = Configuration["AzureAdB2C:ClientId"];
		});
	services.AddMvc();
	services.AddSwaggerGen(c =>
	{
		c.SwaggerDoc("v1", new Info { Title = "Test API", Version = "v1" });
		c.AddSecurityRequirement(new Dictionary<string, IEnumerable<string>>
		{
			{ "oauth2", new[] { "openid", $"https://{Configuration["AzureAdB2C:Tenant"]}/{Configuration["AzureAdB2C:AppIDURI"]}/read.access" } }
		});
		c.AddSecurityDefinition("oauth2", new OAuth2Scheme
		{
			Type = "oauth2",
			Flow = "implicit",
			AuthorizationUrl = $"https://login.microsoftonline.com/{Configuration["AzureAdB2C:Tenant"]}/oauth2/v2.0/authorize?p={Configuration["AzureAdB2C:Policy"]}&response_mode=fragment",
			Scopes = new Dictionary<string, string>
			{
				{"openid", "OpenID"},
				{$"https://{Configuration["AzureAdB2C:Tenant"]}/{Configuration["AzureAdB2C:AppIDURI"]}/read.access", "Test API" }
			}
		});
	});
}

Now you should see sweet HTTP 200!

Summary

Integrating Swagger with Azure B2C looks like a nice idea to avoid passing a bearer token manually. What is more you can incorporate your authentication process into testing, so you now that e.g. assigned scopes allow or block access in the right way. I strongly encourage you to play this feature a little bit so you how powerful tool it is.

Working with Azure Functions and VSTS - retrieving secrets

This post is an extension to the post written by Marek Grabarz here. If you haven't got a chance to read, I strongly recommend you to do so - it presents a bit more general approach to automate Azure Functions and can act as baseline when it comes to build your custom solution.

The problem

You have an ARM template and full CI/CD pipeline prepared. All works smoothly and with easy. You're just about to grab a beer and celebrate success when suddenly you realizes, that you haven't put functions' keys to the output. After searching multiple pages you finally finds Marek's post, which explains in detail what is needed to obtain a secret from a function. 

Unfortunately using Azure Active Directory Authentication Library (aka ADAL) with VSTS results with the following error:

/
2017-04-24T08:16:40.9453294Z GAC    Version        Location                                                                                         
2017-04-24T08:16:40.9463285Z ---    -------        --------                                                                                         
2017-04-24T08:16:40.9523277Z False  v4.0.30319     C:\Program Files (x86)\Microsoft SDKs\Azure\PowerShell\ResourceManager\AzureResourceManager\Az...
2017-04-24T08:16:40.9683275Z False  v4.0.30319     C:\Program Files (x86)\Microsoft SDKs\Azure\PowerShell\ResourceManager\AzureResourceManager\Az...
2017-04-24T08:16:42.5695169Z ##[error]Exception calling "AcquireToken" with "4" argument(s): "user_interaction_required: One of two conditions was encountered: 1. The PromptBehavior.Never flag was passed, but the constraint could not be honored, because user interaction was required. 2. An error occurred during a silent web authentication that prevented the http authentication flow from completing in a short enough time frame"
2017-04-24T08:16:42.6375138Z ##[section]Finishing: Azure PowerShell script: FilePath

All right - maybe using PrompBehaviour.Auto is going to help:

/
2017-04-24T08:42:56.7459955Z GAC    Version        Location                                                                                         
2017-04-24T08:42:56.7459955Z ---    -------        --------                                                                                         
2017-04-24T08:42:56.7519937Z False  v4.0.30319     C:\Program Files (x86)\Microsoft SDKs\Azure\PowerShell\ResourceManager\AzureResourceManager\Az...
2017-04-24T08:42:56.7679946Z False  v4.0.30319     C:\Program Files (x86)\Microsoft SDKs\Azure\PowerShell\ResourceManager\AzureResourceManager\Az...
2017-04-24T08:42:57.9119830Z ##[error]Exception calling "AcquireToken" with "4" argument(s): "Showing a modal dialog box or form when the application is not running in UserInteractive mode is not a valid operation. Specify the ServiceNotification or DefaultDesktopOnly style to display a notification from a service application."
2017-04-24T08:42:58.0019817Z ##[section]Finishing: Azure PowerShell script: FilePath

Apparently the way how VSTS authenticates itself is different than doing it locally(when you check logs, you'll see, that it doesn't call Login-AzureRMAccount - instead Add-AzureRMAccount -ServicePrincipal, what could be the reason, why ADAL is problematic in this particular scenario). We have to find another way to get the token for authentication. 

The solution

It seems, that the best way to obtain a token is to call a REST API under https://login.windows.net/{tenantId}/oauth2/token. To do so you need a couple of things:

  • client_id for the service principal VSTS uses
  • client_secret(a key which is connected to the service principal)

The best way to find them is to do following:

1. Go to the Services panel

2. You should see the endpoint defined for VSTS. From here you can click on Manage Service Principal

You'll be forwarded to the old portal. Now go to the Configure tab - from here you can copy client_id needed for the API. You can also find the Keys section which is the last thing we need here - just add another key and copy its value(remember that once you leave this page, you won't be able to retrieve it). Once we're armed with additional data, we can use it to get our token:

/
$tokenEndpoint = "https://login.windows.net/{tenantId}/oauth2/token"
$body = @{
        'resource'= "https://management.core.windows.net/"
        'client_id' = "client_id"
        'grant_type' = 'client_credentials'
        'client_secret' = "client_secret"
}

$params = @{
    ContentType = 'application/x-www-form-urlencoded'
    Headers = @{'accept'='application/json'}
    Body = $body
    Method = 'Post'
    URI = $TokenEndpoint
}

$token = Invoke-RestMethod @params
$token | select access_token, @{L='Expires';E={[timezone]::CurrentTimeZone.ToLocalTime(([datetime]'1/1/1970').AddSeconds($_.expires_on))}} | fl *

This token can be further used to get a function key like this(assuming you have a master key):

/
$hostKeyRequest = Invoke-RestMethod -Method GET -Uri "https://$functionAppName.azurewebsites.net/admin/HOST/KEYS?CODE=$masterKey" -Headers @{ Authorization = $token }

Summary

Integrating multiple resources in Azure and VSTS can be a little tricky sometimes, but as you can see it still doesn't require much work to get it working. With a simple Powershell script and one call to the API you can authenticate requests from your VSTS instance and make it work with most components available in Azure.