Skip to content

Commit 9e60a74

Browse files
committed
Remove "Bearer" from speech token
1 parent 69925bd commit 9e60a74

7 files changed

Lines changed: 220 additions & 2 deletions

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/bundle/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@
196196
"@msinternal/react-dom-umd": "0.0.0-0",
197197
"@msinternal/react-is": "0.0.0-0",
198198
"@msinternal/react-umd": "0.0.0-0",
199+
"@testduet/given-when-then": "^0.1.0",
199200
"@types/dom-speech-recognition": "^0.0.7",
200201
"@types/mdast": "^4.0.4",
201202
"@types/node": "^25.3.3",

packages/bundle/src/createCognitiveServicesSpeechServicesPonyfillFactory.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { AudioConfig } from 'microsoft-cognitiveservices-speech-sdk';
33
import { createSpeechServicesPonyfill } from 'web-speech-cognitive-services';
44

55
import createMicrophoneAudioConfigAndAudioContext from './speech/createMicrophoneAudioConfigAndAudioContext';
6+
import removeBearerInAuthorizationToken from './speech/removeBearerInAuthorizationToken';
67
import CognitiveServicesAudioOutputFormat from './types/CognitiveServicesAudioOutputFormat';
78
import CognitiveServicesCredentials from './types/CognitiveServicesCredentials';
89
import CognitiveServicesTextNormalization from './types/CognitiveServicesTextNormalization';
@@ -11,7 +12,7 @@ export default function createCognitiveServicesSpeechServicesPonyfillFactory({
1112
audioConfig,
1213
audioContext,
1314
audioInputDeviceId,
14-
credentials,
15+
credentials: rawCredentials,
1516
enableTelemetry,
1617
initialSilenceTimeout,
1718
speechRecognitionEndpointId,
@@ -61,7 +62,7 @@ export default function createCognitiveServicesSpeechServicesPonyfillFactory({
6162
createSpeechServicesPonyfill({
6263
audioConfig,
6364
audioContext,
64-
credentials,
65+
credentials: removeBearerInAuthorizationToken(rawCredentials),
6566
enableTelemetry,
6667
initialSilenceTimeout,
6768
referenceGrammars: referenceGrammarID ? [`luis/${referenceGrammarID}-PRODUCTION`] : [],
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/* eslint-disable @typescript-eslint/no-empty-function */
2+
import isPromiseLike from './isPromiseLike';
3+
4+
test('Promise.withResolvers() should return true', () =>
5+
expect(isPromiseLike(Promise.withResolvers().promise)).toBe(true));
6+
7+
test('new Promise() should return true', () => expect(isPromiseLike(new Promise(() => {}))).toBe(true));
8+
9+
test('Promise-like should return true', () => expect(isPromiseLike({ then: () => {} })).toBe(true));
10+
11+
test('Boolean should return false', () => expect(isPromiseLike(true)).toBe(false));
12+
13+
test('Number should return false', () => expect(isPromiseLike(0)).toBe(false));
14+
15+
test('Object should return false', () => expect(isPromiseLike({ then: 0 })).toBe(false));
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default function isPromiseLike<T>(promiseLike: unknown): promiseLike is Promise<T> {
2+
return (
3+
!!promiseLike && typeof promiseLike === 'object' && 'then' in promiseLike && typeof promiseLike.then === 'function'
4+
);
5+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { scenario } from '@testduet/given-when-then';
2+
import type RemoveBearerInAuthorizationTokenType from './removeBearerInAuthorizationToken';
3+
4+
let removeBearerInAuthorizationToken: typeof RemoveBearerInAuthorizationTokenType;
5+
let consoleWarn: jest.SpyInstance;
6+
7+
beforeAll(() => {
8+
// eslint-disable-next-line @typescript-eslint/no-empty-function
9+
consoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => {});
10+
11+
jest.mock('@msinternal/botframework-webchat-base/utils', () => ({
12+
...jest.requireActual('@msinternal/botframework-webchat-base/utils'),
13+
warnOnce:
14+
(...args) =>
15+
() =>
16+
console.warn(...args)
17+
}));
18+
19+
removeBearerInAuthorizationToken = require('./removeBearerInAuthorizationToken').default;
20+
});
21+
22+
afterEach(() => consoleWarn.mockClear());
23+
24+
scenario('Authorization token prefixed with "Bearer" in ', bdd => {
25+
bdd
26+
.given('resolved value', () => ({
27+
authorizationToken: 'Bearer XYZ',
28+
region: 'westus'
29+
}))
30+
.when('removeBearerInAuthorizationToken', precondition => removeBearerInAuthorizationToken(precondition))
31+
.then('should match snapshot', (_, result) =>
32+
expect(result).toEqual({
33+
authorizationToken: 'XYZ',
34+
region: 'westus'
35+
})
36+
)
37+
.and('should have warned', () => expect(consoleWarn).toHaveBeenCalledTimes(1));
38+
39+
bdd
40+
.given('Promise value', () =>
41+
Promise.resolve({
42+
authorizationToken: 'Bearer XYZ',
43+
region: 'westus'
44+
})
45+
)
46+
.when('removeBearerInAuthorizationToken', precondition => removeBearerInAuthorizationToken(precondition))
47+
.then('should match snapshot', (_, result) =>
48+
expect(result).toEqual({
49+
authorizationToken: 'XYZ',
50+
region: 'westus'
51+
})
52+
)
53+
.and('should have warned', () => expect(consoleWarn).toHaveBeenCalledTimes(1));
54+
55+
bdd
56+
.given('callback of a resolved value', () => () => ({
57+
authorizationToken: 'Bearer XYZ',
58+
region: 'westus'
59+
}))
60+
.when('removeBearerInAuthorizationToken', precondition => (removeBearerInAuthorizationToken(precondition) as any)())
61+
.then('should match snapshot', (_, result) =>
62+
expect(result).toEqual({
63+
authorizationToken: 'XYZ',
64+
region: 'westus'
65+
})
66+
)
67+
.and('should have warned', () => expect(consoleWarn).toHaveBeenCalledTimes(1));
68+
69+
bdd
70+
.given(
71+
'callback of a Promise',
72+
() => () =>
73+
Promise.resolve({
74+
authorizationToken: 'Bearer XYZ',
75+
region: 'westus'
76+
})
77+
)
78+
.when('removeBearerInAuthorizationToken', precondition => (removeBearerInAuthorizationToken(precondition) as any)())
79+
.then('should match snapshot', async (_, result) =>
80+
expect(await result).toEqual({
81+
authorizationToken: 'XYZ',
82+
region: 'westus'
83+
})
84+
)
85+
.and('should have warned', () => expect(consoleWarn).toHaveBeenCalledTimes(1));
86+
});
87+
88+
scenario('Authorization token with no prefix in ', bdd => {
89+
bdd
90+
.given('resolved value', () => ({
91+
authorizationToken: 'XYZ',
92+
region: 'westus'
93+
}))
94+
.when('removeBearerInAuthorizationToken', precondition => removeBearerInAuthorizationToken(precondition))
95+
.then('should left untouched', (precondition, result) => expect(result).toBe(precondition))
96+
.and('should not have warned', () => expect(consoleWarn).not.toHaveBeenCalled());
97+
98+
bdd
99+
.given('Promise value', () =>
100+
Promise.resolve({
101+
authorizationToken: 'XYZ',
102+
region: 'westus'
103+
})
104+
)
105+
.when('removeBearerInAuthorizationToken', precondition => removeBearerInAuthorizationToken(precondition))
106+
.then('should match snapshot', (_, result) =>
107+
expect(result).toEqual({
108+
authorizationToken: 'XYZ',
109+
region: 'westus'
110+
})
111+
)
112+
.and('should not have warned', () => expect(consoleWarn).not.toHaveBeenCalled());
113+
114+
bdd
115+
.given('callback of a resolved value', () => () => ({
116+
authorizationToken: 'XYZ',
117+
region: 'westus'
118+
}))
119+
.when('removeBearerInAuthorizationToken', precondition => (removeBearerInAuthorizationToken(precondition) as any)())
120+
.then('should match snapshot', (_, result) =>
121+
expect(result).toEqual({
122+
authorizationToken: 'XYZ',
123+
region: 'westus'
124+
})
125+
)
126+
.and('should not have warned', () => expect(consoleWarn).not.toHaveBeenCalled());
127+
128+
bdd
129+
.given(
130+
'callback of a Promise',
131+
() => () =>
132+
Promise.resolve({
133+
authorizationToken: 'XYZ',
134+
region: 'westus'
135+
})
136+
)
137+
.when('removeBearerInAuthorizationToken', precondition => (removeBearerInAuthorizationToken(precondition) as any)())
138+
.then('should match snapshot', async (_, result) =>
139+
expect(await result).toEqual({
140+
authorizationToken: 'XYZ',
141+
region: 'westus'
142+
})
143+
)
144+
.and('should not have warned', () => expect(consoleWarn).not.toHaveBeenCalled());
145+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { warnOnce } from '@msinternal/botframework-webchat-base/utils';
2+
import CognitiveServicesCredentials from '../types/CognitiveServicesCredentials';
3+
import isPromiseLike from './isPromiseLike';
4+
5+
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
6+
type CognitiveServicesBaseCredentials = Exclude<Exclude<CognitiveServicesCredentials, Function>, Promise<any>>;
7+
8+
const BEARER_PREFIX_LOWERCASE = 'bearer ';
9+
10+
const warnBearerPrefix = warnOnce('botframework-webchat: Remove "Bearer" from the speech authorization token.');
11+
12+
function removeBearerInAuthorizationTokenForResolved(
13+
credentials: CognitiveServicesBaseCredentials
14+
): CognitiveServicesBaseCredentials {
15+
if (
16+
'authorizationToken' in credentials &&
17+
credentials.authorizationToken?.toLowerCase().startsWith(BEARER_PREFIX_LOWERCASE)
18+
) {
19+
warnBearerPrefix();
20+
21+
return Object.freeze({
22+
...credentials,
23+
authorizationToken: credentials.authorizationToken.substring(BEARER_PREFIX_LOWERCASE.length)
24+
});
25+
}
26+
27+
return credentials;
28+
}
29+
30+
function removeBearerInAuthorizationTokenForPromiseOrResolved(
31+
credentials: CognitiveServicesBaseCredentials | Promise<CognitiveServicesBaseCredentials>
32+
): CognitiveServicesBaseCredentials | Promise<CognitiveServicesBaseCredentials> {
33+
if (isPromiseLike<CognitiveServicesBaseCredentials>(credentials)) {
34+
return credentials.then(credentials => removeBearerInAuthorizationTokenForResolved(credentials));
35+
}
36+
37+
return removeBearerInAuthorizationTokenForResolved(credentials);
38+
}
39+
40+
function removeBearerInAuthorizationToken(credentials: CognitiveServicesCredentials) {
41+
if (typeof credentials === 'function') {
42+
return (() => removeBearerInAuthorizationTokenForPromiseOrResolved(credentials())) satisfies () =>
43+
| CognitiveServicesBaseCredentials
44+
| Promise<CognitiveServicesBaseCredentials> as CognitiveServicesCredentials;
45+
}
46+
47+
return removeBearerInAuthorizationTokenForPromiseOrResolved(credentials);
48+
}
49+
50+
export default removeBearerInAuthorizationToken;

0 commit comments

Comments
 (0)