/** * D4Science boot component that handles keykloak authz * { console.log("Keycloak loaded") return this.initKeycloak() }).then(authenticated => { if (!authenticated) { throw "Failed to authenticate" } console.log("Keycloak initialized and user authenticated") //console.log("Token exp: " + this.expirationDate(this.#keycloak.tokenParsed.exp)) //if an audience is provided then perform also authorization if (this.#audience) { return this.loadConfig() } else { Promise.resolve() } }).then(() => { this.#authenticated = true this.unlock() this.fire("authenticated") }).catch(err => { console.error("Unable to initialize Keycloak", err) }) } loadKeycloak() { return new Promise((resolve, reject) => { if (typeof Keycloak === 'undefined') { const script = document.createElement('script') script.src = this.#url + '/js/keycloak.js' script.type = 'text/javascript' script.addEventListener('load', resolve) document.head.appendChild(script) } else { resolve() } }) } initKeycloak() { this.#keycloak = new Keycloak({ url: this.#url, realm: this.#realm, clientId: this.#clientId }) return this.#keycloak.init({onLoad: 'login-required', checkLoginIframe: false }) } startStateChecker() { this.#interval = window.setInterval(() => { if (this.#locked) { console.log("Still locked. Currently has " + this.#queue.length + " pending requests.") } else if (!this.authenticated) { window.alert("Not authorized!") } else { if (this.#queue.length > 0) { this.#keycloak.updateToken(30).then(() => { if (this.#audience) { console.log("Checking entitlement for audience", this.#audience) const audience = encodeURIComponent(this.#audience) return this.entitlement(audience) } else { return Promise.resolve(this.#keycloak.token) } }).then(token => { console.log("Authorized") console.log("Token exp: " + this.expirationDate(this.parseJwt(token).exp)) //console.log("Datetime: " + (new Date())) //transform all queued requests to fetches console.log("All pending requests to promises") let promises = this.#queue.map(r => { r.request.headers["Authorization"] = "Bearer " + token return r.resolve( fetch(r.url, r.request) ) }) //clear queue this.#queue = [] console.log("Resolving all fetches") return Promise.all(promises) }).catch(err => console.error("Unable to make calls: " + err)) } } }, 300) } parseJwt(token) { var base64Url = token.split('.')[1] var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/') var jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) }).join('')) return JSON.parse(jsonPayload); } expirationDate(utc) { let d = new Date(0) d.setUTCSeconds(utc) return d } checkContext() { const parseJwt = this.parseJwt const expDt = this.expirationDate const audience = encodeURIComponent(this.#audience) this.entitlement(audience).then(function (rpt) { // onGrant callback function. // If authorization was successful you'll receive an RPT // with the necessary permissions to access the resource server console.log(rpt) //console.log("rpt expires: " + expDt(parseJwt(rpt).exp)) }) } secureFetch(url, request) { const p = new Promise((resolve, reject) => { let req = request ? request : {} if ("headers" in req) { req.headers["Authorization"] = null } else { req.headers = { "Authorization" : null} } console.log("Queued request to url ", url) this.#queue.push({ url : url, request : req, resolve : resolve, reject : reject}) }) return p } logout() { if (this.#keycloak) { if (!this.#redirectUrl) { console.error("Missing required @redirectUrl attribute in d4s-boot") } else { this.#keycloak.logout({ redirectUri: this.#redirectUrl }) } } } loadConfig() { const kc = this.#keycloak return new Promise((resolve, reject) => { fetch(kc.authServerUrl + '/realms/' + kc.realm + '/.well-known/uma2-configuration') .then(response => response.json()) .then(json => { this.#config = json resolve(true) }) .catch(err => reject("Failed to fetch uma2-configuration from server: " + err)) }) } /** * Refactor of entitlement in '/js/keycloak-authz.js' * (dependency no longer needed) */ entitlement(resourceServerId, authorizationRequest) { return new Promise((resolve, reject) => { const keycloak = this.#keycloak const config = this.#config if (!keycloak || !config) { reject("Keycloak not initialized") } else { if (!authorizationRequest) { authorizationRequest = {} } var params = "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket&client_id=" + keycloak.clientId if (authorizationRequest.claimToken) { params += "&claim_token=" + authorizationRequest.claimToken if (authorizationRequest.claimTokenFormat) { params += "&claim_token_format=" + authorizationRequest.claimTokenFormat } } params += "&audience=" + resourceServerId var permissions = authorizationRequest.permissions if (!permissions) { permissions = [] } for (var i = 0; i < permissions.length; i++) { var resource = permissions[i] var permission = resource.id if (resource.scopes && resource.scopes.length > 0) { permission += "#" for (j = 0; j < resource.scopes.length; j++) { var scope = resource.scopes[j] if (permission.indexOf('#') != permission.length - 1) { permission += "," } permission += scope } } params += "&permission=" + permission } var metadata = authorizationRequest.metadata if (metadata) { if (metadata.responseIncludeResourceName) { params += "&response_include_resource_name=" + metadata.responseIncludeResourceName } if (metadata.responsePermissionsLimit) { params += "&response_permissions_limit=" + metadata.responsePermissionsLimit } } if (this.#rpt) { params += "&rpt=" + this.#rpt } fetch(config.token_endpoint, { method: 'POST', headers: { "Content-type" : "application/x-www-form-urlencoded", "Authorization" : "Bearer " + this.#keycloak.token }, body: params }) .then(response => response.json()) .then(json => { this.#rpt = json.access_token resolve(this.#rpt) }) .catch(err => reject(err)) } }) } static get observedAttributes() { return ["url", "realm", "gateway", "redirect-url", "context"]; } attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { switch (name) { case "url": this.#url = newValue break case "realm": this.#realm = newValue break case "gateway": this.#clientId = newValue break case "redirect-url": this.#redirectUrl = newValue break case "context": this.#audience = newValue break } } } get authenticated(){ return this.#authenticated } get subject(){ return this.#keycloak.subject } get loginToken(){ return this.#keycloak.tokenParsed } get url() { return this.#url } set url(url) { this.#url = url this.setAttribute("url", url) } get realm() { return this.#realm } set realm(realm) { this.#realm = realm this.setAttribute("realm", realm) } get clientId() { return this.#clientId } set clientId(clientId) { this.#clientId = clientId this.setAttribute("gateway", clientId) } get redirectUrl() { return this.#redirectUrl } set redirectUrl(redirectUrl) { this.#redirectUrl = redirectUrl this.setAttribute("redirect-url", redirectUrl) } get context() { return this.#audience } set context(context) { this.#audience = context this.setAttribute("context", context) } })