.NET Core Angular 401 when calling API

Hi,

I have a .NET Core Angular app that has been setup to make secure calls to an API using but I’m getting a 401error when the client application makes the call to the API.

This issue has been reported several times, but none of the previous posts provide a solution.

I used the auth0-angular SDK as per the guidance in the Complete Guide to Angular User Authentication and the instructions the from the Authorization for ASP.NET Web APIs blog post to setup authorization for the API. I can securely access the API endpoint using the API’s swagger instance as per the blog, but when I try to connect from the client app I get a 401 whether I’m local or in a deployed environment. Below is my implementation.

What am I missing?

I can provide a HAR file on request. LMK

Thanks!

auth_config.json

{
    "domain": "dev-********.us.auth0.com",
    "clientId": "BuLP3AHrIZLE4rxI25OmqwL********",
    "audience": "https://myapp-dev.azurewebsites.net",
    "errorPath": "/error"
}

environment.ts

import config from '../../auth_config.json';

const { domain, clientId, audience, errorPath } = config as {
  domain: string;
  clientId: string;
  audience?: string;
  errorPath: string;
};

export const environment = {
  production: false,
  auth: {
    domain,
    clientId,
    ...(audience && audience !== 'YOUR_API_IDENTIFIER' ? { audience } : null),
    redirectUri: window.location.origin,
    errorPath,
  },
  httpInterceptor: {
    allowedList: [`https://myapp-dev.azurewebsites.net/WeatherForecast`],
  },
};

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { NavMenuComponent } from './nav-menu/nav-menu.component';
import { HomeComponent } from './home/home.component';
import { CounterComponent } from './counter/counter.component';
import { FetchDataComponent } from './fetch-data/fetch-data.component';

import { AuthModule, AuthGuard, AuthHttpInterceptor } from '@auth0/auth0-angular';
import { environment as env } from '../environments/environment';

@NgModule({
  declarations: [
    AppComponent,
    NavMenuComponent,
    HomeComponent,
    CounterComponent,
    FetchDataComponent
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
    HttpClientModule,
    FormsModule,
    AuthModule.forRoot({
      ...env.auth,
      httpInterceptor: {
        ...env.httpInterceptor,
      },
    }),
    RouterModule.forRoot([
    { path: '', component: HomeComponent, pathMatch: 'full' },
    { path: 'counter', component: CounterComponent, canActivate: [AuthGuard] },
    { path: 'fetch-data', component: FetchDataComponent, canActivate: [AuthGuard] },
], { relativeLinkResolution: 'legacy' })
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthHttpInterceptor,
      multi: true,
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Here is the solution to the problem I was experiencing, incase anyone else happens to come across this post.

I had several issues:

  1. My environment configuration was not set properly; needed to specify the uri and tokenOptions/audience in the allowedList. I also had to remove the scopes property I had setup. TL;DR it was causing some other errors and the usage was not quite clear to me so I removed it. The solution for me was to handle permissions on the server and ensure the API was setup properly. After doing that it didn’t seem like it was necessary.
  2. The calling code was missing the @auth0/auth0-angular AuthService; this was needed to add the token to the header of the request.
  3. Authorization on the API was not properly implemented.
  4. I did not have RBAC properly enabled.

Ultimately, I was able to get this working by piecemealing some information from a ticket I opened up with Auth0 and from some other posts in the Auth0 Community. See the references below. I’ve also, included the working code for reference.

References:

environment.ts

import config from '../../auth_config.json';

const { domain, clientId, audience, errorPath } = config as {
  domain: string;
  clientId: string;
  audience?: string;
  errorPath: string;
};

export const environment = {
  production: false,
  auth: {
    domain,
    clientId,
    ...(audience && audience !== 'YOUR_API_IDENTIFIER' ? { audience } : null),
    redirectUri: window.location.origin,
    errorPath,
  },
  httpInterceptor: {
    allowedList: [
      {
        uri: 'https://localhost:5001/*',
        tokenOptions: {
          audience: 'https://myapp-dev.azurewebsites.net'
        }
      } 
    ],
  },
};

fetch-data.component.ts

import { Component, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AuthService } from '@auth0/auth0-angular';

@Component({
  selector: 'app-fetch-data',
  templateUrl: './fetch-data.component.html'
})
export class FetchDataComponent {
  public forecasts: WeatherForecast[];

  constructor(public auth: AuthService, http: HttpClient, @Inject('BASE_URL') baseUrl: string) {
    http.get<WeatherForecast[]>(baseUrl + 'weatherforecast').subscribe(result => {
      this.forecasts = result;
    }, error => console.error(error));
  }
}

interface WeatherForecast {
  date: string;
  temperatureC: number;
  temperatureF: number;
  summary: string;
}

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    // In production, the Angular files will be served from this directory
    services.AddSpaStaticFiles(configuration =>
    {
        configuration.RootPath = "ClientApp/dist";
    });

    var domain = $"https://{Configuration["Auth0:Domain"]}/";

    services.AddCors(options =>
    {
        options.AddPolicy("AllowSpecificOrigin",
            builder =>
            {
                builder
                .WithOrigins("http://localhost:5001")
                .AllowAnyMethod()
                .AllowAnyHeader()
                .AllowCredentials();
            });
    });

    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.Authority = domain;
        options.Audience = Configuration["Auth0:Audience"];
        
        // If the access token does not have a `sub` claim, `User.Identity.Name` will be `null`. Map it to a different claim by setting the NameClaimType below.
        options.TokenValidationParameters = new TokenValidationParameters
        {
            NameClaimType = ClaimTypes.NameIdentifier
        };
    });

    services.AddAuthorization(options =>
    {
        options.AddPolicy("read:weatherforecast", policy => policy.Requirements.Add(new HasScopeRequirement("read:weatherforecast", domain)));
    });

    services.AddSingleton<IAuthorizationHandler, HasScopeHandler>();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...
    app.UseAuthentication();

    app.UseAuthorization();
    ...
}

HasScopeRequirement.cs

public class HasScopeRequirement : IAuthorizationRequirement
{
    public string Issuer { get; }
    public string Scope { get; }

    public HasScopeRequirement(string scope, string issuer)
    {
        Scope = scope ?? throw new ArgumentNullException(nameof(scope));
        Issuer = issuer ?? throw new ArgumentNullException(nameof(issuer));
    }
}

HasScopeHandler.cs

protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasScopeRequirement requirement)
{
        // First check for permissions, they may show up in addition to or instead of scopes...
        if (context.User.HasClaim(c => c.Type == "permissions" && c.Issuer == requirement.Issuer && c.Value == requirement.Scope))
        {
            context.Succeed(requirement);
            return Task.CompletedTask;
        }

        // This is the Auth0 version, which only checks for scopes instead of permissions.
        // If user does not have the scope claim, get out of here
        if (!context.User.HasClaim(c => c.Type == "scope" && c.Issuer == requirement.Issuer))
            return Task.CompletedTask;

        // Split the scopes string into an array
        var scopes = context.User.FindFirst(c => c.Type == "scope" && c.Issuer == requirement.Issuer).Value.Split(' ');

        // Succeed if the scope array contains the required scope
        if (scopes.Any(s => s == requirement.Scope))
            context.Succeed(requirement);

        return Task.CompletedTask;
}

WeatherForecastController.cs

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Muggy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    [Authorize("read:weatherforecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
}
1 Like

Thanks a lot for sharing it all with the rest of community! Really glad you have figured it out yourself!

1 Like