mauriciovigolo / keycloak-angular

Easy Keycloak setup for Angular applications.
MIT License
714 stars 271 forks source link

Server-side rendering (SSR) crash with 'Document not found' error in Keycloak-angular with Angular 17 #539

Open sanjay-noknok opened 5 months ago

sanjay-noknok commented 5 months ago

Issue Description: When enabling server-side rendering (SSR) in a Keycloak-angular project with Angular 17, the application crashes with a "Document not found" error. Disabling SSR resolves the issue.

Steps to Reproduce:

  1. Create a new Keycloak-angular project.
  2. Enable SSR in the Angular configuration.
  3. Implement Keycloak-angular
  4. Attempt to run the application.
  5. Observe the crash with the "Document not found" error.

Expected Behavior: The application should run successfully with SSR enabled, displaying the expected content.

Actual Behavior: The application crashes with a "Document not found" error when SSR is enabled.

Additional Context:

OZIOisgood commented 5 months ago

Hey, @sanjay-noknok Could you provide us the configuration in your project(the way you have “implemented keycloak-angular”) Maybe the problem is in the way how have you provided keycloak-angular things.

sanjay-noknok commented 4 months ago

@OZIOisgood Thank you for your response. Here are my keycloak configurations

keycloak.config.ts i have put this file in src/environments folder

import { KeycloakConfig } from "keycloak-js";

const keycloakConfig: KeycloakConfig = {
  URL: "<keycloak-url-here>",
  realm: "<realm-here>",
  clientId: "<client-id-here>",
};

export default keycloakConfig;

app.config.ts file

import { KeycloakAngularModule, KeycloakService } from "keycloak-angular";
import { APP_INITIALIZER } from "@angular/core";
export const appConfig: ApplicationConfig = {
  providers: [
    { provide: APP_INITIALIZER, useFactory: initializer, multi: true, deps: [KeycloakService] },
    KeycloakService, 
    importProvidersFrom(KeycloakAngularModule),
    //other modules added
   ]
}

assets/silent-check-sso.html

<html>
  <body>
    <script>
      parent.postMessage(location.href, location.origin);
    </script>
  </body>
</html>

keycloak-initializer.ts

import { environment } from "@env/environment";
import { KeycloakOptions, KeycloakService } from "keycloak-angular";

export function initializer(keycloak: KeycloakService): () => Promise<boolean> {
  const options: KeycloakOptions = {
    config: environment.keycloak,
    initOptions: {
      onLoad: "login-required",
      checkLoginIframe: true,
    },
    enableBearerInterceptor: true,
    bearerPrefix: "Bearer",
    loadUserProfileAtStartUp: false,
    bearerExcludedUrls: ["/assets", "/clients/public"],

    shouldAddToken: (request) => {
      const { method, url } = request;

      const isGetRequest = "GET" === method.toUpperCase();
      const acceptablePaths = ["/assets", "/clients/public"];
      const isAcceptablePathMatch = acceptablePaths.some((path) => url.includes(path));

      return !(isGetRequest && isAcceptablePathMatch);
    },

    shouldUpdateToken: (request) => {
      return !request.headers.get("token-update") === false;
    },
  };

  return () => keycloak.init(options);
}

auth.service.ts

import { Injectable } from "@angular/core";
import { KeycloakService } from "keycloak-angular";
import { KeycloakProfile, KeycloakTokenParsed } from "keycloak-js";

@Injectable({
  providedIn: "root",
})
export class AuthService {
  constructor(private keycloakService: KeycloakService) {}

  public getLoggedUser(): KeycloakTokenParsed | undefined {
    try {
      const userDetails: KeycloakTokenParsed | undefined = this.keycloakService.getKeycloakInstance().idTokenParsed;
      return userDetails;
    } catch (e) {
      console.error("Exception", e);
      return undefined;
    }
  }

  public isLoggedIn(): boolean {
    return this.keycloakService.isLoggedIn();
  }

  public loadUserProfile(): Promise<KeycloakProfile> {
    return this.keycloakService.loadUserProfile();
  }

  public login(): void {
    this.keycloakService.login();
  }

  public logout(): void {
    this.keycloakService.logout(window.location.origin);
  }

  public redirectToProfile(): void {
    this.keycloakService.getKeycloakInstance().accountManagement();
  }

  public getRoles(): string[] {
    return this.keycloakService.getUserRoles();
  }
}

auth.guard.ts

import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from "@angular/router";
import { KeycloakAuthGuard, KeycloakService } from "keycloak-angular";

@Injectable({
  providedIn: "root",
})
export class AuthGuard extends KeycloakAuthGuard {
  constructor(
    protected override readonly router: Router,
    protected readonly keycloak: KeycloakService
  ) {
    super(router, keycloak);
  }

  public async isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    // Force the user to log in if currently unauthenticated.
    if (!this.authenticated) {
      await this.keycloak.login({
        redirectUri: window.location.origin + state.url,
      });
    }

    // Get the roles required from the route.
    const requiredRoles = route.data["roles"];

    // Allow the user to proceed if no additional roles are required to access the route.
    if (!Array.isArray(requiredRoles) || requiredRoles.length === 0) {
      return true;
    }

    // Allow the user to proceed if all the required roles are present.
    return requiredRoles.every((role) => this.roles.includes(role));
  }
}
OZIOisgood commented 4 months ago

@sanjay-noknok Guards and Interceptors should now be written as plain JavaScript function. As of Angular 15 they have deprecated class-based guards and interceptors.

Maybe it is a problem, why you are facing some issues. Try to use function-based AuthGuard:

import { CanMatchFn, Router, UrlTree } from '@angular/router';
import { inject } from '@angular/core';

// Services
import { KeycloakService } from 'keycloak-angular';

export const authGuard: CanMatchFn = async (route, segments): Promise<boolean | UrlTree> => {
  const router = inject(Router);
  const keycloakService = inject(KeycloakService);

  const authenticated: boolean = await keycloakService.isLoggedIn();

  if (!authenticated) {
    await keycloakService.login({
      redirectUri: window.location.origin,
    });
  }

  // Get the user Keycloak roles and the required from the route
  const roles: string[] = keycloakService.getUserRoles(true);
  const requiredRoles = route.data?.['roles'];

  // Allow the user to proceed if no additional roles are required to access the route
  if (!Array.isArray(requiredRoles) || requiredRoles.length === 0) {
    return true;
  }

  // Allow the user to proceed if ALL of the required roles are present
  const authorized = requiredRoles.every((role) => roles.includes(role));
  // Allow the user to proceed if ONE of the required roles is present
  //const authorized = requiredRoles.some((role) => roles.includes(role));

  if (authorized) {
    return true;
  }

  // Display my custom HTTP 403 access denied page
  return router.createUrlTree(['/access']);
};

And Bearer Interceptor should be provided as:

export const appConfig: ApplicationConfig = {
  providers: [
    KeycloakService,
    {
      provide: APP_INITIALIZER,
      useFactory: initializeKeycloak,
      multi: true,
      deps: [KeycloakService]
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: KeycloakBearerInterceptor,
      multi: true
    },
    provideRouter(routes),
    provideHttpClient(withInterceptorsFromDi())
  ]
};

withInterceptorsFromDi() is used here to specify that class-based Interceptor will be used, since there is no function-based one in angular-keycloak yet.

sanjay-noknok commented 4 months ago

@OZIOisgood Thank you for your response. I appreciate that. I tried your code and enabled the ssr from angular.json file Still getting the same issue. image If there is any other solution please do share with me.

Thank you.

aknuth commented 4 months ago

having the same problem with SSR and this config:

function initializeKeycloak(keycloak: KeycloakService) {
  return () =>
    keycloak.init({
      config: {
        url: 'http://localhost:8080',
        realm: 'bizmatch',
        clientId: 'bizmatch-angular-client'
      },
      initOptions: {
        onLoad: 'check-sso',
        silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html'
      },
      bearerExcludedUrls: ['/assets'],
      shouldUpdateToken(request) {
        return request.headers.get('token-update') !== 'false';
      }
    });
}

getting an error: "window is not defined" instead of "document is not defined" ..

naeimsf commented 4 months ago

same issue here withAngular 17 SSR enabled.

naeimsf commented 4 months ago

Just found a workaround (StackOverflow) Add these lines to providers in app.config.ts file

KeycloakAngularModule,
    KeycloakService,
    {
      provide: APP_INITIALIZER,
      useFactory: initKeycloak,
      multi: true,
      deps: [KeycloakService],
    },

and this is the initial function

import { KeycloakService } from 'keycloak-angular';
import { environment } from '../environments/environment';

declare var require: any;
const Keycloak = typeof window !== 'undefined' ? import('keycloak-js') : null;

export function initKeycloak(keycloak: KeycloakService) {
  if (Keycloak !== null) {
    return () =>
      keycloak.init({
        config: environment.keycloakConfig,
        initOptions: {
          onLoad: 'check-sso',
          checkLoginIframe: false,
        },
        enableBearerInterceptor: true,
        bearerPrefix: 'Bearer',
      });
  } else {
    return () => {
      return new Promise<Boolean>((resolve, reject) => {
        resolve(true);
      });
    };
  }
}

image

naeimsf commented 3 months ago

Hi again, actually I have an issue with this method. Need to call keycloakService.getToken() in server side which doesn't work as we don't load keycloak in server side.

Is there any example of working keycloak in SSR mode? need to get token on server side.

alfredarcifa commented 2 months ago

Please, follow up. I have the same issue.

ERROR ReferenceError: document is not defined

"keycloak-angular": "^15.2.1", "keycloak-js": "^24.0.3", "@angular/common": "^17.2.0", "@angular/compiler": "^17.2.0", "@angular/core": "^17.2.0",

EnricoMessall commented 1 month ago

Hey there. The problem is overall that SSR doesn't know about the window/document property which is used by keycloak-js. Therefor keycloak-js itself and not only keycloak-angular would need to be Angular based and support the document injection.

The only way to handle that is by using libs that mock the window object and every function that keycloak-js might use from that.