(Note: this is a developement question, I tried to select the most appropriate tag though it seems that they didn’t really fit my question.)
Context
I am working on an application that was made by another developer, with zero knowledge transfer or documentation. Hence I do not necessarily know about the different choices that have been made an I am not necessarily familiar with everything. And although I have secured a web site application long ago, I am no modern security expert.
Current state
The API controllers retrieve the user data from a MemoryCache based on the user id (which is a UID, I guess a GUID.)
This seems flawed as I can send a POST request to the API using the user id and trigger its methods.
The goal
The goal is to prevent the API from being reachable so easily using the user Id only.
From my current understanding the API should do an extra-check based on a temporary token (JWT token). Therefore, I should:
- Be able to pass the JWT token in API request
- Know the JWT token on
- API side Retrieve the JWT token from request on API side
- Compare both
Though I think I understand the concepts, currently, I feel like various parts of the application are expected to make a lot of things under the hood once configured and I don’t get how to bring this token to the requests sent to the API. It is not impossible I in wrong track since the beginning…
(Searching for examples, I find a lot of examples on how to configure Auth0 in for login, but I only find rare references on how to use a temporary token, and when I do find a few, they seem to refer to a different versions/approaches.)
Current state - Technical
Backend - Authentication
Authentication server: Auth0 / Backend: .NET 8.0 WebApi / Frontend: Angular (project currently 17.3.7)
In backend, in program.cs , we have
var domain = $"https://{builder.Configuration["Auth0:Domain"]}/";
var audience = builder.Configuration["Auth0:ApiIdentifier"];
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = domain;
options.Audience = audience;
});
Note
Following some tutorial, I tried adding this to solve some other issue but did not solve it.
I don’t think it’s directly relevant to the question, but I mention it in case it would have interactions with relevant parts.
#if DEBUG
//Source: https://medium.com/@cl0v15/secure-an-angular-application-and-net-web-api-with-auth0-df1db7d6effa
builder.Services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", builder => builder
.WithOrigins([front-end local URL])
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
#endif
Frontend
Auth0Service
On frontend side, we have an “authentication service” numbered v2. (v1 is all commented… And I think I found some examples looking like v1, but looking v2 I may have seen a piece of it in a StackOverflow answer, but not much more.)
Here is the code for this class:
import { Injectable } from '@angular/core';
import { AppState, AuthService, LogoutOptions, RedirectLoginOptions, User } from '@auth0/auth0-angular';
import { SessionActions } from '@[anonymised]/session-data-access';
import { Store } from '@ngrx/store';
import { Observable, combineLatestWith, filter, from, map, take, tap } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class Auth0Service {
constructor(
private auth: AuthService,
private store: Store,
) {
this.auth.isAuthenticated$.subscribe(
(value) => {console.log("Is authenticated?", value);} //Logs true?
);
this.auth.error$
.pipe(
// filter((e) => e instanceof GenericError && e.error === 'login_required'),
map(() => console.log('error in auth.error')),
)
.subscribe();
}
public user$: Observable<User | null | undefined> = this.auth.user$;
public loginWithRedirect(options?: RedirectLoginOptions<AppState> | undefined): void {
console.log("loginWithRedirect", options);
this.auth.loginWithPopup(options);
}
public handleAuthCallback(): void {
this.getUser$.pipe(take(1)).subscribe();
}
private getUser$ = this.auth.isAuthenticated$.pipe(
filter((i) => i === true),
combineLatestWith(this.user$),
map(([, user]) => {
console.log("this.user$", this.user$);
if (user) {
console.log("User", user);
/* Logs a User object with properties
- email
- mail_verified : true
- https://cust.[domain containing company name, ends with .app extension]/token (note: I have never seen this domain anywhere but it's registered by the company) : [value is a custom token assigne to my account in Auth0 MetaData field, token was generate when creating my user in the application database]
- name : [my application login]
- nickname : [my application display name]
- picture : [gravatar URL, I think it's an Auth0 stuff set automatically]
- sub : [my Auth0 user_id in format "auth0|000000000000000000000000", I replaced the id with zeros for this comment
- updated_at : [time stamp from approximately 45 minute ago]
// I don't think there is any JWT token in here
*/
this.store.dispatch(SessionActions.loadSteer({ steerId: user['https://cust.[domain]/token'] }));
}
}),
);
public getTokenSilently() {
from(
this.auth.getAccessTokenSilently().pipe(
filter((i) => i !== undefined && i !== null),
map(() => this.handleAuthCallback()),
),
).subscribe(
(value)=>{console.log('getTokenSilently subscription', value);} //Logs "undefined".
);
}
public logout(options?: LogoutOptions | undefined): Observable<void> {
return this.auth.logout(options);
}
}
getTokenSilently()
is called in initial AppComponent
constructor.
A class named [DomainOrCategory]Service
(please keep in my that I am anonymising the classes)
passes requests for the API to an ApiService
using this type of methods:
Save[entity](form: entityForm): Observable<[entity]Result> {
return this.apiService.post<<[entity]Result >(this.post[entityUrl, sdg);
}
Sending requests to API
In ApiService
public post<T>(endpoint: string, body?: any, options?: Options): Observable<T> {
//Options is empty when I send a POST, and I’m not sure if it’s supposed to hold anything
const endpointUrl = this.buildApiEndpoint(endpoint);
return this.postWithFullUrl(endpointUrl, body, options);
}
public postWithFullUrl<T>(endpoint: string, body?: any, options?: Options): Observable<T> {
return this.httpClient.post<T>(endpoint, body, options);
}
(httpClient is from Angular version 17.3.9)
Backend - API authorisation filer
On backend side, the API uses an authorisation filer
[Authorization filter class] : System.Attribute, Microsoft.ASPNetCore.MVC.Filters.IAsyncResourceFilter { (…)
This filter then tries to identify the user from a MemoryCache which contains the data of all current users. (Currently less than 20, and this access feels quite slows while debugging).
(…)
identifier = Guid.Parse(context.HttpContext.Request.Headers["steerid"]);
(…)
if (!service.MemoryCache.TryGetValue(UserKey, out ICollection<User> userCacheEntries))
{
userCacheEntries = await userRepo.GetUsersAsync(cancellationToken);
service.MemoryCache.Set(UserKey, userCacheEntries);
}
var user = userCacheEntries.SingleOrDefault(i => i.Id == identifier);
If it failed the first time, this method of fetching the user is repeated a second time, in a slightly different way (since userCacheEntries
is already assigned).
Nothing appears to be checking a JWT Token.
Update 1
In frontend, I noticed that AuthService, which is part of classes provided by “auth0-angular” has a idTokenClaims$
propety
this.auth.idTokenClaims$.subscribe(
(value) => {console.log("idTokenClaims$", value);}
);
This code display an object containing various keys. I not sure yet whet each property is but there’s a good chance that one of them contain the token I’m looking for.
I should find the way to include it to the requests.
Yet, I am not sure how to know its expected value on backend in order to compare.