Created
October 29, 2020 04:31
-
-
Save bgeels/f7e109e2372142c9faea99ef489db0b5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
exports.shibb_request = async (url, options) => { | |
options = options || {}; | |
// step 1 in shibb handshake process | |
const shibbAuthHandshakeStep1 = async ( response, idp_server ) => { | |
return response.text().then( data => { | |
// Parse the html page shibboleth redirected us to | |
let parser = new DOMParser(); | |
let html = parser.parseFromString(data, 'text/html'); | |
// Grab the form's action | |
let form_action = html.querySelector('form').getAttribute('action'); | |
// Pick apart the form elements... | |
// | |
// NOTE: Technically you can store a shibb auth key returned in this form in local | |
// storage and send the "shib_idp_ls_success.shib_idp_session_ss" parameter in the | |
// request below as "true" to indicate to shibboleth you've successfully stored it locally. | |
// | |
// When you attempt to auth again the IDP server will be smart enough to know that | |
// you have an auth key stored locally when it sees the shibb session cookie and | |
// forgo a step in the auth process. | |
// | |
// However, this would only save you 1 round trip to the idp server ( in the case | |
// where you didn't already have a shibb session cookie only ) at the cost of added | |
// complexity in this utility script. That being the case, we simply send the | |
// "shib_idp_ls_success.shib_idp_session_ss" parameter as "false" no matter what and | |
// continue on to the extra step. | |
let form_inputs = html.querySelectorAll('form input'); | |
let body = new URLSearchParams(); | |
form_inputs.forEach( (input) => { | |
// skip inputs where the type is 'submit' since we'll be handling the request | |
if( input.getAttribute('type') == 'submit' ){ | |
return; | |
} | |
let name = input.getAttribute('name'); | |
let value = input.getAttribute('value'); | |
body.append(name, value); | |
}); | |
// In some browsers we don't have the full url returned in the form action. | |
// If we don't, prepend it. | |
if(!form_action.match(/^http/)){ | |
form_action = idp_server + form_action; | |
} | |
// Now post the first request to the IDP server the browser would have handled for us | |
return fetch( form_action, { | |
method: 'post', | |
credentials: 'include', | |
body: body | |
}); | |
}); | |
}; | |
// step 2 in shibb handshake process | |
const shibbAuthHandshakeStep2 = async ( response ) => { | |
return response.text().then( data => { | |
// Parse the html page shibboleth redirected us to | |
let parser = new DOMParser(); | |
let html = parser.parseFromString(data, 'text/html'); | |
// Grab the form's action | |
let form_action = html.querySelector('form').getAttribute('action'); | |
// Pick apart the form elements... | |
let form_inputs = html.querySelectorAll('form input'); | |
let body = new URLSearchParams(); | |
form_inputs.forEach( (input) => { | |
// skip inputs where the type is 'submit' since we'll be handling the request | |
if( input.getAttribute('type') == 'submit' ){ | |
return; | |
} | |
let name = input.getAttribute('name'); | |
let value = input.getAttribute('value'); | |
body.append(name, value); | |
}); | |
// In some browsers we don't have the full url returned in the form action. | |
// If we don't, prepend it. | |
if(!form_action.match(/^http/)){ | |
form_action = idp_server + form_action; | |
} | |
// Now post the second request to the IDP server the browser would have handled for us | |
// | |
// Note: This time our form action was the full url to the remote data server. | |
// This is the final request we'll need to get a shibb auth cookie. By default this request will | |
// redirect to the originally request page. | |
// However, we want want to prevent this from happening by sending the "redirects: 'manual'" parameter. | |
// We do this is b/c we don't want to blindly redirect to the original page. If we initially made a post | |
// request we want to ensure all of our parameters we initially passed will be correctly posted to the | |
// remote data server. So handle that manually now. | |
return fetch( form_action, { | |
method: 'post', | |
redirect: 'manual', | |
credentials: 'include', | |
body: body | |
}); | |
}; | |
// Start request | |
let response = await fetch(url, { | |
...options, | |
credentials: 'include' | |
}); | |
// If we weren't redirected we've already authed, so simply return the promise. | |
if(!response.redirected){ | |
return response; | |
} | |
// If we got here we were redirected so check if we need to do the shibboleth auth handshake. | |
// | |
// Check if the url we were directed to looks like a shibboleth url. | |
let match_idp_single = response.url.match(new RegExp("^(.*)/idp/profile/SAML2/Redirect/SSO")); | |
let match_idp_multiple = response.url.match(new RegExp("^.*/shibboleth-ds/index.html\\?(.*)")); | |
let idp_server; | |
// If we matched this pattern, the remote data server only allows you to auth through one idp and it's redirected us directly there. | |
if( match_idp_single ) { | |
// Store the idp_server for use in subsequent handshake steps. | |
idp_server = match_idp_single[1]; | |
} | |
// If we matched this pattern, the remote data server allows you to choose an IDP for authentication. Manually handle this interaction | |
// making an informed decision about which IDP to use. | |
else if( match_idp_multiple ) { | |
// Make a request to the custom /shibb-env location we've setup, to determine which idp | |
// the user is already logged into on the local server. | |
let env_resp = await fetch('/shibb-env'); | |
if(!response.ok){ | |
throw "It appears you don't have your /shibb-env location setup. Can not handle CORs request with multiple idp selectors without this"; | |
} | |
let idp_server_entity = env_resp.headers.get('Shib-Identity-Provider'); | |
url_obj = new URL(idp_server_entity); | |
idp_server = url_obj.protocol+'//'+url_obj.host; | |
let params = new URLSearchParams(decodeURIComponent(response.url.split('?')[1])); | |
let ret = params.get('return'); | |
let target = params.get('target'); | |
response = await fetch(ret+'&'+'entityID='+idp_server_entity+'&target='+target, { | |
method: 'get', | |
credentials: 'include' | |
}); | |
} | |
// If we didn't get a match we were redirected to something but not to a known shibboleth auth page so just return the response promise | |
// and let the application handle it. | |
// At this point we've submitted the initial request to start the authentication process and have been redirected to the first form. | |
// | |
// Parse the first html form and submit the request. | |
let shibb_resp1 = await shibbAuthHandshakeStep1( response, idp_server ); | |
// Parse the second html form and submit the request. | |
let shibb_resp2 = await shibbAuthHandshakeStep2( shibb_resp1 ); | |
// On second form submissions we told it not to follow redirects. You will see the request canceled in the browser. | |
// The last thing it does is try to redirect to the initially requested resource but we want to handle this manually | |
// below since it redirecting automatically can drop off some parameters from the original request. | |
if(!shibb_resp2 || shibb_resp2.type != 'opaqueredirect'){ | |
throw "Unexpected result in shibb handshake step 2, should get a request of type opaqueredirect"; | |
} | |
// Now we have a shibb auth cookie. Make the original request again including the cookie via "credentials: 'include'" | |
return fetch(url, { | |
credentials: 'include', | |
...options | |
}); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment