ASP.NET Web APIs, OAuth2 & Microsoft Identity
A little while back, I wanted to create a secured .net 8 Web API, where users could only authenticate with Microsoft Identity using OAuth2. According to documentation and articles I read, I expected this would be a simple endeavour - at most a couple of hours to code the basics. After all there even is a wizard right in Visual Studio 2022 to create the boilerplate code and Azure App Registration.
In reality, the out-of-the-box code produced by the wizard does not work! In fact I found that even most of the articles and Microsoft's own documentation and tutorials do not work - at least not as described!
After some trial and error, scraping ideas from various places, I stumbled on the full end-to-end solution as outlined below. I'll confess it is not necessarily optimal, and some use cases may need more work - but it will lay a working foundation regardless.
And at least it works.
There are several parts to getting it all to work together and to test it. First let's deal with the App Registration in Azure, which is almost correct in most sources.
Azure app registration
Within the Visual Studio 2022 wizard that creates a Web API with MS Identity authentication is a step that allows you to use an existing or create a new Azure App Registration.
Choosing to create a new App Registration automatically creates a registration allowing both Azure Entra Id and Microsoft Account authentication.
Crucially, it does not create a certificate or secret under the "Certificates & secrets" blade. This is probably deliberate as some users may choose a certificate or secret or indeed Federated credentials. However, it's not clear from the wizard that this step is necessary - I just happened to know that it was.
On the flip-side, all the URLs and other necessary settings are configured. You can still go onto the Azure portal and tweak the configuration of course. For example, adding optional claims to the "Token configuration" to pass more data in the access or id tokens.
Nevertheless, for avoidance of doubt here are the key items of App Registration configuration, some of which are set via the wizard, but mentioned here if you want to do it completely manually.
Branding & properties
- Specify a Name for the app registration
- Optionally, provide a verified publisher domain, if applicable. I'm not entirely sure if this even needs to be a 'real' domain when working with purely Microsoft accounts
Authentication
Platform configurations
Web - Redirect URIs
- Enter the redirect URI(s) of your application. For the WebApi you only need the 'Web' platform.
- All the URI(s) redirect to
<host>:<port>/signin-oidc
by default.
Implicit grant and hybrid flows
For a Web API (or simple .net Web application) only check the 'ID tokens (used for implicit and hybrid flows)' checkbox. Do not check the 'Access tokens' checkbox.
Supported account types
If you, like me, only want Microsoft account authentication, then this should show as "Personal Microsoft accounts only". In the Manifest this corresponds to
"signInAudience": "PersonalMicrosoftAccount",
However, you could, if you also wanted Entra ID authentication alongside Microsoft accounts, have it set to "Accounts in any organizational directory", which corresponds to
"signInAudience": "AzureADandPersonalMicrosoftAccount",
in the Manifest.
Advanced settings
- Live SDK support: set to 'Yes' by default. I haven't yet experimented with this, and I doubt it needs to be set to 'Yes', but I left it as is.
- Allow public client flows: set to 'No', as in general an API is not intended to be accessed directly, and is not a client application.
Certificates & secrets
- Create a Client secret and make sure you copy the value somewhere because it gets masked after you navigate away from the blade.
Token configuration
Adding optional claims can introduce additional information to the token. However, in a base configuration, can be left empty.
API permissions
If using the Visual Studio wizard, a single permission will have been created for the API.

You could add more permissions from the Microsoft Graph API for example, but this is all that's needed for a working scenario. (For example, I added User.Read
from MS Graph.)
If you're creating the permission manually, you can name the permission anything you wish (within the rules), but remember to configure other settings in clients etc accordingly.
Expose an API
- If created by the Visual Studio wizard, it will typically be a concatenation of
api://<appregistrationclientId>
. In theory it could be any unique URI, but the format seems to be conventionally as per the default.
- By default one scope is created by the wizard. More can be added. Notably the scope must match with the permission created in the 'API permissions' blade. Whether this is a convention or a necessity is unclear, but the UI apparently allows divergence.

API middleware
Now this is where the pain was. By far the biggest issue with the Visual Studio wizard (and the documentation) is that the boiler-plate code that is supposed to go with the wizard's App Registration simply does not work.
This is what is generated in Program.cs
by default.
// Add services to the container. This does NOT work!!
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
If you try to call the wizard's/WeatherForecast
endpoint using Postman for example, you'll get a 401 response - with the message Bearer error="invalid_token", error_description="The issuer '(null)' is invalid"
.
The web and Stack Overflow are inundated with what appears to be hundreds of questions and answers with wildly varying levels of complexity, over various .net versions trying to explain how to get a simple .net api to deal with this. I spent weeks (on and off) trying various things to get my solution to work. Eventually I started from scratch, and after following some hunches just got lucky.
It is not an invalid token, despite the error message. The token is valid - and can be tested on JWT.io or JWT.ms . What that reveals is the issuer claim in the token:

Basically, it is the code that is unable to validate the issuer even facing a perfectly good token!
This means that the authentication middleware has to be told about the issuer.
But that is not all. It also has to be told about the issuing authority.
Finally, it needs to be told about valid audiences for the token - i.e. the API which is using the App Registration.
Taken together the provided code is wholly inadequate, and was a huge source of frustration, both to me and apparently, to many people online.
To make matters worse, there doesn't appear to be a consistent and clear document anywhere describing a minimal working code and configuration to do this. What does exist may once have worked, but plainly does not work now. Instead you have to scrape together peices of information from various, frequently contradictory articles both from Microsoft and other bloggers, and Stack Overflow.
Anyway, the actual middleware registration is more like this (annotated with explanations):
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
var config = builder.Configuration;
config.Bind("AzureAd", options); // <= providing the configuration section
// Authority will be Your AzureAd Instance and Tenant Id (or the MS identity GUID)
options.Authority = $"{config["AzureAd:Instance"]}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0"; // <= the value of "iss" noted from the token itself
// The valid audiences are both the Client ID(options.Audience) and api://{ClientID}
options.TokenValidationParameters.ValidAudiences = new[]
{
config["AzureAd:ClientId"],
$"api://{config["AzureAd:ClientId"]}", // <= Application Id URI from "Expose an API" blade in App Registration
};
// Valid issuers here:
options.TokenValidationParameters.ValidIssuers = new[] {
$"{config["AzureAd:Instance"]}{config["AzureAd:TenantId"]}/v2.0", // <= not necessary for pure MS Identity - but needed if also using Entra Id
$"{config["AzureAd:Instance"]}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0", // <= the value of "iss" noted from the token itself
};
});
This is the appsettings.json for the application (obfuscated):
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "crucisconsulting.co.uk",
"TenantId": "d5a01fc1-aaaa-bbbb-cccc-dddddddddddd",
"ClientId": "3399fd17-xxxx-yyyy-zzzz-8e6da5c5791b",
"CallbackPath": "/signin-oidc",
"Scopes": "access_as_user"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
It was only on inspecting the token itself that I found out about the significance of the Microsoft Identity GUID (shown as 9188040d-6c67-4c5b-b112-36a304b66dad
in the snippet) - and how for a pure Microsoft Identity authentication - the Tenant Id is not even necessary.
Testing with Postman
So having configured App Registration and the API middleware, I needed to test the secured API with a tool like Postman. This requires 2 steps:
- Authenticating with Postman, and getting a token
- Making a request to a secured endpoint to ensure that the API middleware and the App Registration are both working together.
Variables
Postman allows variables to be created per target environment for ease of configuration.
Create the following variables to target the Azure environment you'll be using for authentication.
- Name:
BaseUrl
, value '< base url of the api >' e.g. https://localhost:9876
- Name:
ClientSecret
, value '< value of the Client Secret created under App Registration: Certificates & secrets >'
- Name
ClientId
, value '< Application ID from the App Registration >'
- Name
TenantId
, value '< Directory ID of the subscription >', but for exclusively Microsoft Authentication, set the value to consumers
.
Authentication
Select the required collection in Postman and click on the 'Authorization' tab.
- From the 'Auth Type' select 'OAuth 2.0'
- 'Token Name': give the token a name - this is just for Postman and has no consequence outside of Postman
- 'Grant type': select 'Authorization Code (With PKCE)'. (If you enabled implicit authentication in App Registration, you can choose Implicit here.)
- 'Callback URL': This needs to be one of the redirect URIs added in the Authentication blade of the App Registration in Azure. e.g.
https://localhost:9876/signin-oidc
- You can check the 'Authorize using browser' checkbox, but in order for it to work, you need to also add the Postman redirect URI (currently
https://oauth.pstmn.io/v1/callback
) to the App Registration. However, not checking the box removes this dependency.
- 'Auth URL': set to
https://login.microsoftonline.com/{{TenantId}}/oauth2/v2.0/authorize
- 'Access Token URL':
https://login.microsoftonline.com/{{tenant}}/oauth2/v2.0/token
- 'Client ID':
{{ClientId}}
- 'Client Secret':
{{ClientSecret}}
- 'Code Challenge Method': leave as
SHA-256
- Leave 'Code Verifier' blank
- 'Scope':
api://{{ClientId}}/.default
- 'State': can be any alphanumeric string or even left blank.
- 'Client Authentication': leave as
Send as Basic Auth header
To actually get the token, click the 'Get New Access Token' button. This will initiate the authentication process either in Postman or via the browser (depending on whether the checkbox in step 5 above was checked or not).
If the account being used for authentication requires it, it may also trigger MFA. Finally the token will be returned, if successful.

Click the 'Use Token' button to accept the token for requests.
Sending requests
In the URL, select the appropriate method e.g. 'GET' and enter the URL using the base url set in the variables, e.g. {{BaseUrl}}/WeatherForecast
.
Under 'Authorization' ensure the 'Auth Type' is 'Inherit auth from parent', so the request uses the same authorization as configured on the collection.
Finally, create the other aspects of the request as appropriate e.g. Headers and Body.
Click 'Send'. Postman will automatically add a bearer Authorization header to the request using the token obtained earlier. Assuming the token hasn't expired you should see something like below in the Postman console.

Conclusion
I hope this post helps someone to get past the obvious pitfalls in setting up MS Identity with OAuth2 on a Web API.
There are clear points of extensibility in the middleware declarations, for example in token validation. You can also register callbacks to respond to events during the authentication phase.
I'd love to hear from you about your experience of this scenario. Happy coding!