Hello,
Here is the situation I’ve inherited and need to resolve.
We have now refactored to use the Auth0 angular library. In our token we populate through a rule two additional properties with data from the user app_metadata. This is added to both the accessToken and the idToken.
If something in these properties changes we post those changes using the management API from a server application. Then we trigger our client SPA through an event over a socket to do a silent authentication with ignoreCache set to true. We have rotating refresh tokens enabled if that matters.
this.authService.getAccessTokenSilently({ ignoreCache: true })
Now, to my question. Can I rely on that the new tokens that I get back in my SPA have run the rule I mentioned above and that it contains the most recent app_metadata that I posted over the management API? or is there a timing issue in this design? Some eventual consistency issue that I am unaware of and have to account for?
Regards
Hi @gandersson,
Welcome to the Auth0 Community!
There shouldn’t be an issue with eventual consistency as long as you are waiting for the management API request to resolve before requesting a new token. Are you seeing something unexpected?
Hello @dan.woda,
Thank you, then I know that to be safe. As a matter of fact I do have a little bit of a problem. When using the angular auth0 library I have created an rxjs chain like this:
const stream$ = this.authService.getAccessTokenSilently({ ignoreCache: true }).pipe(
switchMap(_ => {
...perform()
}));
For me it is critical that when I execute perform that the access token has been updated. The perform() will trigger an ngrx action that will through an effect call a backend server that relies on that the access token has been updated with the most recent data because the backend uses the properties we have added to the token to authorize access when the user is authenticated.
The above works… most of the time but sometimes it doesn’t. The access token has not been updated when perform() is executed. As I get the har files from the browsers I can see that when it works I see that the auth0 service was contacted with a request and I got a new token. When it doesn’t there seems to be no /token request at all being sent from the angular auth0 library and currently I don’t understand why. Because I am always sending ignoreCache: true.
Regards
@gandersson,
It sounds like the issue here lies in the getAccessTokenSilently call and whether or not the token is pulled from the cache or a new token is requested.
Could you DM me a HAR of what you are experiencing?
Hello again,
I don’t think a HAR file gives any information since no request is made at all. I have however found a workaround. If I just call the authservice with ignoreCache: true as above in part of a stream with switchmap it only works once and then never again for some reason. All the other invokations returned a cached token.
My workaround currently is if I instead of doing this:
const stream$ = this.authService.getAccessTokenSilently({ ignoreCache: true }).pipe(
switchMap(_ => {
...perform()
}));
I do this:
doIt() {
const sub = new Subject();
this.subscriptions.push(
this.authService
.getAccessTokenSilently({ ignoreCache: true })
.pipe(
switchMap(_ => this.authService.user$),
take(1),
)
.subscribe(() => sub.next()),
);
return sub;
}
const stream$ = this.doIt().pipe(
switchMap(_ => {
...perform()
}));
Then this works reliably every single time. I honestly can’t explain why but the idea popped into my head so I must have some thought behind this. I haven’t reached quite that level of rxjs/promise mastery yet to explain why this works… but I will probably make a utility of this so that every time I want to refresh a token as part of an active stream I use doIt().
I ended up digging down to singlePromiseMap of the promise util because I suspected the key that the singlePromise creates. It just contains (${this.options.client_id}::${getTokenOptions.audience}::${getTokenOptions.scope}
), so I was thinking that something else in the chain may already have set the key for the singlePromiseMap so when the ignoreCache: true comes along it was ignored because it already had something on that promise map for the key, which was a promise without that specific option.
That is just pure guesstimation from my part though.
I have played around with this some more. Now I can just do this:
const stream$ = this.authService.getAccessTokenSilently({ ignoreCache: true }).pipe(
take(1),
switchMap(_ => {
...perform()
}));
If I place the take(1) anywhere but exactly after the call to
getAccessTokenSilently({ ignoreCache: true })
then I get a cached token. Because this was the actual code:
const stream$ = this.authService.getAccessTokenSilently({ ignoreCache: true }).pipe(
switchMap(_ => {
...perform1() <-- http request hitting auth interceptor
}),
switchMap(a => {
...perform2(a) <-- http request hitting auth interceptor
}),
switchMap(b => {
...perform3(b) <-- http request hitting auth interceptor
}),
take(1));
When trying to debug with sourcemaps I see that the auth interceptor runs multiple times in my observable chain as it calls different services with the cached token like it should so I still suspect something collides here if I don’t have the take(1) exactly where I have it now. That is all I know right now. take(1) right after the call to getAccessTokenSilently makes my solution not use the cache and work reliably.
I seem to have spoken too soon. It still fails sometimes and no request is sent to auth0 at all. I don’t get this. I really don’t. So the take(1) did not matter either. Haha
Maybe it is comforting as well but the doIt() solution does not work either. I have a test that runs 25 times in succession and half of them don’t get a new token for some reason. No request is even made to auth0.
If I just use a button and call the function over and over it seems to work. I guess I have to debug even further if there aren’t any insights available if there could be something in the auth0 library that could cause a situation like this.
Okay, this is interesting. I did another change:
const stream$ = of().pipe(
switchMap(_ => {
this.authService
.getAccessTokenSilently({ ignoreCache: true })
.subscribe();
return perform();
}));
Instead of using the getAccessTokenSilently as part of the observable stream I now casually just subscribe to it in my first switchMap before returning the observable from perform().
This makes all tests pass finally. I have no other issues with any other observable in the system except getAccessTokenSilently. I don’t get it and when I look in the source code this is the declaration of that function:
public async getTokenSilently(...): Promise<string>
It is a promise, not even an observable and the authservice does not do from(client.getTokenSilently(…)). Maybe that isn’t necessary to turn a promise into an observable?
Internally this function also uses await to make it behave in a synchronized manner.
I don’t understand that function obviously at ALL and it messes up all my observable chains for some reason