superninja
superninja

Reputation: 3401

Authorize attribute always returns 401

I am using client credential/app identity flow (OAuth 2.0) where the API is able to authenticate the web app by its app id. There are 2 things that I need to make sure the authentication is successful:

  1. The access token passed from web app to access the API should be a valid bearer token (eg: not expired, valid format, etc)

  2. The app id from the access token has to be the specified web app

When I put the [authorize] attribute in the controller class, it kept returning 401.

Here is startup.cs class

public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(sharedOptions =>
            {
                sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddAzureAdBearer(options => Configuration.Bind("AzureAd", options));

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseHttpsRedirection();
            app.UseAuthentication();
            app.UseMvc();
        }

AzureAdAuthenticationBuilderExtentsions class

public static class AzureAdAuthenticationBuilderExtentsions
    {
        public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder)
        => builder.AddAzureAdBearer(_ => { });

        public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
        {
            builder.Services.Configure(configureOptions);
            builder.Services.AddSingleton<IConfigureOptions<JwtBearerOptions>, ConfigureAzureOptions>();
            builder.AddJwtBearer();
            return builder;
        }

        private class ConfigureAzureOptions : IConfigureNamedOptions<JwtBearerOptions>
        {
            private readonly AzureAdOptions _azureOptions;

            public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions)
            {
                _azureOptions = azureOptions.Value;
            }

            public void Configure(string name, JwtBearerOptions options)
            {
                options.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidAudiences = new string[] {
                        _azureOptions.ClientId,
                        _azureOptions.ClientIdUrl
                    },
                    ValidateAudience = true,
                    ValidateIssuer = true,
                    ValidateIssuerSigningKey = true,
                    ValidateLifetime = true,
                    RequireExpirationTime = true
                };
                options.Audience = _azureOptions.ClientId;
                options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
            }

            public void Configure(JwtBearerOptions options)
            {
                Configure(Options.DefaultName, options);
            }
        }
    }

Here's AzureAdOptions class

 public class AzureAdOptions
    {
        internal static readonly object Settings;

        public string ClientId { get; set; }

        public string ClientIdUrl { get; set; }

        public string ClientSecret { get; set; }

        public string Instance { get; set; }

        public string Domain { get; set; }

        public string TenantId { get; set; }
    }

And controller class

  [Route("api")]
    [ApiController]
public class FindController : ControllerBase
{
    private IConfiguration _configuration;
    HttpClient _client;
    public ContentController( IConfiguration configuration)
    {
        _configuration = configuration;
    }

    private bool ValidateRequest()
    {
        var authHeader = Request.Headers["Authorization"];
        if (StringValues.IsNullOrEmpty(authHeader) || authHeader.Count == 0)
        {
            throw new UnauthorizedAccessException(Messages.AuthHeaderIsRequired);
        }
        var tokenWithBearer = authHeader.Single();
        var token = tokenWithBearer.Substring(7); //remove bearer in the token
        var jwtHandler = new JwtSecurityTokenHandler();
        if (!jwtHandler.CanReadToken(token))
        {
            throw new FormatException("Invalid JWT Token");
        }

        var tokenS = jwtHandler.ReadToken(token) as JwtSecurityToken;
        var appId = tokenS.Audiences.First();
        if (string.IsNullOrEmpty(appId))
        {
            throw new UnauthorizedAccessException(Messages.AppIdIsMissing);
        }
        var registeredAppId = _configuration.GetSection("AzureAd:AuthorizedApplicationIdList")?.Get<List<string>>();
        return (registeredAppId.Contains(appId)) ? true : false;
    }
    [HttpPost("Find")]
    [Produces("application/json")]
    [Authorize]
    public async Task<IActionResult> Find()
    {
        try
        {
            if (!ValidateRequest())
            {
                return Unauthorized();
            }
         return new ObjectResult("hello world!");
        }
        catch (InvalidOperationException)
        {
            return null;
        }
    }
}

Anyone knows why it keeps returning 401 error? One thing I would like to mention is between the time I start calling the API till it returns 401 error, the break points inside the controller class never got hit...

Upvotes: 2

Views: 1488

Answers (2)

Nan Yu
Nan Yu

Reputation: 27538

If the resource is App ID URI of the api application when acquiring access token for accessing api application . In api application , allowed audience should also include the App ID URI of the api application .

Upvotes: 2

Rohit Saigal
Rohit Saigal

Reputation: 9664

I would make 2 points for you to check:

1. Check for appID matching AuthorizedApplicationIdList in your code

I think the way you have described conditions to check in words is fine, but there is a problem with how you have implemented the second condition in code.

  1. The app id from the access token has to be the specified web app

When implementing this condition, it seems you are setting appId as the value from aud i.e. audience claim in token. This is incorrect, because audience will always be your own API for which this token is intended.

What you want to check instead is the value for appid claim in token, which will be the application ID for client that acquired this token. This should be the front end web app's application ID which you want to check against your list of Authorized applications.

Take a look at Microsoft Docs reference for Access Tokens

enter image description here

enter image description here

Also, you can verify this easily by decoding the token using https://jwt.ms

Relevant code from your post where I see issue:

    var appId = tokenS.Audiences.First();
    if (string.IsNullOrEmpty(appId))
    {
        throw new UnauthorizedAccessException(Messages.AppIdIsMissing);
    }
    var registeredAppId = _configuration.GetSection("AzureAd:AuthorizedApplicationIdList")?.Get<List<string>>();
    return (registeredAppId.Contains(appId)) ? true : false;

2. General Log/Debug

Also, on a side note, you can probably debug or put log/trace statements in your API code to find out exactly where it's failing in your code.. or if it unexpectedly fails somewhere even before your custom logic is called. Maybe while some of the initial validations are being performed.

Upvotes: 1

Related Questions