I have created a guard that calls OAuthService.hasValidIdToken() to determine whether or not my routes can be activated. When I access the app I get redirected to my identity server site. However, after I log in and get redirected back to the app OAuthService.hasValidIdToken() is still returning false so I get bounced straight back to identity server.
I have tried all of the solutions in the previous issues (including setting my redirectUrl to window.location.origin + '/index.html') but none of them seem to work.
In general sounds like you got all the bits right. Any chance you could provide us with a small repro? It's a bit hard to help out otherwise.
I've created an app which replicates the problem:
https://github.com/johnlace1986/OAuthTest.git
The issuer, clientId and scope of the AuthConfig will obviously need to be changed.
My app module has 2 routes, the first has a path of "auth" which loads an AuthComponent. This component contains a button which calls initImplicitFlow(). This redirects the user to identity server and allows them to log in. The second route is a catch all which redirects to "web". AppComponent simply contains a router outlet.
I then have a WebModule which contains a single route with a path of 'web' and a guard which checks hasValidIdToken(). The route loads a WebComponent (which, similarly to AppComponent just contains a router outlet) and has a single child route which is another catch all and loads a HomeComponent. The HomeComponent simply contains the text "You are logged in!".
So when the user first loads the app they should be redirected to 'http://localhost:4200/auth' and the components loaded will be AppComponent > AuthComponent. When they click the login button they are directed to identity server which allows them to login and redirects back to the app. This works fine.
After they have logged in they should be redirected to 'http://localhost:4200/web' and the components loaded will be AppComponent > WebComponent > HomeComponent. However, this is not happening. Instead the user is redirected back to 'http://localhost:4200/auth' and if I put log this.oAuthService.hasValidIdToken() to the console it prints 'false'.
The reason I have structured the app like is that I want to WebComponent to essentially be my master page once the user has logged in so it will contain the header bar and navigation options. However, I don't want these to be visible on the login page (AuthComponent).
I've cloned your repo and can reproduce your scenario, I think. In fact, I'm currently struggling with similar issues in my own current app.
I suspect the culprit is in the fact that the hash fragment that the Identity Server adds to your redirect url (containing the access token and whatnot) is cleared by the router before the TryLogin() method gets a chance to read it. This is because the TryLogin runs _after_ the loadDiscoveryDocument part which is async as it relies on a Ajax call.
In other GitHub issues here I found that initialNavigation: false was set on the router to handle this issue, but that causes other problems (it seems to me you then manually have to do initial navigation?).
A workaround or possibly even solution might lie in this:
this.oauthService.loadDiscoveryDocumentAndTryLogin({ customHashFragment: location.hash });
When I change that in your sample app, things start to work for me. The reason is that when the callback for TryLogin runs (after async loading of the discovery document) the location.hash will have been cleared, but with the customHashFragment you've passed the original hash to it.
I'll have to investigate further later on whether this is a proper solution. Or perhaps someone more knowledgeable might chime in on this thread about solutions.
PS. To better see what's going on I've added this to the app.component.html file after making oauthService public:
<pre>access_token = {{oauthService.getAccessToken()}}</pre>
PS. What I've also found instrumental in helping me debug this, is this piece of code above the .configure(...) call:
this.oauthService.events.subscribe(event => {
if (event instanceof OAuthErrorEvent) {
console.error(event);
} else {
console.warn(event);
}
});
Thank you for looking into this for me! I've added the customHashFragment value and we've definitely made progress but we're not quite there yet.
Now, when I get redirected back to my app from identity server it hits the guard and hasValidIdToken() returns false. However, the HTML snippet you suggested I add to app.component.html is showing a token value and if I manually navigate around my app (i.e. just entering 'http://localhost:4200' into the address bar and hitting enter) the guard gets hit again and now hasValidIdToken() returns true.
I'm assuming this is some kind of async problem whereby the the guard is being hit before OAuthService has had a chance to actually set the token.
I've pushed these changes to my repo.
Yup, that is exactly the point where I got stuck too: the AuthGuard runs before the discovery document is loaded and as such before the access token is saved.
I'm not sure what the "idiomatic" solution is here (one that still allows using the async load of the discovery document).
Some things I've tried, but haven't got the particulars right for yet:
TryLogin separately _before_ loading the discovery document, but then settings are missingPromise<boolean> once the loadDisco..AndTryLogin() finishesNot sure where to go from here yet, but maybe my thoughts/attempts will help you figure out a good solution.
Might be a week or so before I have more time to investigate this further, but if I find something I'll try to remember to share here.
OK cool. Thanks again for looking. I'll see what I end up with and I'll post anything I find on here too.
Wanted to leave a note here, for what it's worth to future visitors. I've experimented further around the combination of AuthGuards and the OAuthService. You can follow my experiments in this repository.
Basically, best I currently could come up with makes the AuthGuard return an Observable<boolean> like this:
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService) { }
canActivate(): Observable<boolean> {
return this.authService.canActivateProtectedRoutes$;
}
}
The AuthService is my own wrapper around the OAuthService. It will not allow access into protected pages until _both_:
OAuthServiceThe wrapper uses two subjects for this, and combines them:
@Injectable()
export class AuthService {
private isAuthenticatedSubject$ = new BehaviorSubject<boolean>(false);
public isAuthenticated$ = this.isAuthenticatedSubject$.asObservable();
private isDoneLoadingSubject$ = new ReplaySubject<boolean>();
public isDoneLoading$ = this.isDoneLoadingSubject$.asObservable();
public canActivateProtectedRoutes$: Observable<boolean> = combineLatest(
this.isAuthenticated$,
this.isDoneLoading$
).pipe(map(values => values.every(b => b)));
// etc.
The isAuthenticated$ observable starts out false by default, and publishes a new value (in a dumb fashion) upon each event from the OAuthService, like this:
this.oauthService.events
.subscribe(_ => {
this.isAuthenticatedSubject$.next(this.oauthService.hasValidAccessToken());
});
The isDoneLoading observable has no initial value, and will publish true once all promises on the OAuthService are resolved (and replay that value to anyone interested):
this.oauthService.loadDiscoveryDocument()
// andTryLogin
// andTrySilentRefresh
// etc.
.then(() => this.isDoneLoadingSubject$.next(true))
.catch(() => this.isDoneLoadingSubject$.next(true));
The effect of this is that:
canActivate() finally (after all ajax calls are done) returns a valueOn my TODO list is to craft some code that updates the logged-in-state for specific situations, like getting a push message from the IDServer that you're logged out, access tokens expiring, silent refreshes failing, etc.
I also fell into this same trap, but you gave me an idea with your solution. Auth guards can return boolean | Promise
Here is my solution:
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | Promise<boolean> {
if (this.oauthService.hasValidIdToken()) {
return true;
}
const promise = new Promise<boolean>( (resolve, reject) => {
return this.oauthService.loadDiscoveryDocumentAndTryLogin().then( (success: boolean) => {
if (!success) {
this.router.navigate(['/signin']);
resolve(false);
} else {
resolve(true);
}
});
});
return promise;
}
Edit: Sorry I post too quickly. I had the original resolve logic reversed. It seems to work now.
Wanted to leave a note here, for what it's worth to future visitors. I've experimented further around the combination of AuthGuards and the OAuthService. You can follow my experiments in this repository.
Basically, best I currently could come up with makes the AuthGuard return an
Observable<boolean>like this:@Injectable() export class AuthGuard implements CanActivate { constructor(private authService: AuthService) { } canActivate(): Observable<boolean> { return this.authService.canActivateProtectedRoutes$; } }The
AuthServiceis my own wrapper around theOAuthService. It will not allow access into protected pages until _both_:
- my wrapper is done calling/handling all ajax calls on the
OAuthService- at that time when it's done, the user has a valid access token
The wrapper uses two subjects for this, and combines them:
@Injectable() export class AuthService { private isAuthenticatedSubject$ = new BehaviorSubject<boolean>(false); public isAuthenticated$ = this.isAuthenticatedSubject$.asObservable(); private isDoneLoadingSubject$ = new ReplaySubject<boolean>(); public isDoneLoading$ = this.isDoneLoadingSubject$.asObservable(); public canActivateProtectedRoutes$: Observable<boolean> = combineLatest( this.isAuthenticated$, this.isDoneLoading$ ).pipe(map(values => values.every(b => b))); // etc.The
isAuthenticated$observable starts outfalseby default, and publishes a new value (in a dumb fashion) upon each event from the OAuthService, like this:this.oauthService.events .subscribe(_ => { this.isAuthenticatedSubject$.next(this.oauthService.hasValidAccessToken()); });The
isDoneLoadingobservable has no initial value, and will publishtrueonce all promises on theOAuthServiceare resolved (and replay that value to anyone interested):this.oauthService.loadDiscoveryDocument() // andTryLogin // andTrySilentRefresh // etc. .then(() => this.isDoneLoadingSubject$.next(true)) .catch(() => this.isDoneLoadingSubject$.next(true));The effect of this is that:
- When you load the app straight on a protected URL, the router outlet won't show anything until the observable returned from
canActivate()finally (after all ajax calls are done) returns a value- When you load the app on a public URL, and try to navigate to a protected URL, the router _might_ block you if the Ajax calls are still in progress, or will immediately allow/deny access based on the state of the observables
On my TODO list is to craft some code that updates the logged-in-state for specific situations, like getting a push message from the IDServer that you're logged out, access tokens expiring, silent refreshes failing, etc.
I really like your solution, but it doesn't work I am following all the steps but I think I am confused in the next:
// andTryLogin
// andTrySilentRefresh
// etc.
Do you have a working example of your solution? Or any advice?
Thank you very much!
From briefly reviewing this code, I believe ReplaySubject takes a parameter in its constructor. You should put new ReplaySubject
I'm not sure, but I always believed that it was a compile time error if you didn't put a number in the constructor of a replay subject. But more than that, if you just think about it, if you don't allocate the spot for the replay subject, it may not be able to replay the value. So once it's done loading, if you subscribe to it, it won't ever emit that it has a value again.
This is just a guess from perusing the code. But I think you should start with that.
Seems there's various liked solutions to the original question in this issue. Going to close it - let us know if this issue is still open (or create a new issue if you're having a different scenario).
@jeroenheijmans , This is happening with me too but only in IE, Edge and Desktop web -add in. for Chrome It is working. Should the above solution work for me ?
Moreover, I am getting the below error(Access Denied) in IE, edge:

Most helpful comment
I also fell into this same trap, but you gave me an idea with your solution. Auth guards can return boolean | Promise. So I thought of just doing the logic with the discovery document as a fallback in case the timing is off. This was an annoying issue to run into due it sometimes working so I'm glad that you all posted and theorized about this issue.
Here is my solution:
Edit: Sorry I post too quickly. I had the original resolve logic reversed. It seems to work now.