Skip to content

feat: Adds MRRT support#1056

Merged
kailash-b merged 2 commits into
masterfrom
feat/SDK-9970
Jul 15, 2026
Merged

feat: Adds MRRT support#1056
kailash-b merged 2 commits into
masterfrom
feat/SDK-9970

Conversation

@kailash-b

@kailash-b kailash-b commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Changes

Adds Multi-Resource Refresh Token (MRRT) support to the Auth0.AuthenticationApi package.

MRRT lets a single refresh token be exchanged for access tokens targeting different APIs (audience) and/or broader scopes, without a full re-authentication. At the protocol level this is already a standard POST /oauth/token refresh-token grant — RefreshTokenRequest already carries Audience and Scope, and GetTokenAsync(RefreshTokenRequest) already sends them. The gap this PR closes is on the response side: surfacing the actually-granted scopes so callers can detect under-granting.

Classes/methods changed:

  • AccessTokenResponse — added Scope property ([JsonPropertyName("scope")]).
    The token endpoint returns the granted scope, which may be narrower than requested. Promoting it to the base response makes it available to every flow that returns AccessTokenResponse.
  • ClientInitiatedBackchannelAuthorizationTokenResponse — removed its duplicate Scope property; it now inherits from the base AccessTokenResponse. This is source-compatible but binary-breaking: the property
    name, wire name (scope), type, and serialization are unchanged, so any code recompiled against this version behaves identically. However, an assembly compiled against a previous version that reads
    ClientInitiatedBackchannelAuthorizationTokenResponse.Scope holds a member reference to the derived type; because the property now lives on the base, that reference will throw MissingMethodException at runtime
    until the consuming assembly is recompiled.
  • AuthenticationApiClient.GetTokenAsync(RefreshTokenRequest) — changed AssertIdTokenValidAssertIdTokenValidIfExisting. A refresh-token grant only returns an id_token when openid scope is requested, which MRRT audience-targeted refreshes frequently omit; the previous call threw "ID token is required but missing." on an absent token. This aligns the refresh flow with the client-credentials, passwordless, and device-code flows, which already validate the ID token only when present. Backward-compatible: refresh responses that do carry an id_token are still validated.

Usage (also added to Examples.md, §5):

var requestedScope = "read:data write:data";

var tokenResponse = await authClient.GetTokenAsync(new RefreshTokenRequest
{
    ClientId = _clientId,
    ClientSecret = _clientSecret,
    RefreshToken = _refreshToken,
    Audience = "https://my-api.example.com", // target a different API (optional)
    Scope = requestedScope                    // request broader scopes (optional)
});

// AccessTokenResponse.Scope reflects what was actually granted — may be narrower.
if (tokenResponse.Scope != requestedScope)
{
    // handle insufficient-scope grant
}

References

Testing

  • This change adds unit test coverage

  • This change adds integration test coverage

  • This change has been tested on the latest version of the platform/language or why not

Checklist

@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 25.49%. Comparing base (9f96325) to head (b3c1407).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1056   +/-   ##
=======================================
  Coverage   25.49%   25.49%           
=======================================
  Files        3154     3154           
  Lines      148230   148230           
  Branches    10015    10015           
=======================================
  Hits        37785    37785           
  Misses     108132   108132           
  Partials     2313     2313           
Flag Coverage Δ
authIntTests 2.32% <100.00%> (ø)
mgmtIntTests 24.52% <50.00%> (+<0.01%) ⬆️
unittests 0.22% <0.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kailash-b
kailash-b marked this pull request as ready for review July 15, 2026 08:28
@kailash-b
kailash-b requested a review from a team as a code owner July 15, 2026 08:28
Comment thread Examples.md Outdated
Console.WriteLine($"Granted Scope: {tokenResponse.Scope}");

// The server may grant fewer scopes than requested — check before relying on them.
if (tokenResponse.Scope != requestedScope)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comparison is not reliable. The scope value is a space separated list and the server does not guarantee any ordering. If the server grants everything but returns the scopes in a different order (like write:data read:data), this check will still report a warning even though nothing is actually missing.

Can we compare them as sets instead? Something like splitting both strings on spaces and checking that every requested scope is present in the granted ones. Since this example gets copied into real apps, I would rather we show the correct pattern here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the example

/// requested scope to detect an insufficient-scope grant.
/// </remarks>
[JsonPropertyName("scope")]
public string Scope { get; set; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving Scope up to the base class is fine for source code, but it is a binary breaking change. Any app that was compiled against the older version and reads ClientInitiatedBackchannelAuthorizationTokenResponse.Scope will throw a MissingMethodException at runtime until it is recompiled.

In practice most people recompile with the new package, so the impact is small, but the PR description says this is non breaking. Can we update the description or the release notes to call this out?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a callout in the PR description. Will ensure to add a callout in the CHANGELOG on Release

).ConfigureAwait(false);

await AssertIdTokenValid(response.IdToken, request.ClientId, request.SigningAlgorithm, request.ClientSecret, request.Organization, request.Nonce).ConfigureAwait(false);
await AssertIdTokenValidIfExisting(response.IdToken, request.ClientId, request.SigningAlgorithm, request.ClientSecret, request.Organization, request.Nonce).ConfigureAwait(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change looks correct for MRRT. A refresh that targets a different audience and drops openid will not return an id_token, so skipping validation when the token is absent is the right call and it matches the other flows.

One small thing worth noting: when a caller does request openid and expects an id_token back, an absent token now passes silently and IdToken will be null instead of throwing. Not a blocker, but it might be worth a line in the docs telling openid callers to null check response.IdToken.

@kailash-b
kailash-b requested a review from nandan-bhat July 15, 2026 09:04
@kailash-b
kailash-b enabled auto-merge (squash) July 15, 2026 09:05
@kailash-b
kailash-b merged commit 62d129f into master Jul 15, 2026
10 checks passed
@kailash-b
kailash-b deleted the feat/SDK-9970 branch July 15, 2026 09:07
@kailash-b kailash-b mentioned this pull request Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants