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": "ボードの名前",