Skip to content

Instantly share code, notes, and snippets.

@bgeels
Created October 29, 2020 04:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bgeels/f7e109e2372142c9faea99ef489db0b5 to your computer and use it in GitHub Desktop.
Save bgeels/f7e109e2372142c9faea99ef489db0b5 to your computer and use it in GitHub Desktop.
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