Skip to content

Commit 5e5caa3

Browse files
committed
fix(auth): default clientSecretExpirySeconds to undefined (no expiration)
The 30-day default caused MCP clients to fail with "Client secret has expired" after 30 days. Aligns with Python SDK behavior (defaults to None). - undefined: omit client_secret_expires_at (no expiry) - 0: set to 0 (no expiry per RFC 7591) - positive: set to now + seconds
1 parent 856d9ec commit 5e5caa3

File tree

2 files changed

+41
-10
lines changed

2 files changed

+41
-10
lines changed

src/server/auth/handlers/register.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ describe('Client Registration Handler', () => {
199199
});
200200

201201
it('sets no expiry when clientSecretExpirySeconds=0', async () => {
202-
// Create handler with no expiry
202+
// Create handler with explicit 0 (no expiry per RFC 7591)
203203
const customApp = express();
204204
const options: ClientRegistrationHandlerOptions = {
205205
clientsStore: mockClientStoreWithRegistration,
@@ -218,6 +218,27 @@ describe('Client Registration Handler', () => {
218218
expect(response.body.client_secret_expires_at).toBe(0);
219219
});
220220

221+
it('omits client_secret_expires_at when clientSecretExpirySeconds is undefined (default)', async () => {
222+
// Create handler with default undefined (no expiry, omit from response)
223+
const customApp = express();
224+
const options: ClientRegistrationHandlerOptions = {
225+
clientsStore: mockClientStoreWithRegistration
226+
// clientSecretExpirySeconds not set - defaults to undefined
227+
};
228+
229+
customApp.use('/register', clientRegistrationHandler(options));
230+
231+
const response = await supertest(customApp)
232+
.post('/register')
233+
.send({
234+
redirect_uris: ['https://example.com/callback']
235+
});
236+
237+
expect(response.status).toBe(201);
238+
expect(response.body.client_secret).toBeDefined(); // Still has secret
239+
expect(response.body.client_secret_expires_at).toBeUndefined(); // But no expiry
240+
});
241+
221242
it('sets no client_id when clientIdGeneration=false', async () => {
222243
// Create handler with no expiry
223244
const customApp = express();

src/server/auth/handlers/register.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ export type ClientRegistrationHandlerOptions = {
1919
clientsStore: OAuthRegisteredClientsStore;
2020

2121
/**
22-
* The number of seconds after which to expire issued client secrets, or 0 to prevent expiration of client secrets (not recommended).
23-
*
24-
* If not set, defaults to 30 days.
22+
* The number of seconds after which to expire issued client secrets.
23+
* - If set to a positive number, client secrets will expire after that many seconds.
24+
* - If set to 0, client_secret_expires_at will be 0 (meaning no expiration per RFC 7591).
25+
* - If not set (undefined), client_secret_expires_at will be omitted from the response (no expiration).
26+
*
27+
* Defaults to undefined (no expiration), consistent with Python SDK behavior.
2528
*/
2629
clientSecretExpirySeconds?: number;
2730

@@ -40,11 +43,9 @@ export type ClientRegistrationHandlerOptions = {
4043
clientIdGeneration?: boolean;
4144
};
4245

43-
const DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS = 30 * 24 * 60 * 60; // 30 days
44-
4546
export function clientRegistrationHandler({
4647
clientsStore,
47-
clientSecretExpirySeconds = DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS,
48+
clientSecretExpirySeconds,
4849
rateLimit: rateLimitConfig,
4950
clientIdGeneration = true,
5051
}: ClientRegistrationHandlerOptions): RequestHandler {
@@ -92,9 +93,18 @@ export function clientRegistrationHandler({
9293
const clientIdIssuedAt = Math.floor(Date.now() / 1000);
9394

9495
// Calculate client secret expiry time
95-
const clientsDoExpire = clientSecretExpirySeconds > 0
96-
const secretExpiryTime = clientsDoExpire ? clientIdIssuedAt + clientSecretExpirySeconds : 0
97-
const clientSecretExpiresAt = isPublicClient ? undefined : secretExpiryTime
96+
// - undefined: omit client_secret_expires_at (no expiration)
97+
// - 0: set to 0 (no expiration per RFC 7591)
98+
// - positive number: set to now + seconds
99+
let clientSecretExpiresAt: number | undefined;
100+
if (!isPublicClient) {
101+
if (clientSecretExpirySeconds !== undefined && clientSecretExpirySeconds > 0) {
102+
clientSecretExpiresAt = clientIdIssuedAt + clientSecretExpirySeconds;
103+
} else if (clientSecretExpirySeconds === 0) {
104+
clientSecretExpiresAt = 0;
105+
}
106+
// else: undefined - omit from response (no expiration)
107+
}
98108

99109
let clientInfo: Omit<OAuthClientInformationFull, "client_id"> & { client_id?: string } = {
100110
...clientMetadata,

0 commit comments

Comments
 (0)