File

src/app/core/session/auth/keycloak/keycloak-auth.service.ts

Description

Handles the remote session with keycloak

Index

Properties
Methods

Constructor

constructor(httpClient: HttpClient, keycloak: KeycloakService)
Parameters :
Name Type Optional
httpClient HttpClient No
keycloak KeycloakService No

Methods

addAuthHeader
addAuthHeader(headers: any)

Add the Bearer auth header to a existing header object.

Parameters :
Name Type Optional
headers any No
Returns : void
changePassword
changePassword()

Open password reset page in browser. Only works with internet connection.

Returns : Promise<any>
createUser
createUser(user: Partial<KeycloakUserDto>)
Parameters :
Name Type Optional
user Partial<KeycloakUserDto> No
Returns : Observable<any>
deleteUser
deleteUser(username: string)
Parameters :
Name Type Optional
username string No
Returns : Observable<any>
getRoles
getRoles()

Get a list of all roles generally available in the user management system.

Returns : Observable<Role[]>
getUser
getUser(username: string)
Parameters :
Name Type Optional
username string No
Async getUserinfo
getUserinfo()
Async login
login()

Check for an existing session or forward to the login page.

Async logout
logout()

Forward to the keycloak logout endpoint to clear the session.

Returns : unknown
logSuccessfulAuth
logSuccessfulAuth()

Log timestamp of last successful authentication

Returns : void
setEmail
setEmail(email: string)
Parameters :
Name Type Optional
email string No
Returns : Observable<any>
updateUser
updateUser(userId: string, user: Partial<KeycloakUserDto>)
Parameters :
Name Type Optional
userId string No
user Partial<KeycloakUserDto> No
Returns : Observable<any>

Properties

accessToken
Type : string
Static Readonly ACCOUNT_MANAGER_ROLE
Type : string
Default value : "account_manager"

Users with this role can create and update other accounts.

Static Readonly LAST_AUTH_KEY
Type : string
Default value : "LAST_REMOTE_LOGIN"
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
import { environment } from "../../../../../environments/environment";
import { SessionInfo } from "../session-info";
import { KeycloakEventTypeLegacy, KeycloakService } from "keycloak-angular";
import { Logging } from "../../../logging/logging.service";
import { Entity } from "../../../entity/model/entity";
import { ParsedJWT, parseJwt } from "../../session-utils";
import { RemoteLoginNotAvailableError } from "./remote-login-not-available.error";
import { switchMap } from "rxjs/operators";

/**
 * Handles the remote session with keycloak
 */
@Injectable()
export class KeycloakAuthService {
  /**
   * Users with this role can create and update other accounts.
   */
  static readonly ACCOUNT_MANAGER_ROLE = "account_manager";
  static readonly LAST_AUTH_KEY = "LAST_REMOTE_LOGIN";
  private keycloakInitialised = false;
  accessToken: string;

  constructor(
    private httpClient: HttpClient,
    private keycloak: KeycloakService,
  ) {}

  /**
   * Check for an existing session or forward to the login page.
   */
  async login(): Promise<SessionInfo> {
    if (!this.keycloakInitialised) {
      await this.initKeycloak();
    }

    await this.keycloak.updateToken();
    let token = await this.keycloak.getToken();
    if (!token) {
      // Forward to the keycloak login page.
      await this.keycloak.login({ redirectUri: location.href });
      token = await this.keycloak.getToken();
    }

    return this.processToken(token);
  }

  private async initKeycloak() {
    try {
      await this.keycloak.init({
        config: window.location.origin + "/assets/keycloak.json",
        initOptions: {
          onLoad: "check-sso",
          silentCheckSsoRedirectUri:
            window.location.origin + "/assets/silent-check-sso.html",
        },
        // GitHub API rejects if non GitHub bearer token is present
        shouldAddToken: ({ url }) => !url.includes("api.github.com"),
      });
    } catch (err) {
      if (
        err?.error ===
        "Timeout when waiting for 3rd party check iframe message."
      ) {
        // this is actually an expected scenario, user's internet is slow or not available
        err = new RemoteLoginNotAvailableError();
      } else {
        Logging.error("Keycloak init failed", err);
      }

      this.keycloakInitialised = false;
      throw err;
    }

    // auto-refresh expiring tokens, as suggested by https://github.com/mauriciovigolo/keycloak-angular?tab=readme-ov-file#keycloak-js-events
    this.keycloak.keycloakEvents$.subscribe((event) => {
      if (event.type == KeycloakEventTypeLegacy.OnTokenExpired) {
        this.login().catch((err) =>
          Logging.debug("automatic token refresh failed", err),
        );
      }
    });

    this.keycloakInitialised = true;
  }

  private processToken(token: string): SessionInfo {
    if (!token) {
      throw new Error("No token received from Keycloak");
    }

    this.accessToken = token;
    this.logSuccessfulAuth();
    const parsedToken: ParsedJWT = parseJwt(this.accessToken);

    const sessionInfo: SessionInfo = {
      name: parsedToken.username ?? parsedToken.sub,
      id: parsedToken.sub,
      roles: parsedToken["_couchdb.roles"],
      email: parsedToken.email,
    };

    if (parsedToken.username) {
      sessionInfo.entityId = parsedToken.username.includes(":")
        ? parsedToken.username
        : // fallback for legacy config: manually add "User" entity prefix
          Entity.createPrefixedId("User", parsedToken.username);
    } else {
      Logging.debug(
        `User not linked with an entity (userId: ${sessionInfo.id} | ${sessionInfo.name})`,
      );
    }

    if (parsedToken.email) {
      sessionInfo.email = parsedToken.email;
    }

    return sessionInfo;
  }

  /**
   * Add the Bearer auth header to a existing header object.
   * @param headers
   */
  addAuthHeader(headers: any) {
    if (this.accessToken) {
      if (headers.set && typeof headers.set === "function") {
        // PouchDB headers are set as a map
        headers.set("Authorization", "Bearer " + this.accessToken);
      } else {
        // Interceptor headers are set as a simple object
        headers["Authorization"] = "Bearer " + this.accessToken;
      }
    }
  }

  /**
   * Forward to the keycloak logout endpoint to clear the session.
   */
  async logout() {
    return await this.keycloak.logout(location.href);
  }

  /**
   * Open password reset page in browser.
   * Only works with internet connection.
   */
  changePassword(): Promise<any> {
    return this.keycloak.login({
      action: "UPDATE_PASSWORD",
      redirectUri: location.href,
    });
  }

  async getUserinfo(): Promise<KeycloakUserDto> {
    const user = await this.keycloak.getKeycloakInstance().loadUserInfo();
    return user as KeycloakUserDto;
  }

  setEmail(email: string): Observable<any> {
    return this.httpClient.put(`${environment.account_url}/account/set-email`, {
      email,
    });
  }

  createUser(user: Partial<KeycloakUserDto>): Observable<any> {
    return this.httpClient.post(`${environment.account_url}/account`, user);
  }

  deleteUser(username: string): Observable<any> {
    return this.getUser(username).pipe(
      switchMap((value) =>
        this.httpClient.delete(
          `${environment.account_url}/account/${value.id}`,
        ),
      ),
    );
  }

  updateUser(userId: string, user: Partial<KeycloakUserDto>): Observable<any> {
    return this.httpClient.put(
      `${environment.account_url}/account/${userId}`,
      user,
    );
  }

  getUser(username: string): Observable<KeycloakUserDto> {
    return this.httpClient.get<KeycloakUserDto>(
      `${environment.account_url}/account/${username}`,
    );
  }

  /**
   * Get a list of all roles generally available in the user management system.
   */
  getRoles(): Observable<Role[]> {
    return this.httpClient.get<Role[]>(
      `${environment.account_url}/account/roles`,
    );
  }

  /**
   * Log timestamp of last successful authentication
   */
  logSuccessfulAuth() {
    localStorage.setItem(
      KeycloakAuthService.LAST_AUTH_KEY,
      new Date().toISOString(),
    );
  }
}

/**
 * Extract of Keycloak role object.
 * See {@link https://www.keycloak.org/docs-api/19.0.3/rest-api/index.html#_rolerepresentation}
 */
export interface Role {
  id: string;
  name: string;
  description: string;
}

/**
 * Extract of Keycloak user object as provided by the external Keycloak Service.
 * See {@link https://www.keycloak.org/docs-api/19.0.3/rest-api/index.html#_userrepresentation}
 *
 * These fields overlap with our internal `SessionInfo` interface that is seen as abstracted from Keycloak.
 */
export interface KeycloakUserDto {
  id: string;
  username: string;
  email: string;
  roles: Role[];
  enabled: boolean;
}

results matching ""

    No results matching ""