4

I want to provide the ability for someone to access my API in a browser using OpenIdConnect or using the OAuth 2.0 authorization code flow using a bearer token. This is similar functionality that I see with Microsoft D365 OData endpoints. I can access them via a browser but they require user authentication, or I can access them via an HTTP GET with a bearer token.

If I configure the Web API like this:

services.AddProtectedWebApi(this.Configuration).
AddSignIn(this.Configuration);

I can access the GET API from the browser and if I am not already authenticated it will prompt me for my credentials and authenticate me. However, if I attempt to access the same API from Postman with a bearer token it returns a web page for authentication.

If I configure the Web API like this:

services.AddProtectedWebApi(this.Configuration);

I get a 401 error in the browser but I am able to access the API from Postman with a bearer token.

I would like to be able to use the bearer token if it is provided and otherwise challenge for user credentials.

My Controller users [Authorize] and I am currently not using any validation in the GET action. I am letting the middleware do all the validation.

3 Answers 3

2

I have solved this problem by adding the schemes to the [Authorize] attribute on the controller. I changed the attribute to [Authorize(AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme + "," + JwtBearerDefaults.AuthenticationScheme)] that will allow me to be authenticated in the browser (by logging into D365 or office) and will then authenticate me for the API. If I call the API from Postman with a bearer token it will also authenticate successfully.

I do see one strange behavior. If I am not already authenticated in the browser (or if I start an incognito session) I am no longer prompted for a my credentials and I receive a 401 result. However, if I decorate the controller with [Authorize] or [Authorize(AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme] I will be prompted in the browser and authenticated succesfully but Postman will receive a login page rather than being authenticated by the bearer token.

I also noticed that IAuthenticationSchemeProvider.GetRequestHandlerSchemesAsync() always returns OpenIdConnectDefaults.AuthenticationScheme as the request scheme. It is odd to me that a call from Postman which uses the JwtBearerDefaults.AuthenticationScheme still returns OpenIdConnectDefaults.AuthenticationScheme as the request scheme.

I hope this helps someone else.

Sign up to request clarification or add additional context in comments.

Comments

2

I've been investigating this exact same problem for the past week and have encountered the same 401 problem you've described in your update post. I think I may have encountered a breakthrough today.

Inspired by https://github.com/aspnet/Security/issues/1469#issuecomment-399239254, I came up with the following:

Startup.cs

services.AddProtectedWebApi(
    options =>
    {
        Configuration.Bind("AzureAd", options);
        options.ForwardDefaultSelector = (context =>
        {
            var authHeader = context.Request.Headers["Authorization"].ToArray();
            if (authHeader.Length > 0 && authHeader[0].StartsWith("Bearer "))
            {
                return JwtBearerDefaults.AuthenticationScheme;
            }

            return OpenIdConnectDefaults.AuthenticationScheme;
        });
    },
    configureMicrosoftIdentityOptions: options => Configuration.Bind("AzureAd", options),
    tokenDecryptionCertificate: certificate
);

services.AddSignIn(Configuration);

And then for my controllers, I've decorated them with the [Authorize] attribute and set the OpenIDConnect and JwtBearer authentication schemes via a class I've implemented that implements IControllerModelConvention, inspired by https://joonasw.net/view/apply-authz-by-default, but applying the schemes instead of policies (since that didn't work).

Comments

2

I got it working in .NET 7 app by setting AddJwtBearer explicitly:

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddJwtBearer(opt =>
    {
        var conf = new MicrosoftIdentityOptions();
        builder.Configuration.GetSection(Constants.AzureAd).Bind(conf);
        
        opt.Audience = $"api://{conf.ClientId}";
        opt.Authority = $"{conf.Instance}{conf.TenantId}";
    })
    .AddMicrosoftIdentityWebApp(builder.Configuration);

Then the API controller needs to know the scheme:

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Route("api/[controller]")]
[ApiController]
public class SampleController : ControllerBase
...

2 Comments

This is only one worked for me. You are the champ :)
Also the only one that worked for me on .NET9. We use OIDC to Authenticate with Azure via MicrosoftIdentityWebApp() and this is the only solution that allowed me to call our back end from Postman with Oauth2.0 Client Credential flow. In my case, I did not need to decorate the controller with [ApiController] or provide a route as routing is done in Startup.cs

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.