fix: preserve CIMD registration provenance across SDK issuer binding (#2242)
`BaseOAuthClientProvider.saveClientInformation` treated every save without an explicit `registrationKind` as DCR. The SDK reaches it from three places in `auth()` and only one is a dynamic registration, so the issuer-binding write overwrote the `cimd` provenance our own pre-registration had just stored — and Connection Info reported `Dynamic (DCR)` for a connection that never issued a `POST /oauth/register`. Recover the kind instead: a `client_id` equal to the configured `clientMetadataUrl` is CIMD (which also covers the SDK's own CIMD write), and otherwise a stored registration with the same `client_id` carries its recorded kind forward. Matching on `client_id` keeps stale CIMD provenance from leaking onto a later DCR registration for the same server. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lmj6X2epWpPE7ScdSKQmtA Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
aab6475527
commit
4789cb954e
1 changed files with 204 additions and 4 deletions
|
|
@ -294,15 +294,15 @@ export class BaseOAuthClientProvider implements OAuthClientProvider {
|
|||
// `OAuthClientInformationContext` ({ issuer }); our own DCR/CIMD callers
|
||||
// pass `SaveClientInformationOptions` ({ registrationKind }). Accept either
|
||||
// and read whichever keys are present: the SDK supplies `issuer` (SEP-2352
|
||||
// per-AS keying) and defaults registration kind to DCR; our callers supply
|
||||
// the registration kind and no issuer yet.
|
||||
// per-AS keying) and no kind — `resolveSdkRegistrationKind` recovers it —
|
||||
// while our callers supply the registration kind and no issuer yet.
|
||||
options?: SaveClientInformationOptions | OAuthClientInformationContext,
|
||||
): Promise<void> {
|
||||
const issuer = options && "issuer" in options ? options.issuer : undefined;
|
||||
const registrationKind =
|
||||
options && "registrationKind" in options
|
||||
? options.registrationKind
|
||||
: "dcr";
|
||||
const issuer = options && "issuer" in options ? options.issuer : undefined;
|
||||
: await this.resolveSdkRegistrationKind(clientInformation, issuer);
|
||||
await this.storage.saveClientInformation(
|
||||
this.serverUrl,
|
||||
clientInformation,
|
||||
|
|
@ -313,6 +313,53 @@ export class BaseOAuthClientProvider implements OAuthClientProvider {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the registration kind for a save that carries no explicit one — that
|
||||
* is, one the SDK made. Three SDK call sites reach here: back-stamping an
|
||||
* existing registration with its `issuer`, the SDK's own CIMD write (whose
|
||||
* `client_id` *is* the metadata-document URL), and a real dynamic
|
||||
* registration. Only the last is `"dcr"`, so defaulting every unstamped save
|
||||
* to it relabels a CIMD registration `Dynamic (DCR)` in Connection Info the
|
||||
* moment the SDK binds it to an issuer — reported as #2242, where no
|
||||
* `POST /register` was ever made.
|
||||
*
|
||||
* `client_id` is what tells the cases apart, so match on it rather than on the
|
||||
* stored kind alone: a DCR `client_id` is minted by the authorization server,
|
||||
* so a later DCR registration for the same server cannot inherit the earlier
|
||||
* CIMD provenance.
|
||||
*/
|
||||
private async resolveSdkRegistrationKind(
|
||||
clientInformation: OAuthClientInformation,
|
||||
issuer: string | undefined,
|
||||
): Promise<SaveClientInformationOptions["registrationKind"]> {
|
||||
const clientMetadataUrl = this.clientMetadataUrl?.trim();
|
||||
if (
|
||||
clientMetadataUrl &&
|
||||
clientInformation.client_id === clientMetadataUrl
|
||||
) {
|
||||
return "cimd";
|
||||
}
|
||||
// Falls back to the unkeyed slot our own pre-registration wrote, since the
|
||||
// issuer slot does not exist yet on the save that creates it. Reading it
|
||||
// covers a CIMD registration whose `clientMetadataUrl` config has since
|
||||
// been cleared, so the provenance is not silently demoted.
|
||||
const stored = await this.storage.getClientInformation(
|
||||
this.serverUrl,
|
||||
false,
|
||||
issuer,
|
||||
);
|
||||
if (!stored || stored.client_id !== clientInformation.client_id) {
|
||||
return "dcr";
|
||||
}
|
||||
const storedKind = await this.storage.getClientRegistrationKind(
|
||||
this.serverUrl,
|
||||
issuer,
|
||||
);
|
||||
// `"static"` lives in the preregistered slot, never this one, so `"cimd"`
|
||||
// is the only kind worth carrying forward.
|
||||
return storedKind === "cimd" ? "cimd" : "dcr";
|
||||
}
|
||||
|
||||
async saveScope(scope: string | undefined): Promise<void> {
|
||||
await this.storage.saveScope(this.serverUrl, scope);
|
||||
this.cachedScope = scope;
|
||||
|
|
|
|||
Loading…
Reference in a new issue