如何基于Auth0、.NET 8、Angular完成现有API的安全加固?
我正在接手一位其他开发者开发的应用,没有任何知识传递或文档。因此我不清楚之前的选型决策,也对部分内容不熟悉。虽然我很久以前做过网站应用的安全加固,但并非现代安全领域专家。
API控制器基于用户ID(一个UID,推测是GUID)从MemoryCache中获取用户数据。这种方式存在漏洞,因为我只需发送包含用户ID的POST请求就能触发API方法。
目标是避免API仅靠用户ID就能轻易被访问。
根据我目前的理解,API应基于临时令牌(JWT token)进行额外校验。因此我需要:
- 能够在API请求中传递JWT令牌
- 在API端获取请求中的JWT令牌
- 对令牌进行校验
虽然我理解相关概念,但目前我不清楚如何将令牌加入API请求,也不确定是否从一开始就走错了方向。(搜索示例时,我找到很多Auth0登录配置的示例,但很少找到临时令牌使用的参考,少数找到的也对应不同版本/方案。)
后端 - 认证
认证服务器:Auth0 / 后端:.NET 8.0 WebApi / 前端:Angular(当前版本17.3.7)
在后端的program.cs中,我们有如下代码:
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; });
说明
我曾按照某教程添加了以下代码以解决其他问题,但并未奏效。我认为这与当前问题无直接关联,但仍提及以防存在交互影响。
#if DEBUG builder.Services.AddCors(options => { options.AddPolicy("CorsPolicy", builder => builder .WithOrigins([front-end local URL]) .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials()); }); #endif
前端
Auth0Service
前端有一个编号为v2的“认证服务”(v1已全部注释…我找到的部分示例类似v1,但v2仅在StackOverflow的一个回答中见过部分实现)。以下是该类的代码:
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()在初始AppComponent的构造函数中被调用。一个名为[DomainOrCategory]Service的类(已匿名处理)通过如下方法将API请求传递给ApiService:
Save[entity](form: entityForm): Observable<[entity]Result> { return this.apiService.post<<[entity]Result >(this.post[entityUrl, sdg); }
发送API请求
在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来自Angular 17.3.9版本)
后端 - API授权过滤器
后端API使用一个授权过滤器:
[Authorization filter class] : System.Attribute, Microsoft.AspNetCore.MVC.Filters.IAsyncResourceFilter { (…)
该过滤器尝试从包含所有当前用户数据的MemoryCache中识别用户(当前用户不足20个,但调试时访问速度很慢)。
(…) 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);
如果第一次获取失败,会以略微不同的方式重复该过程(此时userCacheEntries已赋值)。目前没有任何JWT令牌校验逻辑。
在前端,我注意到auth0-angular提供的AuthService类有一个idTokenClaims$属性:
this.auth.idTokenClaims$.subscribe( (value) => {console.log("idTokenClaims$", value);} );
这段代码会输出一个包含多个键的对象。我目前不确定每个属性的含义,但其中很可能包含我需要的令牌。
我需要找到将其加入请求的方法,但不确定后端预期的令牌值是什么,也不清楚如何进行校验。
内容的提问来源于stack exchange,提问作者TTT

