UNPKG

@dainprotocol/oauth2-token-manager

Version:

A scalable OAuth2 token management library with multi-system support

3 lines 16 kB
'use strict';var crypto=require('crypto'),ironSession=require('iron-session');var m=class{buildUrlParams(e){return Object.entries(e).filter(([,t])=>t!==void 0).map(([t,o])=>`${t}=${encodeURIComponent(o)}`).join("&")}generateAuthorizationUrl(e,r,t){let o={client_id:e.clientId,redirect_uri:e.redirectUri,response_type:"code",scope:e.scopes.join(" "),state:r};(e.usePKCE||e.pkce)&&t&&(o.code_challenge=t,o.code_challenge_method="S256");let i={...e.additionalParams,...e.extraAuthParams};return Object.assign(o,i),`${e.authorizationUrl}?${this.buildUrlParams(o)}`}};var g=class{buildUrlParams(e){return Object.entries(e).filter(([,t])=>t!==void 0).map(([t,o])=>`${t}=${encodeURIComponent(o)}`).join("&")}async exchangeCodeForToken(e,r,t){let o={grant_type:"authorization_code",code:e,redirect_uri:r.redirectUri,client_id:r.clientId};(r.usePKCE||r.pkce)&&t?o.code_verifier=t:r.clientSecret&&(o.client_secret=r.clientSecret);let i=await fetch(r.tokenUrl,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:this.buildUrlParams(o)});if(!i.ok){let x=await i.text();throw new Error(`Token exchange failed: ${i.statusText} - ${x}`)}let n=await i.json(),a=r.responseRootKey?n[r.responseRootKey]:n,p=Date.now(),c=a.expires_in||3600;return {accessToken:a.access_token,refreshToken:a.refresh_token,expiresAt:new Date(p+c*1e3),expiresIn:c,tokenType:a.token_type||"Bearer",scope:a.scope,createdAt:p,raw:n}}async refreshToken(e,r){let t={grant_type:"refresh_token",refresh_token:e,client_id:r.clientId};!(r.usePKCE||r.pkce)&&r.clientSecret&&(t.client_secret=r.clientSecret);let o=await fetch(r.tokenUrl,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:this.buildUrlParams(t)});if(!o.ok){let c=await o.text();throw new Error(`Token refresh failed: ${o.statusText} - ${c}`)}let i=await o.json(),n=r.responseRootKey?i[r.responseRootKey]:i,a=Date.now(),p=n.expires_in||3600;return {accessToken:n.access_token,refreshToken:n.refresh_token||e,expiresAt:new Date(a+p*1e3),expiresIn:p,tokenType:n.token_type||"Bearer",scope:n.scope,createdAt:a,raw:i}}};var u=class{constructor(e,r,t,o){this.config=e;this.authUrlStrategy=r||this.createAuthorizationUrlStrategy(),this.tokenStrategy=t||this.createTokenExchangeStrategy(),this.profileFetcher=o;}authUrlStrategy;tokenStrategy;profileFetcher;async fetchProfile(e){if(!this.profileFetcher)throw new Error("Profile fetcher not configured for this provider");return this.profileFetcher.fetchUserInfo(e)}getProfileEndpoint(){if(!this.profileFetcher)throw new Error("Profile fetcher not configured for this provider");return this.profileFetcher.getEndpoint()}setProfileFetcher(e){this.profileFetcher=e;}hasProfileFetcher(){return !!this.profileFetcher}generateAuthorizationUrl(e,r){return this.authUrlStrategy.generateAuthorizationUrl(this.config,e,r)}async exchangeCodeForToken(e,r){return this.tokenStrategy.exchangeCodeForToken(e,this.config,r)}async refreshToken(e){return this.tokenStrategy.refreshToken(e,this.config)}};var k=class extends u{constructor(e,r,t,o){super(e,r,t,o);}createAuthorizationUrlStrategy(){return new m}createTokenExchangeStrategy(){return new g}};var d=class{constructor(e){this.profileEndpoint=e;}async fetchUserInfo(e){let r=await fetch(this.profileEndpoint,{headers:{Authorization:`Bearer ${e}`,Accept:"application/json",...this.getAdditionalHeaders()}});if(!r.ok)throw new Error(`Failed to fetch profile from ${this.profileEndpoint}: ${r.statusText}`);let t=await r.json();return this.mapToUserProfile(t)}getAdditionalHeaders(){return {}}getEndpoint(){return this.profileEndpoint}};var T=class extends d{constructor(){super("https://www.googleapis.com/oauth2/v2/userinfo");}mapToUserProfile(e){return {email:e.email,name:e.name,id:e.id,avatar:e.picture,username:e.email,raw:e}}};var P=class extends d{constructor(){super("https://api.github.com/user");}mapToUserProfile(e){var r;return {email:e.email,name:e.name||e.login,id:(r=e.id)==null?void 0:r.toString(),avatar:e.avatar_url,username:e.login,raw:e}}getAdditionalHeaders(){return {"User-Agent":"OAuth2-Token-Manager"}}};var v=class extends d{constructor(){super("https://graph.microsoft.com/v1.0/me");}mapToUserProfile(e){return {email:e.mail||e.userPrincipalName,name:e.displayName,id:e.id,avatar:void 0,username:e.userPrincipalName,raw:e}}};var h=class extends d{constructor(r,t,o){super(r);this.mapping=t;this.additionalHeaders=o;}mapToUserProfile(r){return this.mapping?{email:this.getNestedProperty(r,this.mapping.email),name:this.mapping.name?this.getNestedProperty(r,this.mapping.name):void 0,id:this.mapping.id?this.getNestedProperty(r,this.mapping.id):void 0,avatar:this.mapping.avatar?this.getNestedProperty(r,this.mapping.avatar):void 0,username:this.mapping.username?this.getNestedProperty(r,this.mapping.username):void 0,raw:r}:{email:r.email||r.mail||r.emailAddress,name:r.name||r.displayName||r.full_name,id:r.id||r.sub||r.user_id,avatar:r.avatar||r.picture||r.avatar_url,username:r.username||r.login||r.preferred_username,raw:r}}getAdditionalHeaders(){return this.additionalHeaders||{}}getNestedProperty(r,t){return t.split(".").reduce((o,i)=>o==null?void 0:o[i],r)}};var A=class{static createProfileFetcher(e,r,t){if(t!=null&&t.profileUrl)return new h(t.profileUrl,t.profileMapping,t.profileHeaders);switch(e){case "google":return new T;case "github":return new P;case "microsoft":case "outlook":return new v;case "facebook":return new h("https://graph.facebook.com/me?fields=id,name,email,picture");case "generic":default:let o=r.profileUrl||r.userInfoUrl;if(!o)throw new Error(`Profile URL must be provided for ${e} provider`);return new h(o)}}static registerCustomProfileFetcher(e,r){this.customFetchers.set(e,r);}static customFetchers=new Map;static getCustomProfileFetcher(e){return this.customFetchers.get(e)}};var y=class s{static presetConfigs={google:{authorizationUrl:"https://accounts.google.com/o/oauth2/v2/auth",tokenUrl:"https://oauth2.googleapis.com/token",profileUrl:"https://www.googleapis.com/oauth2/v2/userinfo",usePKCE:true,extraAuthParams:{access_type:"offline",prompt:"consent"}},github:{authorizationUrl:"https://github.com/login/oauth/authorize",tokenUrl:"https://github.com/login/oauth/access_token",profileUrl:"https://api.github.com/user"},microsoft:{authorizationUrl:"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",tokenUrl:"https://login.microsoftonline.com/common/oauth2/v2.0/token",profileUrl:"https://graph.microsoft.com/v1.0/me",usePKCE:true},outlook:{authorizationUrl:"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",tokenUrl:"https://login.microsoftonline.com/common/oauth2/v2.0/token",profileUrl:"https://graph.microsoft.com/v1.0/me",usePKCE:true,extraAuthParams:{prompt:"select_account"}},facebook:{authorizationUrl:"https://www.facebook.com/v12.0/dialog/oauth",tokenUrl:"https://graph.facebook.com/v12.0/oauth/access_token",profileUrl:"https://graph.facebook.com/me?fields=id,name,email,picture"}};createProvider(e,r){let t=e!=="generic"?s.presetConfigs[e]||{}:{},o={...t,...r,authorizationUrl:r.authorizationUrl||t.authorizationUrl||"",tokenUrl:r.tokenUrl||t.tokenUrl||"",profileUrl:r.profileUrl||t.profileUrl,extraAuthParams:{...t.extraAuthParams||{},...r.extraAuthParams||{}}},i=A.createProfileFetcher(e,o);return new k(o,void 0,void 0,i)}static registerPreset(e,r){s.presetConfigs[e]=r;}static getPresetConfig(e){return s.presetConfigs[e]}};var l=class{tokens=new Map;states=new Map;profileFetchers=new Map;generateId(){return Math.random().toString(36).substring(2)+Date.now().toString(36)}async saveToken(e){let r=await this.getToken(e.provider,e.email);if(r){let o={...r,...e,id:r.id,createdAt:r.createdAt,updatedAt:new Date};return this.tokens.set(r.id,o),o}let t={...e,id:this.generateId(),createdAt:new Date,updatedAt:new Date};return this.tokens.set(t.id,t),t}async queryTokens(e){let r=Array.from(this.tokens.values());if(e.id&&(r=r.filter(t=>t.id===e.id)),e.provider&&(r=r.filter(t=>t.provider===e.provider)),e.userId&&(r=r.filter(t=>t.userId===e.userId)),e.email&&(r=r.filter(t=>t.email===e.email)),!e.includeExpired){let t=new Date().getTime();r=r.filter(o=>new Date(o.token.expiresAt).getTime()>=t);}return e.offset!==void 0&&(r=r.slice(e.offset)),e.limit!==void 0&&(r=r.slice(0,e.limit)),r}async getToken(e,r){return (await this.queryTokens({provider:e,email:r,includeExpired:true}))[0]||null}async getTokenById(e){return (await this.queryTokens({id:e,includeExpired:true}))[0]||null}async getTokensByUserId(e){return this.queryTokens({userId:e})}async getTokensByEmail(e){return this.queryTokens({email:e})}async getTokensByProvider(e){return this.queryTokens({provider:e})}async getAccounts(e,r){return this.queryTokens({userId:e,provider:r})}async getTokensForEmail(e,r,t){return (await this.queryTokens({userId:e,provider:r,email:t}))[0]||null}async getTokens(e,r){return this.queryTokens({userId:e,provider:r})}async updateToken(e,r){let t=this.tokens.get(e);if(!t)return null;let o={...t,...r.token&&{token:r.token},...r.metadata&&{metadata:{...t.metadata,...r.metadata}},updatedAt:new Date};return this.tokens.set(e,o),o}async deleteToken(e){return this.tokens.delete(e)}async deleteTokenByProviderEmail(e,r){let t=await this.getToken(e,r);return t?this.tokens.delete(t.id):false}async deleteExpiredTokens(){let e=new Date().getTime(),r=Array.from(this.tokens.entries()).filter(([,t])=>new Date(t.token.expiresAt).getTime()<e).map(([t])=>t);return r.forEach(t=>this.tokens.delete(t)),r.length}async saveAuthorizationState(e){let r={...e,createdAt:new Date(Date.now())};return this.states.set(e.state,r),r}async getAuthorizationState(e){return this.states.get(e)||null}async deleteAuthorizationState(e){return this.states.delete(e)}async cleanupExpiredStates(){let e=new Date().getTime(),r=10*60*1e3,t=Array.from(this.states.entries()).filter(([,o])=>e-o.createdAt.getTime()>r).map(([o])=>o);return t.forEach(o=>this.states.delete(o)),t.length}registerProfileFetcher(e,r){this.profileFetchers.set(e,r);}getProfileFetcher(e){return this.profileFetchers.get(e)}getProfileFetchers(){return new Map(this.profileFetchers)}};var b=()=>crypto.randomBytes(32).toString("base64").replace(/[^a-zA-Z0-9]/g,"").substring(0,128),F=s=>crypto.createHash("sha256").update(s).digest("base64url"),C=()=>crypto.randomBytes(16).toString("base64url");var w=class{storage;providerFactory;providers=new Map;providerConfigs=new Map;now;autoRefreshOptions;constructor(e={}){var r,t,o;this.storage=e.storage||new l,this.providerFactory=new y,this.now=Date.now,this.autoRefreshOptions={enabled:((r=e.autoRefresh)==null?void 0:r.enabled)??true,refreshBuffer:((t=e.autoRefresh)==null?void 0:t.refreshBuffer)??10,onRefreshError:(o=e.autoRefresh)==null?void 0:o.onRefreshError},e.providers&&Object.entries(e.providers).forEach(([i,n])=>{this.registerProvider(i,n);});}registerProvider(e,r){this.providerConfigs.set(e,r);let t=this.detectProviderType(e,r),o=this.providerFactory.createProvider(t,r),i=this.storage.getProfileFetcher(e);i&&o.setProfileFetcher(i),this.providers.set(e,o);}async authorize(e){var a;let r=this.providers.get(e.provider);if(!r)throw new Error(`Provider ${e.provider} not found`);let t=C(),o,i;if(((a=this.providerConfigs.get(e.provider))==null?void 0:a.usePKCE)||e.usePKCE){let p=b(),c=F(p);o=r.generateAuthorizationUrl(t,c),i={state:t,codeVerifier:p,config:this.providerConfigs.get(e.provider),metadata:{...e.metadata,userId:e.userId,email:e.email,provider:e.provider}};}else o=r.generateAuthorizationUrl(t),i={state:t,config:this.providerConfigs.get(e.provider),metadata:{...e.metadata,userId:e.userId,email:e.email,provider:e.provider}};return await this.storage.saveAuthorizationState(i),{url:o,state:t}}async handleCallback(e,r){var S,U,O;let t=await this.storage.getAuthorizationState(r);if(!t)throw new Error("Invalid or expired state");let o=(S=t.metadata)==null?void 0:S.provider;if(!o)throw new Error("Provider not found in authorization state");let i=this.providers.get(o);if(!i)throw new Error(`Provider ${o} not found`);let n=await i.exchangeCodeForToken(e,t.codeVerifier),a=(U=t.metadata)==null?void 0:U.userId,p=(O=t.metadata)==null?void 0:O.email;if(!a||!p)throw new Error("User ID and email are required in authorization state");let c;if(i.hasProfileFetcher())try{c=await i.fetchProfile(n.accessToken);}catch(R){console.warn("Failed to fetch user profile:",R);}let x=await this.storage.saveToken({provider:o,userId:a,email:(c==null?void 0:c.email)||p,token:n,metadata:{...t.metadata,profileFetched:!!c}});await this.storage.deleteAuthorizationState(r);let f=this.providerConfigs.get(o);return f!=null&&f.onSuccess&&await f.onSuccess(a,n),{token:x,profile:c}}async getAccessToken(e,r,t={}){return (await this.getValidToken(e,r,t)).accessToken}async getValidToken(e,r,t={}){let o=await this.storage.getToken(e,r);if(!o)throw new Error(`No token found for provider ${e} and email ${r}`);if(!(t.autoRefresh!==false&&this.isTokenExpired(o.token,t)))return o.token;if(!o.token.refreshToken)throw new Error("Token expired and no refresh token available");let n=this.providers.get(e);if(!n)throw new Error(`Provider ${e} not found`);let a=await n.refreshToken(o.token.refreshToken);return await this.storage.updateToken(o.id,{token:a}),a}async queryTokens(e){let r=this.autoRefreshOptions.enabled&&!e.includeExpired?{...e,includeExpired:true}:e,t=await this.storage.queryTokens(r);return this.autoRefreshOptions.enabled&&!e.includeExpired?(await this.refreshTokensIfNeeded(t)).filter(i=>new Date(i.token.expiresAt).getTime()>this.now()):t}async getTokensByUserId(e){return this.queryTokens({userId:e})}async getTokensByEmail(e){return this.queryTokens({email:e})}async deleteToken(e,r){return this.storage.deleteTokenByProviderEmail(e,r)}async cleanupExpiredTokens(){return this.storage.deleteExpiredTokens()}async cleanupExpiredStates(){return this.storage.cleanupExpiredStates()}isTokenExpired(e,r={}){let{expirationBuffer:t=300}=r;if(e.createdAt&&e.expiresIn!==void 0){let n=e.createdAt+e.expiresIn*1e3;return this.now()+t*1e3>=n}let o=new Date(e.expiresAt).getTime();return this.now()+t*1e3>=o}detectProviderType(e,r){var o;let t=r.authorizationUrl.toLowerCase();return t.includes("accounts.google.com")?"google":t.includes("github.com")?"github":t.includes("facebook.com")?"facebook":t.includes("microsoft.com")||t.includes("microsoftonline.com")?e.toLowerCase().includes("outlook")||(o=r.scopes)!=null&&o.some(i=>i.includes("outlook"))?"outlook":"microsoft":"generic"}async refreshTokensIfNeeded(e){let r=e.map(async t=>{if(this.shouldRefreshToken(t))try{let o=this.providers.get(t.provider);if(!o)return console.warn(`Provider ${t.provider} not found for token refresh`),t;if(!t.token.refreshToken)return console.warn(`No refresh token available for ${t.provider}:${t.email}`),t;let i=await o.refreshToken(t.token.refreshToken);return await this.storage.updateToken(t.id,{token:i})||t}catch(o){return this.autoRefreshOptions.onRefreshError&&this.autoRefreshOptions.onRefreshError(o,t),console.error(`Failed to refresh token for ${t.provider}:${t.email}:`,o),t}return t});return Promise.all(r)}shouldRefreshToken(e){if(!e.token.refreshToken)return false;let r=new Date(e.token.expiresAt).getTime(),t=this.now();if(t>=r)return true;let o=this.autoRefreshOptions.refreshBuffer*60*1e3,i=r-o;return t>=i}updateAutoRefreshOptions(e){this.autoRefreshOptions={...this.autoRefreshOptions,...e};}};var ge=(s,e)=>ironSession.sealData(s,{password:e}),ke=(s,e)=>ironSession.unsealData(s,{password:e}); exports.BaseProfileFetcher=d;exports.GenericOAuth2Provider=k;exports.GenericProfileFetcher=h;exports.GitHubProfileFetcher=P;exports.GoogleProfileFetcher=T;exports.InMemoryStorageAdapter=l;exports.MicrosoftProfileFetcher=v;exports.OAuth2Client=w;exports.OAuth2Provider=u;exports.ProfileFetcherFactory=A;exports.StandardAuthorizationUrlStrategy=m;exports.StandardTokenExchangeStrategy=g;exports.createCodeChallenge=F;exports.createCodeVerifier=b;exports.generateState=C;exports.seal=ge;exports.unseal=ke;//# sourceMappingURL=index.cjs.map //# sourceMappingURL=index.cjs.map