@i-novus/oidc-pkce-public-client
Version:
OIDC public client with PKCE for react with tokens sharing by localStorage. React context included
2 lines (1 loc) • 11.4 kB
JavaScript
;var j=Object.defineProperty;var R=(n,e,t)=>e in n?j(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var g=(n,e,t)=>(R(n,typeof e!="symbol"?e+"":e,t),t);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const N=require("jwt-decode"),A=require("uuid"),K=require("crypto-js/sha256"),V=require("crypto-js/enc-base64url"),q=require("react/jsx-runtime"),_=require("react");function E(n){try{return N.jwtDecode(n)}catch(e){throw new Error(`JWT DECODE: ${e}`)}}const G=365*24*60*60*1e3;function b(n){const e=Date.now();Object.keys(localStorage).filter(t=>t.startsWith(n.storeKeyPrefix)).forEach(t=>{let o=!1;try{const s=window.localStorage.getItem(t);if(!s){o=!0;return}const r=JSON.parse(s);e>r.expireIn&&(o=!0)}catch(s){console.error(s),o=!0}finally{o&&window.localStorage.removeItem(t)}}),setTimeout(()=>{b(n)},60*1e3)}function I(n,e={}){const{storeKeyPrefix:t,authority:o,clientId:s}=n;return`${t}${e.keyPostfix??""}${o}/:${s}`}function y(n,e,t={}){const o=I(n,t),r={expireIn:t.expireIn??Date.now()+G,value:e};window.localStorage.setItem(o,JSON.stringify(r))}function D(n,e={}){const t=I(n,e);window.localStorage.removeItem(t)}function m(n,e={}){try{const t=I(n,e),o=window.localStorage.getItem(t);if(!o)return{expireIn:0,value:null};const s=JSON.parse(o);return Date.now()>=s.expireIn?(window.localStorage.removeItem(t),{expireIn:0,value:null}):s}catch(t){return console.error(t),{expireIn:0,value:null}}}function M(n,e){const{storeKeyPrefix:t}=n,{id:o}=e;return`${t}${o}`}function $(n,e,t){const o=M(n,t);window.sessionStorage.setItem(o,JSON.stringify(e))}function C(n,e){try{const t=M(n,e),o=window.sessionStorage.getItem(t);return o?JSON.parse(o):null}catch(t){return console.error(t),null}}const S="access_token:",v="refresh_token:",U="id_token:",x=1e3,B=60*x,F=60*B;class w{constructor(e){g(this,"config");g(this,"initialised",!1);g(this,"events",{login:[],logout:[],accessTokenUpdated:[]});g(this,"login",async(e=window.location.href)=>{const{responseId:t,responseCode:o}=this.getLoginCallbackData();if(t||o){console.error("Login cant be done cause code or state params present in url");return}const{accessToken:s,refreshToken:r}=this.getUserTokens();if(s||r)return;const{clientId:a,responseType:l,scope:i,responseMode:d,pkce:f}=this.config,{authorization_endpoint:k}=await this.getMetadataConfig(),u=A.v4(),p=A.v4()+A.v4()+A.v4(),c=new URL(k);if(c.searchParams.append("client_id",a),c.searchParams.append("redirect_uri",e),c.searchParams.append("response_type",l),c.searchParams.append("response_mode",d),c.searchParams.append("scope",i),c.searchParams.append("state",u),f){const T=K(p),P=V.stringify(T);c.searchParams.append("code_challenge",P),c.searchParams.append("code_challenge_method","S256")}const h={action:"login",id:u,codeVerifier:p,redirectUri:e};$(this.config,h,{id:u}),window.location.assign(c.href),await new Promise(()=>{})});g(this,"logout",async(e={})=>{const{idToken:t}=this.getUserTokens();if(!t)return;e!=null&&e.beforeLogout&&await(e==null?void 0:e.beforeLogout(this));const{logoutBackUrl:o}=this.config,{end_session_endpoint:s}=await this.getMetadataConfig(),r=new URL(s);r.searchParams.append("id_token_hint",t),r.searchParams.append("post_logout_redirect_uri",o),this.removeUserTokens(),window.location.assign(r.href),await new Promise(()=>{})});g(this,"refreshAccessTokenLoop",async()=>{try{const{accessTokenExpireIn:e,refreshToken:t}=this.getUserTokens();if(e-Date.now()<=30*x&&t&&!this.isRefreshRequestsPending()){const{token_endpoint:r}=await this.getMetadataConfig(),{clientId:a,scope:l}=this.config,i=new URLSearchParams;i.append("grant_type","refresh_token"),i.append("refresh_token",t),i.append("scope",l),i.append("client_id",a);const d=await fetch(r,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:i.toString()});if(d.ok&&d.status===200){const f=await d.json();this.setUserTokens(f)}else this.removeUserTokens()}}catch(e){console.error(`REFRESH TOKEN: ${e}`)}setTimeout(this.refreshAccessTokenLoop,10*x)});e?this.config=w.getConfig(e):this.config=null}on(e,t){const o=this.events[e];return o.indexOf(t)===-1&&o.push(t),this}off(e,t){const o=this.events[e].indexOf(t);return o>=0&&this.events[e].splice(o,1),this}emit(e,...t){return this.events[e].forEach(s=>s(...t)),this}static getConfig(e){const t=["query","fragment"],o={subjectType:"public",authority:e.authority.trim().replace(/\/+$/,""),clientId:e.clientId,responseType:"code",responseMode:e.responseMode??"query",grantType:"authorization_code",scope:e.scope??"openid",logoutBackUrl:e.logoutBackUrl??window.location.href,storeKeyPrefix:e.storeKeyPrefix??"OIDC-MIF:",pkce:e.pkce??!0};if(!o.authority)throw new Error("CONFIG: authority is not set");if(!o.clientId)throw new Error("CONFIG: clientId is not set");if(!o.responseMode)throw new Error("CONFIG: responseMode is not set");if(!t.includes(o.responseMode))throw new Error(`CONFIG: responseMode is "${o.responseMode}", but accepted only one of "${t.join('", "')}"`);if(!o.storeKeyPrefix)throw new Error("CONFIG: storeKeyPrefix is not set");return o}async getMetadataConfig(){const e={keyPostfix:"METADATA:",expireIn:Date.now()+12*F},{value:t}=m(this.config,e);if(t!==null)return t;const{authority:o,grantType:s,responseType:r,subjectType:a,responseMode:l}=this.config,i=await fetch(`${o}/.well-known/openid-configuration`);if(!i.ok||i.status!==200)throw new Error(`METADATA: Getting OpenID configuration is failed. Error (${i.status}): ${await i.text()}`);const d=await i.json(),{authorization_endpoint:f,token_endpoint:k,end_session_endpoint:u,grant_types_supported:p,response_types_supported:c,subject_types_supported:h,response_modes_supported:T}=d;if(!f)throw new Error("METADATA: authorization_endpoint is not set");if(!k)throw new Error("METADATA: token_endpoint is not set");if(!u)throw new Error("METADATA: end_session_endpoint is not set");if(!(p!=null&&p.length))throw new Error("METADATA: grant_types_supported is not set or empty");if(!p.includes(s))throw new Error(`METADATA: grant type "${s}" is not supported. Available only "${p.join('", "')}"`);if(!(c!=null&&c.length))throw new Error("METADATA: response_types_supported is not set or empty");if(!c.includes(r))throw new Error(`METADATA: response type "${r}" is not supported. Available only "${c.join('", "')}"`);if(!(h!=null&&h.length))throw new Error("METADATA: subject_types_supported is not set or empty");if(!h.includes(a))throw new Error(`METADATA: subject type "${a}" is not supported. Available only "${h.join('", "')}"`);if(!(T!=null&&T.length))throw new Error("METADATA: response_modes_supported is not set or empty");const P=["query","fragment"].filter(L=>T.includes(L));if(!P.includes(l))throw new Error(`METADATA: response mode "${l}" is not supported. Available only "${P.join('", "')}"`);return y(this.config,d,e),d}getLoginCallbackData(){const{responseMode:e}=this.config,t={};let o=null,s=null;switch(e){case"query":{const{searchParams:r}=new URL(window.location.href);for(const a of r.keys())t[a]=r.get(a);break}case"fragment":{let{hash:r}=window.location;r!=null&&r.startsWith("#")&&(r=decodeURIComponent(r.substring(1)),r.split("&").forEach(l=>{const[i,...d]=l.split("=");t[i]=d.join("=")}));break}default:throw new Error(`INIT: Unknown response mode type "${e}"`)}if(t.code&&t.state)o=t.state||null,s=t.code||null;else if(t.error&&t.state)throw new Error(`${t.error}: ${t.error_description}`);return{responseId:o,responseCode:s}}async init({oidcClientConfig:e,afterLogin:t}={}){if(e&&(this.config=w.getConfig(e)),!this.config)throw new Error("OIDC Config is not initialised");b(this.config);const{responseId:o,responseCode:s}=this.getLoginCallbackData(),r=o&&s?C(this.config,{id:o}):null;if(o&&s&&!r&&(console.error('Authorisation error: "responseId" and "responseCode" is set, but "loginData" is not found'),await new Promise(a=>{setTimeout(a,5e3)}),window.location.assign(window.location.pathname),await new Promise(()=>{})),o&&s&&r){const{grantType:a,clientId:l,pkce:i}=this.config,{token_endpoint:d}=await this.getMetadataConfig(),{codeVerifier:f,redirectUri:k}=r,u=new URLSearchParams;u.append("grant_type",a),u.append("redirect_uri",k),u.append("code",s),u.append("client_id",l),i&&u.append("code_verifier",f);const p=await fetch(d,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:u.toString()});if(!p.ok||p.status!==200)throw new Error(`LOGIN CALLBACK: Getting authorization token is failed. Error (${p.status}): ${await p.text()}`);const c=await p.json();if(!["bearer","Bearer"].includes(c.token_type))throw new Error(`LOGIN CALLBACK: Token is not bearer type. Actual: ${c.token_type}`);this.setUserTokens(c),t&&await t(this),window.location.assign(k),await new Promise(()=>{})}await this.refreshAccessTokenLoop(),this.initialised=!0,window.addEventListener("storage",a=>{if(a.type==="storage"&&a.storageArea===window.localStorage&&I(this.config,{keyPostfix:S})===a.key){const{accessToken:i}=this.getUserTokens();!a.oldValue&&i?this.emit("login",i):a.oldValue&&!i?this.emit("logout"):a.oldValue&&i&&a.oldValue!==i&&this.emit("accessTokenUpdated",i,a.oldValue)}})}setUserTokens(e){const{access_token:t,refresh_token:o,id_token:s}=e,{accessToken:r}=this.getUserTokens();y(this.config,t,{keyPostfix:S,expireIn:w.getTokenExpireIn(t)}),y(this.config,o,{keyPostfix:v,expireIn:w.getTokenExpireIn(o)}),y(this.config,s,{keyPostfix:U,expireIn:w.getTokenExpireIn(s)}),this.initialised&&(r?r!==e.access_token&&this.emit("accessTokenUpdated",e.access_token,r):this.emit("login",e.access_token))}static getTokenExpireIn(e){return E(e).exp*x}removeUserTokens(){const{accessToken:e}=this.getUserTokens();D(this.config,{keyPostfix:S}),D(this.config,{keyPostfix:v}),D(this.config,{keyPostfix:U}),this.initialised&&e&&this.emit("logout")}getUserTokens(){const{expireIn:e,value:t}=m(this.config,{keyPostfix:S}),{value:o}=m(this.config,{keyPostfix:v}),{value:s}=m(this.config,{keyPostfix:U});return{accessTokenExpireIn:e,accessToken:t,refreshToken:o,idToken:s}}isRefreshRequestsPending(){const e={keyPostfix:"refreshLock",expireIn:Date.now()+10*x},{value:t}=m(this.config,e);return t===!0?!0:(y(this.config,!0,e),!1)}}const O=_.createContext({login:()=>{throw new Error("Node not inside <OidcContextProvider>")},logout:()=>{throw new Error("Node not inside <OidcContextProvider>")},accessToken:null,rawParsedToken:null}),H=({oidcClient:n,children:e})=>{const[t,o]=_.useState(()=>{const{accessToken:s}=n.getUserTokens(),r=s===null?null:E(s);return{login:n.login,logout:n.logout,accessToken:s,rawParsedToken:r}});return _.useEffect(()=>{const s=l=>{const i=E(l);o({login:n.login,logout:n.logout,accessToken:l,rawParsedToken:i})},r=()=>{o({login:n.login,logout:n.logout,accessToken:null,rawParsedToken:null})},a=(l,i)=>{if(l===i)return;const d=E(l);o({login:n.login,logout:n.logout,accessToken:l,rawParsedToken:d})};return n.on("login",s),n.on("logout",r),n.on("accessTokenUpdated",a),()=>{n.off("login",s),n.off("logout",r),n.off("accessTokenUpdated",a)}},[n]),q.jsx(O.Provider,{value:t,children:e})},J=()=>_.useContext(O);exports.OidcClient=w;exports.OidcContextProvider=H;exports.cleanerStart=b;exports.getStoreData=m;exports.getStoreKey=I;exports.getTabStoreData=C;exports.getTabStoreKey=M;exports.jwtDecode=E;exports.oidcContext=O;exports.removeStoreData=D;exports.setStoreData=y;exports.setTabStoreData=$;exports.useOidcContext=J;