After successful login, user navigate to home page. but when page is refreshed, login page flashes for a second. Most probably the reason is, same page is rendering on server-side 1st, then on Client-side. I've set it as, if user authentication false, redirect to login page, login then redirect to respected page on authGuard.
export const AuthGuard: CanActivateFn | CanActivateChildFn = (route, state) => {
const router: Router = inject(Router);
return inject(AuthService).checkAuthentication()
.pipe(
switchMap((isAuthenticated)=>{
if (!isAuthenticated) {
return router.createUrlTree(['auth/sign-in'], {queryParams: {param: state.url}})
}
return of(true);
})
);
}
While refreshing page, and when page is rendering on server side it gets Authentication false that's why it renders login page then send it to clint-side. But on client side it get's Authentication true then it's display home page(or other page). however between this time route doesn't change. here is the screen shots where authentication false on server & true on client side at the same time. SSR and CSR Compare Screenshots
However, if i turn off SSR, login page does not flash at all. So, is there any way to solve this flashing issue without turned of SSR mode?
export const routes : Routes = [
{
path: '',
component: LayoutComponent,
canActivate: [AuthGuard],
canActivateChild: [AuthGuard],
children: [
{ path: 'home', component: DashboardComponent },
{ path: 'settings', loadChildren: () => import('./modules/settings/settings.routes') },
{ path: 'users', loadChildren: () => import('./modules/users/users.routes')},
]
},
{
path: 'auth',
loadChildren: ()=> import('./login/login.routes')
},
{ path: '', pathMatch: 'full', redirectTo: 'home' },
{ path: '**', pathMatch: 'full', redirectTo: 'home' },
];
@Injectable({ providedIn: 'root' })
export class AuthService {
private _storeService = inject(StoreService);
signIn(credentials: { username: string; password: string }): Observable<any> {
return this._httpClient.post(AUTH_API + 'User/login', credentials).pipe(
switchMap((response: any) => {
this._storeService.saveAccessToken(response.token); // save token on sessionStorage
this._storeService.saveActiveUser(response.user);
return of(response);
}),
);
}
get accessToken(): string {
return this._storeService.getAccessToken() ?? '';
}
checkAuthentication : Observable<boolean> (){
if ( !this.accessToken || AuthUtils.isTokenExpired(this.accessToken))
{
return of(false);
} else { return of(true) }
}
}
Please let me know if i need to share more code ... thanks.
I've tried going through angular documents / stackoverflow / googleing relating this issue. but couldn't find suitable solution for this problem. also,i could not find that much example with functional approach.
Explanation:
The problem you're experiencing arises from the nature of Server-Side Rendering (SSR) in Angular. When the server renders the page, it doesn't have access to client-specific data like authentication tokens stored in the browser (e.g., in localStorage or sessionStorage). So the server assumes the user is not authenticated and renders the login page. However, once the client-side JavaScript kicks in, it identifies the user as authenticated (thanks to the token) and quickly redirects to the target page. This results in the brief flash of the login page you're observing.
Possible solution:
You could consider implementing a shared authentication state between the server and client. This would involve adding an interceptor to send the authentication token to the server within the initial request (via a cookie). You'll need to adjust your authentication flow to verify the token on the server side and initialize the application with the user already authenticated.
Implementation: (Angular 17 and nodeJS, you can use anything in your backend, just make sure to handle the cookie)
import { HttpInterceptorFn } from '@angular/common/http'
import { inject, PLATFORM_ID } from '@angular/core'
import { isPlatformServer } from '@angular/common'
import { REQUEST } from '../../express.tokens' // I will explain this later
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const platformId = inject(PLATFORM_ID)
const request = inject(REQUEST, { optional: true })
if (isPlatformServer(platformId) && request && request.cookies) {
req = req.clone({
headers: req.headers.set('myCookie', request.cookies),
})
} else {
req = req.clone({ withCredentials: true })
}
return next(req)
}
app.config.ts
, add:provideHttpClient(withFetch(), withInterceptors([authInterceptor])),
Import the REQUEST:
import { REQUEST } from './src/express.tokens' // I will explain this later
Update server.get()
to :
server.get('*', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req
req.cookies = req.headers.cookie // Add this
commonEngine
.render({
bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: browserDistFolder,
providers: [
{ provide: APP_BASE_HREF, useValue: baseUrl },
{ provide: REQUEST, useValue: req }, // Add this
],
})
.then((html) => res.send(html))
.catch((err) => next(err))
})
Add this right after the initialisation of the express server:
// Middleware to handle 'myCookie' (Load data in SSR)
app.use((req, res, next) => {
const myCookie = req.headers['mycookie']
if (myCookie) {
if (typeof myCookie === 'string') {
req.headers.cookie = myCookie
} else {
req.headers.cookie = myCookie[0]
}
}
next()
})
Limitations and considerations:
As you may already know, starting Angular version 17, Universal has been moved into the Angular CLI repo. Code has been refactored and renamed (mostly under @angular/ssr now). With this migration, the express tokens were lost so now we have to define them manually. You need to create a new file express.tokens.ts
:
import { InjectionToken } from '@angular/core'
import { Request, Response } from 'express'
export const REQUEST = new InjectionToken<Request>('REQUEST')
export const RESPONSE = new InjectionToken<Response>('RESPONSE')
This would lead you to what I consider a huge regression from Angular v16 to Angular v17. Now, ng serve
does allow SSR, but it does not use the server.ts, Instead it has it's own middlewares and server instance. This would lead to REQUEST being always null when running ng serve
. This is not documented anywhere btw, but you can check this issue to stay up to date with the latest updates.
Please note that for the REQUEST injection to work in the build, you need to set prerender
to false
in your angular.json
.
Conclusion:
This solution will only work in production builds with prerender
set to false
. I also need to mention that this is only an Angular v17 problem, it works like a charm on all the other versions.
I will update this answer as soon as a workaround is found or the Angular team provides an update.
March 1st update: The Angular team is intending to solve the issue this May (v18).
This is a known problem and we're working on resolving it. Currently, the plan is to ship it as part of the v18 release this May. If anything unexpected pops up and we're unable to deliver in this timeframe we'll follow up here. Source