@primno/dataverse-auth
Version:
Authenticate to Dataverse and Dynamics 365 with a connection string or OAuth 2.0. Provides a token persistence cache.
2 lines (1 loc) • 8.55 kB
JavaScript
import{LogLevel as e,ConfidentialClientApplication as t,PublicClientApplication as r}from"@azure/msal-node";import i from"axios";import{DataProtectionScope as n,PersistenceCreator as s,PersistenceCachePlugin as o}from"@azure/msal-node-extensions";import{parseConnectionString as a}from"@tediousjs/connection-string";class c{constructor(){i.defaults.validateStatus=()=>!0}async sendGetRequestAsync(e,t){const r={method:"get",url:e,headers:t&&t.headers},n=await i(r);return{headers:n.headers,body:n.data,status:n.status}}async sendPostRequestAsync(e,t){const r={method:"post",url:e,data:t&&t.body||"",headers:t&&t.headers},n=await i(r);return{headers:n.headers,body:n.data,status:n.status}}}async function u(e){const t={serviceName:"Primno.DataverseClient",accountName:"MSALCache",dataProtectionScope:n.CurrentUser,usePlaintextFileOnLinux:!1,...e},r=await s.createPersistence(t);return{cachePlugin:new o(r)}}class l{constructor(e,t){this.oAuthOptions=e,this.supportedApplication=t}async getClient(){if(null==this.application){const i=await async function(i){const{credentials:n,persistence:s}=i,o={auth:{clientId:n.clientId??"51f81489-12ee-4a9e-aaae-a2591f45987d",authority:n.authorityUrl,knownAuthorities:[n.authorityUrl]},system:{networkClient:new c,loggerOptions:{loggerCallback(){},logLevel:e.Verbose,piiLoggingEnabled:!1}},cache:s?.enabled?await u(s):void 0};return n.clientSecret||n.clientCertificate?{type:"confidential",client:new t({...o,auth:{...o.auth,clientSecret:n.clientSecret,clientCertificate:n.clientCertificate}})}:{type:"public",client:new r(o)}}(this.oAuthOptions);if(!this.supportedApplication.includes(i.type))throw new Error(`Unsupported application type: ${i.type}`);this.application=i}return this.application.client}async tryGetAccountFromCache(e){if("public"===this.application?.type){const t=this.application.client.getTokenCache();return(await t.getAllAccounts()).find((t=>t.username.toLocaleLowerCase()===e?.toLocaleLowerCase()))}}get url(){return this.oAuthOptions.url}async getToken(){const e=await this.getClient(),{scope:t,userName:r}=this.oAuthOptions.credentials,i=await this.tryGetAccountFromCache(r);if(null!=i){const r=await e.acquireTokenSilent({scopes:[t],account:i});if(null==r)throw new Error("Unable to acquire token silently");return r.accessToken}{const t=await this.acquireToken(e);if(null==t)throw new Error("Unable to acquire token");return t.accessToken}}}class h extends l{constructor(e){if(null==e.deviceCodeCallback)throw new Error("Device code callback is required for device code flow");if(null==e.credentials.userName)throw new Error("Username is required for device code flow to prevent multiple device code requests");super(e,["public"])}async acquireToken(e){const{scope:t}=this.oAuthOptions.credentials,{deviceCodeCallback:r}=this.oAuthOptions,i=await e.acquireTokenByDeviceCode({scopes:[t],deviceCodeCallback:r});if(null!=i&&i.account?.username?.toLowerCase()!==this.oAuthOptions.credentials.userName?.toLowerCase())throw new Error("Device code was not issued for the correct user. Please check your username.");return i}}class d extends l{constructor(e){if(null==e.credentials.userName||null==e.credentials.password)throw new Error("Username and password are required for password flow");super(e,["confidential","public"])}async acquireToken(e){const{scope:t,userName:r,password:i}=this.oAuthOptions.credentials;return await e.acquireTokenByUsernamePassword({scopes:[t],username:r,password:i})}}class p extends l{constructor(e){if(null==e.credentials.clientId||null==e.credentials.clientSecret)throw new Error("Client ID and secret are required for client credential flow");super(e,["confidential"])}async acquireToken(e){const{scope:t}=this.oAuthOptions.credentials;return await e.acquireTokenByClientCredential({scopes:[t]})}}class w{constructor(e){this.tokenProvider=this.getTokenProvider(e)}getTokenProvider(e){switch(e.credentials.grantType){case"device_code":return new h(e);case"password":return new d(e);case"client_credential":return new p(e);default:throw new Error("Invalid grant type")}}get url(){return this.tokenProvider.url}async getToken(){return this.tokenProvider.getToken()}}const f="";function m(e){return null==e||e===f}function y(e,t){for(const r of t)if(null!=e[r.toLowerCase()])return e[r.toLowerCase()]}function C(e){if(null!=e)return"true"===e.toLowerCase()}async function v(e){const t="Bearer",r=i.create({baseURL:e,validateStatus:()=>!0,maxRedirects:0}),n=await r.request({method:"GET",url:"api/discovery/?SDKClientVersion=9.1"});if(null==n.headers)throw new Error("Unable to discover authority");const s=n.headers["www-authenticate"]?.trim();if(s?.startsWith(t)){const e=s.substring(6).split(",").map((e=>{const t=e.trim().split("=");return{key:t[0],value:t[1]}})),t=e.find((e=>"authorization_uri"==e.key))?.value,r=e.find((e=>"resource_id"==e.key))?.value;if(!m(t))return{authUrl:t,resource:r}}throw new Error("Unable to discover authority")}const g=["ServiceUri","Service Uri","Url","Server"],k=["UserName","User Name","UserId","User Id"],A=["Password"],U=["Domain"],S=["HomeRealmUri","Home Realm Uri"],T=["AuthType","AuthenticationType"],P=["RequireNewInstance"],b=["ClientId","AppId","ApplicationId"],N=["RedirectUri","ReplyUrl"],I=["TokenCacheStorePath"],q=["LoginPrompt"],E=["CertificateThumbprint","Thumbprint"],L=["CertificateStoreName","StoreName"],O=["SkipDiscovery"],D=["Integrated Security"],R=["ClientSecret","Secret"];var x,_;!function(e){e[e.AD=0]="AD",e[e.OAuth=1]="OAuth",e[e.Office365=2]="Office365",e[e.Certificate=3]="Certificate",e[e.ClientSecret=4]="ClientSecret"}(x||(x={})),function(e){e[e.Auto=0]="Auto",e[e.Always=1]="Always",e[e.Never=2]="Never"}(_||(_={}));class z{constructor(e){this.connectionString=e;const t=a(e);this.authType=this.parseAuthenticationType(y(t,T)),this.serviceUri=y(t,g),this.userName=y(t,k),this.password=y(t,A),this.clientId=y(t,b),this.clientSecret=y(t,R),this.redirectUri=y(t,N),this.domain=y(t,U),this.tokenCacheStorePath=y(t,I),this.certStoreName=y(t,L),this.certThumbprint=y(t,E),this.homeRealmUri=y(t,S),this.requireNewInstance=C(y(t,P)),this.loginPrompt=this.parseLoginPrompt(y(t,q)),this.skipDiscovery=C(y(t,O)),this.integratedSecurity=y(t,D),this.authType==x.OAuth&&m(this.clientId)&&m(this.redirectUri)&&(this.clientId="51f81489-12ee-4a9e-aaae-a2591f45987d",this.redirectUri="app://58145B91-0C36-4500-8554-080854F2AC97")}parseLoginPrompt(e){switch(e?.toLowerCase()){case"auto":return _.Auto;case"always":return _.Always;case"never":return _.Never}}parseAuthenticationType(e){switch(e?.toLowerCase()){case"oauth":return x.OAuth;case"certificate":return x.Certificate;case"clientsecret":return x.ClientSecret;case"office365":return x.Office365;case"ad":return x.AD}}toString(){return this.connectionString}}function B(e){if(null!=e.certStoreName||null!=e.certThumbprint)throw new Error("Certificate authentication is not supported");if(null!=e.userName&&null!=e.password)return"password";if(null!=e.clientSecret)return"client_credential";if(null==e.loginPrompt||e.loginPrompt===_.Always||e.loginPrompt===_.Auto)return"device_code";throw new Error("Unable to choose a grant type")}class F{constructor(e,t){if(this.options=t,"string"==typeof e)this.csp=new z(e);else{if(!(e instanceof z))throw new Error("Invalid connection string");this.csp=e}if(null==this.csp.serviceUri)throw new Error("Service URI is missing");if(null==this.csp.authType)throw new Error("Authentication type is missing")}get url(){return this.csp.serviceUri}async getTokenProvider(){if(null==this.tokenProvider)switch(this.csp.authType){case x.OAuth:{const e=await v(this.csp.serviceUri),t=function(e,t){if(null==e.clientId)throw new Error("Connection string is missing client id");return{clientId:e.clientId,grantType:B(e),userName:e.userName,password:e.password,redirectUri:e.redirectUri,clientSecret:e.clientSecret,scope:`${t.resource}/.default`,authorityUrl:t.authUrl.replace("oauth2/authorize","").replace("common","organizations")}}(this.csp,e),r={credentials:t,persistence:{enabled:null!=this.csp.tokenCacheStorePath,cachePath:this.csp.tokenCacheStorePath,...this.options?.oAuth?.persistence},deviceCodeCallback:this.options?.oAuth?.deviceCodeCallback,url:this.csp.serviceUri};this.tokenProvider=new w(r);break}default:throw new Error("Unsupported authentication type")}return this.tokenProvider}async getToken(){const e=await this.getTokenProvider();return await e.getToken()}}export{x as AuthenticationType,F as ConnStringTokenProvider,z as ConnectionString,_ as LoginPromptType,w as OAuthTokenProvider,v as discoverAuthority};