Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
sanjay-noknok opened this issue Feb 1, 2024 · 10 comments
Assignees
Labels
enhancement This issue/PR is an enhancement or new feature. evaluate-in-next-version Evaluate if the feature or change might be included in the next library release. need-investigation Needs more investigation to identify if it is a bug.
Milestone

Comments

@sanjay-noknok
Copy link

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:

  • Angular version: 17
  • Keycloak-angular version: [v15.0.0]
  • Keycloak-js version: [v22.0.5]
  • Operating system: [Windows 11]
  • Any relevant configuration changes made: [insert changes made to enable SSR]
@OZIOisgood
Copy link

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
Copy link
Author

sanjay-noknok commented Feb 12, 2024

@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
Copy link

OZIOisgood commented Feb 12, 2024

@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
Copy link
Author

@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
Copy link

aknuth commented Feb 14, 2024

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" ..

@mauriciovigolo mauriciovigolo self-assigned this Feb 15, 2024
@mauriciovigolo mauriciovigolo added the need-investigation Needs more investigation to identify if it is a bug. label Feb 15, 2024
@naeimsf
Copy link

naeimsf commented Feb 27, 2024

same issue here withAngular 17 SSR enabled.

@naeimsf
Copy link

naeimsf commented Feb 27, 2024

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
Copy link

naeimsf commented Mar 29, 2024

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
Copy link

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
Copy link

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.

@mauriciovigolo mauriciovigolo added this to the v16.0.0 milestone Jun 14, 2024
@mauriciovigolo mauriciovigolo added enhancement This issue/PR is an enhancement or new feature. evaluate-in-next-version Evaluate if the feature or change might be included in the next library release. labels Jun 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement This issue/PR is an enhancement or new feature. evaluate-in-next-version Evaluate if the feature or change might be included in the next library release. need-investigation Needs more investigation to identify if it is a bug.
Projects
None yet
Development

No branches or pull requests

7 participants