Here is the solution to the problem I was experiencing, incase anyone else happens to come across this post.
I had several issues:
- 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.
- The calling code was missing the
@auth0/auth0-angular
AuthService; this was needed to add the token to the header of the request. - Authorization on the API was not properly implemented.
- 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:
- Scopes vs Permissions confusion
- ASP.NET Core Web API SDK Quickstarts: Authorization
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();
}
}