From d9cf8ab4a958512a90a3e180af088b9cc0fc0001 Mon Sep 17 00:00:00 2001
From: Patrizio Bekerle <patrizio.bekerle@tugraz.at>
Date: Tue, 16 Jul 2019 09:49:57 +0200
Subject: [PATCH] Initial commit of the authentication web component with demo
 page

---
 packages/auth/.gitignore                |   3 +
 packages/auth/README.md                 |  17 ++
 packages/auth/favicon.ico               | Bin 0 -> 2550 bytes
 packages/auth/i18n.js                   |  29 ++++
 packages/auth/i18n/de/translation.json  |   4 +
 packages/auth/i18n/en/translation.json  |   4 +
 packages/auth/i18next-scanner.config.js |  12 ++
 packages/auth/index.html                |  14 ++
 packages/auth/index.js                  |   2 +
 packages/auth/jsonld.js                 | 218 ++++++++++++++++++++++++
 packages/auth/package.json              |  34 ++++
 packages/auth/rollup.config.js          |  41 +++++
 packages/auth/utils.js                  |  58 +++++++
 packages/auth/vars.js                   |  32 ++++
 packages/auth/vpu-auth-demo.js          |  43 +++++
 packages/auth/vpu-auth.js               | 183 ++++++++++++++++++++
 16 files changed, 694 insertions(+)
 create mode 100644 packages/auth/.gitignore
 create mode 100644 packages/auth/README.md
 create mode 100644 packages/auth/favicon.ico
 create mode 100644 packages/auth/i18n.js
 create mode 100644 packages/auth/i18n/de/translation.json
 create mode 100644 packages/auth/i18n/en/translation.json
 create mode 100644 packages/auth/i18next-scanner.config.js
 create mode 100644 packages/auth/index.html
 create mode 100644 packages/auth/index.js
 create mode 100644 packages/auth/jsonld.js
 create mode 100644 packages/auth/package.json
 create mode 100644 packages/auth/rollup.config.js
 create mode 100644 packages/auth/utils.js
 create mode 100644 packages/auth/vars.js
 create mode 100644 packages/auth/vpu-auth-demo.js
 create mode 100644 packages/auth/vpu-auth.js

diff --git a/packages/auth/.gitignore b/packages/auth/.gitignore
new file mode 100644
index 00000000..28e5452d
--- /dev/null
+++ b/packages/auth/.gitignore
@@ -0,0 +1,3 @@
+dist
+node_modules
+.idea
\ No newline at end of file
diff --git a/packages/auth/README.md b/packages/auth/README.md
new file mode 100644
index 00000000..aebebbcf
--- /dev/null
+++ b/packages/auth/README.md
@@ -0,0 +1,17 @@
+## VPU Auth Web Component
+
+[GitLab Repository](https://gitlab.tugraz.at/VPU/WebComponents/Auth)
+
+## Local development
+
+```bash
+npm install
+
+# constantly builds dist/bundle.js 
+npm run watch-local
+
+# run local webserver
+cd dist; php -S localhost:8002
+```
+
+Jump to <http://localhost:8002> and you should get a Single Sign On login page.
diff --git a/packages/auth/favicon.ico b/packages/auth/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..f6cf22d1afc22f071d2b64c7779bc5214c3adae5
GIT binary patch
literal 2550
zcmZQzU}Ruo5D);-91Iz(3=C=v3{buTLk0^2Lmw*xg9b>9fq_AR0iuq9fq}t+5kx{U
z68r!E{|pQa{~0DTTx0nE<_yEJYwsDZJ%7$HVdYu|hX2M43=C-u3=A_F7#PklFo3wm
z#taO`X$%a;GZ`3+L1O<QO45uO7}C-h7}91kFr=MfU`YE9(KgeVfnjDE1H;Uj3=A{R
zFfh#g4>pS7j4=blnKTB5Gcy?&&YWRjIP;%@;eQ$f!~dBK4FAtC7=ySB#tg;`X$-~;
zGZ~B-&M+7={0BS9*w~oC*f@>B*mx#`vGExOW8?o2C#4xP7^kH%7^lr-FitzeV4U_J
z;>?-G48}9l7>sAmWH6q2hQWB|e~8o17&923Nn<cRGn2vi%ozsbGyfTk{~I$H|4(Bu
z{y&q!80?P!;1B@0B8>qAXELNQoMA{~_|E|LvavBknsFLKn(<7AG~+W2X~zE{!H{Om
zkd~Ilkd`)+Aua6;Bv@t|Go;N-V@R7hlOb&;NbEl(sLmKOq@9_`kap$_L)w}D;Gj$U
zZ_JSPe<nlP|1%6{K>lVp0|`RonG6tk1{^ChjX|N4#xT<u#QzTo>NI19nZ{`hGt*`=
z%uGAOFw^)y!_1k+3^Qk@G0dDflVRpekXipB(E+wQjbY}QnG7?}oMD)G=07-!X8t#3
zn0Y3RVdnpt3^V_qVVL><KPUx2V*(TcGZ{cG14Zu{V`GLh#%T;^jAt^OF+Rg^#`r%Z
zYC-NuOJg{bHk08D$Q^0_A<;e4nBmOKG=?)XXEL0bd4}Q4%>R%iaK@P7%$YQXGiPQp
zoH=ub;mn!;;AC+I<d*+w3}^n&WH<u~53pN6sR0xVAUi=x)tKSGaT>#a<CzTqjn6Rr
zH~tSvPHDyrXVTIb{-@1k_@8!$;Y`|pND`ZA%<z9^8pHpYGa3HRJj3vR=6_JKWB7l@
znBo7KG=~3YW-|OgbB5vnng0y`{~I%$`Jcw{|Nl&e|NqZ0q@{u4D2*ZQKgbS{*&sD(
z42ld(8RFyP8OqDc8QR*~7^Y2|#&G)dX@*UkHZfekex2d&-Mb8rA3tVz_39PF$B!Qw
ze*E~s@ZtA=h9wIYFf=qYfb$^(I6MA_P@s4R<%9qK8RlI}XAod61>+Q628P?~wlQq0
zO=L(CPiHu~?l8mTy^|Ss)%G&jvhy<B<Xg=UC0oa^rAUM!%<3M)xjF9`YL9+lc&VSx
za9-d%gYn~S45u}ZKnnwkC?+N*CS(Ip`5+z`Gk{12FbQHYfmskfm<bUCGr<Iu4>cLY
zgEGMsIv*@Z1_3JiknzL+3=AOLpvb@g!Yzyp3?TfUpMe34!EwRB0Mmn7&Vou?1_lQf
zG8{6Zw7^0SMw9A!m_8UytbUk!d^E@`ba{{%2&0Q*<6~0?5(8m$_1MJF)qwaQbs!94
zBV%l0$nr2YNF5;zQwyUBsYjQG>4VYe@*|rM3l$g*Dq}zxRK|cXsEh$&a2W$CYtYIW
E03q~Dvj6}9

literal 0
HcmV?d00001

diff --git a/packages/auth/i18n.js b/packages/auth/i18n.js
new file mode 100644
index 00000000..a2380632
--- /dev/null
+++ b/packages/auth/i18n.js
@@ -0,0 +1,29 @@
+import i18next from 'i18next';
+
+import de from './i18n/de/translation.json';
+import en from './i18n/en/translation.json';
+
+const i18n = i18next.createInstance();
+
+i18n.init({
+    lng: 'de',
+    fallbackLng: ['de'],
+    debug: false,
+    initImmediate: false, // Don't init async
+    resources: {
+        en: {translation: en},
+        de: {translation: de}
+    },
+});
+
+console.assert(i18n.isInitialized);
+
+function dateTimeFormat(date, options) {
+    return new Intl.DateTimeFormat(i18n.languages, options).format(date);
+}
+
+function numberFormat(number, options) {
+    return new Intl.NumberFormat(i18n.languages, options).format(number);
+}
+
+export {i18n, dateTimeFormat, numberFormat};
diff --git a/packages/auth/i18n/de/translation.json b/packages/auth/i18n/de/translation.json
new file mode 100644
index 00000000..48287f0d
--- /dev/null
+++ b/packages/auth/i18n/de/translation.json
@@ -0,0 +1,4 @@
+{
+  "login": "Einloggen",
+  "logout": "Ausloggen"
+}
diff --git a/packages/auth/i18n/en/translation.json b/packages/auth/i18n/en/translation.json
new file mode 100644
index 00000000..8c6f4faa
--- /dev/null
+++ b/packages/auth/i18n/en/translation.json
@@ -0,0 +1,4 @@
+{
+  "login": "Login",
+  "logout": "Logout"
+}
diff --git a/packages/auth/i18next-scanner.config.js b/packages/auth/i18next-scanner.config.js
new file mode 100644
index 00000000..6c112e37
--- /dev/null
+++ b/packages/auth/i18next-scanner.config.js
@@ -0,0 +1,12 @@
+module.exports = {
+    input: [
+        '*.js',
+    ],
+    output: './',
+    options: {
+        debug: false,
+        removeUnusedKeys: true,
+        sort: true,
+        lngs: ['en','de'],
+    },
+}
diff --git a/packages/auth/index.html b/packages/auth/index.html
new file mode 100644
index 00000000..f332292e
--- /dev/null
+++ b/packages/auth/index.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<html>
+<head>
+    <meta charset="UTF-8">
+    <script src="webcomponents-loader.js"></script>
+    <script type="module" id="vpu-auth-wc-src" src="bundle.js"></script>
+</head>
+
+<body>
+
+<vpu-auth-demo lang="de"></vpu-auth-demo>
+
+</body>
+</html>
diff --git a/packages/auth/index.js b/packages/auth/index.js
new file mode 100644
index 00000000..6f9872ca
--- /dev/null
+++ b/packages/auth/index.js
@@ -0,0 +1,2 @@
+import './vpu-auth';
+import './vpu-auth-demo';
diff --git a/packages/auth/jsonld.js b/packages/auth/jsonld.js
new file mode 100644
index 00000000..6079bb66
--- /dev/null
+++ b/packages/auth/jsonld.js
@@ -0,0 +1,218 @@
+"use strict";
+
+let instances = {};
+let successFunctions = {};
+let failureFunctions = {};
+let initStarted = {};
+
+module.exports = class JSONLD {
+    constructor(baseApiUrl, entities) {
+        this.entities = entities;
+        this.baseApiUrl = baseApiUrl;
+
+        let idToEntityNameMatchList = {};
+        for (const entityName in entities) {
+            const id = entities[entityName]["@id"];
+            idToEntityNameMatchList[id] = entityName;
+        }
+
+        this.idToEntityNameMatchList = idToEntityNameMatchList;
+    }
+
+    static initialize(apiUrl, successFnc, failureFnc) {
+        // if init api call was already successfully finished execute the success function
+        if (instances[apiUrl] !== undefined) {
+            if (typeof successFnc == 'function') successFnc(instances[apiUrl]);
+
+            return;
+        }
+
+        // init the arrays
+        if (successFunctions[apiUrl] === undefined) successFunctions[apiUrl] = [];
+        if (failureFunctions[apiUrl] === undefined) failureFunctions[apiUrl] = [];
+
+        // add success and failure functions
+        if (typeof successFnc == 'function') successFunctions[apiUrl].push(successFnc);
+        if (typeof failureFnc == 'function') failureFunctions[apiUrl].push(failureFnc);
+
+        // check if api call was already started
+        if (initStarted[apiUrl] !== undefined) {
+            return;
+        }
+
+        initStarted[apiUrl] = true;
+
+        // window.VPUAuthToken will be set by on vpu-auth-init
+        document.addEventListener("vpu-auth-init", function(e)
+        {
+            const xhr = new XMLHttpRequest();
+            xhr.open("GET", apiUrl, true);
+            xhr.setRequestHeader('Authorization', 'Bearer ' + window.VPUAuthToken);
+
+            xhr.onreadystatechange = function () {
+                if (xhr.readyState === 4 && xhr.status === 200) {
+                    const json = JSON.parse(xhr.responseText);
+
+                    let entryPoints = {};
+                    for (let property in json) {
+                        // for some reason the properties start with a lower case character
+                        if (!property.startsWith("@")) entryPoints[property.toLowerCase()] = json[property];
+                    }
+
+                    // read the link header of the api response
+                    const utils = require("./utils");
+                    const links = utils.parseLinkHeader(this.getResponseHeader("link"));
+
+                    // get the hydra apiDocumentation url
+                    const apiDocUrl = links["http://www.w3.org/ns/hydra/core#apiDocumentation"];
+
+                    if (apiDocUrl !== undefined) {
+                        // load the hydra apiDocumentation
+                        const docXhr = new XMLHttpRequest();
+                        docXhr.open("GET", apiDocUrl, true);
+                        docXhr.setRequestHeader("Content-Type", "application/json");
+                        docXhr.onreadystatechange = function () {
+                            if (docXhr.readyState === 4 && docXhr.status === 200) {
+                                const json = JSON.parse(docXhr.responseText);
+                                const supportedClasses = json["hydra:supportedClass"];
+
+                                let entities = {};
+                                const baseUrl = utils.parseBaseUrl(apiUrl);
+
+                                // gather the entities
+                                supportedClasses.forEach(function (classData) {
+                                    // add entry point url
+                                    const entityName = classData["hydra:title"];
+                                    let entryPoint = entryPoints[entityName.toLowerCase()];
+                                    if (entryPoint !== undefined && !entryPoint.startsWith("http")) entryPoint = baseUrl + entryPoint;
+                                    classData["@entryPoint"] = entryPoint;
+
+                                    entities[entityName] = classData;
+                                });
+
+                                const instance = new JSONLD(baseUrl, entities);
+                                instances[apiUrl] = instance;
+
+                                // return the initialized JSONLD object
+                                for (const fnc of successFunctions[apiUrl]) if (typeof fnc == 'function') fnc(instance);
+                                successFunctions[apiUrl] = [];
+                            } else {
+                                for (const fnc of failureFunctions[apiUrl]) if (typeof fnc == 'function') fnc();
+                                failureFunctions[apiUrl] = [];
+                            }
+                        };
+
+                        docXhr.send();
+                    } else {
+                        for (const fnc of failureFunctions[apiUrl]) if (typeof fnc == 'function') fnc();
+                        failureFunctions[apiUrl] = [];
+                    }
+                } else {
+                    for (const fnc of failureFunctions[apiUrl]) if (typeof fnc == 'function') fnc();
+                    failureFunctions[apiUrl] = [];
+                }
+            };
+
+            xhr.send();
+        });
+    }
+
+    static getInstance(apiUrl) {
+        return instances[apiUrl];
+    }
+
+    getEntityForIdentifier(identifier) {
+        let entityName = this.getEntityNameForIdentifier(identifier);
+        return this.getEntityForEntityName(entityName);
+    }
+
+    getEntityForEntityName(entityName) {
+        return this.entities[entityName];
+    }
+
+    getApiUrlForIdentifier(identifier) {
+        return this.getEntityForIdentifier(identifier)["@entryPoint"];
+    }
+
+    getApiUrlForEntityName(entityName) {
+        return this.getEntityForEntityName(entityName)["@entryPoint"];
+    }
+
+    getEntityNameForIdentifier(identifier) {
+        return this.idToEntityNameMatchList[identifier];
+    }
+
+    getApiIdentifierList() {
+        let keys = [];
+        for (const property in this.idToEntityNameMatchList) {
+            keys.push(property);
+        }
+
+        return keys;
+    }
+
+    /**
+     * Expands a member of a list to a object with schema.org properties
+     *
+     * @param member
+     */
+    expandMember(member) {
+        const type = member["@type"];
+
+        const entity = this.getEntityForIdentifier(type);
+        let result = {"@id": member["@id"]};
+
+        entity["hydra:supportedProperty"].forEach(function (property) {
+            const id = property["hydra:property"]["@id"];
+            const title = property["hydra:title"];
+
+            result[id] = member[title];
+        });
+
+        return result;
+    }
+
+    /**
+     * Compacts an expanded member of a list to a object with local properties
+     *
+     * @param member
+     * @param localContext
+     */
+    static compactMember(member, localContext) {
+        let result = {};
+
+        for (const property in localContext) {
+            const value = member[localContext[property]];
+
+            if (value !== undefined) {
+                result[property] = value;
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Transforms hydra members to a local context
+     *
+     * @param data
+     * @param localContext
+     * @returns {Array}
+     */
+    transformMembers(data, localContext) {
+        const members = data['hydra:member'];
+
+        if (members === undefined || members.length === 0) {
+            return [];
+        }
+
+        let results = [];
+        let that = this;
+
+        members.forEach(function (member) {
+            results.push(JSONLD.compactMember(that.expandMember(member), localContext));
+        });
+
+        return results;
+    }
+};
diff --git a/packages/auth/package.json b/packages/auth/package.json
new file mode 100644
index 00000000..29ab4b98
--- /dev/null
+++ b/packages/auth/package.json
@@ -0,0 +1,34 @@
+{
+  "name": "vpu-auth-wc",
+  "version": "1.0.0",
+  "devDependencies": {
+    "node-sass": "^4.12.0",
+    "rollup": "^1.11.3",
+    "rollup-plugin-commonjs": "^9.3.4",
+    "rollup-plugin-copy": "^2.0.1",
+    "rollup-plugin-node-resolve": "^4.2.3",
+    "rollup-plugin-postcss": "^2.0.3",
+    "rollup-plugin-serve": "^1.0.1",
+    "rollup-plugin-terser": "^4.0.4",
+    "rollup-plugin-json": "^4.0.0",
+    "rollup-plugin-replace": "^2.2.0",
+    "i18next-scanner": "^2.10.2"
+  },
+  "dependencies": {
+    "@webcomponents/webcomponentsjs": "^2.2.10",
+    "lit-element": "^2.1.0",
+    "i18next": "^17.0.3"
+  },
+  "scripts": {
+    "clean": "rm dist/*",
+    "build": "npm run build-local",
+    "build-local": "rollup -c",
+    "build-dev": "rollup -c --environment BUILD:development",
+    "build-prod": "rollup -c --environment BUILD:production",
+    "build-demo": "rollup -c --environment BUILD:demo",
+    "i18next": "i18next-scanner",
+    "watch": "npm run watch-local",
+    "watch-local": "rollup -c --watch",
+    "watch-dev": "rollup -c --watch --environment BUILD:development"
+  }
+}
diff --git a/packages/auth/rollup.config.js b/packages/auth/rollup.config.js
new file mode 100644
index 00000000..affebebe
--- /dev/null
+++ b/packages/auth/rollup.config.js
@@ -0,0 +1,41 @@
+import resolve from 'rollup-plugin-node-resolve';
+import commonjs from 'rollup-plugin-commonjs';
+import postcss from 'rollup-plugin-postcss';
+import copy from 'rollup-plugin-copy';
+import {terser} from "rollup-plugin-terser";
+import json from 'rollup-plugin-json';
+import replace from "rollup-plugin-replace";
+
+const build = (typeof process.env.BUILD !== 'undefined') ? process.env.BUILD : 'local';
+console.log("build: " + build);
+
+export default {
+    input: 'index.js',
+    output: {
+        file: 'dist/bundle.js',
+        format: 'esm'
+    },
+    plugins: [
+        resolve(),
+        commonjs(),
+        json(),
+        replace({
+            "process.env.BUILD": '"' + build + '"',
+        }),
+        postcss({
+            inject: false,
+            minimize: false,
+            plugins: []
+        }),
+        terser(),
+        copy({
+            targets: [
+                'index.html',
+                'favicon.ico',
+                'node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js',
+                'node_modules/@webcomponents/webcomponentsjs/bundles',
+            ],
+            outputFolder: 'dist'
+        })
+    ]
+};
diff --git a/packages/auth/utils.js b/packages/auth/utils.js
new file mode 100644
index 00000000..30eaa4ec
--- /dev/null
+++ b/packages/auth/utils.js
@@ -0,0 +1,58 @@
+const vars = require("./vars");
+
+module.exports = {
+    getAPiUrl: function(path = "", withPrefix = true) {
+        return vars.apiBaseUrl + (withPrefix ? vars.apiUrlPrefix : "") + path;
+    },
+
+    /**
+     * Parses a link header
+     *
+     * The node module parse-link-header didn't work, so https://gist.github.com/niallo/3109252 became handy
+     *
+     * @param header
+     */
+    parseLinkHeader: (header) => {
+        if (header.length === 0) {
+            throw new Error("input must not be of zero length");
+        }
+
+        // Split parts by comma
+        const parts = header.split(',');
+        const links = {};
+
+        // Parse each part into a named link
+        for(let i=0; i<parts.length; i++) {
+            const section = parts[i].split(';');
+            if (section.length !== 2) {
+                throw new Error("section could not be split on ';'");
+            }
+            const url = section[0].replace(/<(.*)>/, '$1').trim();
+            const name = section[1].replace(/rel="(.*)"/, '$1').trim();
+            links[name] = url;
+        }
+
+        return links;
+    },
+
+    /**
+     * Parses the base url from an url
+     *
+     * @param url
+     * @returns {string}
+     */
+    parseBaseUrl: (url) => {
+        const pathArray = url.split('/');
+        const protocol = pathArray[0];
+        const host = pathArray[2];
+        return protocol + '//' + host;
+    },
+
+    /**
+     * Reads a setting
+     *
+     * @param key
+     * @returns {*}
+     */
+    setting: (key) => vars[key]
+};
diff --git a/packages/auth/vars.js b/packages/auth/vars.js
new file mode 100644
index 00000000..d34d7b63
--- /dev/null
+++ b/packages/auth/vars.js
@@ -0,0 +1,32 @@
+
+switch(process.env.BUILD) {
+    case "development":
+        module.exports = {
+            apiBaseUrl: 'https://mw-dev.tugraz.at',
+            apiUrlPrefix: '',
+            keyCloakClientId: 'auth-dev-mw-frontend',
+        };
+
+        break;
+    case "production":
+        module.exports = {
+            apiBaseUrl: 'https://mw.tugraz.at',
+            apiUrlPrefix: '',
+            keyCloakClientId: 'auth-prod-mw-frontend',
+        };
+        break;
+    case "demo":
+        module.exports = {
+            apiBaseUrl: 'https://api-demo.tugraz.at',
+            apiUrlPrefix: '',
+            keyCloakClientId: 'auth-dev-mw-frontend',
+        };
+        break;
+    case "local":
+    default:
+        module.exports = {
+            apiBaseUrl: 'http://127.0.0.1:8000',
+            apiUrlPrefix: '',
+            keyCloakClientId: 'auth-dev-mw-frontend-local',
+        };
+}
diff --git a/packages/auth/vpu-auth-demo.js b/packages/auth/vpu-auth-demo.js
new file mode 100644
index 00000000..af9600c3
--- /dev/null
+++ b/packages/auth/vpu-auth-demo.js
@@ -0,0 +1,43 @@
+import utils from './utils.js';
+import {i18n} from './i18n.js';
+import {html, LitElement} from 'lit-element';
+
+class LibraryShelving extends LitElement {
+    constructor() {
+        super();
+        this.lang = 'de';
+    }
+
+    static get properties() {
+        return {
+            lang: { type: String },
+        };
+    }
+
+    connectedCallback() {
+        super.connectedCallback();
+        i18n.changeLanguage(this.lang);
+
+        this.updateComplete.then(()=>{
+        });
+    }
+
+    render() {
+        return html`
+            <style>
+            </style>
+            <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css">
+
+            <section class="section">
+                <div class="container">
+                    <h1 class="title">Auth-Demo</h1>
+                </div>
+                <div class="container">
+                    <vpu-auth lang="${this.lang}" client-id="${utils.setting('keyCloakClientId')}" load-person></vpu-auth>
+                </div>
+            </section>
+        `;
+    }
+}
+
+customElements.define('vpu-auth-demo', LibraryShelving);
diff --git a/packages/auth/vpu-auth.js b/packages/auth/vpu-auth.js
new file mode 100644
index 00000000..3f15b840
--- /dev/null
+++ b/packages/auth/vpu-auth.js
@@ -0,0 +1,183 @@
+import {i18n} from './i18n.js';
+import {html, LitElement} from 'lit-element';
+import JSONLD from "./jsonld";
+import utils from "./utils";
+
+/**
+ * Keycloak auth web component
+ * https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter
+ *
+ * Dispatches an event `vpu-auth-init` and sets some global variables:
+ *   window.VPUAuthSubject: Keycloak username
+ *   window.VPUAuthToken: Keycloak token to send with your requests
+ *   window.VPUUserFullName: Full name of the user
+ *   window.VPUPersonId: Person identifier of the user
+ *   window.VPUPerson: Person json object of the user (optional, enable by setting the `load-person` attribute,
+ *                     which will dispatch a `vpu-auth-person-init` event when loaded)
+ */
+class VPUAuth extends LitElement {
+    constructor() {
+        super();
+        this.lang = 'de';
+        this.loadPerson = false;
+        this.clientId = "";
+        this.keyCloakInitCalled = false;
+        this._keycloak = null;
+        this.token = "";
+        this.subject = "";
+        this.name = "";
+        this.personId = "";
+
+        // Create the init event
+        this.initEvent = new CustomEvent("vpu-auth-init", { "detail": "KeyCloak init event" });
+        this.personInitEvent = new CustomEvent("vpu-auth-person-init", { "detail": "KeyCloak person init event" });
+    }
+
+    /**
+     * See: https://lit-element.polymer-project.org/guide/properties#initialize
+     */
+    static get properties() {
+        return {
+            lang: { type: String },
+            loadPerson: { type: Boolean, attribute: 'load-person' },
+            clientId: { type: String, attribute: 'client-id' },
+            name: { type: String, attribute: false },
+            token: { type: String, attribute: false },
+            subject: { type: String, attribute: false },
+            personId: { type: String, attribute: false },
+            keycloak: { type: Object, attribute: false },
+        };
+    }
+
+    connectedCallback() {
+        super.connectedCallback();
+        i18n.changeLanguage(this.lang);
+
+        this.loadKeyCloak();
+
+        this.updateComplete.then(()=>{
+        });
+    }
+
+    loadKeyCloak() {
+        const that = this;
+        console.log("loadKeyCloak");
+
+        if (!this.keyCloakInitCalled) {
+            // inject Keycloak javascript file
+            const script = document.createElement('script');
+            script.type = 'text/javascript';
+            script.async = true;
+            script.onload = function () {
+                that.keyCloakInitCalled = true;
+
+                that._keycloak = Keycloak({
+                    url: 'https://auth-dev.tugraz.at/auth',
+                    realm: 'tugraz',
+                    clientId: that.clientId,
+                });
+
+                that._keycloak.init({onLoad: 'login-required'}).success(function (authenticated) {
+                    console.log(authenticated ? 'authenticated' : 'not authenticated!');
+                    console.log(that._keycloak);
+
+                    that.updateKeycloakData();
+                    that.dispatchInitEvent();
+
+                    if (that.loadPerson) {
+                        JSONLD.initialize(utils.getAPiUrl(), (jsonld) => {
+                            // find the correct api url for the current person
+                            // we are fetching the logged-in person directly to respect the REST philosophy
+                            // see: https://github.com/api-platform/api-platform/issues/337
+                            const apiUrl = jsonld.getApiUrlForEntityName("Person") + '/' + that.personId;
+
+                            fetch(apiUrl, {
+                                headers: {
+                                    'Content-Type': 'application/ld+json',
+                                    'Authorization': 'Bearer ' + that.token,
+                                },
+                            })
+                            .then(response => response.json())
+                            .then((person) => {
+                                window.VPUPerson = person;
+                                that.dispatchPersonInitEvent();
+                            });
+                        });
+                    }
+
+                }).error(function () {
+                    console.log('Failed to initialize');
+                });
+
+                // auto-refresh token
+                that._keycloak.onTokenExpired = function() {
+                    that._keycloak.updateToken(5).success(function(refreshed) {
+                        if (refreshed) {
+                            console.log('Token was successfully refreshed');
+                            that.updateKeycloakData();
+                        } else {
+                            console.log('Token is still valid');
+                        }
+                    }).error(function() {
+                        console.log('Failed to refresh the token, or the session has expired');
+                    });
+                }
+            };
+
+            // https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_a
+            script.src = '//auth-dev.tugraz.at/auth/js/keycloak.js';
+
+            //Append it to the document header
+            document.head.appendChild(script);
+        }
+    }
+
+    logout(e) {
+        this._keycloak.logout();
+    }
+
+    /**
+     * Dispatches the init event
+     */
+    dispatchInitEvent() {
+        document.dispatchEvent(this.initEvent);
+    }
+
+    /**
+     * Dispatches the person init event
+     */
+    dispatchPersonInitEvent() {
+        document.dispatchEvent(this.personInitEvent);
+    }
+
+    updateKeycloakData() {
+        this.name = this._keycloak.idTokenParsed.name;
+        this.token = this._keycloak.token;
+        this.subject = this._keycloak.subject;
+        this.personId = this._keycloak.idTokenParsed.preferred_username;
+
+        window.VPUAuthSubject = this.subject;
+        window.VPUAuthToken = this.token;
+        window.VPUUserFullName = this.name;
+        window.VPUPersonId = this.personId;
+
+        console.log("Bearer " + this.token);
+    }
+
+    render() {
+        return html`
+            <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css">
+
+            <div class="columns is-vcentered"">
+                <div class="column">
+                    ${this.name}
+                </div>
+                <div class="column">
+                    <button @click="${this.logout}" class="button">${i18n.t('logout')}</button>
+                </div>
+            </div>
+        `;
+    }
+}
+
+customElements.define('vpu-auth', VPUAuth);
-- 
GitLab