Hi,
Both. They aren't alternatives, each one fixes a different project, in this order:
context.Request.Scheme = "https", after app.UseForwardedHeaders() and before app.UseAuthentication(). Once deployed, jwks_uri in /.well-known/openid-configuration has to start with https.options.MapInboundClaims = false, and for now set options.TokenValidationParameters.ValidateAudience = false.currentUser in application-configuration.If isAuthenticated is true after that, add the missing resources to OpenIddictScopes as described in the second reply, then turn audience validation back on. If it's still false, send the new identitymodel logs from both projects.
Yaduraj said on Friday he was updating the code, so part of this may already be in place on his side.
Thanks
Hi,
A confirmed email is enough to enable two-factor for a user, no email sender is needed for that. The error comes from the service that hosts the Identity APIs. In ABP v10.6 and earlier, the email and phone two-factor token providers are only registered by AbpIdentityAspNetCoreModule, and the IdentityService of the microservice template doesn't depend on it, so it finds no provider for the user.
You can fix it for now in your IdentityService project. Add the package (same version as your other ABP packages):
<PackageReference Include="Volo.Abp.Identity.AspNetCore" Version="<your-abp-version>" />
Then depend on the module and turn off its cookie authentication setup, since the service uses JWT bearer:
using Volo.Abp.Identity.AspNetCore;
[DependsOn(
//...
typeof(AbpIdentityAspNetCoreModule)
)]
public class MyProjectNameIdentityServiceModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<AbpIdentityAspNetCoreOptions>(options =>
{
options.ConfigureAuthentication = false;
});
//...
}
}
After restarting the service, the Two factor action works for users with a confirmed email.
In v10.7 these token providers are registered in the domain layer, so this workaround won't be needed after upgrading: https://github.com/abpframework/abp/pull/26113
If your solution isn't a microservice one, please share the solution type and your ABP version.
Your ticket has been refunded.
Thanks
Hi,
Thanks for the latest file. Two things, otherwise the scheme change alone won't get you there.
The access token carries no aud claim, while the gateway sets options.Audience from AuthServer:ApiResource. Audience validation will reject it as soon as the signing keys are reachable again.
To confirm that quickly, turn the check off for now in the JwtBearer scheme:
options.TokenValidationParameters.ValidateAudience = false;
If login works with that, the missing aud is what's left. The real fix is on the AuthServer side: ABP fills aud from the resources attached to the granted scopes, so the rows in OpenIddictScopes currently have none. The OpenIddict tables do matter here, contrary to what I said earlier.
select "Name", "Resources" from "OpenIddictScopes";
Resources of the Litmus scope has to contain the API resource name, the way the seeder creates it:
await _scopeManager.CreateAsync(new OpenIddictScopeDescriptor {
Name = "Litmus", DisplayName = "Litmus API", Resources = { "Litmus" }
});
Also take options.MapInboundClaims = false back out. On the API side ABP reads the user id through AbpClaimTypes.UserId, and that stays ClaimTypes.NameIdentifier unless an OpenIddict module runs in the same process, which your gateway doesn't have. The default mapping is what turns sub into that claim, so switching it off leaves currentUser empty. AddAbpJwtBearer keeps the default for this reason, and it also explains why FindFirst("sub") in your new logging comes back null.
Thanks
Hi,
Thanks for the logs, they pinned it down. The gateway reads the discovery document over https, but the jwks_uri inside it is http://api-product.dev.tasconnect.net/.well-known/jwks, and port 80 refuses the connection. With no keys the gateway has no configuration to validate against, which is the IDX10204 you see, and the request ends up anonymous.
OpenIddict builds the endpoint URLs of that document from the scheme of the incoming request, and the identity task receives plain HTTP once the gateway terminates TLS. SetIssuer only fixes the issuer field, not those URLs.
In the identity host, right after app.UseForwardedHeaders() and before app.UseAuthentication():
app.Use(async (context, next) =>
{
if (context.Request.Path.StartsWithSegments("/.well-known"))
{
context.RequestServices
.GetRequiredService<ILogger<LitmusIdentityServerModule>>()
.LogWarning("Scheme: {Scheme}, Host: {Host}, X-Forwarded-Proto: {Proto}, X-Forwarded-Host: {ForwardedHost}",
context.Request.Scheme,
context.Request.Host.Value,
context.Request.Headers["X-Forwarded-Proto"].ToString(),
context.Request.Headers["X-Forwarded-Host"].ToString());
}
context.Request.Scheme = "https";
await next();
});
Once it's deployed, https://api-product.dev.tasconnect.net/.well-known/openid-configuration should list jwks_uri with https.
If it still fails, send the new identitymodel logs from both projects together with those Scheme / X-Forwarded-Proto lines. They tell us what the gateway really forwards, and we go from there.
Thanks
Hi,
Good to hear the identity service is up.
In ABP, currentUser.isAuthenticated is Id.HasValue, so false only tells us the API ended up with no claim matching AbpClaimTypes.UserId on the principal. That covers two different things: the token not being accepted at all, or the token being fine while the user id claim sits under a different claim type than the API expects.
To tell those apart, send me:
ConfigureAuthenticationSetup.ConfigureAuthenticationidentitymodel log, both for one application-configuration call made with that tokenBoth logs are described here: https://abp.io/support/questions/8622/How-to-enable-Debug-logs-for-troubleshoot-problems
The identitymodel one is what shows why a token gets refused. In the API host's Program.cs:
using System.Diagnostics.Tracing;
using Microsoft.IdentityModel.Logging;
IdentityModelEventSource.ShowPII = true;
IdentityModelEventSource.Logger.LogLevel = EventLevel.Verbose;
var wilsonTextLogger = new TextWriterEventListener("Logs/identitymodel.txt");
wilsonTextLogger.EnableEvents(IdentityModelEventSource.Logger, EventLevel.Verbose);
Thanks
Hi,
Please share the details below before the call. Without them I'd still be working through the same things during the meeting.
Two changes came out of this thread last week, both for the identity service:
Program.cs: await AddApplicationAsync and InitializeApplicationAsync, with an async Main. Yaduraj applied this one.dotnet publish output rather than the dotnet build output. No result reported on this one yet.Where does each stand now? Then, from the build that's running, search the identity task's startup log for this line:
Initialized all ABP modules.
If it's there, the ABP pipeline is up and the login problem is elsewhere. If it isn't, the app still isn't initializing, and the lines around it tell me why. Send me that part of the log, unfiltered.
Also:
/.well-known/openid-configuration and /connect/token, status code and bodyThanks
Hi,
That's a bug on our side. UpdateUserRolesBasedOnOrganizationUnits shouldn't write back to input, and we're changing it to a local variable exactly the way you suggested.
Until that ships, you can override the app service and put the caller's original value back after the base call:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Identity;
using Volo.Abp.Identity.Emailing;
using Volo.Abp.Identity.UserInvitations;
using Volo.Abp.Identity.UserSharing;
using Volo.Abp.Threading;
namespace MyCompanyName.MyProjectName.Identity;
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IIdentityUserAppService))]
public class MyIdentityUserAppService : IdentityUserAppService
{
public MyIdentityUserAppService(
IdentityUserManager userManager,
IIdentityUserRepository userRepository,
IIdentityRoleRepository roleRepository,
IOrganizationUnitRepository organizationUnitRepository,
IIdentityClaimTypeRepository identityClaimTypeRepository,
IdentityProTwoFactorManager identityProTwoFactorManager,
IOptions<IdentityOptions> identityOptions,
IDistributedEventBus distributedEventBus,
IOptions<AbpIdentityOptions> abpIdentityOptions,
IPermissionChecker permissionChecker,
IDistributedCache<IdentityUserDownloadTokenCacheItem, string> downloadTokenCache,
IDistributedCache<ImportInvalidUsersCacheItem, string> importInvalidUsersCache,
IdentitySessionManager identitySessionManager,
IdentityUserTwoFactorChecker identityUserTwoFactorChecker,
ICancellationTokenProvider cancellationTokenProvider,
UserSharingManager userSharingManager,
UserInvitationManager userInvitationManager,
IIdentityUserInvitationRepository userInvitationRepository,
IIdentityEmailSender identityEmailSender)
: base(
userManager,
userRepository,
roleRepository,
organizationUnitRepository,
identityClaimTypeRepository,
identityProTwoFactorManager,
identityOptions,
distributedEventBus,
abpIdentityOptions,
permissionChecker,
downloadTokenCache,
importInvalidUsersCache,
identitySessionManager,
identityUserTwoFactorChecker,
cancellationTokenProvider,
userSharingManager,
userInvitationManager,
userInvitationRepository,
identityEmailSender)
{
}
protected override async Task UpdateUserRolesBasedOnOrganizationUnits(IdentityUser user, IdentityUserCreateOrUpdateDtoBase input)
{
var organizationUnitIds = input.OrganizationUnitIds;
await base.UpdateUserRolesBasedOnOrganizationUnits(user, input);
input.OrganizationUnitIds = organizationUnitIds;
}
}
Drop it into your *.Application project. That constructor is the 10.4 one and it does change between versions, so re-check it if you upgrade before the fix is out.
If you'd rather not replace the service, sending the current ids from the caller works too:
input.OrganizationUnitIds = (await _identityUserAppService.GetOrganizationUnitsAsync(id))
.Select(x => x.Id)
.ToArray();
await _identityUserAppService.UpdateAsync(id, input);
The fix will be in v10.7.0. Your ticket has been refunded.
Thanks
Hi,
ABP doesn't have an undo/redo feature, and we haven't written anything on this topic.
I wouldn't build it on EntityChange / EntityPropertyChange. A few things make it unusable as a restore source:
HideErrors is true by default, so a failed audit save doesn't fail the business transaction. Your restore data can be silently missing.Deleted entity change with zero property changes, so the values from before the delete are not there.SaveChanges, and EF marks cascade deleted children as deleted during SaveChanges.EF change tracking doesn't work for this either. SaveChanges uses acceptAllChangesOnSuccess: true, so when it returns the entries are back to Unchanged and the original values have been replaced by the current ones. That's also why ABP reads the tracker right before calling SaveChanges. Caching the entries doesn't help, the DbContext is gone at the end of the unit of work anyway.
What works is a separate implementation in your own application layer, with the aggregate root as the unit of undo, not the entity and not the property:
public class UndoOperation : AggregateRoot<Guid>
{
public Guid? UserId { get; set; }
public string AggregateName { get; set; }
public string AggregateId { get; set; }
public string BeforeJson { get; set; }
public string AfterJson { get; set; }
public bool IsUndone { get; set; }
public DateTime Time { get; set; }
protected UndoOperation()
{
}
public UndoOperation(Guid id)
: base(id)
{
}
}
public class UndoRecorder : ITransientDependency
{
private readonly IRepository<UndoOperation, Guid> _undoOperationRepository;
private readonly IJsonSerializer _jsonSerializer;
private readonly ICurrentUser _currentUser;
private readonly IGuidGenerator _guidGenerator;
private readonly IClock _clock;
public UndoRecorder(
IRepository<UndoOperation, Guid> undoOperationRepository,
IJsonSerializer jsonSerializer,
ICurrentUser currentUser,
IGuidGenerator guidGenerator,
IClock clock)
{
_undoOperationRepository = undoOperationRepository;
_jsonSerializer = jsonSerializer;
_currentUser = currentUser;
_guidGenerator = guidGenerator;
_clock = clock;
}
public string Snapshot<TAggregateRoot>(TAggregateRoot aggregateRoot)
where TAggregateRoot : class
{
return aggregateRoot == null ? null : _jsonSerializer.Serialize(aggregateRoot);
}
public async Task RecordAsync(string aggregateName, string aggregateId, string before, string after)
{
await _undoOperationRepository.InsertAsync(
new UndoOperation(_guidGenerator.Create())
{
UserId = _currentUser.Id,
AggregateName = aggregateName,
AggregateId = aggregateId,
BeforeJson = before,
AfterJson = after,
Time = _clock.Now
},
autoSave: false
);
}
}
Then put the restore logic on the aggregate itself and record in your mutating methods:
public class Book : FullAuditedAggregateRoot<Guid>
{
public string Name { get; set; }
public List<Chapter> Chapters { get; set; }
public void RestoreFrom(Book snapshot)
{
Name = snapshot.Name;
IsDeleted = false;
DeletionTime = null;
DeleterId = null;
Chapters.RemoveAll(c => snapshot.Chapters.All(s => s.Id != c.Id));
foreach (var snapshotChapter in snapshot.Chapters)
{
var chapter = Chapters.FirstOrDefault(c => c.Id == snapshotChapter.Id);
if (chapter == null)
{
Chapters.Add(snapshotChapter);
}
else
{
chapter.Title = snapshotChapter.Title;
}
}
}
}
public class BookAppService : ApplicationService
{
// ... constructor injection of the repositories, UndoRecorder, IJsonSerializer and IDataFilter
[UnitOfWork(isTransactional: true)]
public async Task UpdateAsync(Guid id, UpdateBookDto input)
{
var book = await _bookRepository.GetAsync(id, includeDetails: true);
var before = _undoRecorder.Snapshot(book);
book.Name = input.Name;
await _bookRepository.UpdateAsync(book);
await CurrentUnitOfWork.SaveChangesAsync();
await _undoRecorder.RecordAsync(nameof(Book), id.ToString(), before, _undoRecorder.Snapshot(book));
}
[UnitOfWork(isTransactional: true)]
public async Task DeleteAsync(Guid id)
{
var book = await _bookRepository.GetAsync(id, includeDetails: true);
var before = _undoRecorder.Snapshot(book);
await _bookRepository.DeleteAsync(book);
await CurrentUnitOfWork.SaveChangesAsync();
await _undoRecorder.RecordAsync(nameof(Book), id.ToString(), before, null);
}
[UnitOfWork(isTransactional: true)]
public async Task UndoAsync(Guid operationId)
{
var operation = await _undoOperationRepository.GetAsync(operationId);
var snapshot = _jsonSerializer.Deserialize<Book>(operation.BeforeJson);
using (_dataFilter.Disable<ISoftDelete>())
{
var book = await _bookRepository.FindAsync(snapshot.Id, includeDetails: true);
if (book == null)
{
await _bookRepository.InsertAsync(snapshot);
}
else
{
book.RestoreFrom(snapshot);
await _bookRepository.UpdateAsync(book);
}
}
operation.IsUndone = true;
await _undoOperationRepository.UpdateAsync(operation);
}
}
Redo is the same method with AfterJson instead of BeforeJson.
On the three points you asked about:
includeDetails: true. RestoreFrom is where you reconcile the child collection by id, and that part is domain specific, there is no generic version of it.Two things worth planning for from the start: store the concurrency stamp (or a hash) of the current state with the operation and refuse the undo when it no longer matches, otherwise you silently overwrite what another user did in the meantime. And put a version number on the snapshot payload, because renaming or removing a property later makes the old JSON unusable.
Thanks