2022-04-27 19:38:36 +02:00
/ * *
* D4Science boot component that handles keykloak authz
* < d4s - boot - 2
* /@url: keycloak url, for example: https:/ / accounts . d4science . org / auth ]
* / @ r e a l m : i s t h e r e a l m [ d e f a u l t s t o : d 4 s c i e n c e ]
* / @ g a t e w a y : i s t h e c l i e n t i d , f o r e x a m p l e : " p r e . d 4 s c i e n c e . o r g "
* / @ c o n t e x t : i s t h e a u d i e n c e , f o r e x a m p l e : " % 2 F p r e d 4 s % 2 F p r e p r o d % 2 F p r e V R E "
* / @ r e d i r e c t - u r l : w h e r e t o g o o n l o g o u t
* /
2022-03-22 11:27:28 +01:00
window . customElements . define ( 'd4s-boot-2' , class extends HTMLElement {
2022-04-27 19:38:36 +02:00
# keycloak = null
# authorization = null
# url = null
# realm = "d4science"
# clientId = null
# redirectUrl = null
# audience = null
# authenticated = false
# locked = true
# queue = [ ]
# interval = null
2022-04-29 18:29:30 +02:00
# config = null
# rpt = null
2022-04-27 19:38:36 +02:00
constructor ( ) {
super ( )
}
2022-04-12 19:08:38 +02:00
2022-04-27 19:38:36 +02:00
unlock ( ) {
this . # locked = false
}
2022-03-22 11:27:28 +01:00
2022-04-27 19:38:36 +02:00
fire ( etype ) {
2022-12-14 10:28:04 +01:00
const tp = this . # keycloak . tokenParsed
var outtp = {
aud : tp . aud ,
azp : tp . azp ,
email : tp . email ,
email _verified : tp . email _verified ,
exp : tp . exp ,
family _name : tp . family _name ,
given _name : tp . given _name ,
locale : tp . locale ,
name : tp . name ,
preferred _username : tp . preferred _username
}
const evt = new CustomEvent ( etype , { detail : outtp } )
2022-04-27 19:38:36 +02:00
document . dispatchEvent ( evt )
}
2022-04-15 13:05:33 +02:00
2022-04-27 19:38:36 +02:00
connectedCallback ( ) {
this . startStateChecker ( )
2022-04-12 19:08:38 +02:00
2022-04-27 19:38:36 +02:00
this . loadKeycloak ( ) . then ( ( ) => {
console . log ( "Keycloak loaded" )
return this . initKeycloak ( )
2022-03-22 11:27:28 +01:00
2022-04-29 18:29:30 +02:00
} ) . then ( authenticated => {
2022-04-27 19:38:36 +02:00
if ( ! authenticated ) {
throw "Failed to authenticate"
}
console . log ( "Keycloak initialized and user authenticated" )
2022-04-29 18:29:30 +02:00
//console.log("Token exp: " + this.expirationDate(this.#keycloak.tokenParsed.exp))
2022-04-27 19:38:36 +02:00
//if an audience is provided then perform also authorization
if ( this . # audience ) {
2022-04-29 18:29:30 +02:00
return this . loadConfig ( )
2022-04-27 19:38:36 +02:00
} else {
2022-04-29 18:29:30 +02:00
Promise . resolve ( )
2022-04-27 19:38:36 +02:00
}
2022-03-22 11:27:28 +01:00
2022-04-27 19:38:36 +02:00
} ) . then ( ( ) => {
this . # authenticated = true
this . unlock ( )
this . fire ( "authenticated" )
2022-03-22 11:27:28 +01:00
2022-04-27 19:38:36 +02:00
} ) . catch ( err => {
console . error ( "Unable to initialize Keycloak" , err )
} )
}
2022-03-22 11:27:28 +01:00
2022-04-27 19:38:36 +02:00
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 ( )
}
} )
}
2022-03-22 11:27:28 +01:00
2022-04-27 19:38:36 +02:00
initKeycloak ( ) {
this . # keycloak = new Keycloak ( {
url : this . # url ,
realm : this . # realm ,
clientId : this . # clientId
} )
2022-03-30 11:40:05 +02:00
2022-04-27 19:38:36 +02:00
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 )
2022-04-29 18:29:30 +02:00
return this . entitlement ( audience )
2022-03-22 11:27:28 +01:00
} else {
2022-04-27 19:38:36 +02:00
return Promise . resolve ( this . # keycloak . token )
2022-03-22 11:27:28 +01:00
}
2022-04-27 19:38:36 +02:00
} ) . then ( token => {
2022-12-14 10:28:04 +01:00
console . log ( "Authorized. Token exp: " + this . expirationDate ( this . parseJwt ( token ) . exp ) )
2022-04-27 19:38:36 +02:00
//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 )
2022-03-22 11:27:28 +01:00
2022-12-14 10:28:04 +01:00
} ) . catch ( err => console . error ( "Unable to make calls: " + err ) ) // Sometimes throws: Unable to make calls: TypeError: Cannot read properties of undefined (reading 'split')
2022-03-22 11:27:28 +01:00
}
2022-04-27 19:38:36 +02:00
}
} , 300 )
}
2022-04-15 13:05:33 +02:00
2022-04-27 19:38:36 +02:00
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 ( '' ) )
2022-03-22 11:27:28 +01:00
2022-04-27 19:38:36 +02:00
return JSON . parse ( jsonPayload ) ;
}
2022-03-22 11:27:28 +01:00
2022-04-27 19:38:36 +02:00
expirationDate ( utc ) {
let d = new Date ( 0 )
d . setUTCSeconds ( utc )
return d
}
2022-03-22 11:27:28 +01:00
2022-04-27 19:38:36 +02:00
checkContext ( ) {
const parseJwt = this . parseJwt
const expDt = this . expirationDate
const audience = encodeURIComponent ( this . # audience )
2022-04-29 18:29:30 +02:00
this . entitlement ( audience ) . then ( function ( rpt ) {
2022-04-27 19:38:36 +02:00
// onGrant callback function.
// If authorization was successful you'll receive an RPT
// with the necessary permissions to access the resource server
2022-12-14 10:28:04 +01:00
//console.log(rpt)
2022-04-27 19:38:36 +02:00
//console.log("rpt expires: " + expDt(parseJwt(rpt).exp))
} )
}
2022-03-22 11:27:28 +01:00
2022-04-27 19:38:36 +02:00
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
}
2022-03-22 11:27:28 +01:00
2022-04-27 19:38:36 +02:00
logout ( ) {
if ( this . # keycloak ) {
if ( ! this . # redirectUrl ) {
console . error ( "Missing required @redirectUrl attribute in d4s-boot" )
} else {
this . # keycloak . logout ( {
redirectUri : this . # redirectUrl
} )
}
2022-03-22 11:27:28 +01:00
}
2022-04-27 19:38:36 +02:00
}
2022-03-22 11:27:28 +01:00
2022-04-29 18:29:30 +02:00
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
2022-12-14 10:28:04 +01:00
console . log ( "Keycloak uma2 configuration loaded" )
2022-04-29 18:29:30 +02:00
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 ) )
}
} )
}
2022-04-27 19:38:36 +02:00
static get observedAttributes ( ) {
return [ "url" , "realm" , "gateway" , "redirect-url" , "context" ] ;
}
2022-03-22 11:27:28 +01:00
2022-04-27 19:38:36 +02:00
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
}
2022-03-22 11:27:28 +01:00
}
2022-04-27 19:38:36 +02:00
}
2022-03-22 11:27:28 +01:00
2022-04-27 19:38:36 +02:00
get authenticated ( ) {
return this . # authenticated
}
2022-03-22 11:27:28 +01:00
2022-07-21 18:38:24 +02:00
get subject ( ) {
return this . # keycloak . subject
}
get loginToken ( ) {
return this . # keycloak . tokenParsed
}
2022-04-27 19:38:36 +02:00
get url ( ) {
return this . # url
}
2022-04-12 19:08:38 +02:00
2022-04-27 19:38:36 +02:00
set url ( url ) {
this . # url = url
this . setAttribute ( "url" , url )
}
2022-04-12 19:08:38 +02:00
2022-04-27 19:38:36 +02:00
get realm ( ) {
return this . # realm
}
2022-04-12 19:08:38 +02:00
2022-04-27 19:38:36 +02:00
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 )
}
} )
2022-04-12 19:08:38 +02:00