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

Calling login method breaks SPA. #580

Open
jacobdab opened this issue Sep 23, 2024 · 0 comments
Open

Calling login method breaks SPA. #580

jacobdab opened this issue Sep 23, 2024 · 0 comments

Comments

@jacobdab
Copy link

Hello,

During app start my application in providers initialize keycloak using SSO and this works flawless'y not reopening application, but after that there is call keycloak.login done similiary that found in Readme, that causes SPA break and redirect to keycloak just to instantly back, causing the reiniatialization of whole app. As after registration data are collected from few sources then aggregated and put into database that keycloak use. It takes 3-5s to init that, as all providers need to be first resolved then app proceed to load rest. Is there way to make login silent?

Bug Report or Feature Request (mark with an x)

- [X] bug report -> please search for issues before submitting
- [ ] feature request

Versions.

"@angular/core": "^17.3.12",
"keycloak-angular": "^15.3.0",
"keycloak-js": "^24.0.5",

Repro steps.

// app.module.ts

@NgModule({
    declarations: [
        AppComponent,
    ],
    imports: [
        CoreModule,
        AppRoutingModule,
        GoogleMapsModule,
        StoreModule.forRoot(fromApp.appReducer),
        StoreModule.forFeature('credit', creditReducer),
        StoreModule.forFeature('process', processReducer),
        StoreModule.forFeature('search', searchReducer),

        // ngx-translate and the loader module
        TranslateModule.forRoot({
            loader: {
                provide: TranslateLoader,
                useFactory: HttpLoaderFactory,
                deps: [HttpClient],
            },
        }),
        ViewsModule,
        DirectivesModule,
        BrowserModule,
        BrowserAnimationsModule,
        KeycloakAngularModule,

        // toastr styles are "imported" via css in angular.json, because otherwise we need to import bootstrap functions
        // and that will surely clash with foundation ones
        ToastrModule.forRoot(),
        NgxMaskModule.forRoot(),
        StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: environment.production, connectInZone: true }),
        EffectsModule.forRoot([ProcessEffects, AuthEffects]),
        ExpertToolbarComponentModule,
        ErrorBannersComponentModule,
    ],
    providers: [
        {
            provide: APP_INITIALIZER,
            useFactory: initializeTranslations,
            deps: [TranslateService, Injector, EnvService],
            multi: true,
        },
        {
            provide: APP_INITIALIZER,
            useFactory: initializeKeycloak,
            multi: true,
            deps: [KeycloakService, Meta, EnvService, Store, AuthService, UserConsentsService],
        },
        {
            provide: HTTP_INTERCEPTORS,
            useClass: AuthInterceptor,
            multi: true,
        },
        {
            provide: APP_INITIALIZER,
            useFactory: initializeExpert,
            multi: true,
            deps: [Store, SessionStorageService],
        },
        {
            provide: HTTP_INTERCEPTORS,
            useClass: ErrorCachingInterceptor,
            multi: true,
        },
        {
            provide: HTTP_INTERCEPTORS,
            useClass: GlobalInterceptor,
            multi: true,
        },
        {
            provide: ErrorHandler,
            useClass: ErrorService,
        },
        { provide: Window, useValue: window },
        { provide: Document, useValue: document },
        {
            provide: LOCALE_ID,
            useValue: 'pl-PL',
        },
    ],
    bootstrap: [AppComponent],
    exports: [],
})

// initalizeKeycloak.ts

import { KeycloakService } from 'keycloak-angular';
import { Meta } from '@angular/platform-browser';
import { EnvService } from '@services/env/env.service';
import { Store } from '@ngrx/store';
import * as fromApp from '@store/app.reducer';
import { AuthService } from '@services/auth/auth.service';
import { of } from 'rxjs';
import { catchError, concatMap, first, map } from 'rxjs/operators';
import { fromPromise } from 'rxjs/internal-compatibility';
import { concatLatestFrom } from '@ngrx/effects';
import { selectAuth } from '@store/auth/auth.selectors';
import { UserRole } from '@src/app/enums/user-role.enum';
import { UserConsentsService } from '@services/user-consents/user-consents.service';
import { AuthStateCredentials } from '@src/app/models/auth-state-credentials.model';
import { AppRoutes } from '@src/app/enums/app-routes.enum';

export function initializeKeycloak (keycloak: KeycloakService, meta: Meta, env: EnvService, store: Store<fromApp.AppState>, authService: AuthService, userConsentsService: UserConsentsService): () => Promise<boolean> {
    let url: string = env.useLocalBackend ? 'http://keycloak:8080' : 'https://other-external-path';
    let realm = env.useLocalBackend ? 'Realm1' : 'Realm2';
    let clientId = env.useLocalBackend ? 'client1' : 'client2';

    // Always what comes in metaTags is more important than whatever has been set up above.
    // Unless it's undefined, then use current one
    url = meta.getTag('name="application-sso-url"')?.content || url;
    realm = meta.getTag('name="application-sso-realm"')?.content || realm;
    clientId = meta.getTag('name="application-sso-client-id"')?.content || clientId;

    const queryParams = new URL(window.location.href).searchParams;

    return (): Promise<boolean> => {
        return fromPromise(keycloak.init({
            config: {
                url,
                realm,
                clientId,
            },
            initOptions: {
                onLoad: 'check-sso',
                pkceMethod: 'S256',
                silentCheckSsoRedirectUri:
                    window.location.origin + '/assets/silent-check-sso.html',
            },
        })).pipe(
            map((isAuthenticated: boolean) => {
                console.log('isAuthenticated', isAuthenticated);

                if (queryParams.get('autologin') && !isAuthenticated) {
                    const redirectUri = `${window.location.origin}/${AppRoutes.AGREEMENTS}`;

                    setTimeout(() => {
                        void keycloak.login({
                            locale: 'pl',
                            idpHint: 'hint-hidden',
                            redirectUri,
                        });
                    }, 5000);
                }

                authService.initAuthState(isAuthenticated);

                return isAuthenticated;
            }),
            concatLatestFrom(() => {
                return store.select(selectAuth).pipe(first());
            }),
            map(([isAuthenticated, authState]) => {
                if (
                    authState.isAuthenticated &&
                    (
                        authState.roles.includes(UserRole.multiformUser) ||
                        (authState.roles.includes(UserRole.multiformExpert) && authState.inRoleUserId)
                    )
                ) {
                    void authService.initUserData(isAuthenticated).subscribe();
                }

                return [isAuthenticated, authState];
            }),
            concatMap(([isAuthenticated, authState]) => {
                if ((authState as AuthStateCredentials).isAuthenticated && (authState as AuthStateCredentials).roles.includes(UserRole.multiformUser)) {
                    return userConsentsService.getConsentsAndRedirect();
                }

                return of(isAuthenticated as boolean);
            }),
            catchError((error) => {
                console.error(error);

                authService.initAuthState(false);

                return Promise.resolve(true);
            }),
        ).toPromise();
    };
}

Desired functionality.

Silent login functionality without breaking SPA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant