Latest commit

History

87 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

JavaScript Single Page Application and Ubisecure SSO

Example of a JavaScript Single Page Application that uses OpenID Connect 1.0 for logon and then invokes an OAuth 2.0 protected API. Launch this app by navigating to

https://psteniusubi.github.io/SimpleSPA/spa.html

The code for the API invoked by this application is in SimpleAPI

See also my related blog article Ubisecure SSO and Single Page Applications.

Code review

This project is hosted on GitHub Pages here. It also runs as-is on Apache HTTP server or any other web server that serves static resources.

This application is implemented in a single html page spa.html.

Get provider metadata

This method fetches the OpenID Provider metadata configuration information. The issuer parameter is the name of the OpenID Provider.

asyncfunctiongetConfiguration(issuer){consturi=`${issuer}/.well-known/openid-configuration`;constresponse=awaitfetch(uri);if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Send authentication request

This method builds an OpenID Connect authentication request and redirects the web browser to the OpenID Provider.

The code also creates a random nonce and PKCE code verifier. These items are stored in local storage with window.localStorage.setItem.

Generating PKCE code verifier

asyncfunctionnewCodeVerifier(method){switch(method){case"plain":
case"S256":
returnbtoaUrlSafe(Array.from(window.crypto.getRandomValues(newUint8Array(32)),t=>String.fromCharCode(t)).join(""))case"":
casenull:
returnnull;default:
throw"invalid argument";}}

PKCE code challenge

asyncfunctiongetCodeChallenge(method,code_verifier){switch(method){case"plain":
if(code_verifier===null)throw"invalid argument";returncode_verifier;case"S256":
if(code_verifier===null)throw"invalid argument";letbytes=Uint8Array.from(code_verifier,t=>t.charCodeAt(0));bytes=awaitwindow.crypto.subtle.digest("SHA-256",bytes);returnbtoaUrlSafe(Array.from(newUint8Array(bytes),t=>String.fromCharCode(t)).join(""));case"":
casenull:
returnnull;default:
throw"invalid argument";}}

Authentication request

asyncfunctionsendAuthenticationRequest(configuration,client_id,scope){constauthorization_request=newURL(configuration.authorization_endpoint);authorization_request.searchParams.set("response_type","code");authorization_request.searchParams.set("scope",scope);authorization_request.searchParams.set("client_id",client_id);authorization_request.searchParams.set("redirect_uri",location.origin+location.pathname);// nonceconstnonce=Array.from(window.crypto.getRandomValues(newUint32Array(4)),t=>t.toString(36)).join("");authorization_request.searchParams.set("nonce",nonce);window.localStorage.setItem("/SimpleSPA#nonce",nonce);// code_challenge_methodconstcode_challenge_method="S256";authorization_request.searchParams.set("code_challenge_method",code_challenge_method);// code_verifierconstcode_verifier=awaitnewCodeVerifier(code_challenge_method);window.localStorage.setItem("/SimpleSPA#code_verifier",code_verifier);// code_challengeconstcode_challenge=awaitgetCodeChallenge(code_challenge_method,code_verifier);authorization_request.searchParams.set("code_challenge",code_challenge);location.assign(authorization_request);}

Handle authorization response

The OpenID Provider redirects user agent back with authorization response message, containing either code or error parameters.

This code looks for code or error url parameters, then uses window.history.replaceState to remove url parameters from history.

If a code parameter is present then a token request is issued.

asyncfunctionhandleAuthenticationResponse(){constparams=newURLSearchParams(location.search.substr(1));if(params.has("code")){window.history.replaceState(null,null,location.pathname);constconfig=awaitgetConfiguration(registration.issuer);consttokenResponse=awaitinvokeTokenRequest(config,registration.client_id,registration.client_secret,params.get("code"));if("id_token"intokenResponse){constjwks=awaitgetJWKS(config);constid_token=awaitdecodeJWT(jwks,tokenResponse.id_token);constsignature_status=(id_token.signature===true) ? "signature verified" : "invalid signature";document.getElementById("signature").innerText=`(${signature_status})`;set_value("id_token",JSON.stringify(id_token.claims,null,2));constnonce_status=(id_token.claims.nonce==localStorage.getItem("/SimpleSPA#nonce")) ? "nonce verified" : "invalid nonce";document.getElementById("nonce").innerText=`(${nonce_status})`;localStorage.removeItem("/SimpleSPA#nonce");}if("access_token"intokenResponse){fetchWithToken=(input,init)=>{varrequest=newRequest(input,init);request.headers.set("Authorization","Bearer "+tokenResponse.access_token);returnwindow.fetch(request);};}else{fetchWithToken=null;}return;}if(params.has("error")){set_value("id_token",`error=${params.get("error")}`);}}

Invoke token request

The following builds and invokes an OAuth authorization code grant token request.

asyncfunctioninvokeTokenRequest(configuration,client_id,client_secret,code){consttoken_endpoint=configuration.token_endpoint;constheaders={"Content-Type": "application/x-www-form-urlencoded"};constbody=newURLSearchParams();body.set("grant_type","authorization_code");body.set("code",code);body.set("client_id",client_id);body.set("client_secret",client_secret);body.set("redirect_uri",location.origin+location.pathname);constcode_verifier=window.localStorage.getItem("/SimpleSPA#code_verifier");if(code_verifier){body.set("code_verifier",code_verifier);}try{constresponse=awaitfetch(token_endpoint,{method: "POST",mode: "cors",headers: headers,body: body.toString()});if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}finally{window.localStorage.removeItem("/SimpleSPA#code_verifier");}}

Get provider keys

The OpenID Provider's public keys are found in a JWKS document found from address specified by jwks_uri metadata property.

asyncfunctiongetJWKS(config){consturi=config.jwks_uri;constresponse=awaitfetch(uri);if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Validate ID Token integrity

ID Token is formatted as JWT, with three base64url encoded segments separated by "." character. The first part contains header, second part contains claims and final part is the signature which covers the first and second part. The WebCrypto API works with Uint8Array types so some type conversion with Uint8Array.from is needed.

asyncfunctiondecodeJWT(jwks,jwt){constjws=jwt.split(".");constheader=JSON.parse(atobUrlSafe(jws[0]));constclaims=JSON.parse(atobUrlSafe(jws[1]));consttext2verify=Uint8Array.from(jws[0]+"."+jws[1],t=>t.charCodeAt(0));constsignature=Uint8Array.from(atobUrlSafe(jws[2]),t=>t.charCodeAt(0));

Each signing key from OpenID Provider's jwks document is converted into WebCrypto Key with window.crypto.subtle.importKey. Then signature verification is attempted with window.crypto.subtle.verify.

Apparently there are some interoperability issues with JWK formatted keys and WebCrypto API which requires some transformation. One would assume algorithm and key identifiers of JWK, JWS and WebCrypto would be compatible but that appear to not be the case. In the example below I have hard coded RS256 algorithm. A real world solution needs to map JWK and JWS identifiers into WebCrypto identifiers.

constkeys=jwks.keys.filter(isSig).map(toJwk);constRS256={name: "RSASSA-PKCS1-v1_5",hash: {name: "SHA-256"},};for(constjwkofkeys){try{constkey=awaitwindow.crypto.subtle.importKey("jwk",jwk,RS256,false,["verify"]);constresult=awaitwindow.crypto.subtle.verify(RS256,key,signature,text2verify);if(result===true){return{"header": header,"claims": claims,"signature": true,"jwk": jwk,};}}catch{// ignore}}returnnegative;}

Invoke OAuth protected API

When invoking an OAuth protected API the access token is put into the Authorization http request header with the Bearer scheme.

fetchWithToken=(input,init)=>{varrequest=newRequest(input,init);request.headers.set("Authorization","Bearer "+tokenResponse.access_token);returnwindow.fetch(request);};
asyncfunctioninvokeApi(){const_fetch=fetchWithToken||window.fetch;constresponse=await_fetch(api_endpoint,{mode: "cors",cache: "no-store"});if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Running the application

This application is ready to run with Ubisecure SSO at login.example.ubidemo.com.

With GitHub Pages

  1. Navigate to https://psteniusubi.github.io/SimpleSPA/spa.html

About

JavaScript Single Page Application and Ubisecure SSO

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Used by

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

87 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

JavaScript Single Page Application and Ubisecure SSO

Example of a JavaScript Single Page Application that uses OpenID Connect 1.0 for logon and then invokes an OAuth 2.0 protected API. Launch this app by navigating to

https://psteniusubi.github.io/SimpleSPA/spa.html

The code for the API invoked by this application is in SimpleAPI

See also my related blog article Ubisecure SSO and Single Page Applications.

Code review

This project is hosted on GitHub Pages here. It also runs as-is on Apache HTTP server or any other web server that serves static resources.

This application is implemented in a single html page spa.html.

Get provider metadata

This method fetches the OpenID Provider metadata configuration information. The issuer parameter is the name of the OpenID Provider.

asyncfunctiongetConfiguration(issuer){consturi=`${issuer}/.well-known/openid-configuration`;constresponse=awaitfetch(uri);if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Send authentication request

This method builds an OpenID Connect authentication request and redirects the web browser to the OpenID Provider.

The code also creates a random nonce and PKCE code verifier. These items are stored in local storage with window.localStorage.setItem.

Generating PKCE code verifier

asyncfunctionnewCodeVerifier(method){switch(method){case"plain":
case"S256":
returnbtoaUrlSafe(Array.from(window.crypto.getRandomValues(newUint8Array(32)),t=>String.fromCharCode(t)).join(""))case"":
casenull:
returnnull;default:
throw"invalid argument";}}

PKCE code challenge

asyncfunctiongetCodeChallenge(method,code_verifier){switch(method){case"plain":
if(code_verifier===null)throw"invalid argument";returncode_verifier;case"S256":
if(code_verifier===null)throw"invalid argument";letbytes=Uint8Array.from(code_verifier,t=>t.charCodeAt(0));bytes=awaitwindow.crypto.subtle.digest("SHA-256",bytes);returnbtoaUrlSafe(Array.from(newUint8Array(bytes),t=>String.fromCharCode(t)).join(""));case"":
casenull:
returnnull;default:
throw"invalid argument";}}

Authentication request

asyncfunctionsendAuthenticationRequest(configuration,client_id,scope){constauthorization_request=newURL(configuration.authorization_endpoint);authorization_request.searchParams.set("response_type","code");authorization_request.searchParams.set("scope",scope);authorization_request.searchParams.set("client_id",client_id);authorization_request.searchParams.set("redirect_uri",location.origin+location.pathname);// nonceconstnonce=Array.from(window.crypto.getRandomValues(newUint32Array(4)),t=>t.toString(36)).join("");authorization_request.searchParams.set("nonce",nonce);window.localStorage.setItem("/SimpleSPA#nonce",nonce);// code_challenge_methodconstcode_challenge_method="S256";authorization_request.searchParams.set("code_challenge_method",code_challenge_method);// code_verifierconstcode_verifier=awaitnewCodeVerifier(code_challenge_method);window.localStorage.setItem("/SimpleSPA#code_verifier",code_verifier);// code_challengeconstcode_challenge=awaitgetCodeChallenge(code_challenge_method,code_verifier);authorization_request.searchParams.set("code_challenge",code_challenge);location.assign(authorization_request);}

Handle authorization response

The OpenID Provider redirects user agent back with authorization response message, containing either code or error parameters.

This code looks for code or error url parameters, then uses window.history.replaceState to remove url parameters from history.

If a code parameter is present then a token request is issued.

asyncfunctionhandleAuthenticationResponse(){constparams=newURLSearchParams(location.search.substr(1));if(params.has("code")){window.history.replaceState(null,null,location.pathname);constconfig=awaitgetConfiguration(registration.issuer);consttokenResponse=awaitinvokeTokenRequest(config,registration.client_id,registration.client_secret,params.get("code"));if("id_token"intokenResponse){constjwks=awaitgetJWKS(config);constid_token=awaitdecodeJWT(jwks,tokenResponse.id_token);constsignature_status=(id_token.signature===true) ? "signature verified" : "invalid signature";document.getElementById("signature").innerText=`(${signature_status})`;set_value("id_token",JSON.stringify(id_token.claims,null,2));constnonce_status=(id_token.claims.nonce==localStorage.getItem("/SimpleSPA#nonce")) ? "nonce verified" : "invalid nonce";document.getElementById("nonce").innerText=`(${nonce_status})`;localStorage.removeItem("/SimpleSPA#nonce");}if("access_token"intokenResponse){fetchWithToken=(input,init)=>{varrequest=newRequest(input,init);request.headers.set("Authorization","Bearer "+tokenResponse.access_token);returnwindow.fetch(request);};}else{fetchWithToken=null;}return;}if(params.has("error")){set_value("id_token",`error=${params.get("error")}`);}}

Invoke token request

The following builds and invokes an OAuth authorization code grant token request.

asyncfunctioninvokeTokenRequest(configuration,client_id,client_secret,code){consttoken_endpoint=configuration.token_endpoint;constheaders={"Content-Type": "application/x-www-form-urlencoded"};constbody=newURLSearchParams();body.set("grant_type","authorization_code");body.set("code",code);body.set("client_id",client_id);body.set("client_secret",client_secret);body.set("redirect_uri",location.origin+location.pathname);constcode_verifier=window.localStorage.getItem("/SimpleSPA#code_verifier");if(code_verifier){body.set("code_verifier",code_verifier);}try{constresponse=awaitfetch(token_endpoint,{method: "POST",mode: "cors",headers: headers,body: body.toString()});if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}finally{window.localStorage.removeItem("/SimpleSPA#code_verifier");}}

Get provider keys

The OpenID Provider's public keys are found in a JWKS document found from address specified by jwks_uri metadata property.

asyncfunctiongetJWKS(config){consturi=config.jwks_uri;constresponse=awaitfetch(uri);if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Validate ID Token integrity

ID Token is formatted as JWT, with three base64url encoded segments separated by "." character. The first part contains header, second part contains claims and final part is the signature which covers the first and second part. The WebCrypto API works with Uint8Array types so some type conversion with Uint8Array.from is needed.

asyncfunctiondecodeJWT(jwks,jwt){constjws=jwt.split(".");constheader=JSON.parse(atobUrlSafe(jws[0]));constclaims=JSON.parse(atobUrlSafe(jws[1]));consttext2verify=Uint8Array.from(jws[0]+"."+jws[1],t=>t.charCodeAt(0));constsignature=Uint8Array.from(atobUrlSafe(jws[2]),t=>t.charCodeAt(0));

Each signing key from OpenID Provider's jwks document is converted into WebCrypto Key with window.crypto.subtle.importKey. Then signature verification is attempted with window.crypto.subtle.verify.

Apparently there are some interoperability issues with JWK formatted keys and WebCrypto API which requires some transformation. One would assume algorithm and key identifiers of JWK, JWS and WebCrypto would be compatible but that appear to not be the case. In the example below I have hard coded RS256 algorithm. A real world solution needs to map JWK and JWS identifiers into WebCrypto identifiers.

constkeys=jwks.keys.filter(isSig).map(toJwk);constRS256={name: "RSASSA-PKCS1-v1_5",hash: {name: "SHA-256"},};for(constjwkofkeys){try{constkey=awaitwindow.crypto.subtle.importKey("jwk",jwk,RS256,false,["verify"]);constresult=awaitwindow.crypto.subtle.verify(RS256,key,signature,text2verify);if(result===true){return{"header": header,"claims": claims,"signature": true,"jwk": jwk,};}}catch{// ignore}}returnnegative;}

Invoke OAuth protected API

When invoking an OAuth protected API the access token is put into the Authorization http request header with the Bearer scheme.

fetchWithToken=(input,init)=>{varrequest=newRequest(input,init);request.headers.set("Authorization","Bearer "+tokenResponse.access_token);returnwindow.fetch(request);};
asyncfunctioninvokeApi(){const_fetch=fetchWithToken||window.fetch;constresponse=await_fetch(api_endpoint,{mode: "cors",cache: "no-store"});if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Running the application

This application is ready to run with Ubisecure SSO at login.example.ubidemo.com.

With GitHub Pages

  1. Navigate to https://psteniusubi.github.io/SimpleSPA/spa.html

About

JavaScript Single Page Application and Ubisecure SSO

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Used by

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

87 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

JavaScript Single Page Application and Ubisecure SSO

Example of a JavaScript Single Page Application that uses OpenID Connect 1.0 for logon and then invokes an OAuth 2.0 protected API. Launch this app by navigating to

https://psteniusubi.github.io/SimpleSPA/spa.html

The code for the API invoked by this application is in SimpleAPI

See also my related blog article Ubisecure SSO and Single Page Applications.

Code review

This project is hosted on GitHub Pages here. It also runs as-is on Apache HTTP server or any other web server that serves static resources.

This application is implemented in a single html page spa.html.

Get provider metadata

This method fetches the OpenID Provider metadata configuration information. The issuer parameter is the name of the OpenID Provider.

asyncfunctiongetConfiguration(issuer){consturi=`${issuer}/.well-known/openid-configuration`;constresponse=awaitfetch(uri);if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Send authentication request

This method builds an OpenID Connect authentication request and redirects the web browser to the OpenID Provider.

The code also creates a random nonce and PKCE code verifier. These items are stored in local storage with window.localStorage.setItem.

Generating PKCE code verifier

asyncfunctionnewCodeVerifier(method){switch(method){case"plain":
case"S256":
returnbtoaUrlSafe(Array.from(window.crypto.getRandomValues(newUint8Array(32)),t=>String.fromCharCode(t)).join(""))case"":
casenull:
returnnull;default:
throw"invalid argument";}}

PKCE code challenge

asyncfunctiongetCodeChallenge(method,code_verifier){switch(method){case"plain":
if(code_verifier===null)throw"invalid argument";returncode_verifier;case"S256":
if(code_verifier===null)throw"invalid argument";letbytes=Uint8Array.from(code_verifier,t=>t.charCodeAt(0));bytes=awaitwindow.crypto.subtle.digest("SHA-256",bytes);returnbtoaUrlSafe(Array.from(newUint8Array(bytes),t=>String.fromCharCode(t)).join(""));case"":
casenull:
returnnull;default:
throw"invalid argument";}}

Authentication request

asyncfunctionsendAuthenticationRequest(configuration,client_id,scope){constauthorization_request=newURL(configuration.authorization_endpoint);authorization_request.searchParams.set("response_type","code");authorization_request.searchParams.set("scope",scope);authorization_request.searchParams.set("client_id",client_id);authorization_request.searchParams.set("redirect_uri",location.origin+location.pathname);// nonceconstnonce=Array.from(window.crypto.getRandomValues(newUint32Array(4)),t=>t.toString(36)).join("");authorization_request.searchParams.set("nonce",nonce);window.localStorage.setItem("/SimpleSPA#nonce",nonce);// code_challenge_methodconstcode_challenge_method="S256";authorization_request.searchParams.set("code_challenge_method",code_challenge_method);// code_verifierconstcode_verifier=awaitnewCodeVerifier(code_challenge_method);window.localStorage.setItem("/SimpleSPA#code_verifier",code_verifier);// code_challengeconstcode_challenge=awaitgetCodeChallenge(code_challenge_method,code_verifier);authorization_request.searchParams.set("code_challenge",code_challenge);location.assign(authorization_request);}

Handle authorization response

The OpenID Provider redirects user agent back with authorization response message, containing either code or error parameters.

This code looks for code or error url parameters, then uses window.history.replaceState to remove url parameters from history.

If a code parameter is present then a token request is issued.

asyncfunctionhandleAuthenticationResponse(){constparams=newURLSearchParams(location.search.substr(1));if(params.has("code")){window.history.replaceState(null,null,location.pathname);constconfig=awaitgetConfiguration(registration.issuer);consttokenResponse=awaitinvokeTokenRequest(config,registration.client_id,registration.client_secret,params.get("code"));if("id_token"intokenResponse){constjwks=awaitgetJWKS(config);constid_token=awaitdecodeJWT(jwks,tokenResponse.id_token);constsignature_status=(id_token.signature===true) ? "signature verified" : "invalid signature";document.getElementById("signature").innerText=`(${signature_status})`;set_value("id_token",JSON.stringify(id_token.claims,null,2));constnonce_status=(id_token.claims.nonce==localStorage.getItem("/SimpleSPA#nonce")) ? "nonce verified" : "invalid nonce";document.getElementById("nonce").innerText=`(${nonce_status})`;localStorage.removeItem("/SimpleSPA#nonce");}if("access_token"intokenResponse){fetchWithToken=(input,init)=>{varrequest=newRequest(input,init);request.headers.set("Authorization","Bearer "+tokenResponse.access_token);returnwindow.fetch(request);};}else{fetchWithToken=null;}return;}if(params.has("error")){set_value("id_token",`error=${params.get("error")}`);}}

Invoke token request

The following builds and invokes an OAuth authorization code grant token request.

asyncfunctioninvokeTokenRequest(configuration,client_id,client_secret,code){consttoken_endpoint=configuration.token_endpoint;constheaders={"Content-Type": "application/x-www-form-urlencoded"};constbody=newURLSearchParams();body.set("grant_type","authorization_code");body.set("code",code);body.set("client_id",client_id);body.set("client_secret",client_secret);body.set("redirect_uri",location.origin+location.pathname);constcode_verifier=window.localStorage.getItem("/SimpleSPA#code_verifier");if(code_verifier){body.set("code_verifier",code_verifier);}try{constresponse=awaitfetch(token_endpoint,{method: "POST",mode: "cors",headers: headers,body: body.toString()});if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}finally{window.localStorage.removeItem("/SimpleSPA#code_verifier");}}

Get provider keys

The OpenID Provider's public keys are found in a JWKS document found from address specified by jwks_uri metadata property.

asyncfunctiongetJWKS(config){consturi=config.jwks_uri;constresponse=awaitfetch(uri);if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Validate ID Token integrity

ID Token is formatted as JWT, with three base64url encoded segments separated by "." character. The first part contains header, second part contains claims and final part is the signature which covers the first and second part. The WebCrypto API works with Uint8Array types so some type conversion with Uint8Array.from is needed.

asyncfunctiondecodeJWT(jwks,jwt){constjws=jwt.split(".");constheader=JSON.parse(atobUrlSafe(jws[0]));constclaims=JSON.parse(atobUrlSafe(jws[1]));consttext2verify=Uint8Array.from(jws[0]+"."+jws[1],t=>t.charCodeAt(0));constsignature=Uint8Array.from(atobUrlSafe(jws[2]),t=>t.charCodeAt(0));

Each signing key from OpenID Provider's jwks document is converted into WebCrypto Key with window.crypto.subtle.importKey. Then signature verification is attempted with window.crypto.subtle.verify.

Apparently there are some interoperability issues with JWK formatted keys and WebCrypto API which requires some transformation. One would assume algorithm and key identifiers of JWK, JWS and WebCrypto would be compatible but that appear to not be the case. In the example below I have hard coded RS256 algorithm. A real world solution needs to map JWK and JWS identifiers into WebCrypto identifiers.

constkeys=jwks.keys.filter(isSig).map(toJwk);constRS256={name: "RSASSA-PKCS1-v1_5",hash: {name: "SHA-256"},};for(constjwkofkeys){try{constkey=awaitwindow.crypto.subtle.importKey("jwk",jwk,RS256,false,["verify"]);constresult=awaitwindow.crypto.subtle.verify(RS256,key,signature,text2verify);if(result===true){return{"header": header,"claims": claims,"signature": true,"jwk": jwk,};}}catch{// ignore}}returnnegative;}

Invoke OAuth protected API

When invoking an OAuth protected API the access token is put into the Authorization http request header with the Bearer scheme.

fetchWithToken=(input,init)=>{varrequest=newRequest(input,init);request.headers.set("Authorization","Bearer "+tokenResponse.access_token);returnwindow.fetch(request);};
asyncfunctioninvokeApi(){const_fetch=fetchWithToken||window.fetch;constresponse=await_fetch(api_endpoint,{mode: "cors",cache: "no-store"});if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Running the application

This application is ready to run with Ubisecure SSO at login.example.ubidemo.com.

With GitHub Pages

  1. Navigate to https://psteniusubi.github.io/SimpleSPA/spa.html

About

JavaScript Single Page Application and Ubisecure SSO

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Used by

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

87 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

JavaScript Single Page Application and Ubisecure SSO

Example of a JavaScript Single Page Application that uses OpenID Connect 1.0 for logon and then invokes an OAuth 2.0 protected API. Launch this app by navigating to

https://psteniusubi.github.io/SimpleSPA/spa.html

The code for the API invoked by this application is in SimpleAPI

See also my related blog article Ubisecure SSO and Single Page Applications.

Code review

This project is hosted on GitHub Pages here. It also runs as-is on Apache HTTP server or any other web server that serves static resources.

This application is implemented in a single html page spa.html.

Get provider metadata

This method fetches the OpenID Provider metadata configuration information. The issuer parameter is the name of the OpenID Provider.

asyncfunctiongetConfiguration(issuer){consturi=`${issuer}/.well-known/openid-configuration`;constresponse=awaitfetch(uri);if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Send authentication request

This method builds an OpenID Connect authentication request and redirects the web browser to the OpenID Provider.

The code also creates a random nonce and PKCE code verifier. These items are stored in local storage with window.localStorage.setItem.

Generating PKCE code verifier

asyncfunctionnewCodeVerifier(method){switch(method){case"plain":
case"S256":
returnbtoaUrlSafe(Array.from(window.crypto.getRandomValues(newUint8Array(32)),t=>String.fromCharCode(t)).join(""))case"":
casenull:
returnnull;default:
throw"invalid argument";}}

PKCE code challenge

asyncfunctiongetCodeChallenge(method,code_verifier){switch(method){case"plain":
if(code_verifier===null)throw"invalid argument";returncode_verifier;case"S256":
if(code_verifier===null)throw"invalid argument";letbytes=Uint8Array.from(code_verifier,t=>t.charCodeAt(0));bytes=awaitwindow.crypto.subtle.digest("SHA-256",bytes);returnbtoaUrlSafe(Array.from(newUint8Array(bytes),t=>String.fromCharCode(t)).join(""));case"":
casenull:
returnnull;default:
throw"invalid argument";}}

Authentication request

asyncfunctionsendAuthenticationRequest(configuration,client_id,scope){constauthorization_request=newURL(configuration.authorization_endpoint);authorization_request.searchParams.set("response_type","code");authorization_request.searchParams.set("scope",scope);authorization_request.searchParams.set("client_id",client_id);authorization_request.searchParams.set("redirect_uri",location.origin+location.pathname);// nonceconstnonce=Array.from(window.crypto.getRandomValues(newUint32Array(4)),t=>t.toString(36)).join("");authorization_request.searchParams.set("nonce",nonce);window.localStorage.setItem("/SimpleSPA#nonce",nonce);// code_challenge_methodconstcode_challenge_method="S256";authorization_request.searchParams.set("code_challenge_method",code_challenge_method);// code_verifierconstcode_verifier=awaitnewCodeVerifier(code_challenge_method);window.localStorage.setItem("/SimpleSPA#code_verifier",code_verifier);// code_challengeconstcode_challenge=awaitgetCodeChallenge(code_challenge_method,code_verifier);authorization_request.searchParams.set("code_challenge",code_challenge);location.assign(authorization_request);}

Handle authorization response

The OpenID Provider redirects user agent back with authorization response message, containing either code or error parameters.

This code looks for code or error url parameters, then uses window.history.replaceState to remove url parameters from history.

If a code parameter is present then a token request is issued.

asyncfunctionhandleAuthenticationResponse(){constparams=newURLSearchParams(location.search.substr(1));if(params.has("code")){window.history.replaceState(null,null,location.pathname);constconfig=awaitgetConfiguration(registration.issuer);consttokenResponse=awaitinvokeTokenRequest(config,registration.client_id,registration.client_secret,params.get("code"));if("id_token"intokenResponse){constjwks=awaitgetJWKS(config);constid_token=awaitdecodeJWT(jwks,tokenResponse.id_token);constsignature_status=(id_token.signature===true) ? "signature verified" : "invalid signature";document.getElementById("signature").innerText=`(${signature_status})`;set_value("id_token",JSON.stringify(id_token.claims,null,2));constnonce_status=(id_token.claims.nonce==localStorage.getItem("/SimpleSPA#nonce")) ? "nonce verified" : "invalid nonce";document.getElementById("nonce").innerText=`(${nonce_status})`;localStorage.removeItem("/SimpleSPA#nonce");}if("access_token"intokenResponse){fetchWithToken=(input,init)=>{varrequest=newRequest(input,init);request.headers.set("Authorization","Bearer "+tokenResponse.access_token);returnwindow.fetch(request);};}else{fetchWithToken=null;}return;}if(params.has("error")){set_value("id_token",`error=${params.get("error")}`);}}

Invoke token request

The following builds and invokes an OAuth authorization code grant token request.

asyncfunctioninvokeTokenRequest(configuration,client_id,client_secret,code){consttoken_endpoint=configuration.token_endpoint;constheaders={"Content-Type": "application/x-www-form-urlencoded"};constbody=newURLSearchParams();body.set("grant_type","authorization_code");body.set("code",code);body.set("client_id",client_id);body.set("client_secret",client_secret);body.set("redirect_uri",location.origin+location.pathname);constcode_verifier=window.localStorage.getItem("/SimpleSPA#code_verifier");if(code_verifier){body.set("code_verifier",code_verifier);}try{constresponse=awaitfetch(token_endpoint,{method: "POST",mode: "cors",headers: headers,body: body.toString()});if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}finally{window.localStorage.removeItem("/SimpleSPA#code_verifier");}}

Get provider keys

The OpenID Provider's public keys are found in a JWKS document found from address specified by jwks_uri metadata property.

asyncfunctiongetJWKS(config){consturi=config.jwks_uri;constresponse=awaitfetch(uri);if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Validate ID Token integrity

ID Token is formatted as JWT, with three base64url encoded segments separated by "." character. The first part contains header, second part contains claims and final part is the signature which covers the first and second part. The WebCrypto API works with Uint8Array types so some type conversion with Uint8Array.from is needed.

asyncfunctiondecodeJWT(jwks,jwt){constjws=jwt.split(".");constheader=JSON.parse(atobUrlSafe(jws[0]));constclaims=JSON.parse(atobUrlSafe(jws[1]));consttext2verify=Uint8Array.from(jws[0]+"."+jws[1],t=>t.charCodeAt(0));constsignature=Uint8Array.from(atobUrlSafe(jws[2]),t=>t.charCodeAt(0));

Each signing key from OpenID Provider's jwks document is converted into WebCrypto Key with window.crypto.subtle.importKey. Then signature verification is attempted with window.crypto.subtle.verify.

Apparently there are some interoperability issues with JWK formatted keys and WebCrypto API which requires some transformation. One would assume algorithm and key identifiers of JWK, JWS and WebCrypto would be compatible but that appear to not be the case. In the example below I have hard coded RS256 algorithm. A real world solution needs to map JWK and JWS identifiers into WebCrypto identifiers.

constkeys=jwks.keys.filter(isSig).map(toJwk);constRS256={name: "RSASSA-PKCS1-v1_5",hash: {name: "SHA-256"},};for(constjwkofkeys){try{constkey=awaitwindow.crypto.subtle.importKey("jwk",jwk,RS256,false,["verify"]);constresult=awaitwindow.crypto.subtle.verify(RS256,key,signature,text2verify);if(result===true){return{"header": header,"claims": claims,"signature": true,"jwk": jwk,};}}catch{// ignore}}returnnegative;}

Invoke OAuth protected API

When invoking an OAuth protected API the access token is put into the Authorization http request header with the Bearer scheme.

fetchWithToken=(input,init)=>{varrequest=newRequest(input,init);request.headers.set("Authorization","Bearer "+tokenResponse.access_token);returnwindow.fetch(request);};
asyncfunctioninvokeApi(){const_fetch=fetchWithToken||window.fetch;constresponse=await_fetch(api_endpoint,{mode: "cors",cache: "no-store"});if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Running the application

This application is ready to run with Ubisecure SSO at login.example.ubidemo.com.

With GitHub Pages

  1. Navigate to https://psteniusubi.github.io/SimpleSPA/spa.html

About

JavaScript Single Page Application and Ubisecure SSO

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Used by

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

87 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

JavaScript Single Page Application and Ubisecure SSO

Example of a JavaScript Single Page Application that uses OpenID Connect 1.0 for logon and then invokes an OAuth 2.0 protected API. Launch this app by navigating to

https://psteniusubi.github.io/SimpleSPA/spa.html

The code for the API invoked by this application is in SimpleAPI

See also my related blog article Ubisecure SSO and Single Page Applications.

Code review

This project is hosted on GitHub Pages here. It also runs as-is on Apache HTTP server or any other web server that serves static resources.

This application is implemented in a single html page spa.html.

Get provider metadata

This method fetches the OpenID Provider metadata configuration information. The issuer parameter is the name of the OpenID Provider.

asyncfunctiongetConfiguration(issuer){consturi=`${issuer}/.well-known/openid-configuration`;constresponse=awaitfetch(uri);if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Send authentication request

This method builds an OpenID Connect authentication request and redirects the web browser to the OpenID Provider.

The code also creates a random nonce and PKCE code verifier. These items are stored in local storage with window.localStorage.setItem.

Generating PKCE code verifier

asyncfunctionnewCodeVerifier(method){switch(method){case"plain":
case"S256":
returnbtoaUrlSafe(Array.from(window.crypto.getRandomValues(newUint8Array(32)),t=>String.fromCharCode(t)).join(""))case"":
casenull:
returnnull;default:
throw"invalid argument";}}

PKCE code challenge

asyncfunctiongetCodeChallenge(method,code_verifier){switch(method){case"plain":
if(code_verifier===null)throw"invalid argument";returncode_verifier;case"S256":
if(code_verifier===null)throw"invalid argument";letbytes=Uint8Array.from(code_verifier,t=>t.charCodeAt(0));bytes=awaitwindow.crypto.subtle.digest("SHA-256",bytes);returnbtoaUrlSafe(Array.from(newUint8Array(bytes),t=>String.fromCharCode(t)).join(""));case"":
casenull:
returnnull;default:
throw"invalid argument";}}

Authentication request

asyncfunctionsendAuthenticationRequest(configuration,client_id,scope){constauthorization_request=newURL(configuration.authorization_endpoint);authorization_request.searchParams.set("response_type","code");authorization_request.searchParams.set("scope",scope);authorization_request.searchParams.set("client_id",client_id);authorization_request.searchParams.set("redirect_uri",location.origin+location.pathname);// nonceconstnonce=Array.from(window.crypto.getRandomValues(newUint32Array(4)),t=>t.toString(36)).join("");authorization_request.searchParams.set("nonce",nonce);window.localStorage.setItem("/SimpleSPA#nonce",nonce);// code_challenge_methodconstcode_challenge_method="S256";authorization_request.searchParams.set("code_challenge_method",code_challenge_method);// code_verifierconstcode_verifier=awaitnewCodeVerifier(code_challenge_method);window.localStorage.setItem("/SimpleSPA#code_verifier",code_verifier);// code_challengeconstcode_challenge=awaitgetCodeChallenge(code_challenge_method,code_verifier);authorization_request.searchParams.set("code_challenge",code_challenge);location.assign(authorization_request);}

Handle authorization response

The OpenID Provider redirects user agent back with authorization response message, containing either code or error parameters.

This code looks for code or error url parameters, then uses window.history.replaceState to remove url parameters from history.

If a code parameter is present then a token request is issued.

asyncfunctionhandleAuthenticationResponse(){constparams=newURLSearchParams(location.search.substr(1));if(params.has("code")){window.history.replaceState(null,null,location.pathname);constconfig=awaitgetConfiguration(registration.issuer);consttokenResponse=awaitinvokeTokenRequest(config,registration.client_id,registration.client_secret,params.get("code"));if("id_token"intokenResponse){constjwks=awaitgetJWKS(config);constid_token=awaitdecodeJWT(jwks,tokenResponse.id_token);constsignature_status=(id_token.signature===true) ? "signature verified" : "invalid signature";document.getElementById("signature").innerText=`(${signature_status})`;set_value("id_token",JSON.stringify(id_token.claims,null,2));constnonce_status=(id_token.claims.nonce==localStorage.getItem("/SimpleSPA#nonce")) ? "nonce verified" : "invalid nonce";document.getElementById("nonce").innerText=`(${nonce_status})`;localStorage.removeItem("/SimpleSPA#nonce");}if("access_token"intokenResponse){fetchWithToken=(input,init)=>{varrequest=newRequest(input,init);request.headers.set("Authorization","Bearer "+tokenResponse.access_token);returnwindow.fetch(request);};}else{fetchWithToken=null;}return;}if(params.has("error")){set_value("id_token",`error=${params.get("error")}`);}}

Invoke token request

The following builds and invokes an OAuth authorization code grant token request.

asyncfunctioninvokeTokenRequest(configuration,client_id,client_secret,code){consttoken_endpoint=configuration.token_endpoint;constheaders={"Content-Type": "application/x-www-form-urlencoded"};constbody=newURLSearchParams();body.set("grant_type","authorization_code");body.set("code",code);body.set("client_id",client_id);body.set("client_secret",client_secret);body.set("redirect_uri",location.origin+location.pathname);constcode_verifier=window.localStorage.getItem("/SimpleSPA#code_verifier");if(code_verifier){body.set("code_verifier",code_verifier);}try{constresponse=awaitfetch(token_endpoint,{method: "POST",mode: "cors",headers: headers,body: body.toString()});if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}finally{window.localStorage.removeItem("/SimpleSPA#code_verifier");}}

Get provider keys

The OpenID Provider's public keys are found in a JWKS document found from address specified by jwks_uri metadata property.

asyncfunctiongetJWKS(config){consturi=config.jwks_uri;constresponse=awaitfetch(uri);if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Validate ID Token integrity

ID Token is formatted as JWT, with three base64url encoded segments separated by "." character. The first part contains header, second part contains claims and final part is the signature which covers the first and second part. The WebCrypto API works with Uint8Array types so some type conversion with Uint8Array.from is needed.

asyncfunctiondecodeJWT(jwks,jwt){constjws=jwt.split(".");constheader=JSON.parse(atobUrlSafe(jws[0]));constclaims=JSON.parse(atobUrlSafe(jws[1]));consttext2verify=Uint8Array.from(jws[0]+"."+jws[1],t=>t.charCodeAt(0));constsignature=Uint8Array.from(atobUrlSafe(jws[2]),t=>t.charCodeAt(0));

Each signing key from OpenID Provider's jwks document is converted into WebCrypto Key with window.crypto.subtle.importKey. Then signature verification is attempted with window.crypto.subtle.verify.

Apparently there are some interoperability issues with JWK formatted keys and WebCrypto API which requires some transformation. One would assume algorithm and key identifiers of JWK, JWS and WebCrypto would be compatible but that appear to not be the case. In the example below I have hard coded RS256 algorithm. A real world solution needs to map JWK and JWS identifiers into WebCrypto identifiers.

constkeys=jwks.keys.filter(isSig).map(toJwk);constRS256={name: "RSASSA-PKCS1-v1_5",hash: {name: "SHA-256"},};for(constjwkofkeys){try{constkey=awaitwindow.crypto.subtle.importKey("jwk",jwk,RS256,false,["verify"]);constresult=awaitwindow.crypto.subtle.verify(RS256,key,signature,text2verify);if(result===true){return{"header": header,"claims": claims,"signature": true,"jwk": jwk,};}}catch{// ignore}}returnnegative;}

Invoke OAuth protected API

When invoking an OAuth protected API the access token is put into the Authorization http request header with the Bearer scheme.

fetchWithToken=(input,init)=>{varrequest=newRequest(input,init);request.headers.set("Authorization","Bearer "+tokenResponse.access_token);returnwindow.fetch(request);};
asyncfunctioninvokeApi(){const_fetch=fetchWithToken||window.fetch;constresponse=await_fetch(api_endpoint,{mode: "cors",cache: "no-store"});if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Running the application

This application is ready to run with Ubisecure SSO at login.example.ubidemo.com.

With GitHub Pages

  1. Navigate to https://psteniusubi.github.io/SimpleSPA/spa.html

About

JavaScript Single Page Application and Ubisecure SSO

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Used by

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

87 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

JavaScript Single Page Application and Ubisecure SSO

Example of a JavaScript Single Page Application that uses OpenID Connect 1.0 for logon and then invokes an OAuth 2.0 protected API. Launch this app by navigating to

https://psteniusubi.github.io/SimpleSPA/spa.html

The code for the API invoked by this application is in SimpleAPI

See also my related blog article Ubisecure SSO and Single Page Applications.

Code review

This project is hosted on GitHub Pages here. It also runs as-is on Apache HTTP server or any other web server that serves static resources.

This application is implemented in a single html page spa.html.

Get provider metadata

This method fetches the OpenID Provider metadata configuration information. The issuer parameter is the name of the OpenID Provider.

asyncfunctiongetConfiguration(issuer){consturi=`${issuer}/.well-known/openid-configuration`;constresponse=awaitfetch(uri);if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Send authentication request

This method builds an OpenID Connect authentication request and redirects the web browser to the OpenID Provider.

The code also creates a random nonce and PKCE code verifier. These items are stored in local storage with window.localStorage.setItem.

Generating PKCE code verifier

asyncfunctionnewCodeVerifier(method){switch(method){case"plain":
case"S256":
returnbtoaUrlSafe(Array.from(window.crypto.getRandomValues(newUint8Array(32)),t=>String.fromCharCode(t)).join(""))case"":
casenull:
returnnull;default:
throw"invalid argument";}}

PKCE code challenge

asyncfunctiongetCodeChallenge(method,code_verifier){switch(method){case"plain":
if(code_verifier===null)throw"invalid argument";returncode_verifier;case"S256":
if(code_verifier===null)throw"invalid argument";letbytes=Uint8Array.from(code_verifier,t=>t.charCodeAt(0));bytes=awaitwindow.crypto.subtle.digest("SHA-256",bytes);returnbtoaUrlSafe(Array.from(newUint8Array(bytes),t=>String.fromCharCode(t)).join(""));case"":
casenull:
returnnull;default:
throw"invalid argument";}}

Authentication request

asyncfunctionsendAuthenticationRequest(configuration,client_id,scope){constauthorization_request=newURL(configuration.authorization_endpoint);authorization_request.searchParams.set("response_type","code");authorization_request.searchParams.set("scope",scope);authorization_request.searchParams.set("client_id",client_id);authorization_request.searchParams.set("redirect_uri",location.origin+location.pathname);// nonceconstnonce=Array.from(window.crypto.getRandomValues(newUint32Array(4)),t=>t.toString(36)).join("");authorization_request.searchParams.set("nonce",nonce);window.localStorage.setItem("/SimpleSPA#nonce",nonce);// code_challenge_methodconstcode_challenge_method="S256";authorization_request.searchParams.set("code_challenge_method",code_challenge_method);// code_verifierconstcode_verifier=awaitnewCodeVerifier(code_challenge_method);window.localStorage.setItem("/SimpleSPA#code_verifier",code_verifier);// code_challengeconstcode_challenge=awaitgetCodeChallenge(code_challenge_method,code_verifier);authorization_request.searchParams.set("code_challenge",code_challenge);location.assign(authorization_request);}

Handle authorization response

The OpenID Provider redirects user agent back with authorization response message, containing either code or error parameters.

This code looks for code or error url parameters, then uses window.history.replaceState to remove url parameters from history.

If a code parameter is present then a token request is issued.

asyncfunctionhandleAuthenticationResponse(){constparams=newURLSearchParams(location.search.substr(1));if(params.has("code")){window.history.replaceState(null,null,location.pathname);constconfig=awaitgetConfiguration(registration.issuer);consttokenResponse=awaitinvokeTokenRequest(config,registration.client_id,registration.client_secret,params.get("code"));if("id_token"intokenResponse){constjwks=awaitgetJWKS(config);constid_token=awaitdecodeJWT(jwks,tokenResponse.id_token);constsignature_status=(id_token.signature===true) ? "signature verified" : "invalid signature";document.getElementById("signature").innerText=`(${signature_status})`;set_value("id_token",JSON.stringify(id_token.claims,null,2));constnonce_status=(id_token.claims.nonce==localStorage.getItem("/SimpleSPA#nonce")) ? "nonce verified" : "invalid nonce";document.getElementById("nonce").innerText=`(${nonce_status})`;localStorage.removeItem("/SimpleSPA#nonce");}if("access_token"intokenResponse){fetchWithToken=(input,init)=>{varrequest=newRequest(input,init);request.headers.set("Authorization","Bearer "+tokenResponse.access_token);returnwindow.fetch(request);};}else{fetchWithToken=null;}return;}if(params.has("error")){set_value("id_token",`error=${params.get("error")}`);}}

Invoke token request

The following builds and invokes an OAuth authorization code grant token request.

asyncfunctioninvokeTokenRequest(configuration,client_id,client_secret,code){consttoken_endpoint=configuration.token_endpoint;constheaders={"Content-Type": "application/x-www-form-urlencoded"};constbody=newURLSearchParams();body.set("grant_type","authorization_code");body.set("code",code);body.set("client_id",client_id);body.set("client_secret",client_secret);body.set("redirect_uri",location.origin+location.pathname);constcode_verifier=window.localStorage.getItem("/SimpleSPA#code_verifier");if(code_verifier){body.set("code_verifier",code_verifier);}try{constresponse=awaitfetch(token_endpoint,{method: "POST",mode: "cors",headers: headers,body: body.toString()});if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}finally{window.localStorage.removeItem("/SimpleSPA#code_verifier");}}

Get provider keys

The OpenID Provider's public keys are found in a JWKS document found from address specified by jwks_uri metadata property.

asyncfunctiongetJWKS(config){consturi=config.jwks_uri;constresponse=awaitfetch(uri);if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Validate ID Token integrity

ID Token is formatted as JWT, with three base64url encoded segments separated by "." character. The first part contains header, second part contains claims and final part is the signature which covers the first and second part. The WebCrypto API works with Uint8Array types so some type conversion with Uint8Array.from is needed.

asyncfunctiondecodeJWT(jwks,jwt){constjws=jwt.split(".");constheader=JSON.parse(atobUrlSafe(jws[0]));constclaims=JSON.parse(atobUrlSafe(jws[1]));consttext2verify=Uint8Array.from(jws[0]+"."+jws[1],t=>t.charCodeAt(0));constsignature=Uint8Array.from(atobUrlSafe(jws[2]),t=>t.charCodeAt(0));

Each signing key from OpenID Provider's jwks document is converted into WebCrypto Key with window.crypto.subtle.importKey. Then signature verification is attempted with window.crypto.subtle.verify.

Apparently there are some interoperability issues with JWK formatted keys and WebCrypto API which requires some transformation. One would assume algorithm and key identifiers of JWK, JWS and WebCrypto would be compatible but that appear to not be the case. In the example below I have hard coded RS256 algorithm. A real world solution needs to map JWK and JWS identifiers into WebCrypto identifiers.

constkeys=jwks.keys.filter(isSig).map(toJwk);constRS256={name: "RSASSA-PKCS1-v1_5",hash: {name: "SHA-256"},};for(constjwkofkeys){try{constkey=awaitwindow.crypto.subtle.importKey("jwk",jwk,RS256,false,["verify"]);constresult=awaitwindow.crypto.subtle.verify(RS256,key,signature,text2verify);if(result===true){return{"header": header,"claims": claims,"signature": true,"jwk": jwk,};}}catch{// ignore}}returnnegative;}

Invoke OAuth protected API

When invoking an OAuth protected API the access token is put into the Authorization http request header with the Bearer scheme.

fetchWithToken=(input,init)=>{varrequest=newRequest(input,init);request.headers.set("Authorization","Bearer "+tokenResponse.access_token);returnwindow.fetch(request);};
asyncfunctioninvokeApi(){const_fetch=fetchWithToken||window.fetch;constresponse=await_fetch(api_endpoint,{mode: "cors",cache: "no-store"});if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Running the application

This application is ready to run with Ubisecure SSO at login.example.ubidemo.com.

With GitHub Pages

  1. Navigate to https://psteniusubi.github.io/SimpleSPA/spa.html

About

JavaScript Single Page Application and Ubisecure SSO

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Used by

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

87 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

JavaScript Single Page Application and Ubisecure SSO

Example of a JavaScript Single Page Application that uses OpenID Connect 1.0 for logon and then invokes an OAuth 2.0 protected API. Launch this app by navigating to

https://psteniusubi.github.io/SimpleSPA/spa.html

The code for the API invoked by this application is in SimpleAPI

See also my related blog article Ubisecure SSO and Single Page Applications.

Code review

This project is hosted on GitHub Pages here. It also runs as-is on Apache HTTP server or any other web server that serves static resources.

This application is implemented in a single html page spa.html.

Get provider metadata

This method fetches the OpenID Provider metadata configuration information. The issuer parameter is the name of the OpenID Provider.

asyncfunctiongetConfiguration(issuer){consturi=`${issuer}/.well-known/openid-configuration`;constresponse=awaitfetch(uri);if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Send authentication request

This method builds an OpenID Connect authentication request and redirects the web browser to the OpenID Provider.

The code also creates a random nonce and PKCE code verifier. These items are stored in local storage with window.localStorage.setItem.

Generating PKCE code verifier

asyncfunctionnewCodeVerifier(method){switch(method){case"plain":
case"S256":
returnbtoaUrlSafe(Array.from(window.crypto.getRandomValues(newUint8Array(32)),t=>String.fromCharCode(t)).join(""))case"":
casenull:
returnnull;default:
throw"invalid argument";}}

PKCE code challenge

asyncfunctiongetCodeChallenge(method,code_verifier){switch(method){case"plain":
if(code_verifier===null)throw"invalid argument";returncode_verifier;case"S256":
if(code_verifier===null)throw"invalid argument";letbytes=Uint8Array.from(code_verifier,t=>t.charCodeAt(0));bytes=awaitwindow.crypto.subtle.digest("SHA-256",bytes);returnbtoaUrlSafe(Array.from(newUint8Array(bytes),t=>String.fromCharCode(t)).join(""));case"":
casenull:
returnnull;default:
throw"invalid argument";}}

Authentication request

asyncfunctionsendAuthenticationRequest(configuration,client_id,scope){constauthorization_request=newURL(configuration.authorization_endpoint);authorization_request.searchParams.set("response_type","code");authorization_request.searchParams.set("scope",scope);authorization_request.searchParams.set("client_id",client_id);authorization_request.searchParams.set("redirect_uri",location.origin+location.pathname);// nonceconstnonce=Array.from(window.crypto.getRandomValues(newUint32Array(4)),t=>t.toString(36)).join("");authorization_request.searchParams.set("nonce",nonce);window.localStorage.setItem("/SimpleSPA#nonce",nonce);// code_challenge_methodconstcode_challenge_method="S256";authorization_request.searchParams.set("code_challenge_method",code_challenge_method);// code_verifierconstcode_verifier=awaitnewCodeVerifier(code_challenge_method);window.localStorage.setItem("/SimpleSPA#code_verifier",code_verifier);// code_challengeconstcode_challenge=awaitgetCodeChallenge(code_challenge_method,code_verifier);authorization_request.searchParams.set("code_challenge",code_challenge);location.assign(authorization_request);}

Handle authorization response

The OpenID Provider redirects user agent back with authorization response message, containing either code or error parameters.

This code looks for code or error url parameters, then uses window.history.replaceState to remove url parameters from history.

If a code parameter is present then a token request is issued.

asyncfunctionhandleAuthenticationResponse(){constparams=newURLSearchParams(location.search.substr(1));if(params.has("code")){window.history.replaceState(null,null,location.pathname);constconfig=awaitgetConfiguration(registration.issuer);consttokenResponse=awaitinvokeTokenRequest(config,registration.client_id,registration.client_secret,params.get("code"));if("id_token"intokenResponse){constjwks=awaitgetJWKS(config);constid_token=awaitdecodeJWT(jwks,tokenResponse.id_token);constsignature_status=(id_token.signature===true) ? "signature verified" : "invalid signature";document.getElementById("signature").innerText=`(${signature_status})`;set_value("id_token",JSON.stringify(id_token.claims,null,2));constnonce_status=(id_token.claims.nonce==localStorage.getItem("/SimpleSPA#nonce")) ? "nonce verified" : "invalid nonce";document.getElementById("nonce").innerText=`(${nonce_status})`;localStorage.removeItem("/SimpleSPA#nonce");}if("access_token"intokenResponse){fetchWithToken=(input,init)=>{varrequest=newRequest(input,init);request.headers.set("Authorization","Bearer "+tokenResponse.access_token);returnwindow.fetch(request);};}else{fetchWithToken=null;}return;}if(params.has("error")){set_value("id_token",`error=${params.get("error")}`);}}

Invoke token request

The following builds and invokes an OAuth authorization code grant token request.

asyncfunctioninvokeTokenRequest(configuration,client_id,client_secret,code){consttoken_endpoint=configuration.token_endpoint;constheaders={"Content-Type": "application/x-www-form-urlencoded"};constbody=newURLSearchParams();body.set("grant_type","authorization_code");body.set("code",code);body.set("client_id",client_id);body.set("client_secret",client_secret);body.set("redirect_uri",location.origin+location.pathname);constcode_verifier=window.localStorage.getItem("/SimpleSPA#code_verifier");if(code_verifier){body.set("code_verifier",code_verifier);}try{constresponse=awaitfetch(token_endpoint,{method: "POST",mode: "cors",headers: headers,body: body.toString()});if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}finally{window.localStorage.removeItem("/SimpleSPA#code_verifier");}}

Get provider keys

The OpenID Provider's public keys are found in a JWKS document found from address specified by jwks_uri metadata property.

asyncfunctiongetJWKS(config){consturi=config.jwks_uri;constresponse=awaitfetch(uri);if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Validate ID Token integrity

ID Token is formatted as JWT, with three base64url encoded segments separated by "." character. The first part contains header, second part contains claims and final part is the signature which covers the first and second part. The WebCrypto API works with Uint8Array types so some type conversion with Uint8Array.from is needed.

asyncfunctiondecodeJWT(jwks,jwt){constjws=jwt.split(".");constheader=JSON.parse(atobUrlSafe(jws[0]));constclaims=JSON.parse(atobUrlSafe(jws[1]));consttext2verify=Uint8Array.from(jws[0]+"."+jws[1],t=>t.charCodeAt(0));constsignature=Uint8Array.from(atobUrlSafe(jws[2]),t=>t.charCodeAt(0));

Each signing key from OpenID Provider's jwks document is converted into WebCrypto Key with window.crypto.subtle.importKey. Then signature verification is attempted with window.crypto.subtle.verify.

Apparently there are some interoperability issues with JWK formatted keys and WebCrypto API which requires some transformation. One would assume algorithm and key identifiers of JWK, JWS and WebCrypto would be compatible but that appear to not be the case. In the example below I have hard coded RS256 algorithm. A real world solution needs to map JWK and JWS identifiers into WebCrypto identifiers.

constkeys=jwks.keys.filter(isSig).map(toJwk);constRS256={name: "RSASSA-PKCS1-v1_5",hash: {name: "SHA-256"},};for(constjwkofkeys){try{constkey=awaitwindow.crypto.subtle.importKey("jwk",jwk,RS256,false,["verify"]);constresult=awaitwindow.crypto.subtle.verify(RS256,key,signature,text2verify);if(result===true){return{"header": header,"claims": claims,"signature": true,"jwk": jwk,};}}catch{// ignore}}returnnegative;}

Invoke OAuth protected API

When invoking an OAuth protected API the access token is put into the Authorization http request header with the Bearer scheme.

fetchWithToken=(input,init)=>{varrequest=newRequest(input,init);request.headers.set("Authorization","Bearer "+tokenResponse.access_token);returnwindow.fetch(request);};
asyncfunctioninvokeApi(){const_fetch=fetchWithToken||window.fetch;constresponse=await_fetch(api_endpoint,{mode: "cors",cache: "no-store"});if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Running the application

This application is ready to run with Ubisecure SSO at login.example.ubidemo.com.

With GitHub Pages

  1. Navigate to https://psteniusubi.github.io/SimpleSPA/spa.html

About

JavaScript Single Page Application and Ubisecure SSO

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Used by

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

87 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

JavaScript Single Page Application and Ubisecure SSO

Example of a JavaScript Single Page Application that uses OpenID Connect 1.0 for logon and then invokes an OAuth 2.0 protected API. Launch this app by navigating to

https://psteniusubi.github.io/SimpleSPA/spa.html

The code for the API invoked by this application is in SimpleAPI

See also my related blog article Ubisecure SSO and Single Page Applications.

Code review

This project is hosted on GitHub Pages here. It also runs as-is on Apache HTTP server or any other web server that serves static resources.

This application is implemented in a single html page spa.html.

Get provider metadata

This method fetches the OpenID Provider metadata configuration information. The issuer parameter is the name of the OpenID Provider.

asyncfunctiongetConfiguration(issuer){consturi=`${issuer}/.well-known/openid-configuration`;constresponse=awaitfetch(uri);if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Send authentication request

This method builds an OpenID Connect authentication request and redirects the web browser to the OpenID Provider.

The code also creates a random nonce and PKCE code verifier. These items are stored in local storage with window.localStorage.setItem.

Generating PKCE code verifier

asyncfunctionnewCodeVerifier(method){switch(method){case"plain":
case"S256":
returnbtoaUrlSafe(Array.from(window.crypto.getRandomValues(newUint8Array(32)),t=>String.fromCharCode(t)).join(""))case"":
casenull:
returnnull;default:
throw"invalid argument";}}

PKCE code challenge

asyncfunctiongetCodeChallenge(method,code_verifier){switch(method){case"plain":
if(code_verifier===null)throw"invalid argument";returncode_verifier;case"S256":
if(code_verifier===null)throw"invalid argument";letbytes=Uint8Array.from(code_verifier,t=>t.charCodeAt(0));bytes=awaitwindow.crypto.subtle.digest("SHA-256",bytes);returnbtoaUrlSafe(Array.from(newUint8Array(bytes),t=>String.fromCharCode(t)).join(""));case"":
casenull:
returnnull;default:
throw"invalid argument";}}

Authentication request

asyncfunctionsendAuthenticationRequest(configuration,client_id,scope){constauthorization_request=newURL(configuration.authorization_endpoint);authorization_request.searchParams.set("response_type","code");authorization_request.searchParams.set("scope",scope);authorization_request.searchParams.set("client_id",client_id);authorization_request.searchParams.set("redirect_uri",location.origin+location.pathname);// nonceconstnonce=Array.from(window.crypto.getRandomValues(newUint32Array(4)),t=>t.toString(36)).join("");authorization_request.searchParams.set("nonce",nonce);window.localStorage.setItem("/SimpleSPA#nonce",nonce);// code_challenge_methodconstcode_challenge_method="S256";authorization_request.searchParams.set("code_challenge_method",code_challenge_method);// code_verifierconstcode_verifier=awaitnewCodeVerifier(code_challenge_method);window.localStorage.setItem("/SimpleSPA#code_verifier",code_verifier);// code_challengeconstcode_challenge=awaitgetCodeChallenge(code_challenge_method,code_verifier);authorization_request.searchParams.set("code_challenge",code_challenge);location.assign(authorization_request);}

Handle authorization response

The OpenID Provider redirects user agent back with authorization response message, containing either code or error parameters.

This code looks for code or error url parameters, then uses window.history.replaceState to remove url parameters from history.

If a code parameter is present then a token request is issued.

asyncfunctionhandleAuthenticationResponse(){constparams=newURLSearchParams(location.search.substr(1));if(params.has("code")){window.history.replaceState(null,null,location.pathname);constconfig=awaitgetConfiguration(registration.issuer);consttokenResponse=awaitinvokeTokenRequest(config,registration.client_id,registration.client_secret,params.get("code"));if("id_token"intokenResponse){constjwks=awaitgetJWKS(config);constid_token=awaitdecodeJWT(jwks,tokenResponse.id_token);constsignature_status=(id_token.signature===true) ? "signature verified" : "invalid signature";document.getElementById("signature").innerText=`(${signature_status})`;set_value("id_token",JSON.stringify(id_token.claims,null,2));constnonce_status=(id_token.claims.nonce==localStorage.getItem("/SimpleSPA#nonce")) ? "nonce verified" : "invalid nonce";document.getElementById("nonce").innerText=`(${nonce_status})`;localStorage.removeItem("/SimpleSPA#nonce");}if("access_token"intokenResponse){fetchWithToken=(input,init)=>{varrequest=newRequest(input,init);request.headers.set("Authorization","Bearer "+tokenResponse.access_token);returnwindow.fetch(request);};}else{fetchWithToken=null;}return;}if(params.has("error")){set_value("id_token",`error=${params.get("error")}`);}}

Invoke token request

The following builds and invokes an OAuth authorization code grant token request.

asyncfunctioninvokeTokenRequest(configuration,client_id,client_secret,code){consttoken_endpoint=configuration.token_endpoint;constheaders={"Content-Type": "application/x-www-form-urlencoded"};constbody=newURLSearchParams();body.set("grant_type","authorization_code");body.set("code",code);body.set("client_id",client_id);body.set("client_secret",client_secret);body.set("redirect_uri",location.origin+location.pathname);constcode_verifier=window.localStorage.getItem("/SimpleSPA#code_verifier");if(code_verifier){body.set("code_verifier",code_verifier);}try{constresponse=awaitfetch(token_endpoint,{method: "POST",mode: "cors",headers: headers,body: body.toString()});if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}finally{window.localStorage.removeItem("/SimpleSPA#code_verifier");}}

Get provider keys

The OpenID Provider's public keys are found in a JWKS document found from address specified by jwks_uri metadata property.

asyncfunctiongetJWKS(config){consturi=config.jwks_uri;constresponse=awaitfetch(uri);if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Validate ID Token integrity

ID Token is formatted as JWT, with three base64url encoded segments separated by "." character. The first part contains header, second part contains claims and final part is the signature which covers the first and second part. The WebCrypto API works with Uint8Array types so some type conversion with Uint8Array.from is needed.

asyncfunctiondecodeJWT(jwks,jwt){constjws=jwt.split(".");constheader=JSON.parse(atobUrlSafe(jws[0]));constclaims=JSON.parse(atobUrlSafe(jws[1]));consttext2verify=Uint8Array.from(jws[0]+"."+jws[1],t=>t.charCodeAt(0));constsignature=Uint8Array.from(atobUrlSafe(jws[2]),t=>t.charCodeAt(0));

Each signing key from OpenID Provider's jwks document is converted into WebCrypto Key with window.crypto.subtle.importKey. Then signature verification is attempted with window.crypto.subtle.verify.

Apparently there are some interoperability issues with JWK formatted keys and WebCrypto API which requires some transformation. One would assume algorithm and key identifiers of JWK, JWS and WebCrypto would be compatible but that appear to not be the case. In the example below I have hard coded RS256 algorithm. A real world solution needs to map JWK and JWS identifiers into WebCrypto identifiers.

constkeys=jwks.keys.filter(isSig).map(toJwk);constRS256={name: "RSASSA-PKCS1-v1_5",hash: {name: "SHA-256"},};for(constjwkofkeys){try{constkey=awaitwindow.crypto.subtle.importKey("jwk",jwk,RS256,false,["verify"]);constresult=awaitwindow.crypto.subtle.verify(RS256,key,signature,text2verify);if(result===true){return{"header": header,"claims": claims,"signature": true,"jwk": jwk,};}}catch{// ignore}}returnnegative;}

Invoke OAuth protected API

When invoking an OAuth protected API the access token is put into the Authorization http request header with the Bearer scheme.

fetchWithToken=(input,init)=>{varrequest=newRequest(input,init);request.headers.set("Authorization","Bearer "+tokenResponse.access_token);returnwindow.fetch(request);};
asyncfunctioninvokeApi(){const_fetch=fetchWithToken||window.fetch;constresponse=await_fetch(api_endpoint,{mode: "cors",cache: "no-store"});if(!response.ok)throw{error: "http_error",response: response};returnawaitresponse.json();}

Running the application

This application is ready to run with Ubisecure SSO at login.example.ubidemo.com.

With GitHub Pages

  1. Navigate to https://psteniusubi.github.io/SimpleSPA/spa.html

About

JavaScript Single Page Application and Ubisecure SSO

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Used by

Contributors