Hi,
The telemetry exit handlers keep disposed applications alive even after the telemetry task finishes. We've prepared a fix.
Please keep Abp:Telemetry:IsEnabled set to false in your test configuration for now.
Thanks
Hi,
When two navigation properties point to the same entity (IdentityUser here), ABP Suite generates the lookup only for the first one in UserMaintenances.razor.cs. The razor page still uses supervisorCollection, so the build fails.
For now, you can add the missing part to UserMaintenances.Extended.razor.cs. Suite doesn't overwrite this file when you regenerate the page.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AbpSolution1.Shared;
namespace AbpSolution1.Blazor.Pages;
public partial class UserMaintenances
{
private IReadOnlyList<LookupDto<Guid>> supervisorCollection { get; set; } = new List<LookupDto<Guid>>();
protected override async Task OnParametersSetAsync()
{
supervisorCollection = (await UserMaintenancesAppService.GetsupervisorLookupAsync(new LookupRequestDto())).Items;
}
}
The app service already has GetsupervisorLookupAsync, so nothing else needs to change.
This is fixed in ABP Suite 10.7.0. After you upgrade Suite and regenerate the page, remove this code from the extended file, because the generated file will have supervisorCollection itself.
Thanks
Hi,
Support questions are the best place to share suggestions like this. We check them first, and we refund the ticket for suggestions, so you don't need to worry about your ticket count.
Thanks
Hi,
The Account module has a setting for this. Go to Administration > Settings > Account > General and enable Prevent email enumeration.
After that, the Forgot Password page shows the same message for both registered and unknown emails:
If the provided e-mail address is registered in the system, we will send a password reset link. If you don't get an email within a few minutes, please check your spam box or try again later.
The /api/account/send-password-reset-code endpoint also returns the same response in both cases.
If you use multi-tenancy, this setting is saved per tenant. Enabling it on the host doesn't change the tenants, so each tenant admin needs to enable it on their own settings page. To enable it for the host and all tenants at once, add this to the appsettings.json of your AuthServer (or the web app if it's not tiered):
"Settings": {
"Abp.Account.PreventEmailEnumeration": "true"
}
If you want to use your own text, change the PreventEmailEnumerationPasswordResetMailSentMessage text of the AbpAccount resource in Administration > Language Management > Language Texts.
Thanks
Hi,
The dropdowns in CountryHdrs.razor use SAPServerCollection and NonSAPSystemCollection, but Suite declares them in CountryHdrs.razor.cs with the plural names (SAPServersCollection, NonSAPSystemsCollection).
You can add two properties to the CountryHdrs partial class in CountryHdrs.Extended.razor.cs. Keep the namespace and any code that's already in that file. Suite doesn't overwrite this file when you regenerate the entity:
using System;
using System.Collections.Generic;
using AbpSolution1.Shared;
namespace AbpSolution1.Blazor.Pages;
public partial class CountryHdrs
{
private IReadOnlyList<LookupDto<Guid>> SAPServerCollection => SAPServersCollection;
private IReadOnlyList<LookupDto<Guid>> NonSAPSystemCollection => NonSAPSystemsCollection;
}
This is fixed in ABP Suite 10.7.0. If you upgrade later and regenerate the entity, remove these two properties, otherwise the build will fail again. Your ticket has been refunded.
Thanks
Hi,
ABP doesn't have a BFF setup for Angular at the moment. The "BFF" in the microservice docs means one API gateway per client type, it doesn't keep the tokens on the server.
It's a reasonable request, and we'll create an internal task to look into it. It's a fairly big change, so it's hard to give you a timeline in the short term.
@abp/ng.core only defines the auth services, and provideAbpOAuth() from @abp/ng.oauth registers the implementations. For cookie auth, don't call provideAbpOAuth() and provide your own implementations for these tokens from @abp/ng.core:
AuthService: init, isAuthenticated, navigateToLogin and logout, which can redirect to the login and logout endpoints of your BFF.authGuard and asyncAuthGuard (and AuthGuard if you still use the class guard). They can check currentUser.isAuthenticated from ConfigStateService.ApiInterceptor, also registered as HTTP_INTERCEPTORS. Keep the Accept-Language, tenant and X-Requested-With headers and don't add the Authorization header. With X-Requested-With, the cookie handler returns 401 instead of redirecting to the login page.PIPE_TO_LOGIN_FN_KEY, CHECK_AUTHENTICATION_STATE_FN_KEY and AuthErrorFilterService.The 401 handler in @abp/ng.theme.shared refreshes the application configuration and calls AuthService.logout(), so it will use your implementation. /api/abp/application-configuration works as long as the BFF forwards it to the gateway with the access token. Since the browser then signs in with a cookie, the BFF also needs CSRF protection.
Login, the guards, the 401 handling and logout work this way when the Angular app is served from the same origin as the host that issues the cookie. We don't have a full BFF sample yet, so please take this as a starting point.
Thanks
Hi,
refresh_token in memory too. It is then shared between tabs through the same SharedWorker as the access token. Extend MemoryTokenStorageService like this:import { Injectable, inject } from '@angular/core';
import { AbpLocalStorageService } from '@abp/ng.core';
import { MemoryTokenStorageService } from '@abp/ng.oauth';
@Injectable({ providedIn: 'root' })
export class RefreshTokenInMemoryStorageService extends MemoryTokenStorageService {
constructor() {
super();
const storage = this as any;
storage.keysShouldStoreInMemory.push('refresh_token');
storage.port?.postMessage({ action: 'get', key: 'refresh_token' });
inject(AbpLocalStorageService).removeItem('refresh_token');
}
}
Then register it after provideAbpOAuth() in app.config.ts:
import { MemoryTokenStorageService, provideAbpOAuth } from '@abp/ng.oauth';
import { RefreshTokenInMemoryStorageService } from './refresh-token-in-memory-storage.service';
providers: [
// ...
provideAbpOAuth(),
{ provide: MemoryTokenStorageService, useExisting: RefreshTokenInMemoryStorageService },
// ...
]
A few notes:
OAuthStorage yourself (for example with localStorage), remove that. Otherwise this class is not used.data: URL, so your CSP needs to allow data: in worker-src. If the worker can't start, each tab keeps its own tokens.MemoryTokenStorageService, so treat it as a temporary workaround.removeItem call deletes the refresh token that older builds left in localStorage.PreConfigureServices of your AuthServer module:PreConfigure<OpenIddictServerBuilder>(builder =>
{
builder.SetRefreshTokenReuseLeeway(TimeSpan.FromSeconds(10));
});
A redeemed refresh token is then accepted again only within 10 seconds of its first redemption.
worker-src CSP.Thanks
Hi,
The Angular logout already calls the revocation endpoint, and the AuthServer deletes the user's session. The APIs then rely on dynamic claims to notice that the session is gone. In the microservice solution this check has two problems.
app.UseAuthorization() runs before app.UseDynamicClaims(). A controller that relies only on [Authorize] has already passed authorization when dynamic claims clears the user, so it still returns data. Move the existing app.UseAuthorization() call to right after the existing app.UseDynamicClaims() call, and keep all the other middleware calls where they are:app.UseDynamicClaims();
app.UseAuthorization();
After this change, such a controller rejects the revoked token with 403, not 401.
Add this handler to the AuthServer and the IdentityService (the apps that delete sessions):
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.EventBus;
using Volo.Abp.Identity;
using Volo.Abp.MultiTenancy;
namespace MyCompanyName.MyProjectName;
public class RevokedIdentitySessionHandler : ILocalEventHandler<EntityDeletedEventData<IdentitySession>>, ITransientDependency
{
protected IDistributedCache<RevokedIdentitySessionCacheItem> Cache { get; }
public RevokedIdentitySessionHandler(IDistributedCache<RevokedIdentitySessionCacheItem> cache)
{
Cache = cache;
}
public virtual async Task HandleEventAsync(EntityDeletedEventData<IdentitySession> eventData)
{
// Keep it longer than your maximum access token lifetime plus the clock skew.
await Cache.SetAsync(eventData.Entity.SessionId, new RevokedIdentitySessionCacheItem(), new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
}, hideErrors: false);
}
}
[Serializable]
[IgnoreMultiTenancy]
[CacheName("RevokedIdentitySession")]
public class RevokedIdentitySessionCacheItem
{
}
Add this contributor to each of your other API services, with the same RevokedIdentitySessionCacheItem class:
using System;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Volo.Abp.Caching;
using Volo.Abp.Security.Claims;
namespace MyCompanyName.MyProjectName;
public class RevokedIdentitySessionDynamicClaimsPrincipalContributor : AbpDynamicClaimsPrincipalContributorBase
{
public override async Task ContributeAsync(AbpClaimsPrincipalContributorContext context)
{
var sessionId = context.ClaimsPrincipal.Identities.FirstOrDefault()?.FindSessionId();
if (sessionId == null)
{
return;
}
var cache = context.GetRequiredService<IDistributedCache<RevokedIdentitySessionCacheItem>>();
try
{
if (await cache.GetAsync(sessionId, hideErrors: false) != null)
{
context.ClaimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
}
}
catch (Exception e)
{
context.GetRequiredService<ILogger<RevokedIdentitySessionDynamicClaimsPrincipalContributor>>()
.LogError(e, "Could not check the revoked session cache.");
context.ClaimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
}
}
}
This needs dynamic claims enabled in these services (IsDynamicClaimsEnabled = true, which the template sets), and all apps need to use the same Redis database and the same AbpDistributedCacheOptions.KeyPrefix. It only covers sessions deleted after you deploy it. You don't need reference tokens or introspection for this workaround.
We'll fix both in the framework and the microservice template.
Thanks
Hi,
Thanks for the suggestion. The Chat module doesn't have this today, and the avatar is rendered inside the Chat page, so there is no smaller extension point to override right now.
We'll analyze this request: show the profile picture when the user has one, keep the initials as the fallback, and make it optional.
Thanks