From 96b4404eab73fae7fe52f25a1016a5f8b011540a Mon Sep 17 00:00:00 2001 From: Martin Lingstuyl Date: Thu, 22 Aug 2024 22:40:40 +0200 Subject: [PATCH 1/2] Implements accessToken as new authType. Closes #5816 --- src/Auth.ts | 110 ++++++++++++++++++----------- src/config.ts | 1 + src/m365/commands/login.ts | 134 +++++++++++++++++++++++++++++++++++- src/m365/commands/status.ts | 11 ++- src/utils/accessToken.ts | 68 ++++++++++++++++++ 5 files changed, 279 insertions(+), 45 deletions(-) diff --git a/src/Auth.ts b/src/Auth.ts index cc86d97f3fb..767942f63e8 100644 --- a/src/Auth.ts +++ b/src/Auth.ts @@ -108,7 +108,8 @@ export enum AuthType { Certificate, Identity, Browser, - Secret + Secret, + AccessToken } export enum CertificateType { @@ -199,46 +200,43 @@ export class Auth { } public async ensureAccessToken(resource: string, logger: Logger, debug: boolean = false, fetchNew: boolean = false): Promise { - const now: Date = new Date(); - const accessToken: AccessToken | undefined = this.connection.accessTokens[resource]; - const expiresOn: Date = accessToken && accessToken.expiresOn ? - // if expiresOn is serialized from the service file, it's set as a string - // if it's coming from MSAL, it's a Date - typeof accessToken.expiresOn === 'string' ? new Date(accessToken.expiresOn) : accessToken.expiresOn - : new Date(0); + let getTokenPromise: ((resource: string, logger: Logger, debug: boolean, fetchNew: boolean) => Promise) | undefined; - if (!fetchNew && accessToken && expiresOn > now) { - if (debug) { - await logger.logToStderr(`Existing access token ${accessToken.accessToken} still valid. Returning...`); - } - return accessToken.accessToken; - } - else { - if (debug) { - if (!accessToken) { - await logger.logToStderr(`No token found for resource ${resource}.`); + // If the authType is accessToken, we handle returning the token later on. + if (this.connection.authType !== AuthType.AccessToken) { + const accessToken: AccessToken | undefined = this.connection.accessTokens[resource]; + + if (!fetchNew && accessToken && !this.accessTokenExpired(accessToken)) { + if (debug) { + await logger.logToStderr(`Existing access token ${accessToken.accessToken} still valid. Returning...`); } - else { - await logger.logToStderr(`Access token expired. Token: ${accessToken.accessToken}, ExpiresAt: ${accessToken.expiresOn}`); + return accessToken.accessToken; + } + else { + if (debug) { + if (!accessToken) { + await logger.logToStderr(`No token found for resource ${resource}.`); + } + else { + await logger.logToStderr(`Access token expired. Token: ${accessToken.accessToken}, ExpiresAt: ${accessToken.expiresOn}`); + } } } - } - let getTokenPromise: ((resource: string, logger: Logger, debug: boolean, fetchNew: boolean) => Promise) | undefined; - - // When using an application identity, you can't retrieve the access token silently, because there is - // no account. Also (for cert auth) clientApplication is instantiated later - // after inspecting the specified cert and calculating thumbprint if one - // wasn't specified - if (this.connection.authType !== AuthType.Certificate && - this.connection.authType !== AuthType.Secret && - this.connection.authType !== AuthType.Identity) { - this.clientApplication = await this.getPublicClient(logger, debug); - if (this.clientApplication) { - const accounts = await this.clientApplication.getTokenCache().getAllAccounts(); - // if there is an account in the cache and it's active, we can try to get the token silently - if (accounts.filter(a => a.localAccountId === this.connection.identityId).length > 0 && this.connection.active === true) { - getTokenPromise = this.ensureAccessTokenSilent.bind(this); + // When using an application identity, you can't retrieve the access token silently, because there is + // no account. Also (for cert auth) clientApplication is instantiated later + // after inspecting the specified cert and calculating thumbprint if one + // wasn't specified + if (this.connection.authType !== AuthType.Certificate && + this.connection.authType !== AuthType.Secret && + this.connection.authType !== AuthType.Identity) { + this.clientApplication = await this.getPublicClient(logger, debug); + if (this.clientApplication) { + const accounts = await this.clientApplication.getTokenCache().getAllAccounts(); + // if there is an account in the cache and it's active, we can try to get the token silently + if (accounts.filter(a => a.localAccountId === this.connection.identityId).length > 0 && this.connection.active === true) { + getTokenPromise = this.ensureAccessTokenSilent.bind(this); + } } } } @@ -263,6 +261,9 @@ export class Auth { case AuthType.Secret: getTokenPromise = this.ensureAccessTokenWithSecret.bind(this); break; + case AuthType.AccessToken: + getTokenPromise = this.ensureAccessTokenWithAccessToken.bind(this); + break; } } @@ -304,6 +305,17 @@ export class Auth { return response.accessToken; } + public accessTokenExpired(accessToken: AccessToken): boolean { + const now: Date = new Date(); + const expiresOn: Date = accessToken && accessToken.expiresOn ? + // if expiresOn is serialized from the service file, it's set as a string + // if it's coming from MSAL, it's a Date + typeof accessToken.expiresOn === 'string' ? new Date(accessToken.expiresOn) : accessToken.expiresOn + : new Date(0); + + return expiresOn <= now; + } + private async getAuthClientConfiguration(logger: Logger, debug: boolean, certificateThumbprint?: string, certificatePrivateKey?: string, clientSecret?: string): Promise { const msal: typeof Msal = await import('@azure/msal-node'); const { LogLevel } = msal; @@ -420,13 +432,13 @@ export class Auth { } // Asserting identityId because it is expected to be available at this point. - assert(this.connection.identityId !== undefined); + assert(this.connection.identityId !== undefined, "identityId is undefined"); const account = await (this.clientApplication as Msal.ClientApplication) .getTokenCache().getAccountByLocalId(this.connection.identityId); // Asserting account because it is expected to be available at this point. - assert(account !== null); + assert(account !== null, "account is null"); return (this.clientApplication as Msal.ClientApplication).acquireTokenSilent({ account: account, @@ -706,6 +718,24 @@ export class Auth { }); } + private async ensureAccessTokenWithAccessToken(resource: string, logger: Logger, debug: boolean): Promise { + const accessToken: AccessToken | undefined = this.connection.accessTokens[resource]; + + if (!accessToken) { + throw `No token found for resource ${resource}.`; + } + + if (this.accessTokenExpired(accessToken)) { + throw `Access token expired. Token: ${accessToken.accessToken}, ExpiresAt: ${accessToken.expiresOn}`; + } + + if (debug) { + await logger.logToStderr(`Existing access token ${accessToken.accessToken} still valid. Returning...`); + } + + return accessToken; + } + private async calculateThumbprint(certificate: NodeForge.pki.Certificate): Promise { const nodeForge = (await import('node-forge')).default; const { md, asn1, pki } = nodeForge; @@ -878,8 +908,8 @@ export class Auth { public getConnectionDetails(connection: Connection): ConnectionDetails { // Asserting name and identityId because they are optional, but required at this point. - assert(connection.identityName !== undefined); - assert(connection.name !== undefined); + assert(connection.identityName !== undefined, "identity name is undefined"); + assert(connection.name !== undefined, "connection name is undefined"); const details: ConnectionDetails = { connectionName: connection.name, diff --git a/src/config.ts b/src/config.ts index bd0c50f4ffc..285568b249f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,6 +6,7 @@ export default { applicationName: `CLI for Microsoft 365 v${app.packageJson().version}`, delimiter: 'm365\$', cliEntraAppId: process.env.CLIMICROSOFT365_ENTRAAPPID || process.env.CLIMICROSOFT365_AADAPPID || cliEntraAppId, + cliEnvEntraAppId: process.env.CLIMICROSOFT365_ENTRAAPPID || process.env.CLIMICROSOFT365_AADAPPID, tenant: process.env.CLIMICROSOFT365_TENANT || 'common', configstoreName: 'cli-m365-config' }; \ No newline at end of file diff --git a/src/m365/commands/login.ts b/src/m365/commands/login.ts index 8d6c6522b43..4f8e6d6629c 100644 --- a/src/m365/commands/login.ts +++ b/src/m365/commands/login.ts @@ -10,10 +10,11 @@ import config from '../../config.js'; import { settingsNames } from '../../settingsNames.js'; import { zod } from '../../utils/zod.js'; import commands from './commands.js'; +import * as accessTokenUtil from '../../utils/accessToken.js'; const options = globalOptionsZod .extend({ - authType: zod.alias('t', z.enum(['certificate', 'deviceCode', 'password', 'identity', 'browser', 'secret']).optional()), + authType: zod.alias('t', z.enum(['certificate', 'deviceCode', 'password', 'identity', 'browser', 'secret', 'accessToken']).optional()), cloud: z.nativeEnum(CloudType).optional().default(CloudType.Public), userName: zod.alias('u', z.string().optional()), password: zod.alias('p', z.string().optional()), @@ -26,6 +27,7 @@ const options = globalOptionsZod appId: z.string().optional(), tenant: z.string().optional(), secret: zod.alias('s', z.string().optional()), + accessToken: zod.alias('a', z.string().or(z.array(z.string())).optional()), connectionName: z.string().optional() }) .strict(); @@ -64,6 +66,21 @@ class LoginCommand extends Command { }) .refine(options => options.authType !== 'secret' || options.secret, { message: 'Secret is required when using secret authentication' + }) + .refine(options => options.authType !== 'accessToken' || options.accessToken, { + message: 'accessToken is required when using accessToken authentication' + }) + .refine(options => !(options.authType === 'accessToken' && options.accessToken && this.tokensForMultipleTenants(options.accessToken, options.tenant)), { + message: 'The provided accessToken is not for the specified tenant or the access tokens are not for the same tenant' + }) + .refine(options => !(options.authType === 'accessToken' && options.accessToken && this.tokensForMultipleApps(options.accessToken, options.appId)), { + message: 'The provided access token is not for the specified app or the access tokens are not for the same app' + }) + .refine(options => !(options.authType === 'accessToken' && options.accessToken && this.tokensForTheSameResources(options.accessToken)), { + message: 'Specify access tokens that are not for the same resource' + }) + .refine(options => !(options.authType === 'accessToken' && options.accessToken && this.tokenExpired(options.accessToken)), { + message: 'The provided access token has expired' }); } @@ -107,13 +124,37 @@ class LoginCommand extends Command { case 'secret': auth.connection.authType = AuthType.Secret; auth.connection.secret = args.options.secret; + break; + case 'accessToken': + const accessTokens = typeof args.options.accessToken === "string" ? [args.options.accessToken] : args.options.accessToken as string[]; + auth.connection.authType = AuthType.AccessToken; + auth.connection.appId = accessTokenUtil.accessToken.getTenantIdFromAccessToken(accessTokens[0]); + auth.connection.tenant = accessTokenUtil.accessToken.getAppIdFromAccessToken(accessTokens[0]); + + for (const token of accessTokens) { + const resource = accessTokenUtil.accessToken.getAudienceFromAccessToken(token); + const expiresOn = accessTokenUtil.accessToken.getExpirationFromAccessToken(token); + + auth.connection.accessTokens[resource] = { + expiresOn: expiresOn as Date || null, + accessToken: token + }; + }; + break; } auth.connection.cloudType = args.options.cloud; try { - await auth.ensureAccessToken(auth.defaultResource, logger, this.debug); + if (auth.connection.authType !== AuthType.AccessToken) { + await auth.ensureAccessToken(auth.defaultResource, logger, this.debug); + } + else { + for (const resource of Object.keys(auth.connection.accessTokens)) { + await auth.ensureAccessToken(resource, logger, this.debug); + } + } auth.connection.active = true; } catch (error: any) { @@ -123,7 +164,12 @@ class LoginCommand extends Command { await logger.logToStderr(''); } - throw new CommandError(error.message); + if (error instanceof Error) { + throw new CommandError(error.message); + } + else { + throw new CommandError(error); + } } const details = auth.getConnectionDetails(auth.connection); @@ -151,6 +197,88 @@ class LoginCommand extends Command { await this.initAction(args, logger); await this.commandAction(logger, args); } + + private tokensForMultipleTenants(accessTokenValue: string | string[] | undefined, tenantValue: string | undefined): boolean { + const accessTokens = typeof accessTokenValue === "string" ? [accessTokenValue] : accessTokenValue as string[];; + let tenant = tenantValue || config.tenant; + let forMultipleTenants: boolean = false; + + for (const token of accessTokens) { + const tenantIdInAccessToken = accessTokenUtil.accessToken.getTenantIdFromAccessToken(token); + + if (tenant !== 'common' && tenant !== tenantIdInAccessToken) { + forMultipleTenants = true; + break; + } + + tenant = tenantIdInAccessToken; + }; + + return forMultipleTenants; + } + + private tokensForMultipleApps(accessTokenValue: string | string[] | undefined, appIdValue: string | undefined): boolean { + const accessTokens = typeof accessTokenValue === "string" ? [accessTokenValue] : accessTokenValue as string[];; + let appId = appIdValue || config.cliEnvEntraAppId || ''; + let forMultipleApps: boolean = false; + + for (const token of accessTokens) { + const appIdInAccessToken = accessTokenUtil.accessToken.getAppIdFromAccessToken(token); + + if (appId !== '' && appId !== appIdInAccessToken) { + forMultipleApps = true; + break; + } + + appId = appIdInAccessToken; + }; + + return forMultipleApps; + } + + private tokensForTheSameResources(accessTokenValue: string | string[] | undefined): boolean { + const accessTokens = typeof accessTokenValue === "string" ? [accessTokenValue] : accessTokenValue as string[];; + let forTheSameResources: boolean = false; + const resources: string[] = []; + + if ((accessTokens as string[]).length === 1) { + return false; + } + + for (const token of accessTokens) { + const resource = accessTokenUtil.accessToken.getAudienceFromAccessToken(token); + + if (resources.indexOf(resource) > -1) { + forTheSameResources = true; + break; + } + + resources.push(resource); + }; + + return forTheSameResources; + } + + private tokenExpired(accessTokenValue: string | string[] | undefined): boolean { + const accessTokens = typeof accessTokenValue === "string" ? [accessTokenValue] : accessTokenValue as string[];; + let tokenExpired: boolean = false; + + for (const token of accessTokens) { + const expiresOn = accessTokenUtil.accessToken.getExpirationFromAccessToken(token); + + const accessToken = { + expiresOn: expiresOn as Date || null, + accessToken: token + }; + + if (auth.accessTokenExpired(accessToken)) { + tokenExpired = true; + break; + } + }; + + return tokenExpired; + } } export default new LoginCommand(); \ No newline at end of file diff --git a/src/m365/commands/status.ts b/src/m365/commands/status.ts index 836a766f576..c8c98b48f4a 100644 --- a/src/m365/commands/status.ts +++ b/src/m365/commands/status.ts @@ -1,4 +1,4 @@ -import auth from '../../Auth.js'; +import auth, { AuthType } from '../../Auth.js'; import { Logger } from '../../cli/Logger.js'; import Command, { CommandArgs, CommandError } from '../../Command.js'; import commands from './commands.js'; @@ -15,7 +15,14 @@ class StatusCommand extends Command { public async commandAction(logger: Logger): Promise { if (auth.connection.active) { try { - await auth.ensureAccessToken(auth.defaultResource, logger, this.debug); + if (auth.connection.authType !== AuthType.AccessToken) { + await auth.ensureAccessToken(auth.defaultResource, logger, this.debug); + } + else { + for (const resource of Object.keys(auth.connection.accessTokens)) { + await auth.ensureAccessToken(resource, logger, this.debug); + } + } } catch (err: any) { if (this.debug) { diff --git a/src/utils/accessToken.ts b/src/utils/accessToken.ts index cfe2ed99a8c..0b60946c7b6 100644 --- a/src/utils/accessToken.ts +++ b/src/utils/accessToken.ts @@ -96,6 +96,74 @@ export const accessToken = { return userId; }, + getAppIdFromAccessToken(accessToken: string): string { + let appId: string = ''; + + if (!accessToken || accessToken.length === 0) { + return appId; + } + + const chunks = accessToken.split('.'); + if (chunks.length !== 3) { + return appId; + } + + const tokenString: string = Buffer.from(chunks[1], 'base64').toString(); + try { + const token: any = JSON.parse(tokenString); + appId = token.appid; + } + catch { + } + + return appId; + }, + + getAudienceFromAccessToken(accessToken: string): string { + let audience: string = ''; + + if (!accessToken || accessToken.length === 0) { + return audience; + } + + const chunks = accessToken.split('.'); + if (chunks.length !== 3) { + return audience; + } + + const tokenString: string = Buffer.from(chunks[1], 'base64').toString(); + try { + const token: any = JSON.parse(tokenString); + audience = token.aud; + } + catch { + } + + return audience; + }, + + getExpirationFromAccessToken(accessToken: string): Date | undefined { + if (!accessToken || accessToken.length === 0) { + return undefined; + } + + const chunks = accessToken.split('.'); + if (chunks.length !== 3) { + return undefined; + } + + const tokenString: string = Buffer.from(chunks[1], 'base64').toString(); + try { + const token: any = JSON.parse(tokenString); + const expiration = token.exp; + return new Date(expiration * 1000); + } + catch { + } + + return; + }, + /** * Asserts the presence of a delegated access token. * @throws {CommandError} Will throw an error if the access token is not available. From 0535ec88f6b35cad219a52bf5a31b8faf4be8d7d Mon Sep 17 00:00:00 2001 From: Martin Lingstuyl Date: Fri, 23 Aug 2024 15:42:11 +0200 Subject: [PATCH 2/2] Resolve comments --- src/Auth.ts | 83 ++++++----- src/Command.ts | 7 +- src/m365/commands/login.ts | 76 +++++----- src/m365/entra/commands/app/app-add.ts | 2 +- .../entra/commands/m365group/m365group-set.ts | 2 +- .../commands/pim/pim-role-assignment-add.ts | 4 +- src/m365/entra/commands/user/user-set.ts | 4 +- src/m365/file/commands/convert/convert-pdf.ts | 2 +- .../outlook/commands/message/message-list.ts | 2 +- .../commands/message/message-remove.ts | 2 +- .../pa/commands/app/app-permission-remove.ts | 2 +- .../commands/auditlog/auditlog-list.ts | 2 +- .../meeting/meeting-attendancereport-get.ts | 2 +- src/m365/tenant/commands/id/id-get.ts | 2 +- src/m365/tenant/commands/info/info-get.ts | 2 +- .../commands/engage/engage-community-add.ts | 2 +- src/utils/accessToken.ts | 138 +++++------------- 17 files changed, 129 insertions(+), 205 deletions(-) diff --git a/src/Auth.ts b/src/Auth.ts index 767942f63e8..2039c11dfec 100644 --- a/src/Auth.ts +++ b/src/Auth.ts @@ -200,43 +200,46 @@ export class Auth { } public async ensureAccessToken(resource: string, logger: Logger, debug: boolean = false, fetchNew: boolean = false): Promise { - let getTokenPromise: ((resource: string, logger: Logger, debug: boolean, fetchNew: boolean) => Promise) | undefined; + const accessToken: AccessToken | undefined = this.connection.accessTokens[resource]; - // If the authType is accessToken, we handle returning the token later on. - if (this.connection.authType !== AuthType.AccessToken) { - const accessToken: AccessToken | undefined = this.connection.accessTokens[resource]; + if (!fetchNew && accessToken && !this.accessTokenExpired(accessToken)) { + if (debug) { + await logger.logToStderr(`Existing access token ${accessToken.accessToken} still valid. Returning...`); + } - if (!fetchNew && accessToken && !this.accessTokenExpired(accessToken)) { - if (debug) { - await logger.logToStderr(`Existing access token ${accessToken.accessToken} still valid. Returning...`); - } + // If the authType is accessToken, we handle returning the token later on. + if (this.connection.authType !== AuthType.AccessToken) { return accessToken.accessToken; } - else { - if (debug) { - if (!accessToken) { - await logger.logToStderr(`No token found for resource ${resource}.`); - } - else { - await logger.logToStderr(`Access token expired. Token: ${accessToken.accessToken}, ExpiresAt: ${accessToken.expiresOn}`); - } + } + else { + if (debug) { + if (!accessToken) { + await logger.logToStderr(`No token found for resource ${resource}.`); + } + else { + await logger.logToStderr(`Access token expired. Token: ${accessToken.accessToken}, ExpiresAt: ${accessToken.expiresOn}`); } } + } + + let getTokenPromise: ((resource: string, logger: Logger, debug: boolean, fetchNew: boolean) => Promise) | undefined; - // When using an application identity, you can't retrieve the access token silently, because there is - // no account. Also (for cert auth) clientApplication is instantiated later - // after inspecting the specified cert and calculating thumbprint if one - // wasn't specified - if (this.connection.authType !== AuthType.Certificate && - this.connection.authType !== AuthType.Secret && - this.connection.authType !== AuthType.Identity) { - this.clientApplication = await this.getPublicClient(logger, debug); - if (this.clientApplication) { - const accounts = await this.clientApplication.getTokenCache().getAllAccounts(); - // if there is an account in the cache and it's active, we can try to get the token silently - if (accounts.filter(a => a.localAccountId === this.connection.identityId).length > 0 && this.connection.active === true) { - getTokenPromise = this.ensureAccessTokenSilent.bind(this); - } + // When using an application identity, you can't retrieve the access token silently, because there is + // no account. Also (for cert auth) clientApplication is instantiated later + // after inspecting the specified cert and calculating thumbprint if one + // wasn't specified. For accessToken auth, we can't fetch a new token, + // as the tokens are passed in through the login command. + if (this.connection.authType !== AuthType.Certificate && + this.connection.authType !== AuthType.Secret && + this.connection.authType !== AuthType.Identity && + this.connection.authType !== AuthType.AccessToken) { + this.clientApplication = await this.getPublicClient(logger, debug); + if (this.clientApplication) { + const accounts = await this.clientApplication.getTokenCache().getAllAccounts(); + // if there is an account in the cache and it's active, we can try to get the token silently + if (accounts.filter(a => a.localAccountId === this.connection.identityId).length > 0 && this.connection.active === true) { + getTokenPromise = this.ensureAccessTokenSilent.bind(this); } } } @@ -262,7 +265,7 @@ export class Auth { getTokenPromise = this.ensureAccessTokenWithSecret.bind(this); break; case AuthType.AccessToken: - getTokenPromise = this.ensureAccessTokenWithAccessToken.bind(this); + getTokenPromise = this.returnValidAccessTokenForResource.bind(this); break; } } @@ -432,13 +435,13 @@ export class Auth { } // Asserting identityId because it is expected to be available at this point. - assert(this.connection.identityId !== undefined, "identityId is undefined"); + assert(this.connection.identityId !== undefined, 'identityId is undefined'); const account = await (this.clientApplication as Msal.ClientApplication) .getTokenCache().getAccountByLocalId(this.connection.identityId); // Asserting account because it is expected to be available at this point. - assert(account !== null, "account is null"); + assert(account !== null, 'account is null'); return (this.clientApplication as Msal.ClientApplication).acquireTokenSilent({ account: account, @@ -519,7 +522,7 @@ export class Auth { const pemObjs = pem.decode(cert); if (this.connection.thumbprint === undefined) { - const pemCertObj = pemObjs.find(pem => pem.type === "CERTIFICATE"); + const pemCertObj = pemObjs.find(pem => pem.type === 'CERTIFICATE'); const pemCertStr: string = pem.encode(pemCertObj!); const pemCert = pki.certificateFromPem(pemCertStr); @@ -669,11 +672,11 @@ export class Auth { let isNotFoundResponse = false; if (e.error && e.error.Message) { // check if it is Azure Function api 'not found' response - isNotFoundResponse = (e.error.Message.indexOf("No Managed Identity found") !== -1); + isNotFoundResponse = (e.error.Message.indexOf('No Managed Identity found') !== -1); } else if (e.error && e.error.error_description) { // check if it is Azure VM api 'not found' response - isNotFoundResponse = (e.error.error_description === "Identity not found"); + isNotFoundResponse = (e.error.error_description === 'Identity not found'); } if (!isNotFoundResponse) { @@ -718,7 +721,7 @@ export class Auth { }); } - private async ensureAccessTokenWithAccessToken(resource: string, logger: Logger, debug: boolean): Promise { + private async returnValidAccessTokenForResource(resource: string, logger: Logger, debug: boolean): Promise { const accessToken: AccessToken | undefined = this.connection.accessTokens[resource]; if (!accessToken) { @@ -726,7 +729,7 @@ export class Auth { } if (this.accessTokenExpired(accessToken)) { - throw `Access token expired. Token: ${accessToken.accessToken}, ExpiresAt: ${accessToken.expiresOn}`; + throw `Access token for resource '${resource}' expired. ExpiresAt: ${accessToken.expiresOn}`; } if (debug) { @@ -908,8 +911,8 @@ export class Auth { public getConnectionDetails(connection: Connection): ConnectionDetails { // Asserting name and identityId because they are optional, but required at this point. - assert(connection.identityName !== undefined, "identity name is undefined"); - assert(connection.name !== undefined, "connection name is undefined"); + assert(connection.identityName !== undefined, 'identity name is undefined'); + assert(connection.name !== undefined, 'connection name is undefined'); const details: ConnectionDetails = { connectionName: connection.name, diff --git a/src/Command.ts b/src/Command.ts index 44012c8e677..27347c82fed 100644 --- a/src/Command.ts +++ b/src/Command.ts @@ -513,11 +513,12 @@ export default abstract class Command { } private loadValuesFromAccessToken(args: CommandArgs): void { - if (!auth.connection.accessTokens[auth.defaultResource]) { + const resource = Object.keys(auth.connection.accessTokens)[0]; + if (!auth.connection.accessTokens[resource]) { return; } - const token = auth.connection.accessTokens[auth.defaultResource].accessToken; + const token = auth.connection.accessTokens[resource].accessToken; const optionNames: string[] = Object.getOwnPropertyNames(args.options); optionNames.forEach(option => { const value = args.options[option]; @@ -527,7 +528,7 @@ export default abstract class Command { const lowerCaseValue = value.toLowerCase().trim(); if (lowerCaseValue === '@meid' || lowerCaseValue === '@meusername') { - const isAppOnlyAccessToken = accessToken.isAppOnlyAccessToken(auth.connection.accessTokens[auth.defaultResource].accessToken); + const isAppOnlyAccessToken = accessToken.isAppOnlyAccessToken(auth.connection.accessTokens[resource].accessToken); if (isAppOnlyAccessToken) { throw `It's not possible to use ${value} with application permissions`; } diff --git a/src/m365/commands/login.ts b/src/m365/commands/login.ts index 4f8e6d6629c..81a50f74337 100644 --- a/src/m365/commands/login.ts +++ b/src/m365/commands/login.ts @@ -10,7 +10,7 @@ import config from '../../config.js'; import { settingsNames } from '../../settingsNames.js'; import { zod } from '../../utils/zod.js'; import commands from './commands.js'; -import * as accessTokenUtil from '../../utils/accessToken.js'; +import { accessToken as accessTokenUtil } from '../../utils/accessToken.js'; const options = globalOptionsZod .extend({ @@ -70,16 +70,16 @@ class LoginCommand extends Command { .refine(options => options.authType !== 'accessToken' || options.accessToken, { message: 'accessToken is required when using accessToken authentication' }) - .refine(options => !(options.authType === 'accessToken' && options.accessToken && this.tokensForMultipleTenants(options.accessToken, options.tenant)), { + .refine(options => !(options.authType === 'accessToken' && options.accessToken && !this.validatesAccessTokensAreForSingleTenant(options)), { message: 'The provided accessToken is not for the specified tenant or the access tokens are not for the same tenant' }) - .refine(options => !(options.authType === 'accessToken' && options.accessToken && this.tokensForMultipleApps(options.accessToken, options.appId)), { + .refine(options => !(options.authType === 'accessToken' && options.accessToken && !this.validatesAccessTokensAreForSingleApp(options)), { message: 'The provided access token is not for the specified app or the access tokens are not for the same app' }) - .refine(options => !(options.authType === 'accessToken' && options.accessToken && this.tokensForTheSameResources(options.accessToken)), { + .refine(options => !(options.authType === 'accessToken' && options.accessToken && !this.validatesAccessTokensAreForSingleResource(options)), { message: 'Specify access tokens that are not for the same resource' }) - .refine(options => !(options.authType === 'accessToken' && options.accessToken && this.tokenExpired(options.accessToken)), { + .refine(options => !(options.authType === 'accessToken' && options.accessToken && !this.validatesAccessTokensNotExpired(options)), { message: 'The provided access token has expired' }); } @@ -126,14 +126,14 @@ class LoginCommand extends Command { auth.connection.secret = args.options.secret; break; case 'accessToken': - const accessTokens = typeof args.options.accessToken === "string" ? [args.options.accessToken] : args.options.accessToken as string[]; + const accessTokens = typeof args.options.accessToken === 'string' ? [args.options.accessToken] : args.options.accessToken as string[]; auth.connection.authType = AuthType.AccessToken; - auth.connection.appId = accessTokenUtil.accessToken.getTenantIdFromAccessToken(accessTokens[0]); - auth.connection.tenant = accessTokenUtil.accessToken.getAppIdFromAccessToken(accessTokens[0]); + auth.connection.appId = accessTokenUtil.getTenantIdFromAccessToken(accessTokens[0]); + auth.connection.tenant = accessTokenUtil.getAppIdFromAccessToken(accessTokens[0]); for (const token of accessTokens) { - const resource = accessTokenUtil.accessToken.getAudienceFromAccessToken(token); - const expiresOn = accessTokenUtil.accessToken.getExpirationFromAccessToken(token); + const resource = accessTokenUtil.getAudienceFromAccessToken(token); + const expiresOn = accessTokenUtil.getExpirationFromAccessToken(token); auth.connection.accessTokens[resource] = { expiresOn: expiresOn as Date || null, @@ -198,73 +198,66 @@ class LoginCommand extends Command { await this.commandAction(logger, args); } - private tokensForMultipleTenants(accessTokenValue: string | string[] | undefined, tenantValue: string | undefined): boolean { - const accessTokens = typeof accessTokenValue === "string" ? [accessTokenValue] : accessTokenValue as string[];; - let tenant = tenantValue || config.tenant; - let forMultipleTenants: boolean = false; + private validatesAccessTokensAreForSingleTenant(options: Options): boolean { + const accessTokens = typeof options.accessToken === 'string' ? [options.accessToken] : options.accessToken as string[]; + let tenant = options.tenant || config.tenant; for (const token of accessTokens) { - const tenantIdInAccessToken = accessTokenUtil.accessToken.getTenantIdFromAccessToken(token); + const tenantIdInAccessToken = accessTokenUtil.getTenantIdFromAccessToken(token); if (tenant !== 'common' && tenant !== tenantIdInAccessToken) { - forMultipleTenants = true; - break; + return false; } tenant = tenantIdInAccessToken; }; - return forMultipleTenants; + return true; } - private tokensForMultipleApps(accessTokenValue: string | string[] | undefined, appIdValue: string | undefined): boolean { - const accessTokens = typeof accessTokenValue === "string" ? [accessTokenValue] : accessTokenValue as string[];; - let appId = appIdValue || config.cliEnvEntraAppId || ''; - let forMultipleApps: boolean = false; + private validatesAccessTokensAreForSingleApp(options: Options): boolean { + const accessTokens = typeof options.accessToken === 'string' ? [options.accessToken] : options.accessToken as string[]; + let appId = options.appId || config.cliEnvEntraAppId || ''; for (const token of accessTokens) { - const appIdInAccessToken = accessTokenUtil.accessToken.getAppIdFromAccessToken(token); + const appIdInAccessToken = accessTokenUtil.getAppIdFromAccessToken(token); if (appId !== '' && appId !== appIdInAccessToken) { - forMultipleApps = true; - break; + return false; } appId = appIdInAccessToken; }; - return forMultipleApps; + return true; } - private tokensForTheSameResources(accessTokenValue: string | string[] | undefined): boolean { - const accessTokens = typeof accessTokenValue === "string" ? [accessTokenValue] : accessTokenValue as string[];; - let forTheSameResources: boolean = false; + private validatesAccessTokensAreForSingleResource(options: Options): boolean { + const accessTokens = typeof options.accessToken === 'string' ? [options.accessToken] : options.accessToken as string[]; const resources: string[] = []; - if ((accessTokens as string[]).length === 1) { - return false; + if (accessTokens.length === 1) { + return true; } for (const token of accessTokens) { - const resource = accessTokenUtil.accessToken.getAudienceFromAccessToken(token); + const resource = accessTokenUtil.getAudienceFromAccessToken(token); if (resources.indexOf(resource) > -1) { - forTheSameResources = true; - break; + return false; } resources.push(resource); }; - return forTheSameResources; + return true; } - private tokenExpired(accessTokenValue: string | string[] | undefined): boolean { - const accessTokens = typeof accessTokenValue === "string" ? [accessTokenValue] : accessTokenValue as string[];; - let tokenExpired: boolean = false; + private validatesAccessTokensNotExpired(options: Options): boolean { + const accessTokens = typeof options.accessToken === 'string' ? [options.accessToken] : options.accessToken as string[]; for (const token of accessTokens) { - const expiresOn = accessTokenUtil.accessToken.getExpirationFromAccessToken(token); + const expiresOn = accessTokenUtil.getExpirationFromAccessToken(token); const accessToken = { expiresOn: expiresOn as Date || null, @@ -272,12 +265,11 @@ class LoginCommand extends Command { }; if (auth.accessTokenExpired(accessToken)) { - tokenExpired = true; - break; + return false; } }; - return tokenExpired; + return true; } } diff --git a/src/m365/entra/commands/app/app-add.ts b/src/m365/entra/commands/app/app-add.ts index 2a51964bceb..135dd8da83b 100644 --- a/src/m365/entra/commands/app/app-add.ts +++ b/src/m365/entra/commands/app/app-add.ts @@ -268,7 +268,7 @@ class EntraAppAddCommand extends GraphCommand { // directory. If we in the future extend the command with allowing // users to create Microsoft Entra app in a different directory, we'll need to // adjust this - appInfo.tenantId = accessToken.getTenantIdFromAccessToken(auth.connection.accessTokens[auth.defaultResource].accessToken); + appInfo.tenantId = accessToken.getTenantIdFromAccessToken(auth.connection.accessTokens[Object.keys(auth.connection.accessTokens)[0]].accessToken); appInfo = await this.updateAppFromManifest(args, appInfo); appInfo = await this.grantAdminConsent(appInfo, args.options.grantAdminConsent, logger); appInfo = await this.configureUri(args, appInfo, logger); diff --git a/src/m365/entra/commands/m365group/m365group-set.ts b/src/m365/entra/commands/m365group/m365group-set.ts index cb4a19e09c1..30796dc1363 100644 --- a/src/m365/entra/commands/m365group/m365group-set.ts +++ b/src/m365/entra/commands/m365group/m365group-set.ts @@ -176,7 +176,7 @@ class EntraM365GroupSetCommand extends GraphCommand { await this.showDeprecationWarning(logger, aadCommands.M365GROUP_SET, commands.M365GROUP_SET); try { - if ((args.options.allowExternalSenders !== undefined || args.options.autoSubscribeNewMembers !== undefined) && accessToken.isAppOnlyAccessToken(auth.connection.accessTokens[auth.defaultResource].accessToken)) { + if ((args.options.allowExternalSenders !== undefined || args.options.autoSubscribeNewMembers !== undefined) && accessToken.isAppOnlyAccessToken(auth.connection.accessTokens[Object.keys(auth.connection.accessTokens)[0]].accessToken)) { throw `Option 'allowExternalSenders' and 'autoSubscribeNewMembers' can only be used when using delegated permissions.`; } diff --git a/src/m365/entra/commands/pim/pim-role-assignment-add.ts b/src/m365/entra/commands/pim/pim-role-assignment-add.ts index 14179d05b34..5c80b0179ab 100644 --- a/src/m365/entra/commands/pim/pim-role-assignment-add.ts +++ b/src/m365/entra/commands/pim/pim-role-assignment-add.ts @@ -188,7 +188,7 @@ class EntraPimRoleAssignmentAddCommand extends GraphCommand { public async commandAction(logger: Logger, args: CommandArgs): Promise { const { userId, userName, groupId, groupName, startDateTime, endDateTime, ticketNumber, ticketSystem } = args.options; try { - const token = auth.connection.accessTokens[auth.defaultResource].accessToken; + const token = auth.connection.accessTokens[Object.keys(auth.connection.accessTokens)[0]].accessToken; const isAppOnlyAccessToken = accessToken.isAppOnlyAccessToken(token); if (isAppOnlyAccessToken) { @@ -271,7 +271,7 @@ class EntraPimRoleAssignmentAddCommand extends GraphCommand { await logger.logToStderr(`Retrieving id of the current user`); } - const token = auth.connection.accessTokens[auth.defaultResource].accessToken; + const token = auth.connection.accessTokens[Object.keys(auth.connection.accessTokens)[0]].accessToken; return accessToken.getUserIdFromAccessToken(token); } diff --git a/src/m365/entra/commands/user/user-set.ts b/src/m365/entra/commands/user/user-set.ts index 84cf11eadfe..b232f774bf8 100644 --- a/src/m365/entra/commands/user/user-set.ts +++ b/src/m365/entra/commands/user/user-set.ts @@ -253,10 +253,10 @@ class EntraUserSetCommand extends GraphCommand { try { if (args.options.currentPassword) { - if (args.options.id && args.options.id !== accessToken.getUserIdFromAccessToken(auth.connection.accessTokens[auth.defaultResource].accessToken)) { + if (args.options.id && args.options.id !== accessToken.getUserIdFromAccessToken(auth.connection.accessTokens[Object.keys(auth.connection.accessTokens)[0]].accessToken)) { throw `You can only change your own password. Please use --id @meId to reference to your own userId`; } - else if (args.options.userName && args.options.userName.toLowerCase() !== accessToken.getUserNameFromAccessToken(auth.connection.accessTokens[auth.defaultResource].accessToken).toLowerCase()) { + else if (args.options.userName && args.options.userName.toLowerCase() !== accessToken.getUserNameFromAccessToken(auth.connection.accessTokens[Object.keys(auth.connection.accessTokens)[0]].accessToken).toLowerCase()) { throw 'You can only change your own password. Please use --userName @meUserName to reference to your own user principal name'; } } diff --git a/src/m365/file/commands/convert/convert-pdf.ts b/src/m365/file/commands/convert/convert-pdf.ts index 668ecfc3bc5..9fa8fbb4005 100644 --- a/src/m365/file/commands/convert/convert-pdf.ts +++ b/src/m365/file/commands/convert/convert-pdf.ts @@ -80,7 +80,7 @@ class FileConvertPdfCommand extends GraphCommand { let targetIsLocalFile: boolean = true; let error: any; - const isAppOnlyAccessToken: boolean | undefined = accessToken.isAppOnlyAccessToken(auth.connection.accessTokens[auth.defaultResource].accessToken); + const isAppOnlyAccessToken: boolean | undefined = accessToken.isAppOnlyAccessToken(auth.connection.accessTokens[Object.keys(auth.connection.accessTokens)[0]].accessToken); if (typeof isAppOnlyAccessToken === 'undefined') { throw 'Unable to determine authentication type'; } diff --git a/src/m365/outlook/commands/message/message-list.ts b/src/m365/outlook/commands/message/message-list.ts index 51017e1ae8f..5c21378582c 100644 --- a/src/m365/outlook/commands/message/message-list.ts +++ b/src/m365/outlook/commands/message/message-list.ts @@ -142,7 +142,7 @@ class OutlookMessageListCommand extends GraphCommand { public async commandAction(logger: Logger, args: CommandArgs): Promise { try { - if (!args.options.userId && !args.options.userName && accessToken.isAppOnlyAccessToken(auth.connection.accessTokens[auth.defaultResource].accessToken)) { + if (!args.options.userId && !args.options.userName && accessToken.isAppOnlyAccessToken(auth.connection.accessTokens[Object.keys(auth.connection.accessTokens)[0]].accessToken)) { throw 'You must specify either the userId or userName option when using app-only permissions.'; } diff --git a/src/m365/outlook/commands/message/message-remove.ts b/src/m365/outlook/commands/message/message-remove.ts index 71c47ef2ac6..2e79eea991d 100644 --- a/src/m365/outlook/commands/message/message-remove.ts +++ b/src/m365/outlook/commands/message/message-remove.ts @@ -87,7 +87,7 @@ class OutlookMessageRemoveCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - const isAppOnlyAccessToken: boolean | undefined = accessToken.isAppOnlyAccessToken(auth.connection.accessTokens[auth.defaultResource].accessToken); + const isAppOnlyAccessToken: boolean | undefined = accessToken.isAppOnlyAccessToken(auth.connection.accessTokens[Object.keys(auth.connection.accessTokens)[0]].accessToken); let principalUrl = ''; if (isAppOnlyAccessToken) { diff --git a/src/m365/pa/commands/app/app-permission-remove.ts b/src/m365/pa/commands/app/app-permission-remove.ts index 65b0f022865..fc57afc0c9f 100644 --- a/src/m365/pa/commands/app/app-permission-remove.ts +++ b/src/m365/pa/commands/app/app-permission-remove.ts @@ -190,7 +190,7 @@ class PaAppPermissionRemoveCommand extends PowerAppsCommand { return userId; } - return `tenant-${accessToken.getTenantIdFromAccessToken(Auth.connection.accessTokens[Auth.defaultResource].accessToken)}`; + return `tenant-${accessToken.getTenantIdFromAccessToken(Auth.connection.accessTokens[Object.keys(Auth.connection.accessTokens)[0]].accessToken)}`; } } diff --git a/src/m365/purview/commands/auditlog/auditlog-list.ts b/src/m365/purview/commands/auditlog/auditlog-list.ts index 098dd09f8f9..04149c41acd 100644 --- a/src/m365/purview/commands/auditlog/auditlog-list.ts +++ b/src/m365/purview/commands/auditlog/auditlog-list.ts @@ -121,7 +121,7 @@ class PurviewAuditLogListCommand extends O365MgmtCommand { await logger.logToStderr(`Getting audit logs for content type '${args.options.contentType}' within a time frame from '${startTime.toISOString()}' to '${endTime.toISOString()}'.`); } - const tenantId = accessToken.getTenantIdFromAccessToken(Auth.connection.accessTokens[Auth.defaultResource].accessToken); + const tenantId = accessToken.getTenantIdFromAccessToken(Auth.connection.accessTokens[Object.keys(Auth.connection.accessTokens)[0]].accessToken); const contentTypeValue = args.options.contentType === 'DLP' ? 'DLP.All' : 'Audit.' + args.options.contentType; await this.ensureSubscription(tenantId, contentTypeValue); diff --git a/src/m365/teams/commands/meeting/meeting-attendancereport-get.ts b/src/m365/teams/commands/meeting/meeting-attendancereport-get.ts index e6bef437bf1..3509c18628e 100644 --- a/src/m365/teams/commands/meeting/meeting-attendancereport-get.ts +++ b/src/m365/teams/commands/meeting/meeting-attendancereport-get.ts @@ -107,7 +107,7 @@ class TeamsMeetingAttendancereportGetCommand extends GraphCommand { public async commandAction(logger: Logger, args: CommandArgs): Promise { try { - const isAppOnlyAccessToken: boolean | undefined = accessToken.isAppOnlyAccessToken(auth.connection.accessTokens[auth.defaultResource].accessToken); + const isAppOnlyAccessToken: boolean | undefined = accessToken.isAppOnlyAccessToken(auth.connection.accessTokens[Object.keys(auth.connection.accessTokens)[0]].accessToken); if (isAppOnlyAccessToken && !args.options.userId && !args.options.userName && !args.options.email) { throw `The option 'userId', 'userName' or 'email' is required when retrieving meeting attendance report using app only permissions.`; } diff --git a/src/m365/tenant/commands/id/id-get.ts b/src/m365/tenant/commands/id/id-get.ts index adc64f455c3..8d6b7d235fa 100644 --- a/src/m365/tenant/commands/id/id-get.ts +++ b/src/m365/tenant/commands/id/id-get.ts @@ -49,7 +49,7 @@ class TenantIdGetCommand extends Command { public async commandAction(logger: Logger, args: CommandArgs): Promise { let domainName: string | undefined = args.options.domainName; if (!domainName) { - const userName: string = accessToken.getUserNameFromAccessToken(auth.connection.accessTokens[auth.defaultResource].accessToken); + const userName: string = accessToken.getUserNameFromAccessToken(auth.connection.accessTokens[Object.keys(auth.connection.accessTokens)[0]].accessToken); domainName = userName.split('@')[1]; } diff --git a/src/m365/tenant/commands/info/info-get.ts b/src/m365/tenant/commands/info/info-get.ts index 49ab1b5f765..3a7c65647f9 100644 --- a/src/m365/tenant/commands/info/info-get.ts +++ b/src/m365/tenant/commands/info/info-get.ts @@ -75,7 +75,7 @@ class TenantInfoGetCommand extends GraphCommand { const tenantId: string | undefined = args.options.tenantId; if (!domainName && !tenantId) { - const userName: string = accessToken.getUserNameFromAccessToken(auth.connection.accessTokens[auth.defaultResource].accessToken); + const userName: string = accessToken.getUserNameFromAccessToken(auth.connection.accessTokens[Object.keys(auth.connection.accessTokens)[0]].accessToken); domainName = userName.split('@')[1]; } diff --git a/src/m365/viva/commands/engage/engage-community-add.ts b/src/m365/viva/commands/engage/engage-community-add.ts index 81ff01b6461..04d95e66cfc 100644 --- a/src/m365/viva/commands/engage/engage-community-add.ts +++ b/src/m365/viva/commands/engage/engage-community-add.ts @@ -126,7 +126,7 @@ class VivaEngageCommunityAddCommand extends GraphCommand { public async commandAction(logger: Logger, args: CommandArgs): Promise { const { displayName, description, privacy, adminEntraIds, adminEntraUserNames, wait } = args.options; - const isAppOnlyAccessToken = accessToken.isAppOnlyAccessToken(auth.connection.accessTokens[auth.defaultResource].accessToken); + const isAppOnlyAccessToken = accessToken.isAppOnlyAccessToken(auth.connection.accessTokens[Object.keys(auth.connection.accessTokens)[0]].accessToken); if (isAppOnlyAccessToken && !adminEntraIds && !adminEntraUserNames) { this.handleError(`Specify at least one admin using either adminEntraIds or adminEntraUserNames options when using application permissions.`); } diff --git a/src/utils/accessToken.ts b/src/utils/accessToken.ts index 0b60946c7b6..98ce3ee539c 100644 --- a/src/utils/accessToken.ts +++ b/src/utils/accessToken.ts @@ -1,5 +1,5 @@ -import auth from "../Auth.js"; -import { CommandError } from "../Command.js"; +import auth from '../Auth.js'; +import { CommandError } from '../Command.js'; export const accessToken = { isAppOnlyAccessToken(accessToken: string): boolean | undefined { @@ -25,143 +25,71 @@ export const accessToken = { return isAppOnlyAccessToken; }, - getTenantIdFromAccessToken(accessToken: string): string { - let tenantId: string = ''; - + getClaimsFromAccessToken(accessToken: string, ...claimNames: string[]): { [claimName: string]: string | number | undefined } | undefined { if (!accessToken || accessToken.length === 0) { - return tenantId; + return undefined; } const chunks = accessToken.split('.'); if (chunks.length !== 3) { - return tenantId; + return undefined; } const tokenString: string = Buffer.from(chunks[1], 'base64').toString(); try { const token: any = JSON.parse(tokenString); - tenantId = token.tid; - } - catch { - } - return tenantId; - }, + const claimsObject: { [claimName: string]: string | number | undefined } = claimNames.reduce((claimsObject: any, claimName: string) => { + const claimValue = token[claimName]; - getUserNameFromAccessToken(accessToken: string): string { - let userName: string = ''; + if (claimValue) { + claimsObject[claimName] = token[claimName]; + } - if (!accessToken || accessToken.length === 0) { - return userName; - } + return claimsObject; + }, {}); - const chunks = accessToken.split('.'); - if (chunks.length !== 3) { - return userName; - } - - const tokenString: string = Buffer.from(chunks[1], 'base64').toString(); - try { - const token: any = JSON.parse(tokenString); - // if authenticated using certificate, there is no upn so use - // app display name instead - userName = token.upn || token.app_displayname; + return claimsObject; } catch { } - return userName; + return; }, - getUserIdFromAccessToken(accessToken: string): string { - let userId: string = ''; - - if (!accessToken || accessToken.length === 0) { - return userId; - } - - const chunks = accessToken.split('.'); - if (chunks.length !== 3) { - return userId; - } + getTenantIdFromAccessToken(accessToken: string): string { + const claims = this.getClaimsFromAccessToken(accessToken, 'tid'); + return claims?.tid as string || ''; + }, - const tokenString: string = Buffer.from(chunks[1], 'base64').toString(); - try { - const token: any = JSON.parse(tokenString); - userId = token.oid; - } - catch { - } + getUserNameFromAccessToken(accessToken: string): string { + const claims = this.getClaimsFromAccessToken(accessToken, 'upn', 'app_displayname'); + return claims?.upn as string || claims?.app_displayname as string || ''; + }, - return userId; + getUserIdFromAccessToken(accessToken: string): string { + const claims = this.getClaimsFromAccessToken(accessToken, 'oid'); + return claims?.oid as string || ''; }, getAppIdFromAccessToken(accessToken: string): string { - let appId: string = ''; - - if (!accessToken || accessToken.length === 0) { - return appId; - } - - const chunks = accessToken.split('.'); - if (chunks.length !== 3) { - return appId; - } - - const tokenString: string = Buffer.from(chunks[1], 'base64').toString(); - try { - const token: any = JSON.parse(tokenString); - appId = token.appid; - } - catch { - } - - return appId; + const claims = this.getClaimsFromAccessToken(accessToken, 'appid'); + return claims?.appid as string || ''; }, getAudienceFromAccessToken(accessToken: string): string { - let audience: string = ''; - - if (!accessToken || accessToken.length === 0) { - return audience; - } - - const chunks = accessToken.split('.'); - if (chunks.length !== 3) { - return audience; - } - - const tokenString: string = Buffer.from(chunks[1], 'base64').toString(); - try { - const token: any = JSON.parse(tokenString); - audience = token.aud; - } - catch { - } - - return audience; + const claims = this.getClaimsFromAccessToken(accessToken, 'aud'); + return claims?.aud as string || ''; }, getExpirationFromAccessToken(accessToken: string): Date | undefined { - if (!accessToken || accessToken.length === 0) { - return undefined; - } + const claims = this.getClaimsFromAccessToken(accessToken, 'exp'); - const chunks = accessToken.split('.'); - if (chunks.length !== 3) { + if (!claims?.exp) { return undefined; } - const tokenString: string = Buffer.from(chunks[1], 'base64').toString(); - try { - const token: any = JSON.parse(tokenString); - const expiration = token.exp; - return new Date(expiration * 1000); - } - catch { - } - - return; + return new Date(claims.exp as number * 1000); }, /** @@ -170,7 +98,7 @@ export const accessToken = { * @throws {CommandError} Will throw an error if the access token is an application-only access token. */ assertDelegatedAccessToken(): void { - const accessToken = auth?.connection?.accessTokens?.[auth.defaultResource]?.accessToken; + const accessToken = auth?.connection?.accessTokens?.[Object.keys(auth.connection.accessTokens)[0]]?.accessToken; if (!accessToken) { throw new CommandError('No access token found.'); }