@digital-blueprint/lunchlottery-app
Version:
[GitHub Repository](https://github.com/digital-blueprint/lunchlottery-app) | [npmjs package](https://www.npmjs.com/package/@digital-blueprint/lunchlottery-app) | [Unpkg CDN](https://unpkg.com/browse/@digital-blueprint/lunchlottery-app/)
255 lines (232 loc) • 9.49 kB
JavaScript
import {createInstance} from './i18n.js';
import {KeycloakWrapper} from './keycloak.js';
import {LoginStatus} from './util.js';
import {AdapterLitElement, combineURLs, LangMixin, sendNotification} from '@dbp-toolkit/common';
/**
* 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.subject: Keycloak username
* 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
* auth.user-id: Identifier of the user
*/
export class AuthKeycloak extends LangMixin(AdapterLitElement, createInstance) {
constructor() {
super();
this.forceLogin = false;
this.token = '';
this.subject = '';
this.name = '';
this.tryLogin = false;
this.entryPointUrl = '';
this._user = null;
this._userId = '';
this._authenticated = false;
this._loginStatus = LoginStatus.UNKNOWN;
this.requestedLoginStatus = LoginStatus.UNKNOWN;
// Keycloak config
this.keycloakUrl = null;
this.realm = null;
this.clientId = null;
this.silentCheckSsoRedirectUri = null;
this.noCheckLoginIframe = false;
this.scope = null;
this.idpHint = '';
this._onKCChanged = this._onKCChanged.bind(this);
// inject a data-testid attribute for Playwright
if (window.playwright) {
this.setAttribute('data-testid', 'dbp-auth-keycloak');
}
}
update(changedProperties) {
// console.log("changedProperties", changedProperties);
changedProperties.forEach((oldValue, propName) => {
switch (propName) {
case 'requestedLoginStatus': {
console.log('requested-login-status changed', this.requestedLoginStatus);
let newStatus = this.requestedLoginStatus;
// reset so the next change will be detected if below fails or gets cancelled
this.requestedLoginStatus = LoginStatus.UNKNOWN;
switch (newStatus) {
case LoginStatus.LOGGED_IN:
this._kcwrapper.login({lang: this.lang, scope: this.scope || ''});
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);
}
break;
}
break;
}
}
});
super.update(changedProperties);
}
async _fetchUser(userId, token) {
const apiUrl = combineURLs(
this.entryPointUrl,
`/frontend/users/${encodeURIComponent(userId)}`,
);
let response = await fetch(apiUrl, {
headers: {
Authorization: 'Bearer ' + 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;
this._authenticated = kc.authenticated;
if (kc.authenticated) {
let userChanged = kc.subject !== this.subject;
if (userChanged) {
if (this._loginStatus === LoginStatus.LOGGED_IN) {
this._setLoginStatus(LoginStatus.LOGGING_OUT);
this._setLoggedOut();
}
const userId = kc.idTokenParsed.preferred_username;
this._userId = userId;
let user;
try {
user = await this._fetchUser(userId, kc.token);
} 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 (userId === this._userId) {
this._user = user;
}
}
let tokenChanged = this.token !== kc.token;
this.token = kc.token;
this.name = kc.idTokenParsed.name;
this.subject = kc.subject;
if (this._user !== null) {
this._setLoginStatus(LoginStatus.LOGGED_IN, tokenChanged);
}
} else {
if (this._loginStatus === LoginStatus.LOGGED_IN) {
this._setLoginStatus(LoginStatus.LOGGING_OUT);
}
this._setLoggedOut();
}
}
_setLoggedOut() {
this.name = '';
this.token = '';
this.subject = '';
this._userId = '';
this._user = null;
this._setLoginStatus(LoginStatus.LOGGED_OUT);
}
sendSetPropertyEvents() {
const auth = {
'login-status': this._loginStatus,
subject: this.subject,
token: this.token,
'user-full-name': this.name,
'user-id': this._userId,
// Deprecated
'person-id': this._userId,
person: this._user,
_roles: this._user ? this._user.roles : [],
};
this.sendSetPropertyEvent('auth', auth);
}
_setLoginStatus(status, force) {
if (this._loginStatus === status && !force) return;
this._loginStatus = status;
this.sendSetPropertyEvents();
}
static get properties() {
return {
...super.properties,
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'},
noCheckLoginIframe: {type: Boolean, attribute: 'no-check-login-iframe'},
};
}
connectedCallback() {
super.connectedCallback();
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.noCheckLoginIframe,
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);
sendNotification({
summary: this._i18n.t('login-failed'),
type: 'danger',
timeout: 5,
});
throw error;
}
};
handleLogin();
}
disconnectedCallback() {
this._kcwrapper.close();
this._kcwrapper.removeEventListener('changed', this._onKCChanged);
super.disconnectedCallback();
}
}