Skip to content
This repository was archived by the owner on Jan 5, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions libraries/botbuilder-core/tests/transcriptUtilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,13 @@ const decompressZip = (inputPath, outputPath, callback) => {
.pipe(unzip.Parse())
.on('entry', (entry) => {
if (entry.type === 'File' && entry.path.includes(zipTranscriptsRelativePath)) {
const fileExtractPath = path.join(outputPath, entry.path);
ensureDirectoryExists(fileExtractPath);
entry.pipe(fs.createWriteStream(fileExtractPath)).on('error', console.log);
if (entry.path.indexOf('..') == -1) {
const fileExtractPath = path.join(outputPath, entry.path);
ensureDirectoryExists(fileExtractPath);
entry.pipe(fs.createWriteStream(fileExtractPath)).on('error', console.log);
} else {
console.warn(`Skipping file ${entry.path} as it contains '..' in its path.`);
}
} else {
entry.autodrain();
}
Expand Down
29 changes: 15 additions & 14 deletions libraries/botbuilder/src/channelServiceHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,27 @@ export class ChannelServiceHandler extends ChannelServiceHandlerBase {
}

protected async authenticate(authHeader: string): Promise<ClaimsIdentity> {
if (!authHeader) {
const isAuthDisabled = await this.credentialProvider.isAuthenticationDisabled();
if (!isAuthDisabled) {
throw new StatusCodeError(StatusCodes.UNAUTHORIZED);
}
const isAuthDisabled = await this.credentialProvider.isAuthenticationDisabled();

if (isAuthDisabled) {
// In the scenario where Auth is disabled, we still want to have the
// IsAuthenticated flag set in the ClaimsIdentity. To do this requires
// adding in an empty claim.
// Since ChannelServiceHandler calls are always a skill callback call, we set the skill claim too.
return SkillValidation.createAnonymousSkillClaim();
}
} else {
if (!authHeader) {
throw new StatusCodeError(StatusCodes.UNAUTHORIZED);
}

return JwtTokenValidation.validateAuthHeader(
authHeader,
this.credentialProvider,
this.channelService,
'unknown',
undefined,
this.authConfig,
);
return JwtTokenValidation.validateAuthHeader(
authHeader,
this.credentialProvider,
this.channelService,
'unknown',
undefined,
this.authConfig,
);
}
}
}
2 changes: 1 addition & 1 deletion libraries/botbuilder/tests/channelServiceHandler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const {

const AUTH_HEADER = 'Bearer HelloWorld';
const AUTH_CONFIG = new AuthenticationConfiguration();
const CREDENTIALS = new SimpleCredentialProvider('', '');
const CREDENTIALS = new SimpleCredentialProvider('appId', 'appSecret');
const ACTIVITY = { id: 'testId', type: ActivityTypes.Message };

class NoAuthHandler extends ChannelServiceHandler {
Expand Down
4 changes: 2 additions & 2 deletions libraries/botbuilder/tests/cloudAdapter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,8 @@ describe('CloudAdapter', function () {
const claimsValidators = allowedCallersClaimsValidator(['*']);
const authConfig = new AuthenticationConfiguration([], claimsValidators, validTokenIssuers);
const credentialsFactory = new ConfigurationServiceClientCredentialFactory({
MicrosoftAppId: '',
MicrosoftAppPassword: '',
MicrosoftAppId: 'app-id',
MicrosoftAppPassword: 'app-password',
MicrosoftAppType: '',
MicrosoftAppTenantId: '',
});
Expand Down
59 changes: 34 additions & 25 deletions libraries/botframework-connector/src/auth/jwtTokenValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,9 @@ export namespace JwtTokenValidation {
authConfig = new AuthenticationConfiguration();
}

// eslint-disable-next-line prettier/prettier
if (!authHeader.trim()) { // CodeQL [SM01513] We manually validate incoming tokens. Checking for empty header as part of that.
const isAuthDisabled = await credentials.isAuthenticationDisabled();
if (!isAuthDisabled) {
throw new AuthenticationError(
'Unauthorized Access. Request is not authorized',
StatusCodes.UNAUTHORIZED,
);
}
const isAuthDisabled = await credentials.isAuthenticationDisabled();

if (isAuthDisabled) {
// Check if the activity is for a skill call and is coming from the Emulator.
if (
activity.channelId === Channels.Emulator &&
Expand All @@ -70,18 +63,25 @@ export namespace JwtTokenValidation {
// IsAuthenticated flag set in the ClaimsIdentity. To do this requires
// adding in an empty claim.
return new ClaimsIdentity([], AuthenticationConstants.AnonymousAuthType);
}
} else {
if (!authHeader.trim()) {
throw new AuthenticationError(
'Unauthorized Access. Request is not authorized',
StatusCodes.UNAUTHORIZED,
);
}

const claimsIdentity: ClaimsIdentity = await validateAuthHeader(
authHeader,
credentials,
channelService,
activity.channelId,
activity.serviceUrl,
authConfig,
);
const claimsIdentity: ClaimsIdentity = await validateAuthHeader(
authHeader,
credentials,
channelService,
activity.channelId,
activity.serviceUrl,
authConfig,
);

return claimsIdentity;
return claimsIdentity;
}
}

/**
Expand Down Expand Up @@ -153,8 +153,7 @@ export namespace JwtTokenValidation {
}

if (isPublicAzure(channelService)) {
// eslint-disable-next-line prettier/prettier
if (serviceUrl.trim()) { // CodeQL [SM01513] We manually validate incoming tokens. Checking for empty serviceUrl as part of that.
if (isValidServiceURL(serviceUrl)) {
return await ChannelValidation.authenticateChannelTokenWithServiceUrl(
authHeader,
credentials,
Expand All @@ -167,8 +166,7 @@ export namespace JwtTokenValidation {
}

if (isGovernment(channelService)) {
// eslint-disable-next-line prettier/prettier
if (serviceUrl.trim()) { // CodeQL [SM01513] We manually validate incoming tokens. Checking for empty serviceUrl as part of that.
if (isValidServiceURL(serviceUrl)) {
return await GovernmentChannelValidation.authenticateChannelTokenWithServiceUrl(
authHeader,
credentials,
Expand All @@ -181,8 +179,7 @@ export namespace JwtTokenValidation {
}

// Otherwise use Enterprise Channel Validation
// eslint-disable-next-line prettier/prettier
if (serviceUrl.trim()) { // CodeQL [SM01513] We manually validate incoming tokens. Checking for empty serviceUrl as part of that.
if (isValidServiceURL(serviceUrl)) {
return await EnterpriseChannelValidation.authenticateChannelTokenWithServiceUrl(
authHeader,
credentials,
Expand Down Expand Up @@ -270,6 +267,18 @@ export namespace JwtTokenValidation {
return !channelService || channelService.length === 0;
}

function isValidServiceURL(serviceUrl: string): boolean {
const trimmedUrl = serviceUrl.trim();
const absoluteUrl =
trimmedUrl.startsWith('http') || trimmedUrl.startsWith('wss') ? trimmedUrl : `https://${trimmedUrl}`;
try {
const newUri = new URL(absoluteUrl);
return !!newUri;
} catch {
return false;
}
}

/**
* Determine whether or not a channel service is government
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,22 +85,17 @@ export class ParameterizedBotFrameworkAuthentication extends BotFrameworkAuthent
* @returns The identity validation result.
*/
async authenticateChannelRequest(authHeader: string): Promise<ClaimsIdentity> {
if (!authHeader.trim()) {
const isAuthDisabled = await this.credentialsFactory.isAuthenticationDisabled();
if (!isAuthDisabled) {
if (await this.credentialsFactory.isAuthenticationDisabled()) {
return SkillValidation.createAnonymousSkillClaim();
} else {
if (!authHeader.trim()) {
throw new AuthenticationError(
'Unauthorized Access. Request is not authorized',
StatusCodes.UNAUTHORIZED,
);
}

// In the scenario where auth is disabled, we still want to have the isAuthenticated flag set in the
// ClaimsIdentity. To do this requires adding in an empty claim. Since ChannelServiceHandler calls are
// always a skill callback call, we set the skill claim too.
return SkillValidation.createAnonymousSkillClaim();
return this.JwtTokenValidation_validateAuthHeader(authHeader, 'unknown', null);
}

return this.JwtTokenValidation_validateAuthHeader(authHeader, 'unknown', null);
}

/**
Expand Down Expand Up @@ -213,15 +208,7 @@ export class ParameterizedBotFrameworkAuthentication extends BotFrameworkAuthent
activity: Partial<Activity>,
authHeader: string,
): Promise<ClaimsIdentity> {
if (!authHeader.trim()) {
const isAuthDisabled = await this.credentialsFactory.isAuthenticationDisabled();
if (!isAuthDisabled) {
throw new AuthenticationError(
'Unauthorized Access. Request is not authorized',
StatusCodes.UNAUTHORIZED,
);
}

if (await this.credentialsFactory.isAuthenticationDisabled()) {
// Check if the activity is for a skill call and is coming from the Emulator.
if (activity.channelId === Channels.Emulator && activity.recipient?.role === RoleTypes.Skill) {
return SkillValidation.createAnonymousSkillClaim();
Expand All @@ -231,15 +218,21 @@ export class ParameterizedBotFrameworkAuthentication extends BotFrameworkAuthent
// IsAuthenticated flag set in the ClaimsIdentity. To do this requires
// adding in an empty claim.
return new ClaimsIdentity([], AuthenticationConstants.AnonymousAuthType);
}

const claimsIdentity: ClaimsIdentity = await this.JwtTokenValidation_validateAuthHeader(
authHeader,
activity.channelId,
activity.serviceUrl,
);
} else {
if (!authHeader.trim()) {
throw new AuthenticationError(
'Unauthorized Access. Request is not authorized',
StatusCodes.UNAUTHORIZED,
);
}
const claimsIdentity: ClaimsIdentity = await this.JwtTokenValidation_validateAuthHeader(
authHeader,
activity.channelId,
activity.serviceUrl,
);

return claimsIdentity;
return claimsIdentity;
}
}

private async JwtTokenValidation_validateAuthHeader(
Expand Down
Loading