2019-02-14 15:39:19 +00:00
import Ember from 'ember' ;
import { inject as service } from '@ember/service' ;
import Component from './outer-html' ;
2019-05-03 03:20:28 +00:00
import { later } from '@ember/runloop' ;
2019-02-14 15:39:19 +00:00
import { task , timeout , waitForEvent } from 'ember-concurrency' ;
import { computed } from '@ember/object' ;
2021-12-17 03:44:29 +00:00
import { waitFor } from '@ember/test-waiters' ;
2019-02-14 15:39:19 +00:00
2021-12-17 03:44:29 +00:00
const WAIT _TIME = 500 ;
2019-02-14 15:39:19 +00:00
const ERROR _WINDOW _CLOSED =
'The provider window was closed before authentication was complete. Please click Sign In to try again.' ;
const ERROR _MISSING _PARAMS =
'The callback from the provider did not supply all of the required parameters. Please click Sign In to try again. If the problem persists, you may want to contact your administrator.' ;
2020-05-08 23:00:28 +00:00
const ERROR _JWT _LOGIN = 'OIDC login is not configured for this mount' ;
export { ERROR _WINDOW _CLOSED , ERROR _MISSING _PARAMS , ERROR _JWT _LOGIN } ;
2019-02-14 15:39:19 +00:00
export default Component . extend ( {
store : service ( ) ,
2021-04-07 16:46:06 +00:00
featureFlagService : service ( 'featureFlag' ) ,
2019-02-14 15:39:19 +00:00
selectedAuthPath : null ,
2019-03-05 16:03:54 +00:00
selectedAuthType : null ,
2019-02-14 15:39:19 +00:00
roleName : null ,
role : null ,
2020-05-08 23:00:28 +00:00
errorMessage : null ,
2019-02-14 15:39:19 +00:00
onRoleName ( ) { } ,
onLoading ( ) { } ,
onError ( ) { } ,
onToken ( ) { } ,
onNamespace ( ) { } ,
didReceiveAttrs ( ) {
2021-12-17 03:44:29 +00:00
this . _super ( ) ;
2019-05-03 03:20:28 +00:00
let { oldSelectedAuthPath , selectedAuthPath } = this ;
let shouldDebounce = ! oldSelectedAuthPath && ! selectedAuthPath ;
if ( oldSelectedAuthPath !== selectedAuthPath ) {
this . set ( 'role' , null ) ;
this . onRoleName ( this . roleName ) ;
this . fetchRole . perform ( null , { debounce : false } ) ;
} else if ( shouldDebounce ) {
this . fetchRole . perform ( this . roleName ) ;
}
2020-05-08 23:00:28 +00:00
this . set ( 'errorMessage' , null ) ;
2019-05-03 03:20:28 +00:00
this . set ( 'oldSelectedAuthPath' , selectedAuthPath ) ;
2019-02-14 15:39:19 +00:00
} ,
2020-05-08 23:00:28 +00:00
// Assumes authentication using OIDC until it's known that the mount is
// configured for JWT authentication via static keys, JWKS, or OIDC discovery.
2021-12-17 03:44:29 +00:00
isOIDC : computed ( 'errorMessage' , function ( ) {
2020-05-08 23:00:28 +00:00
return this . errorMessage !== ERROR _JWT _LOGIN ;
2019-02-14 15:39:19 +00:00
} ) ,
getWindow ( ) {
return this . window || window ;
} ,
2021-12-17 03:44:29 +00:00
fetchRole : task (
waitFor ( function * ( roleName , options = { debounce : true } ) {
if ( options . debounce ) {
this . onRoleName ( roleName ) ;
// debounce
yield timeout ( Ember . testing ? 0 : WAIT _TIME ) ;
2019-02-14 15:39:19 +00:00
}
2021-12-17 03:44:29 +00:00
let path = this . selectedAuthPath || this . selectedAuthType ;
let id = JSON . stringify ( [ path , roleName ] ) ;
let role = null ;
try {
role = yield this . store . findRecord ( 'role-jwt' , id , { adapterOptions : { namespace : this . namespace } } ) ;
} catch ( e ) {
// throwing here causes failures in tests
if ( ( ! e . httpStatus || e . httpStatus !== 400 ) && ! Ember . testing ) {
throw e ;
}
if ( e . errors && e . errors . length > 0 ) {
this . set ( 'errorMessage' , e . errors [ 0 ] ) ;
}
2020-05-08 23:00:28 +00:00
}
2021-12-17 03:44:29 +00:00
this . set ( 'role' , role ) ;
} )
) . restartable ( ) ,
2019-02-14 15:39:19 +00:00
handleOIDCError ( err ) {
this . onLoading ( false ) ;
this . prepareForOIDC . cancelAll ( ) ;
this . onError ( err ) ;
} ,
2021-12-17 03:44:29 +00:00
prepareForOIDC : task ( function * ( oidcWindow ) {
2021-06-17 20:56:04 +00:00
const thisWindow = this . getWindow ( ) ;
2019-02-14 15:39:19 +00:00
// show the loading animation in the parent
this . onLoading ( true ) ;
// start watching the popup window and the current one
this . watchPopup . perform ( oidcWindow ) ;
this . watchCurrent . perform ( oidcWindow ) ;
2021-11-15 15:48:11 +00:00
// wait for message posted from oidc callback
// see issue https://github.com/hashicorp/vault/issues/12436
// ensure that postMessage event is from expected source
while ( true ) {
const event = yield waitForEvent ( thisWindow , 'message' ) ;
if ( event . origin !== thisWindow . origin || ! event . isTrusted ) {
return this . handleOIDCError ( ) ;
}
if ( event . data . source === 'oidc-callback' ) {
return this . exchangeOIDC . perform ( event . data , oidcWindow ) ;
}
// continue to wait for the correct message
2021-06-17 20:56:04 +00:00
}
2019-02-14 15:39:19 +00:00
} ) ,
2021-12-17 03:44:29 +00:00
watchPopup : task ( function * ( oidcWindow ) {
2019-02-14 15:39:19 +00:00
while ( true ) {
yield timeout ( WAIT _TIME ) ;
if ( ! oidcWindow || oidcWindow . closed ) {
return this . handleOIDCError ( ERROR _WINDOW _CLOSED ) ;
}
}
} ) ,
2021-12-17 03:44:29 +00:00
watchCurrent : task ( function * ( oidcWindow ) {
2021-06-17 20:56:04 +00:00
// when user is about to change pages, close the popup window
2019-02-14 15:39:19 +00:00
yield waitForEvent ( this . getWindow ( ) , 'beforeunload' ) ;
oidcWindow . close ( ) ;
} ) ,
closeWindow ( oidcWindow ) {
this . watchPopup . cancelAll ( ) ;
this . watchCurrent . cancelAll ( ) ;
oidcWindow . close ( ) ;
} ,
2021-12-17 03:44:29 +00:00
exchangeOIDC : task ( function * ( oidcState , oidcWindow ) {
2020-01-15 21:27:12 +00:00
if ( oidcState === null || oidcState === undefined ) {
2019-02-14 15:39:19 +00:00
return ;
}
this . onLoading ( true ) ;
2021-06-17 20:56:04 +00:00
let { namespace , path , state , code } = oidcState ;
2019-02-14 15:39:19 +00:00
2020-10-26 22:17:21 +00:00
// The namespace can be either be passed as a query paramter, or be embedded
// in the state param in the format `<state_id>,ns=<namespace>`. So if
// `namespace` is empty, check for namespace in state as well.
2021-04-07 16:46:06 +00:00
if ( namespace === '' || this . featureFlagService . managedNamespaceRoot ) {
2020-10-26 22:17:21 +00:00
let i = state . indexOf ( ',ns=' ) ;
if ( i >= 0 ) {
// ",ns=" is 4 characters
namespace = state . substring ( i + 4 ) ;
state = state . substring ( 0 , i ) ;
}
}
2019-02-14 15:39:19 +00:00
// defer closing of the window, but continue executing the task
later ( ( ) => {
this . closeWindow ( oidcWindow ) ;
} , WAIT _TIME ) ;
if ( ! path || ! state || ! code ) {
return this . handleOIDCError ( ERROR _MISSING _PARAMS ) ;
}
let adapter = this . store . adapterFor ( 'auth-method' ) ;
this . onNamespace ( namespace ) ;
let resp ;
// do the OIDC exchange, set the token on the parent component
// and submit auth form
try {
resp = yield adapter . exchangeOIDC ( path , state , code ) ;
} catch ( e ) {
return this . handleOIDCError ( e ) ;
}
let token = resp . auth . client _token ;
this . onToken ( token ) ;
yield this . onSubmit ( ) ;
} ) ,
actions : {
async startOIDCAuth ( data , e ) {
this . onError ( null ) ;
if ( e && e . preventDefault ) {
e . preventDefault ( ) ;
}
2020-05-08 23:00:28 +00:00
if ( ! this . isOIDC || ! this . role || ! this . role . authUrl ) {
2019-02-14 15:39:19 +00:00
return ;
}
2022-04-07 14:30:29 +00:00
try {
await this . fetchRole . perform ( this . roleName , { debounce : false } ) ;
} catch ( error ) {
// this task could be cancelled if the instances in didReceiveAttrs resolve after this was started
if ( error ? . name !== 'TaskCancelation' ) {
throw error ;
}
}
2019-02-14 15:39:19 +00:00
let win = this . getWindow ( ) ;
const POPUP _WIDTH = 500 ;
const POPUP _HEIGHT = 600 ;
let left = win . screen . width / 2 - POPUP _WIDTH / 2 ;
let top = win . screen . height / 2 - POPUP _HEIGHT / 2 ;
let oidcWindow = win . open (
this . role . authUrl ,
'vaultOIDCWindow' ,
2019-02-21 22:21:23 +00:00
` width= ${ POPUP _WIDTH } ,height= ${ POPUP _HEIGHT } ,resizable,scrollbars=yes,top= ${ top } ,left= ${ left } `
2019-02-14 15:39:19 +00:00
) ;
this . prepareForOIDC . perform ( oidcWindow ) ;
} ,
} ,
} ) ;