Skip to content
Snippets Groups Projects
auth-keycloak.js 9.3 KiB
Newer Older
import {createInstance} from './i18n.js';
import JSONLD from '@dbp-toolkit/common/jsonld';
Reiter, Christoph's avatar
Reiter, Christoph committed
import {KeycloakWrapper} from './keycloak.js';
import {LoginStatus} from './util.js';
import {AdapterLitElement} from '@dbp-toolkit/common';
import {send} from '@dbp-toolkit/common/notification';

/**
 * Keycloak auth web component
 * https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter
 *
 * Emits a dbp-set-property event for the attribute "auth":
 *   auth.login-status: Login status (see object LoginStatus)
 *   auth.token: Keycloak token to send with your requests
 *   auth.user-full-name: Full name of the user
export class AuthKeycloak extends AdapterLitElement {
    constructor() {
        super();
        this.forceLogin = false;
Reiter, Christoph's avatar
Reiter, Christoph committed
        this.token = '';
        this.subject = '';
        this.name = '';
        this.tryLogin = false;
        this.entryPointUrl = '';
Reiter, Christoph's avatar
Reiter, Christoph committed
        this._userId = '';
        this._loginStatus = LoginStatus.UNKNOWN;
        this.requestedLoginStatus = LoginStatus.UNKNOWN;
        this._i18n = createInstance();
        // Keycloak config
        this.keycloakUrl = null;
        this.realm = null;
        this.clientId = null;
        this.silentCheckSsoRedirectUri = null;
        this.scope = null;
        this.idpHint = '';
        this._onKCChanged = this._onKCChanged.bind(this);
    update(changedProperties) {
Patrizio Bekerle's avatar
Patrizio Bekerle committed
        // console.log("changedProperties", changedProperties);
        changedProperties.forEach((oldValue, propName) => {
            switch (propName) {
                case 'lang':
                    this._i18n.changeLanguage(this.lang);
Reiter, Christoph's avatar
Reiter, Christoph committed
                    break;
                case 'entryPointUrl':
                    // for preloading the instance
                    JSONLD.getInstance(this.entryPointUrl, this.lang);
Reiter, Christoph's avatar
Reiter, Christoph committed
                    break;
                case 'requestedLoginStatus':
Reiter, Christoph's avatar
Reiter, Christoph committed
                    console.log('requested-login-status changed', this.requestedLoginStatus);
                    switch (this.requestedLoginStatus) {
                        case LoginStatus.LOGGED_IN:
                            this._kcwrapper.login({lang: this.lang, scope: this.scope || ''});
Reiter, Christoph's avatar
Reiter, Christoph committed
                            break;
                        case LoginStatus.LOGGED_OUT:
                            // Keycloak will redirect right away without emitting events, so we have
                            // to do this manually here
                            if (this._loginStatus === LoginStatus.LOGGED_IN) {
                                this._setLoginStatus(LoginStatus.LOGGING_OUT);
                            }
                            this._kcwrapper.logout();
                            // In case logout was aborted, for example with beforeunload,
                            // revert back to being logged in
                            if (this._loginStatus === LoginStatus.LOGGING_OUT) {
                                this._setLoginStatus(LoginStatus.LOGGED_IN);
                            }
Reiter, Christoph's avatar
Reiter, Christoph committed
                            break;
Reiter, Christoph's avatar
Reiter, Christoph committed
                    break;
            }
        });

        super.update(changedProperties);
        jsonld = await JSONLD.getInstance(this.entryPointUrl, this.lang);
Reiter, Christoph's avatar
Reiter, Christoph committed
            baseUrl = jsonld.getApiUrlForEntityName('FrontendUser');
        } catch (error) {
Reiter, Christoph's avatar
Reiter, Christoph committed
            baseUrl = jsonld.getApiUrlForEntityName('Person');
        }
        const apiUrl = baseUrl + '/' + encodeURIComponent(userId);

        let response = await fetch(apiUrl, {
            headers: {
Reiter, Christoph's avatar
Reiter, Christoph committed
                Authorization: 'Bearer ' + this.token,
            },
        });
        if (!response.ok) {
            throw response;
        }
        let user = await response.json();
        let dummyUser = {
            roles: user['roles'] ?? [],
        };
        return dummyUser;
    }

    async _onKCChanged(event) {
        const kc = event.detail;

        if (kc.authenticated) {
Reiter, Christoph's avatar
Reiter, Christoph committed
            let tokenChanged = this.token !== kc.token;
            this.name = kc.idTokenParsed.name;
            this.token = kc.token;

            this.subject = kc.subject;
            const userId = kc.idTokenParsed.preferred_username;
Reiter, Christoph's avatar
Reiter, Christoph committed
            let userChanged = userId !== this._userId;
                let user;
                try {
                    user = await this._fetchUser(userId);
                } catch (error) {
                    // In case fetching the user failed then likely the API backend
                    // is not set up or broken. Return a user without any roles so we
                    // can show something at least.
                    console.error(error);
                    user = {roles: []};
                }
            if (this._user !== null) {
                this._setLoginStatus(LoginStatus.LOGGED_IN, tokenChanged || userChanged);
            }
        } else {
            if (this._loginStatus === LoginStatus.LOGGED_IN) {
                this._setLoginStatus(LoginStatus.LOGGING_OUT);
            }
Reiter, Christoph's avatar
Reiter, Christoph committed
            this.name = '';
            this.token = '';
            this.subject = '';
            this._userId = '';

            this._setLoginStatus(LoginStatus.LOGGED_OUT);
        }
    }

            'login-status': this._loginStatus,
Reiter, Christoph's avatar
Reiter, Christoph committed
            subject: this.subject,
            token: this.token,
            'user-full-name': this.name,
            'user-id': this._userId,
            // Deprecated
            'person-id': this._userId,
Reiter, Christoph's avatar
Reiter, Christoph committed
            person: this._user,
        // inject a window.DBPAuth variable for Cypress/Playwright
        if (window.Cypress || window.playwright) {
            window.DBPAuth = auth;
            console.log("Cypress/Playwright detected");
        }

        this.sendSetPropertyEvent('auth', auth);
    _setLoginStatus(status, force) {
Reiter, Christoph's avatar
Reiter, Christoph committed
        if (this._loginStatus === status && !force) return;

        this._loginStatus = status;
        this.sendSetPropertyEvents();
    }

    static get properties() {
        return {
            ...super.properties,
Reiter, Christoph's avatar
Reiter, Christoph committed
            lang: {type: String},
            forceLogin: {type: Boolean, attribute: 'force-login'},
            tryLogin: {type: Boolean, attribute: 'try-login'},
            entryPointUrl: {type: String, attribute: 'entry-point-url'},
            name: {type: String, attribute: false},
            token: {type: String, attribute: false},
            subject: {type: String, attribute: false},
            _userId: {type: String, attribute: false},
            _user: {type: Object, attribute: false},
            _loginStatus: {type: String, attribute: false},
            keycloakUrl: {type: String, attribute: 'url'},
            realm: {type: String},
            clientId: {type: String, attribute: 'client-id'},
            silentCheckSsoRedirectUri: {type: String, attribute: 'silent-check-sso-redirect-uri'},
            scope: {type: String},
            idpHint: {type: String, attribute: 'idp-hint'},
            requestedLoginStatus: {type: String, attribute: 'requested-login-status'},
    }

    connectedCallback() {
        super.connectedCallback();

Reiter, Christoph's avatar
Reiter, Christoph committed
        if (!this.keycloakUrl) throw Error('url not set');
        if (!this.realm) throw Error('realm not set');
        if (!this.clientId) throw Error('client-id not set');
        this._kcwrapper = new KeycloakWrapper(
            this.keycloakUrl,
            this.realm,
            this.clientId,
            this.silentCheckSsoRedirectUri,
            this.idpHint
        );
        this._kcwrapper.addEventListener('changed', this._onKCChanged);

        const handleLogin = async () => {
            try {
                if (this.forceLogin || this._kcwrapper.isLoggingIn()) {
                    this._setLoginStatus(LoginStatus.LOGGING_IN);
                    await this._kcwrapper.login({lang: this.lang, scope: this.scope || ''});
                } else if (this.tryLogin) {
                    this._setLoginStatus(LoginStatus.LOGGING_IN);
                    await this._kcwrapper.tryLogin();
                    if (!this._authenticated) {
                        this._setLoginStatus(LoginStatus.LOGGED_OUT);
                    }
                } else {
                    this._setLoginStatus(LoginStatus.LOGGED_OUT);
            } catch (error) {
                // In case the keycloak server is offline for example
                this._setLoginStatus(LoginStatus.LOGGED_OUT);
                send({
                    summary: this._i18n.t('login-failed'),
                    type: 'danger',
                    timeout: 5,
                });
                throw error;
            }
        };

        handleLogin();
    }

    disconnectedCallback() {
        this._kcwrapper.removeEventListener('changed', this._onKCChanged);

        super.disconnectedCallback();