diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
index 1c69bf74..0ef02d8b 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
@@ -1253,6 +1253,20 @@ public sealed record AuthenticateParams
/// Bearer token obtained from the resource's authorization server
public required string Token { get; init; }
+ /// The access token's remaining lifetime, in seconds, when this
+ /// `authenticate` request is sent. This corresponds to `expires_in` in an
+ /// OAuth 2.0 token response (RFC 6749 section 5.1).
+ ///
+ /// If the client retained the original token response, it MUST subtract the
+ /// elapsed time before forwarding this value. Omit this field when the
+ /// authorization server did not supply an expiry or the expiry is otherwise
+ /// unknown. When supplied, the value MUST be a positive integer.
+ ///
+ /// This field is irrelevant when `token` is empty to revoke authentication
+ /// and SHOULD be omitted in that case.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public long? ExpiresIn { get; init; }
+
/// OAuth scopes the token grants, when known. Lets the server determine
/// whether a specific challenge — e.g. the `requiredScopes` on a live
/// `McpServerAuthRequiredState` or `ToolCallAuthRequiredState.auth` — is
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs
index fd3a7f2e..9b5bd1a1 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs
@@ -16,7 +16,8 @@ public enum AuthRequiredReason
/// The client has not yet authenticated for the resource
[WireValue("required")]
Required,
- /// A previously valid token has expired or been revoked
+ /// A previously valid token has expired or been revoked. The client must
+ /// acquire or renew the credential rather than replaying the challenged token.
[WireValue("expired")]
Expired,
}
@@ -151,8 +152,9 @@ public sealed record ProgressParams
/// to; the `resource` field carries the complete OAuth protected resource
/// metadata (per RFC 9728).
///
-/// Clients should obtain a fresh token and push it via the `authenticate`
-/// command.
+/// Clients should obtain or renew the credential and push the resulting token
+/// via the `authenticate` command. When `reason` is `expired`, clients MUST NOT
+/// blindly replay the challenged token.
public sealed record AuthRequiredParams
{
/// Channel URI this notification belongs to
diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go
index 9e7b6d9a..9ef81b6f 100644
--- a/clients/go/ahptypes/commands.generated.go
+++ b/clients/go/ahptypes/commands.generated.go
@@ -997,6 +997,18 @@ type AuthenticateParams struct {
Resource string `json:"resource"`
// Bearer token obtained from the resource's authorization server
Token string `json:"token"`
+ // The access token's remaining lifetime, in seconds, when this
+ // `authenticate` request is sent. This corresponds to `expires_in` in an
+ // OAuth 2.0 token response (RFC 6749 section 5.1).
+ //
+ // If the client retained the original token response, it MUST subtract the
+ // elapsed time before forwarding this value. Omit this field when the
+ // authorization server did not supply an expiry or the expiry is otherwise
+ // unknown. When supplied, the value MUST be a positive integer.
+ //
+ // This field is irrelevant when `token` is empty to revoke authentication
+ // and SHOULD be omitted in that case.
+ ExpiresIn *int64 `json:"expiresIn,omitempty"`
// OAuth scopes the token grants, when known. Lets the server determine
// whether a specific challenge — e.g. the `requiredScopes` on a live
// `McpServerAuthRequiredState` or `ToolCallAuthRequiredState.auth` — is
diff --git a/clients/go/ahptypes/notifications.generated.go b/clients/go/ahptypes/notifications.generated.go
index b92501f6..62db00de 100644
--- a/clients/go/ahptypes/notifications.generated.go
+++ b/clients/go/ahptypes/notifications.generated.go
@@ -21,7 +21,8 @@ type AuthRequiredReason string
const (
// The client has not yet authenticated for the resource
AuthRequiredReasonRequired AuthRequiredReason = "required"
- // A previously valid token has expired or been revoked
+ // A previously valid token has expired or been revoked. The client must
+ // acquire or renew the credential rather than replaying the challenged token.
AuthRequiredReasonExpired AuthRequiredReason = "expired"
)
@@ -141,8 +142,9 @@ type ProgressParams struct {
// to; the `resource` field carries the complete OAuth protected resource
// metadata (per RFC 9728).
//
-// Clients should obtain a fresh token and push it via the `authenticate`
-// command.
+// Clients should obtain or renew the credential and push the resulting token
+// via the `authenticate` command. When `reason` is `expired`, clients MUST NOT
+// blindly replay the challenged token.
type AuthRequiredParams struct {
// Channel URI this notification belongs to
Channel URI `json:"channel"`
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
index ad5ac527..01fb0e15 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
@@ -1220,6 +1220,20 @@ data class AuthenticateParams(
* Bearer token obtained from the resource's authorization server
*/
val token: String,
+ /**
+ * The access token's remaining lifetime, in seconds, when this
+ * `authenticate` request is sent. This corresponds to `expires_in` in an
+ * OAuth 2.0 token response (RFC 6749 section 5.1).
+ *
+ * If the client retained the original token response, it MUST subtract the
+ * elapsed time before forwarding this value. Omit this field when the
+ * authorization server did not supply an expiry or the expiry is otherwise
+ * unknown. When supplied, the value MUST be a positive integer.
+ *
+ * This field is irrelevant when `token` is empty to revoke authentication
+ * and SHOULD be omitted in that case.
+ */
+ val expiresIn: Long? = null,
/**
* OAuth scopes the token grants, when known. Lets the server determine
* whether a specific challenge — e.g. the `requiredScopes` on a live
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt
index 193a72f4..21d0f0ff 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt
@@ -34,7 +34,8 @@ value class AuthRequiredReason(val rawValue: String) {
*/
val REQUIRED: AuthRequiredReason = AuthRequiredReason("required")
/**
- * A previously valid token has expired or been revoked
+ * A previously valid token has expired or been revoked. The client must
+ * acquire or renew the credential rather than replaying the challenged token.
*/
val EXPIRED: AuthRequiredReason = AuthRequiredReason("expired")
}
diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs
index 37e3b642..e3dfe255 100644
--- a/clients/rust/crates/ahp-types/src/commands.rs
+++ b/clients/rust/crates/ahp-types/src/commands.rs
@@ -1288,6 +1288,19 @@ pub struct AuthenticateParams {
pub resource: String,
/// Bearer token obtained from the resource's authorization server
pub token: String,
+ /// The access token's remaining lifetime, in seconds, when this
+ /// `authenticate` request is sent. This corresponds to `expires_in` in an
+ /// OAuth 2.0 token response (RFC 6749 section 5.1).
+ ///
+ /// If the client retained the original token response, it MUST subtract the
+ /// elapsed time before forwarding this value. Omit this field when the
+ /// authorization server did not supply an expiry or the expiry is otherwise
+ /// unknown. When supplied, the value MUST be a positive integer.
+ ///
+ /// This field is irrelevant when `token` is empty to revoke authentication
+ /// and SHOULD be omitted in that case.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub expires_in: Option,
/// OAuth scopes the token grants, when known. Lets the server determine
/// whether a specific challenge — e.g. the `requiredScopes` on a live
/// `McpServerAuthRequiredState` or `ToolCallAuthRequiredState.auth` — is
diff --git a/clients/rust/crates/ahp-types/src/notifications.rs b/clients/rust/crates/ahp-types/src/notifications.rs
index d50cc301..3fa4cce5 100644
--- a/clients/rust/crates/ahp-types/src/notifications.rs
+++ b/clients/rust/crates/ahp-types/src/notifications.rs
@@ -24,7 +24,8 @@ use crate::state::{
pub enum AuthRequiredReason {
/// The client has not yet authenticated for the resource
Required,
- /// A previously valid token has expired or been revoked
+ /// A previously valid token has expired or been revoked. The client must
+ /// acquire or renew the credential rather than replaying the challenged token.
Expired,
/// Unknown raw value from a newer protocol version, preserved verbatim.
Unknown(String),
@@ -183,8 +184,9 @@ pub struct ProgressParams {
/// to; the `resource` field carries the complete OAuth protected resource
/// metadata (per RFC 9728).
///
-/// Clients should obtain a fresh token and push it via the `authenticate`
-/// command.
+/// Clients should obtain or renew the credential and push the resulting token
+/// via the `authenticate` command. When `reason` is `expired`, clients MUST NOT
+/// blindly replay the challenged token.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthRequiredParams {
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
index bb6ee682..9d9ca3e2 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
@@ -1465,6 +1465,18 @@ public struct AuthenticateParams: Codable, Sendable {
public var resource: String
/// Bearer token obtained from the resource's authorization server
public var token: String
+ /// The access token's remaining lifetime, in seconds, when this
+ /// `authenticate` request is sent. This corresponds to `expires_in` in an
+ /// OAuth 2.0 token response (RFC 6749 section 5.1).
+ ///
+ /// If the client retained the original token response, it MUST subtract the
+ /// elapsed time before forwarding this value. Omit this field when the
+ /// authorization server did not supply an expiry or the expiry is otherwise
+ /// unknown. When supplied, the value MUST be a positive integer.
+ ///
+ /// This field is irrelevant when `token` is empty to revoke authentication
+ /// and SHOULD be omitted in that case.
+ public var expiresIn: Int?
/// OAuth scopes the token grants, when known. Lets the server determine
/// whether a specific challenge — e.g. the `requiredScopes` on a live
/// `McpServerAuthRequiredState` or `ToolCallAuthRequiredState.auth` — is
@@ -1478,6 +1490,7 @@ public struct AuthenticateParams: Codable, Sendable {
case meta = "_meta"
case resource
case token
+ case expiresIn
case scopes
}
@@ -1486,12 +1499,14 @@ public struct AuthenticateParams: Codable, Sendable {
meta: [String: AnyCodable]? = nil,
resource: String,
token: String,
+ expiresIn: Int? = nil,
scopes: [String]? = nil
) {
self.channel = channel
self.meta = meta
self.resource = resource
self.token = token
+ self.expiresIn = expiresIn
self.scopes = scopes
}
}
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift
index 7040fdf8..f865befd 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift
@@ -8,7 +8,8 @@ import Foundation
public enum AuthRequiredReason: Codable, Sendable, Equatable {
/// The client has not yet authenticated for the resource
case required
- /// A previously valid token has expired or been revoked
+ /// A previously valid token has expired or been revoked. The client must
+ /// acquire or renew the credential rather than replaying the challenged token.
case expired
/// Unknown raw value from a newer protocol version, preserved verbatim.
case unknown(String)
diff --git a/docs/.changes/20260901-authenticate-token-lifetime.json b/docs/.changes/20260901-authenticate-token-lifetime.json
new file mode 100644
index 00000000..5014ccfc
--- /dev/null
+++ b/docs/.changes/20260901-authenticate-token-lifetime.json
@@ -0,0 +1,4 @@
+{
+ "type": "added",
+ "message": "`AuthenticateParams.expiresIn` carries an OAuth access token's remaining lifetime in seconds."
+}
diff --git a/docs/specification/authentication.md b/docs/specification/authentication.md
index 294ec13f..a52fc76a 100644
--- a/docs/specification/authentication.md
+++ b/docs/specification/authentication.md
@@ -18,7 +18,7 @@ sequenceDiagram
C->>AS: OAuth token request
AS-->>C: Token
- C->>S: authenticate({ resource, token })
+ C->>S: authenticate({ resource, token, expiresIn })
S-->>C: {}
C->>S: createSession / other commands
@@ -99,6 +99,7 @@ Clients push Bearer tokens to the server using the [`authenticate`](/reference/c
"channel": "ahp-root://",
"resource": "https://api.github.com",
"token": "gho_xxxxxxxxxxxx",
+ "expiresIn": 3540,
"scopes": ["read:user", "user:email"]
}
}
@@ -111,6 +112,10 @@ Clients push Bearer tokens to the server using the [`authenticate`](/reference/c
}
```
+`expiresIn` is optional and corresponds to the `expires_in` field in an OAuth 2.0 token response, as defined by [RFC 6749 section 5.1](https://datatracker.ietf.org/doc/html/rfc6749#section-5.1). It is the access token's remaining lifetime in seconds when the client sends the `authenticate` request. When supplied, it MUST be a positive integer.
+
+If the client retained the original token response, it MUST subtract elapsed time from the original `expires_in` value before forwarding it. The client MUST omit `expiresIn` when the authorization server did not supply an expiry or the expiry is otherwise unknown. An empty `token` revokes authentication for the resource; `expiresIn` is irrelevant and SHOULD be omitted in that request.
+
`scopes` is optional and lets the client tell the server which OAuth scopes the pushed token actually grants — useful when resolving a `requiredScopes` challenge (from a live `McpServerAuthRequiredState` or `ToolCallAuthRequiredState.auth`) without the server needing to decode an opaque token.
If the token is invalid or the resource is unrecognized, the server MUST return a JSON-RPC error (e.g. `AuthRequired` `-32007` or `InvalidParams` `-32602`).
@@ -190,7 +195,9 @@ The `resource` field carries the complete [`ProtectedResourceMetadata`](/referen
| Value | Description |
|---|---|
| `required` | The client has not yet authenticated for the resource |
-| `expired` | A previously valid token has expired or been revoked |
+| `expired` | A previously valid token has expired or been revoked; the client must acquire or renew the credential |
+
+When `reason` is `expired`, the client MUST acquire a new credential or renew the existing credential before calling `authenticate` again. It MUST NOT blindly replay the challenged token.
Like all protocol notifications, `auth/required` is ephemeral and is **not** replayed on reconnection. Clients SHOULD re-check authentication requirements after reconnecting.
@@ -211,6 +218,10 @@ Using the standard OAuth 2.0 Protected Resource Metadata format means:
- Tokens can be refreshed or rotated without re-initializing the connection
- Not all clients need to authenticate (some agents may not require auth)
+### Why `expiresIn` instead of `expiresAt`?
+
+`expiresIn` follows the OAuth token endpoint's existing `expires_in` vocabulary and reports the lifetime relative to the `authenticate` request. A relative lifetime does not require the client and host clocks to be synchronized.
+
### Why not store auth status in root state?
Root state is global and visible to all subscribed clients. Authentication status is per-connection (each client authenticates independently), so it is kept imperative via commands and notifications rather than polluting the shared state tree.
diff --git a/schema/commands.schema.json b/schema/commands.schema.json
index 6b792f21..22902368 100644
--- a/schema/commands.schema.json
+++ b/schema/commands.schema.json
@@ -875,6 +875,11 @@
"type": "string",
"description": "Bearer token obtained from the resource's authorization server"
},
+ "expiresIn": {
+ "type": "integer",
+ "description": "The access token's remaining lifetime, in seconds, when this\n`authenticate` request is sent. This corresponds to `expires_in` in an\nOAuth 2.0 token response (RFC 6749 section 5.1).\n\nIf the client retained the original token response, it MUST subtract the\nelapsed time before forwarding this value. Omit this field when the\nauthorization server did not supply an expiry or the expiry is otherwise\nunknown. When supplied, the value MUST be a positive integer.\n\nThis field is irrelevant when `token` is empty to revoke authentication\nand SHOULD be omitted in that case.",
+ "minimum": 1
+ },
"scopes": {
"type": "array",
"items": {
diff --git a/schema/errors.schema.json b/schema/errors.schema.json
index 46c00019..71cb746b 100644
--- a/schema/errors.schema.json
+++ b/schema/errors.schema.json
@@ -6527,6 +6527,11 @@
"type": "string",
"description": "Bearer token obtained from the resource's authorization server"
},
+ "expiresIn": {
+ "type": "integer",
+ "description": "The access token's remaining lifetime, in seconds, when this\n`authenticate` request is sent. This corresponds to `expires_in` in an\nOAuth 2.0 token response (RFC 6749 section 5.1).\n\nIf the client retained the original token response, it MUST subtract the\nelapsed time before forwarding this value. Omit this field when the\nauthorization server did not supply an expiry or the expiry is otherwise\nunknown. When supplied, the value MUST be a positive integer.\n\nThis field is irrelevant when `token` is empty to revoke authentication\nand SHOULD be omitted in that case.",
+ "minimum": 1
+ },
"scopes": {
"type": "array",
"items": {
diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json
index edf2ca34..01271335 100644
--- a/schema/notifications.schema.json
+++ b/schema/notifications.schema.json
@@ -7,7 +7,7 @@
"$defs": {
"AuthRequiredParams": {
"type": "object",
- "description": "Sent by the server when a protected resource requires (re-)authentication.\n\nThis notification MAY be associated with any channel — for example, an\nagent advertised on the root channel, or a per-session resource. The\n`channel` field identifies the subscription the auth requirement belongs\nto; the `resource` field carries the complete OAuth protected resource\nmetadata (per RFC 9728).\n\nClients should obtain a fresh token and push it via the `authenticate`\ncommand.",
+ "description": "Sent by the server when a protected resource requires (re-)authentication.\n\nThis notification MAY be associated with any channel — for example, an\nagent advertised on the root channel, or a per-session resource. The\n`channel` field identifies the subscription the auth requirement belongs\nto; the `resource` field carries the complete OAuth protected resource\nmetadata (per RFC 9728).\n\nClients should obtain or renew the credential and push the resulting token\nvia the `authenticate` command. When `reason` is `expired`, clients MUST NOT\nblindly replay the challenged token.",
"properties": {
"channel": {
"$ref": "#/$defs/URI",
diff --git a/scripts/generate-json-schema.test.ts b/scripts/generate-json-schema.test.ts
index 5ede4cd1..53985809 100644
--- a/scripts/generate-json-schema.test.ts
+++ b/scripts/generate-json-schema.test.ts
@@ -183,6 +183,23 @@ describe('generated JSON schemas', () => {
assert.equal(properties.minIntervalMinutes.type, 'number');
});
+ it('exposes the optional authenticate token lifetime', () => {
+ if (file !== 'commands.schema.json') {
+ return;
+ }
+ const defs = schema.$defs as Record>;
+ const authenticate = defs.AuthenticateParams;
+ const properties = authenticate.properties as Record>;
+ const required = authenticate.required as string[];
+ const expiresIn = properties.expiresIn;
+
+ assert.equal(expiresIn.type, 'integer');
+ assert.equal(expiresIn.minimum, 1);
+ assert.equal(required.includes('expiresIn'), false);
+ assert.match(expiresIn.description as string, /remaining lifetime, in seconds/);
+ assert.match(expiresIn.description as string, /MUST be a positive integer/);
+ });
+
it('constrains every ChatOrigin branch to a distinct kind', () => {
const defs = schema.$defs as Record>;
const chatOrigin = defs.ChatOrigin;
diff --git a/scripts/generate-json-schema.ts b/scripts/generate-json-schema.ts
index 5d023770..0ac0fb4f 100644
--- a/scripts/generate-json-schema.ts
+++ b/scripts/generate-json-schema.ts
@@ -32,6 +32,7 @@ interface JsonSchema {
items?: JsonSchema;
enum?: Array;
const?: string | number | boolean;
+ minimum?: number;
oneOf?: JsonSchema[];
allOf?: JsonSchema[];
anyOf?: JsonSchema[];
@@ -55,6 +56,28 @@ function getPropertyDescription(prop: PropertySignature): string {
return normalizeDescription(jsDocs[0].getDescription());
}
+function hasPropertyTag(prop: PropertySignature, tagName: string): boolean {
+ return prop.getJsDocs()
+ .flatMap(doc => doc.getTags())
+ .some(tag => tag.getTagName() === tagName);
+}
+
+function getNumericPropertyTag(prop: PropertySignature, tagName: string): number | undefined {
+ const tag = prop.getJsDocs()
+ .flatMap(doc => doc.getTags())
+ .find(candidate => candidate.getTagName() === tagName);
+ if (!tag) return undefined;
+
+ const text = tag.getCommentText()?.trim();
+ const value = text === undefined ? Number.NaN : Number(text);
+ if (!Number.isFinite(value)) {
+ throw new Error(
+ `${prop.getSourceFile().getFilePath()}: ${prop.getName()} has invalid @${tagName} value ${JSON.stringify(text)}`,
+ );
+ }
+ return value;
+}
+
function getInterfaceDescription(node: InterfaceDeclaration): string {
const jsDocs = node.getJsDocs();
if (jsDocs.length === 0) return '';
@@ -336,6 +359,23 @@ function interfaceToSchema(iface: InterfaceDeclaration, project: Project): JsonS
const desc = getPropertyDescription(prop);
const propSchema = typeTextToSchema(typeText, project);
if (desc) propSchema.description = desc;
+ if (hasPropertyTag(prop, 'integer')) {
+ if (propSchema.type !== 'number') {
+ throw new Error(
+ `${prop.getSourceFile().getFilePath()}: ${name} uses @integer on ${typeText}`,
+ );
+ }
+ propSchema.type = 'integer';
+ }
+ const minimum = getNumericPropertyTag(prop, 'minimum');
+ if (minimum !== undefined) {
+ if (propSchema.type !== 'number' && propSchema.type !== 'integer') {
+ throw new Error(
+ `${prop.getSourceFile().getFilePath()}: ${name} uses a numeric schema constraint on ${typeText}`,
+ );
+ }
+ propSchema.minimum = minimum;
+ }
schema.properties![name] = propSchema;
if (!prop.hasQuestionToken() && !typeAdmitsUndefined(typeText)) {
if (!schema.required!.includes(name)) {
diff --git a/types/common/commands.ts b/types/common/commands.ts
index 14d18144..d2bff1e8 100644
--- a/types/common/commands.ts
+++ b/types/common/commands.ts
@@ -1136,7 +1136,8 @@ export interface ResourceMkdirResult {
* ```jsonc
* // Client → Server
* { "jsonrpc": "2.0", "id": 3, "method": "authenticate",
- * "params": { "channel": "ahp-root://", "resource": "https://api.github.com", "token": "gho_xxxx" } }
+ * "params": { "channel": "ahp-root://", "resource": "https://api.github.com",
+ * "token": "gho_xxxx", "expiresIn": 3540 } }
*
* // Server → Client (success)
* { "jsonrpc": "2.0", "id": 3, "result": {} }
@@ -1156,6 +1157,23 @@ export interface AuthenticateParams extends BaseParams {
resource: string;
/** Bearer token obtained from the resource's authorization server */
token: string;
+ /**
+ * The access token's remaining lifetime, in seconds, when this
+ * `authenticate` request is sent. This corresponds to `expires_in` in an
+ * OAuth 2.0 token response (RFC 6749 section 5.1).
+ *
+ * If the client retained the original token response, it MUST subtract the
+ * elapsed time before forwarding this value. Omit this field when the
+ * authorization server did not supply an expiry or the expiry is otherwise
+ * unknown. When supplied, the value MUST be a positive integer.
+ *
+ * This field is irrelevant when `token` is empty to revoke authentication
+ * and SHOULD be omitted in that case.
+ *
+ * @integer
+ * @minimum 1
+ */
+ expiresIn?: number;
/**
* OAuth scopes the token grants, when known. Lets the server determine
* whether a specific challenge — e.g. the `requiredScopes` on a live
diff --git a/types/common/notifications.ts b/types/common/notifications.ts
index 9a33de69..330f9f0f 100644
--- a/types/common/notifications.ts
+++ b/types/common/notifications.ts
@@ -16,7 +16,10 @@ import type { ProtectedResourceMetadata, URI } from './state.js';
export const enum AuthRequiredReason {
/** The client has not yet authenticated for the resource */
Required = 'required',
- /** A previously valid token has expired or been revoked */
+ /**
+ * A previously valid token has expired or been revoked. The client must
+ * acquire or renew the credential rather than replaying the challenged token.
+ */
Expired = 'expired',
}
@@ -31,8 +34,9 @@ export const enum AuthRequiredReason {
* to; the `resource` field carries the complete OAuth protected resource
* metadata (per RFC 9728).
*
- * Clients should obtain a fresh token and push it via the `authenticate`
- * command.
+ * Clients should obtain or renew the credential and push the resulting token
+ * via the `authenticate` command. When `reason` is `expired`, clients MUST NOT
+ * blindly replay the challenged token.
*
* @category Protocol Notifications
* @method auth/required