Skip to content
Snippets Groups Projects
Commit d9cf8ab4 authored by Bekerle, Patrizio's avatar Bekerle, Patrizio :fire: Committed by Reiter, Christoph
Browse files

Initial commit of the authentication web component with demo page

parent 981fb3e7
No related branches found
No related tags found
No related merge requests found
Showing with 694 additions and 0 deletions
dist
node_modules
.idea
\ No newline at end of file
## 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.
packages/auth/favicon.ico

2.49 KiB

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};
{
"login": "Einloggen",
"logout": "Ausloggen"
}
{
"login": "Login",
"logout": "Logout"
}
module.exports = {
input: [
'*.js',
],
output: './',
options: {
debug: false,
removeUnusedKeys: true,
sort: true,
lngs: ['en','de'],
},
}
<!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>
import './vpu-auth';
import './vpu-auth-demo';
"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;
}
};
{
"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"
}
}
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'
})
]
};
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]
};
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',
};
}
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);
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);
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment