I'm using the new auth0-spa-js library with universal login. I followed the guide on https://auth0.com/docs/quickstart/spa/angular2/01-login to the letter, but still - on browser reload client.isAuthenticated()
will always return false and it will redirect to the login page.
This is very frustrating.
EDIT: Removed links to github, and added my code directly in post as requested
EDIT2: Solution posted at the bottom of this post
auth0 configuration
Application
Allowed Callback URLs: http://localhost:3000/callback
Allowed Web Origins: http://localhost:3000
Allowed Logout URLs: http://localhost:3000
Allowed Origins (CORS): http://localhost:3000
JWT Expiration 36000
API
Token expiration: 86400
Token Expiration For Browser Flows: 7200
Don't exactly know what the difference is between these two sections (Application/Api config), nor which of these I am actually using when going through the normal universal login flow, but I'll post them anyway.
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { AppRoutes } from './app.routing';
import { AppComponent } from './app.component';
import { DashboardComponent } from './views/dashboard/dashboard.component';
import { CallbackComponent } from './shared/auth/callback/callback.component';
@NgModule({
declarations: [
AppComponent,
DashboardComponent,
CallbackComponent
],
imports: [
BrowserModule,
AppRoutes,
HttpClientModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
app.routing.ts
import { Routes, RouterModule } from '@angular/router';
import { CallbackComponent } from './shared/auth/callback/callback.component';
import { DashboardComponent } from './views/dashboard/dashboard.component';
import { AuthGuard } from './shared/auth/auth.guard';
const routes: Routes = [
{ path: '', pathMatch: 'full', component: DashboardComponent, canActivate: [AuthGuard] },
{ path: 'callback', component: CallbackComponent },
{ path: '**', redirectTo: '' }
];
export const AppRoutes = RouterModule.forRoot(routes);
app.component.ts
import { Component, OnInit } from '@angular/core';
import { AuthService } from './shared/auth/auth.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
title = 'logic-energy';
constructor(private auth: AuthService) { }
ngOnInit() {
// On initial load, check authentication state with authorization server
// Set up local auth streams if user is already authenticated
this.auth.localAuthSetup();
}
}
auth.service.ts
import { Injectable } from '@angular/core';
import createAuth0Client from '@auth0/auth0-spa-js';
import Auth0Client from '@auth0/auth0-spa-js/dist/typings/Auth0Client';
import { environment } from 'src/environments/environment';
import { from, of, Observable, BehaviorSubject, combineLatest, throwError } from 'rxjs';
import { tap, catchError, concatMap, shareReplay, take } from 'rxjs/operators';
import { Router } from '@angular/router';
@Injectable({
providedIn: 'root'
})
export class AuthService {
// Create an observable of Auth0 instance of client
auth0Client$ = (from(
createAuth0Client({
domain: environment.auth.domain,
client_id: environment.auth.clientId,
redirect_uri: `${window.location.origin}/callback`
})
) as Observable<Auth0Client>).pipe(
shareReplay(1), // Every subscription receives the same shared value
catchError(err => throwError(err))
);
// Define observables for SDK methods that return promises by default
// For each Auth0 SDK method, first ensure the client instance is ready
// concatMap: Using the client instance, call SDK method; SDK returns a promise
// from: Convert that resulting promise into an observable
isAuthenticated$ = this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.isAuthenticated())),
tap(res => this.loggedIn = res)
);
handleRedirectCallback$ = this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.handleRedirectCallback()))
);
// Create subject and public observable of user profile data
private userProfileSubject$ = new BehaviorSubject<any>(null);
userProfile$ = this.userProfileSubject$.asObservable();
// Create a local property for login status
loggedIn: boolean = null;
constructor(private router: Router) { }
// When calling, options can be passed if desired
// https://auth0.github.io/auth0-spa-js/classes/auth0client.html#getuser
getUser$(options?): Observable<any> {
return this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.getUser(options))),
tap(user => this.userProfileSubject$.next(user))
);
}
localAuthSetup() {
// This should only be called on app initialization
// Set up local authentication streams
const checkAuth$ = this.isAuthenticated$.pipe(
concatMap((loggedIn: boolean) => {
if (loggedIn) {
// If authenticated, get user and set in app
// NOTE: you could pass options here if needed
return this.getUser$();
}
// If not authenticated, return stream that emits 'false'
return of(loggedIn);
})
);
checkAuth$.subscribe((response: { [key: string]: any } | boolean) => {
// If authenticated, response will be user object
// If not authenticated, response will be 'false'
this.loggedIn = !!response;
});
}
login(redirectPath: string = '/') {
// A desired redirect path can be passed to login method
// (e.g., from a route guard)
// Ensure Auth0 client instance exists
this.auth0Client$.subscribe((client: Auth0Client) => {
// Call method to log in
client.loginWithRedirect({
redirect_uri: `${window.location.origin}/callback`,
appState: { target: redirectPath }
});
});
}
handleAuthCallback() {
// Only the callback component should call this method
// Call when app reloads after user logs in with Auth0
let targetRoute: string; // Path to redirect to after login processsed
const authComplete$ = this.handleRedirectCallback$.pipe(
// Have client, now call method to handle auth callback redirect
tap(cbRes => {
// Get and set target redirect route from callback results
targetRoute = cbRes.appState && cbRes.appState.target ? cbRes.appState.target : '/';
}),
concatMap(() => {
// Redirect callback complete; get user and login status
return combineLatest(
this.getUser$(),
this.isAuthenticated$
);
})
);
// Subscribe to authentication completion observable
// Response will be an array of user and login status
// authComplete$.subscribe(([user, loggedIn]) => {
authComplete$.subscribe(([user, loggedIn]) => {
// Redirect to target route after callback processing
this.router.navigate([targetRoute]);
});
}
logout() {
// Ensure Auth0 client instance exists
this.auth0Client$.subscribe((client: Auth0Client) => {
// Call method to log out
client.logout({
client_id: environment.auth.clientId,
returnTo: `${window.location.origin}`
});
});
}
getTokenSilently$(options?): Observable<string> {
return this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.getTokenSilently(options)))
);
}
}
auth.guard.ts
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, CanActivate } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';
import { tap } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService) {}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean|UrlTree> | boolean {
return this.auth.isAuthenticated$.pipe(
tap(loggedIn => {
if (!loggedIn) {
this.auth.login(state.url);
}
})
);
}
}
callback.component.ts
import { Component, OnInit } from '@angular/core';
import { AuthService } from '../auth.service';
@Component({
selector: 'app-callback',
templateUrl: './callback.component.html',
styleUrls: ['./callback.component.scss']
})
export class CallbackComponent implements OnInit {
constructor(private auth: AuthService) { }
ngOnInit() {
this.auth.handleAuthCallback();
}
}
By inspecting the network tab in devtools, I can see the following calls are made:
Before login:
authorize
using several query params => returning HTTP 200 with an empty body.login
=> Returning the login pageAfter login:
authorize
=> returning HTTP 302 empty bodyauthorize
again (with a different set of params) => returning HTTP 302 empty bodyauthorize
again (with a another different set of params) => returning HTTP 302 empty bodycallback
=> returning HTTP 302 empty bodycallback
=> returning HTTP 200 with my callback htmlNOTE: Every other time it stops here and will not redirect to root, which is kind of strange.
After callback redirects:
authorize
=> returning HTTP 200 empty bodytoken
=> which receives a valid token. I can Base64 decode it, and it seems ok (except for some garbage in the nonce
property)Hitting refresh in the browser, will repeat this process
I've double checked the auth0 configuration. This works as expected on a react app using the older auth0-js, I'm using the same client_id with the same url's configured.
What am I doing wrong? Is there a manual step I have to do which is not described in the documentation? Do I have to migrate to the older auth0-js library for this to work?
Update
I've set some breakpoints in auth0-spa-js, and I see that when the application starts, it tries to run the getTokenSilently()
, but it allways rejects the promise with a "login_required"
.
Even right after login, it calls the url first and rejects (even if the http request returns HTTP 200, because response has an empty body?), then it tries the internal cache after and it goes through.
As long as I do not refresh, auth0 will use the token from the cache, but it throws immediately if it tries to validate from http.
One thing I see, the following code is run each time getTokenSilently()
is not fetching from cache:
stateIn = encodeState(createRandomString());
nonceIn = createRandomString();
code_verifier = createRandomString();
return [4 /*yield*/, sha256(code_verifier)];
In other words, it asks auth0 backend if I'm authenticated based on completely random strings, always. Shouldn't it be storing some of these in the browser if this is what allows it to identify me and my session?
Update 2 / Solution
Uhm... It would seem that the Chrome Plugin "Privacy Badger", which can prevent cookies from being stored, actually also has an effect on the site if you browse it through other browsers (when chrome is open). It actually cleared out the session the moment it was processed. The code above works, I just had to tweak the plugin. Urk...
In case I'm not the only one who forgets what extensions are installed, I will leave this issue here, so others might not waste an entire day debugging something which needn't be debugged.
I just noticed that you do not have a route registered for your call back:
const routes: Routes = [
{ path: '', pathMatch: 'full', component: DashboardComponent, canActivate: [AuthGuard] },
{ path: 'callback', component: CallbackComponent },
{ path: '**', redirectTo: '' }
];
https://auth0.com/docs/quickstart/spa/angular2/01-login#handle-login-redirects
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With