diff --git a/README.md b/README.md
index 0560c10d..f0341d14 100644
--- a/README.md
+++ b/README.md
@@ -85,6 +85,16 @@ WBO supports authentication with a JWT. This should be passed in as a query with
The `AUTH_SECRET_KEY` variable in [`configuration.js`](./server/configuration.js) should be filled with the secret key for the JWT.
+WBO supports authentication with OIDC based on [`keycloak`](https://github.com/keycloak)
+Some important environment variables are :
+- `KEYCLOAK_ENABLE` is used for enable OIDC authentication
+- `KEYCLOAK_URL` is the URL of Keycloak, eg, `https://keycloak-server/auth`
+- `KEYCLOAK_REALM` is the Realm name, eg, **myrealm**
+- `KEYCLOAK_CLIENTID` is the Public Client Id, eg, **myapp**
+- `KEYCLOAK_USERINFO_ATTRIBUTE` is the attribute name for authorization, and it is not mandatory
+
+For any further information you can see [`here`](https://www.keycloak.org/docs/latest/securing_apps/#_javascript_adapter)
+
## Configuration
When you start a WBO server, it loads its configuration from several environment variables.
diff --git a/client-data/board.html b/client-data/board.html
index f6516cd9..e0991a97 100644
--- a/client-data/board.html
+++ b/client-data/board.html
@@ -21,6 +21,7 @@
{{/languages}}
+
@@ -90,6 +91,7 @@
+
diff --git a/client-data/js/keycloak.js b/client-data/js/keycloak.js
new file mode 100644
index 00000000..3d1d2730
--- /dev/null
+++ b/client-data/js/keycloak.js
@@ -0,0 +1,2387 @@
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define('keycloak', factory) :
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Keycloak = factory());
+})(this, (function () { 'use strict';
+
+ var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
+
+ function commonjsRequire (path) {
+ throw new Error('Could not dynamically require "' + path + '". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.');
+ }
+
+ var es6Promise_min = {exports: {}};
+
+ (function (module, exports) {
+ !function(t,e){module.exports=e();}(commonjsGlobal,function(){function t(t){var e=typeof t;return null!==t&&("object"===e||"function"===e)}function e(t){return "function"==typeof t}function n(t){W=t;}function r(t){z=t;}function o(){return function(){return process.nextTick(a)}}function i(){return "undefined"!=typeof U?function(){U(a);}:c()}function s(){var t=0,e=new H(a),n=document.createTextNode("");return e.observe(n,{characterData:!0}),function(){n.data=t=++t%2;}}function u(){var t=new MessageChannel;return t.port1.onmessage=a,function(){return t.port2.postMessage(0)}}function c(){var t=setTimeout;return function(){return t(a,1)}}function a(){for(var t=0;t 0) {
+ throw new Error('Invalid string. Length must be a multiple of 4')
+ }
+
+ // Trim off extra bytes after placeholder bytes are found
+ // See: https://github.com/beatgammit/base64-js/issues/42
+ var validLen = b64.indexOf('=');
+ if (validLen === -1) validLen = len;
+
+ var placeHoldersLen = validLen === len
+ ? 0
+ : 4 - (validLen % 4);
+
+ return [validLen, placeHoldersLen]
+ }
+
+ // base64 is 4/3 + up to two characters of the original data
+ function byteLength (b64) {
+ var lens = getLens(b64);
+ var validLen = lens[0];
+ var placeHoldersLen = lens[1];
+ return ((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen
+ }
+
+ function _byteLength (b64, validLen, placeHoldersLen) {
+ return ((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen
+ }
+
+ function toByteArray (b64) {
+ var tmp;
+ var lens = getLens(b64);
+ var validLen = lens[0];
+ var placeHoldersLen = lens[1];
+
+ var arr = new Arr(_byteLength(b64, validLen, placeHoldersLen));
+
+ var curByte = 0;
+
+ // if there are placeholders, only get up to the last complete 4 chars
+ var len = placeHoldersLen > 0
+ ? validLen - 4
+ : validLen;
+
+ var i;
+ for (i = 0; i < len; i += 4) {
+ tmp =
+ (revLookup[b64.charCodeAt(i)] << 18) |
+ (revLookup[b64.charCodeAt(i + 1)] << 12) |
+ (revLookup[b64.charCodeAt(i + 2)] << 6) |
+ revLookup[b64.charCodeAt(i + 3)];
+ arr[curByte++] = (tmp >> 16) & 0xFF;
+ arr[curByte++] = (tmp >> 8) & 0xFF;
+ arr[curByte++] = tmp & 0xFF;
+ }
+
+ if (placeHoldersLen === 2) {
+ tmp =
+ (revLookup[b64.charCodeAt(i)] << 2) |
+ (revLookup[b64.charCodeAt(i + 1)] >> 4);
+ arr[curByte++] = tmp & 0xFF;
+ }
+
+ if (placeHoldersLen === 1) {
+ tmp =
+ (revLookup[b64.charCodeAt(i)] << 10) |
+ (revLookup[b64.charCodeAt(i + 1)] << 4) |
+ (revLookup[b64.charCodeAt(i + 2)] >> 2);
+ arr[curByte++] = (tmp >> 8) & 0xFF;
+ arr[curByte++] = tmp & 0xFF;
+ }
+
+ return arr
+ }
+
+ function tripletToBase64 (num) {
+ return lookup[num >> 18 & 0x3F] +
+ lookup[num >> 12 & 0x3F] +
+ lookup[num >> 6 & 0x3F] +
+ lookup[num & 0x3F]
+ }
+
+ function encodeChunk (uint8, start, end) {
+ var tmp;
+ var output = [];
+ for (var i = start; i < end; i += 3) {
+ tmp =
+ ((uint8[i] << 16) & 0xFF0000) +
+ ((uint8[i + 1] << 8) & 0xFF00) +
+ (uint8[i + 2] & 0xFF);
+ output.push(tripletToBase64(tmp));
+ }
+ return output.join('')
+ }
+
+ function fromByteArray (uint8) {
+ var tmp;
+ var len = uint8.length;
+ var extraBytes = len % 3; // if we have 1 byte left, pad 2 bytes
+ var parts = [];
+ var maxChunkLength = 16383; // must be multiple of 3
+
+ // go through the array every three bytes, we'll deal with trailing stuff later
+ for (var i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) {
+ parts.push(encodeChunk(uint8, i, (i + maxChunkLength) > len2 ? len2 : (i + maxChunkLength)));
+ }
+
+ // pad the end with zeros, but make sure to not forget the extra bytes
+ if (extraBytes === 1) {
+ tmp = uint8[len - 1];
+ parts.push(
+ lookup[tmp >> 2] +
+ lookup[(tmp << 4) & 0x3F] +
+ '=='
+ );
+ } else if (extraBytes === 2) {
+ tmp = (uint8[len - 2] << 8) + uint8[len - 1];
+ parts.push(
+ lookup[tmp >> 10] +
+ lookup[(tmp >> 4) & 0x3F] +
+ lookup[(tmp << 2) & 0x3F] +
+ '='
+ );
+ }
+
+ return parts.join('')
+ }
+
+ var sha256 = {exports: {}};
+
+ /**
+ * [js-sha256]{@link https://github.com/emn178/js-sha256}
+ *
+ * @version 0.9.0
+ * @author Chen, Yi-Cyuan [emn178@gmail.com]
+ * @copyright Chen, Yi-Cyuan 2014-2017
+ * @license MIT
+ */
+
+ (function (module) {
+ /*jslint bitwise: true */
+ (function () {
+
+ var ERROR = 'input is invalid type';
+ var WINDOW = typeof window === 'object';
+ var root = WINDOW ? window : {};
+ if (root.JS_SHA256_NO_WINDOW) {
+ WINDOW = false;
+ }
+ var WEB_WORKER = !WINDOW && typeof self === 'object';
+ var NODE_JS = !root.JS_SHA256_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node;
+ if (NODE_JS) {
+ root = commonjsGlobal;
+ } else if (WEB_WORKER) {
+ root = self;
+ }
+ var COMMON_JS = !root.JS_SHA256_NO_COMMON_JS && 'object' === 'object' && module.exports;
+ var ARRAY_BUFFER = !root.JS_SHA256_NO_ARRAY_BUFFER && typeof ArrayBuffer !== 'undefined';
+ var HEX_CHARS = '0123456789abcdef'.split('');
+ var EXTRA = [-2147483648, 8388608, 32768, 128];
+ var SHIFT = [24, 16, 8, 0];
+ var K = [
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
+ ];
+ var OUTPUT_TYPES = ['hex', 'array', 'digest', 'arrayBuffer'];
+
+ var blocks = [];
+
+ if (root.JS_SHA256_NO_NODE_JS || !Array.isArray) {
+ Array.isArray = function (obj) {
+ return Object.prototype.toString.call(obj) === '[object Array]';
+ };
+ }
+
+ if (ARRAY_BUFFER && (root.JS_SHA256_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView)) {
+ ArrayBuffer.isView = function (obj) {
+ return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer;
+ };
+ }
+
+ var createOutputMethod = function (outputType, is224) {
+ return function (message) {
+ return new Sha256(is224, true).update(message)[outputType]();
+ };
+ };
+
+ var createMethod = function (is224) {
+ var method = createOutputMethod('hex', is224);
+ if (NODE_JS) {
+ method = nodeWrap(method, is224);
+ }
+ method.create = function () {
+ return new Sha256(is224);
+ };
+ method.update = function (message) {
+ return method.create().update(message);
+ };
+ for (var i = 0; i < OUTPUT_TYPES.length; ++i) {
+ var type = OUTPUT_TYPES[i];
+ method[type] = createOutputMethod(type, is224);
+ }
+ return method;
+ };
+
+ var nodeWrap = function (method, is224) {
+ var crypto = eval("require('crypto')");
+ var Buffer = eval("require('buffer').Buffer");
+ var algorithm = is224 ? 'sha224' : 'sha256';
+ var nodeMethod = function (message) {
+ if (typeof message === 'string') {
+ return crypto.createHash(algorithm).update(message, 'utf8').digest('hex');
+ } else {
+ if (message === null || message === undefined) {
+ throw new Error(ERROR);
+ } else if (message.constructor === ArrayBuffer) {
+ message = new Uint8Array(message);
+ }
+ }
+ if (Array.isArray(message) || ArrayBuffer.isView(message) ||
+ message.constructor === Buffer) {
+ return crypto.createHash(algorithm).update(new Buffer(message)).digest('hex');
+ } else {
+ return method(message);
+ }
+ };
+ return nodeMethod;
+ };
+
+ var createHmacOutputMethod = function (outputType, is224) {
+ return function (key, message) {
+ return new HmacSha256(key, is224, true).update(message)[outputType]();
+ };
+ };
+
+ var createHmacMethod = function (is224) {
+ var method = createHmacOutputMethod('hex', is224);
+ method.create = function (key) {
+ return new HmacSha256(key, is224);
+ };
+ method.update = function (key, message) {
+ return method.create(key).update(message);
+ };
+ for (var i = 0; i < OUTPUT_TYPES.length; ++i) {
+ var type = OUTPUT_TYPES[i];
+ method[type] = createHmacOutputMethod(type, is224);
+ }
+ return method;
+ };
+
+ function Sha256(is224, sharedMemory) {
+ if (sharedMemory) {
+ blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] =
+ blocks[4] = blocks[5] = blocks[6] = blocks[7] =
+ blocks[8] = blocks[9] = blocks[10] = blocks[11] =
+ blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
+ this.blocks = blocks;
+ } else {
+ this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+ }
+
+ if (is224) {
+ this.h0 = 0xc1059ed8;
+ this.h1 = 0x367cd507;
+ this.h2 = 0x3070dd17;
+ this.h3 = 0xf70e5939;
+ this.h4 = 0xffc00b31;
+ this.h5 = 0x68581511;
+ this.h6 = 0x64f98fa7;
+ this.h7 = 0xbefa4fa4;
+ } else { // 256
+ this.h0 = 0x6a09e667;
+ this.h1 = 0xbb67ae85;
+ this.h2 = 0x3c6ef372;
+ this.h3 = 0xa54ff53a;
+ this.h4 = 0x510e527f;
+ this.h5 = 0x9b05688c;
+ this.h6 = 0x1f83d9ab;
+ this.h7 = 0x5be0cd19;
+ }
+
+ this.block = this.start = this.bytes = this.hBytes = 0;
+ this.finalized = this.hashed = false;
+ this.first = true;
+ this.is224 = is224;
+ }
+
+ Sha256.prototype.update = function (message) {
+ if (this.finalized) {
+ return;
+ }
+ var notString, type = typeof message;
+ if (type !== 'string') {
+ if (type === 'object') {
+ if (message === null) {
+ throw new Error(ERROR);
+ } else if (ARRAY_BUFFER && message.constructor === ArrayBuffer) {
+ message = new Uint8Array(message);
+ } else if (!Array.isArray(message)) {
+ if (!ARRAY_BUFFER || !ArrayBuffer.isView(message)) {
+ throw new Error(ERROR);
+ }
+ }
+ } else {
+ throw new Error(ERROR);
+ }
+ notString = true;
+ }
+ var code, index = 0, i, length = message.length, blocks = this.blocks;
+
+ while (index < length) {
+ if (this.hashed) {
+ this.hashed = false;
+ blocks[0] = this.block;
+ blocks[16] = blocks[1] = blocks[2] = blocks[3] =
+ blocks[4] = blocks[5] = blocks[6] = blocks[7] =
+ blocks[8] = blocks[9] = blocks[10] = blocks[11] =
+ blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
+ }
+
+ if (notString) {
+ for (i = this.start; index < length && i < 64; ++index) {
+ blocks[i >> 2] |= message[index] << SHIFT[i++ & 3];
+ }
+ } else {
+ for (i = this.start; index < length && i < 64; ++index) {
+ code = message.charCodeAt(index);
+ if (code < 0x80) {
+ blocks[i >> 2] |= code << SHIFT[i++ & 3];
+ } else if (code < 0x800) {
+ blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
+ } else if (code < 0xd800 || code >= 0xe000) {
+ blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
+ } else {
+ code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff));
+ blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
+ }
+ }
+ }
+
+ this.lastByteIndex = i;
+ this.bytes += i - this.start;
+ if (i >= 64) {
+ this.block = blocks[16];
+ this.start = i - 64;
+ this.hash();
+ this.hashed = true;
+ } else {
+ this.start = i;
+ }
+ }
+ if (this.bytes > 4294967295) {
+ this.hBytes += this.bytes / 4294967296 << 0;
+ this.bytes = this.bytes % 4294967296;
+ }
+ return this;
+ };
+
+ Sha256.prototype.finalize = function () {
+ if (this.finalized) {
+ return;
+ }
+ this.finalized = true;
+ var blocks = this.blocks, i = this.lastByteIndex;
+ blocks[16] = this.block;
+ blocks[i >> 2] |= EXTRA[i & 3];
+ this.block = blocks[16];
+ if (i >= 56) {
+ if (!this.hashed) {
+ this.hash();
+ }
+ blocks[0] = this.block;
+ blocks[16] = blocks[1] = blocks[2] = blocks[3] =
+ blocks[4] = blocks[5] = blocks[6] = blocks[7] =
+ blocks[8] = blocks[9] = blocks[10] = blocks[11] =
+ blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
+ }
+ blocks[14] = this.hBytes << 3 | this.bytes >>> 29;
+ blocks[15] = this.bytes << 3;
+ this.hash();
+ };
+
+ Sha256.prototype.hash = function () {
+ var a = this.h0, b = this.h1, c = this.h2, d = this.h3, e = this.h4, f = this.h5, g = this.h6,
+ h = this.h7, blocks = this.blocks, j, s0, s1, maj, t1, t2, ch, ab, da, cd, bc;
+
+ for (j = 16; j < 64; ++j) {
+ // rightrotate
+ t1 = blocks[j - 15];
+ s0 = ((t1 >>> 7) | (t1 << 25)) ^ ((t1 >>> 18) | (t1 << 14)) ^ (t1 >>> 3);
+ t1 = blocks[j - 2];
+ s1 = ((t1 >>> 17) | (t1 << 15)) ^ ((t1 >>> 19) | (t1 << 13)) ^ (t1 >>> 10);
+ blocks[j] = blocks[j - 16] + s0 + blocks[j - 7] + s1 << 0;
+ }
+
+ bc = b & c;
+ for (j = 0; j < 64; j += 4) {
+ if (this.first) {
+ if (this.is224) {
+ ab = 300032;
+ t1 = blocks[0] - 1413257819;
+ h = t1 - 150054599 << 0;
+ d = t1 + 24177077 << 0;
+ } else {
+ ab = 704751109;
+ t1 = blocks[0] - 210244248;
+ h = t1 - 1521486534 << 0;
+ d = t1 + 143694565 << 0;
+ }
+ this.first = false;
+ } else {
+ s0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
+ s1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
+ ab = a & b;
+ maj = ab ^ (a & c) ^ bc;
+ ch = (e & f) ^ (~e & g);
+ t1 = h + s1 + ch + K[j] + blocks[j];
+ t2 = s0 + maj;
+ h = d + t1 << 0;
+ d = t1 + t2 << 0;
+ }
+ s0 = ((d >>> 2) | (d << 30)) ^ ((d >>> 13) | (d << 19)) ^ ((d >>> 22) | (d << 10));
+ s1 = ((h >>> 6) | (h << 26)) ^ ((h >>> 11) | (h << 21)) ^ ((h >>> 25) | (h << 7));
+ da = d & a;
+ maj = da ^ (d & b) ^ ab;
+ ch = (h & e) ^ (~h & f);
+ t1 = g + s1 + ch + K[j + 1] + blocks[j + 1];
+ t2 = s0 + maj;
+ g = c + t1 << 0;
+ c = t1 + t2 << 0;
+ s0 = ((c >>> 2) | (c << 30)) ^ ((c >>> 13) | (c << 19)) ^ ((c >>> 22) | (c << 10));
+ s1 = ((g >>> 6) | (g << 26)) ^ ((g >>> 11) | (g << 21)) ^ ((g >>> 25) | (g << 7));
+ cd = c & d;
+ maj = cd ^ (c & a) ^ da;
+ ch = (g & h) ^ (~g & e);
+ t1 = f + s1 + ch + K[j + 2] + blocks[j + 2];
+ t2 = s0 + maj;
+ f = b + t1 << 0;
+ b = t1 + t2 << 0;
+ s0 = ((b >>> 2) | (b << 30)) ^ ((b >>> 13) | (b << 19)) ^ ((b >>> 22) | (b << 10));
+ s1 = ((f >>> 6) | (f << 26)) ^ ((f >>> 11) | (f << 21)) ^ ((f >>> 25) | (f << 7));
+ bc = b & c;
+ maj = bc ^ (b & d) ^ cd;
+ ch = (f & g) ^ (~f & h);
+ t1 = e + s1 + ch + K[j + 3] + blocks[j + 3];
+ t2 = s0 + maj;
+ e = a + t1 << 0;
+ a = t1 + t2 << 0;
+ }
+
+ this.h0 = this.h0 + a << 0;
+ this.h1 = this.h1 + b << 0;
+ this.h2 = this.h2 + c << 0;
+ this.h3 = this.h3 + d << 0;
+ this.h4 = this.h4 + e << 0;
+ this.h5 = this.h5 + f << 0;
+ this.h6 = this.h6 + g << 0;
+ this.h7 = this.h7 + h << 0;
+ };
+
+ Sha256.prototype.hex = function () {
+ this.finalize();
+
+ var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5,
+ h6 = this.h6, h7 = this.h7;
+
+ var hex = HEX_CHARS[(h0 >> 28) & 0x0F] + HEX_CHARS[(h0 >> 24) & 0x0F] +
+ HEX_CHARS[(h0 >> 20) & 0x0F] + HEX_CHARS[(h0 >> 16) & 0x0F] +
+ HEX_CHARS[(h0 >> 12) & 0x0F] + HEX_CHARS[(h0 >> 8) & 0x0F] +
+ HEX_CHARS[(h0 >> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] +
+ HEX_CHARS[(h1 >> 28) & 0x0F] + HEX_CHARS[(h1 >> 24) & 0x0F] +
+ HEX_CHARS[(h1 >> 20) & 0x0F] + HEX_CHARS[(h1 >> 16) & 0x0F] +
+ HEX_CHARS[(h1 >> 12) & 0x0F] + HEX_CHARS[(h1 >> 8) & 0x0F] +
+ HEX_CHARS[(h1 >> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] +
+ HEX_CHARS[(h2 >> 28) & 0x0F] + HEX_CHARS[(h2 >> 24) & 0x0F] +
+ HEX_CHARS[(h2 >> 20) & 0x0F] + HEX_CHARS[(h2 >> 16) & 0x0F] +
+ HEX_CHARS[(h2 >> 12) & 0x0F] + HEX_CHARS[(h2 >> 8) & 0x0F] +
+ HEX_CHARS[(h2 >> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] +
+ HEX_CHARS[(h3 >> 28) & 0x0F] + HEX_CHARS[(h3 >> 24) & 0x0F] +
+ HEX_CHARS[(h3 >> 20) & 0x0F] + HEX_CHARS[(h3 >> 16) & 0x0F] +
+ HEX_CHARS[(h3 >> 12) & 0x0F] + HEX_CHARS[(h3 >> 8) & 0x0F] +
+ HEX_CHARS[(h3 >> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] +
+ HEX_CHARS[(h4 >> 28) & 0x0F] + HEX_CHARS[(h4 >> 24) & 0x0F] +
+ HEX_CHARS[(h4 >> 20) & 0x0F] + HEX_CHARS[(h4 >> 16) & 0x0F] +
+ HEX_CHARS[(h4 >> 12) & 0x0F] + HEX_CHARS[(h4 >> 8) & 0x0F] +
+ HEX_CHARS[(h4 >> 4) & 0x0F] + HEX_CHARS[h4 & 0x0F] +
+ HEX_CHARS[(h5 >> 28) & 0x0F] + HEX_CHARS[(h5 >> 24) & 0x0F] +
+ HEX_CHARS[(h5 >> 20) & 0x0F] + HEX_CHARS[(h5 >> 16) & 0x0F] +
+ HEX_CHARS[(h5 >> 12) & 0x0F] + HEX_CHARS[(h5 >> 8) & 0x0F] +
+ HEX_CHARS[(h5 >> 4) & 0x0F] + HEX_CHARS[h5 & 0x0F] +
+ HEX_CHARS[(h6 >> 28) & 0x0F] + HEX_CHARS[(h6 >> 24) & 0x0F] +
+ HEX_CHARS[(h6 >> 20) & 0x0F] + HEX_CHARS[(h6 >> 16) & 0x0F] +
+ HEX_CHARS[(h6 >> 12) & 0x0F] + HEX_CHARS[(h6 >> 8) & 0x0F] +
+ HEX_CHARS[(h6 >> 4) & 0x0F] + HEX_CHARS[h6 & 0x0F];
+ if (!this.is224) {
+ hex += HEX_CHARS[(h7 >> 28) & 0x0F] + HEX_CHARS[(h7 >> 24) & 0x0F] +
+ HEX_CHARS[(h7 >> 20) & 0x0F] + HEX_CHARS[(h7 >> 16) & 0x0F] +
+ HEX_CHARS[(h7 >> 12) & 0x0F] + HEX_CHARS[(h7 >> 8) & 0x0F] +
+ HEX_CHARS[(h7 >> 4) & 0x0F] + HEX_CHARS[h7 & 0x0F];
+ }
+ return hex;
+ };
+
+ Sha256.prototype.toString = Sha256.prototype.hex;
+
+ Sha256.prototype.digest = function () {
+ this.finalize();
+
+ var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5,
+ h6 = this.h6, h7 = this.h7;
+
+ var arr = [
+ (h0 >> 24) & 0xFF, (h0 >> 16) & 0xFF, (h0 >> 8) & 0xFF, h0 & 0xFF,
+ (h1 >> 24) & 0xFF, (h1 >> 16) & 0xFF, (h1 >> 8) & 0xFF, h1 & 0xFF,
+ (h2 >> 24) & 0xFF, (h2 >> 16) & 0xFF, (h2 >> 8) & 0xFF, h2 & 0xFF,
+ (h3 >> 24) & 0xFF, (h3 >> 16) & 0xFF, (h3 >> 8) & 0xFF, h3 & 0xFF,
+ (h4 >> 24) & 0xFF, (h4 >> 16) & 0xFF, (h4 >> 8) & 0xFF, h4 & 0xFF,
+ (h5 >> 24) & 0xFF, (h5 >> 16) & 0xFF, (h5 >> 8) & 0xFF, h5 & 0xFF,
+ (h6 >> 24) & 0xFF, (h6 >> 16) & 0xFF, (h6 >> 8) & 0xFF, h6 & 0xFF
+ ];
+ if (!this.is224) {
+ arr.push((h7 >> 24) & 0xFF, (h7 >> 16) & 0xFF, (h7 >> 8) & 0xFF, h7 & 0xFF);
+ }
+ return arr;
+ };
+
+ Sha256.prototype.array = Sha256.prototype.digest;
+
+ Sha256.prototype.arrayBuffer = function () {
+ this.finalize();
+
+ var buffer = new ArrayBuffer(this.is224 ? 28 : 32);
+ var dataView = new DataView(buffer);
+ dataView.setUint32(0, this.h0);
+ dataView.setUint32(4, this.h1);
+ dataView.setUint32(8, this.h2);
+ dataView.setUint32(12, this.h3);
+ dataView.setUint32(16, this.h4);
+ dataView.setUint32(20, this.h5);
+ dataView.setUint32(24, this.h6);
+ if (!this.is224) {
+ dataView.setUint32(28, this.h7);
+ }
+ return buffer;
+ };
+
+ function HmacSha256(key, is224, sharedMemory) {
+ var i, type = typeof key;
+ if (type === 'string') {
+ var bytes = [], length = key.length, index = 0, code;
+ for (i = 0; i < length; ++i) {
+ code = key.charCodeAt(i);
+ if (code < 0x80) {
+ bytes[index++] = code;
+ } else if (code < 0x800) {
+ bytes[index++] = (0xc0 | (code >> 6));
+ bytes[index++] = (0x80 | (code & 0x3f));
+ } else if (code < 0xd800 || code >= 0xe000) {
+ bytes[index++] = (0xe0 | (code >> 12));
+ bytes[index++] = (0x80 | ((code >> 6) & 0x3f));
+ bytes[index++] = (0x80 | (code & 0x3f));
+ } else {
+ code = 0x10000 + (((code & 0x3ff) << 10) | (key.charCodeAt(++i) & 0x3ff));
+ bytes[index++] = (0xf0 | (code >> 18));
+ bytes[index++] = (0x80 | ((code >> 12) & 0x3f));
+ bytes[index++] = (0x80 | ((code >> 6) & 0x3f));
+ bytes[index++] = (0x80 | (code & 0x3f));
+ }
+ }
+ key = bytes;
+ } else {
+ if (type === 'object') {
+ if (key === null) {
+ throw new Error(ERROR);
+ } else if (ARRAY_BUFFER && key.constructor === ArrayBuffer) {
+ key = new Uint8Array(key);
+ } else if (!Array.isArray(key)) {
+ if (!ARRAY_BUFFER || !ArrayBuffer.isView(key)) {
+ throw new Error(ERROR);
+ }
+ }
+ } else {
+ throw new Error(ERROR);
+ }
+ }
+
+ if (key.length > 64) {
+ key = (new Sha256(is224, true)).update(key).array();
+ }
+
+ var oKeyPad = [], iKeyPad = [];
+ for (i = 0; i < 64; ++i) {
+ var b = key[i] || 0;
+ oKeyPad[i] = 0x5c ^ b;
+ iKeyPad[i] = 0x36 ^ b;
+ }
+
+ Sha256.call(this, is224, sharedMemory);
+
+ this.update(iKeyPad);
+ this.oKeyPad = oKeyPad;
+ this.inner = true;
+ this.sharedMemory = sharedMemory;
+ }
+ HmacSha256.prototype = new Sha256();
+
+ HmacSha256.prototype.finalize = function () {
+ Sha256.prototype.finalize.call(this);
+ if (this.inner) {
+ this.inner = false;
+ var innerHash = this.array();
+ Sha256.call(this, this.is224, this.sharedMemory);
+ this.update(this.oKeyPad);
+ this.update(innerHash);
+ Sha256.prototype.finalize.call(this);
+ }
+ };
+
+ var exports = createMethod();
+ exports.sha256 = exports;
+ exports.sha224 = createMethod(true);
+ exports.sha256.hmac = createHmacMethod();
+ exports.sha224.hmac = createHmacMethod(true);
+
+ if (COMMON_JS) {
+ module.exports = exports;
+ } else {
+ root.sha256 = exports.sha256;
+ root.sha224 = exports.sha224;
+ }
+ })();
+ }(sha256));
+
+ if (typeof es6Promise_min.exports.Promise === 'undefined') {
+ throw Error('Keycloak requires an environment that supports Promises. Make sure that you include the appropriate polyfill.');
+ }
+
+ var loggedPromiseDeprecation = false;
+
+ function logPromiseDeprecation() {
+ if (!loggedPromiseDeprecation) {
+ loggedPromiseDeprecation = true;
+ console.warn('[KEYCLOAK] Usage of legacy style promise methods such as `.error()` and `.success()` has been deprecated and support will be removed in future versions. Use standard style promise methods such as `.then() and `.catch()` instead.');
+ }
+ }
+
+ function Keycloak (config) {
+ if (!(this instanceof Keycloak)) {
+ return new Keycloak(config);
+ }
+
+ var kc = this;
+ var adapter;
+ var refreshQueue = [];
+ var callbackStorage;
+
+ var loginIframe = {
+ enable: true,
+ callbackList: [],
+ interval: 5
+ };
+
+ var scripts = document.getElementsByTagName('script');
+ for (var i = 0; i < scripts.length; i++) {
+ if ((scripts[i].src.indexOf('keycloak.js') !== -1 || scripts[i].src.indexOf('keycloak.min.js') !== -1) && scripts[i].src.indexOf('version=') !== -1) {
+ kc.iframeVersion = scripts[i].src.substring(scripts[i].src.indexOf('version=') + 8).split('&')[0];
+ }
+ }
+
+ var useNonce = true;
+ var logInfo = createLogger(console.info);
+ var logWarn = createLogger(console.warn);
+
+ kc.init = function (initOptions) {
+ kc.authenticated = false;
+
+ callbackStorage = createCallbackStorage();
+ var adapters = ['default', 'cordova', 'cordova-native'];
+
+ if (initOptions && adapters.indexOf(initOptions.adapter) > -1) {
+ adapter = loadAdapter(initOptions.adapter);
+ } else if (initOptions && typeof initOptions.adapter === "object") {
+ adapter = initOptions.adapter;
+ } else {
+ if (window.Cordova || window.cordova) {
+ adapter = loadAdapter('cordova');
+ } else {
+ adapter = loadAdapter();
+ }
+ }
+
+ if (initOptions) {
+ if (typeof initOptions.useNonce !== 'undefined') {
+ useNonce = initOptions.useNonce;
+ }
+
+ if (typeof initOptions.checkLoginIframe !== 'undefined') {
+ loginIframe.enable = initOptions.checkLoginIframe;
+ }
+
+ if (initOptions.checkLoginIframeInterval) {
+ loginIframe.interval = initOptions.checkLoginIframeInterval;
+ }
+
+ if (initOptions.onLoad === 'login-required') {
+ kc.loginRequired = true;
+ }
+
+ if (initOptions.responseMode) {
+ if (initOptions.responseMode === 'query' || initOptions.responseMode === 'fragment') {
+ kc.responseMode = initOptions.responseMode;
+ } else {
+ throw 'Invalid value for responseMode';
+ }
+ }
+
+ if (initOptions.flow) {
+ switch (initOptions.flow) {
+ case 'standard':
+ kc.responseType = 'code';
+ break;
+ case 'implicit':
+ kc.responseType = 'id_token token';
+ break;
+ case 'hybrid':
+ kc.responseType = 'code id_token token';
+ break;
+ default:
+ throw 'Invalid value for flow';
+ }
+ kc.flow = initOptions.flow;
+ }
+
+ if (initOptions.timeSkew != null) {
+ kc.timeSkew = initOptions.timeSkew;
+ }
+
+ if(initOptions.redirectUri) {
+ kc.redirectUri = initOptions.redirectUri;
+ }
+
+ if (initOptions.silentCheckSsoRedirectUri) {
+ kc.silentCheckSsoRedirectUri = initOptions.silentCheckSsoRedirectUri;
+ }
+
+ if (typeof initOptions.silentCheckSsoFallback === 'boolean') {
+ kc.silentCheckSsoFallback = initOptions.silentCheckSsoFallback;
+ } else {
+ kc.silentCheckSsoFallback = true;
+ }
+
+ if (initOptions.pkceMethod) {
+ if (initOptions.pkceMethod !== "S256") {
+ throw 'Invalid value for pkceMethod';
+ }
+ kc.pkceMethod = initOptions.pkceMethod;
+ }
+
+ if (typeof initOptions.enableLogging === 'boolean') {
+ kc.enableLogging = initOptions.enableLogging;
+ } else {
+ kc.enableLogging = false;
+ }
+
+ if (typeof initOptions.scope === 'string') {
+ kc.scope = initOptions.scope;
+ }
+
+ if (typeof initOptions.messageReceiveTimeout === 'number' && initOptions.messageReceiveTimeout > 0) {
+ kc.messageReceiveTimeout = initOptions.messageReceiveTimeout;
+ } else {
+ kc.messageReceiveTimeout = 10000;
+ }
+ }
+
+ if (!kc.responseMode) {
+ kc.responseMode = 'fragment';
+ }
+ if (!kc.responseType) {
+ kc.responseType = 'code';
+ kc.flow = 'standard';
+ }
+
+ var promise = createPromise();
+
+ var initPromise = createPromise();
+ initPromise.promise.then(function() {
+ kc.onReady && kc.onReady(kc.authenticated);
+ promise.setSuccess(kc.authenticated);
+ }).catch(function(error) {
+ promise.setError(error);
+ });
+
+ var configPromise = loadConfig();
+
+ function onLoad() {
+ var doLogin = function(prompt) {
+ if (!prompt) {
+ options.prompt = 'none';
+ }
+
+ kc.login(options).then(function () {
+ initPromise.setSuccess();
+ }).catch(function (error) {
+ initPromise.setError(error);
+ });
+ };
+
+ var checkSsoSilently = function() {
+ var ifrm = document.createElement("iframe");
+ var src = kc.createLoginUrl({prompt: 'none', redirectUri: kc.silentCheckSsoRedirectUri});
+ ifrm.setAttribute("src", src);
+ ifrm.setAttribute("title", "keycloak-silent-check-sso");
+ ifrm.style.display = "none";
+ document.body.appendChild(ifrm);
+
+ var messageCallback = function(event) {
+ if (event.origin !== window.location.origin || ifrm.contentWindow !== event.source) {
+ return;
+ }
+
+ var oauth = parseCallback(event.data);
+ processCallback(oauth, initPromise);
+
+ document.body.removeChild(ifrm);
+ window.removeEventListener("message", messageCallback);
+ };
+
+ window.addEventListener("message", messageCallback);
+ };
+
+ var options = {};
+ switch (initOptions.onLoad) {
+ case 'check-sso':
+ if (loginIframe.enable) {
+ setupCheckLoginIframe().then(function() {
+ checkLoginIframe().then(function (unchanged) {
+ if (!unchanged) {
+ kc.silentCheckSsoRedirectUri ? checkSsoSilently() : doLogin(false);
+ } else {
+ initPromise.setSuccess();
+ }
+ }).catch(function (error) {
+ initPromise.setError(error);
+ });
+ });
+ } else {
+ kc.silentCheckSsoRedirectUri ? checkSsoSilently() : doLogin(false);
+ }
+ break;
+ case 'login-required':
+ doLogin(true);
+ break;
+ default:
+ throw 'Invalid value for onLoad';
+ }
+ }
+
+ function processInit() {
+ var callback = parseCallback(window.location.href);
+
+ if (callback) {
+ window.history.replaceState(window.history.state, null, callback.newUrl);
+ }
+
+ if (callback && callback.valid) {
+ return setupCheckLoginIframe().then(function() {
+ processCallback(callback, initPromise);
+ }).catch(function (error) {
+ initPromise.setError(error);
+ });
+ } else if (initOptions) {
+ if (initOptions.token && initOptions.refreshToken) {
+ setToken(initOptions.token, initOptions.refreshToken, initOptions.idToken);
+
+ if (loginIframe.enable) {
+ setupCheckLoginIframe().then(function() {
+ checkLoginIframe().then(function (unchanged) {
+ if (unchanged) {
+ kc.onAuthSuccess && kc.onAuthSuccess();
+ initPromise.setSuccess();
+ scheduleCheckIframe();
+ } else {
+ initPromise.setSuccess();
+ }
+ }).catch(function (error) {
+ initPromise.setError(error);
+ });
+ });
+ } else {
+ kc.updateToken(-1).then(function() {
+ kc.onAuthSuccess && kc.onAuthSuccess();
+ initPromise.setSuccess();
+ }).catch(function(error) {
+ kc.onAuthError && kc.onAuthError();
+ if (initOptions.onLoad) {
+ onLoad();
+ } else {
+ initPromise.setError(error);
+ }
+ });
+ }
+ } else if (initOptions.onLoad) {
+ onLoad();
+ } else {
+ initPromise.setSuccess();
+ }
+ } else {
+ initPromise.setSuccess();
+ }
+ }
+
+ function domReady() {
+ var promise = createPromise();
+
+ var checkReadyState = function () {
+ if (document.readyState === 'interactive' || document.readyState === 'complete') {
+ document.removeEventListener('readystatechange', checkReadyState);
+ promise.setSuccess();
+ }
+ };
+ document.addEventListener('readystatechange', checkReadyState);
+
+ checkReadyState(); // just in case the event was already fired and we missed it (in case the init is done later than at the load time, i.e. it's done from code)
+
+ return promise.promise;
+ }
+
+ configPromise.then(function () {
+ domReady()
+ .then(check3pCookiesSupported)
+ .then(processInit)
+ .catch(function (error) {
+ promise.setError(error);
+ });
+ });
+ configPromise.catch(function (error) {
+ promise.setError(error);
+ });
+
+ return promise.promise;
+ };
+
+ kc.login = function (options) {
+ return adapter.login(options);
+ };
+
+ function generateRandomData(len) {
+ // use web crypto APIs if possible
+ var array = null;
+ var crypto = window.crypto || window.msCrypto;
+ if (crypto && crypto.getRandomValues && window.Uint8Array) {
+ array = new Uint8Array(len);
+ crypto.getRandomValues(array);
+ return array;
+ }
+
+ // fallback to Math random
+ array = new Array(len);
+ for (var j = 0; j < array.length; j++) {
+ array[j] = Math.floor(256 * Math.random());
+ }
+ return array;
+ }
+
+ function generateCodeVerifier(len) {
+ return generateRandomString(len, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789');
+ }
+
+ function generateRandomString(len, alphabet){
+ var randomData = generateRandomData(len);
+ var chars = new Array(len);
+ for (var i = 0; i < len; i++) {
+ chars[i] = alphabet.charCodeAt(randomData[i] % alphabet.length);
+ }
+ return String.fromCharCode.apply(null, chars);
+ }
+
+ function generatePkceChallenge(pkceMethod, codeVerifier) {
+ switch (pkceMethod) {
+ // The use of the "plain" method is considered insecure and therefore not supported.
+ case "S256":
+ // hash codeVerifier, then encode as url-safe base64 without padding
+ var hashBytes = new Uint8Array(sha256.exports.sha256.arrayBuffer(codeVerifier));
+ var encodedHash = base64Js.fromByteArray(hashBytes)
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/\=/g, '');
+ return encodedHash;
+ default:
+ throw 'Invalid value for pkceMethod';
+ }
+ }
+
+ kc.createLoginUrl = function(options) {
+ var state = createUUID();
+ var nonce = createUUID();
+
+ var redirectUri = adapter.redirectUri(options);
+
+ var callbackState = {
+ state: state,
+ nonce: nonce,
+ redirectUri: encodeURIComponent(redirectUri)
+ };
+
+ if (options && options.prompt) {
+ callbackState.prompt = options.prompt;
+ }
+
+ var baseUrl;
+ if (options && options.action == 'register') {
+ baseUrl = kc.endpoints.register();
+ } else {
+ baseUrl = kc.endpoints.authorize();
+ }
+
+ var scope = options && options.scope || kc.scope;
+ if (!scope) {
+ // if scope is not set, default to "openid"
+ scope = "openid";
+ } else if (scope.indexOf("openid") === -1) {
+ // if openid scope is missing, prefix the given scopes with it
+ scope = "openid " + scope;
+ }
+
+ var url = baseUrl
+ + '?client_id=' + encodeURIComponent(kc.clientId)
+ + '&redirect_uri=' + encodeURIComponent(redirectUri)
+ + '&state=' + encodeURIComponent(state)
+ + '&response_mode=' + encodeURIComponent(kc.responseMode)
+ + '&response_type=' + encodeURIComponent(kc.responseType)
+ + '&scope=' + encodeURIComponent(scope);
+ if (useNonce) {
+ url = url + '&nonce=' + encodeURIComponent(nonce);
+ }
+
+ if (options && options.prompt) {
+ url += '&prompt=' + encodeURIComponent(options.prompt);
+ }
+
+ if (options && options.maxAge) {
+ url += '&max_age=' + encodeURIComponent(options.maxAge);
+ }
+
+ if (options && options.loginHint) {
+ url += '&login_hint=' + encodeURIComponent(options.loginHint);
+ }
+
+ if (options && options.idpHint) {
+ url += '&kc_idp_hint=' + encodeURIComponent(options.idpHint);
+ }
+
+ if (options && options.action && options.action != 'register') {
+ url += '&kc_action=' + encodeURIComponent(options.action);
+ }
+
+ if (options && options.locale) {
+ url += '&ui_locales=' + encodeURIComponent(options.locale);
+ }
+
+ if (kc.pkceMethod) {
+ var codeVerifier = generateCodeVerifier(96);
+ callbackState.pkceCodeVerifier = codeVerifier;
+ var pkceChallenge = generatePkceChallenge(kc.pkceMethod, codeVerifier);
+ url += '&code_challenge=' + pkceChallenge;
+ url += '&code_challenge_method=' + kc.pkceMethod;
+ }
+
+ callbackStorage.add(callbackState);
+
+ return url;
+ };
+
+ kc.logout = function(options) {
+ return adapter.logout(options);
+ };
+
+ kc.createLogoutUrl = function(options) {
+ var url = kc.endpoints.logout()
+ + '?redirect_uri=' + encodeURIComponent(adapter.redirectUri(options, false));
+
+ return url;
+ };
+
+ kc.register = function (options) {
+ return adapter.register(options);
+ };
+
+ kc.createRegisterUrl = function(options) {
+ if (!options) {
+ options = {};
+ }
+ options.action = 'register';
+ return kc.createLoginUrl(options);
+ };
+
+ kc.createAccountUrl = function(options) {
+ var realm = getRealmUrl();
+ var url = undefined;
+ if (typeof realm !== 'undefined') {
+ url = realm
+ + '/account'
+ + '?referrer=' + encodeURIComponent(kc.clientId)
+ + '&referrer_uri=' + encodeURIComponent(adapter.redirectUri(options));
+ }
+ return url;
+ };
+
+ kc.accountManagement = function() {
+ return adapter.accountManagement();
+ };
+
+ kc.hasRealmRole = function (role) {
+ var access = kc.realmAccess;
+ return !!access && access.roles.indexOf(role) >= 0;
+ };
+
+ kc.hasResourceRole = function(role, resource) {
+ if (!kc.resourceAccess) {
+ return false;
+ }
+
+ var access = kc.resourceAccess[resource || kc.clientId];
+ return !!access && access.roles.indexOf(role) >= 0;
+ };
+
+ kc.loadUserProfile = function() {
+ var url = getRealmUrl() + '/account';
+ var req = new XMLHttpRequest();
+ req.open('GET', url, true);
+ req.setRequestHeader('Accept', 'application/json');
+ req.setRequestHeader('Authorization', 'bearer ' + kc.token);
+
+ var promise = createPromise();
+
+ req.onreadystatechange = function () {
+ if (req.readyState == 4) {
+ if (req.status == 200) {
+ kc.profile = JSON.parse(req.responseText);
+ promise.setSuccess(kc.profile);
+ } else {
+ promise.setError();
+ }
+ }
+ };
+
+ req.send();
+
+ return promise.promise;
+ };
+
+ kc.loadUserInfo = function() {
+ var url = kc.endpoints.userinfo();
+ var req = new XMLHttpRequest();
+ req.open('GET', url, true);
+ req.setRequestHeader('Accept', 'application/json');
+ req.setRequestHeader('Authorization', 'bearer ' + kc.token);
+
+ var promise = createPromise();
+
+ req.onreadystatechange = function () {
+ if (req.readyState == 4) {
+ if (req.status == 200) {
+ kc.userInfo = JSON.parse(req.responseText);
+ promise.setSuccess(kc.userInfo);
+ } else {
+ promise.setError();
+ }
+ }
+ };
+
+ req.send();
+
+ return promise.promise;
+ };
+
+ kc.isTokenExpired = function(minValidity) {
+ if (!kc.tokenParsed || (!kc.refreshToken && kc.flow != 'implicit' )) {
+ throw 'Not authenticated';
+ }
+
+ if (kc.timeSkew == null) {
+ logInfo('[KEYCLOAK] Unable to determine if token is expired as timeskew is not set');
+ return true;
+ }
+
+ var expiresIn = kc.tokenParsed['exp'] - Math.ceil(new Date().getTime() / 1000) + kc.timeSkew;
+ if (minValidity) {
+ if (isNaN(minValidity)) {
+ throw 'Invalid minValidity';
+ }
+ expiresIn -= minValidity;
+ }
+ return expiresIn < 0;
+ };
+
+ kc.updateToken = function(minValidity) {
+ var promise = createPromise();
+
+ if (!kc.refreshToken) {
+ promise.setError();
+ return promise.promise;
+ }
+
+ minValidity = minValidity || 5;
+
+ var exec = function() {
+ var refreshToken = false;
+ if (minValidity == -1) {
+ refreshToken = true;
+ logInfo('[KEYCLOAK] Refreshing token: forced refresh');
+ } else if (!kc.tokenParsed || kc.isTokenExpired(minValidity)) {
+ refreshToken = true;
+ logInfo('[KEYCLOAK] Refreshing token: token expired');
+ }
+
+ if (!refreshToken) {
+ promise.setSuccess(false);
+ } else {
+ var params = 'grant_type=refresh_token&' + 'refresh_token=' + kc.refreshToken;
+ var url = kc.endpoints.token();
+
+ refreshQueue.push(promise);
+
+ if (refreshQueue.length == 1) {
+ var req = new XMLHttpRequest();
+ req.open('POST', url, true);
+ req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
+ req.withCredentials = true;
+
+ params += '&client_id=' + encodeURIComponent(kc.clientId);
+
+ var timeLocal = new Date().getTime();
+
+ req.onreadystatechange = function () {
+ if (req.readyState == 4) {
+ if (req.status == 200) {
+ logInfo('[KEYCLOAK] Token refreshed');
+
+ timeLocal = (timeLocal + new Date().getTime()) / 2;
+
+ var tokenResponse = JSON.parse(req.responseText);
+
+ setToken(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token'], timeLocal);
+
+ kc.onAuthRefreshSuccess && kc.onAuthRefreshSuccess();
+ for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) {
+ p.setSuccess(true);
+ }
+ } else {
+ logWarn('[KEYCLOAK] Failed to refresh token');
+
+ if (req.status == 400) {
+ kc.clearToken();
+ }
+
+ kc.onAuthRefreshError && kc.onAuthRefreshError();
+ for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) {
+ p.setError(true);
+ }
+ }
+ }
+ };
+
+ req.send(params);
+ }
+ }
+ };
+
+ if (loginIframe.enable) {
+ var iframePromise = checkLoginIframe();
+ iframePromise.then(function() {
+ exec();
+ }).catch(function(error) {
+ promise.setError(error);
+ });
+ } else {
+ exec();
+ }
+
+ return promise.promise;
+ };
+
+ kc.clearToken = function() {
+ if (kc.token) {
+ setToken(null, null, null);
+ kc.onAuthLogout && kc.onAuthLogout();
+ if (kc.loginRequired) {
+ kc.login();
+ }
+ }
+ };
+
+ function getRealmUrl() {
+ if (typeof kc.authServerUrl !== 'undefined') {
+ if (kc.authServerUrl.charAt(kc.authServerUrl.length - 1) == '/') {
+ return kc.authServerUrl + 'realms/' + encodeURIComponent(kc.realm);
+ } else {
+ return kc.authServerUrl + '/realms/' + encodeURIComponent(kc.realm);
+ }
+ } else {
+ return undefined;
+ }
+ }
+
+ function getOrigin() {
+ if (!window.location.origin) {
+ return window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port: '');
+ } else {
+ return window.location.origin;
+ }
+ }
+
+ function processCallback(oauth, promise) {
+ var code = oauth.code;
+ var error = oauth.error;
+ var prompt = oauth.prompt;
+
+ var timeLocal = new Date().getTime();
+
+ if (oauth['kc_action_status']) {
+ kc.onActionUpdate && kc.onActionUpdate(oauth['kc_action_status']);
+ }
+
+ if (error) {
+ if (prompt != 'none') {
+ var errorData = { error: error, error_description: oauth.error_description };
+ kc.onAuthError && kc.onAuthError(errorData);
+ promise && promise.setError(errorData);
+ } else {
+ promise && promise.setSuccess();
+ }
+ return;
+ } else if ((kc.flow != 'standard') && (oauth.access_token || oauth.id_token)) {
+ authSuccess(oauth.access_token, null, oauth.id_token, true);
+ }
+
+ if ((kc.flow != 'implicit') && code) {
+ var params = 'code=' + code + '&grant_type=authorization_code';
+ var url = kc.endpoints.token();
+
+ var req = new XMLHttpRequest();
+ req.open('POST', url, true);
+ req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
+
+ params += '&client_id=' + encodeURIComponent(kc.clientId);
+ params += '&redirect_uri=' + oauth.redirectUri;
+
+ if (oauth.pkceCodeVerifier) {
+ params += '&code_verifier=' + oauth.pkceCodeVerifier;
+ }
+
+ req.withCredentials = true;
+
+ req.onreadystatechange = function() {
+ if (req.readyState == 4) {
+ if (req.status == 200) {
+
+ var tokenResponse = JSON.parse(req.responseText);
+ authSuccess(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token'], kc.flow === 'standard');
+ scheduleCheckIframe();
+ } else {
+ kc.onAuthError && kc.onAuthError();
+ promise && promise.setError();
+ }
+ }
+ };
+
+ req.send(params);
+ }
+
+ function authSuccess(accessToken, refreshToken, idToken, fulfillPromise) {
+ timeLocal = (timeLocal + new Date().getTime()) / 2;
+
+ setToken(accessToken, refreshToken, idToken, timeLocal);
+
+ if (useNonce && ((kc.tokenParsed && kc.tokenParsed.nonce != oauth.storedNonce) ||
+ (kc.refreshTokenParsed && kc.refreshTokenParsed.nonce != oauth.storedNonce) ||
+ (kc.idTokenParsed && kc.idTokenParsed.nonce != oauth.storedNonce))) {
+
+ logInfo('[KEYCLOAK] Invalid nonce, clearing token');
+ kc.clearToken();
+ promise && promise.setError();
+ } else {
+ if (fulfillPromise) {
+ kc.onAuthSuccess && kc.onAuthSuccess();
+ promise && promise.setSuccess();
+ }
+ }
+ }
+
+ }
+
+ function loadConfig(url) {
+ var promise = createPromise();
+ var configUrl;
+
+ if (!config) {
+ configUrl = 'keycloak.json';
+ } else if (typeof config === 'string') {
+ configUrl = config;
+ }
+
+ function setupOidcEndoints(oidcConfiguration) {
+ if (! oidcConfiguration) {
+ kc.endpoints = {
+ authorize: function() {
+ return getRealmUrl() + '/protocol/openid-connect/auth';
+ },
+ token: function() {
+ return getRealmUrl() + '/protocol/openid-connect/token';
+ },
+ logout: function() {
+ return getRealmUrl() + '/protocol/openid-connect/logout';
+ },
+ checkSessionIframe: function() {
+ var src = getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html';
+ if (kc.iframeVersion) {
+ src = src + '?version=' + kc.iframeVersion;
+ }
+ return src;
+ },
+ thirdPartyCookiesIframe: function() {
+ var src = getRealmUrl() + '/protocol/openid-connect/3p-cookies/step1.html';
+ if (kc.iframeVersion) {
+ src = src + '?version=' + kc.iframeVersion;
+ }
+ return src;
+ },
+ register: function() {
+ return getRealmUrl() + '/protocol/openid-connect/registrations';
+ },
+ userinfo: function() {
+ return getRealmUrl() + '/protocol/openid-connect/userinfo';
+ }
+ };
+ } else {
+ kc.endpoints = {
+ authorize: function() {
+ return oidcConfiguration.authorization_endpoint;
+ },
+ token: function() {
+ return oidcConfiguration.token_endpoint;
+ },
+ logout: function() {
+ if (!oidcConfiguration.end_session_endpoint) {
+ throw "Not supported by the OIDC server";
+ }
+ return oidcConfiguration.end_session_endpoint;
+ },
+ checkSessionIframe: function() {
+ if (!oidcConfiguration.check_session_iframe) {
+ throw "Not supported by the OIDC server";
+ }
+ return oidcConfiguration.check_session_iframe;
+ },
+ register: function() {
+ throw 'Redirection to "Register user" page not supported in standard OIDC mode';
+ },
+ userinfo: function() {
+ if (!oidcConfiguration.userinfo_endpoint) {
+ throw "Not supported by the OIDC server";
+ }
+ return oidcConfiguration.userinfo_endpoint;
+ }
+ };
+ }
+ }
+
+ if (configUrl) {
+ var req = new XMLHttpRequest();
+ req.open('GET', configUrl, true);
+ req.setRequestHeader('Accept', 'application/json');
+
+ req.onreadystatechange = function () {
+ if (req.readyState == 4) {
+ if (req.status == 200 || fileLoaded(req)) {
+ var config = JSON.parse(req.responseText);
+
+ kc.authServerUrl = config['auth-server-url'];
+ kc.realm = config['realm'];
+ kc.clientId = config['resource'];
+ setupOidcEndoints(null);
+ promise.setSuccess();
+ } else {
+ promise.setError();
+ }
+ }
+ };
+
+ req.send();
+ } else {
+ if (!config.clientId) {
+ throw 'clientId missing';
+ }
+
+ kc.clientId = config.clientId;
+
+ var oidcProvider = config['oidcProvider'];
+ if (!oidcProvider) {
+ if (!config['url']) {
+ var scripts = document.getElementsByTagName('script');
+ for (var i = 0; i < scripts.length; i++) {
+ if (scripts[i].src.match(/.*keycloak\.js/)) {
+ config.url = scripts[i].src.substr(0, scripts[i].src.indexOf('/js/keycloak.js'));
+ break;
+ }
+ }
+ }
+ if (!config.realm) {
+ throw 'realm missing';
+ }
+
+ kc.authServerUrl = config.url;
+ kc.realm = config.realm;
+ setupOidcEndoints(null);
+ promise.setSuccess();
+ } else {
+ if (typeof oidcProvider === 'string') {
+ var oidcProviderConfigUrl;
+ if (oidcProvider.charAt(oidcProvider.length - 1) == '/') {
+ oidcProviderConfigUrl = oidcProvider + '.well-known/openid-configuration';
+ } else {
+ oidcProviderConfigUrl = oidcProvider + '/.well-known/openid-configuration';
+ }
+ var req = new XMLHttpRequest();
+ req.open('GET', oidcProviderConfigUrl, true);
+ req.setRequestHeader('Accept', 'application/json');
+
+ req.onreadystatechange = function () {
+ if (req.readyState == 4) {
+ if (req.status == 200 || fileLoaded(req)) {
+ var oidcProviderConfig = JSON.parse(req.responseText);
+ setupOidcEndoints(oidcProviderConfig);
+ promise.setSuccess();
+ } else {
+ promise.setError();
+ }
+ }
+ };
+
+ req.send();
+ } else {
+ setupOidcEndoints(oidcProvider);
+ promise.setSuccess();
+ }
+ }
+ }
+
+ return promise.promise;
+ }
+
+ function fileLoaded(xhr) {
+ return xhr.status == 0 && xhr.responseText && xhr.responseURL.startsWith('file:');
+ }
+
+ function setToken(token, refreshToken, idToken, timeLocal) {
+ if (kc.tokenTimeoutHandle) {
+ clearTimeout(kc.tokenTimeoutHandle);
+ kc.tokenTimeoutHandle = null;
+ }
+
+ if (refreshToken) {
+ kc.refreshToken = refreshToken;
+ kc.refreshTokenParsed = decodeToken(refreshToken);
+ } else {
+ delete kc.refreshToken;
+ delete kc.refreshTokenParsed;
+ }
+
+ if (idToken) {
+ kc.idToken = idToken;
+ kc.idTokenParsed = decodeToken(idToken);
+ } else {
+ delete kc.idToken;
+ delete kc.idTokenParsed;
+ }
+
+ if (token) {
+ kc.token = token;
+ kc.tokenParsed = decodeToken(token);
+ kc.sessionId = kc.tokenParsed.session_state;
+ kc.authenticated = true;
+ kc.subject = kc.tokenParsed.sub;
+ kc.realmAccess = kc.tokenParsed.realm_access;
+ kc.resourceAccess = kc.tokenParsed.resource_access;
+
+ if (timeLocal) {
+ kc.timeSkew = Math.floor(timeLocal / 1000) - kc.tokenParsed.iat;
+ }
+
+ if (kc.timeSkew != null) {
+ logInfo('[KEYCLOAK] Estimated time difference between browser and server is ' + kc.timeSkew + ' seconds');
+
+ if (kc.onTokenExpired) {
+ var expiresIn = (kc.tokenParsed['exp'] - (new Date().getTime() / 1000) + kc.timeSkew) * 1000;
+ logInfo('[KEYCLOAK] Token expires in ' + Math.round(expiresIn / 1000) + ' s');
+ if (expiresIn <= 0) {
+ kc.onTokenExpired();
+ } else {
+ kc.tokenTimeoutHandle = setTimeout(kc.onTokenExpired, expiresIn);
+ }
+ }
+ }
+ } else {
+ delete kc.token;
+ delete kc.tokenParsed;
+ delete kc.subject;
+ delete kc.realmAccess;
+ delete kc.resourceAccess;
+
+ kc.authenticated = false;
+ }
+ }
+
+ function decodeToken(str) {
+ str = str.split('.')[1];
+
+ str = str.replace(/-/g, '+');
+ str = str.replace(/_/g, '/');
+ switch (str.length % 4) {
+ case 0:
+ break;
+ case 2:
+ str += '==';
+ break;
+ case 3:
+ str += '=';
+ break;
+ default:
+ throw 'Invalid token';
+ }
+
+ str = decodeURIComponent(escape(atob(str)));
+
+ str = JSON.parse(str);
+ return str;
+ }
+
+ function createUUID() {
+ var hexDigits = '0123456789abcdef';
+ var s = generateRandomString(36, hexDigits).split("");
+ s[14] = '4';
+ s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
+ s[8] = s[13] = s[18] = s[23] = '-';
+ var uuid = s.join('');
+ return uuid;
+ }
+
+ function parseCallback(url) {
+ var oauth = parseCallbackUrl(url);
+ if (!oauth) {
+ return;
+ }
+
+ var oauthState = callbackStorage.get(oauth.state);
+
+ if (oauthState) {
+ oauth.valid = true;
+ oauth.redirectUri = oauthState.redirectUri;
+ oauth.storedNonce = oauthState.nonce;
+ oauth.prompt = oauthState.prompt;
+ oauth.pkceCodeVerifier = oauthState.pkceCodeVerifier;
+ }
+
+ return oauth;
+ }
+
+ function parseCallbackUrl(url) {
+ var supportedParams;
+ switch (kc.flow) {
+ case 'standard':
+ supportedParams = ['code', 'state', 'session_state', 'kc_action_status'];
+ break;
+ case 'implicit':
+ supportedParams = ['access_token', 'token_type', 'id_token', 'state', 'session_state', 'expires_in', 'kc_action_status'];
+ break;
+ case 'hybrid':
+ supportedParams = ['access_token', 'token_type', 'id_token', 'code', 'state', 'session_state', 'expires_in', 'kc_action_status'];
+ break;
+ }
+
+ supportedParams.push('error');
+ supportedParams.push('error_description');
+ supportedParams.push('error_uri');
+
+ var queryIndex = url.indexOf('?');
+ var fragmentIndex = url.indexOf('#');
+
+ var newUrl;
+ var parsed;
+
+ if (kc.responseMode === 'query' && queryIndex !== -1) {
+ newUrl = url.substring(0, queryIndex);
+ parsed = parseCallbackParams(url.substring(queryIndex + 1, fragmentIndex !== -1 ? fragmentIndex : url.length), supportedParams);
+ if (parsed.paramsString !== '') {
+ newUrl += '?' + parsed.paramsString;
+ }
+ if (fragmentIndex !== -1) {
+ newUrl += url.substring(fragmentIndex);
+ }
+ } else if (kc.responseMode === 'fragment' && fragmentIndex !== -1) {
+ newUrl = url.substring(0, fragmentIndex);
+ parsed = parseCallbackParams(url.substring(fragmentIndex + 1), supportedParams);
+ if (parsed.paramsString !== '') {
+ newUrl += '#' + parsed.paramsString;
+ }
+ }
+
+ if (parsed && parsed.oauthParams) {
+ if (kc.flow === 'standard' || kc.flow === 'hybrid') {
+ if ((parsed.oauthParams.code || parsed.oauthParams.error) && parsed.oauthParams.state) {
+ parsed.oauthParams.newUrl = newUrl;
+ return parsed.oauthParams;
+ }
+ } else if (kc.flow === 'implicit') {
+ if ((parsed.oauthParams.access_token || parsed.oauthParams.error) && parsed.oauthParams.state) {
+ parsed.oauthParams.newUrl = newUrl;
+ return parsed.oauthParams;
+ }
+ }
+ }
+ }
+
+ function parseCallbackParams(paramsString, supportedParams) {
+ var p = paramsString.split('&');
+ var result = {
+ paramsString: '',
+ oauthParams: {}
+ };
+ for (var i = 0; i < p.length; i++) {
+ var split = p[i].indexOf("=");
+ var key = p[i].slice(0, split);
+ if (supportedParams.indexOf(key) !== -1) {
+ result.oauthParams[key] = p[i].slice(split + 1);
+ } else {
+ if (result.paramsString !== '') {
+ result.paramsString += '&';
+ }
+ result.paramsString += p[i];
+ }
+ }
+ return result;
+ }
+
+ function createPromise() {
+ // Need to create a native Promise which also preserves the
+ // interface of the custom promise type previously used by the API
+ var p = {
+ setSuccess: function(result) {
+ p.resolve(result);
+ },
+
+ setError: function(result) {
+ p.reject(result);
+ }
+ };
+ p.promise = new es6Promise_min.exports.Promise(function(resolve, reject) {
+ p.resolve = resolve;
+ p.reject = reject;
+ });
+
+ p.promise.success = function(callback) {
+ logPromiseDeprecation();
+
+ this.then(function handleSuccess(value) {
+ callback(value);
+ });
+
+ return this;
+ };
+
+ p.promise.error = function(callback) {
+ logPromiseDeprecation();
+
+ this.catch(function handleError(error) {
+ callback(error);
+ });
+
+ return this;
+ };
+
+ return p;
+ }
+
+ // Function to extend existing native Promise with timeout
+ function applyTimeoutToPromise(promise, timeout, errorMessage) {
+ var timeoutHandle = null;
+ var timeoutPromise = new es6Promise_min.exports.Promise(function (resolve, reject) {
+ timeoutHandle = setTimeout(function () {
+ reject({ "error": errorMessage || "Promise is not settled within timeout of " + timeout + "ms" });
+ }, timeout);
+ });
+
+ return es6Promise_min.exports.Promise.race([promise, timeoutPromise]).finally(function () {
+ clearTimeout(timeoutHandle);
+ });
+ }
+
+ function setupCheckLoginIframe() {
+ var promise = createPromise();
+
+ if (!loginIframe.enable) {
+ promise.setSuccess();
+ return promise.promise;
+ }
+
+ if (loginIframe.iframe) {
+ promise.setSuccess();
+ return promise.promise;
+ }
+
+ var iframe = document.createElement('iframe');
+ loginIframe.iframe = iframe;
+
+ iframe.onload = function() {
+ var authUrl = kc.endpoints.authorize();
+ if (authUrl.charAt(0) === '/') {
+ loginIframe.iframeOrigin = getOrigin();
+ } else {
+ loginIframe.iframeOrigin = authUrl.substring(0, authUrl.indexOf('/', 8));
+ }
+ promise.setSuccess();
+ };
+
+ var src = kc.endpoints.checkSessionIframe();
+ iframe.setAttribute('src', src );
+ iframe.setAttribute('title', 'keycloak-session-iframe' );
+ iframe.style.display = 'none';
+ document.body.appendChild(iframe);
+
+ var messageCallback = function(event) {
+ if ((event.origin !== loginIframe.iframeOrigin) || (loginIframe.iframe.contentWindow !== event.source)) {
+ return;
+ }
+
+ if (!(event.data == 'unchanged' || event.data == 'changed' || event.data == 'error')) {
+ return;
+ }
+
+
+ if (event.data != 'unchanged') {
+ kc.clearToken();
+ }
+
+ var callbacks = loginIframe.callbackList.splice(0, loginIframe.callbackList.length);
+
+ for (var i = callbacks.length - 1; i >= 0; --i) {
+ var promise = callbacks[i];
+ if (event.data == 'error') {
+ promise.setError();
+ } else {
+ promise.setSuccess(event.data == 'unchanged');
+ }
+ }
+ };
+
+ window.addEventListener('message', messageCallback, false);
+
+ return promise.promise;
+ }
+
+ function scheduleCheckIframe() {
+ if (loginIframe.enable) {
+ if (kc.token) {
+ setTimeout(function() {
+ checkLoginIframe().then(function(unchanged) {
+ if (unchanged) {
+ scheduleCheckIframe();
+ }
+ });
+ }, loginIframe.interval * 1000);
+ }
+ }
+ }
+
+ function checkLoginIframe() {
+ var promise = createPromise();
+
+ if (loginIframe.iframe && loginIframe.iframeOrigin ) {
+ var msg = kc.clientId + ' ' + (kc.sessionId ? kc.sessionId : '');
+ loginIframe.callbackList.push(promise);
+ var origin = loginIframe.iframeOrigin;
+ if (loginIframe.callbackList.length == 1) {
+ loginIframe.iframe.contentWindow.postMessage(msg, origin);
+ }
+ } else {
+ promise.setSuccess();
+ }
+
+ return promise.promise;
+ }
+
+ function check3pCookiesSupported() {
+ var promise = createPromise();
+
+ if (loginIframe.enable || kc.silentCheckSsoRedirectUri) {
+ var iframe = document.createElement('iframe');
+ iframe.setAttribute('src', kc.endpoints.thirdPartyCookiesIframe());
+ iframe.setAttribute('title', 'keycloak-3p-check-iframe' );
+ iframe.style.display = 'none';
+ document.body.appendChild(iframe);
+
+ var messageCallback = function(event) {
+ if (iframe.contentWindow !== event.source) {
+ return;
+ }
+
+ if (event.data !== "supported" && event.data !== "unsupported") {
+ return;
+ } else if (event.data === "unsupported") {
+ loginIframe.enable = false;
+ if (kc.silentCheckSsoFallback) {
+ kc.silentCheckSsoRedirectUri = false;
+ }
+ logWarn("[KEYCLOAK] 3rd party cookies aren't supported by this browser. checkLoginIframe and " +
+ "silent check-sso are not available.");
+ }
+
+ document.body.removeChild(iframe);
+ window.removeEventListener("message", messageCallback);
+ promise.setSuccess();
+ };
+
+ window.addEventListener('message', messageCallback, false);
+ } else {
+ promise.setSuccess();
+ }
+
+ return applyTimeoutToPromise(promise.promise, kc.messageReceiveTimeout, "Timeout when waiting for 3rd party check iframe message.");
+ }
+
+ function loadAdapter(type) {
+ if (!type || type == 'default') {
+ return {
+ login: function(options) {
+ window.location.replace(kc.createLoginUrl(options));
+ return createPromise().promise;
+ },
+
+ logout: function(options) {
+ window.location.replace(kc.createLogoutUrl(options));
+ return createPromise().promise;
+ },
+
+ register: function(options) {
+ window.location.replace(kc.createRegisterUrl(options));
+ return createPromise().promise;
+ },
+
+ accountManagement : function() {
+ var accountUrl = kc.createAccountUrl();
+ if (typeof accountUrl !== 'undefined') {
+ window.location.href = accountUrl;
+ } else {
+ throw "Not supported by the OIDC server";
+ }
+ return createPromise().promise;
+ },
+
+ redirectUri: function(options, encodeHash) {
+
+ if (options && options.redirectUri) {
+ return options.redirectUri;
+ } else if (kc.redirectUri) {
+ return kc.redirectUri;
+ } else {
+ return location.href;
+ }
+ }
+ };
+ }
+
+ if (type == 'cordova') {
+ loginIframe.enable = false;
+ var cordovaOpenWindowWrapper = function(loginUrl, target, options) {
+ if (window.cordova && window.cordova.InAppBrowser) {
+ // Use inappbrowser for IOS and Android if available
+ return window.cordova.InAppBrowser.open(loginUrl, target, options);
+ } else {
+ return window.open(loginUrl, target, options);
+ }
+ };
+
+ var shallowCloneCordovaOptions = function (userOptions) {
+ if (userOptions && userOptions.cordovaOptions) {
+ return Object.keys(userOptions.cordovaOptions).reduce(function (options, optionName) {
+ options[optionName] = userOptions.cordovaOptions[optionName];
+ return options;
+ }, {});
+ } else {
+ return {};
+ }
+ };
+
+ var formatCordovaOptions = function (cordovaOptions) {
+ return Object.keys(cordovaOptions).reduce(function (options, optionName) {
+ options.push(optionName+"="+cordovaOptions[optionName]);
+ return options;
+ }, []).join(",");
+ };
+
+ var createCordovaOptions = function (userOptions) {
+ var cordovaOptions = shallowCloneCordovaOptions(userOptions);
+ cordovaOptions.location = 'no';
+ if (userOptions && userOptions.prompt == 'none') {
+ cordovaOptions.hidden = 'yes';
+ }
+ return formatCordovaOptions(cordovaOptions);
+ };
+
+ return {
+ login: function(options) {
+ var promise = createPromise();
+
+ var cordovaOptions = createCordovaOptions(options);
+ var loginUrl = kc.createLoginUrl(options);
+ var ref = cordovaOpenWindowWrapper(loginUrl, '_blank', cordovaOptions);
+ var completed = false;
+
+ var closed = false;
+ var closeBrowser = function() {
+ closed = true;
+ ref.close();
+ };
+
+ ref.addEventListener('loadstart', function(event) {
+ if (event.url.indexOf('http://localhost') == 0) {
+ var callback = parseCallback(event.url);
+ processCallback(callback, promise);
+ closeBrowser();
+ completed = true;
+ }
+ });
+
+ ref.addEventListener('loaderror', function(event) {
+ if (!completed) {
+ if (event.url.indexOf('http://localhost') == 0) {
+ var callback = parseCallback(event.url);
+ processCallback(callback, promise);
+ closeBrowser();
+ completed = true;
+ } else {
+ promise.setError();
+ closeBrowser();
+ }
+ }
+ });
+
+ ref.addEventListener('exit', function(event) {
+ if (!closed) {
+ promise.setError({
+ reason: "closed_by_user"
+ });
+ }
+ });
+
+ return promise.promise;
+ },
+
+ logout: function(options) {
+ var promise = createPromise();
+
+ var logoutUrl = kc.createLogoutUrl(options);
+ var ref = cordovaOpenWindowWrapper(logoutUrl, '_blank', 'location=no,hidden=yes,clearcache=yes');
+
+ var error;
+
+ ref.addEventListener('loadstart', function(event) {
+ if (event.url.indexOf('http://localhost') == 0) {
+ ref.close();
+ }
+ });
+
+ ref.addEventListener('loaderror', function(event) {
+ if (event.url.indexOf('http://localhost') == 0) {
+ ref.close();
+ } else {
+ error = true;
+ ref.close();
+ }
+ });
+
+ ref.addEventListener('exit', function(event) {
+ if (error) {
+ promise.setError();
+ } else {
+ kc.clearToken();
+ promise.setSuccess();
+ }
+ });
+
+ return promise.promise;
+ },
+
+ register : function(options) {
+ var promise = createPromise();
+ var registerUrl = kc.createRegisterUrl();
+ var cordovaOptions = createCordovaOptions(options);
+ var ref = cordovaOpenWindowWrapper(registerUrl, '_blank', cordovaOptions);
+ ref.addEventListener('loadstart', function(event) {
+ if (event.url.indexOf('http://localhost') == 0) {
+ ref.close();
+ var oauth = parseCallback(event.url);
+ processCallback(oauth, promise);
+ }
+ });
+ return promise.promise;
+ },
+
+ accountManagement : function() {
+ var accountUrl = kc.createAccountUrl();
+ if (typeof accountUrl !== 'undefined') {
+ var ref = cordovaOpenWindowWrapper(accountUrl, '_blank', 'location=no');
+ ref.addEventListener('loadstart', function(event) {
+ if (event.url.indexOf('http://localhost') == 0) {
+ ref.close();
+ }
+ });
+ } else {
+ throw "Not supported by the OIDC server";
+ }
+ },
+
+ redirectUri: function(options) {
+ return 'http://localhost';
+ }
+ }
+ }
+
+ if (type == 'cordova-native') {
+ loginIframe.enable = false;
+
+ return {
+ login: function(options) {
+ var promise = createPromise();
+ var loginUrl = kc.createLoginUrl(options);
+
+ universalLinks.subscribe('keycloak', function(event) {
+ universalLinks.unsubscribe('keycloak');
+ window.cordova.plugins.browsertab.close();
+ var oauth = parseCallback(event.url);
+ processCallback(oauth, promise);
+ });
+
+ window.cordova.plugins.browsertab.openUrl(loginUrl);
+ return promise.promise;
+ },
+
+ logout: function(options) {
+ var promise = createPromise();
+ var logoutUrl = kc.createLogoutUrl(options);
+
+ universalLinks.subscribe('keycloak', function(event) {
+ universalLinks.unsubscribe('keycloak');
+ window.cordova.plugins.browsertab.close();
+ kc.clearToken();
+ promise.setSuccess();
+ });
+
+ window.cordova.plugins.browsertab.openUrl(logoutUrl);
+ return promise.promise;
+ },
+
+ register : function(options) {
+ var promise = createPromise();
+ var registerUrl = kc.createRegisterUrl(options);
+ universalLinks.subscribe('keycloak' , function(event) {
+ universalLinks.unsubscribe('keycloak');
+ window.cordova.plugins.browsertab.close();
+ var oauth = parseCallback(event.url);
+ processCallback(oauth, promise);
+ });
+ window.cordova.plugins.browsertab.openUrl(registerUrl);
+ return promise.promise;
+
+ },
+
+ accountManagement : function() {
+ var accountUrl = kc.createAccountUrl();
+ if (typeof accountUrl !== 'undefined') {
+ window.cordova.plugins.browsertab.openUrl(accountUrl);
+ } else {
+ throw "Not supported by the OIDC server";
+ }
+ },
+
+ redirectUri: function(options) {
+ if (options && options.redirectUri) {
+ return options.redirectUri;
+ } else if (kc.redirectUri) {
+ return kc.redirectUri;
+ } else {
+ return "http://localhost";
+ }
+ }
+ }
+ }
+
+ throw 'invalid adapter type: ' + type;
+ }
+
+ var LocalStorage = function() {
+ if (!(this instanceof LocalStorage)) {
+ return new LocalStorage();
+ }
+
+ localStorage.setItem('kc-test', 'test');
+ localStorage.removeItem('kc-test');
+
+ var cs = this;
+
+ function clearExpired() {
+ var time = new Date().getTime();
+ for (var i = 0; i < localStorage.length; i++) {
+ var key = localStorage.key(i);
+ if (key && key.indexOf('kc-callback-') == 0) {
+ var value = localStorage.getItem(key);
+ if (value) {
+ try {
+ var expires = JSON.parse(value).expires;
+ if (!expires || expires < time) {
+ localStorage.removeItem(key);
+ }
+ } catch (err) {
+ localStorage.removeItem(key);
+ }
+ }
+ }
+ }
+ }
+
+ cs.get = function(state) {
+ if (!state) {
+ return;
+ }
+
+ var key = 'kc-callback-' + state;
+ var value = localStorage.getItem(key);
+ if (value) {
+ localStorage.removeItem(key);
+ value = JSON.parse(value);
+ }
+
+ clearExpired();
+ return value;
+ };
+
+ cs.add = function(state) {
+ clearExpired();
+
+ var key = 'kc-callback-' + state.state;
+ state.expires = new Date().getTime() + (60 * 60 * 1000);
+ localStorage.setItem(key, JSON.stringify(state));
+ };
+ };
+
+ var CookieStorage = function() {
+ if (!(this instanceof CookieStorage)) {
+ return new CookieStorage();
+ }
+
+ var cs = this;
+
+ cs.get = function(state) {
+ if (!state) {
+ return;
+ }
+
+ var value = getCookie('kc-callback-' + state);
+ setCookie('kc-callback-' + state, '', cookieExpiration(-100));
+ if (value) {
+ return JSON.parse(value);
+ }
+ };
+
+ cs.add = function(state) {
+ setCookie('kc-callback-' + state.state, JSON.stringify(state), cookieExpiration(60));
+ };
+
+ cs.removeItem = function(key) {
+ setCookie(key, '', cookieExpiration(-100));
+ };
+
+ var cookieExpiration = function (minutes) {
+ var exp = new Date();
+ exp.setTime(exp.getTime() + (minutes*60*1000));
+ return exp;
+ };
+
+ var getCookie = function (key) {
+ var name = key + '=';
+ var ca = document.cookie.split(';');
+ for (var i = 0; i < ca.length; i++) {
+ var c = ca[i];
+ while (c.charAt(0) == ' ') {
+ c = c.substring(1);
+ }
+ if (c.indexOf(name) == 0) {
+ return c.substring(name.length, c.length);
+ }
+ }
+ return '';
+ };
+
+ var setCookie = function (key, value, expirationDate) {
+ var cookie = key + '=' + value + '; '
+ + 'expires=' + expirationDate.toUTCString() + '; ';
+ document.cookie = cookie;
+ };
+ };
+
+ function createCallbackStorage() {
+ try {
+ return new LocalStorage();
+ } catch (err) {
+ }
+
+ return new CookieStorage();
+ }
+
+ function createLogger(fn) {
+ return function() {
+ if (kc.enableLogging) {
+ fn.apply(console, Array.prototype.slice.call(arguments));
+ }
+ };
+ }
+ }
+
+ return Keycloak;
+
+}));
diff --git a/client-data/tools/keycloak/icon.svg b/client-data/tools/keycloak/icon.svg
new file mode 100644
index 00000000..c7ff64bb
--- /dev/null
+++ b/client-data/tools/keycloak/icon.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/client-data/tools/keycloak/keycloak.css b/client-data/tools/keycloak/keycloak.css
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/client-data/tools/keycloak/keycloak.css
@@ -0,0 +1 @@
+
diff --git a/client-data/tools/keycloak/keycloak.js b/client-data/tools/keycloak/keycloak.js
new file mode 100644
index 00000000..fff799b7
--- /dev/null
+++ b/client-data/tools/keycloak/keycloak.js
@@ -0,0 +1,65 @@
+/**
+ * WHITEBOPHIR
+ *********************************************************
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page.
+ *
+ * Copyright (C) 2013 Ophir LOJKINE
+ *
+ *
+ * The JavaScript code in this page is free software: you can
+ * redistribute it and/or modify it under the terms of the GNU
+ * General Public License (GNU GPL) as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option)
+ * any later version. The code is distributed WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
+ *
+ * As additional permission under GNU GPL version 3 section 7, you
+ * may distribute non-source (e.g., minimized or compacted) forms of
+ * that code without the copy of the GNU GPL normally required by
+ * section 4, provided you include this license notice and a URL
+ * through which recipients can access the Corresponding Source.
+ *
+ * @licend
+ */
+
+(function () { //Code isolation
+ if (Tools.server_config.KEYCLOAK_ENABLE) {
+ var keycloak = Keycloak({
+ url: Tools.server_config.KEYCLOAK_URL,
+ realm: Tools.server_config.KEYCLOAK_REALM,
+ clientId: Tools.server_config.KEYCLOAK_CLIENTID
+ });
+
+ keycloak.init({
+ onLoad: 'login-required'
+ }).catch(function(e) {
+ console.log(e);
+ });
+
+ function onStart() {
+ keycloak.logout({
+ redirectUri: window.location.href.split("/boards/")[0]
+ });
+ }
+
+ keycloak.onAuthSuccess = function() {
+ keycloak.loadUserInfo().then(function(userInfo) {
+ if (Tools.server_config.KEYCLOAK_USERINFO_ATTRIBUTE) {
+ if (!userInfo[Tools.server_config.KEYCLOAK_USERINFO_ATTRIBUTE]) {
+ alert(Tools.i18n.t("access_denied" || "") + userInfo.preferred_username);
+ keycloak.logout();
+ }
+ }
+ Tools.add({ //The new tool
+ "name": Tools.i18n.t("logout" || "") + userInfo.given_name + " " + userInfo.family_name,
+ "shortcut": "L",
+ "onstart": onStart,
+ "stylesheet": "tools/keycloak/keycloak.css",
+ "icon": "tools/keycloak/icon.svg"
+ });
+ });
+ }
+ }
+})(); //End of code isolation
diff --git a/package-lock.json b/package-lock.json
index e7cb6c84..bbb14b20 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,7 +5,6 @@
"requires": true,
"packages": {
"": {
- "name": "whitebophir",
"version": "1.17.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
@@ -13,6 +12,7 @@
"async-mutex": "^0.3.1",
"handlebars": "^4.7.7",
"jsonwebtoken": "^8.5.1",
+ "keycloak-js": "^17.0.1",
"polyfill-library": "^3.107.1",
"serve-static": "^1.14.1",
"socket.io": "^4",
@@ -364,6 +364,25 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
@@ -1899,6 +1918,11 @@
"node": "*"
}
},
+ "node_modules/js-sha256": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
+ "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="
+ },
"node_modules/js-yaml": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
@@ -2006,6 +2030,15 @@
"safe-buffer": "^5.0.1"
}
},
+ "node_modules/keycloak-js": {
+ "version": "17.0.1",
+ "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-17.0.1.tgz",
+ "integrity": "sha512-mbLBSoogCBX5VYeKCdEz8BaRWVL9twzSqArRU3Mo3Z7vEO1mghGZJ5IzREfiMEi7kTUZtk5i9mu+Yc0koGkK6g==",
+ "dependencies": {
+ "base64-js": "^1.5.1",
+ "js-sha256": "^0.9.0"
+ }
+ },
"node_modules/keyv": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.5.tgz",
@@ -4299,6 +4332,11 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
+ "base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
+ },
"base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
@@ -5451,6 +5489,11 @@
"minimatch": "^3.0.4"
}
},
+ "js-sha256": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
+ "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="
+ },
"js-yaml": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
@@ -5548,6 +5591,15 @@
"safe-buffer": "^5.0.1"
}
},
+ "keycloak-js": {
+ "version": "17.0.1",
+ "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-17.0.1.tgz",
+ "integrity": "sha512-mbLBSoogCBX5VYeKCdEz8BaRWVL9twzSqArRU3Mo3Z7vEO1mghGZJ5IzREfiMEi7kTUZtk5i9mu+Yc0koGkK6g==",
+ "requires": {
+ "base64-js": "^1.5.1",
+ "js-sha256": "^0.9.0"
+ }
+ },
"keyv": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.5.tgz",
diff --git a/package.json b/package.json
index 7d4250d1..ea28510e 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"async-mutex": "^0.3.1",
"handlebars": "^4.7.7",
"jsonwebtoken": "^8.5.1",
+ "keycloak-js": "^17.0.1",
"polyfill-library": "^3.107.1",
"serve-static": "^1.14.1",
"socket.io": "^4",
diff --git a/server/client_configuration.js b/server/client_configuration.js
index e8957a1d..8408ae6b 100644
--- a/server/client_configuration.js
+++ b/server/client_configuration.js
@@ -8,4 +8,10 @@ module.exports = {
BLOCKED_TOOLS: config.BLOCKED_TOOLS,
BLOCKED_SELECTION_BUTTONS: config.BLOCKED_SELECTION_BUTTONS,
AUTO_FINGER_WHITEOUT: config.AUTO_FINGER_WHITEOUT,
+ KEYCLOAK_ENABLE: config.KEYCLOAK_ENABLE,
+ KEYCLOAK_URL: config.KEYCLOAK_URL,
+ KEYCLOAK_REALM: config.KEYCLOAK_REALM,
+ KEYCLOAK_CLIENTID: config.KEYCLOAK_CLIENTID,
+ KEYCLOAK_USERINFO_ATTRIBUTE: config.KEYCLOAK_USERINFO_ATTRIBUTE
+
};
diff --git a/server/configuration.js b/server/configuration.js
index 04036b98..e883fba3 100644
--- a/server/configuration.js
+++ b/server/configuration.js
@@ -57,4 +57,15 @@ module.exports = {
/** Secret key for jwt */
AUTH_SECRET_KEY: (process.env["AUTH_SECRET_KEY"] || ""),
+
+ KEYCLOAK_ENABLE: (process.env["KEYCLOAK_ENABLE"] || false),
+
+ KEYCLOAK_URL: process.env["KEYCLOAK_URL"],
+
+ KEYCLOAK_REALM: process.env["KEYCLOAK_REALM"],
+
+ KEYCLOAK_CLIENTID: process.env["KEYCLOAK_CLIENTID"],
+
+ KEYCLOAK_USERINFO_ATTRIBUTE: process.env["KEYCLOAK_USERINFO_ATTRIBUTE"],
+
};
diff --git a/server/translations.json b/server/translations.json
index ba6d0bc0..d08adbc9 100644
--- a/server/translations.json
+++ b/server/translations.json
@@ -37,7 +37,9 @@
"tools": "Werkzeuge",
"view_source": "Quellcode auf GitHub",
"white-out": "Korrekturflüssigkeit",
- "zoom": "Zoom"
+ "zoom": "Zoom",
+ "access_denied": "System nicht verfügbar für ",
+ "logout": "Logout "
},
"en": {
"White-out": "White-out",
@@ -77,7 +79,9 @@
"text": "Text",
"tools": "Tools",
"view_source": "Source code on GitHub",
- "zoom": "Zoom"
+ "zoom": "Zoom",
+ "access_denied": "System not available for ",
+ "logout": "Logout "
},
"es": {
"board_name_placeholder": "Nombre de la pizarra …",
@@ -116,7 +120,9 @@
"tools": "Herramientas",
"view_source": "Código fuente en GitHub",
"white-out": "Blanqueado",
- "zoom": "Zoom"
+ "zoom": "Zoom",
+ "access_denied": "Sistema no disponible para ",
+ "logout": "Logout "
},
"fr": {
"board_name_placeholder": "Nom du tableau…",
@@ -154,7 +160,9 @@
"text": "Texte",
"tools": "Outils",
"view_source": "Code source sur GitHub",
- "white-out": "Blanco"
+ "white-out": "Blanco",
+ "access_denied": "Système non disponible pour ",
+ "logout": "Logout "
},
"hu": {
"White-out": "Lefedő",
@@ -232,7 +240,9 @@
"tools": "Strumenti",
"view_source": "Codice sorgente su GitHub",
"white-out": "Bianchetto",
- "zoom": "Zoom"
+ "zoom": "Zoom",
+ "access_denied": "Sistema non disponibile per ",
+ "logout": "Logout "
},
"ja": {
"board_name_placeholder": "ボードの名前",