Skip to content

Commit 6774bc8

Browse files
committed
feat(msteams): support federated credentials and certificate auth (#40855)
Add support for alternative authentication methods in the MS Teams channel, enabling passwordless bot authentication for enterprise tenants that block client secrets via tenant-wide policy. New config fields under channels.msteams: - authType: 'clientSecret' (default) | 'certificate' | 'federatedCredential' - certPemFile / certKeyFile / sendX5C: for certificate-based auth - ficClientId: FIC client ID for federated credential auth - widAssertionFile: workload identity assertion file path Fully backward compatible: existing appId + appPassword configs continue to work unchanged.
1 parent fa3fafd commit 6774bc8

File tree

5 files changed

+113
-17
lines changed

5 files changed

+113
-17
lines changed

extensions/msteams/src/errors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ export function formatMSTeamsSendErrorHint(
193193
classification: MSTeamsSendErrorClassification,
194194
): string | undefined {
195195
if (classification.kind === "auth") {
196-
return "check msteams appId/appPassword/tenantId (or env vars MSTEAMS_APP_ID/MSTEAMS_APP_PASSWORD/MSTEAMS_TENANT_ID)";
196+
return "check msteams appId/tenantId and auth config (appPassword, certificate, or federated credential)";
197197
}
198198
if (classification.kind === "throttled") {
199199
return "Teams throttled the bot; backing off may help";

extensions/msteams/src/probe.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ export async function probeMSTeams(cfg?: MSTeamsConfig): Promise<ProbeMSTeamsRes
5555
if (!creds) {
5656
return {
5757
ok: false,
58-
error: "missing credentials (appId, appPassword, tenantId)",
58+
error:
59+
"missing credentials (appId, tenantId, and one of: appPassword, certificate, or federated credential)",
5960
};
6061
}
6162

extensions/msteams/src/sdk.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,28 @@ export function buildMSTeamsAuthConfig(
1212
creds: MSTeamsCredentials,
1313
sdk: MSTeamsSdk,
1414
): MSTeamsAuthConfig {
15-
return sdk.getAuthConfigWithDefaults({
15+
const base: Parameters<MSTeamsSdk["getAuthConfigWithDefaults"]>[0] = {
1616
clientId: creds.appId,
17-
clientSecret: creds.appPassword,
1817
tenantId: creds.tenantId,
19-
});
18+
};
19+
20+
switch (creds.authType) {
21+
case "certificate":
22+
base.certPemFile = creds.certPemFile;
23+
base.certKeyFile = creds.certKeyFile;
24+
if (creds.sendX5C != null) base.sendX5C = creds.sendX5C;
25+
break;
26+
case "federatedCredential":
27+
if (creds.ficClientId) base.FICClientId = creds.ficClientId;
28+
if (creds.widAssertionFile) base.WIDAssertionFile = creds.widAssertionFile;
29+
break;
30+
case "clientSecret":
31+
default:
32+
base.clientSecret = creds.appPassword;
33+
break;
34+
}
35+
36+
return sdk.getAuthConfigWithDefaults(base);
2037
}
2138

2239
export function createMSTeamsAdapter(

extensions/msteams/src/token.ts

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,89 @@ import {
55
normalizeSecretInputString,
66
} from "./secret-input.js";
77

8+
export type MSTeamsAuthType = "clientSecret" | "certificate" | "federatedCredential";
9+
810
export type MSTeamsCredentials = {
911
appId: string;
1012
appPassword: string;
1113
tenantId: string;
14+
authType: MSTeamsAuthType;
15+
certPemFile?: string;
16+
certKeyFile?: string;
17+
sendX5C?: boolean;
18+
ficClientId?: string;
19+
widAssertionFile?: string;
1220
};
1321

1422
export function hasConfiguredMSTeamsCredentials(cfg?: MSTeamsConfig): boolean {
15-
return Boolean(
16-
normalizeSecretInputString(cfg?.appId) &&
17-
hasConfiguredSecretInput(cfg?.appPassword) &&
18-
normalizeSecretInputString(cfg?.tenantId),
19-
);
23+
const appId = normalizeSecretInputString(cfg?.appId);
24+
const tenantId = normalizeSecretInputString(cfg?.tenantId);
25+
if (!appId || !tenantId) return false;
26+
27+
const authType = cfg?.authType ?? "clientSecret";
28+
29+
switch (authType) {
30+
case "certificate":
31+
return Boolean(cfg?.certPemFile && cfg?.certKeyFile);
32+
case "federatedCredential":
33+
return Boolean(cfg?.ficClientId || cfg?.widAssertionFile);
34+
case "clientSecret":
35+
default:
36+
return Boolean(hasConfiguredSecretInput(cfg?.appPassword));
37+
}
2038
}
2139

2240
export function resolveMSTeamsCredentials(cfg?: MSTeamsConfig): MSTeamsCredentials | undefined {
2341
const appId =
2442
normalizeSecretInputString(cfg?.appId) ||
2543
normalizeSecretInputString(process.env.MSTEAMS_APP_ID);
26-
const appPassword =
27-
normalizeResolvedSecretInputString({
28-
value: cfg?.appPassword,
29-
path: "channels.msteams.appPassword",
30-
}) || normalizeSecretInputString(process.env.MSTEAMS_APP_PASSWORD);
3144
const tenantId =
3245
normalizeSecretInputString(cfg?.tenantId) ||
3346
normalizeSecretInputString(process.env.MSTEAMS_TENANT_ID);
3447

35-
if (!appId || !appPassword || !tenantId) {
48+
if (!appId || !tenantId) {
3649
return undefined;
3750
}
3851

39-
return { appId, appPassword, tenantId };
52+
const authType: MSTeamsAuthType = cfg?.authType ?? "clientSecret";
53+
54+
switch (authType) {
55+
case "certificate": {
56+
const certPemFile = cfg?.certPemFile;
57+
const certKeyFile = cfg?.certKeyFile;
58+
if (!certPemFile || !certKeyFile) return undefined;
59+
return {
60+
appId,
61+
appPassword: "", // not used for certificate auth
62+
tenantId,
63+
authType,
64+
certPemFile,
65+
certKeyFile,
66+
sendX5C: cfg?.sendX5C,
67+
};
68+
}
69+
case "federatedCredential": {
70+
const ficClientId = cfg?.ficClientId;
71+
const widAssertionFile = cfg?.widAssertionFile;
72+
if (!ficClientId && !widAssertionFile) return undefined;
73+
return {
74+
appId,
75+
appPassword: "", // not used for federated auth
76+
tenantId,
77+
authType,
78+
ficClientId,
79+
widAssertionFile,
80+
};
81+
}
82+
case "clientSecret":
83+
default: {
84+
const appPassword =
85+
normalizeResolvedSecretInputString({
86+
value: cfg?.appPassword,
87+
path: "channels.msteams.appPassword",
88+
}) || normalizeSecretInputString(process.env.MSTEAMS_APP_PASSWORD);
89+
if (!appPassword) return undefined;
90+
return { appId, appPassword, tenantId, authType: "clientSecret" };
91+
}
92+
}
4093
}

src/config/types.msteams.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,31 @@ export type MSTeamsConfig = {
6161
appId?: string;
6262
/** Azure Bot App Password / Client Secret. */
6363
appPassword?: SecretInput;
64+
/**
65+
* Authentication type for the bot.
66+
* - "clientSecret" (default): Uses appPassword (client secret).
67+
* - "certificate": Uses certPemFile + certKeyFile for certificate-based auth.
68+
* - "federatedCredential": Uses FIC (First-party Integration Channel) client ID
69+
* with workload identity federation (no secret needed).
70+
*/
71+
authType?: "clientSecret" | "certificate" | "federatedCredential";
72+
/** Path to the certificate PEM file (used when authType is "certificate"). */
73+
certPemFile?: string;
74+
/** Path to the certificate key file (used when authType is "certificate"). */
75+
certKeyFile?: string;
76+
/** Whether to send the X5C param for SNI authentication (used with certificate auth). */
77+
sendX5C?: boolean;
78+
/**
79+
* The FIC (First-party Integration Channel) client ID for federated credential auth.
80+
* This is the client ID of a user-assigned managed identity with a federated credential
81+
* configured on the App Registration.
82+
*/
83+
ficClientId?: string;
84+
/**
85+
* Path to the workload identity assertion file (e.g., K8s service account token).
86+
* Used with federated credential auth when running in Kubernetes or similar environments.
87+
*/
88+
widAssertionFile?: string;
6489
/** Azure AD Tenant ID (for single-tenant bots). */
6590
tenantId?: string;
6691
/** Webhook server configuration. */

0 commit comments

Comments
 (0)