Skip to content

Commit 89254d6

Browse files
committed
fix(msteams): keep delegated auth healthy when an expired token can auto-refresh (#106566)
1 parent 345083a commit 89254d6

2 files changed

Lines changed: 45 additions & 4 deletions

File tree

extensions/msteams/src/probe.test.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@ describe("msteams probe", () => {
103103
});
104104
});
105105

106-
it("reports delegated tokens expired when the process clock is invalid", async () => {
106+
it("treats an expired delegated access token as healthy when a refresh token is present", async () => {
107+
// NaN clock forces the expiry check to fail; a stored refresh token means
108+
// the client auto-refreshes, so the probe must not report a health failure.
107109
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(Number.NaN);
108110
hostMockState.delegatedTokens = {
109111
accessToken: "delegated-token",
@@ -120,6 +122,39 @@ describe("msteams probe", () => {
120122
delegatedAuth: { enabled: true },
121123
} as unknown as MSTeamsConfig;
122124

125+
try {
126+
await expect(probeMSTeams(cfg)).resolves.toEqual({
127+
ok: true,
128+
appId: "app",
129+
graph: { ok: true, roles: undefined, scopes: undefined },
130+
delegatedAuth: {
131+
ok: true,
132+
scopes: ["ChatMessage.Send"],
133+
userPrincipalName: "[email protected]",
134+
},
135+
});
136+
} finally {
137+
nowSpy.mockRestore();
138+
}
139+
});
140+
141+
it("reports delegated auth unhealthy when the token is expired and no refresh token is stored", async () => {
142+
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(Number.NaN);
143+
hostMockState.delegatedTokens = {
144+
accessToken: "delegated-token",
145+
refreshToken: "",
146+
expiresAt: Date.parse("2030-01-01T00:00:00.000Z"),
147+
scopes: ["ChatMessage.Send"],
148+
userPrincipalName: "[email protected]",
149+
};
150+
const cfg = {
151+
enabled: true,
152+
appId: "app",
153+
appPassword: "pw",
154+
tenantId: "tenant",
155+
delegatedAuth: { enabled: true },
156+
} as unknown as MSTeamsConfig;
157+
123158
try {
124159
await expect(probeMSTeams(cfg)).resolves.toEqual({
125160
ok: true,
@@ -129,7 +164,7 @@ describe("msteams probe", () => {
129164
ok: false,
130165
scopes: ["ChatMessage.Send"],
131166
userPrincipalName: "[email protected]",
132-
error: "token expired (will auto-refresh on next use)",
167+
error: "token expired and no refresh token available (re-run setup wizard)",
133168
},
134169
});
135170
} finally {

extensions/msteams/src/probe.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,17 @@ export async function probeMSTeams(cfg?: MSTeamsConfig): Promise<ProbeMSTeamsRes
103103
const tokens = loadDelegatedTokens();
104104
if (tokens) {
105105
const isExpired = !isFutureDateTimestampMs(tokens.expiresAt);
106+
// An expired access token is not a health failure while a refresh
107+
// token is stored: the client refreshes it on the next API call. Only
108+
// an expired token with no refresh token needs operator re-auth.
109+
const canAutoRefresh = Boolean(tokens.refreshToken);
106110
delegatedAuth = {
107-
ok: !isExpired,
111+
ok: !isExpired || canAutoRefresh,
108112
scopes: tokens.scopes,
109113
userPrincipalName: tokens.userPrincipalName,
110-
...(isExpired ? { error: "token expired (will auto-refresh on next use)" } : {}),
114+
...(isExpired && !canAutoRefresh
115+
? { error: "token expired and no refresh token available (re-run setup wizard)" }
116+
: {}),
111117
};
112118
} else {
113119
delegatedAuth = { ok: false, error: "no delegated tokens found (run setup wizard)" };

0 commit comments

Comments
 (0)