Skip to content

Derive ARM auth scopes from endpoint URL#928

Open
cataggar wants to merge 1 commit intoAzure:mainfrom
cataggar:fix/arm-auth-scope
Open

Derive ARM auth scopes from endpoint URL#928
cataggar wants to merge 1 commit intoAzure:mainfrom
cataggar:fix/arm-auth-scope

Conversation

@cataggar
Copy link
Copy Markdown
Member

@cataggar cataggar commented Apr 4, 2026

Problem

ARM TypeSpec specs declare OAuth2 with relative scopes like user_impersonation. The emitter passes this bare scope verbatim to BearerTokenAuthorizationPolicy::new(). When Azure CLI or other identity libraries try to acquire a token for just user_impersonation, it resolves to Microsoft Graph (00000003-0000-0000-c000-000000000000) instead of Azure Management, causing authentication failures:

AADSTS65002: Consent between first party application '04b07795-...'
and first party resource '00000003-...' must be configured via preauthorization

Fix

When scopes are relative (not absolute URLs), generate code that derives the scope from the endpoint origin at runtime using the standard Azure SDK pattern: {endpoint_origin}/.default.

Before (broken):

BearerTokenAuthorizationPolicy::new(credential, vec!["user_impersonation"])

After (fixed):

BearerTokenAuthorizationPolicy::new(
    credential,
    vec![format!("{}/.default", endpoint.origin().ascii_serialization())],
)

Absolute scopes (e.g. https://vault.azure.net/.default used by data-plane services) are still emitted as static strings.

This is consistent with other Azure SDK languages (Python uses f"{base_url}/.default", Go uses cloud.Services[ResourceManager].Audience + "/.default").

Validation

ARM TypeSpec specs declare OAuth2 with relative scopes like
'user_impersonation'. Passing this bare scope to the Azure
identity libraries fails because it resolves to the wrong
resource (Microsoft Graph instead of Azure Management).

When scopes are relative (not absolute URLs), generate code
that derives the scope from the endpoint origin at runtime
using the '{endpoint_origin}/.default' pattern. This is
consistent with other Azure SDK languages (Python, Go) and
correctly handles sovereign clouds.

Absolute scopes (e.g. 'https://vault.azure.net/.default')
are still emitted as static strings.
@cataggar cataggar marked this pull request as ready for review April 7, 2026 18:21
Copilot AI review requested due to automatic review settings April 7, 2026 18:21
@cataggar cataggar marked this pull request as draft April 7, 2026 18:22
@cataggar cataggar marked this pull request as ready for review April 7, 2026 18:23
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Updates the Rust TypeSpec emitter to correctly derive Azure Resource Manager (ARM) OAuth2 scopes from the service endpoint when specs provide relative scopes (e.g., user_impersonation), preventing tokens from being requested for the wrong resource.

Changes:

  • Emit {endpoint_origin}/.default at runtime for relative scopes when constructing BearerTokenAuthorizationPolicy.
  • Keep absolute (URL) scopes emitted as static strings.
  • Regenerate affected spector test crates to reflect the new scope behavior.

Reviewed changes

Copilot reviewed 1 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/typespec-rust/src/codegen/clients.ts Adds relative-scope detection and emits runtime-derived /.default scope based on endpoint origin.
packages/typespec-rust/test/spector/azure/resource-manager/resources/src/generated/clients/resources_client.rs Updates generated auth policy scopes to use derived /.default.
packages/typespec-rust/test/spector/azure/resource-manager/operation-templates/src/generated/clients/operation_templates_client.rs Updates generated auth policy scopes to use derived /.default.
packages/typespec-rust/test/spector/azure/resource-manager/non-resource/src/generated/clients/non_resource_client.rs Updates generated auth policy scopes to use derived /.default.
packages/typespec-rust/test/spector/azure/resource-manager/multi-service/src/generated/clients/combined_client.rs Updates generated auth policy scopes to use derived /.default.
packages/typespec-rust/test/spector/azure/resource-manager/multi-service-shared-models/src/generated/clients/combined_client.rs Updates generated auth policy scopes to use derived /.default.
packages/typespec-rust/test/spector/azure/resource-manager/multi-service-older-versions/src/generated/clients/combined_client.rs Updates generated auth policy scopes to use derived /.default.
packages/typespec-rust/test/spector/azure/resource-manager/method-subscription-id/src/generated/clients/method_subscription_id_client.rs Updates generated auth policy scopes to use derived /.default.
packages/typespec-rust/test/spector/azure/resource-manager/large-header/src/generated/clients/large_header_client.rs Updates generated auth policy scopes to use derived /.default.
packages/typespec-rust/test/spector/azure/resource-manager/common-properties/src/generated/clients/common_properties_client.rs Updates generated auth policy scopes to use derived /.default.
Comments suppressed due to low confidence (1)

packages/typespec-rust/src/codegen/clients.ts:700

  • If arcTokenCred.scopes ever contains a mix of relative and absolute scopes, this branch drops all provided scopes and emits only the derived /.default scope. A more robust approach is to transform each scope independently (relative → derived runtime /.default, absolute → keep as-is) so mixed inputs don’t silently lose information.
      const hasRelativeScopes = arcTokenCred.scopes.some(s => !s.startsWith('http'));
      if (hasRelativeScopes) {
        // Relative scopes (e.g. "user_impersonation" from ARM specs) must be
        // qualified at runtime using the service endpoint so that sovereign
        // clouds work correctly.  The standard Azure SDK pattern is
        // "{endpoint_origin}/.default".
        return `let auth_policy: Arc<dyn Policy> = Arc::new(BearerTokenAuthorizationPolicy::new(credential, vec![format!("{}/.default", ${endpointParamName}.origin().ascii_serialization())]));`;
      }
      const scopes = new Array<string>();
      for (const scope of arcTokenCred.scopes) {
        scopes.push(`"${scope}"`);
      }
      return `let auth_policy: Arc<dyn Policy> = Arc::new(BearerTokenAuthorizationPolicy::new(credential, vec![${scopes.join(', ')}]));`;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

const arcTokenCred = utils.asTypeOf<rust.TokenCredential>(param.type, 'tokenCredential', 'arc');
if (arcTokenCred) {
use.add('azure_core::http::policies', 'auth::BearerTokenAuthorizationPolicy', 'Policy');
const hasRelativeScopes = arcTokenCred.scopes.some(s => !s.startsWith('http'));
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

!s.startsWith('http') misclassifies non-HTTP absolute scopes (e.g., api://{client-id}/.default) as “relative”, which would incorrectly replace them with {endpoint_origin}/.default. Consider detecting “absolute” scopes by checking for a URI scheme (e.g., s.includes('://')) or using URL parsing where possible, and only treating truly relative bare scopes as relative.

Suggested change
const hasRelativeScopes = arcTokenCred.scopes.some(s => !s.startsWith('http'));
const hasRelativeScopes = arcTokenCred.scopes.some(s => !s.includes('://'));

Copilot uses AI. Check for mistakes.
Comment on lines +688 to +689
const hasRelativeScopes = arcTokenCred.scopes.some(s => !s.startsWith('http'));
if (hasRelativeScopes) {
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

This introduces new branching behavior for scope classification. Add an emitter test that covers (1) relative scopes (user_impersonation), (2) absolute https scopes (https://vault.azure.net/.default), and (3) non-HTTP absolute scopes (e.g., api://.../.default) to prevent regressions in the relative/absolute detection.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@copilot apply changes based on active feedback

@heaths
Copy link
Copy Markdown
Member

heaths commented Apr 7, 2026

@antkmsft @jhendrixMSFT the code changes look fine to me, but I don't know enough about ARM auth scopes. Can you review and approve?

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.

5 participants