From 6f1b48f84ae0fd77d7584e1533d27aedb218e54a Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Mon, 3 Aug 2026 10:18:45 -0300 Subject: [PATCH 1/4] fix: harden OIDC auth without breaking existing configs Migrate to oidc-client-ts with auth-code+PKCE by default while keeping grantType: "implicit" working unchanged. Restore deep links via OIDC state, add silent renew, and recover from 401 without dropping the route. --- README.md | 9 +- package.json | 3 +- pnpm-lock.yaml | 35 ++- pnpm-workspace.yaml | 1 + public/silent-renew.html | 31 +++ public/vendor/oidc-client-ts.min.js | 2 + src/App.tsx | 122 +++++++---- src/auth/OidcManager.tsx | 317 ++++++++++++++++++++-------- src/auth/index.d.ts | 16 +- 9 files changed, 379 insertions(+), 157 deletions(-) create mode 100644 public/silent-renew.html create mode 100644 public/vendor/oidc-client-ts.min.js diff --git a/README.md b/README.md index e79f57d8..1ceef77e 100644 --- a/README.md +++ b/README.md @@ -373,10 +373,13 @@ window.config = { #### OAuth 2.0 configuration -Create an [OIDC client ID for web application](https://developers.google.com/identity/sign-in/web/sign-in). +Create an [OIDC client ID for a web / single-page application](https://developers.google.com/identity/sign-in/web/sign-in) and register the app origin as an authorized redirect URI (same value as Slim's `path` / app root). -Note that Google's OIDC implementation does currently not yet support the authorization code grant type with PKCE challenge for private clients. -For the time being, the legacy implicit grand type has to be used. +**Preferred grant:** omit `grantType` (or set it to authorization code). Slim uses the Authorization Code flow with PKCE via `oidc-client-ts`. + +**Legacy / compatibility:** `grantType: "implicit"` remains fully supported for existing deployments (including many Google Cloud Healthcare setups). No public config changes are required when upgrading Slim. + +Silent token renewal uses `silent-renew.html` at the app root (do not remove it from the deployed `public/` assets). The deep link the user was on before login is restored through the OIDC `state` parameter (not `localStorage`). ## Development diff --git a/package.json b/package.json index e828334b..f8385578 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "fmt": "biome format --write .", "format": "biome format --write .", "test": "craco test --watchAll=false", + "sync:oidc-vendor": "mkdir -p public/vendor && cp node_modules/oidc-client-ts/dist/browser/oidc-client-ts.min.js public/vendor/oidc-client-ts.min.js", "predeploy": "REACT_APP_CONFIG=demo PUBLIC_URL='https://imagingdatacommons.github.io/slim/' ./scripts/set-git-env.sh craco build", "deploy": "gh-pages -d build", "clean": "rm -rf ./build ./node_modules", @@ -43,7 +44,7 @@ "detect-browser": "^5.2.1", "dicom-microscopy-viewer": "^0.48.22", "dicomweb-client": "0.10.3", - "oidc-client": "^1.11.5", + "oidc-client-ts": "3.5.0", "ol": "^10.7.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb916027..929fbba5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,9 +42,9 @@ importers: dicomweb-client: specifier: 0.10.3 version: 0.10.3 - oidc-client: - specifier: ^1.11.5 - version: 1.11.5 + oidc-client-ts: + specifier: 3.5.0 + version: 3.5.0 ol: specifier: ^10.7.0 version: 10.9.0 @@ -2348,9 +2348,6 @@ packages: bare-url@2.4.5: resolution: {integrity: sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==} - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.33: resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} engines: {node: '>=6.0.0'} @@ -2749,9 +2746,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - crypto-js@4.2.0: - resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} - crypto-random-string@2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} @@ -4613,6 +4607,10 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -5239,8 +5237,9 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} - oidc-client@1.11.5: - resolution: {integrity: sha512-LcKrKC8Av0m/KD/4EFmo9Sg8fSQ+WFJWBrmtWd+tZkNn3WT/sQG3REmPANE9tzzhbjW6VkTNy4xhAXCfPApAOg==} + oidc-client-ts@3.5.0: + resolution: {integrity: sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==} + engines: {node: '>=18'} ol@10.9.0: resolution: {integrity: sha512-svbbgVQUmEHaKpLQ8kRySojs59Brvgl2zYIrqG9eQNXGfsbi55rQasZIDpwpQzDL6OlzrUb0H4hQaiX9wDoGmA==} @@ -10441,8 +10440,6 @@ snapshots: dependencies: bare-path: 3.0.1 - base64-js@1.5.1: {} - baseline-browser-mapping@2.10.33: {} batch@0.6.1: {} @@ -10867,8 +10864,6 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - crypto-js@4.2.0: {} - crypto-random-string@2.0.0: {} crypto-random-string@4.0.0: @@ -13245,6 +13240,8 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + jwt-decode@4.0.0: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -13704,13 +13701,9 @@ snapshots: obuf@1.1.2: {} - oidc-client@1.11.5: + oidc-client-ts@3.5.0: dependencies: - acorn: 7.4.1 - base64-js: 1.5.1 - core-js: 3.49.0 - crypto-js: 4.2.0 - serialize-javascript: 7.0.5 + jwt-decode: 4.0.0 ol@10.9.0: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 85c0a879..93a9abc9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,6 +4,7 @@ strictPeerDependencies: false allowBuilds: core-js: true core-js-pure: true + dicom-microscopy-viewer: true overrides: '@types/d3-dispatch': 3.0.6 diff --git a/public/silent-renew.html b/public/silent-renew.html new file mode 100644 index 00000000..166d428b --- /dev/null +++ b/public/silent-renew.html @@ -0,0 +1,31 @@ + + + + + Silent renew + + + + + + + diff --git a/public/vendor/oidc-client-ts.min.js b/public/vendor/oidc-client-ts.min.js new file mode 100644 index 00000000..00d6d688 --- /dev/null +++ b/public/vendor/oidc-client-ts.min.js @@ -0,0 +1,2 @@ +"use strict";var oidc=(()=>{var ke=Object.defineProperty;var Ke=Object.getOwnPropertyDescriptor;var Fe=Object.getOwnPropertyNames;var $e=Object.prototype.hasOwnProperty;var Be=(l,e)=>{for(var t in e)ke(l,t,{get:e[t],enumerable:!0})},ze=(l,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Fe(e))!$e.call(l,i)&&i!==t&&ke(l,i,{get:()=>e[i],enumerable:!(r=Ke(e,i))||r.enumerable});return l};var Qe=l=>ze(ke({},"__esModule",{value:!0}),l);var gt={};Be(gt,{AccessTokenEvents:()=>Y,CheckSessionIFrame:()=>Z,DPoPState:()=>W,ErrorResponse:()=>w,ErrorTimeout:()=>E,InMemoryWebStorage:()=>q,IndexedDbDPoPStore:()=>ve,Log:()=>Q,Logger:()=>g,MetadataService:()=>ee,OidcClient:()=>ae,OidcClientSettingsStore:()=>U,SessionMonitor:()=>ie,SigninResponse:()=>H,SigninState:()=>D,SignoutResponse:()=>re,State:()=>P,User:()=>L,UserManager:()=>Te,UserManagerSettingsStore:()=>ne,Version:()=>qe,WebStorageStateStore:()=>N});var Ve={debug:()=>{},info:()=>{},warn:()=>{},error:()=>{}},x,R,Q=(s=>(s[s.NONE=0]="NONE",s[s.ERROR=1]="ERROR",s[s.WARN=2]="WARN",s[s.INFO=3]="INFO",s[s.DEBUG=4]="DEBUG",s))(Q||{});(r=>{function l(){x=3,R=Ve}r.reset=l;function e(i){if(!(0<=i&&i<=4))throw new Error("Invalid log level");x=i}r.setLevel=e;function t(i){R=i}r.setLogger=t})(Q||(Q={}));var g=class l{constructor(e){this._name=e}debug(...e){x>=4&&R.debug(l._format(this._name,this._method),...e)}info(...e){x>=3&&R.info(l._format(this._name,this._method),...e)}warn(...e){x>=2&&R.warn(l._format(this._name,this._method),...e)}error(...e){x>=1&&R.error(l._format(this._name,this._method),...e)}throw(e){throw this.error(e),e}create(e){let t=Object.create(this);return t._method=e,t.debug("begin"),t}static createStatic(e,t){let r=new l(`${e}.${t}`);return r.debug("begin"),r}static _format(e,t){let r=`[${e}]`;return t?`${r} ${t}:`:r}static debug(e,...t){x>=4&&R.debug(l._format(e),...t)}static info(e,...t){x>=3&&R.info(l._format(e),...t)}static warn(e,...t){x>=2&&R.warn(l._format(e),...t)}static error(e,...t){x>=1&&R.error(l._format(e),...t)}};Q.reset();var V=class extends Error{};V.prototype.name="InvalidTokenError";function Ge(l){return decodeURIComponent(atob(l).replace(/(.)/g,(e,t)=>{let r=t.charCodeAt(0).toString(16).toUpperCase();return r.length<2&&(r="0"+r),"%"+r}))}function Xe(l){let e=l.replace(/-/g,"+").replace(/_/g,"/");switch(e.length%4){case 0:break;case 2:e+="==";break;case 3:e+="=";break;default:throw new Error("base64 string is not of the correct length")}try{return Ge(e)}catch{return atob(e)}}function Ue(l,e){if(typeof l!="string")throw new V("Invalid token specified: must be a string");e||(e={});let t=e.header===!0?0:1,r=l.split(".")[t];if(typeof r!="string")throw new V(`Invalid token specified: missing part #${t+1}`);let i;try{i=Xe(r)}catch(s){throw new V(`Invalid token specified: invalid base64 for part #${t+1} (${s.message})`)}try{return JSON.parse(i)}catch(s){throw new V(`Invalid token specified: invalid json for part #${t+1} (${s.message})`)}}var C=class{static decode(e){try{return Ue(e)}catch(t){throw g.error("JwtUtils.decode",t),t}}static async generateSignedJwt(e,t,r){let i=m.encodeBase64Url(new TextEncoder().encode(JSON.stringify(e))),s=m.encodeBase64Url(new TextEncoder().encode(JSON.stringify(t))),n=`${i}.${s}`,o=await window.crypto.subtle.sign({name:"ECDSA",hash:{name:"SHA-256"}},r,new TextEncoder().encode(n)),a=m.encodeBase64Url(new Uint8Array(o));return`${n}.${a}`}static async generateSignedJwtWithHmac(e,t,r){let i=m.encodeBase64Url(new TextEncoder().encode(JSON.stringify(e))),s=m.encodeBase64Url(new TextEncoder().encode(JSON.stringify(t))),n=`${i}.${s}`,o=await window.crypto.subtle.sign("HMAC",r,new TextEncoder().encode(n)),a=m.encodeBase64Url(new Uint8Array(o));return`${n}.${a}`}};var Ye="10000000-1000-4000-8000-100000000000",Pe=l=>btoa([...new Uint8Array(l)].map(e=>String.fromCharCode(e)).join("")),S=class S{static _randomWord(){let e=new Uint32Array(1);return crypto.getRandomValues(e),e[0]}static generateUUIDv4(){return Ye.replace(/[018]/g,t=>(+t^S._randomWord()&15>>+t/4).toString(16)).replace(/-/g,"")}static generateCodeVerifier(){return S.generateUUIDv4()+S.generateUUIDv4()+S.generateUUIDv4()}static async generateCodeChallenge(e){if(!crypto.subtle)throw new Error("Crypto.subtle is available only in secure contexts (HTTPS).");try{let r=new TextEncoder().encode(e),i=await crypto.subtle.digest("SHA-256",r);return Pe(i).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}catch(t){throw g.error("CryptoUtils.generateCodeChallenge",t),t}}static generateBasicAuth(e,t){let i=new TextEncoder().encode([e,t].join(":"));return Pe(i)}static async hash(e,t){let r=new TextEncoder().encode(t),i=await crypto.subtle.digest(e,r);return new Uint8Array(i)}static async customCalculateJwkThumbprint(e){let t;switch(e.kty){case"RSA":t={e:e.e,kty:e.kty,n:e.n};break;case"EC":t={crv:e.crv,kty:e.kty,x:e.x,y:e.y};break;case"OKP":t={crv:e.crv,kty:e.kty,x:e.x};break;case"oct":t={crv:e.k,kty:e.kty};break;default:throw new Error("Unknown jwk type")}let r=await S.hash("SHA-256",JSON.stringify(t));return S.encodeBase64Url(r)}static async generateDPoPProof({url:e,accessToken:t,httpMethod:r,keyPair:i,nonce:s}){let n,o,a={jti:window.crypto.randomUUID(),htm:r!=null?r:"GET",htu:e,iat:Math.floor(Date.now()/1e3)};t&&(n=await S.hash("SHA-256",t),o=S.encodeBase64Url(n),a.ath=o),s&&(a.nonce=s);try{let c=await crypto.subtle.exportKey("jwk",i.publicKey),d={alg:"ES256",typ:"dpop+jwt",jwk:{crv:c.crv,kty:c.kty,x:c.x,y:c.y}};return await C.generateSignedJwt(d,a,i.privateKey)}catch(c){throw c instanceof TypeError?new Error(`Error exporting dpop public key: ${c.message}`):c}}static async generateDPoPJkt(e){try{let t=await crypto.subtle.exportKey("jwk",e.publicKey);return await S.customCalculateJwkThumbprint(t)}catch(t){throw t instanceof TypeError?new Error(`Could not retrieve dpop keys from storage: ${t.message}`):t}}static async generateDPoPKeys(){return await window.crypto.subtle.generateKey({name:"ECDSA",namedCurve:"P-256"},!1,["sign","verify"])}static async generateClientAssertionJwt(e,t,r,i="HS256"){let s=Math.floor(Date.now()/1e3),n={alg:i,typ:"JWT"},o={iss:e,sub:e,aud:r,jti:S.generateUUIDv4(),exp:s+300,iat:s},c={HS256:"SHA-256",HS384:"SHA-384",HS512:"SHA-512"}[i];if(!c)throw new Error(`Unsupported algorithm: ${i}. Supported algorithms are: HS256, HS384, HS512`);let d=new TextEncoder,u=await crypto.subtle.importKey("raw",d.encode(t),{name:"HMAC",hash:c},!1,["sign"]);return await C.generateSignedJwtWithHmac(n,o,u)}};S.encodeBase64Url=e=>Pe(e).replace(/=/g,"").replace(/\+/g,"-").replace(/\//g,"_");var m=S;var b=class{constructor(e){this._name=e;this._callbacks=[];this._logger=new g(`Event('${this._name}')`)}addHandler(e){return this._callbacks.push(e),()=>this.removeHandler(e)}removeHandler(e){let t=this._callbacks.lastIndexOf(e);t>=0&&this._callbacks.splice(t,1)}async raise(...e){this._logger.debug("raise:",...e);for(let t of this._callbacks)await t(...e)}};var oe=class{static center({...e}){var t,r,i;return e.width==null&&(e.width=(t=[800,720,600,480].find(s=>s<=window.outerWidth/1.618))!=null?t:360),(r=e.left)!=null||(e.left=Math.max(0,Math.round(window.screenX+(window.outerWidth-e.width)/2))),e.height!=null&&((i=e.top)!=null||(e.top=Math.max(0,Math.round(window.screenY+(window.outerHeight-e.height)/2)))),e}static serialize(e){return Object.entries(e).filter(([,t])=>t!=null).map(([t,r])=>`${t}=${typeof r!="boolean"?r:r?"yes":"no"}`).join(",")}};var f=class l extends b{constructor(){super(...arguments);this._logger=new g(`Timer('${this._name}')`);this._timerHandle=null;this._expiration=0;this._callback=()=>{let t=this._expiration-l.getEpochTime();this._logger.debug("timer completes in",t),this._expiration<=l.getEpochTime()&&(this.cancel(),super.raise())}}static getEpochTime(){return Math.floor(Date.now()/1e3)}init(t){let r=this._logger.create("init");t=Math.max(Math.floor(t),1);let i=l.getEpochTime()+t;if(this.expiration===i&&this._timerHandle){r.debug("skipping since already initialized for expiration at",this.expiration);return}this.cancel(),r.debug("using duration",t),this._expiration=i;let s=Math.min(t,5);this._timerHandle=setInterval(this._callback,s*1e3)}get expiration(){return this._expiration}cancel(){this._logger.create("cancel"),this._timerHandle&&(clearInterval(this._timerHandle),this._timerHandle=null)}};var G=class{static readParams(e,t="query"){if(!e)throw new TypeError("Invalid URL");let i=new URL(e,"http://127.0.0.1")[t==="fragment"?"hash":"search"];return new URLSearchParams(i.slice(1))}},T=";";var w=class extends Error{constructor(t,r){var i,s,n;super(t.error_description||t.error||"");this.form=r;this.name="ErrorResponse";if(!t.error)throw g.error("ErrorResponse","No error passed"),new Error("No error passed");this.error=t.error,this.error_description=(i=t.error_description)!=null?i:null,this.error_uri=(s=t.error_uri)!=null?s:null,this.state=t.userState,this.session_state=(n=t.session_state)!=null?n:null,this.url_state=t.url_state}};var E=class extends Error{constructor(t){super(t);this.name="ErrorTimeout"}};var Y=class{constructor(e){this._logger=new g("AccessTokenEvents");this._expiringTimer=new f("Access token expiring");this._expiredTimer=new f("Access token expired");this._expiringNotificationTimeInSeconds=e.expiringNotificationTimeInSeconds}async load(e){let t=this._logger.create("load");if(e.access_token&&e.expires_in!==void 0){let r=e.expires_in;if(t.debug("access token present, remaining duration:",r),r>0){let s=r-this._expiringNotificationTimeInSeconds;s<=0&&(s=1),t.debug("registering expiring timer, raising in",s,"seconds"),this._expiringTimer.init(s)}else t.debug("canceling existing expiring timer because we're past expiration."),this._expiringTimer.cancel();let i=r+1;t.debug("registering expired timer, raising in",i,"seconds"),this._expiredTimer.init(i)}else this._expiringTimer.cancel(),this._expiredTimer.cancel()}async unload(){this._logger.debug("unload: canceling existing access token timers"),this._expiringTimer.cancel(),this._expiredTimer.cancel()}addAccessTokenExpiring(e){return this._expiringTimer.addHandler(e)}removeAccessTokenExpiring(e){this._expiringTimer.removeHandler(e)}addAccessTokenExpired(e){return this._expiredTimer.addHandler(e)}removeAccessTokenExpired(e){this._expiredTimer.removeHandler(e)}};var Z=class{constructor(e,t,r,i,s){this._callback=e;this._client_id=t;this._intervalInSeconds=i;this._stopOnError=s;this._logger=new g("CheckSessionIFrame");this._timer=null;this._session_state=null;this._message=e=>{e.origin===this._frame_origin&&e.source===this._frame.contentWindow&&(e.data==="error"?(this._logger.error("error message from check session op iframe"),this._stopOnError&&this.stop()):e.data==="changed"?(this._logger.debug("changed message from check session op iframe"),this.stop(),this._callback()):this._logger.debug(e.data+" message from check session op iframe"))};let n=new URL(r);this._frame_origin=n.origin,this._frame=window.document.createElement("iframe"),this._frame.style.visibility="hidden",this._frame.style.position="fixed",this._frame.style.left="-1000px",this._frame.style.top="0",this._frame.width="0",this._frame.height="0",this._frame.src=n.href}load(){return new Promise(e=>{this._frame.onload=()=>{e()},window.document.body.appendChild(this._frame),window.addEventListener("message",this._message,!1)})}start(e){if(this._session_state===e)return;this._logger.create("start"),this.stop(),this._session_state=e;let t=()=>{!this._frame.contentWindow||!this._session_state||this._frame.contentWindow.postMessage(this._client_id+" "+this._session_state,this._frame_origin)};t(),this._timer=setInterval(t,this._intervalInSeconds*1e3)}stop(){this._logger.create("stop"),this._session_state=null,this._timer&&(clearInterval(this._timer),this._timer=null)}};var q=class{constructor(){this._logger=new g("InMemoryWebStorage");this._data={}}clear(){this._logger.create("clear"),this._data={}}getItem(e){return this._logger.create(`getItem('${e}')`),this._data[e]}setItem(e,t){this._logger.create(`setItem('${e}')`),this._data[e]=t}removeItem(e){this._logger.create(`removeItem('${e}')`),delete this._data[e]}get length(){return Object.getOwnPropertyNames(this._data).length}key(e){return Object.getOwnPropertyNames(this._data)[e]}};var X=class extends Error{constructor(t,r){super(r);this.name="ErrorDPoPNonce";this.nonce=t}};var M=class{constructor(e=[],t=null,r={}){this._jwtHandler=t;this._extraHeaders=r;this._logger=new g("JsonService");this._contentTypes=[];this._contentTypes.push(...e,"application/json"),t&&this._contentTypes.push("application/jwt")}async fetchWithTimeout(e,t={}){let{timeoutInSeconds:r,...i}=t;if(!r)return await fetch(e,i);let s=new AbortController,n=setTimeout(()=>s.abort(),r*1e3);try{return await fetch(e,{...t,signal:s.signal})}catch(o){throw o instanceof DOMException&&o.name==="AbortError"?new E("Network timed out"):o}finally{clearTimeout(n)}}async getJson(e,{token:t,credentials:r,timeoutInSeconds:i}={}){let s=this._logger.create("getJson"),n={Accept:this._contentTypes.join(", ")};t&&(s.debug("token passed, setting Authorization header"),n.Authorization="Bearer "+t),this._appendExtraHeaders(n);let o;try{s.debug("url:",e),o=await this.fetchWithTimeout(e,{method:"GET",headers:n,timeoutInSeconds:i,credentials:r})}catch(d){throw s.error("Network Error"),d}s.debug("HTTP response received, status",o.status);let a=o.headers.get("Content-Type");if(a&&!this._contentTypes.find(d=>a.startsWith(d))&&s.throw(new Error(`Invalid response Content-Type: ${a!=null?a:"undefined"}, from URL: ${e}`)),o.ok&&this._jwtHandler&&(a!=null&&a.startsWith("application/jwt")))return await this._jwtHandler(await o.text());let c;try{c=await o.json()}catch(d){throw s.error("Error parsing JSON response",d),o.ok?d:new Error(`${o.statusText} (${o.status})`)}if(!o.ok)throw s.error("Error from server:",c),c.error?new w(c):new Error(`${o.statusText} (${o.status}): ${JSON.stringify(c)}`);return c}async postForm(e,{body:t,basicAuth:r,timeoutInSeconds:i,initCredentials:s,extraHeaders:n}){let o=this._logger.create("postForm"),a={Accept:this._contentTypes.join(", "),"Content-Type":"application/x-www-form-urlencoded",...n};r!==void 0&&(a.Authorization="Basic "+r),this._appendExtraHeaders(a);let c;try{o.debug("url:",e),c=await this.fetchWithTimeout(e,{method:"POST",headers:a,body:t,timeoutInSeconds:i,credentials:s})}catch(h){throw o.error("Network error"),h}o.debug("HTTP response received, status",c.status);let d=c.headers.get("Content-Type");if(d&&!this._contentTypes.find(h=>d.startsWith(h)))throw new Error(`Invalid response Content-Type: ${d!=null?d:"undefined"}, from URL: ${e}`);let u=await c.text(),p={};if(u)try{p=JSON.parse(u)}catch(h){throw o.error("Error parsing JSON response",h),c.ok?h:new Error(`${c.statusText} (${c.status})`)}if(!c.ok){if(o.error("Error from server:",p),c.headers.has("dpop-nonce")){let h=c.headers.get("dpop-nonce");throw new X(h,`${JSON.stringify(p)}`)}throw p.error?new w(p,t):new Error(`${c.statusText} (${c.status}): ${JSON.stringify(p)}`)}return p}_appendExtraHeaders(e){let t=this._logger.create("appendExtraHeaders"),r=Object.keys(this._extraHeaders),i=["accept","content-type"],s=["authorization"];r.length!==0&&r.forEach(n=>{if(i.includes(n.toLocaleLowerCase())){t.warn("Protected header could not be set",n,i);return}if(s.includes(n.toLocaleLowerCase())&&Object.keys(e).includes(n)){t.warn("Header could not be overridden",n,s);return}let o=typeof this._extraHeaders[n]=="function"?this._extraHeaders[n]():this._extraHeaders[n];o&&o!==""&&(e[n]=o)})}};var ee=class{constructor(e){this._settings=e;this._logger=new g("MetadataService");this._signingKeys=null;this._metadata=null;this._metadataUrl=this._settings.metadataUrl,this._jsonService=new M(["application/jwk-set+json"],null,this._settings.extraHeaders),this._settings.signingKeys&&(this._logger.debug("using signingKeys from settings"),this._signingKeys=this._settings.signingKeys),this._settings.metadata&&(this._logger.debug("using metadata from settings"),this._metadata=this._settings.metadata),this._settings.fetchRequestCredentials&&(this._logger.debug("using fetchRequestCredentials from settings"),this._fetchRequestCredentials=this._settings.fetchRequestCredentials)}resetSigningKeys(){this._signingKeys=null}async getMetadata(){let e=this._logger.create("getMetadata");if(this._metadata)return e.debug("using cached values"),this._metadata;if(!this._metadataUrl)throw e.throw(new Error("No authority or metadataUrl configured on settings")),null;e.debug("getting metadata from",this._metadataUrl);let t=await this._jsonService.getJson(this._metadataUrl,{credentials:this._fetchRequestCredentials,timeoutInSeconds:this._settings.requestTimeoutInSeconds});return e.debug("merging remote JSON with seed metadata"),this._metadata=Object.assign({},t,this._settings.metadataSeed),this._metadata}getIssuer(){return this._getMetadataProperty("issuer")}getAuthorizationEndpoint(){return this._getMetadataProperty("authorization_endpoint")}getUserInfoEndpoint(){return this._getMetadataProperty("userinfo_endpoint")}getTokenEndpoint(e=!0){return this._getMetadataProperty("token_endpoint",e)}getCheckSessionIframe(){return this._getMetadataProperty("check_session_iframe",!0)}getEndSessionEndpoint(){return this._getMetadataProperty("end_session_endpoint",!0)}getRevocationEndpoint(e=!0){return this._getMetadataProperty("revocation_endpoint",e)}getKeysEndpoint(e=!0){return this._getMetadataProperty("jwks_uri",e)}async _getMetadataProperty(e,t=!1){let r=this._logger.create(`_getMetadataProperty('${e}')`),i=await this.getMetadata();if(r.debug("resolved"),i[e]===void 0){if(t===!0){r.warn("Metadata does not contain optional property");return}r.throw(new Error("Metadata does not contain property "+e))}return i[e]}async getSigningKeys(){let e=this._logger.create("getSigningKeys");if(this._signingKeys)return e.debug("returning signingKeys from cache"),this._signingKeys;let t=await this.getKeysEndpoint(!1);e.debug("got jwks_uri",t);let r=await this._jsonService.getJson(t,{timeoutInSeconds:this._settings.requestTimeoutInSeconds});if(e.debug("got key set",r),!Array.isArray(r.keys))throw e.throw(new Error("Missing keys on keyset")),null;return this._signingKeys=r.keys,this._signingKeys}};var N=class{constructor({prefix:e="oidc.",store:t=localStorage}={}){this._logger=new g("WebStorageStateStore");this._store=t,this._prefix=e}async set(e,t){this._logger.create(`set('${e}')`),e=this._prefix+e,await this._store.setItem(e,t)}async get(e){return this._logger.create(`get('${e}')`),e=this._prefix+e,await this._store.getItem(e)}async remove(e){this._logger.create(`remove('${e}')`),e=this._prefix+e;let t=await this._store.getItem(e);return await this._store.removeItem(e),t}async getAllKeys(){this._logger.create("getAllKeys");let e=await this._store.length,t=[];for(let r=0;r{let t=this._logger.create("_getClaimsFromJwt");try{let r=C.decode(e);return t.debug("JWT decoding successful"),r}catch(r){throw t.error("Error parsing JWT response"),r}};this._jsonService=new M(void 0,this._getClaimsFromJwt,this._settings.extraHeaders)}async getClaims(e){let t=this._logger.create("getClaims");e||this._logger.throw(new Error("No token passed"));let r=await this._metadataService.getUserInfoEndpoint();t.debug("got userinfo url",r);let i=await this._jsonService.getJson(r,{token:e,credentials:this._settings.fetchRequestCredentials,timeoutInSeconds:this._settings.requestTimeoutInSeconds});return t.debug("got claims",i),i}};var te=class{constructor(e,t){this._settings=e;this._metadataService=t;this._logger=new g("TokenClient");this._jsonService=new M(this._settings.revokeTokenAdditionalContentTypes,null,this._settings.extraHeaders)}async exchangeCode({grant_type:e="authorization_code",redirect_uri:t=this._settings.redirect_uri,client_id:r=this._settings.client_id,client_secret:i=this._settings.client_secret,extraHeaders:s,...n}){let o=this._logger.create("exchangeCode");r||o.throw(new Error("A client_id is required")),t||o.throw(new Error("A redirect_uri is required")),n.code||o.throw(new Error("A code is required"));let a=new URLSearchParams({grant_type:e,redirect_uri:t});for(let[p,h]of Object.entries(n))h!=null&&a.set(p,h);if((this._settings.client_authentication==="client_secret_basic"||this._settings.client_authentication==="client_secret_jwt")&&i==null)throw o.throw(new Error("A client_secret is required")),null;let c,d=await this._metadataService.getTokenEndpoint(!1);switch(this._settings.client_authentication){case"client_secret_basic":c=m.generateBasicAuth(r,i);break;case"client_secret_post":a.append("client_id",r),i&&a.append("client_secret",i);break;case"client_secret_jwt":{let p=await m.generateClientAssertionJwt(r,i,d,this._settings.token_endpoint_auth_signing_alg);a.append("client_id",r),a.append("client_assertion_type","urn:ietf:params:oauth:client-assertion-type:jwt-bearer"),a.append("client_assertion",p);break}}o.debug("got token endpoint");let u=await this._jsonService.postForm(d,{body:a,basicAuth:c,timeoutInSeconds:this._settings.requestTimeoutInSeconds,initCredentials:this._settings.fetchRequestCredentials,extraHeaders:s});return o.debug("got response"),u}async exchangeCredentials({grant_type:e="password",client_id:t=this._settings.client_id,client_secret:r=this._settings.client_secret,scope:i=this._settings.scope,...s}){let n=this._logger.create("exchangeCredentials");t||n.throw(new Error("A client_id is required"));let o=new URLSearchParams({grant_type:e});this._settings.omitScopeWhenRequesting||o.set("scope",i);for(let[u,p]of Object.entries(s))p!=null&&o.set(u,p);if((this._settings.client_authentication==="client_secret_basic"||this._settings.client_authentication==="client_secret_jwt")&&r==null)throw n.throw(new Error("A client_secret is required")),null;let a,c=await this._metadataService.getTokenEndpoint(!1);switch(this._settings.client_authentication){case"client_secret_basic":a=m.generateBasicAuth(t,r);break;case"client_secret_post":o.append("client_id",t),r&&o.append("client_secret",r);break;case"client_secret_jwt":{let u=await m.generateClientAssertionJwt(t,r,c,this._settings.token_endpoint_auth_signing_alg);o.append("client_id",t),o.append("client_assertion_type","urn:ietf:params:oauth:client-assertion-type:jwt-bearer"),o.append("client_assertion",u);break}}n.debug("got token endpoint");let d=await this._jsonService.postForm(c,{body:o,basicAuth:a,timeoutInSeconds:this._settings.requestTimeoutInSeconds,initCredentials:this._settings.fetchRequestCredentials});return n.debug("got response"),d}async exchangeRefreshToken({grant_type:e="refresh_token",client_id:t=this._settings.client_id,client_secret:r=this._settings.client_secret,timeoutInSeconds:i,extraHeaders:s,...n}){let o=this._logger.create("exchangeRefreshToken");t||o.throw(new Error("A client_id is required")),n.refresh_token||o.throw(new Error("A refresh_token is required"));let a=new URLSearchParams({grant_type:e});for(let[p,h]of Object.entries(n))Array.isArray(h)?h.forEach(v=>a.append(p,v)):h!=null&&a.set(p,h);if((this._settings.client_authentication==="client_secret_basic"||this._settings.client_authentication==="client_secret_jwt")&&r==null)throw o.throw(new Error("A client_secret is required")),null;let c,d=await this._metadataService.getTokenEndpoint(!1);switch(this._settings.client_authentication){case"client_secret_basic":c=m.generateBasicAuth(t,r);break;case"client_secret_post":a.append("client_id",t),r&&a.append("client_secret",r);break;case"client_secret_jwt":{let p=await m.generateClientAssertionJwt(t,r,d,this._settings.token_endpoint_auth_signing_alg);a.append("client_id",t),a.append("client_assertion_type","urn:ietf:params:oauth:client-assertion-type:jwt-bearer"),a.append("client_assertion",p);break}}o.debug("got token endpoint");let u=await this._jsonService.postForm(d,{body:a,basicAuth:c,timeoutInSeconds:i,initCredentials:this._settings.fetchRequestCredentials,extraHeaders:s});return o.debug("got response"),u}async revoke(e){var s;let t=this._logger.create("revoke");e.token||t.throw(new Error("A token is required"));let r=await this._metadataService.getRevocationEndpoint(!1);t.debug(`got revocation endpoint, revoking ${(s=e.token_type_hint)!=null?s:"default token type"}`);let i=new URLSearchParams;for(let[n,o]of Object.entries(e))o!=null&&i.set(n,o);i.set("client_id",this._settings.client_id),this._settings.client_secret&&i.set("client_secret",this._settings.client_secret),await this._jsonService.postForm(r,{body:i,timeoutInSeconds:this._settings.requestTimeoutInSeconds}),t.debug("got response")}};var ge=class{constructor(e,t,r){this._settings=e;this._metadataService=t;this._claimsService=r;this._logger=new g("ResponseValidator");this._userInfoService=new de(this._settings,this._metadataService),this._tokenClient=new te(this._settings,this._metadataService)}async validateSigninResponse(e,t,r){let i=this._logger.create("validateSigninResponse");this._processSigninState(e,t),i.debug("state processed"),await this._processCode(e,t,r),i.debug("code processed"),e.isOpenId&&this._validateIdTokenAttributes(e,"",t.nonce),i.debug("tokens validated"),await this._processClaims(e,t==null?void 0:t.skipUserInfo,e.isOpenId),i.debug("claims processed")}async validateCredentialsResponse(e,t){let r=this._logger.create("validateCredentialsResponse"),i=e.isOpenId&&!!e.id_token;i&&this._validateIdTokenAttributes(e),r.debug("tokens validated"),await this._processClaims(e,t,i),r.debug("claims processed")}async validateRefreshResponse(e,t){var s,n;let r=this._logger.create("validateRefreshResponse");e.userState=t.data,(s=e.session_state)!=null||(e.session_state=t.session_state),(n=e.scope)!=null||(e.scope=t.scope),e.isOpenId&&e.id_token&&(this._validateIdTokenAttributes(e,t.id_token),r.debug("ID Token validated")),e.id_token||(e.id_token=t.id_token,e.profile=t.profile);let i=e.isOpenId&&!!e.id_token;await this._processClaims(e,!1,i),r.debug("claims processed")}validateSignoutResponse(e,t){let r=this._logger.create("validateSignoutResponse");if(t.id!==e.state&&r.throw(new Error("State does not match")),r.debug("state validated"),e.userState=t.data,e.error)throw r.warn("Response was error",e.error),new w(e)}_processSigninState(e,t){var i;let r=this._logger.create("_processSigninState");if(t.id!==e.state&&r.throw(new Error("State does not match")),t.client_id||r.throw(new Error("No client_id on state")),t.authority||r.throw(new Error("No authority on state")),this._settings.authority!==t.authority&&r.throw(new Error("authority mismatch on settings vs. signin state")),this._settings.client_id&&this._settings.client_id!==t.client_id&&r.throw(new Error("client_id mismatch on settings vs. signin state")),r.debug("state validated"),e.userState=t.data,e.url_state=t.url_state,(i=e.scope)!=null||(e.scope=t.scope),e.error)throw r.warn("Response was error",e.error),new w(e);t.code_verifier&&!e.code&&r.throw(new Error("Expected code in response"))}async _processClaims(e,t=!1,r=!0){let i=this._logger.create("_processClaims");if(e.profile=this._claimsService.filterProtocolClaims(e.profile),t||!this._settings.loadUserInfo||!e.access_token){i.debug("not loading user info");return}i.debug("loading user info");let s=await this._userInfoService.getClaims(e.access_token);i.debug("user info claims received from user info endpoint"),r&&s.sub!==e.profile.sub&&i.throw(new Error("subject from UserInfo response does not match subject in ID Token")),e.profile=this._claimsService.mergeClaims(e.profile,this._claimsService.filterProtocolClaims(s)),i.debug("user info claims received, updated profile:",e.profile)}async _processCode(e,t,r){let i=this._logger.create("_processCode");if(e.code){i.debug("Validating code");let s=await this._tokenClient.exchangeCode({client_id:t.client_id,client_secret:t.client_secret,code:e.code,redirect_uri:t.redirect_uri,code_verifier:t.code_verifier,extraHeaders:r,...t.extraTokenParams});Object.assign(e,s)}else i.debug("No code to process")}_validateIdTokenAttributes(e,t,r){var n;let i=this._logger.create("_validateIdTokenAttributes");i.debug("decoding ID Token JWT");let s=C.decode((n=e.id_token)!=null?n:"");if(s.sub||i.throw(new Error("ID Token is missing a subject claim")),r&&s.nonce!==r&&i.throw(new Error("nonce in id_token does not match nonce in client storage")),t){let o=C.decode(t);s.sub!==o.sub&&i.throw(new Error("sub in id_token does not match current sub")),s.auth_time&&s.auth_time!==o.auth_time&&i.throw(new Error("auth_time in id_token does not match original auth_time")),s.azp&&s.azp!==o.azp&&i.throw(new Error("azp in id_token does not match original azp")),!s.azp&&o.azp&&i.throw(new Error("azp not in id_token, but present in original id_token"))}e.profile=s}};var P=class l{constructor(e){this.id=e.id||m.generateUUIDv4(),this.data=e.data,e.created&&e.created>0?this.created=e.created:this.created=f.getEpochTime(),this.request_type=e.request_type,this.url_state=e.url_state}toStorageString(){return new g("State").create("toStorageString"),JSON.stringify({id:this.id,data:this.data,created:this.created,request_type:this.request_type,url_state:this.url_state})}static fromStorageString(e){return g.createStatic("State","fromStorageString"),Promise.resolve(new l(JSON.parse(e)))}static async clearStaleState(e,t){let r=g.createStatic("State","clearStaleState"),i=f.getEpochTime()-t,s=await e.getAllKeys();r.debug("got keys",s);for(let n=0;n_.searchParams.append("resource",k));for(let[O,k]of Object.entries({response_mode:a,...$,...j}))k!=null&&_.searchParams.append(O,k.toString());return new ue({url:_.href,state:y})}};ue._logger=new g("SigninRequest");var pe=ue;var it="openid",H=class{constructor(e){this.access_token="";this.token_type="";this.profile={};if(this.state=e.get("state"),this.session_state=e.get("session_state"),this.state){let t=decodeURIComponent(this.state).split(T);this.state=t[0],t.length>1&&(this.url_state=t.slice(1).join(T))}this.error=e.get("error"),this.error_description=e.get("error_description"),this.error_uri=e.get("error_uri"),this.code=e.get("code")}get expires_in(){if(this.expires_at!==void 0)return this.expires_at-f.getEpochTime()}set expires_in(e){typeof e=="string"&&(e=Number(e)),e!==void 0&&e>=0&&(this.expires_at=Math.floor(e)+f.getEpochTime())}get isOpenId(){var e;return((e=this.scope)==null?void 0:e.split(" ").includes(it))||!!this.id_token}};var he=class{constructor({url:e,state_data:t,id_token_hint:r,post_logout_redirect_uri:i,extraQueryParams:s,request_type:n,client_id:o,url_state:a}){this._logger=new g("SignoutRequest");if(!e)throw this._logger.error("ctor: No url passed"),new Error("url");let c=new URL(e);if(r&&c.searchParams.append("id_token_hint",r),o&&c.searchParams.append("client_id",o),i&&(c.searchParams.append("post_logout_redirect_uri",i),t||a)){this.state=new P({data:t,request_type:n,url_state:a});let d=this.state.id;a&&(d=`${d}${T}${a}`),c.searchParams.append("state",d)}for(let[d,u]of Object.entries({...s}))u!=null&&c.searchParams.append(d,u.toString());this.url=c.href}};var re=class{constructor(e){if(this.state=e.get("state"),this.state){let t=decodeURIComponent(this.state).split(T);this.state=t[0],t.length>1&&(this.url_state=t.slice(1).join(T))}this.error=e.get("error"),this.error_description=e.get("error_description"),this.error_uri=e.get("error_uri")}};var st=["nbf","jti","auth_time","nonce","acr","amr","azp","at_hash"],nt=["sub","iss","aud","exp","iat"],me=class{constructor(e){this._settings=e;this._logger=new g("ClaimsService")}filterProtocolClaims(e){let t={...e};if(this._settings.filterProtocolClaims){let r;Array.isArray(this._settings.filterProtocolClaims)?r=this._settings.filterProtocolClaims:r=st;for(let i of r)nt.includes(i)||delete t[i]}return t}mergeClaims(e,t){let r={...e};for(let[i,s]of Object.entries(t))if(r[i]!==s)if(Array.isArray(r[i])||Array.isArray(s))if(this._settings.mergeClaimsStrategy.array=="replace")r[i]=s;else{let n=Array.isArray(r[i])?r[i]:[r[i]];for(let o of Array.isArray(s)?s:[s])n.includes(o)||n.push(o);r[i]=n}else typeof r[i]=="object"&&typeof s=="object"?r[i]=this.mergeClaims(r[i],s):r[i]=s;return r}};var W=class{constructor(e,t){this.keys=e;this.nonce=t}};var ae=class{constructor(e,t){this._logger=new g("OidcClient");this.settings=e instanceof U?e:new U(e),this.metadataService=t!=null?t:new ee(this.settings),this._claimsService=new me(this.settings),this._validator=new ge(this.settings,this.metadataService,this._claimsService),this._tokenClient=new te(this.settings,this.metadataService)}async createSigninRequest({state:e,request:t,request_uri:r,request_type:i,id_token_hint:s,login_hint:n,skipUserInfo:o,nonce:a,url_state:c,response_type:d=this.settings.response_type,scope:u=this.settings.scope,redirect_uri:p=this.settings.redirect_uri,prompt:h=this.settings.prompt,display:v=this.settings.display,max_age:j=this.settings.max_age,ui_locales:J=this.settings.ui_locales,acr_values:K=this.settings.acr_values,resource:A=this.settings.resource,response_mode:F=this.settings.response_mode,extraQueryParams:$=this.settings.extraQueryParams,extraTokenParams:y=this.settings.extraTokenParams,dpopJkt:_,omitScopeWhenRequesting:I=this.settings.omitScopeWhenRequesting}){let O=this._logger.create("createSigninRequest");if(d!=="code")throw new Error("Only the Authorization Code flow (with PKCE) is supported");let k=await this.metadataService.getAuthorizationEndpoint();O.debug("Received authorization endpoint",k);let B=await pe.create({url:k,authority:this.settings.authority,client_id:this.settings.client_id,redirect_uri:p,response_type:d,scope:u,state_data:e,url_state:c,prompt:h,display:v,max_age:j,ui_locales:J,id_token_hint:s,login_hint:n,acr_values:K,dpopJkt:_,resource:A,request:t,request_uri:r,extraQueryParams:$,extraTokenParams:y,request_type:i,response_mode:F,client_secret:this.settings.client_secret,skipUserInfo:o,nonce:a,disablePKCE:this.settings.disablePKCE,omitScopeWhenRequesting:I});await this.clearStaleState();let z=B.state;return await this.settings.stateStore.set(z.id,z.toStorageString()),B}async readSigninResponseState(e,t=!1){let r=this._logger.create("readSigninResponseState"),i=new H(G.readParams(e,this.settings.response_mode));if(!i.state)throw r.throw(new Error("No state in response")),null;let s=await this.settings.stateStore[t?"remove":"get"](i.state);if(!s)throw r.throw(new Error("No matching state found in storage")),null;return{state:await D.fromStorageString(s),response:i}}async processSigninResponse(e,t,r=!0){let i=this._logger.create("processSigninResponse"),{state:s,response:n}=await this.readSigninResponseState(e,r);if(i.debug("received state from storage; validating response"),this.settings.dpop&&this.settings.dpop.store){let o=await this.getDpopProof(this.settings.dpop.store);t={...t,DPoP:o}}try{await this._validator.validateSigninResponse(n,s,t)}catch(o){if(o instanceof X&&this.settings.dpop){let a=await this.getDpopProof(this.settings.dpop.store,o.nonce);t.DPoP=a,await this._validator.validateSigninResponse(n,s,t)}else throw o}return n}async getDpopProof(e,t){let r,i;return(await e.getAllKeys()).includes(this.settings.client_id)?(i=await e.get(this.settings.client_id),i.nonce!==t&&t&&(i.nonce=t,await e.set(this.settings.client_id,i))):(r=await m.generateDPoPKeys(),i=new W(r,t),await e.set(this.settings.client_id,i)),await m.generateDPoPProof({url:await this.metadataService.getTokenEndpoint(!1),httpMethod:"POST",keyPair:i.keys,nonce:i.nonce})}async processResourceOwnerPasswordCredentials({username:e,password:t,skipUserInfo:r=!1,extraTokenParams:i={}}){let s=await this._tokenClient.exchangeCredentials({username:e,password:t,...i}),n=new H(new URLSearchParams);return Object.assign(n,s),await this._validator.validateCredentialsResponse(n,r),n}async useRefreshToken({state:e,redirect_uri:t,resource:r,timeoutInSeconds:i,extraHeaders:s,extraTokenParams:n}){var u;let o=this._logger.create("useRefreshToken"),a;if(this.settings.refreshTokenAllowedScope===void 0)a=e.scope;else{let p=this.settings.refreshTokenAllowedScope.split(" ");a=(((u=e.scope)==null?void 0:u.split(" "))||[]).filter(v=>p.includes(v)).join(" ")}if(this.settings.dpop&&this.settings.dpop.store){let p=await this.getDpopProof(this.settings.dpop.store);s={...s,DPoP:p}}let c;try{c=await this._tokenClient.exchangeRefreshToken({refresh_token:e.refresh_token,scope:a,redirect_uri:t,resource:r,timeoutInSeconds:i,extraHeaders:s,...n})}catch(p){if(p instanceof X&&this.settings.dpop)s.DPoP=await this.getDpopProof(this.settings.dpop.store,p.nonce),c=await this._tokenClient.exchangeRefreshToken({refresh_token:e.refresh_token,scope:a,redirect_uri:t,resource:r,timeoutInSeconds:i,extraHeaders:s,...n});else throw p}let d=new H(new URLSearchParams);return Object.assign(d,c),o.debug("validating response",d),await this._validator.validateRefreshResponse(d,{...e,scope:a}),d}async createSignoutRequest({state:e,id_token_hint:t,client_id:r,request_type:i,url_state:s,post_logout_redirect_uri:n=this.settings.post_logout_redirect_uri,extraQueryParams:o=this.settings.extraQueryParams}={}){let a=this._logger.create("createSignoutRequest"),c=await this.metadataService.getEndSessionEndpoint();if(!c)throw a.throw(new Error("No end session endpoint")),null;a.debug("Received end session endpoint",c),!r&&n&&!t&&(r=this.settings.client_id);let d=new he({url:c,id_token_hint:t,client_id:r,post_logout_redirect_uri:n,state_data:e,extraQueryParams:o,request_type:i,url_state:s});await this.clearStaleState();let u=d.state;return u&&(a.debug("Signout request has state to persist"),await this.settings.stateStore.set(u.id,u.toStorageString())),d}async readSignoutResponseState(e,t=!1){let r=this._logger.create("readSignoutResponseState"),i=new re(G.readParams(e,this.settings.response_mode));if(!i.state){if(r.debug("No state in response"),i.error)throw r.warn("Response was error:",i.error),new w(i);return{state:void 0,response:i}}let s=await this.settings.stateStore[t?"remove":"get"](i.state);if(!s)throw r.throw(new Error("No matching state found in storage")),null;return{state:await P.fromStorageString(s),response:i}}async processSignoutResponse(e){let t=this._logger.create("processSignoutResponse"),{state:r,response:i}=await this.readSignoutResponseState(e,!0);return r?(t.debug("Received state from storage; validating response"),this._validator.validateSignoutResponse(i,r)):t.debug("No state from storage; skipping response validation"),i}clearStaleState(){return this._logger.create("clearStaleState"),P.clearStaleState(this.settings.stateStore,this.settings.staleStateAgeInSeconds)}async revokeToken(e,t){return this._logger.create("revokeToken"),await this._tokenClient.revoke({token:e,token_type_hint:t})}};var ie=class{constructor(e){this._userManager=e;this._logger=new g("SessionMonitor");this._start=async e=>{let t=e.session_state;if(!t)return;let r=this._logger.create("_start");if(e.profile?(this._sub=e.profile.sub,r.debug("session_state",t,", sub",this._sub)):(this._sub=void 0,r.debug("session_state",t,", anonymous user")),this._checkSessionIFrame){this._checkSessionIFrame.start(t);return}try{let i=await this._userManager.metadataService.getCheckSessionIframe();if(i){r.debug("initializing check session iframe");let s=this._userManager.settings.client_id,n=this._userManager.settings.checkSessionIntervalInSeconds,o=this._userManager.settings.stopCheckSessionOnError,a=new Z(this._callback,s,i,n,o);await a.load(),this._checkSessionIFrame=a,a.start(t)}else r.warn("no check session iframe found in the metadata")}catch(i){r.error("Error from getCheckSessionIframe:",i instanceof Error?i.message:i)}};this._stop=()=>{let e=this._logger.create("_stop");if(this._sub=void 0,this._checkSessionIFrame&&this._checkSessionIFrame.stop(),this._userManager.settings.monitorAnonymousSession){let t=setInterval(async()=>{clearInterval(t);try{let r=await this._userManager.querySessionStatus();if(r){let i={session_state:r.session_state,profile:r.sub?{sub:r.sub}:null};this._start(i)}}catch(r){e.error("error from querySessionStatus",r instanceof Error?r.message:r)}},1e3)}};this._callback=async()=>{let e=this._logger.create("_callback");try{let t=await this._userManager.querySessionStatus(),r=!0;t&&this._checkSessionIFrame?t.sub===this._sub?(r=!1,this._checkSessionIFrame.start(t.session_state),e.debug("same sub still logged in at OP, session state has changed, restarting check session iframe; session_state",t.session_state),await this._userManager.events._raiseUserSessionChanged()):e.debug("different subject signed into OP",t.sub):e.debug("subject no longer signed into OP"),r?this._sub?await this._userManager.events._raiseUserSignedOut():await this._userManager.events._raiseUserSignedIn():e.debug("no change in session detected, no event to raise")}catch(t){this._sub&&(e.debug("Error calling queryCurrentSigninSession; raising signed out event",t),await this._userManager.events._raiseUserSignedOut())}};e||this._logger.throw(new Error("No user manager passed")),this._userManager.events.addUserLoaded(this._start),this._userManager.events.addUserUnloaded(this._stop),this._init().catch(t=>{this._logger.error(t)})}async _init(){this._logger.create("_init");let e=await this._userManager.getUser();if(e)this._start(e);else if(this._userManager.settings.monitorAnonymousSession){let t=await this._userManager.querySessionStatus();if(t){let r={session_state:t.session_state,profile:t.sub?{sub:t.sub}:null};this._start(r)}}}};var L=class l{constructor(e){var t;this.id_token=e.id_token,this.session_state=(t=e.session_state)!=null?t:null,this.access_token=e.access_token,this.refresh_token=e.refresh_token,this.token_type=e.token_type,this.scope=e.scope,this.profile=e.profile,this.expires_at=e.expires_at,this.state=e.userState,this.url_state=e.url_state}get expires_in(){if(this.expires_at!==void 0)return this.expires_at-f.getEpochTime()}set expires_in(e){e!==void 0&&(this.expires_at=Math.floor(e)+f.getEpochTime())}get expired(){let e=this.expires_in;if(e!==void 0)return e<=0}get scopes(){var e,t;return(t=(e=this.scope)==null?void 0:e.split(" "))!=null?t:[]}toStorageString(){return new g("User").create("toStorageString"),JSON.stringify({id_token:this.id_token,session_state:this.session_state,access_token:this.access_token,refresh_token:this.refresh_token,token_type:this.token_type,scope:this.scope,profile:this.profile,expires_at:this.expires_at})}static fromStorageString(e){return g.createStatic("User","fromStorageString"),new l(JSON.parse(e))}};var Ae="oidc-client",se=class{constructor(){this._abort=new b("Window navigation aborted");this._disposeHandlers=new Set;this._window=null}async navigate(e){let t=this._logger.create("navigate");if(!this._window)throw new Error("Attempted to navigate on a disposed window");t.debug("setting URL in window"),this._window.location.replace(e.url);let{url:r,keepOpen:i}=await new Promise((s,n)=>{let o=c=>{var p;let d=c.data,u=(p=e.scriptOrigin)!=null?p:window.location.origin;if(!(c.origin!==u||(d==null?void 0:d.source)!==Ae)){try{let h=G.readParams(d.url,e.response_mode).get("state");if(h||t.warn("no state found in response url"),c.source!==this._window&&h!==e.state)return}catch{this._dispose(),n(new Error("Invalid response from window"))}s(d)}};window.addEventListener("message",o,!1),this._disposeHandlers.add(()=>window.removeEventListener("message",o,!1));let a=new BroadcastChannel(`oidc-client-popup-${e.state}`);a.addEventListener("message",o,!1),this._disposeHandlers.add(()=>a.close()),this._disposeHandlers.add(this._abort.addHandler(c=>{this._dispose(),n(c)}))});return t.debug("got response from window"),this._dispose(),i||this.close(),{url:r}}_dispose(){this._logger.create("_dispose");for(let e of this._disposeHandlers)e();this._disposeHandlers.clear()}static _notifyParent(e,t,r=!1,i=window.location.origin){let s={source:Ae,url:t,keepOpen:r},n=new g("_notifyParent");if(e)n.debug("With parent. Using parent.postMessage."),e.postMessage(s,i);else{n.debug("No parent. Using BroadcastChannel.");let o=new URL(t).searchParams.get("state");if(!o)throw new Error("No parent and no state in URL. Can't complete notification.");let a=new BroadcastChannel(`oidc-client-popup-${o}`);a.postMessage(s),a.close()}}};var xe={location:!1,toolbar:!1,height:640,closePopupWindowAfterInSeconds:-1},Re="_blank",ot=60,at=2,Ce=10,ne=class extends U{constructor(e){let{popup_redirect_uri:t=e.redirect_uri,popup_post_logout_redirect_uri:r=e.post_logout_redirect_uri,popupWindowFeatures:i=xe,popupWindowTarget:s=Re,redirectMethod:n="assign",redirectTarget:o="self",iframeNotifyParentOrigin:a=e.iframeNotifyParentOrigin,iframeScriptOrigin:c=e.iframeScriptOrigin,requestTimeoutInSeconds:d,silent_redirect_uri:u=e.redirect_uri,silentRequestTimeoutInSeconds:p,automaticSilentRenew:h=!0,validateSubOnSilentRenew:v=!0,includeIdTokenInSilentRenew:j=!1,monitorSession:J=!1,monitorAnonymousSession:K=!1,checkSessionIntervalInSeconds:A=at,query_status_response_type:F="code",stopCheckSessionOnError:$=!0,revokeTokenTypes:y=["access_token","refresh_token"],revokeTokensOnSignout:_=!1,includeIdTokenInSilentSignout:I=!1,accessTokenExpiringNotificationTimeInSeconds:O=ot,maxSilentRenewTimeoutRetries:k,userStore:B}=e;if(super(e),this.popup_redirect_uri=t,this.popup_post_logout_redirect_uri=r,this.popupWindowFeatures=i,this.popupWindowTarget=s,this.redirectMethod=n,this.redirectTarget=o,this.iframeNotifyParentOrigin=a,this.iframeScriptOrigin=c,this.silent_redirect_uri=u,this.silentRequestTimeoutInSeconds=p||d||Ce,this.automaticSilentRenew=h,this.validateSubOnSilentRenew=v,this.includeIdTokenInSilentRenew=j,this.monitorSession=J,this.monitorAnonymousSession=K,this.checkSessionIntervalInSeconds=A,this.stopCheckSessionOnError=$,this.query_status_response_type=F,this.revokeTokenTypes=y,this.revokeTokensOnSignout=_,this.includeIdTokenInSilentSignout=I,this.accessTokenExpiringNotificationTimeInSeconds=O,this.maxSilentRenewTimeoutRetries=k,B)this.userStore=B;else{let z=typeof window!="undefined"?window.sessionStorage:new q;this.userStore=new N({store:z})}}};var ce=class l extends se{constructor({silentRequestTimeoutInSeconds:t=Ce}){super();this._logger=new g("IFrameWindow");this._timeoutInSeconds=t,this._frame=l.createHiddenIframe(),this._window=this._frame.contentWindow}static createHiddenIframe(){let t=window.document.createElement("iframe");return t.style.visibility="hidden",t.style.position="fixed",t.style.left="-1000px",t.style.top="0",t.width="0",t.height="0",window.document.body.appendChild(t),t}async navigate(t){this._logger.debug("navigate: Using timeout of:",this._timeoutInSeconds);let r=setTimeout(()=>{this._abort.raise(new E("IFrame timed out without a response"))},this._timeoutInSeconds*1e3);return this._disposeHandlers.add(()=>clearTimeout(r)),await super.navigate(t)}close(){var t;this._frame&&(this._frame.parentNode&&(this._frame.addEventListener("load",r=>{var s;let i=r.target;(s=i.parentNode)==null||s.removeChild(i),this._abort.raise(new Error("IFrame removed from DOM"))},!0),(t=this._frame.contentWindow)==null||t.location.replace("about:blank")),this._frame=null),this._window=null}static notifyParent(t,r){return super._notifyParent(window.parent,t,!1,r)}};var _e=class{constructor(e){this._settings=e;this._logger=new g("IFrameNavigator")}async prepare({silentRequestTimeoutInSeconds:e=this._settings.silentRequestTimeoutInSeconds}){return new ce({silentRequestTimeoutInSeconds:e})}async callback(e){this._logger.create("callback"),ce.notifyParent(e,this._settings.iframeNotifyParentOrigin)}};var ct=500,lt=1e3,le=class extends se{constructor({popupWindowTarget:t=Re,popupWindowFeatures:r={},popupSignal:i,popupAbortOnClose:s}){super();this._logger=new g("PopupWindow");let n=oe.center({...xe,...r});this._window=window.open(void 0,t,oe.serialize(n)),this.abortOnClose=!!s,i&&i.addEventListener("abort",()=>{var o;this._abort.raise(new Error((o=i.reason)!=null?o:"Popup aborted"))}),r.closePopupWindowAfterInSeconds&&r.closePopupWindowAfterInSeconds>0&&setTimeout(()=>{if(!this._window||typeof this._window.closed!="boolean"||this._window.closed){this._abort.raise(new Error("Popup blocked by user"));return}this.close()},r.closePopupWindowAfterInSeconds*lt)}async navigate(t){var s;(s=this._window)==null||s.focus();let r=setInterval(()=>{(!this._window||this._window.closed)&&(this._logger.debug("Popup closed by user or isolated by redirect"),i(),this._disposeHandlers.delete(i),this.abortOnClose&&this._abort.raise(new Error("Popup closed by user")))},ct),i=()=>clearInterval(r);return this._disposeHandlers.add(i),await super.navigate(t)}close(){this._window&&(this._window.closed||(this._window.close(),this._abort.raise(new Error("Popup closed")))),this._window=null}static notifyOpener(t,r){super._notifyParent(window.opener,t,r),!r&&!window.opener&&window.close()}};var fe=class{constructor(e){this._settings=e;this._logger=new g("PopupNavigator")}async prepare({popupWindowFeatures:e=this._settings.popupWindowFeatures,popupWindowTarget:t=this._settings.popupWindowTarget,popupSignal:r,popupAbortOnClose:i}){return new le({popupWindowFeatures:e,popupWindowTarget:t,popupSignal:r,popupAbortOnClose:i})}async callback(e,{keepOpen:t=!1}){this._logger.create("callback"),le.notifyOpener(e,t)}};var we=class{constructor(e){this._settings=e;this._logger=new g("RedirectNavigator")}async prepare({redirectMethod:e=this._settings.redirectMethod,redirectTarget:t=this._settings.redirectTarget}){var n;this._logger.create("prepare");let r=window.self;t==="top"&&(r=(n=window.top)!=null?n:window.self);let i=r.location[e].bind(r.location),s;return{navigate:async o=>(this._logger.create("navigate"),await new Promise((c,d)=>{s=d,window.addEventListener("pageshow",()=>c(window.location.href)),i(o.url)})),close:()=>{this._logger.create("close"),s==null||s(new Error("Redirect aborted")),r.stop()}}}async callback(){}};var Se=class extends Y{constructor(t){super({expiringNotificationTimeInSeconds:t.accessTokenExpiringNotificationTimeInSeconds});this._logger=new g("UserManagerEvents");this._userLoaded=new b("User loaded");this._userUnloaded=new b("User unloaded");this._silentRenewError=new b("Silent renew error");this._userSignedIn=new b("User signed in");this._userSignedOut=new b("User signed out");this._userSessionChanged=new b("User session changed")}async load(t,r=!0){await super.load(t),r&&await this._userLoaded.raise(t)}async unload(){await super.unload(),await this._userUnloaded.raise()}addUserLoaded(t){return this._userLoaded.addHandler(t)}removeUserLoaded(t){return this._userLoaded.removeHandler(t)}addUserUnloaded(t){return this._userUnloaded.addHandler(t)}removeUserUnloaded(t){return this._userUnloaded.removeHandler(t)}addSilentRenewError(t){return this._silentRenewError.addHandler(t)}removeSilentRenewError(t){return this._silentRenewError.removeHandler(t)}async _raiseSilentRenewError(t){await this._silentRenewError.raise(t)}addUserSignedIn(t){return this._userSignedIn.addHandler(t)}removeUserSignedIn(t){this._userSignedIn.removeHandler(t)}async _raiseUserSignedIn(){await this._userSignedIn.raise()}addUserSignedOut(t){return this._userSignedOut.addHandler(t)}removeUserSignedOut(t){this._userSignedOut.removeHandler(t)}async _raiseUserSignedOut(){await this._userSignedOut.raise()}addUserSessionChanged(t){return this._userSessionChanged.addHandler(t)}removeUserSessionChanged(t){this._userSessionChanged.removeHandler(t)}async _raiseUserSessionChanged(){await this._userSessionChanged.raise()}};var be=class{constructor(e){this._userManager=e;this._logger=new g("SilentRenewService");this._isStarted=!1;this._retryTimer=new f("Retry Silent Renew");this._timeoutRetryCount=0;this._tokenExpiring=async()=>{let e=this._logger.create("_tokenExpiring");try{await this._userManager.signinSilent(),this._timeoutRetryCount=0,e.debug("silent token renewal successful")}catch(t){if(t instanceof E){this._timeoutRetryCount++;let r=this._userManager.settings.maxSilentRenewTimeoutRetries;if(r!==void 0&&this._timeoutRetryCount>r){e.error(`Timeout retry limit reached (${this._timeoutRetryCount} > ${r}), raising silentRenewError:`,t),this._timeoutRetryCount=0,await this._userManager.events._raiseSilentRenewError(t);return}e.warn(`ErrorTimeout from signinSilent (attempt ${this._timeoutRetryCount}), retry in 5s:`,t),this._retryTimer.init(5);return}e.error("Error from signinSilent:",t),this._timeoutRetryCount=0,await this._userManager.events._raiseSilentRenewError(t)}}}async start(){let e=this._logger.create("start");if(!this._isStarted){this._isStarted=!0,this._userManager.events.addAccessTokenExpiring(this._tokenExpiring),this._retryTimer.addHandler(this._tokenExpiring);try{await this._userManager.getUser()}catch(t){e.error("getUser error",t)}}}stop(){this._isStarted&&(this._retryTimer.cancel(),this._retryTimer.removeHandler(this._tokenExpiring),this._userManager.events.removeAccessTokenExpiring(this._tokenExpiring),this._isStarted=!1)}};var ye=class{constructor(e){this.refresh_token=e.refresh_token,this.id_token=e.id_token,this.session_state=e.session_state,this.scope=e.scope,this.profile=e.profile,this.data=e.state}};var Te=class{constructor(e,t,r,i){this._logger=new g("UserManager");this.settings=new ne(e),this._client=new ae(e),this._redirectNavigator=t!=null?t:new we(this.settings),this._popupNavigator=r!=null?r:new fe(this.settings),this._iframeNavigator=i!=null?i:new _e(this.settings),this._events=new Se(this.settings),this._silentRenewService=new be(this),this.settings.automaticSilentRenew&&this.startSilentRenew(),this._sessionMonitor=null,this.settings.monitorSession&&(this._sessionMonitor=new ie(this))}get events(){return this._events}get metadataService(){return this._client.metadataService}async getUser(e=!1){let t=this._logger.create("getUser"),r=await this._loadUser();return r?(t.info("user loaded"),await this._events.load(r,e),r):(t.info("user not found in storage"),null)}async removeUser(){let e=this._logger.create("removeUser");await this.storeUser(null),e.info("user removed from storage"),await this._events.unload()}async signinRedirect(e={}){var n;this._logger.create("signinRedirect");let{redirectMethod:t,...r}=e,i;(n=this.settings.dpop)!=null&&n.bind_authorization_code&&(i=await this.generateDPoPJkt(this.settings.dpop));let s=await this._redirectNavigator.prepare({redirectMethod:t});await this._signinStart({request_type:"si:r",dpopJkt:i,...r},s)}async signinRedirectCallback(e=window.location.href){let t=this._logger.create("signinRedirectCallback"),r=await this._signinEnd(e);return r.profile&&r.profile.sub?t.info("success, signed in subject",r.profile.sub):t.info("no subject"),r}async signinResourceOwnerCredentials({username:e,password:t,skipUserInfo:r=!1}){let i=this._logger.create("signinResourceOwnerCredential"),s=await this._client.processResourceOwnerPasswordCredentials({username:e,password:t,skipUserInfo:r,extraTokenParams:this.settings.extraTokenParams});i.debug("got signin response");let n=await this._buildUser(s);return n.profile&&n.profile.sub?i.info("success, signed in subject",n.profile.sub):i.info("no subject"),n}async signinPopup(e={}){var p;let t=this._logger.create("signinPopup"),r;(p=this.settings.dpop)!=null&&p.bind_authorization_code&&(r=await this.generateDPoPJkt(this.settings.dpop));let{popupWindowFeatures:i,popupWindowTarget:s,popupSignal:n,popupAbortOnClose:o,...a}=e,c=this.settings.popup_redirect_uri;c||t.throw(new Error("No popup_redirect_uri configured"));let d=await this._popupNavigator.prepare({popupWindowFeatures:i,popupWindowTarget:s,popupSignal:n,popupAbortOnClose:o}),u=await this._signin({request_type:"si:p",redirect_uri:c,display:"popup",dpopJkt:r,...a},d);return u&&(u.profile&&u.profile.sub?t.info("success, signed in subject",u.profile.sub):t.info("no subject")),u}async signinPopupCallback(e=window.location.href,t=!1){let r=this._logger.create("signinPopupCallback");await this._popupNavigator.callback(e,{keepOpen:t}),r.info("success")}async signinSilent(e={}){var d,u;let t=this._logger.create("signinSilent"),{silentRequestTimeoutInSeconds:r,...i}=e,s=await this._loadUser();if(!e.forceIframeAuth&&(s!=null&&s.refresh_token)){t.debug("using refresh token");let p=new ye(s);return await this._useRefreshToken({state:p,redirect_uri:i.redirect_uri,resource:i.resource,extraTokenParams:i.extraTokenParams,timeoutInSeconds:r})}let n;(d=this.settings.dpop)!=null&&d.bind_authorization_code&&(n=await this.generateDPoPJkt(this.settings.dpop));let o=this.settings.silent_redirect_uri;o||t.throw(new Error("No silent_redirect_uri configured"));let a;s&&this.settings.validateSubOnSilentRenew&&(t.debug("subject prior to silent renew:",s.profile.sub),a=s.profile.sub);let c=await this._iframeNavigator.prepare({silentRequestTimeoutInSeconds:r});return s=await this._signin({request_type:"si:s",redirect_uri:o,prompt:"none",id_token_hint:this.settings.includeIdTokenInSilentRenew?s==null?void 0:s.id_token:void 0,dpopJkt:n,...i},c,a),s&&((u=s.profile)!=null&&u.sub?t.info("success, signed in subject",s.profile.sub):t.info("no subject")),s}async _useRefreshToken(e){let t=await this._client.useRefreshToken({timeoutInSeconds:this.settings.silentRequestTimeoutInSeconds,...e}),r=new L({...e.state,...t});return await this.storeUser(r),await this._events.load(r),r}async signinSilentCallback(e=window.location.href){let t=this._logger.create("signinSilentCallback");await this._iframeNavigator.callback(e),t.info("success")}async signinCallback(e=window.location.href){let{state:t}=await this._client.readSigninResponseState(e);switch(t.request_type){case"si:r":return await this.signinRedirectCallback(e);case"si:p":await this.signinPopupCallback(e);break;case"si:s":await this.signinSilentCallback(e);break;default:throw new Error("invalid request_type in state")}}async signoutCallback(e=window.location.href,t=!1){let{state:r}=await this._client.readSignoutResponseState(e);if(r)switch(r.request_type){case"so:r":return await this.signoutRedirectCallback(e);case"so:p":await this.signoutPopupCallback(e,t);break;case"so:s":await this.signoutSilentCallback(e);break;default:throw new Error("invalid request_type in state")}}async querySessionStatus(e={}){let t=this._logger.create("querySessionStatus"),{silentRequestTimeoutInSeconds:r,...i}=e,s=this.settings.silent_redirect_uri;s||t.throw(new Error("No silent_redirect_uri configured"));let n=await this._loadUser(),o=await this._iframeNavigator.prepare({silentRequestTimeoutInSeconds:r}),a=await this._signinStart({request_type:"si:s",redirect_uri:s,prompt:"none",id_token_hint:this.settings.includeIdTokenInSilentRenew?n==null?void 0:n.id_token:void 0,response_type:this.settings.query_status_response_type,scope:"openid",skipUserInfo:!0,...i},o);try{let c={},d=await this._client.processSigninResponse(a.url,c);return t.debug("got signin response"),d.session_state&&d.profile.sub?(t.info("success for subject",d.profile.sub),{session_state:d.session_state,sub:d.profile.sub}):(t.info("success, user not authenticated"),null)}catch(c){if(this.settings.monitorAnonymousSession&&c instanceof w)switch(c.error){case"login_required":case"consent_required":case"interaction_required":case"account_selection_required":return t.info("success for anonymous user"),{session_state:c.session_state}}throw c}}async _signin(e,t,r){let i=await this._signinStart(e,t);return await this._signinEnd(i.url,r)}async _signinStart(e,t){let r=this._logger.create("_signinStart");try{let i=await this._client.createSigninRequest(e);return r.debug("got signin request"),await t.navigate({url:i.url,state:i.state.id,response_mode:i.state.response_mode,scriptOrigin:this.settings.iframeScriptOrigin})}catch(i){throw r.debug("error after preparing navigator, closing navigator window"),t.close(),i}}async _signinEnd(e,t){let r=this._logger.create("_signinEnd"),i={},s=await this._client.processSigninResponse(e,i);return r.debug("got signin response"),await this._buildUser(s,t)}async _buildUser(e,t){let r=this._logger.create("_buildUser"),i=new L(e);if(t){if(t!==i.profile.sub)throw r.debug("current user does not match user returned from signin. sub from signin:",i.profile.sub),new w({...e,error:"login_required"});r.debug("current user matches user returned from signin")}return await this.storeUser(i),r.debug("user stored"),await this._events.load(i),i}async signoutRedirect(e={}){let t=this._logger.create("signoutRedirect"),{redirectMethod:r,...i}=e,s=await this._redirectNavigator.prepare({redirectMethod:r});await this._signoutStart({request_type:"so:r",post_logout_redirect_uri:this.settings.post_logout_redirect_uri,...i},s),t.info("success")}async signoutRedirectCallback(e=window.location.href){let t=this._logger.create("signoutRedirectCallback"),r=await this._signoutEnd(e);return t.info("success"),r}async signoutPopup(e={}){let t=this._logger.create("signoutPopup"),{popupWindowFeatures:r,popupWindowTarget:i,popupSignal:s,...n}=e,o=this.settings.popup_post_logout_redirect_uri,a=await this._popupNavigator.prepare({popupWindowFeatures:r,popupWindowTarget:i,popupSignal:s});await this._signout({request_type:"so:p",post_logout_redirect_uri:o,state:o==null?void 0:{},...n},a),t.info("success")}async signoutPopupCallback(e=window.location.href,t=!1){let r=this._logger.create("signoutPopupCallback");await this._popupNavigator.callback(e,{keepOpen:t}),r.info("success")}async _signout(e,t){let r=await this._signoutStart(e,t);return await this._signoutEnd(r.url)}async _signoutStart(e={},t){var i;let r=this._logger.create("_signoutStart");try{let s=await this._loadUser();r.debug("loaded current user from storage"),this.settings.revokeTokensOnSignout&&await this._revokeInternal(s);let n=e.id_token_hint||s&&s.id_token;n&&(r.debug("setting id_token_hint in signout request"),e.id_token_hint=n),await this.removeUser(),r.debug("user removed, creating signout request");let o=await this._client.createSignoutRequest(e);return r.debug("got signout request"),await t.navigate({url:o.url,state:(i=o.state)==null?void 0:i.id,scriptOrigin:this.settings.iframeScriptOrigin})}catch(s){throw r.debug("error after preparing navigator, closing navigator window"),t.close(),s}}async _signoutEnd(e){let t=this._logger.create("_signoutEnd"),r=await this._client.processSignoutResponse(e);return t.debug("got signout response"),r}async signoutSilent(e={}){var a;let t=this._logger.create("signoutSilent"),{silentRequestTimeoutInSeconds:r,...i}=e,s=this.settings.includeIdTokenInSilentSignout?(a=await this._loadUser())==null?void 0:a.id_token:void 0,n=this.settings.popup_post_logout_redirect_uri,o=await this._iframeNavigator.prepare({silentRequestTimeoutInSeconds:r});await this._signout({request_type:"so:s",post_logout_redirect_uri:n,id_token_hint:s,...i},o),t.info("success")}async signoutSilentCallback(e=window.location.href){let t=this._logger.create("signoutSilentCallback");await this._iframeNavigator.callback(e),t.info("success")}async revokeTokens(e){let t=await this._loadUser();await this._revokeInternal(t,e)}async _revokeInternal(e,t=this.settings.revokeTokenTypes){let r=this._logger.create("_revokeInternal");if(!e)return;let i=t.filter(s=>typeof e[s]=="string");if(!i.length){r.debug("no need to revoke due to no token(s)");return}for(let s of i)await this._client.revokeToken(e[s],s),r.info(`${s} revoked successfully`),s!=="access_token"&&(e[s]=null);await this.storeUser(e),r.debug("user stored"),await this._events.load(e)}startSilentRenew(){this._logger.create("startSilentRenew"),this._silentRenewService.start()}stopSilentRenew(){this._silentRenewService.stop()}get _userStoreKey(){return`user:${this.settings.authority}:${this.settings.client_id}`}async _loadUser(){let e=this._logger.create("_loadUser"),t=await this.settings.userStore.get(this._userStoreKey);return t?(e.debug("user storageString loaded"),L.fromStorageString(t)):(e.debug("no user storageString"),null)}async storeUser(e){let t=this._logger.create("storeUser");if(e){t.debug("storing user");let r=e.toStorageString();await this.settings.userStore.set(this._userStoreKey,r)}else this._logger.debug("removing user"),await this.settings.userStore.remove(this._userStoreKey),this.settings.dpop&&await this.settings.dpop.store.remove(this.settings.client_id)}async clearStaleState(){await this._client.clearStaleState()}async dpopProof(e,t,r,i){var n,o;let s=await((o=(n=this.settings.dpop)==null?void 0:n.store)==null?void 0:o.get(this.settings.client_id));if(s)return await m.generateDPoPProof({url:e,accessToken:t==null?void 0:t.access_token,httpMethod:r,keyPair:s.keys,nonce:i})}async generateDPoPJkt(e){let t=await e.store.get(this.settings.client_id);if(!t){let r=await m.generateDPoPKeys();t=new W(r),await e.store.set(this.settings.client_id,t)}return await m.generateDPoPJkt(t.keys)}};var Oe="3.5.0";var qe=Oe;var ve=class{constructor(){this._dbName="oidc";this._storeName="dpop"}async set(e,t){await(await this.createStore(this._dbName,this._storeName))("readwrite",i=>(i.put(t,e),this.promisifyRequest(i.transaction)))}async get(e){return await(await this.createStore(this._dbName,this._storeName))("readonly",r=>this.promisifyRequest(r.get(e)))}async remove(e){let t=await this.get(e);return await(await this.createStore(this._dbName,this._storeName))("readwrite",i=>this.promisifyRequest(i.delete(e))),t}async getAllKeys(){return await(await this.createStore(this._dbName,this._storeName))("readonly",t=>this.promisifyRequest(t.getAllKeys()))}promisifyRequest(e){return new Promise((t,r)=>{e.oncomplete=e.onsuccess=()=>t(e.result),e.onabort=e.onerror=()=>r(e.error)})}async createStore(e,t){let r=indexedDB.open(e);r.onupgradeneeded=()=>r.result.createObjectStore(t);let i=await this.promisifyRequest(r);return async(s,n)=>{let a=i.transaction(t,s).objectStore(t);return await n(a)}}};return Qe(gt);})(); +//# sourceMappingURL=oidc-client-ts.min.js.map diff --git a/src/App.tsx b/src/App.tsx index 70f0cde4..6f1f9daa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -188,13 +188,16 @@ interface AppState { class App extends React.Component { private readonly auth?: AuthManager + private reauthInProgress = false + private unsubscribeAuthorization?: () => void private readonly handleDICOMwebError = ( error: dwc.api.DICOMwebClientError, serverSettings: ServerSettings, ): void => { if (error.status === 401) { - this.signIn() + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.ensureAuthorized() } else if (error.status === 403) { // eslint-disable-next-line @typescript-eslint/no-floating-promises NotificationMiddleware.onError( @@ -349,9 +352,9 @@ class App extends React.Component { tmpClient.updateHeaders(this.state.clients.default.headers) // Re-apply auth so the new client has the current token (avoids 401 when switching mid-session) if (this.auth != null && this.state.user != null) { - const token = await this.auth.getAuthorization() - if (token != null) { - tmpClient.updateHeaders({ Authorization: `Bearer ${token}` }) + const authorization = await this.auth.getAuthorization() + if (authorization != null) { + tmpClient.updateHeaders({ Authorization: authorization }) } } /** @@ -368,48 +371,86 @@ class App extends React.Component { }) } + applyAuthorization = (authorization: string): void => { + for (const key in this.state.clients) { + this.state.clients[key].updateHeaders({ Authorization: authorization }) + } + for (const key in this.state.defaultClients) { + this.state.defaultClients[key].updateHeaders({ + Authorization: authorization, + }) + } + } + /** * Handle successful authentication event. * * Authorizes the DICOMweb client to access the DICOMweb server and directs - * the user back to the App. - * - * @param user - Information about the user - * @param authorization - Value of the "Authorization" HTTP header field + * the user back to the pre-login route (via OIDC state). */ handleSignIn = ({ user, authorization, + returnUrl, }: { user: User authorization: string + returnUrl?: string }): void => { - for (const key in this.state.clients) { - const client = this.state.clients[key] - client.updateHeaders({ Authorization: authorization }) + this.applyAuthorization(authorization) + this.setState({ user }) + + if (returnUrl != null && returnUrl !== '') { + const current = `${window.location.pathname}${window.location.search}` + if (returnUrl !== current) { + window.location.assign(returnUrl) + } } - const storedPath = window.localStorage.getItem('slim_path') - const storedSearch = window.localStorage.getItem('slim_search') - if (storedPath !== null && storedPath !== '') { - const currentPath = window.location.pathname - if (storedPath !== currentPath) { - let path = storedPath - if (storedSearch !== null && storedSearch !== '') { - path += storedSearch - } - window.location.href = path + } + + /** + * Recover from an expired/missing access token without losing the route. + * Tries silent renew first; falls back to interactive redirect with returnUrl. + */ + ensureAuthorized = async (): Promise => { + if (this.auth == null || this.reauthInProgress) { + return + } + this.reauthInProgress = true + try { + const authorization = await this.auth.renewAuthorization() + if (authorization != null) { + this.applyAuthorization(authorization) + return } + console.info('silent renew unavailable; starting interactive sign-in') + await this.auth.signIn({ + onSignIn: this.handleSignIn, + returnUrl: `${window.location.pathname}${window.location.search}`, + }) + } catch (error) { + console.error(error) + // eslint-disable-next-line @typescript-eslint/no-floating-promises + NotificationMiddleware.onError( + NotificationMiddlewareContext.AUTH, + new CustomError( + errorTypes.AUTHENTICATION, + 'Could not renew authorization.', + ), + ) + } finally { + this.reauthInProgress = false } - window.localStorage.removeItem('slim_path') - window.localStorage.removeItem('slim_search') - this.setState({ user }) } signIn(): void { if (this.auth !== undefined) { console.info('try to sign in user') this.auth - .signIn({ onSignIn: this.handleSignIn }) + .signIn({ + onSignIn: this.handleSignIn, + returnUrl: `${window.location.pathname}${window.location.search}`, + }) .then(() => { console.info('sign-in was successful') this.setState({ @@ -443,12 +484,6 @@ class App extends React.Component { } componentDidMount(): void { - const path = window.localStorage.getItem('slim_path') - if (path === null || path === undefined || path === '') { - window.localStorage.setItem('slim_path', window.location.pathname) - window.localStorage.setItem('slim_search', window.location.search) - } - // Restore cached server selection if it exists const cachedServerUrl = window.localStorage.getItem('slim_selected_server') if ( @@ -456,12 +491,25 @@ class App extends React.Component { cachedServerUrl !== undefined && cachedServerUrl !== '' ) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises this.handleServerSelection({ url: cachedServerUrl }) } + if (this.auth != null) { + this.unsubscribeAuthorization = this.auth.onAuthorizationChange( + (authorization) => { + this.applyAuthorization(authorization) + }, + ) + } + this.signIn() } + componentWillUnmount(): void { + this.unsubscribeAuthorization?.() + } + render(): React.ReactNode { const appInfo = { name: this.props.name, @@ -486,16 +534,10 @@ class App extends React.Component { let isLogoutPossible = false let onLogout: () => void - if ( - // eslint-disable-next-line @typescript-eslint/prefer-optional-chain - this.props.config.oidc != null && - this.props.config.oidc.endSessionEndpoint != null - ) { + if (this.auth != null) { onLogout = (): void => { - if (this.auth != null) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.auth.signOut() - } + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.auth?.signOut() } isLogoutPossible = true } else { diff --git a/src/auth/OidcManager.tsx b/src/auth/OidcManager.tsx index d1d4fedc..fca8f530 100644 --- a/src/auth/OidcManager.tsx +++ b/src/auth/OidcManager.tsx @@ -1,12 +1,25 @@ -import { type User as UserData, UserManager } from 'oidc-client' +import { + type User as UserData, + UserManager, + type UserManagerSettings, +} from 'oidc-client-ts' import type { OidcSettings } from '../AppConfig' import NotificationMiddleware, { NotificationMiddlewareContext, } from '../services/NotificationMiddleware' import { CustomError, errorTypes } from '../utils/CustomError' -import { isAuthorizationCodeInUrl } from '../utils/url' -import type { AuthManager, SignInCallback, User } from '.' +import { isAuthorizationCodeInUrl, joinUrl } from '../utils/url' +import type { + AuthManager, + AuthorizationCallback, + SignInCallback, + User, +} from '.' + +interface ReturnUrlState { + returnUrl?: string +} const createUser = (userData: UserData | null): User => { let profile: UserData['profile'] | undefined @@ -44,85 +57,163 @@ const createUser = (userData: UserData | null): User => { } } +const authorizationFromUser = (userData: UserData): string => { + const tokenType = userData.token_type || 'Bearer' + return `${tokenType} ${userData.access_token}` +} + +const clearAuthParamsFromUrl = (): void => { + const url = new URL(window.location.href) + const authParams = [ + 'code', + 'state', + 'session_state', + 'iss', + 'id_token', + 'access_token', + 'token_type', + 'expires_in', + 'scope', + 'error', + 'error_description', + ] + for (const key of authParams) { + url.searchParams.delete(key) + } + // Implicit / hybrid responses put tokens in the hash fragment. + url.hash = '' + const cleaned = `${url.pathname}${url.search}` + window.history.replaceState({}, document.title, cleaned) +} + +const readReturnUrl = (userData: UserData): string | undefined => { + const state = userData.state as ReturnUrlState | string | null | undefined + if (state == null) { + return undefined + } + if (typeof state === 'string') { + return state || undefined + } + if (typeof state.returnUrl === 'string' && state.returnUrl !== '') { + return state.returnUrl + } + return undefined +} + +const currentReturnUrl = (): string => { + return `${window.location.pathname}${window.location.search}` +} + export default class OidcManager implements AuthManager { private _oidc: UserManager + private readonly _ready: Promise + private readonly _authorizationListeners = new Set() - constructor(baseUri: string, settings: OidcSettings) { - let responseType = 'code' - if (settings.grantType !== undefined) { - if (settings.grantType === 'implicit') { - responseType = 'id_token token' - } - } - this._oidc = new UserManager({ + constructor(appUri: string, settings: OidcSettings) { + const isImplicit = settings.grantType === 'implicit' + const responseType = isImplicit ? 'id_token token' : 'code' + const redirectUri = appUri + const silentRedirectUri = joinUrl('silent-renew.html', appUri) + const postLogoutRedirectUri = joinUrl('logout', appUri) + + const baseSettings: UserManagerSettings = { authority: settings.authority, client_id: settings.clientId, - redirect_uri: baseUri, + redirect_uri: redirectUri, + silent_redirect_uri: silentRedirectUri, + post_logout_redirect_uri: postLogoutRedirectUri, scope: settings.scope, response_type: responseType, loadUserInfo: true, automaticSilentRenew: true, - revokeAccessTokenOnSignout: true, - post_logout_redirect_uri: `${baseUri}/logout`, + revokeTokensOnSignout: true, + } + + // PKCE is the oidc-client-ts default for code flow; leave it enabled. + // Implicit grant does not use PKCE. + this._oidc = new UserManager(baseSettings) + this._wireInternalEvents() + this._ready = this._applyOptionalMetadata(baseSettings, settings) + } + + private _wireInternalEvents(): void { + this._oidc.events.addUserLoaded((userData) => { + this._notifyAuthorization(authorizationFromUser(userData)) }) - if ( - settings.endSessionEndpoint !== null && - settings.endSessionEndpoint !== undefined - ) { - /* - * Unfortunately, the end session endpoint alone cannot be provided to - * the construction of UserManager and the other metadata parameters - * would need to be provided as well. However, configuring all of them - * individually would not be desirable and they will be automatically - * determined anyways. Therefore, we first construct an object, get the - * metadata, update the metadata, and then reconstruct an object with the - * updated metadata. - */ - this._oidc.metadataService - .getMetadata() - .then((metadata) => { - if ( - settings.endSessionEndpoint !== null && - settings.endSessionEndpoint !== undefined - ) { - metadata.end_session_endpoint = settings.endSessionEndpoint - this._oidc = new UserManager({ - authority: settings.authority, - client_id: settings.clientId, - redirect_uri: baseUri, - scope: settings.scope, - response_type: responseType, - loadUserInfo: true, - automaticSilentRenew: true, - revokeAccessTokenOnSignout: true, - post_logout_redirect_uri: `${baseUri}/logout`, - metadata, - }) - } - }) - .catch((error) => { - console.error( - 'failed to get metadata from authorization server: ', - error, - ) - }) + } + + private _notifyAuthorization(authorization: string): void { + for (const listener of this._authorizationListeners) { + listener(authorization) + } + } + + private async _applyOptionalMetadata( + baseSettings: UserManagerSettings, + settings: OidcSettings, + ): Promise { + const needsMetadataPatch = + (settings.endSessionEndpoint != null && + settings.endSessionEndpoint !== '') || + (settings.authorizationEndpoint != null && + settings.authorizationEndpoint !== '') + + if (!needsMetadataPatch) { + return + } + + try { + const metadata = await this._oidc.metadataService.getMetadata() + if ( + settings.endSessionEndpoint != null && + settings.endSessionEndpoint !== '' + ) { + metadata.end_session_endpoint = settings.endSessionEndpoint + } + if ( + settings.authorizationEndpoint != null && + settings.authorizationEndpoint !== '' + ) { + metadata.authorization_endpoint = settings.authorizationEndpoint + } + this._oidc = new UserManager({ + ...baseSettings, + metadata, + }) + this._wireInternalEvents() + } catch (error) { + console.error('failed to get metadata from authorization server: ', error) } } + private async _ensureReady(): Promise { + await this._ready + return this._oidc + } + /** * Sign-in to authenticate the user and obtain authorization. */ signIn = async ({ onSignIn, + returnUrl, }: { onSignIn?: SignInCallback + returnUrl?: string }): Promise => { + const oidc = await this._ensureReady() + const handleSignIn = (userData: UserData): void => { const user = createUser(userData) - const authorization = `${userData.token_type} ${userData.access_token}` + const authorization = authorizationFromUser(userData) + const resolvedReturnUrl = readReturnUrl(userData) if (onSignIn != null) { console.info('handling sign-in using provided callback function') - onSignIn({ user, authorization }) + onSignIn({ + user, + authorization, + returnUrl: resolvedReturnUrl, + }) } else { console.warn('no callback function was provided to handle sign-in') } @@ -130,24 +221,27 @@ export default class OidcManager implements AuthManager { if (isAuthorizationCodeInUrl(window.location)) { /* Handle the callback from the authorization server: extract the code - * from the callback URL, obtain user information and the access token - * for the DICOMweb server. + * (or implicit tokens) from the callback URL, obtain user information + * and the access token for the DICOMweb server. */ console.info('obtaining authorization') - const userData = await this._oidc.signinCallback() - if (userData != null) { - console.info('obtained user data: ', userData) - handleSignIn(userData) - } + const userData = await oidc.signinRedirectCallback() + clearAuthParamsFromUrl() + console.info('obtained user data: ', userData) + handleSignIn(userData) } else { /* Redirect to the authorization server to authenticate the user * and authorize the application to obtain user information and access * the DICOMweb server. */ - const userData = await this._oidc.getUser() + const userData = await oidc.getUser() if (userData === null || userData === undefined || userData.expired) { console.info('authenticating user') - await this._oidc.signinRedirect() + await oidc.signinRedirect({ + state: { + returnUrl: returnUrl ?? currentReturnUrl(), + }, + }) } else { console.info('user has already been authenticated') handleSignIn(userData) @@ -157,46 +251,87 @@ export default class OidcManager implements AuthManager { /** * Sign-out to revoke authorization. + * Falls back to local session clear when the IdP has no end-session endpoint. */ signOut = async (): Promise => { console.log('signing out user and revoking authorization') - return await this._oidc.signoutRedirect() + const oidc = await this._ensureReady() + try { + const metadata = await oidc.metadataService.getMetadata() + if ( + metadata.end_session_endpoint == null || + metadata.end_session_endpoint === '' + ) { + await oidc.removeUser() + window.location.assign(joinUrl('logout', oidc.settings.redirect_uri)) + return + } + await oidc.signoutRedirect() + } catch (error) { + console.error('sign-out redirect failed; clearing local session', error) + await oidc.removeUser() + window.location.assign(joinUrl('logout', oidc.settings.redirect_uri)) + } } /** * Get authorization. Requires prior sign-in. + * Returns a full HTTP Authorization header value (e.g. "Bearer …"). */ getAuthorization = async (): Promise => { - return await this._oidc.getUser().then((userData) => { - if (userData !== null && userData !== undefined) { - return userData.access_token - } else { - NotificationMiddleware.onError( - NotificationMiddlewareContext.AUTH, - new CustomError( - errorTypes.AUTHENTICATION, - 'Failed to obtain user profile.', - ), - ) - } - }) + const oidc = await this._ensureReady() + const userData = await oidc.getUser() + if (userData !== null && userData !== undefined && !userData.expired) { + return authorizationFromUser(userData) + } + NotificationMiddleware.onError( + NotificationMiddlewareContext.AUTH, + new CustomError( + errorTypes.AUTHENTICATION, + 'Failed to obtain user profile.', + ), + ) + return undefined } /** * Get user information. Requires prior sign-in. */ getUser = async (): Promise => { - return await this._oidc.getUser().then((userData) => { - if (userData === null || userData === undefined) { - NotificationMiddleware.onError( - NotificationMiddlewareContext.AUTH, - new CustomError( - errorTypes.AUTHENTICATION, - 'Failed to obtain user information.', - ), - ) + const oidc = await this._ensureReady() + const userData = await oidc.getUser() + if (userData === null || userData === undefined) { + NotificationMiddleware.onError( + NotificationMiddlewareContext.AUTH, + new CustomError( + errorTypes.AUTHENTICATION, + 'Failed to obtain user information.', + ), + ) + } + return createUser(userData) + } + + renewAuthorization = async (): Promise => { + const oidc = await this._ensureReady() + try { + const userData = await oidc.signinSilent() + if (userData == null || userData.expired) { + return undefined } - return createUser(userData) - }) + const authorization = authorizationFromUser(userData) + this._notifyAuthorization(authorization) + return authorization + } catch (error) { + console.warn('silent authorization renew failed', error) + return undefined + } + } + + onAuthorizationChange = (callback: AuthorizationCallback): (() => void) => { + this._authorizationListeners.add(callback) + return () => { + this._authorizationListeners.delete(callback) + } } } diff --git a/src/auth/index.d.ts b/src/auth/index.d.ts index 2180fb11..917d6849 100644 --- a/src/auth/index.d.ts +++ b/src/auth/index.d.ts @@ -1,19 +1,33 @@ export type SignInCallback = ({ user, authorization, + returnUrl, }: { user: User authorization: string + returnUrl?: string }) => void +export type AuthorizationCallback = (authorization: string) => void + export interface User { name: string | undefined email: string | undefined } export interface AuthManager { - signIn: ({ onSignIn }: { onSignIn: SignInCallback }) => Promise + signIn: ({ + onSignIn, + returnUrl, + }: { + onSignIn?: SignInCallback + returnUrl?: string + }) => Promise signOut: () => Promise getAuthorization: () => Promise getUser: () => Promise + /** Attempt silent token renewal; returns a full Authorization header value. */ + renewAuthorization: () => Promise + /** Subscribe to authorization updates (e.g. after silent renew). */ + onAuthorizationChange: (callback: AuthorizationCallback) => () => void } From cfb677ee22bb92e358155ce2620fc344d21b6719 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Mon, 3 Aug 2026 10:32:47 -0300 Subject: [PATCH 2/4] fix: keep oidc-client for non-breaking auth hardening Drop the oidc-client-ts migration that broke grantType: "implicit". Retain state-based deep-link restore, safer 401 renew, and silent renew via the existing redirect_uri with an iframe bootstrap path. --- README.md | 10 +-- package.json | 3 +- pnpm-lock.yaml | 35 ++++++---- public/silent-renew.html | 31 --------- public/vendor/oidc-client-ts.min.js | 2 - src/auth/OidcManager.tsx | 76 +++++++++++++++++----- src/auth/__tests__/isSafeReturnUrl.test.ts | 33 ++++++++++ src/index.tsx | 55 +++++++++++----- 8 files changed, 156 insertions(+), 89 deletions(-) delete mode 100644 public/silent-renew.html delete mode 100644 public/vendor/oidc-client-ts.min.js create mode 100644 src/auth/__tests__/isSafeReturnUrl.test.ts diff --git a/README.md b/README.md index 1ceef77e..dea5a890 100644 --- a/README.md +++ b/README.md @@ -373,13 +373,13 @@ window.config = { #### OAuth 2.0 configuration -Create an [OIDC client ID for a web / single-page application](https://developers.google.com/identity/sign-in/web/sign-in) and register the app origin as an authorized redirect URI (same value as Slim's `path` / app root). +Create an [OIDC client ID for web application](https://developers.google.com/identity/sign-in/web/sign-in) and register the app origin as an authorized redirect URI (same value as Slim's `path` / app root). -**Preferred grant:** omit `grantType` (or set it to authorization code). Slim uses the Authorization Code flow with PKCE via `oidc-client-ts`. +Existing configs continue to work without changes: +- `grantType: "implicit"` (common for Google Cloud Healthcare setups) remains supported +- Omitting `grantType` uses the authorization code response type (`code`) -**Legacy / compatibility:** `grantType: "implicit"` remains fully supported for existing deployments (including many Google Cloud Healthcare setups). No public config changes are required when upgrading Slim. - -Silent token renewal uses `silent-renew.html` at the app root (do not remove it from the deployed `public/` assets). The deep link the user was on before login is restored through the OIDC `state` parameter (not `localStorage`). +Deep links are restored after login through the OIDC `state` parameter (not `localStorage`). Silent token renewal reuses the same registered redirect URI (no additional IdP redirect URI is required). ## Development diff --git a/package.json b/package.json index f8385578..b4e41eb8 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "fmt": "biome format --write .", "format": "biome format --write .", "test": "craco test --watchAll=false", - "sync:oidc-vendor": "mkdir -p public/vendor && cp node_modules/oidc-client-ts/dist/browser/oidc-client-ts.min.js public/vendor/oidc-client-ts.min.js", "predeploy": "REACT_APP_CONFIG=demo PUBLIC_URL='https://imagingdatacommons.github.io/slim/' ./scripts/set-git-env.sh craco build", "deploy": "gh-pages -d build", "clean": "rm -rf ./build ./node_modules", @@ -44,7 +43,7 @@ "detect-browser": "^5.2.1", "dicom-microscopy-viewer": "^0.48.22", "dicomweb-client": "0.10.3", - "oidc-client-ts": "3.5.0", + "oidc-client": "1.11.5", "ol": "^10.7.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 929fbba5..1de39298 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,9 +42,9 @@ importers: dicomweb-client: specifier: 0.10.3 version: 0.10.3 - oidc-client-ts: - specifier: 3.5.0 - version: 3.5.0 + oidc-client: + specifier: 1.11.5 + version: 1.11.5 ol: specifier: ^10.7.0 version: 10.9.0 @@ -2348,6 +2348,9 @@ packages: bare-url@2.4.5: resolution: {integrity: sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.33: resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} engines: {node: '>=6.0.0'} @@ -2746,6 +2749,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + crypto-random-string@2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} @@ -4607,10 +4613,6 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} - jwt-decode@4.0.0: - resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} - engines: {node: '>=18'} - keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -5237,9 +5239,8 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} - oidc-client-ts@3.5.0: - resolution: {integrity: sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==} - engines: {node: '>=18'} + oidc-client@1.11.5: + resolution: {integrity: sha512-LcKrKC8Av0m/KD/4EFmo9Sg8fSQ+WFJWBrmtWd+tZkNn3WT/sQG3REmPANE9tzzhbjW6VkTNy4xhAXCfPApAOg==} ol@10.9.0: resolution: {integrity: sha512-svbbgVQUmEHaKpLQ8kRySojs59Brvgl2zYIrqG9eQNXGfsbi55rQasZIDpwpQzDL6OlzrUb0H4hQaiX9wDoGmA==} @@ -10440,6 +10441,8 @@ snapshots: dependencies: bare-path: 3.0.1 + base64-js@1.5.1: {} + baseline-browser-mapping@2.10.33: {} batch@0.6.1: {} @@ -10864,6 +10867,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-js@4.2.0: {} + crypto-random-string@2.0.0: {} crypto-random-string@4.0.0: @@ -13240,8 +13245,6 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 - jwt-decode@4.0.0: {} - keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -13701,9 +13704,13 @@ snapshots: obuf@1.1.2: {} - oidc-client-ts@3.5.0: + oidc-client@1.11.5: dependencies: - jwt-decode: 4.0.0 + acorn: 7.4.1 + base64-js: 1.5.1 + core-js: 3.49.0 + crypto-js: 4.2.0 + serialize-javascript: 7.0.5 ol@10.9.0: dependencies: diff --git a/public/silent-renew.html b/public/silent-renew.html deleted file mode 100644 index 166d428b..00000000 --- a/public/silent-renew.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - Silent renew - - - - - - - diff --git a/public/vendor/oidc-client-ts.min.js b/public/vendor/oidc-client-ts.min.js deleted file mode 100644 index 00d6d688..00000000 --- a/public/vendor/oidc-client-ts.min.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict";var oidc=(()=>{var ke=Object.defineProperty;var Ke=Object.getOwnPropertyDescriptor;var Fe=Object.getOwnPropertyNames;var $e=Object.prototype.hasOwnProperty;var Be=(l,e)=>{for(var t in e)ke(l,t,{get:e[t],enumerable:!0})},ze=(l,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Fe(e))!$e.call(l,i)&&i!==t&&ke(l,i,{get:()=>e[i],enumerable:!(r=Ke(e,i))||r.enumerable});return l};var Qe=l=>ze(ke({},"__esModule",{value:!0}),l);var gt={};Be(gt,{AccessTokenEvents:()=>Y,CheckSessionIFrame:()=>Z,DPoPState:()=>W,ErrorResponse:()=>w,ErrorTimeout:()=>E,InMemoryWebStorage:()=>q,IndexedDbDPoPStore:()=>ve,Log:()=>Q,Logger:()=>g,MetadataService:()=>ee,OidcClient:()=>ae,OidcClientSettingsStore:()=>U,SessionMonitor:()=>ie,SigninResponse:()=>H,SigninState:()=>D,SignoutResponse:()=>re,State:()=>P,User:()=>L,UserManager:()=>Te,UserManagerSettingsStore:()=>ne,Version:()=>qe,WebStorageStateStore:()=>N});var Ve={debug:()=>{},info:()=>{},warn:()=>{},error:()=>{}},x,R,Q=(s=>(s[s.NONE=0]="NONE",s[s.ERROR=1]="ERROR",s[s.WARN=2]="WARN",s[s.INFO=3]="INFO",s[s.DEBUG=4]="DEBUG",s))(Q||{});(r=>{function l(){x=3,R=Ve}r.reset=l;function e(i){if(!(0<=i&&i<=4))throw new Error("Invalid log level");x=i}r.setLevel=e;function t(i){R=i}r.setLogger=t})(Q||(Q={}));var g=class l{constructor(e){this._name=e}debug(...e){x>=4&&R.debug(l._format(this._name,this._method),...e)}info(...e){x>=3&&R.info(l._format(this._name,this._method),...e)}warn(...e){x>=2&&R.warn(l._format(this._name,this._method),...e)}error(...e){x>=1&&R.error(l._format(this._name,this._method),...e)}throw(e){throw this.error(e),e}create(e){let t=Object.create(this);return t._method=e,t.debug("begin"),t}static createStatic(e,t){let r=new l(`${e}.${t}`);return r.debug("begin"),r}static _format(e,t){let r=`[${e}]`;return t?`${r} ${t}:`:r}static debug(e,...t){x>=4&&R.debug(l._format(e),...t)}static info(e,...t){x>=3&&R.info(l._format(e),...t)}static warn(e,...t){x>=2&&R.warn(l._format(e),...t)}static error(e,...t){x>=1&&R.error(l._format(e),...t)}};Q.reset();var V=class extends Error{};V.prototype.name="InvalidTokenError";function Ge(l){return decodeURIComponent(atob(l).replace(/(.)/g,(e,t)=>{let r=t.charCodeAt(0).toString(16).toUpperCase();return r.length<2&&(r="0"+r),"%"+r}))}function Xe(l){let e=l.replace(/-/g,"+").replace(/_/g,"/");switch(e.length%4){case 0:break;case 2:e+="==";break;case 3:e+="=";break;default:throw new Error("base64 string is not of the correct length")}try{return Ge(e)}catch{return atob(e)}}function Ue(l,e){if(typeof l!="string")throw new V("Invalid token specified: must be a string");e||(e={});let t=e.header===!0?0:1,r=l.split(".")[t];if(typeof r!="string")throw new V(`Invalid token specified: missing part #${t+1}`);let i;try{i=Xe(r)}catch(s){throw new V(`Invalid token specified: invalid base64 for part #${t+1} (${s.message})`)}try{return JSON.parse(i)}catch(s){throw new V(`Invalid token specified: invalid json for part #${t+1} (${s.message})`)}}var C=class{static decode(e){try{return Ue(e)}catch(t){throw g.error("JwtUtils.decode",t),t}}static async generateSignedJwt(e,t,r){let i=m.encodeBase64Url(new TextEncoder().encode(JSON.stringify(e))),s=m.encodeBase64Url(new TextEncoder().encode(JSON.stringify(t))),n=`${i}.${s}`,o=await window.crypto.subtle.sign({name:"ECDSA",hash:{name:"SHA-256"}},r,new TextEncoder().encode(n)),a=m.encodeBase64Url(new Uint8Array(o));return`${n}.${a}`}static async generateSignedJwtWithHmac(e,t,r){let i=m.encodeBase64Url(new TextEncoder().encode(JSON.stringify(e))),s=m.encodeBase64Url(new TextEncoder().encode(JSON.stringify(t))),n=`${i}.${s}`,o=await window.crypto.subtle.sign("HMAC",r,new TextEncoder().encode(n)),a=m.encodeBase64Url(new Uint8Array(o));return`${n}.${a}`}};var Ye="10000000-1000-4000-8000-100000000000",Pe=l=>btoa([...new Uint8Array(l)].map(e=>String.fromCharCode(e)).join("")),S=class S{static _randomWord(){let e=new Uint32Array(1);return crypto.getRandomValues(e),e[0]}static generateUUIDv4(){return Ye.replace(/[018]/g,t=>(+t^S._randomWord()&15>>+t/4).toString(16)).replace(/-/g,"")}static generateCodeVerifier(){return S.generateUUIDv4()+S.generateUUIDv4()+S.generateUUIDv4()}static async generateCodeChallenge(e){if(!crypto.subtle)throw new Error("Crypto.subtle is available only in secure contexts (HTTPS).");try{let r=new TextEncoder().encode(e),i=await crypto.subtle.digest("SHA-256",r);return Pe(i).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}catch(t){throw g.error("CryptoUtils.generateCodeChallenge",t),t}}static generateBasicAuth(e,t){let i=new TextEncoder().encode([e,t].join(":"));return Pe(i)}static async hash(e,t){let r=new TextEncoder().encode(t),i=await crypto.subtle.digest(e,r);return new Uint8Array(i)}static async customCalculateJwkThumbprint(e){let t;switch(e.kty){case"RSA":t={e:e.e,kty:e.kty,n:e.n};break;case"EC":t={crv:e.crv,kty:e.kty,x:e.x,y:e.y};break;case"OKP":t={crv:e.crv,kty:e.kty,x:e.x};break;case"oct":t={crv:e.k,kty:e.kty};break;default:throw new Error("Unknown jwk type")}let r=await S.hash("SHA-256",JSON.stringify(t));return S.encodeBase64Url(r)}static async generateDPoPProof({url:e,accessToken:t,httpMethod:r,keyPair:i,nonce:s}){let n,o,a={jti:window.crypto.randomUUID(),htm:r!=null?r:"GET",htu:e,iat:Math.floor(Date.now()/1e3)};t&&(n=await S.hash("SHA-256",t),o=S.encodeBase64Url(n),a.ath=o),s&&(a.nonce=s);try{let c=await crypto.subtle.exportKey("jwk",i.publicKey),d={alg:"ES256",typ:"dpop+jwt",jwk:{crv:c.crv,kty:c.kty,x:c.x,y:c.y}};return await C.generateSignedJwt(d,a,i.privateKey)}catch(c){throw c instanceof TypeError?new Error(`Error exporting dpop public key: ${c.message}`):c}}static async generateDPoPJkt(e){try{let t=await crypto.subtle.exportKey("jwk",e.publicKey);return await S.customCalculateJwkThumbprint(t)}catch(t){throw t instanceof TypeError?new Error(`Could not retrieve dpop keys from storage: ${t.message}`):t}}static async generateDPoPKeys(){return await window.crypto.subtle.generateKey({name:"ECDSA",namedCurve:"P-256"},!1,["sign","verify"])}static async generateClientAssertionJwt(e,t,r,i="HS256"){let s=Math.floor(Date.now()/1e3),n={alg:i,typ:"JWT"},o={iss:e,sub:e,aud:r,jti:S.generateUUIDv4(),exp:s+300,iat:s},c={HS256:"SHA-256",HS384:"SHA-384",HS512:"SHA-512"}[i];if(!c)throw new Error(`Unsupported algorithm: ${i}. Supported algorithms are: HS256, HS384, HS512`);let d=new TextEncoder,u=await crypto.subtle.importKey("raw",d.encode(t),{name:"HMAC",hash:c},!1,["sign"]);return await C.generateSignedJwtWithHmac(n,o,u)}};S.encodeBase64Url=e=>Pe(e).replace(/=/g,"").replace(/\+/g,"-").replace(/\//g,"_");var m=S;var b=class{constructor(e){this._name=e;this._callbacks=[];this._logger=new g(`Event('${this._name}')`)}addHandler(e){return this._callbacks.push(e),()=>this.removeHandler(e)}removeHandler(e){let t=this._callbacks.lastIndexOf(e);t>=0&&this._callbacks.splice(t,1)}async raise(...e){this._logger.debug("raise:",...e);for(let t of this._callbacks)await t(...e)}};var oe=class{static center({...e}){var t,r,i;return e.width==null&&(e.width=(t=[800,720,600,480].find(s=>s<=window.outerWidth/1.618))!=null?t:360),(r=e.left)!=null||(e.left=Math.max(0,Math.round(window.screenX+(window.outerWidth-e.width)/2))),e.height!=null&&((i=e.top)!=null||(e.top=Math.max(0,Math.round(window.screenY+(window.outerHeight-e.height)/2)))),e}static serialize(e){return Object.entries(e).filter(([,t])=>t!=null).map(([t,r])=>`${t}=${typeof r!="boolean"?r:r?"yes":"no"}`).join(",")}};var f=class l extends b{constructor(){super(...arguments);this._logger=new g(`Timer('${this._name}')`);this._timerHandle=null;this._expiration=0;this._callback=()=>{let t=this._expiration-l.getEpochTime();this._logger.debug("timer completes in",t),this._expiration<=l.getEpochTime()&&(this.cancel(),super.raise())}}static getEpochTime(){return Math.floor(Date.now()/1e3)}init(t){let r=this._logger.create("init");t=Math.max(Math.floor(t),1);let i=l.getEpochTime()+t;if(this.expiration===i&&this._timerHandle){r.debug("skipping since already initialized for expiration at",this.expiration);return}this.cancel(),r.debug("using duration",t),this._expiration=i;let s=Math.min(t,5);this._timerHandle=setInterval(this._callback,s*1e3)}get expiration(){return this._expiration}cancel(){this._logger.create("cancel"),this._timerHandle&&(clearInterval(this._timerHandle),this._timerHandle=null)}};var G=class{static readParams(e,t="query"){if(!e)throw new TypeError("Invalid URL");let i=new URL(e,"http://127.0.0.1")[t==="fragment"?"hash":"search"];return new URLSearchParams(i.slice(1))}},T=";";var w=class extends Error{constructor(t,r){var i,s,n;super(t.error_description||t.error||"");this.form=r;this.name="ErrorResponse";if(!t.error)throw g.error("ErrorResponse","No error passed"),new Error("No error passed");this.error=t.error,this.error_description=(i=t.error_description)!=null?i:null,this.error_uri=(s=t.error_uri)!=null?s:null,this.state=t.userState,this.session_state=(n=t.session_state)!=null?n:null,this.url_state=t.url_state}};var E=class extends Error{constructor(t){super(t);this.name="ErrorTimeout"}};var Y=class{constructor(e){this._logger=new g("AccessTokenEvents");this._expiringTimer=new f("Access token expiring");this._expiredTimer=new f("Access token expired");this._expiringNotificationTimeInSeconds=e.expiringNotificationTimeInSeconds}async load(e){let t=this._logger.create("load");if(e.access_token&&e.expires_in!==void 0){let r=e.expires_in;if(t.debug("access token present, remaining duration:",r),r>0){let s=r-this._expiringNotificationTimeInSeconds;s<=0&&(s=1),t.debug("registering expiring timer, raising in",s,"seconds"),this._expiringTimer.init(s)}else t.debug("canceling existing expiring timer because we're past expiration."),this._expiringTimer.cancel();let i=r+1;t.debug("registering expired timer, raising in",i,"seconds"),this._expiredTimer.init(i)}else this._expiringTimer.cancel(),this._expiredTimer.cancel()}async unload(){this._logger.debug("unload: canceling existing access token timers"),this._expiringTimer.cancel(),this._expiredTimer.cancel()}addAccessTokenExpiring(e){return this._expiringTimer.addHandler(e)}removeAccessTokenExpiring(e){this._expiringTimer.removeHandler(e)}addAccessTokenExpired(e){return this._expiredTimer.addHandler(e)}removeAccessTokenExpired(e){this._expiredTimer.removeHandler(e)}};var Z=class{constructor(e,t,r,i,s){this._callback=e;this._client_id=t;this._intervalInSeconds=i;this._stopOnError=s;this._logger=new g("CheckSessionIFrame");this._timer=null;this._session_state=null;this._message=e=>{e.origin===this._frame_origin&&e.source===this._frame.contentWindow&&(e.data==="error"?(this._logger.error("error message from check session op iframe"),this._stopOnError&&this.stop()):e.data==="changed"?(this._logger.debug("changed message from check session op iframe"),this.stop(),this._callback()):this._logger.debug(e.data+" message from check session op iframe"))};let n=new URL(r);this._frame_origin=n.origin,this._frame=window.document.createElement("iframe"),this._frame.style.visibility="hidden",this._frame.style.position="fixed",this._frame.style.left="-1000px",this._frame.style.top="0",this._frame.width="0",this._frame.height="0",this._frame.src=n.href}load(){return new Promise(e=>{this._frame.onload=()=>{e()},window.document.body.appendChild(this._frame),window.addEventListener("message",this._message,!1)})}start(e){if(this._session_state===e)return;this._logger.create("start"),this.stop(),this._session_state=e;let t=()=>{!this._frame.contentWindow||!this._session_state||this._frame.contentWindow.postMessage(this._client_id+" "+this._session_state,this._frame_origin)};t(),this._timer=setInterval(t,this._intervalInSeconds*1e3)}stop(){this._logger.create("stop"),this._session_state=null,this._timer&&(clearInterval(this._timer),this._timer=null)}};var q=class{constructor(){this._logger=new g("InMemoryWebStorage");this._data={}}clear(){this._logger.create("clear"),this._data={}}getItem(e){return this._logger.create(`getItem('${e}')`),this._data[e]}setItem(e,t){this._logger.create(`setItem('${e}')`),this._data[e]=t}removeItem(e){this._logger.create(`removeItem('${e}')`),delete this._data[e]}get length(){return Object.getOwnPropertyNames(this._data).length}key(e){return Object.getOwnPropertyNames(this._data)[e]}};var X=class extends Error{constructor(t,r){super(r);this.name="ErrorDPoPNonce";this.nonce=t}};var M=class{constructor(e=[],t=null,r={}){this._jwtHandler=t;this._extraHeaders=r;this._logger=new g("JsonService");this._contentTypes=[];this._contentTypes.push(...e,"application/json"),t&&this._contentTypes.push("application/jwt")}async fetchWithTimeout(e,t={}){let{timeoutInSeconds:r,...i}=t;if(!r)return await fetch(e,i);let s=new AbortController,n=setTimeout(()=>s.abort(),r*1e3);try{return await fetch(e,{...t,signal:s.signal})}catch(o){throw o instanceof DOMException&&o.name==="AbortError"?new E("Network timed out"):o}finally{clearTimeout(n)}}async getJson(e,{token:t,credentials:r,timeoutInSeconds:i}={}){let s=this._logger.create("getJson"),n={Accept:this._contentTypes.join(", ")};t&&(s.debug("token passed, setting Authorization header"),n.Authorization="Bearer "+t),this._appendExtraHeaders(n);let o;try{s.debug("url:",e),o=await this.fetchWithTimeout(e,{method:"GET",headers:n,timeoutInSeconds:i,credentials:r})}catch(d){throw s.error("Network Error"),d}s.debug("HTTP response received, status",o.status);let a=o.headers.get("Content-Type");if(a&&!this._contentTypes.find(d=>a.startsWith(d))&&s.throw(new Error(`Invalid response Content-Type: ${a!=null?a:"undefined"}, from URL: ${e}`)),o.ok&&this._jwtHandler&&(a!=null&&a.startsWith("application/jwt")))return await this._jwtHandler(await o.text());let c;try{c=await o.json()}catch(d){throw s.error("Error parsing JSON response",d),o.ok?d:new Error(`${o.statusText} (${o.status})`)}if(!o.ok)throw s.error("Error from server:",c),c.error?new w(c):new Error(`${o.statusText} (${o.status}): ${JSON.stringify(c)}`);return c}async postForm(e,{body:t,basicAuth:r,timeoutInSeconds:i,initCredentials:s,extraHeaders:n}){let o=this._logger.create("postForm"),a={Accept:this._contentTypes.join(", "),"Content-Type":"application/x-www-form-urlencoded",...n};r!==void 0&&(a.Authorization="Basic "+r),this._appendExtraHeaders(a);let c;try{o.debug("url:",e),c=await this.fetchWithTimeout(e,{method:"POST",headers:a,body:t,timeoutInSeconds:i,credentials:s})}catch(h){throw o.error("Network error"),h}o.debug("HTTP response received, status",c.status);let d=c.headers.get("Content-Type");if(d&&!this._contentTypes.find(h=>d.startsWith(h)))throw new Error(`Invalid response Content-Type: ${d!=null?d:"undefined"}, from URL: ${e}`);let u=await c.text(),p={};if(u)try{p=JSON.parse(u)}catch(h){throw o.error("Error parsing JSON response",h),c.ok?h:new Error(`${c.statusText} (${c.status})`)}if(!c.ok){if(o.error("Error from server:",p),c.headers.has("dpop-nonce")){let h=c.headers.get("dpop-nonce");throw new X(h,`${JSON.stringify(p)}`)}throw p.error?new w(p,t):new Error(`${c.statusText} (${c.status}): ${JSON.stringify(p)}`)}return p}_appendExtraHeaders(e){let t=this._logger.create("appendExtraHeaders"),r=Object.keys(this._extraHeaders),i=["accept","content-type"],s=["authorization"];r.length!==0&&r.forEach(n=>{if(i.includes(n.toLocaleLowerCase())){t.warn("Protected header could not be set",n,i);return}if(s.includes(n.toLocaleLowerCase())&&Object.keys(e).includes(n)){t.warn("Header could not be overridden",n,s);return}let o=typeof this._extraHeaders[n]=="function"?this._extraHeaders[n]():this._extraHeaders[n];o&&o!==""&&(e[n]=o)})}};var ee=class{constructor(e){this._settings=e;this._logger=new g("MetadataService");this._signingKeys=null;this._metadata=null;this._metadataUrl=this._settings.metadataUrl,this._jsonService=new M(["application/jwk-set+json"],null,this._settings.extraHeaders),this._settings.signingKeys&&(this._logger.debug("using signingKeys from settings"),this._signingKeys=this._settings.signingKeys),this._settings.metadata&&(this._logger.debug("using metadata from settings"),this._metadata=this._settings.metadata),this._settings.fetchRequestCredentials&&(this._logger.debug("using fetchRequestCredentials from settings"),this._fetchRequestCredentials=this._settings.fetchRequestCredentials)}resetSigningKeys(){this._signingKeys=null}async getMetadata(){let e=this._logger.create("getMetadata");if(this._metadata)return e.debug("using cached values"),this._metadata;if(!this._metadataUrl)throw e.throw(new Error("No authority or metadataUrl configured on settings")),null;e.debug("getting metadata from",this._metadataUrl);let t=await this._jsonService.getJson(this._metadataUrl,{credentials:this._fetchRequestCredentials,timeoutInSeconds:this._settings.requestTimeoutInSeconds});return e.debug("merging remote JSON with seed metadata"),this._metadata=Object.assign({},t,this._settings.metadataSeed),this._metadata}getIssuer(){return this._getMetadataProperty("issuer")}getAuthorizationEndpoint(){return this._getMetadataProperty("authorization_endpoint")}getUserInfoEndpoint(){return this._getMetadataProperty("userinfo_endpoint")}getTokenEndpoint(e=!0){return this._getMetadataProperty("token_endpoint",e)}getCheckSessionIframe(){return this._getMetadataProperty("check_session_iframe",!0)}getEndSessionEndpoint(){return this._getMetadataProperty("end_session_endpoint",!0)}getRevocationEndpoint(e=!0){return this._getMetadataProperty("revocation_endpoint",e)}getKeysEndpoint(e=!0){return this._getMetadataProperty("jwks_uri",e)}async _getMetadataProperty(e,t=!1){let r=this._logger.create(`_getMetadataProperty('${e}')`),i=await this.getMetadata();if(r.debug("resolved"),i[e]===void 0){if(t===!0){r.warn("Metadata does not contain optional property");return}r.throw(new Error("Metadata does not contain property "+e))}return i[e]}async getSigningKeys(){let e=this._logger.create("getSigningKeys");if(this._signingKeys)return e.debug("returning signingKeys from cache"),this._signingKeys;let t=await this.getKeysEndpoint(!1);e.debug("got jwks_uri",t);let r=await this._jsonService.getJson(t,{timeoutInSeconds:this._settings.requestTimeoutInSeconds});if(e.debug("got key set",r),!Array.isArray(r.keys))throw e.throw(new Error("Missing keys on keyset")),null;return this._signingKeys=r.keys,this._signingKeys}};var N=class{constructor({prefix:e="oidc.",store:t=localStorage}={}){this._logger=new g("WebStorageStateStore");this._store=t,this._prefix=e}async set(e,t){this._logger.create(`set('${e}')`),e=this._prefix+e,await this._store.setItem(e,t)}async get(e){return this._logger.create(`get('${e}')`),e=this._prefix+e,await this._store.getItem(e)}async remove(e){this._logger.create(`remove('${e}')`),e=this._prefix+e;let t=await this._store.getItem(e);return await this._store.removeItem(e),t}async getAllKeys(){this._logger.create("getAllKeys");let e=await this._store.length,t=[];for(let r=0;r{let t=this._logger.create("_getClaimsFromJwt");try{let r=C.decode(e);return t.debug("JWT decoding successful"),r}catch(r){throw t.error("Error parsing JWT response"),r}};this._jsonService=new M(void 0,this._getClaimsFromJwt,this._settings.extraHeaders)}async getClaims(e){let t=this._logger.create("getClaims");e||this._logger.throw(new Error("No token passed"));let r=await this._metadataService.getUserInfoEndpoint();t.debug("got userinfo url",r);let i=await this._jsonService.getJson(r,{token:e,credentials:this._settings.fetchRequestCredentials,timeoutInSeconds:this._settings.requestTimeoutInSeconds});return t.debug("got claims",i),i}};var te=class{constructor(e,t){this._settings=e;this._metadataService=t;this._logger=new g("TokenClient");this._jsonService=new M(this._settings.revokeTokenAdditionalContentTypes,null,this._settings.extraHeaders)}async exchangeCode({grant_type:e="authorization_code",redirect_uri:t=this._settings.redirect_uri,client_id:r=this._settings.client_id,client_secret:i=this._settings.client_secret,extraHeaders:s,...n}){let o=this._logger.create("exchangeCode");r||o.throw(new Error("A client_id is required")),t||o.throw(new Error("A redirect_uri is required")),n.code||o.throw(new Error("A code is required"));let a=new URLSearchParams({grant_type:e,redirect_uri:t});for(let[p,h]of Object.entries(n))h!=null&&a.set(p,h);if((this._settings.client_authentication==="client_secret_basic"||this._settings.client_authentication==="client_secret_jwt")&&i==null)throw o.throw(new Error("A client_secret is required")),null;let c,d=await this._metadataService.getTokenEndpoint(!1);switch(this._settings.client_authentication){case"client_secret_basic":c=m.generateBasicAuth(r,i);break;case"client_secret_post":a.append("client_id",r),i&&a.append("client_secret",i);break;case"client_secret_jwt":{let p=await m.generateClientAssertionJwt(r,i,d,this._settings.token_endpoint_auth_signing_alg);a.append("client_id",r),a.append("client_assertion_type","urn:ietf:params:oauth:client-assertion-type:jwt-bearer"),a.append("client_assertion",p);break}}o.debug("got token endpoint");let u=await this._jsonService.postForm(d,{body:a,basicAuth:c,timeoutInSeconds:this._settings.requestTimeoutInSeconds,initCredentials:this._settings.fetchRequestCredentials,extraHeaders:s});return o.debug("got response"),u}async exchangeCredentials({grant_type:e="password",client_id:t=this._settings.client_id,client_secret:r=this._settings.client_secret,scope:i=this._settings.scope,...s}){let n=this._logger.create("exchangeCredentials");t||n.throw(new Error("A client_id is required"));let o=new URLSearchParams({grant_type:e});this._settings.omitScopeWhenRequesting||o.set("scope",i);for(let[u,p]of Object.entries(s))p!=null&&o.set(u,p);if((this._settings.client_authentication==="client_secret_basic"||this._settings.client_authentication==="client_secret_jwt")&&r==null)throw n.throw(new Error("A client_secret is required")),null;let a,c=await this._metadataService.getTokenEndpoint(!1);switch(this._settings.client_authentication){case"client_secret_basic":a=m.generateBasicAuth(t,r);break;case"client_secret_post":o.append("client_id",t),r&&o.append("client_secret",r);break;case"client_secret_jwt":{let u=await m.generateClientAssertionJwt(t,r,c,this._settings.token_endpoint_auth_signing_alg);o.append("client_id",t),o.append("client_assertion_type","urn:ietf:params:oauth:client-assertion-type:jwt-bearer"),o.append("client_assertion",u);break}}n.debug("got token endpoint");let d=await this._jsonService.postForm(c,{body:o,basicAuth:a,timeoutInSeconds:this._settings.requestTimeoutInSeconds,initCredentials:this._settings.fetchRequestCredentials});return n.debug("got response"),d}async exchangeRefreshToken({grant_type:e="refresh_token",client_id:t=this._settings.client_id,client_secret:r=this._settings.client_secret,timeoutInSeconds:i,extraHeaders:s,...n}){let o=this._logger.create("exchangeRefreshToken");t||o.throw(new Error("A client_id is required")),n.refresh_token||o.throw(new Error("A refresh_token is required"));let a=new URLSearchParams({grant_type:e});for(let[p,h]of Object.entries(n))Array.isArray(h)?h.forEach(v=>a.append(p,v)):h!=null&&a.set(p,h);if((this._settings.client_authentication==="client_secret_basic"||this._settings.client_authentication==="client_secret_jwt")&&r==null)throw o.throw(new Error("A client_secret is required")),null;let c,d=await this._metadataService.getTokenEndpoint(!1);switch(this._settings.client_authentication){case"client_secret_basic":c=m.generateBasicAuth(t,r);break;case"client_secret_post":a.append("client_id",t),r&&a.append("client_secret",r);break;case"client_secret_jwt":{let p=await m.generateClientAssertionJwt(t,r,d,this._settings.token_endpoint_auth_signing_alg);a.append("client_id",t),a.append("client_assertion_type","urn:ietf:params:oauth:client-assertion-type:jwt-bearer"),a.append("client_assertion",p);break}}o.debug("got token endpoint");let u=await this._jsonService.postForm(d,{body:a,basicAuth:c,timeoutInSeconds:i,initCredentials:this._settings.fetchRequestCredentials,extraHeaders:s});return o.debug("got response"),u}async revoke(e){var s;let t=this._logger.create("revoke");e.token||t.throw(new Error("A token is required"));let r=await this._metadataService.getRevocationEndpoint(!1);t.debug(`got revocation endpoint, revoking ${(s=e.token_type_hint)!=null?s:"default token type"}`);let i=new URLSearchParams;for(let[n,o]of Object.entries(e))o!=null&&i.set(n,o);i.set("client_id",this._settings.client_id),this._settings.client_secret&&i.set("client_secret",this._settings.client_secret),await this._jsonService.postForm(r,{body:i,timeoutInSeconds:this._settings.requestTimeoutInSeconds}),t.debug("got response")}};var ge=class{constructor(e,t,r){this._settings=e;this._metadataService=t;this._claimsService=r;this._logger=new g("ResponseValidator");this._userInfoService=new de(this._settings,this._metadataService),this._tokenClient=new te(this._settings,this._metadataService)}async validateSigninResponse(e,t,r){let i=this._logger.create("validateSigninResponse");this._processSigninState(e,t),i.debug("state processed"),await this._processCode(e,t,r),i.debug("code processed"),e.isOpenId&&this._validateIdTokenAttributes(e,"",t.nonce),i.debug("tokens validated"),await this._processClaims(e,t==null?void 0:t.skipUserInfo,e.isOpenId),i.debug("claims processed")}async validateCredentialsResponse(e,t){let r=this._logger.create("validateCredentialsResponse"),i=e.isOpenId&&!!e.id_token;i&&this._validateIdTokenAttributes(e),r.debug("tokens validated"),await this._processClaims(e,t,i),r.debug("claims processed")}async validateRefreshResponse(e,t){var s,n;let r=this._logger.create("validateRefreshResponse");e.userState=t.data,(s=e.session_state)!=null||(e.session_state=t.session_state),(n=e.scope)!=null||(e.scope=t.scope),e.isOpenId&&e.id_token&&(this._validateIdTokenAttributes(e,t.id_token),r.debug("ID Token validated")),e.id_token||(e.id_token=t.id_token,e.profile=t.profile);let i=e.isOpenId&&!!e.id_token;await this._processClaims(e,!1,i),r.debug("claims processed")}validateSignoutResponse(e,t){let r=this._logger.create("validateSignoutResponse");if(t.id!==e.state&&r.throw(new Error("State does not match")),r.debug("state validated"),e.userState=t.data,e.error)throw r.warn("Response was error",e.error),new w(e)}_processSigninState(e,t){var i;let r=this._logger.create("_processSigninState");if(t.id!==e.state&&r.throw(new Error("State does not match")),t.client_id||r.throw(new Error("No client_id on state")),t.authority||r.throw(new Error("No authority on state")),this._settings.authority!==t.authority&&r.throw(new Error("authority mismatch on settings vs. signin state")),this._settings.client_id&&this._settings.client_id!==t.client_id&&r.throw(new Error("client_id mismatch on settings vs. signin state")),r.debug("state validated"),e.userState=t.data,e.url_state=t.url_state,(i=e.scope)!=null||(e.scope=t.scope),e.error)throw r.warn("Response was error",e.error),new w(e);t.code_verifier&&!e.code&&r.throw(new Error("Expected code in response"))}async _processClaims(e,t=!1,r=!0){let i=this._logger.create("_processClaims");if(e.profile=this._claimsService.filterProtocolClaims(e.profile),t||!this._settings.loadUserInfo||!e.access_token){i.debug("not loading user info");return}i.debug("loading user info");let s=await this._userInfoService.getClaims(e.access_token);i.debug("user info claims received from user info endpoint"),r&&s.sub!==e.profile.sub&&i.throw(new Error("subject from UserInfo response does not match subject in ID Token")),e.profile=this._claimsService.mergeClaims(e.profile,this._claimsService.filterProtocolClaims(s)),i.debug("user info claims received, updated profile:",e.profile)}async _processCode(e,t,r){let i=this._logger.create("_processCode");if(e.code){i.debug("Validating code");let s=await this._tokenClient.exchangeCode({client_id:t.client_id,client_secret:t.client_secret,code:e.code,redirect_uri:t.redirect_uri,code_verifier:t.code_verifier,extraHeaders:r,...t.extraTokenParams});Object.assign(e,s)}else i.debug("No code to process")}_validateIdTokenAttributes(e,t,r){var n;let i=this._logger.create("_validateIdTokenAttributes");i.debug("decoding ID Token JWT");let s=C.decode((n=e.id_token)!=null?n:"");if(s.sub||i.throw(new Error("ID Token is missing a subject claim")),r&&s.nonce!==r&&i.throw(new Error("nonce in id_token does not match nonce in client storage")),t){let o=C.decode(t);s.sub!==o.sub&&i.throw(new Error("sub in id_token does not match current sub")),s.auth_time&&s.auth_time!==o.auth_time&&i.throw(new Error("auth_time in id_token does not match original auth_time")),s.azp&&s.azp!==o.azp&&i.throw(new Error("azp in id_token does not match original azp")),!s.azp&&o.azp&&i.throw(new Error("azp not in id_token, but present in original id_token"))}e.profile=s}};var P=class l{constructor(e){this.id=e.id||m.generateUUIDv4(),this.data=e.data,e.created&&e.created>0?this.created=e.created:this.created=f.getEpochTime(),this.request_type=e.request_type,this.url_state=e.url_state}toStorageString(){return new g("State").create("toStorageString"),JSON.stringify({id:this.id,data:this.data,created:this.created,request_type:this.request_type,url_state:this.url_state})}static fromStorageString(e){return g.createStatic("State","fromStorageString"),Promise.resolve(new l(JSON.parse(e)))}static async clearStaleState(e,t){let r=g.createStatic("State","clearStaleState"),i=f.getEpochTime()-t,s=await e.getAllKeys();r.debug("got keys",s);for(let n=0;n_.searchParams.append("resource",k));for(let[O,k]of Object.entries({response_mode:a,...$,...j}))k!=null&&_.searchParams.append(O,k.toString());return new ue({url:_.href,state:y})}};ue._logger=new g("SigninRequest");var pe=ue;var it="openid",H=class{constructor(e){this.access_token="";this.token_type="";this.profile={};if(this.state=e.get("state"),this.session_state=e.get("session_state"),this.state){let t=decodeURIComponent(this.state).split(T);this.state=t[0],t.length>1&&(this.url_state=t.slice(1).join(T))}this.error=e.get("error"),this.error_description=e.get("error_description"),this.error_uri=e.get("error_uri"),this.code=e.get("code")}get expires_in(){if(this.expires_at!==void 0)return this.expires_at-f.getEpochTime()}set expires_in(e){typeof e=="string"&&(e=Number(e)),e!==void 0&&e>=0&&(this.expires_at=Math.floor(e)+f.getEpochTime())}get isOpenId(){var e;return((e=this.scope)==null?void 0:e.split(" ").includes(it))||!!this.id_token}};var he=class{constructor({url:e,state_data:t,id_token_hint:r,post_logout_redirect_uri:i,extraQueryParams:s,request_type:n,client_id:o,url_state:a}){this._logger=new g("SignoutRequest");if(!e)throw this._logger.error("ctor: No url passed"),new Error("url");let c=new URL(e);if(r&&c.searchParams.append("id_token_hint",r),o&&c.searchParams.append("client_id",o),i&&(c.searchParams.append("post_logout_redirect_uri",i),t||a)){this.state=new P({data:t,request_type:n,url_state:a});let d=this.state.id;a&&(d=`${d}${T}${a}`),c.searchParams.append("state",d)}for(let[d,u]of Object.entries({...s}))u!=null&&c.searchParams.append(d,u.toString());this.url=c.href}};var re=class{constructor(e){if(this.state=e.get("state"),this.state){let t=decodeURIComponent(this.state).split(T);this.state=t[0],t.length>1&&(this.url_state=t.slice(1).join(T))}this.error=e.get("error"),this.error_description=e.get("error_description"),this.error_uri=e.get("error_uri")}};var st=["nbf","jti","auth_time","nonce","acr","amr","azp","at_hash"],nt=["sub","iss","aud","exp","iat"],me=class{constructor(e){this._settings=e;this._logger=new g("ClaimsService")}filterProtocolClaims(e){let t={...e};if(this._settings.filterProtocolClaims){let r;Array.isArray(this._settings.filterProtocolClaims)?r=this._settings.filterProtocolClaims:r=st;for(let i of r)nt.includes(i)||delete t[i]}return t}mergeClaims(e,t){let r={...e};for(let[i,s]of Object.entries(t))if(r[i]!==s)if(Array.isArray(r[i])||Array.isArray(s))if(this._settings.mergeClaimsStrategy.array=="replace")r[i]=s;else{let n=Array.isArray(r[i])?r[i]:[r[i]];for(let o of Array.isArray(s)?s:[s])n.includes(o)||n.push(o);r[i]=n}else typeof r[i]=="object"&&typeof s=="object"?r[i]=this.mergeClaims(r[i],s):r[i]=s;return r}};var W=class{constructor(e,t){this.keys=e;this.nonce=t}};var ae=class{constructor(e,t){this._logger=new g("OidcClient");this.settings=e instanceof U?e:new U(e),this.metadataService=t!=null?t:new ee(this.settings),this._claimsService=new me(this.settings),this._validator=new ge(this.settings,this.metadataService,this._claimsService),this._tokenClient=new te(this.settings,this.metadataService)}async createSigninRequest({state:e,request:t,request_uri:r,request_type:i,id_token_hint:s,login_hint:n,skipUserInfo:o,nonce:a,url_state:c,response_type:d=this.settings.response_type,scope:u=this.settings.scope,redirect_uri:p=this.settings.redirect_uri,prompt:h=this.settings.prompt,display:v=this.settings.display,max_age:j=this.settings.max_age,ui_locales:J=this.settings.ui_locales,acr_values:K=this.settings.acr_values,resource:A=this.settings.resource,response_mode:F=this.settings.response_mode,extraQueryParams:$=this.settings.extraQueryParams,extraTokenParams:y=this.settings.extraTokenParams,dpopJkt:_,omitScopeWhenRequesting:I=this.settings.omitScopeWhenRequesting}){let O=this._logger.create("createSigninRequest");if(d!=="code")throw new Error("Only the Authorization Code flow (with PKCE) is supported");let k=await this.metadataService.getAuthorizationEndpoint();O.debug("Received authorization endpoint",k);let B=await pe.create({url:k,authority:this.settings.authority,client_id:this.settings.client_id,redirect_uri:p,response_type:d,scope:u,state_data:e,url_state:c,prompt:h,display:v,max_age:j,ui_locales:J,id_token_hint:s,login_hint:n,acr_values:K,dpopJkt:_,resource:A,request:t,request_uri:r,extraQueryParams:$,extraTokenParams:y,request_type:i,response_mode:F,client_secret:this.settings.client_secret,skipUserInfo:o,nonce:a,disablePKCE:this.settings.disablePKCE,omitScopeWhenRequesting:I});await this.clearStaleState();let z=B.state;return await this.settings.stateStore.set(z.id,z.toStorageString()),B}async readSigninResponseState(e,t=!1){let r=this._logger.create("readSigninResponseState"),i=new H(G.readParams(e,this.settings.response_mode));if(!i.state)throw r.throw(new Error("No state in response")),null;let s=await this.settings.stateStore[t?"remove":"get"](i.state);if(!s)throw r.throw(new Error("No matching state found in storage")),null;return{state:await D.fromStorageString(s),response:i}}async processSigninResponse(e,t,r=!0){let i=this._logger.create("processSigninResponse"),{state:s,response:n}=await this.readSigninResponseState(e,r);if(i.debug("received state from storage; validating response"),this.settings.dpop&&this.settings.dpop.store){let o=await this.getDpopProof(this.settings.dpop.store);t={...t,DPoP:o}}try{await this._validator.validateSigninResponse(n,s,t)}catch(o){if(o instanceof X&&this.settings.dpop){let a=await this.getDpopProof(this.settings.dpop.store,o.nonce);t.DPoP=a,await this._validator.validateSigninResponse(n,s,t)}else throw o}return n}async getDpopProof(e,t){let r,i;return(await e.getAllKeys()).includes(this.settings.client_id)?(i=await e.get(this.settings.client_id),i.nonce!==t&&t&&(i.nonce=t,await e.set(this.settings.client_id,i))):(r=await m.generateDPoPKeys(),i=new W(r,t),await e.set(this.settings.client_id,i)),await m.generateDPoPProof({url:await this.metadataService.getTokenEndpoint(!1),httpMethod:"POST",keyPair:i.keys,nonce:i.nonce})}async processResourceOwnerPasswordCredentials({username:e,password:t,skipUserInfo:r=!1,extraTokenParams:i={}}){let s=await this._tokenClient.exchangeCredentials({username:e,password:t,...i}),n=new H(new URLSearchParams);return Object.assign(n,s),await this._validator.validateCredentialsResponse(n,r),n}async useRefreshToken({state:e,redirect_uri:t,resource:r,timeoutInSeconds:i,extraHeaders:s,extraTokenParams:n}){var u;let o=this._logger.create("useRefreshToken"),a;if(this.settings.refreshTokenAllowedScope===void 0)a=e.scope;else{let p=this.settings.refreshTokenAllowedScope.split(" ");a=(((u=e.scope)==null?void 0:u.split(" "))||[]).filter(v=>p.includes(v)).join(" ")}if(this.settings.dpop&&this.settings.dpop.store){let p=await this.getDpopProof(this.settings.dpop.store);s={...s,DPoP:p}}let c;try{c=await this._tokenClient.exchangeRefreshToken({refresh_token:e.refresh_token,scope:a,redirect_uri:t,resource:r,timeoutInSeconds:i,extraHeaders:s,...n})}catch(p){if(p instanceof X&&this.settings.dpop)s.DPoP=await this.getDpopProof(this.settings.dpop.store,p.nonce),c=await this._tokenClient.exchangeRefreshToken({refresh_token:e.refresh_token,scope:a,redirect_uri:t,resource:r,timeoutInSeconds:i,extraHeaders:s,...n});else throw p}let d=new H(new URLSearchParams);return Object.assign(d,c),o.debug("validating response",d),await this._validator.validateRefreshResponse(d,{...e,scope:a}),d}async createSignoutRequest({state:e,id_token_hint:t,client_id:r,request_type:i,url_state:s,post_logout_redirect_uri:n=this.settings.post_logout_redirect_uri,extraQueryParams:o=this.settings.extraQueryParams}={}){let a=this._logger.create("createSignoutRequest"),c=await this.metadataService.getEndSessionEndpoint();if(!c)throw a.throw(new Error("No end session endpoint")),null;a.debug("Received end session endpoint",c),!r&&n&&!t&&(r=this.settings.client_id);let d=new he({url:c,id_token_hint:t,client_id:r,post_logout_redirect_uri:n,state_data:e,extraQueryParams:o,request_type:i,url_state:s});await this.clearStaleState();let u=d.state;return u&&(a.debug("Signout request has state to persist"),await this.settings.stateStore.set(u.id,u.toStorageString())),d}async readSignoutResponseState(e,t=!1){let r=this._logger.create("readSignoutResponseState"),i=new re(G.readParams(e,this.settings.response_mode));if(!i.state){if(r.debug("No state in response"),i.error)throw r.warn("Response was error:",i.error),new w(i);return{state:void 0,response:i}}let s=await this.settings.stateStore[t?"remove":"get"](i.state);if(!s)throw r.throw(new Error("No matching state found in storage")),null;return{state:await P.fromStorageString(s),response:i}}async processSignoutResponse(e){let t=this._logger.create("processSignoutResponse"),{state:r,response:i}=await this.readSignoutResponseState(e,!0);return r?(t.debug("Received state from storage; validating response"),this._validator.validateSignoutResponse(i,r)):t.debug("No state from storage; skipping response validation"),i}clearStaleState(){return this._logger.create("clearStaleState"),P.clearStaleState(this.settings.stateStore,this.settings.staleStateAgeInSeconds)}async revokeToken(e,t){return this._logger.create("revokeToken"),await this._tokenClient.revoke({token:e,token_type_hint:t})}};var ie=class{constructor(e){this._userManager=e;this._logger=new g("SessionMonitor");this._start=async e=>{let t=e.session_state;if(!t)return;let r=this._logger.create("_start");if(e.profile?(this._sub=e.profile.sub,r.debug("session_state",t,", sub",this._sub)):(this._sub=void 0,r.debug("session_state",t,", anonymous user")),this._checkSessionIFrame){this._checkSessionIFrame.start(t);return}try{let i=await this._userManager.metadataService.getCheckSessionIframe();if(i){r.debug("initializing check session iframe");let s=this._userManager.settings.client_id,n=this._userManager.settings.checkSessionIntervalInSeconds,o=this._userManager.settings.stopCheckSessionOnError,a=new Z(this._callback,s,i,n,o);await a.load(),this._checkSessionIFrame=a,a.start(t)}else r.warn("no check session iframe found in the metadata")}catch(i){r.error("Error from getCheckSessionIframe:",i instanceof Error?i.message:i)}};this._stop=()=>{let e=this._logger.create("_stop");if(this._sub=void 0,this._checkSessionIFrame&&this._checkSessionIFrame.stop(),this._userManager.settings.monitorAnonymousSession){let t=setInterval(async()=>{clearInterval(t);try{let r=await this._userManager.querySessionStatus();if(r){let i={session_state:r.session_state,profile:r.sub?{sub:r.sub}:null};this._start(i)}}catch(r){e.error("error from querySessionStatus",r instanceof Error?r.message:r)}},1e3)}};this._callback=async()=>{let e=this._logger.create("_callback");try{let t=await this._userManager.querySessionStatus(),r=!0;t&&this._checkSessionIFrame?t.sub===this._sub?(r=!1,this._checkSessionIFrame.start(t.session_state),e.debug("same sub still logged in at OP, session state has changed, restarting check session iframe; session_state",t.session_state),await this._userManager.events._raiseUserSessionChanged()):e.debug("different subject signed into OP",t.sub):e.debug("subject no longer signed into OP"),r?this._sub?await this._userManager.events._raiseUserSignedOut():await this._userManager.events._raiseUserSignedIn():e.debug("no change in session detected, no event to raise")}catch(t){this._sub&&(e.debug("Error calling queryCurrentSigninSession; raising signed out event",t),await this._userManager.events._raiseUserSignedOut())}};e||this._logger.throw(new Error("No user manager passed")),this._userManager.events.addUserLoaded(this._start),this._userManager.events.addUserUnloaded(this._stop),this._init().catch(t=>{this._logger.error(t)})}async _init(){this._logger.create("_init");let e=await this._userManager.getUser();if(e)this._start(e);else if(this._userManager.settings.monitorAnonymousSession){let t=await this._userManager.querySessionStatus();if(t){let r={session_state:t.session_state,profile:t.sub?{sub:t.sub}:null};this._start(r)}}}};var L=class l{constructor(e){var t;this.id_token=e.id_token,this.session_state=(t=e.session_state)!=null?t:null,this.access_token=e.access_token,this.refresh_token=e.refresh_token,this.token_type=e.token_type,this.scope=e.scope,this.profile=e.profile,this.expires_at=e.expires_at,this.state=e.userState,this.url_state=e.url_state}get expires_in(){if(this.expires_at!==void 0)return this.expires_at-f.getEpochTime()}set expires_in(e){e!==void 0&&(this.expires_at=Math.floor(e)+f.getEpochTime())}get expired(){let e=this.expires_in;if(e!==void 0)return e<=0}get scopes(){var e,t;return(t=(e=this.scope)==null?void 0:e.split(" "))!=null?t:[]}toStorageString(){return new g("User").create("toStorageString"),JSON.stringify({id_token:this.id_token,session_state:this.session_state,access_token:this.access_token,refresh_token:this.refresh_token,token_type:this.token_type,scope:this.scope,profile:this.profile,expires_at:this.expires_at})}static fromStorageString(e){return g.createStatic("User","fromStorageString"),new l(JSON.parse(e))}};var Ae="oidc-client",se=class{constructor(){this._abort=new b("Window navigation aborted");this._disposeHandlers=new Set;this._window=null}async navigate(e){let t=this._logger.create("navigate");if(!this._window)throw new Error("Attempted to navigate on a disposed window");t.debug("setting URL in window"),this._window.location.replace(e.url);let{url:r,keepOpen:i}=await new Promise((s,n)=>{let o=c=>{var p;let d=c.data,u=(p=e.scriptOrigin)!=null?p:window.location.origin;if(!(c.origin!==u||(d==null?void 0:d.source)!==Ae)){try{let h=G.readParams(d.url,e.response_mode).get("state");if(h||t.warn("no state found in response url"),c.source!==this._window&&h!==e.state)return}catch{this._dispose(),n(new Error("Invalid response from window"))}s(d)}};window.addEventListener("message",o,!1),this._disposeHandlers.add(()=>window.removeEventListener("message",o,!1));let a=new BroadcastChannel(`oidc-client-popup-${e.state}`);a.addEventListener("message",o,!1),this._disposeHandlers.add(()=>a.close()),this._disposeHandlers.add(this._abort.addHandler(c=>{this._dispose(),n(c)}))});return t.debug("got response from window"),this._dispose(),i||this.close(),{url:r}}_dispose(){this._logger.create("_dispose");for(let e of this._disposeHandlers)e();this._disposeHandlers.clear()}static _notifyParent(e,t,r=!1,i=window.location.origin){let s={source:Ae,url:t,keepOpen:r},n=new g("_notifyParent");if(e)n.debug("With parent. Using parent.postMessage."),e.postMessage(s,i);else{n.debug("No parent. Using BroadcastChannel.");let o=new URL(t).searchParams.get("state");if(!o)throw new Error("No parent and no state in URL. Can't complete notification.");let a=new BroadcastChannel(`oidc-client-popup-${o}`);a.postMessage(s),a.close()}}};var xe={location:!1,toolbar:!1,height:640,closePopupWindowAfterInSeconds:-1},Re="_blank",ot=60,at=2,Ce=10,ne=class extends U{constructor(e){let{popup_redirect_uri:t=e.redirect_uri,popup_post_logout_redirect_uri:r=e.post_logout_redirect_uri,popupWindowFeatures:i=xe,popupWindowTarget:s=Re,redirectMethod:n="assign",redirectTarget:o="self",iframeNotifyParentOrigin:a=e.iframeNotifyParentOrigin,iframeScriptOrigin:c=e.iframeScriptOrigin,requestTimeoutInSeconds:d,silent_redirect_uri:u=e.redirect_uri,silentRequestTimeoutInSeconds:p,automaticSilentRenew:h=!0,validateSubOnSilentRenew:v=!0,includeIdTokenInSilentRenew:j=!1,monitorSession:J=!1,monitorAnonymousSession:K=!1,checkSessionIntervalInSeconds:A=at,query_status_response_type:F="code",stopCheckSessionOnError:$=!0,revokeTokenTypes:y=["access_token","refresh_token"],revokeTokensOnSignout:_=!1,includeIdTokenInSilentSignout:I=!1,accessTokenExpiringNotificationTimeInSeconds:O=ot,maxSilentRenewTimeoutRetries:k,userStore:B}=e;if(super(e),this.popup_redirect_uri=t,this.popup_post_logout_redirect_uri=r,this.popupWindowFeatures=i,this.popupWindowTarget=s,this.redirectMethod=n,this.redirectTarget=o,this.iframeNotifyParentOrigin=a,this.iframeScriptOrigin=c,this.silent_redirect_uri=u,this.silentRequestTimeoutInSeconds=p||d||Ce,this.automaticSilentRenew=h,this.validateSubOnSilentRenew=v,this.includeIdTokenInSilentRenew=j,this.monitorSession=J,this.monitorAnonymousSession=K,this.checkSessionIntervalInSeconds=A,this.stopCheckSessionOnError=$,this.query_status_response_type=F,this.revokeTokenTypes=y,this.revokeTokensOnSignout=_,this.includeIdTokenInSilentSignout=I,this.accessTokenExpiringNotificationTimeInSeconds=O,this.maxSilentRenewTimeoutRetries=k,B)this.userStore=B;else{let z=typeof window!="undefined"?window.sessionStorage:new q;this.userStore=new N({store:z})}}};var ce=class l extends se{constructor({silentRequestTimeoutInSeconds:t=Ce}){super();this._logger=new g("IFrameWindow");this._timeoutInSeconds=t,this._frame=l.createHiddenIframe(),this._window=this._frame.contentWindow}static createHiddenIframe(){let t=window.document.createElement("iframe");return t.style.visibility="hidden",t.style.position="fixed",t.style.left="-1000px",t.style.top="0",t.width="0",t.height="0",window.document.body.appendChild(t),t}async navigate(t){this._logger.debug("navigate: Using timeout of:",this._timeoutInSeconds);let r=setTimeout(()=>{this._abort.raise(new E("IFrame timed out without a response"))},this._timeoutInSeconds*1e3);return this._disposeHandlers.add(()=>clearTimeout(r)),await super.navigate(t)}close(){var t;this._frame&&(this._frame.parentNode&&(this._frame.addEventListener("load",r=>{var s;let i=r.target;(s=i.parentNode)==null||s.removeChild(i),this._abort.raise(new Error("IFrame removed from DOM"))},!0),(t=this._frame.contentWindow)==null||t.location.replace("about:blank")),this._frame=null),this._window=null}static notifyParent(t,r){return super._notifyParent(window.parent,t,!1,r)}};var _e=class{constructor(e){this._settings=e;this._logger=new g("IFrameNavigator")}async prepare({silentRequestTimeoutInSeconds:e=this._settings.silentRequestTimeoutInSeconds}){return new ce({silentRequestTimeoutInSeconds:e})}async callback(e){this._logger.create("callback"),ce.notifyParent(e,this._settings.iframeNotifyParentOrigin)}};var ct=500,lt=1e3,le=class extends se{constructor({popupWindowTarget:t=Re,popupWindowFeatures:r={},popupSignal:i,popupAbortOnClose:s}){super();this._logger=new g("PopupWindow");let n=oe.center({...xe,...r});this._window=window.open(void 0,t,oe.serialize(n)),this.abortOnClose=!!s,i&&i.addEventListener("abort",()=>{var o;this._abort.raise(new Error((o=i.reason)!=null?o:"Popup aborted"))}),r.closePopupWindowAfterInSeconds&&r.closePopupWindowAfterInSeconds>0&&setTimeout(()=>{if(!this._window||typeof this._window.closed!="boolean"||this._window.closed){this._abort.raise(new Error("Popup blocked by user"));return}this.close()},r.closePopupWindowAfterInSeconds*lt)}async navigate(t){var s;(s=this._window)==null||s.focus();let r=setInterval(()=>{(!this._window||this._window.closed)&&(this._logger.debug("Popup closed by user or isolated by redirect"),i(),this._disposeHandlers.delete(i),this.abortOnClose&&this._abort.raise(new Error("Popup closed by user")))},ct),i=()=>clearInterval(r);return this._disposeHandlers.add(i),await super.navigate(t)}close(){this._window&&(this._window.closed||(this._window.close(),this._abort.raise(new Error("Popup closed")))),this._window=null}static notifyOpener(t,r){super._notifyParent(window.opener,t,r),!r&&!window.opener&&window.close()}};var fe=class{constructor(e){this._settings=e;this._logger=new g("PopupNavigator")}async prepare({popupWindowFeatures:e=this._settings.popupWindowFeatures,popupWindowTarget:t=this._settings.popupWindowTarget,popupSignal:r,popupAbortOnClose:i}){return new le({popupWindowFeatures:e,popupWindowTarget:t,popupSignal:r,popupAbortOnClose:i})}async callback(e,{keepOpen:t=!1}){this._logger.create("callback"),le.notifyOpener(e,t)}};var we=class{constructor(e){this._settings=e;this._logger=new g("RedirectNavigator")}async prepare({redirectMethod:e=this._settings.redirectMethod,redirectTarget:t=this._settings.redirectTarget}){var n;this._logger.create("prepare");let r=window.self;t==="top"&&(r=(n=window.top)!=null?n:window.self);let i=r.location[e].bind(r.location),s;return{navigate:async o=>(this._logger.create("navigate"),await new Promise((c,d)=>{s=d,window.addEventListener("pageshow",()=>c(window.location.href)),i(o.url)})),close:()=>{this._logger.create("close"),s==null||s(new Error("Redirect aborted")),r.stop()}}}async callback(){}};var Se=class extends Y{constructor(t){super({expiringNotificationTimeInSeconds:t.accessTokenExpiringNotificationTimeInSeconds});this._logger=new g("UserManagerEvents");this._userLoaded=new b("User loaded");this._userUnloaded=new b("User unloaded");this._silentRenewError=new b("Silent renew error");this._userSignedIn=new b("User signed in");this._userSignedOut=new b("User signed out");this._userSessionChanged=new b("User session changed")}async load(t,r=!0){await super.load(t),r&&await this._userLoaded.raise(t)}async unload(){await super.unload(),await this._userUnloaded.raise()}addUserLoaded(t){return this._userLoaded.addHandler(t)}removeUserLoaded(t){return this._userLoaded.removeHandler(t)}addUserUnloaded(t){return this._userUnloaded.addHandler(t)}removeUserUnloaded(t){return this._userUnloaded.removeHandler(t)}addSilentRenewError(t){return this._silentRenewError.addHandler(t)}removeSilentRenewError(t){return this._silentRenewError.removeHandler(t)}async _raiseSilentRenewError(t){await this._silentRenewError.raise(t)}addUserSignedIn(t){return this._userSignedIn.addHandler(t)}removeUserSignedIn(t){this._userSignedIn.removeHandler(t)}async _raiseUserSignedIn(){await this._userSignedIn.raise()}addUserSignedOut(t){return this._userSignedOut.addHandler(t)}removeUserSignedOut(t){this._userSignedOut.removeHandler(t)}async _raiseUserSignedOut(){await this._userSignedOut.raise()}addUserSessionChanged(t){return this._userSessionChanged.addHandler(t)}removeUserSessionChanged(t){this._userSessionChanged.removeHandler(t)}async _raiseUserSessionChanged(){await this._userSessionChanged.raise()}};var be=class{constructor(e){this._userManager=e;this._logger=new g("SilentRenewService");this._isStarted=!1;this._retryTimer=new f("Retry Silent Renew");this._timeoutRetryCount=0;this._tokenExpiring=async()=>{let e=this._logger.create("_tokenExpiring");try{await this._userManager.signinSilent(),this._timeoutRetryCount=0,e.debug("silent token renewal successful")}catch(t){if(t instanceof E){this._timeoutRetryCount++;let r=this._userManager.settings.maxSilentRenewTimeoutRetries;if(r!==void 0&&this._timeoutRetryCount>r){e.error(`Timeout retry limit reached (${this._timeoutRetryCount} > ${r}), raising silentRenewError:`,t),this._timeoutRetryCount=0,await this._userManager.events._raiseSilentRenewError(t);return}e.warn(`ErrorTimeout from signinSilent (attempt ${this._timeoutRetryCount}), retry in 5s:`,t),this._retryTimer.init(5);return}e.error("Error from signinSilent:",t),this._timeoutRetryCount=0,await this._userManager.events._raiseSilentRenewError(t)}}}async start(){let e=this._logger.create("start");if(!this._isStarted){this._isStarted=!0,this._userManager.events.addAccessTokenExpiring(this._tokenExpiring),this._retryTimer.addHandler(this._tokenExpiring);try{await this._userManager.getUser()}catch(t){e.error("getUser error",t)}}}stop(){this._isStarted&&(this._retryTimer.cancel(),this._retryTimer.removeHandler(this._tokenExpiring),this._userManager.events.removeAccessTokenExpiring(this._tokenExpiring),this._isStarted=!1)}};var ye=class{constructor(e){this.refresh_token=e.refresh_token,this.id_token=e.id_token,this.session_state=e.session_state,this.scope=e.scope,this.profile=e.profile,this.data=e.state}};var Te=class{constructor(e,t,r,i){this._logger=new g("UserManager");this.settings=new ne(e),this._client=new ae(e),this._redirectNavigator=t!=null?t:new we(this.settings),this._popupNavigator=r!=null?r:new fe(this.settings),this._iframeNavigator=i!=null?i:new _e(this.settings),this._events=new Se(this.settings),this._silentRenewService=new be(this),this.settings.automaticSilentRenew&&this.startSilentRenew(),this._sessionMonitor=null,this.settings.monitorSession&&(this._sessionMonitor=new ie(this))}get events(){return this._events}get metadataService(){return this._client.metadataService}async getUser(e=!1){let t=this._logger.create("getUser"),r=await this._loadUser();return r?(t.info("user loaded"),await this._events.load(r,e),r):(t.info("user not found in storage"),null)}async removeUser(){let e=this._logger.create("removeUser");await this.storeUser(null),e.info("user removed from storage"),await this._events.unload()}async signinRedirect(e={}){var n;this._logger.create("signinRedirect");let{redirectMethod:t,...r}=e,i;(n=this.settings.dpop)!=null&&n.bind_authorization_code&&(i=await this.generateDPoPJkt(this.settings.dpop));let s=await this._redirectNavigator.prepare({redirectMethod:t});await this._signinStart({request_type:"si:r",dpopJkt:i,...r},s)}async signinRedirectCallback(e=window.location.href){let t=this._logger.create("signinRedirectCallback"),r=await this._signinEnd(e);return r.profile&&r.profile.sub?t.info("success, signed in subject",r.profile.sub):t.info("no subject"),r}async signinResourceOwnerCredentials({username:e,password:t,skipUserInfo:r=!1}){let i=this._logger.create("signinResourceOwnerCredential"),s=await this._client.processResourceOwnerPasswordCredentials({username:e,password:t,skipUserInfo:r,extraTokenParams:this.settings.extraTokenParams});i.debug("got signin response");let n=await this._buildUser(s);return n.profile&&n.profile.sub?i.info("success, signed in subject",n.profile.sub):i.info("no subject"),n}async signinPopup(e={}){var p;let t=this._logger.create("signinPopup"),r;(p=this.settings.dpop)!=null&&p.bind_authorization_code&&(r=await this.generateDPoPJkt(this.settings.dpop));let{popupWindowFeatures:i,popupWindowTarget:s,popupSignal:n,popupAbortOnClose:o,...a}=e,c=this.settings.popup_redirect_uri;c||t.throw(new Error("No popup_redirect_uri configured"));let d=await this._popupNavigator.prepare({popupWindowFeatures:i,popupWindowTarget:s,popupSignal:n,popupAbortOnClose:o}),u=await this._signin({request_type:"si:p",redirect_uri:c,display:"popup",dpopJkt:r,...a},d);return u&&(u.profile&&u.profile.sub?t.info("success, signed in subject",u.profile.sub):t.info("no subject")),u}async signinPopupCallback(e=window.location.href,t=!1){let r=this._logger.create("signinPopupCallback");await this._popupNavigator.callback(e,{keepOpen:t}),r.info("success")}async signinSilent(e={}){var d,u;let t=this._logger.create("signinSilent"),{silentRequestTimeoutInSeconds:r,...i}=e,s=await this._loadUser();if(!e.forceIframeAuth&&(s!=null&&s.refresh_token)){t.debug("using refresh token");let p=new ye(s);return await this._useRefreshToken({state:p,redirect_uri:i.redirect_uri,resource:i.resource,extraTokenParams:i.extraTokenParams,timeoutInSeconds:r})}let n;(d=this.settings.dpop)!=null&&d.bind_authorization_code&&(n=await this.generateDPoPJkt(this.settings.dpop));let o=this.settings.silent_redirect_uri;o||t.throw(new Error("No silent_redirect_uri configured"));let a;s&&this.settings.validateSubOnSilentRenew&&(t.debug("subject prior to silent renew:",s.profile.sub),a=s.profile.sub);let c=await this._iframeNavigator.prepare({silentRequestTimeoutInSeconds:r});return s=await this._signin({request_type:"si:s",redirect_uri:o,prompt:"none",id_token_hint:this.settings.includeIdTokenInSilentRenew?s==null?void 0:s.id_token:void 0,dpopJkt:n,...i},c,a),s&&((u=s.profile)!=null&&u.sub?t.info("success, signed in subject",s.profile.sub):t.info("no subject")),s}async _useRefreshToken(e){let t=await this._client.useRefreshToken({timeoutInSeconds:this.settings.silentRequestTimeoutInSeconds,...e}),r=new L({...e.state,...t});return await this.storeUser(r),await this._events.load(r),r}async signinSilentCallback(e=window.location.href){let t=this._logger.create("signinSilentCallback");await this._iframeNavigator.callback(e),t.info("success")}async signinCallback(e=window.location.href){let{state:t}=await this._client.readSigninResponseState(e);switch(t.request_type){case"si:r":return await this.signinRedirectCallback(e);case"si:p":await this.signinPopupCallback(e);break;case"si:s":await this.signinSilentCallback(e);break;default:throw new Error("invalid request_type in state")}}async signoutCallback(e=window.location.href,t=!1){let{state:r}=await this._client.readSignoutResponseState(e);if(r)switch(r.request_type){case"so:r":return await this.signoutRedirectCallback(e);case"so:p":await this.signoutPopupCallback(e,t);break;case"so:s":await this.signoutSilentCallback(e);break;default:throw new Error("invalid request_type in state")}}async querySessionStatus(e={}){let t=this._logger.create("querySessionStatus"),{silentRequestTimeoutInSeconds:r,...i}=e,s=this.settings.silent_redirect_uri;s||t.throw(new Error("No silent_redirect_uri configured"));let n=await this._loadUser(),o=await this._iframeNavigator.prepare({silentRequestTimeoutInSeconds:r}),a=await this._signinStart({request_type:"si:s",redirect_uri:s,prompt:"none",id_token_hint:this.settings.includeIdTokenInSilentRenew?n==null?void 0:n.id_token:void 0,response_type:this.settings.query_status_response_type,scope:"openid",skipUserInfo:!0,...i},o);try{let c={},d=await this._client.processSigninResponse(a.url,c);return t.debug("got signin response"),d.session_state&&d.profile.sub?(t.info("success for subject",d.profile.sub),{session_state:d.session_state,sub:d.profile.sub}):(t.info("success, user not authenticated"),null)}catch(c){if(this.settings.monitorAnonymousSession&&c instanceof w)switch(c.error){case"login_required":case"consent_required":case"interaction_required":case"account_selection_required":return t.info("success for anonymous user"),{session_state:c.session_state}}throw c}}async _signin(e,t,r){let i=await this._signinStart(e,t);return await this._signinEnd(i.url,r)}async _signinStart(e,t){let r=this._logger.create("_signinStart");try{let i=await this._client.createSigninRequest(e);return r.debug("got signin request"),await t.navigate({url:i.url,state:i.state.id,response_mode:i.state.response_mode,scriptOrigin:this.settings.iframeScriptOrigin})}catch(i){throw r.debug("error after preparing navigator, closing navigator window"),t.close(),i}}async _signinEnd(e,t){let r=this._logger.create("_signinEnd"),i={},s=await this._client.processSigninResponse(e,i);return r.debug("got signin response"),await this._buildUser(s,t)}async _buildUser(e,t){let r=this._logger.create("_buildUser"),i=new L(e);if(t){if(t!==i.profile.sub)throw r.debug("current user does not match user returned from signin. sub from signin:",i.profile.sub),new w({...e,error:"login_required"});r.debug("current user matches user returned from signin")}return await this.storeUser(i),r.debug("user stored"),await this._events.load(i),i}async signoutRedirect(e={}){let t=this._logger.create("signoutRedirect"),{redirectMethod:r,...i}=e,s=await this._redirectNavigator.prepare({redirectMethod:r});await this._signoutStart({request_type:"so:r",post_logout_redirect_uri:this.settings.post_logout_redirect_uri,...i},s),t.info("success")}async signoutRedirectCallback(e=window.location.href){let t=this._logger.create("signoutRedirectCallback"),r=await this._signoutEnd(e);return t.info("success"),r}async signoutPopup(e={}){let t=this._logger.create("signoutPopup"),{popupWindowFeatures:r,popupWindowTarget:i,popupSignal:s,...n}=e,o=this.settings.popup_post_logout_redirect_uri,a=await this._popupNavigator.prepare({popupWindowFeatures:r,popupWindowTarget:i,popupSignal:s});await this._signout({request_type:"so:p",post_logout_redirect_uri:o,state:o==null?void 0:{},...n},a),t.info("success")}async signoutPopupCallback(e=window.location.href,t=!1){let r=this._logger.create("signoutPopupCallback");await this._popupNavigator.callback(e,{keepOpen:t}),r.info("success")}async _signout(e,t){let r=await this._signoutStart(e,t);return await this._signoutEnd(r.url)}async _signoutStart(e={},t){var i;let r=this._logger.create("_signoutStart");try{let s=await this._loadUser();r.debug("loaded current user from storage"),this.settings.revokeTokensOnSignout&&await this._revokeInternal(s);let n=e.id_token_hint||s&&s.id_token;n&&(r.debug("setting id_token_hint in signout request"),e.id_token_hint=n),await this.removeUser(),r.debug("user removed, creating signout request");let o=await this._client.createSignoutRequest(e);return r.debug("got signout request"),await t.navigate({url:o.url,state:(i=o.state)==null?void 0:i.id,scriptOrigin:this.settings.iframeScriptOrigin})}catch(s){throw r.debug("error after preparing navigator, closing navigator window"),t.close(),s}}async _signoutEnd(e){let t=this._logger.create("_signoutEnd"),r=await this._client.processSignoutResponse(e);return t.debug("got signout response"),r}async signoutSilent(e={}){var a;let t=this._logger.create("signoutSilent"),{silentRequestTimeoutInSeconds:r,...i}=e,s=this.settings.includeIdTokenInSilentSignout?(a=await this._loadUser())==null?void 0:a.id_token:void 0,n=this.settings.popup_post_logout_redirect_uri,o=await this._iframeNavigator.prepare({silentRequestTimeoutInSeconds:r});await this._signout({request_type:"so:s",post_logout_redirect_uri:n,id_token_hint:s,...i},o),t.info("success")}async signoutSilentCallback(e=window.location.href){let t=this._logger.create("signoutSilentCallback");await this._iframeNavigator.callback(e),t.info("success")}async revokeTokens(e){let t=await this._loadUser();await this._revokeInternal(t,e)}async _revokeInternal(e,t=this.settings.revokeTokenTypes){let r=this._logger.create("_revokeInternal");if(!e)return;let i=t.filter(s=>typeof e[s]=="string");if(!i.length){r.debug("no need to revoke due to no token(s)");return}for(let s of i)await this._client.revokeToken(e[s],s),r.info(`${s} revoked successfully`),s!=="access_token"&&(e[s]=null);await this.storeUser(e),r.debug("user stored"),await this._events.load(e)}startSilentRenew(){this._logger.create("startSilentRenew"),this._silentRenewService.start()}stopSilentRenew(){this._silentRenewService.stop()}get _userStoreKey(){return`user:${this.settings.authority}:${this.settings.client_id}`}async _loadUser(){let e=this._logger.create("_loadUser"),t=await this.settings.userStore.get(this._userStoreKey);return t?(e.debug("user storageString loaded"),L.fromStorageString(t)):(e.debug("no user storageString"),null)}async storeUser(e){let t=this._logger.create("storeUser");if(e){t.debug("storing user");let r=e.toStorageString();await this.settings.userStore.set(this._userStoreKey,r)}else this._logger.debug("removing user"),await this.settings.userStore.remove(this._userStoreKey),this.settings.dpop&&await this.settings.dpop.store.remove(this.settings.client_id)}async clearStaleState(){await this._client.clearStaleState()}async dpopProof(e,t,r,i){var n,o;let s=await((o=(n=this.settings.dpop)==null?void 0:n.store)==null?void 0:o.get(this.settings.client_id));if(s)return await m.generateDPoPProof({url:e,accessToken:t==null?void 0:t.access_token,httpMethod:r,keyPair:s.keys,nonce:i})}async generateDPoPJkt(e){let t=await e.store.get(this.settings.client_id);if(!t){let r=await m.generateDPoPKeys();t=new W(r),await e.store.set(this.settings.client_id,t)}return await m.generateDPoPJkt(t.keys)}};var Oe="3.5.0";var qe=Oe;var ve=class{constructor(){this._dbName="oidc";this._storeName="dpop"}async set(e,t){await(await this.createStore(this._dbName,this._storeName))("readwrite",i=>(i.put(t,e),this.promisifyRequest(i.transaction)))}async get(e){return await(await this.createStore(this._dbName,this._storeName))("readonly",r=>this.promisifyRequest(r.get(e)))}async remove(e){let t=await this.get(e);return await(await this.createStore(this._dbName,this._storeName))("readwrite",i=>this.promisifyRequest(i.delete(e))),t}async getAllKeys(){return await(await this.createStore(this._dbName,this._storeName))("readonly",t=>this.promisifyRequest(t.getAllKeys()))}promisifyRequest(e){return new Promise((t,r)=>{e.oncomplete=e.onsuccess=()=>t(e.result),e.onabort=e.onerror=()=>r(e.error)})}async createStore(e,t){let r=indexedDB.open(e);r.onupgradeneeded=()=>r.result.createObjectStore(t);let i=await this.promisifyRequest(r);return async(s,n)=>{let a=i.transaction(t,s).objectStore(t);return await n(a)}}};return Qe(gt);})(); -//# sourceMappingURL=oidc-client-ts.min.js.map diff --git a/src/auth/OidcManager.tsx b/src/auth/OidcManager.tsx index fca8f530..4cdfc25a 100644 --- a/src/auth/OidcManager.tsx +++ b/src/auth/OidcManager.tsx @@ -1,8 +1,4 @@ -import { - type User as UserData, - UserManager, - type UserManagerSettings, -} from 'oidc-client-ts' +import { type User as UserData, UserManager } from 'oidc-client' import type { OidcSettings } from '../AppConfig' import NotificationMiddleware, { @@ -100,10 +96,42 @@ const readReturnUrl = (userData: UserData): string | undefined => { return undefined } +/** Only allow same-origin relative paths (block open redirects). */ +export const isSafeReturnUrl = (returnUrl: string): boolean => { + if (!returnUrl.startsWith('/') || returnUrl.startsWith('//')) { + return false + } + try { + const parsed = new URL(returnUrl, window.location.origin) + return parsed.origin === window.location.origin + } catch { + return false + } +} + const currentReturnUrl = (): string => { return `${window.location.pathname}${window.location.search}` } +/** + * Complete an OIDC silent-renew callback when this window is an iframe. + * Returns true when the caller should skip mounting the React app. + */ +export const completeSilentRenewIfFrame = async (): Promise => { + if (window.parent === window) { + return false + } + if (!isAuthorizationCodeInUrl(window.location)) { + return false + } + try { + await new UserManager({}).signinSilentCallback() + } catch (error) { + console.error('silent renew callback failed', error) + } + return true +} + export default class OidcManager implements AuthManager { private _oidc: UserManager private readonly _ready: Promise @@ -113,10 +141,15 @@ export default class OidcManager implements AuthManager { const isImplicit = settings.grantType === 'implicit' const responseType = isImplicit ? 'id_token token' : 'code' const redirectUri = appUri - const silentRedirectUri = joinUrl('silent-renew.html', appUri) + /* + * Reuse the main redirect_uri for silent renew so existing IdP client + * registrations (app root only) keep working. The iframe path is handled + * in index.tsx via completeSilentRenewIfFrame() before React mounts. + */ + const silentRedirectUri = redirectUri const postLogoutRedirectUri = joinUrl('logout', appUri) - const baseSettings: UserManagerSettings = { + const baseSettings = { authority: settings.authority, client_id: settings.clientId, redirect_uri: redirectUri, @@ -126,11 +159,9 @@ export default class OidcManager implements AuthManager { response_type: responseType, loadUserInfo: true, automaticSilentRenew: true, - revokeTokensOnSignout: true, + revokeAccessTokenOnSignout: true, } - // PKCE is the oidc-client-ts default for code flow; leave it enabled. - // Implicit grant does not use PKCE. this._oidc = new UserManager(baseSettings) this._wireInternalEvents() this._ready = this._applyOptionalMetadata(baseSettings, settings) @@ -149,7 +180,7 @@ export default class OidcManager implements AuthManager { } private async _applyOptionalMetadata( - baseSettings: UserManagerSettings, + baseSettings: ConstructorParameters[0], settings: OidcSettings, ): Promise { const needsMetadataPatch = @@ -203,10 +234,19 @@ export default class OidcManager implements AuthManager { }): Promise => { const oidc = await this._ensureReady() - const handleSignIn = (userData: UserData): void => { + const handleSignIn = ( + userData: UserData, + { includeReturnUrl }: { includeReturnUrl: boolean }, + ): void => { const user = createUser(userData) const authorization = authorizationFromUser(userData) - const resolvedReturnUrl = readReturnUrl(userData) + let resolvedReturnUrl: string | undefined + if (includeReturnUrl) { + const candidate = readReturnUrl(userData) + if (candidate != null && isSafeReturnUrl(candidate)) { + resolvedReturnUrl = candidate + } + } if (onSignIn != null) { console.info('handling sign-in using provided callback function') onSignIn({ @@ -228,7 +268,7 @@ export default class OidcManager implements AuthManager { const userData = await oidc.signinRedirectCallback() clearAuthParamsFromUrl() console.info('obtained user data: ', userData) - handleSignIn(userData) + handleSignIn(userData, { includeReturnUrl: true }) } else { /* Redirect to the authorization server to authenticate the user * and authorize the application to obtain user information and access @@ -244,7 +284,8 @@ export default class OidcManager implements AuthManager { }) } else { console.info('user has already been authenticated') - handleSignIn(userData) + // Do not re-apply persisted returnUrl on warm sessions. + handleSignIn(userData, { includeReturnUrl: false }) } } } @@ -256,6 +297,7 @@ export default class OidcManager implements AuthManager { signOut = async (): Promise => { console.log('signing out user and revoking authorization') const oidc = await this._ensureReady() + const logoutUri = joinUrl('logout', oidc.settings.redirect_uri ?? '/') try { const metadata = await oidc.metadataService.getMetadata() if ( @@ -263,14 +305,14 @@ export default class OidcManager implements AuthManager { metadata.end_session_endpoint === '' ) { await oidc.removeUser() - window.location.assign(joinUrl('logout', oidc.settings.redirect_uri)) + window.location.assign(logoutUri) return } await oidc.signoutRedirect() } catch (error) { console.error('sign-out redirect failed; clearing local session', error) await oidc.removeUser() - window.location.assign(joinUrl('logout', oidc.settings.redirect_uri)) + window.location.assign(logoutUri) } } diff --git a/src/auth/__tests__/isSafeReturnUrl.test.ts b/src/auth/__tests__/isSafeReturnUrl.test.ts new file mode 100644 index 00000000..19820724 --- /dev/null +++ b/src/auth/__tests__/isSafeReturnUrl.test.ts @@ -0,0 +1,33 @@ +import { isSafeReturnUrl } from '../OidcManager' + +describe('isSafeReturnUrl', () => { + const originalLocation = window.location + + beforeAll(() => { + Object.defineProperty(window, 'location', { + configurable: true, + value: { + ...originalLocation, + origin: 'https://example.com', + }, + }) + }) + + afterAll(() => { + Object.defineProperty(window, 'location', { + configurable: true, + value: originalLocation, + }) + }) + + it('accepts same-origin relative paths', () => { + expect(isSafeReturnUrl('/studies/1.2.3')).toBe(true) + expect(isSafeReturnUrl('/studies/1.2.3?state=abc')).toBe(true) + }) + + it('rejects absolute and protocol-relative URLs', () => { + expect(isSafeReturnUrl('https://evil.example/phish')).toBe(false) + expect(isSafeReturnUrl('//evil.example/phish')).toBe(false) + expect(isSafeReturnUrl('https://example.com/studies/1')).toBe(false) + }) +}) diff --git a/src/index.tsx b/src/index.tsx index 85302049..3d20d85c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -122,22 +122,41 @@ message.config({ duration: config.messages?.duration ?? 5, }) -const container = document.getElementById('root') -if (container == null) { - throw new Error('Root element not found') +const mountApp = (): void => { + const container = document.getElementById('root') + if (container == null) { + throw new Error('Root element not found') + } + const root = createRoot(container) + root.render( + /// / + Loading application...}> + + + + , + // + ) } -const root = createRoot(container) -root.render( - /// / - Loading application...}> - - - - , - // -) + +/* + * Silent renew reuses the app redirect_uri (no extra IdP registration). + * When oidc-client loads that URI in a hidden iframe, complete the callback + * here and skip mounting React. + */ +void import('./auth/OidcManager') + .then(async ({ completeSilentRenewIfFrame }) => { + const handled = await completeSilentRenewIfFrame() + if (!handled) { + mountApp() + } + }) + .catch((error) => { + console.error('failed to initialize auth bootstrap', error) + mountApp() + }) From 2a57c6548b63be7be23ab7d77054c9f0ae360a05 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Mon, 3 Aug 2026 10:46:07 -0300 Subject: [PATCH 3/4] fix: harden mid-session OIDC reauth against races and stale UI Keep the interactive reauth guard set after IdP redirect starts so concurrent 401s cannot overwrite OIDC state, and remount routed views after silent renew so failed requests refetch with the new token. --- package.json | 2 +- pnpm-lock.yaml | 2 +- src/App.tsx | 30 ++++++++++++++++++++++++---- src/auth/OidcManager.tsx | 43 ++++++++++++++++++++++------------------ src/auth/index.d.ts | 5 ++++- 5 files changed, 56 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index b4e41eb8..e828334b 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "detect-browser": "^5.2.1", "dicom-microscopy-viewer": "^0.48.22", "dicomweb-client": "0.10.3", - "oidc-client": "1.11.5", + "oidc-client": "^1.11.5", "ol": "^10.7.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1de39298..fb916027 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,7 +43,7 @@ importers: specifier: 0.10.3 version: 0.10.3 oidc-client: - specifier: 1.11.5 + specifier: ^1.11.5 version: 1.11.5 ol: specifier: ^10.7.0 diff --git a/src/App.tsx b/src/App.tsx index 6f1f9daa..d6a520d4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -184,6 +184,8 @@ interface AppState { redirectTo?: string wasAuthSuccessful: boolean error?: ErrorMessageSettings + /** Bumped after mid-session auth recovery so views remount and refetch. */ + authRecoveryKey: number } class App extends React.Component { @@ -295,6 +297,7 @@ class App extends React.Component { defaultClients, isLoading: true, wasAuthSuccessful: false, + authRecoveryKey: 0, } } @@ -417,17 +420,29 @@ class App extends React.Component { return } this.reauthInProgress = true + let redirectedToIdp = false try { const authorization = await this.auth.renewAuthorization() if (authorization != null) { this.applyAuthorization(authorization) + // Remount routed views so in-flight 401 failures refetch with the new token. + this.setState((state) => ({ + authRecoveryKey: state.authRecoveryKey + 1, + })) return } console.info('silent renew unavailable; starting interactive sign-in') - await this.auth.signIn({ + const outcome = await this.auth.signIn({ onSignIn: this.handleSignIn, returnUrl: `${window.location.pathname}${window.location.search}`, }) + redirectedToIdp = outcome === 'redirected' + if (outcome === 'completed') { + // Token was refreshed without leaving the page; remount views to refetch. + this.setState((state) => ({ + authRecoveryKey: state.authRecoveryKey + 1, + })) + } } catch (error) { console.error(error) // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -439,7 +454,11 @@ class App extends React.Component { ), ) } finally { - this.reauthInProgress = false + // oidc-client resolves signinRedirect as soon as navigation is assigned. + // Keep the guard set until unload so concurrent 401s cannot start another redirect. + if (!redirectedToIdp) { + this.reauthInProgress = false + } } } @@ -451,7 +470,10 @@ class App extends React.Component { onSignIn: this.handleSignIn, returnUrl: `${window.location.pathname}${window.location.search}`, }) - .then(() => { + .then((outcome) => { + if (outcome === 'redirected') { + return + } console.info('sign-in was successful') this.setState({ isLoading: false, @@ -580,7 +602,7 @@ class App extends React.Component { } else { return ( - + => { + }): Promise => { const oidc = await this._ensureReady() const handleSignIn = ( @@ -269,25 +270,29 @@ export default class OidcManager implements AuthManager { clearAuthParamsFromUrl() console.info('obtained user data: ', userData) handleSignIn(userData, { includeReturnUrl: true }) - } else { - /* Redirect to the authorization server to authenticate the user - * and authorize the application to obtain user information and access - * the DICOMweb server. - */ - const userData = await oidc.getUser() - if (userData === null || userData === undefined || userData.expired) { - console.info('authenticating user') - await oidc.signinRedirect({ - state: { - returnUrl: returnUrl ?? currentReturnUrl(), - }, - }) - } else { - console.info('user has already been authenticated') - // Do not re-apply persisted returnUrl on warm sessions. - handleSignIn(userData, { includeReturnUrl: false }) - } + return 'completed' } + + /* Redirect to the authorization server to authenticate the user + * and authorize the application to obtain user information and access + * the DICOMweb server. + */ + const userData = await oidc.getUser() + if (userData === null || userData === undefined || userData.expired) { + console.info('authenticating user') + await oidc.signinRedirect({ + state: { + returnUrl: returnUrl ?? currentReturnUrl(), + }, + }) + // oidc-client resolves as soon as navigation is assigned; page unload follows. + return 'redirected' + } + + console.info('user has already been authenticated') + // Do not re-apply persisted returnUrl on warm sessions. + handleSignIn(userData, { includeReturnUrl: false }) + return 'completed' } /** diff --git a/src/auth/index.d.ts b/src/auth/index.d.ts index 917d6849..9ec233a7 100644 --- a/src/auth/index.d.ts +++ b/src/auth/index.d.ts @@ -10,6 +10,9 @@ export type SignInCallback = ({ export type AuthorizationCallback = (authorization: string) => void +/** Outcome of signIn: redirected means the page is navigating to the IdP. */ +export type SignInOutcome = 'completed' | 'redirected' + export interface User { name: string | undefined email: string | undefined @@ -22,7 +25,7 @@ export interface AuthManager { }: { onSignIn?: SignInCallback returnUrl?: string - }) => Promise + }) => Promise signOut: () => Promise getAuthorization: () => Promise getUser: () => Promise From 6d10814ee8f784f5e94ef9364116cfa32e32faec Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Mon, 3 Aug 2026 11:15:02 -0300 Subject: [PATCH 4/4] fix: never boot SPA inside OIDC silent-renew error iframes Detect IdP error callbacks (e.g. login_required) as renew iframe responses and skip React mount so the iframe cannot corrupt parent OIDC sessionStorage during interactive re-auth fallback. --- src/auth/OidcManager.tsx | 14 ++++++++-- src/index.tsx | 5 ++-- src/utils/__tests__/url.test.ts | 46 ++++++++++++++++++++++++++++++++- src/utils/url.tsx | 23 +++++++++++++++++ 4 files changed, 83 insertions(+), 5 deletions(-) diff --git a/src/auth/OidcManager.tsx b/src/auth/OidcManager.tsx index b1713a8f..6757cdf4 100644 --- a/src/auth/OidcManager.tsx +++ b/src/auth/OidcManager.tsx @@ -5,7 +5,11 @@ import NotificationMiddleware, { NotificationMiddlewareContext, } from '../services/NotificationMiddleware' import { CustomError, errorTypes } from '../utils/CustomError' -import { isAuthorizationCodeInUrl, joinUrl } from '../utils/url' +import { + isAuthorizationCodeInUrl, + isOidcAuthorizeCallbackUrl, + joinUrl, +} from '../utils/url' import type { AuthManager, AuthorizationCallback, @@ -117,12 +121,17 @@ const currentReturnUrl = (): string => { /** * Complete an OIDC silent-renew callback when this window is an iframe. * Returns true when the caller should skip mounting the React app. + * + * Must never mount the SPA inside a renew iframe: it shares sessionStorage + * with the parent and can corrupt in-flight interactive re-auth. This includes + * IdP error redirects such as `error=login_required` that do not carry a code. */ export const completeSilentRenewIfFrame = async (): Promise => { if (window.parent === window) { return false } - if (!isAuthorizationCodeInUrl(window.location)) { + // Embedded Slim (non-OIDC iframe) should still mount; only OIDC callbacks skip it. + if (!isOidcAuthorizeCallbackUrl(window.location)) { return false } try { @@ -130,6 +139,7 @@ export const completeSilentRenewIfFrame = async (): Promise => { } catch (error) { console.error('silent renew callback failed', error) } + // Always skip SPA mount for OIDC iframe callbacks (success or error). return true } diff --git a/src/index.tsx b/src/index.tsx index 3d20d85c..4fd20d7a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -146,8 +146,9 @@ const mountApp = (): void => { /* * Silent renew reuses the app redirect_uri (no extra IdP registration). - * When oidc-client loads that URI in a hidden iframe, complete the callback - * here and skip mounting React. + * When oidc-client loads that URI in a hidden iframe (success or error), + * complete the callback here and skip mounting React so the iframe cannot + * share/corrupt the parent sessionStorage OIDC state. */ void import('./auth/OidcManager') .then(async ({ completeSilentRenewIfFrame }) => { diff --git a/src/utils/__tests__/url.test.ts b/src/utils/__tests__/url.test.ts index 32da4b36..6f74697e 100644 --- a/src/utils/__tests__/url.test.ts +++ b/src/utils/__tests__/url.test.ts @@ -1,4 +1,9 @@ -import { GCP_HEALTHCARE_V1_BASE, normalizeServerUrl } from '../url' +import { + GCP_HEALTHCARE_V1_BASE, + isAuthorizationCodeInUrl, + isOidcAuthorizeCallbackUrl, + normalizeServerUrl, +} from '../url' const storePath = '/projects/idc-sandbox-000/locations/us-central1/datasets/fedorov-dev-healthcare/dicomStores/sardana-lut-test' @@ -61,3 +66,42 @@ describe('normalizeServerUrl', () => { expect(normalizeServerUrl(proxyUrl)).toBe(proxyUrl) }) }) + +describe('isOidcAuthorizeCallbackUrl', () => { + it('detects authorization code and implicit success responses', () => { + expect( + isOidcAuthorizeCallbackUrl({ search: '?code=abc&state=s', hash: '' }), + ).toBe(true) + expect( + isAuthorizationCodeInUrl({ search: '?code=abc&state=s', hash: '' }), + ).toBe(true) + expect( + isOidcAuthorizeCallbackUrl({ + search: '', + hash: '#access_token=tok&token_type=Bearer', + }), + ).toBe(true) + }) + + it('detects IdP error responses without code/id_token (silent renew failure)', () => { + expect( + isOidcAuthorizeCallbackUrl({ + search: '?error=login_required&state=s', + hash: '', + }), + ).toBe(true) + expect( + isAuthorizationCodeInUrl({ + search: '?error=login_required&state=s', + hash: '', + }), + ).toBe(false) + }) + + it('ignores ordinary app URLs', () => { + expect( + isOidcAuthorizeCallbackUrl({ search: '?state=presentation', hash: '' }), + ).toBe(false) + expect(isOidcAuthorizeCallbackUrl({ search: '', hash: '' })).toBe(false) + }) +}) diff --git a/src/utils/url.tsx b/src/utils/url.tsx index e148233f..de93b67a 100644 --- a/src/utils/url.tsx +++ b/src/utils/url.tsx @@ -89,3 +89,26 @@ export const isAuthorizationCodeInUrl = (location: { hashParams.get('session_state'), ) } + +/** + * True when the URL looks like an OIDC authorize redirect back to the app + * (success or error). Used to detect silent-renew iframe callbacks that must + * not boot the React SPA (including `error=login_required` responses that + * lack code/id_token/session_state). + */ +export const isOidcAuthorizeCallbackUrl = (location: { + search: string + hash: string +}): boolean => { + if (isAuthorizationCodeInUrl(location)) { + return true + } + const searchParams = new URLSearchParams(location.search) + const hashParams = new URLSearchParams(location.hash.replace('#', '?')) + return Boolean( + searchParams.get('error') ?? + searchParams.get('access_token') ?? + hashParams.get('error') ?? + hashParams.get('access_token'), + ) +}