I know the basic principles of web app :) After all I see that there's no magic place to place this code. So I will need to use "if" guard condition inside a middleware checking if the request is authorized, so the permission definition provider can be added (once). I'm not closing the ticket yet: could be another questions coming along the way.
So the only place I can place the check is a middleware - meaning on each request "if [user is authenticated]"? I wanted to avoid this, but if there's no other place which is executed only once, I have no choice.
Sorry, it's not what I meant. I mean what is a suitable ABP code (handler, contributor, service method) which is executed once after the user has been successfully authenticated and which is not a part of Identity Server project?
Our solution is ALREADY a very customized (and we have not used dynamic permissions so far, I don't know what would turn out from this), so I am not afraid to experiment with static permissions. But anyway, I'd like to know the place where I can assign my per-tenant permissions. It needs to be a part of solution and invoked once after the user has been authorized. It must not happen in Identity Server project. As I see it, using a middleware is a bad choice, because the middleware Invoke method is get invoked on each request. Instead, I need to make it only once.
I have already overridden StaticPermissionDefinitionStore
(for other task), so I can extend it with some new functionality.
But I need to understand how should I implement the task.
The aim is the following: there's configurable parameter in the database which can be different depending on the tenant and other things and in fact it determines the list of permissions.
So I need to read this parameter once the user is authenticated and his current tenant is known.
After this, I need to add just read permissions to the rest of permissions (and be able to assign them to roles as usual).
I know there are dynamic permissions in ABP, but I'd prefer not to make use of them.
What I have noticed:
then logging out from any page brings me to site Login dialog. Which looks like if the user IS logged out:
If I click "Login" - I am brought to Identity Server Login page (note: I am not automatically logged in!)
However, using AuthGuard
for a default route ('') causes this behavior I've described in the very beginning. Though, I don't want to go to the first login form, so this "workaround" does not suit me.
I also took a look at Identity Server log (this is the fragment of what is happening after pressing "Logout") and the following looks weird to me: why the authorize request is sent straight after successful token revocation? The configuration of Identity Server looks usual to me, we have not changed it, but probably some configuration aspects are missing?
2023-09-18 01:10:05.334 -05:00 [DBG] Refresh token revoked 2023-09-18 01:10:06.116 -05:00 [INF] Token revocation complete 2023-09-18 01:10:06.116 -05:00 [INF] {"ClientId":"XXX","ClientName":"XXX","TokenType":"refresh_token","Token":"****6492","Category":"Token","Name":"Token Revoked Success","EventType":"Success","Id":2010,"Message":null,"ActivityId":"800001b8-0004-f900-b63f-84710c7967bb","TimeStamp":"2023-09-18T06:10:06.0000000Z","ProcessId":24860,"LocalIpAddress":"::1:44357","RemoteIpAddress":"::1","$type":"TokenRevokedSuccessEvent"} 2023-09-18 01:10:06.707 -05:00 [INF] Request finished HTTP/1.1 POST https://localhost:44357/connect/revocation application/x-www-form-urlencoded 149 - 200 - - 5005.9323ms 2023-09-18 01:10:06.808 -05:00 [INF] Request starting HTTP/1.1 GET https://localhost:44357/connect/authorize?response_type=code&client_id=XXX&state=YYY&redirect_uri=https%3A%2F%2Flocalhost%3A4200&scope=ZZZ&code_challenge=MMM&code_challenge_method=S256&nonce=NNN&culture=en&ui-culture=en - - 2023-09-18 01:10:06.840 -05:00 [DBG] Request path /connect/authorize matched to endpoint type Authorize 2023-09-18 01:10:06.859 -05:00 [DBG] Endpoint enabled: Authorize, successfully created handler: IdentityServer4.Endpoints.AuthorizeEndpoint 2023-09-18 01:10:06.859 -05:00 [INF] Invoking IdentityServer endpoint: IdentityServer4.Endpoints.AuthorizeEndpoint for /connect/authorize 2023-09-18 01:10:06.862 -05:00 [DBG] Start authorize request
There is a lot of information here - cut - but finally there is also the error (which does not prevent user logging in):
2023-09-18 01:10:06.926 -05:00 [DBG] client configuration validation for client XXX succeeded. 2023-09-18 01:10:06.926 -05:00 [DBG] Checking for PKCE parameters 2023-09-18 01:10:07.021 -05:00 [INF] {"Details":"System.Threading.Tasks.TaskCanceledException: A task was canceled.\r\n at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenInternalAsync(Boolean errorsExpected, CancellationToken cancellationToken)\r\n at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenInternalAsync(Boolean errorsExpected, CancellationToken cancellationToken)\r\n at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenAsync(CancellationToken cancellationToken, Boolean errorsExpected)\r\n at Oracle.EntityFrameworkCore.Storage.Internal.OracleRelationalCommandBuilderFactory.OracleRelationalCommandBuilder.OracleRelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)\r\n at Microsoft.EntityFrameworkCore.Query.Internal.SplitQueryingEnumerable
1.AsyncEnumerator.InitializeReaderAsync(AsyncEnumerator enumerator, CancellationToken cancellationToken)\r\n at Oracle.EntityFrameworkCore.Storage.Internal.OracleExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func
4 operation, Func4 verifySucceeded, CancellationToken cancellationToken)\r\n at Microsoft.EntityFrameworkCore.Query.Internal.SplitQueryingEnumerable
1.AsyncEnumerator.MoveNextAsync()\r\n at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable1 source, CancellationToken cancellationToken)\r\n at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable
1 source, CancellationToken cancellationToken)\r\n at Volo.Abp.IdentityServer.IdentityResources.IdentityResourceRepository.GetListByScopeNameAsync(String[] scopeNames, Boolean includeDetails, CancellationToken cancellationToken)\r\n at Castle.DynamicProxy.AsyncInterceptorBase.ProceedAsynchronous[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo)\r\n at Volo.Abp.Castle.DynamicProxy.CastleAbpMethodInvocationAdapterWithReturnValue1.ProceedAsync()\r\n at Volo.Abp.Uow.UnitOfWorkInterceptor.InterceptAsync(IAbpMethodInvocation invocation)\r\n at Volo.Abp.Castle.DynamicProxy.CastleAsyncAbpInterceptorAdapter
1.InterceptAsync[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo, Func3 proceed)\r\n at Volo.Abp.IdentityServer.ResourceStore.<FindIdentityResourcesByScopeNameAsync>b__34_0(String[] keys)\r\n at Volo.Abp.IdentityServer.ResourceStore.GetCacheItemsAsync[TEntity,TModel](IDistributedCache
1 cache, IEnumerable1 keys, Func
2 entityFactory, Func3 cacheItemsFactory, String cacheKeyPrefix)\r\n at Volo.Abp.IdentityServer.ResourceStore.FindIdentityResourcesByScopeNameAsync(IEnumerable
1 scopeNames)\r\n at IdentityServer4.Stores.IResourceStoreExtensions.FindResourcesByScopeAsync(IResourceStore store, IEnumerable1 scopeNames)\r\n at IdentityServer4.Stores.IResourceStoreExtensions.FindEnabledResourcesByScopeAsync(IResourceStore store, IEnumerable
1 scopeNames)\r\n at IdentityServer4.Validation.DefaultResourceValidator.ValidateRequestedResourcesAsync(ResourceValidationRequest request)\r\n at IdentityServer4.Validation.AuthorizeRequestValidator.ValidateScopeAsync(ValidatedAuthorizeRequest request)\r\n at IdentityServer4.Validation.AuthorizeRequestValidator.ValidateAsync(NameValueCollection parameters, ClaimsPrincipal subject)\r\n at IdentityServer4.Endpoints.AuthorizeEndpointBase.ProcessAuthorizeRequestAsync(NameValueCollection parameters, ClaimsPrincipal user, ConsentResponse consent)\r\n at IdentityServer4.Endpoints.AuthorizeEndpoint.ProcessAsync(HttpContext context)\r\n at IdentityServer4.Hosting.IdentityServerMiddleware.Invoke(HttpContext context, IEndpointRouter router, IUserSession session, IEventService events, IBackChannelLogoutService backChannelLogoutService)","Category":"Error","Name":"Unhandled Exception","EventType":"Error","Id":3000,"Message":"A task was canceled.","ActivityId":"800000d9-0007-ff00-b63f-84710c7967bb","TimeStamp":"2023-09-18T06:10:07.0000000Z","ProcessId":24860,"LocalIpAddress":"::1:44357","RemoteIpAddress":"::1","$type":"UnhandledExceptionEvent"} 2023-09-18 01:10:07.021 -05:00 [INF] {"Details":"System.Threading.Tasks.TaskCanceledException: A task was canceled.\r\n at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenInternalAsync(Boolean errorsExpected, CancellationToken cancellationToken)\r\n at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenInternalAsync(Boolean errorsExpected, CancellationToken cancellationToken)\r\n at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenAsync(CancellationToken cancellationToken, Boolean errorsExpected)\r\n at Oracle.EntityFrameworkCore.Storage.Internal.OracleRelationalCommandBuilderFactory.OracleRelationalCommandBuilder.OracleRelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)\r\n at Microsoft.EntityFrameworkCore.Query.Internal.SplitQueryingEnumerable1.AsyncEnumerator.InitializeReaderAsync(AsyncEnumerator enumerator, CancellationToken cancellationToken)\r\n at Oracle.EntityFrameworkCore.Storage.Internal.OracleExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func
4 operation, Func4 verifySucceeded, CancellationToken cancellationToken)\r\n at Microsoft.EntityFrameworkCore.Query.Internal.SplitQueryingEnumerable
1.AsyncEnumerator.MoveNextAsync()\r\n at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable1 source, CancellationToken cancellationToken)\r\n at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable
1 source, CancellationToken cancellationToken)\r\n at Volo.Abp.IdentityServer.IdentityResources.IdentityResourceRepository.GetListByScopeNameAsync(String[] scopeNames, Boolean includeDetails, CancellationToken cancellationToken)\r\n at Castle.DynamicProxy.AsyncInterceptorBase.ProceedAsynchronous[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo)\r\n at Volo.Abp.Castle.DynamicProxy.CastleAbpMethodInvocationAdapterWithReturnValue1.ProceedAsync()\r\n at Volo.Abp.Uow.UnitOfWorkInterceptor.InterceptAsync(IAbpMethodInvocation invocation)\r\n at Volo.Abp.Castle.DynamicProxy.CastleAsyncAbpInterceptorAdapter
1.InterceptAsync[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo, Func3 proceed)\r\n at Volo.Abp.IdentityServer.ResourceStore.<FindIdentityResourcesByScopeNameAsync>b__34_0(String[] keys)\r\n at Volo.Abp.IdentityServer.ResourceStore.GetCacheItemsAsync[TEntity,TModel](IDistributedCache
1 cache, IEnumerable1 keys, Func
2 entityFactory, Func3 cacheItemsFactory, String cacheKeyPrefix)\r\n at Volo.Abp.IdentityServer.ResourceStore.FindIdentityResourcesByScopeNameAsync(IEnumerable
1 scopeNames)\r\n at IdentityServer4.Stores.IResourceStoreExtensions.FindResourcesByScopeAsync(IResourceStore store, IEnumerable1 scopeNames)\r\n at IdentityServer4.Stores.IResourceStoreExtensions.FindEnabledResourcesByScopeAsync(IResourceStore store, IEnumerable
1 scopeNames)\r\n at IdentityServer4.Validation.DefaultResourceValidator.ValidateRequestedResourcesAsync(ResourceValidationRequest request)\r\n at IdentityServer4.Validation.AuthorizeRequestValidator.ValidateScopeAsync(ValidatedAuthorizeRequest request)\r\n at IdentityServer4.Validation.AuthorizeRequestValidator.ValidateAsync(NameValueCollection parameters, ClaimsPrincipal subject)\r\n at IdentityServer4.Endpoints.AuthorizeEndpointBase.ProcessAuthorizeRequestAsync(NameValueCollection parameters, ClaimsPrincipal user, ConsentResponse consent)\r\n at IdentityServer4.Endpoints.AuthorizeEndpoint.ProcessAsync(HttpContext context)\r\n at IdentityServer4.Hosting.IdentityServerMiddleware.Invoke(HttpContext context, IEndpointRouter router, IUserSession session, IEventService events, IBackChannelLogoutService backChannelLogoutService)","Category":"Error","Name":"Unhandled Exception","EventType":"Error","Id":3000,"Message":"A task was canceled.","ActivityId":"800001e1-0004-f500-b63f-84710c7967bb","TimeStamp":"2023-09-18T06:10:07.0000000Z","ProcessId":24860,"LocalIpAddress":"::1:44357","RemoteIpAddress":"::1","$type":"UnhandledExceptionEvent"} 2023-09-18 01:10:07.021 -05:00 [FTL] Unhandled exception: A task was canceled.
I do not have ngOnInit override from component's base class. So I put the breakpoint in the base class ngOnInit method. And this method is only invoked when the user is authenticated.
currently app.component.ts is the bootstraped component https://angular.io/guide/bootstrapping so if you place any api call in there it will be called so you have to use conditional operator there to calling api when the state is not authenticated.
Sorry, but i didn't get it. I have a lot of different components like the one below - with a bunch of API calls triggered based on different conditions - button clicks, subscriptions, route parameter values, etc.:
@Component({ ... })
export class ComponentHHH extends BaseComponent {
//Condition method can be invoked based on various conditions in unknown moment of time
ConditionMethodAAA() {
....
this.apiServiceXXX.apiMethodYYY(...).pipe(takeUntil(this.unsubscriber$))
.subscribe((response) => { ... });
....
this.apiServiceZZZ.apiMethodAAA(...).pipe(takeUntil(this.unsubscriber$))
.subscribe((response) => { ... });
....
}
ConditionMethodBBB() {
....
this.apiServiceDDD.apiMethodSSS(...).pipe(takeUntil(this.unsubscriber$))
.subscribe((response) => { ... });
....
}
}
If I activate another component in the menu - this.unsubscriber$ (property in "BaseComponent" class) receives null and completes. Due to this and to using "takeUntil", no matter which API calls were active, they all will be removed. I've tried to subcribe to "logout" event in "BaseComponent" and do the same I do with this.unsubscriber$ in "ngOnDestroy". However this does not work - API calls keep being invoked. Even more now: logout works only from Home page (probably these two things are unrelated, but I am not sure). I have not found explanation to this.
So you suggest to summon "app.component.ts" to resolve the problem. But I cannot figure out, how it can help here: there is no connection between AppComponent and another components API calls.
Hi Anjali.
First of all, ngOnDestroy
with the suggested code already present in the base class HomeComponent
is inherited from. Nevertheless, I've added ngOnDestroy
here for test, too.
Also I've replace this.oAuthService.events
with your suggestion.
After pressing "Logout" button I'm getting "Logout event received" message. But API call is STILL invoked as always. There has to be explanation WHY it is called though.
P.S. Please hold on - I want to check something...
UPDATE: I made some experiments.
Good news:
In the given specific case the API call I mentioned before was also invoked by other service - this was the reason it "resurrected" all the time after logout;
Bad news:
In a common case, only using if (this.authService.isAuthenticated)
check or similar like you suggested helps resolving the initial problem. However, it is not a good approach: we have hundreds of API calls from hundreds of components. It is not a way to go to add hundreds of "if". Instead, it is supposed that for both component destroying and user logout action unsubscriber$
needs to received proper null
value and be completed
:
this.apiService
.apiCall(...)
.pipe(
finalize(() => (...)),
takeUntil(this.unsubscriber$) //this has to be enough for all cases
)
.subscribe((...) => {
...
});
The problem is that for some reason using logout
event does not help to resolve this issue. I use this in Component base class (no matter, "tap" or "subscribe" actually) and this block DOES get triggered, however eventually API is still invoked for unknown for me reason:
ngOnInit(): void {
this.oAuthService.events
.pipe
(
filter(event => event?.type === 'logout'),
takeUntil(this.unsubscriber$),
tap(() => {
this.unsubscriber$.next(null);
this.unsubscriber$.complete();
})
)
.subscribe();
I've tried to make unsubscriber$
as ReplaySubject
and create its new with buffer == 1, but it does not change the things.
I am confused even more now: it appeared that logging out works only for Home page: if I take any page - even the ABP page - after clicking "logout" button the user is eventually redirected to Home page again without landing at Identity Server Login page:
and then - all the rest our services required for the page are loaded as usual...
And this is workflow when I click "Logout" on Home page:
Afterwards I'm redirected to Identity Server Login page as expected. All the relevant pages have canActivate: [AuthGuard]
It's absolutely strange given that "Logout" functionality is the same for all pages, i.e. the same code has to be invoked. Do you have any clues? If required, I will create a new ticket for this issue...