So since I don't yet have a nice guide on this blog for how to do Azure AD authentication in an API, here you go! This article is going to be a bit longer, so I'll split it into two parts.
This first part will look at:
- Registering an API and a client app in Azure AD
- Creating a basic ASP.NET Core API and adding authentication
- Creating a test client app that calls the API
The next part will look at:
- Defining custom delegated and application permissions
- Creating a console app which uses application permissions to call the API (meant to be run as an Azure Web Job)
- Multi-tenant app scenario, the considerations that you need to make
We will be using the v1 endpoint for this article. The new v2 application registration portal will converge with the current registration portal at some point. At the moment there is no published timeline when this will happen though. I would like to rather revisit this article after the convergence has happened than write it for the v2 model and portal which will not be up to date later anyway.
You can find the sample app in GitHub.
Registering an API and a test client in AAD
To get started with registering our API, let's open the AAD Management Azure Portal. There we need to go to Azure Active Directory -> App registration -> New application registration.
Here I will enter (you can set the name and URL to anything you like, you can change them later too):
- Name: Todo API
- Application type: Web app / API
- Sign-on URL:
https://localhost
- This one does not have to match to the API's address, users won't sign in to it directly anyway
- But since it does show up in the My Apps portal, it can be a good idea to set this to the actual URL :)
After the API is created, copy the Application ID and put it somewhere.
Then go to Settings -> Properties. Copy the App ID URI and put it next to the application id. If your API needs to call other APIs like the Microsoft Graph API, you will also need to add a key. For now we will not need that, since our API does not need to call any other APIs.
You can check my previous article on the On-behalf-of flow to see how your API can continue the delegation chain. Calling other APIs with application permissions can be done with client credentials relatively simply.
Next we can register the client app we will use to call the API. Go back to create a new app registration and this time specify the following info:
- Name: Todo Client
- Application type: Native
- Sign-on URL:
https://localhost
- Once again this does not really matter as we will use device code authentication which does not use redirect URIs
After it has been created, once again write down the Application ID.
Then we need to add permissions to the client to call the Todo API. So we go to Required permissions and click Add. When you get to the API selection, you can type Todo in the search, and the API should show up. Select it, and then choose Access Todo API as the required permission.
By default, every Web app/API in Azure AD has this delegated permission available. In the second part we will look at how more can be added.
Finally we need the Azure AD tenant id. You can get it from the Properties blade of Azure Active Directory.
Creating a basic ASP.NET Core API with authentication
Then we'll create the API in Visual Studio 2017. I used the default API template for an ASP.NET Core app as the basis, and you can see the full source code here: GitHub.
The first thing we should do is put the necessary configuration in place.
For that, I put the following in the appsettings.json
file:
{
"Authentication": {
"Authority": "",
"ClientId": "",
"AppIdUri": ""
}
}
The Authority should be https://login.microsoftonline.com/your-tenant-id
if you are using the general Azure AD endpoints.
There are different endpoints for certain Azure environments:
- Germany
- China
- U.S. Government
More info on those in the documentation.
It can also be a good idea to have a separate appsettings.Development.json
and
appsettings.Production.json
file,
and make two applications in Azure AD.
Then you use one in dev/test environments, and the other in production.
The authority might be common, in which case you can put it in the appsettings.json
file.
Never put secrets in these files.
These three pieces of information are not secrets (just identifiers),
and so can be kept here.
Next let's look at the dependency injection and middleware configuration:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(o =>
{
o.Filters.Add(new AuthorizeFilter("default"));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddAuthorization(o =>
{
o.AddPolicy("default", policy =>
{
// Require the basic "Access app-name" claim by default
policy.RequireClaim(Constants.ScopeClaimType, "user_impersonation");
});
});
services
.AddAuthentication(o =>
{
o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.Authority = Configuration["Authentication:Authority"];
o.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
// Both App ID URI and client id are valid audiences in the access token
ValidAudiences = new List<string>
{
Configuration["Authentication:AppIdUri"],
Configuration["Authentication:ClientId"]
}
};
});
// Add claims transformation to split the scope claim value
services.AddSingleton<IClaimsTransformation, AzureAdScopeClaimTransformation>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Very important that this is before MVC (or anything that will require authentication)
app.UseAuthentication();
app.UseMvc();
}
}
The important bits are adding authentication services,
configuring JWT Bearer authentication as the default,
configuring authorization,
and of course actually applying authentication with app.UseAuthentication();
.
I've forgotten that last one many times.
Note that we require the caller to have the user_impersonation
scope.
This is the default delegated permission that exists in every Web app/API in Azure AD.
We do not want apps to call our API unless this basic permission has been granted.
In the next part we will look at adding delegated and application permissions for the API,
so we will revisit this then.
We don't need to use [Authorize]
now on controllers/actions since we defined a default authorization policy here that applies globally.
If you only require an authenticated user, any confidential client in your Azure AD can acquire an access token for your API and call it.
So it is important that you implement the user_impersonation
scope check at minimum.
I also added a custom claims transformation to split the scope claim into multiple claims.
If the caller has multiple scopes, they are put into one claim, separated by spaces.
This transformation allows you to check the caller has a scope with some value without having
to do claim.Value.Split(' ').Contains("value")
every time.
Making a test client app
For the test client, I decided to make a .NET Core Console Application. Now ADAL.NET will not enable us to use a GUI for authentication since .NET Standard doesn't give us that capability. So instead we will use device code authentication.
I will not dive too deep into the client app here, but you can see the full source code here.
The important points:
- Use the same authority as in the API
- Use the API's App Id URI as the resource
- Acquire an access token with the device code flow
- Attach the token to requests to the API as a header:
Authorization: Bearer access-token-here
End of part 1
We went through quite a lot of things in this article. Now we have an API with basic authentication setup, as well as a client app we can use for testing.
Try to remember these key points:
- Use JWT Bearer authentication in your API + make it the default scheme
- Set the
Authority
to point to your Azure AD tenant (or the common endpoint, which we discuss in the next part) - Configure
ValidAudiences
on the API with both the Client Id and App Id URI, both are basically valid - At minimum, require the
user_impersonation
scope for all requests- Unless you define custom permissions, then require those
- Don't forget
app.UseAuthentication()
in your middleware pipeline :D - Define a default authorization policy and require that globally across the API by default