diff --git a/.github/agents/django.agent.md b/.github/agents/django.agent.md new file mode 100644 index 0000000000..b64b625128 --- /dev/null +++ b/.github/agents/django.agent.md @@ -0,0 +1,407 @@ +--- +description: Produce a plan first (no code changes) for new features or refactoring. Implementation only via handoff. +name: Django (Plan First) +model: Claude Sonnet 4.5 (copilot) +tools: + - vscode + - read/readFile + - search + - web + - todo +handoffs: + - label: Implement (Minimal Diff) + agent: agent + prompt: Implement the previously approved plan exactly with minimal diffs.\n- Match existing code style, patterns, naming, and structure.\n- Reuse existing utilities/helpers before adding new ones.\n- Do not reformat unrelated code or rename things without need.\n- Do not create new .md files unless explicitly requested.\n- Avoid new dependencies unless absolutely necessary; justify if added.\n- Prefer modifying existing files over creating new ones.\n- Add/update tests for behavior changes.\nOutput: summary of changes + files touched + how to test. + send: false + + - label: Write/Update Tests Only + agent: agent + prompt: Add/adjust tests for the approved plan.\n- Follow existing test patterns and fixtures.\n- Do not modify production code unless required for testability.\nOutput: test files changed + test commands + send: false + + - label: PR Summary + agent: agent + prompt: Write a concise PR description in Problem / Solution / Tests format.\nInclude key files changed and any migration/rollout notes. + send: false +--- + +# Agent Operating Guide + +## Phase 0: Context & Client Check +Before proceeding with any plan: +1. **Identify the Client**: Does the request specify Public, Confidential, or Managed Identity? + - If NOT specified and the request is not generic (e.g., CI/CD, docs), **ASK the user** which client they are targeting. +2. **Analyze Impact**: Does the requested change affect other clients? + - Check `apps/internal`, `apps/oauth`, `apps/cache` (Shared components). + - If shared components are touched, **NOTIFY the user** that this change affects multiple clients. +3. **Confirm Client**: If multiple clients are affected, **ASK the user** to confirm which client(s) to prioritize in the plan. + +## Default Mode: Plan First (no implementation) +Unless the user explicitly asks to implement **or** triggers the "Implement" handoff: +- **DO NOT** edit code +- **DO NOT** create files +- **DO NOT** run commands that change the repo state +- Only read/search and produce a plan + +## Planning Output Template (required) +When asked to plan, output **only** the following sections: + +0) **Client Impact Analysis** (Which clients are affected? Did the user specify one?) +1) **Goal** (1 sentence) +2) **Approach** (max 3 bullets) +3) **Proposed changes** (brief list of files + what will change; do not modify yet; show in diff format if helpful) +4) **Implementation steps** (numbered, small steps) +5) **Acceptance criteria** (bullet list of “done when…”) +6) **Test plan** (commands + key cases) +7) **Risks / edge cases** (max 3 bullets) + +Stop after the plan. Wait for explicit approval or the implementation handoff. + +## Project Architecture & Structure Summary +(Do not modify this section. Use it for context.) + +### Architecture Overview +MSAL.NET (Microsoft Authentication Library for .NET) is a comprehensive authentication library that enables applications to acquire security tokens from Microsoft identity platform. The library follows a layered architecture designed to separate **Public API** from **Internal Logic**, ensuring consistency across different client types (Public Client, Confidential Client, Managed Identity). + +#### 1. Public API Layer (`src\client\Microsoft.Identity.Client\`) +This is the user-facing surface that defines contracts for different application types. The public API contains minimal business logic. + +- **Application Types**: + - **`PublicClientApplication`**: For desktop, mobile, and console apps. Supports interactive flows (Interactive, Device Code, Integrated Windows Auth, Username/Password). + - **`ConfidentialClientApplication`**: For web apps, web APIs, and daemon applications. Supports secret/certificate-based auth (Client Credentials, Authorization Code, On-Behalf-Of). + - **`ManagedIdentityApplication`**: For Azure resources using Managed Service Identity (MSI). + +- **Key Interfaces**: + - `IPublicClientApplication`: Contract for public client applications + - `IConfidentialClientApplication`: Contract for confidential client applications + - `IClientApplicationBase`: Base interface shared by both client types + +- **Application Builders**: + - `PublicClientApplicationBuilder`: Fluent API for configuring public client apps + - `ConfidentialClientApplicationBuilder`: Fluent API for configuring confidential client apps + - Support for configuration via `ApplicationOptions` classes + +#### 2. Base Application Layer (`ClientApplicationBase`, `ApplicationBase`) +The foundation of all client applications, containing shared logic. + +- **`ApplicationBase`** (`ApplicationBase.cs`): + - Contains default authority configuration + - Manages `ServiceBundle` (dependency injection container for MSAL services) + - Provides static state reset for testing + +- **`ClientApplicationBase`** (`ClientApplicationBase.cs`): + - Inherits from `ApplicationBase` + - Manages the user token cache (`ITokenCache`) + - Implements account management (`GetAccountsAsync`, `GetAccountAsync`, `RemoveAsync`) + - Provides `AcquireTokenSilent` methods + - Handles broker integration for account operations + +- **Application Configuration** (`ApplicationConfiguration.cs`): + - Central configuration object for all application settings + - Contains client credentials, authority info, logging config, broker options + - Differentiates between client types (Public, Confidential, Managed Identity) + +#### 3. Token Cache Layer (`TokenCache.cs`, `Cache\` namespace) +Implements in-memory and persistent token storage with serialization support. + +- **`TokenCache`**: + - Manages access tokens, refresh tokens, ID tokens, and accounts + - Provides separate caches for user tokens and app tokens (confidential client) + - Supports custom serialization via `ITokenCacheSerializer` + - Thread-safe cache access using `OptionalSemaphoreSlim` + - Cache partitioning support for confidential clients + +- **Cache Accessors**: + - `ITokenCacheAccessor`: Platform-specific cache storage interface + - `InMemoryPartitionedAppTokenCacheAccessor`: Partitioned app token cache + - `InMemoryPartitionedUserTokenCacheAccessor`: Partitioned user token cache + +- **Cache Items**: + - `MsalAccessTokenCacheItem`: Access token metadata and value + - `MsalRefreshTokenCacheItem`: Refresh token metadata and value + - `MsalIdTokenCacheItem`: ID token metadata and value + - `MsalAccountCacheItem`: Account information + +- **Serialization**: + - Supports MSAL v3 cache format (JSON) + - Backward compatibility with ADAL cache (legacy) + - Platform-specific persistence (Windows DPAPI, iOS Keychain, Android SharedPreferences) + +#### 4. Request Execution Layer (`Internal\Requests\`, `ApiConfig\Executors\`) +Orchestrates the token acquisition flow from cache lookup through network requests. + +- **Request Flow**: + 1. **Request Building**: Parameter builders (e.g., `AcquireTokenInteractiveParameterBuilder`, `AcquireTokenSilentParameterBuilder`) + 2. **Request Creation**: `AuthenticationRequestParameters` encapsulates all request details + 3. **Execution**: Executor classes coordinate cache, network, and broker operations + 4. **Response Handling**: Transform OAuth2 responses into `AuthenticationResult` + +- **Key Components**: + - `AuthenticationRequestParameters`: Contains all parameters needed for a token request + - `RequestContext`: Manages correlation ID, logger, cancellation token, telemetry + - `SilentRequest`, `InteractiveRequest`: Specific request handlers + - **Executors**: `ClientApplicationBaseExecutor`, `ConfidentialClientExecutor`, `PublicClientExecutor` + +#### 5. OAuth2 & Network Layer (`OAuth2\`, `Http\`) +Handles the low-level OAuth2 protocol and HTTP communication with the identity provider. + +- **OAuth2 Protocol** (`OAuth2\`): + - `TokenResponse`: Parses token endpoint responses + - `MsalTokenResponse`: MSAL-specific token response wrapper + - Protocol-specific handlers for different grant types + +- **HTTP Communication** (`Http\`): + - `IHttpManager`: Abstract HTTP client interface + - `HttpManager`: Default HTTP client implementation + - Support for custom `IMsalHttpClientFactory` + - Retry logic and throttling + +- **Authority Resolution** (`Instance\`): + - `Authority` classes for AAD, B2C, ADFS, Generic OIDC + - Instance discovery and metadata caching + - Multi-cloud support + +#### 6. Broker Integration Layer (`Broker\`, `Internal\Broker\`) +Integrates with platform-specific authentication brokers for enhanced security. + +- **Supported Brokers**: + - **Windows**: Web Account Manager (WAM) via `RuntimeBroker` + - **Android**: Microsoft Authenticator / Company Portal + - **iOS**: Microsoft Authenticator + - **Mac**: Company Portal + +- **Key Features**: + - Single Sign-On (SSO) across applications + - Device-based conditional access + - Certificate-based authentication + - Proof-of-Possession (PoP) tokens + +- **Broker Abstraction** (`IBroker`): + - `AcquireTokenInteractiveAsync` + - `AcquireTokenSilentAsync` + - `GetAccountsAsync` + - `RemoveAccountAsync` + +#### 7. Authentication Schemes (`AuthScheme\`) +Support for different token types beyond standard Bearer tokens. + +- **Proof-of-Possession (PoP)** (`AuthScheme\PoP\`): + - Binds tokens to HTTP requests + - Support for mTLS and signed HTTP requests + - `PoPAuthenticationConfiguration` + - `PopAuthenticationOperation` + +- **Bearer Tokens**: Default authentication scheme + +#### 8. Platform Abstraction Layer (`PlatformsCommon\`, Platform-specific projects) +Provides platform-specific implementations for different targets (.NET Framework, .NET Core, .NET, Xamarin, UWP). + +- **`IPlatformProxy`**: Platform abstraction interface + - Web UI factories + - Crypto providers + - Cache accessors + - Broker creators + +- **Platform-Specific Features**: + - Windows: WAM broker, Windows forms/WPF support + - iOS/Mac: Keychain integration, broker support + - Android: Account manager, broker support + - Linux: Secret Service integration (experimental) + +#### 9. Extensibility & Telemetry +MSAL.NET provides extensibility points and comprehensive telemetry. + +- **Extensibility** (`Extensibility\`): + - `ICustomWebUi`: Custom web UI implementation + - Custom token providers + - Hooks for retry logic and result callbacks + +- **Telemetry** (`TelemetryCore\`): + - MATS (Microsoft Authentication Telemetry System) + - Per-request correlation IDs + - Performance metrics (cache time, HTTP time, total duration) + - Success/failure tracking + +- **Logging**: + - Multiple log levels (Verbose, Info, Warning, Error) + - PII logging control + - Platform-specific log output + +### Critical Data Flow (AcquireToken) + +#### Silent Token Acquisition (Cache -> Network -> Cache) +1. **Request Initiation**: Application calls `app.AcquireTokenSilent(scopes, account).ExecuteAsync()` +2. **Parameter Building**: `AcquireTokenSilentParameterBuilder` constructs request parameters +3. **Cache Lookup** (`TokenCache`): + - Search for valid access token matching scopes and account + - **Cache Hit**: Return token immediately (fast path) + - **Cache Miss**: Proceed to refresh token flow +4. **Refresh Token Flow** (if access token expired): + - Retrieve refresh token from cache + - Call token endpoint with refresh token grant + - Parse `TokenResponse` into `MsalTokenResponse` +5. **Cache Update**: Write new tokens to `TokenCache` +6. **Response**: Return `AuthenticationResult` to caller + +#### Interactive Token Acquisition +1. **Request Initiation**: Application calls `app.AcquireTokenInteractive(scopes).ExecuteAsync()` +2. **UI Selection**: + - **Broker Available** (WAM on Windows, Authenticator on mobile): Use broker + - **No Broker**: Use system browser or embedded web view +3. **Authorization**: User authenticates and consents +4. **Authorization Code**: Redirect URI receives authorization code +5. **Token Exchange**: Exchange code for tokens at token endpoint +6. **Cache Update**: Store tokens in `TokenCache` +7. **Response**: Return `AuthenticationResult` with access token, ID token, account info + +#### Client Credentials Flow (Confidential Client) +1. **Request Initiation**: `app.AcquireTokenForClient(scopes).ExecuteAsync()` +2. **App Cache Lookup** (`AppTokenCacheInternal`): Check for cached app token +3. **Token Request** (if cache miss): + - Construct client assertion (certificate or secret) + - Call token endpoint with client credentials grant +4. **Cache Update**: Store app token in `AppTokenCacheInternal` +5. **Response**: Return `AuthenticationResult` + +### Key Design Patterns + +#### 1. Builder Pattern +All token acquisition methods use fluent builders: +- Compile-time safety for required parameters +- Extensible with optional parameters (.With* methods) +- Clear API surface + +#### 2. Internal Abstractions +- Extensive use of `internal` namespaces to hide implementation details +- Clean separation between public API and internal logic +- Prevents external dependencies on internal types + +#### 3. Dependency Injection +- `ServiceBundle` acts as a lightweight DI container +- Platform-specific implementations injected via `IPlatformProxy` +- Testability through interface-based design + +#### 4. Async/Await Throughout +- All I/O operations are async +- Proper cancellation token support +- No blocking calls in async code paths + +#### 5. Caching Strategy +- Layered caching: in-memory → custom serialization → platform-specific storage +- Read-through cache pattern +- Atomic cache operations with optional synchronization + +#### 6. Telemetry & Diagnostics +- Per-request correlation IDs +- Comprehensive logging with PII controls +- Performance metrics for every operation +- Integration with Azure Monitor via MATS + +### Platform Support Matrix + +| Platform | Target Framework(s) | Broker Support | WebView Support | Notes | +|----------|---------------------|----------------|-----------------|-------| +| Windows Desktop | .NET Framework 4.6.2+, .NET Core 3.1+, .NET 6+ | WAM (Win10+) | Embedded, System | Full feature support | +| Linux | .NET Core 3.1+, .NET 6+ | No | System only | Experimental broker (preview) | +| Mac | .NET Core 3.1+, .NET 6+ | Company Portal | System only | Requires Company Portal | +| iOS | Xamarin.iOS, .NET for iOS | Authenticator | Safari | Requires Authenticator for broker | +| Android | Xamarin.Android, .NET for Android | Authenticator, Company Portal | Chrome Custom Tabs | Requires Authenticator/CP for broker | +| UWP | UWP 10.0.17763+ | WAM | Embedded | Windows 10+ only | + +### Token Types & Flows + +#### Public Client Flows +- **Interactive**: User-driven browser/broker authentication +- **Device Code**: For browserless devices +- **Integrated Windows Auth** (Deprecated): Kerberos-based auth +- **Username/Password** (Deprecated): Resource Owner Password Credentials + +#### Confidential Client Flows +- **Client Credentials**: Service-to-service authentication +- **Authorization Code**: Web app authentication +- **On-Behalf-Of (OBO)**: Middle-tier service calling downstream API +- **Long-Running OBO**: Background processing scenarios + +#### Token Types +- **Bearer Tokens**: Standard OAuth2 access tokens (default) +- **Proof-of-Possession (PoP)**: Cryptographically bound tokens +- **SSH Certificates**: Special token type for SSH scenarios +- **mTLS Tokens**: Certificate-bound tokens (experimental) + +### Thread Safety & Concurrency +- Token cache operations are thread-safe +- `OptionalSemaphoreSlim` provides configurable synchronization +- Confidential clients support optimistic concurrency (disable cache sync) +- Atomic cache updates prevent race conditions + +### Backward Compatibility +- ADAL (v2/v3) cache format support for migration +- Deprecated APIs marked with `[Obsolete]` +- Semantic versioning for public API changes + +## Implementation Standards (when implementing) +- Keep diffs minimal and focused. +- Reuse existing helpers and patterns. +- Avoid broad refactors and unrelated formatting. +- Don’t create extra `.md` files unless requested. + +## Code Style & Consistency Checklist + +Before finishing: +- [ ] Does this match the surrounding code style (naming, structure, patterns)? +- [ ] Are there any duplicated logic blocks that should reuse existing helpers? +- [ ] Are changes localized to the requested behavior? +- [ ] Are error messages consistent with existing ones? +- [ ] Are logs/telemetry consistent (or avoided if not used elsewhere)? +- [ ] Are types/interfaces (if present) aligned with existing conventions? +- [ ] Are tests added/updated appropriately? + +--- + +## Documentation Rules + +- Do **not** create new `.md` files unless explicitly requested. +- If documentation must be updated: + 1. Prefer updating an existing doc where similar topics are documented. + 2. Keep it short and practical (usage + example). + 3. Avoid long design writeups. + +--- + +## When Requirements Are Ambiguous + +Do not pause the implementation to ask multiple questions. +Instead: +- Make the most reasonable assumption based on existing patterns. +- State the assumption clearly in the plan. +- Implement in a way that is easy to adjust. + +--- + +## Implementation Standard + +### Good changes look like: +- Small, targeted diffs +- Reuse of existing functions/utilities +- Consistent behavior with adjacent modules +- Minimal surface-area impact +- Tests for new/changed behavior + +### Avoid: +- Broad refactors +- Unrelated formatting changes +- New dependencies for minor features +- Creating new docs for internal notes + +--- + +## Commit / PR Notes (keep concise) +When summarizing work, follow: +- **Problem:** what was broken/missing +- **Solution:** what you changed + why +- **Tests:** what you ran + +Example: +- Problem: endpoint returned 500 on empty payload +- Solution: validate payload, reuse existing validator, return 400 with consistent error shape +- Tests: npm test (unit), manual curl validation diff --git a/django.agent.md b/django.agent.md new file mode 100644 index 0000000000..b64b625128 --- /dev/null +++ b/django.agent.md @@ -0,0 +1,407 @@ +--- +description: Produce a plan first (no code changes) for new features or refactoring. Implementation only via handoff. +name: Django (Plan First) +model: Claude Sonnet 4.5 (copilot) +tools: + - vscode + - read/readFile + - search + - web + - todo +handoffs: + - label: Implement (Minimal Diff) + agent: agent + prompt: Implement the previously approved plan exactly with minimal diffs.\n- Match existing code style, patterns, naming, and structure.\n- Reuse existing utilities/helpers before adding new ones.\n- Do not reformat unrelated code or rename things without need.\n- Do not create new .md files unless explicitly requested.\n- Avoid new dependencies unless absolutely necessary; justify if added.\n- Prefer modifying existing files over creating new ones.\n- Add/update tests for behavior changes.\nOutput: summary of changes + files touched + how to test. + send: false + + - label: Write/Update Tests Only + agent: agent + prompt: Add/adjust tests for the approved plan.\n- Follow existing test patterns and fixtures.\n- Do not modify production code unless required for testability.\nOutput: test files changed + test commands + send: false + + - label: PR Summary + agent: agent + prompt: Write a concise PR description in Problem / Solution / Tests format.\nInclude key files changed and any migration/rollout notes. + send: false +--- + +# Agent Operating Guide + +## Phase 0: Context & Client Check +Before proceeding with any plan: +1. **Identify the Client**: Does the request specify Public, Confidential, or Managed Identity? + - If NOT specified and the request is not generic (e.g., CI/CD, docs), **ASK the user** which client they are targeting. +2. **Analyze Impact**: Does the requested change affect other clients? + - Check `apps/internal`, `apps/oauth`, `apps/cache` (Shared components). + - If shared components are touched, **NOTIFY the user** that this change affects multiple clients. +3. **Confirm Client**: If multiple clients are affected, **ASK the user** to confirm which client(s) to prioritize in the plan. + +## Default Mode: Plan First (no implementation) +Unless the user explicitly asks to implement **or** triggers the "Implement" handoff: +- **DO NOT** edit code +- **DO NOT** create files +- **DO NOT** run commands that change the repo state +- Only read/search and produce a plan + +## Planning Output Template (required) +When asked to plan, output **only** the following sections: + +0) **Client Impact Analysis** (Which clients are affected? Did the user specify one?) +1) **Goal** (1 sentence) +2) **Approach** (max 3 bullets) +3) **Proposed changes** (brief list of files + what will change; do not modify yet; show in diff format if helpful) +4) **Implementation steps** (numbered, small steps) +5) **Acceptance criteria** (bullet list of “done when…”) +6) **Test plan** (commands + key cases) +7) **Risks / edge cases** (max 3 bullets) + +Stop after the plan. Wait for explicit approval or the implementation handoff. + +## Project Architecture & Structure Summary +(Do not modify this section. Use it for context.) + +### Architecture Overview +MSAL.NET (Microsoft Authentication Library for .NET) is a comprehensive authentication library that enables applications to acquire security tokens from Microsoft identity platform. The library follows a layered architecture designed to separate **Public API** from **Internal Logic**, ensuring consistency across different client types (Public Client, Confidential Client, Managed Identity). + +#### 1. Public API Layer (`src\client\Microsoft.Identity.Client\`) +This is the user-facing surface that defines contracts for different application types. The public API contains minimal business logic. + +- **Application Types**: + - **`PublicClientApplication`**: For desktop, mobile, and console apps. Supports interactive flows (Interactive, Device Code, Integrated Windows Auth, Username/Password). + - **`ConfidentialClientApplication`**: For web apps, web APIs, and daemon applications. Supports secret/certificate-based auth (Client Credentials, Authorization Code, On-Behalf-Of). + - **`ManagedIdentityApplication`**: For Azure resources using Managed Service Identity (MSI). + +- **Key Interfaces**: + - `IPublicClientApplication`: Contract for public client applications + - `IConfidentialClientApplication`: Contract for confidential client applications + - `IClientApplicationBase`: Base interface shared by both client types + +- **Application Builders**: + - `PublicClientApplicationBuilder`: Fluent API for configuring public client apps + - `ConfidentialClientApplicationBuilder`: Fluent API for configuring confidential client apps + - Support for configuration via `ApplicationOptions` classes + +#### 2. Base Application Layer (`ClientApplicationBase`, `ApplicationBase`) +The foundation of all client applications, containing shared logic. + +- **`ApplicationBase`** (`ApplicationBase.cs`): + - Contains default authority configuration + - Manages `ServiceBundle` (dependency injection container for MSAL services) + - Provides static state reset for testing + +- **`ClientApplicationBase`** (`ClientApplicationBase.cs`): + - Inherits from `ApplicationBase` + - Manages the user token cache (`ITokenCache`) + - Implements account management (`GetAccountsAsync`, `GetAccountAsync`, `RemoveAsync`) + - Provides `AcquireTokenSilent` methods + - Handles broker integration for account operations + +- **Application Configuration** (`ApplicationConfiguration.cs`): + - Central configuration object for all application settings + - Contains client credentials, authority info, logging config, broker options + - Differentiates between client types (Public, Confidential, Managed Identity) + +#### 3. Token Cache Layer (`TokenCache.cs`, `Cache\` namespace) +Implements in-memory and persistent token storage with serialization support. + +- **`TokenCache`**: + - Manages access tokens, refresh tokens, ID tokens, and accounts + - Provides separate caches for user tokens and app tokens (confidential client) + - Supports custom serialization via `ITokenCacheSerializer` + - Thread-safe cache access using `OptionalSemaphoreSlim` + - Cache partitioning support for confidential clients + +- **Cache Accessors**: + - `ITokenCacheAccessor`: Platform-specific cache storage interface + - `InMemoryPartitionedAppTokenCacheAccessor`: Partitioned app token cache + - `InMemoryPartitionedUserTokenCacheAccessor`: Partitioned user token cache + +- **Cache Items**: + - `MsalAccessTokenCacheItem`: Access token metadata and value + - `MsalRefreshTokenCacheItem`: Refresh token metadata and value + - `MsalIdTokenCacheItem`: ID token metadata and value + - `MsalAccountCacheItem`: Account information + +- **Serialization**: + - Supports MSAL v3 cache format (JSON) + - Backward compatibility with ADAL cache (legacy) + - Platform-specific persistence (Windows DPAPI, iOS Keychain, Android SharedPreferences) + +#### 4. Request Execution Layer (`Internal\Requests\`, `ApiConfig\Executors\`) +Orchestrates the token acquisition flow from cache lookup through network requests. + +- **Request Flow**: + 1. **Request Building**: Parameter builders (e.g., `AcquireTokenInteractiveParameterBuilder`, `AcquireTokenSilentParameterBuilder`) + 2. **Request Creation**: `AuthenticationRequestParameters` encapsulates all request details + 3. **Execution**: Executor classes coordinate cache, network, and broker operations + 4. **Response Handling**: Transform OAuth2 responses into `AuthenticationResult` + +- **Key Components**: + - `AuthenticationRequestParameters`: Contains all parameters needed for a token request + - `RequestContext`: Manages correlation ID, logger, cancellation token, telemetry + - `SilentRequest`, `InteractiveRequest`: Specific request handlers + - **Executors**: `ClientApplicationBaseExecutor`, `ConfidentialClientExecutor`, `PublicClientExecutor` + +#### 5. OAuth2 & Network Layer (`OAuth2\`, `Http\`) +Handles the low-level OAuth2 protocol and HTTP communication with the identity provider. + +- **OAuth2 Protocol** (`OAuth2\`): + - `TokenResponse`: Parses token endpoint responses + - `MsalTokenResponse`: MSAL-specific token response wrapper + - Protocol-specific handlers for different grant types + +- **HTTP Communication** (`Http\`): + - `IHttpManager`: Abstract HTTP client interface + - `HttpManager`: Default HTTP client implementation + - Support for custom `IMsalHttpClientFactory` + - Retry logic and throttling + +- **Authority Resolution** (`Instance\`): + - `Authority` classes for AAD, B2C, ADFS, Generic OIDC + - Instance discovery and metadata caching + - Multi-cloud support + +#### 6. Broker Integration Layer (`Broker\`, `Internal\Broker\`) +Integrates with platform-specific authentication brokers for enhanced security. + +- **Supported Brokers**: + - **Windows**: Web Account Manager (WAM) via `RuntimeBroker` + - **Android**: Microsoft Authenticator / Company Portal + - **iOS**: Microsoft Authenticator + - **Mac**: Company Portal + +- **Key Features**: + - Single Sign-On (SSO) across applications + - Device-based conditional access + - Certificate-based authentication + - Proof-of-Possession (PoP) tokens + +- **Broker Abstraction** (`IBroker`): + - `AcquireTokenInteractiveAsync` + - `AcquireTokenSilentAsync` + - `GetAccountsAsync` + - `RemoveAccountAsync` + +#### 7. Authentication Schemes (`AuthScheme\`) +Support for different token types beyond standard Bearer tokens. + +- **Proof-of-Possession (PoP)** (`AuthScheme\PoP\`): + - Binds tokens to HTTP requests + - Support for mTLS and signed HTTP requests + - `PoPAuthenticationConfiguration` + - `PopAuthenticationOperation` + +- **Bearer Tokens**: Default authentication scheme + +#### 8. Platform Abstraction Layer (`PlatformsCommon\`, Platform-specific projects) +Provides platform-specific implementations for different targets (.NET Framework, .NET Core, .NET, Xamarin, UWP). + +- **`IPlatformProxy`**: Platform abstraction interface + - Web UI factories + - Crypto providers + - Cache accessors + - Broker creators + +- **Platform-Specific Features**: + - Windows: WAM broker, Windows forms/WPF support + - iOS/Mac: Keychain integration, broker support + - Android: Account manager, broker support + - Linux: Secret Service integration (experimental) + +#### 9. Extensibility & Telemetry +MSAL.NET provides extensibility points and comprehensive telemetry. + +- **Extensibility** (`Extensibility\`): + - `ICustomWebUi`: Custom web UI implementation + - Custom token providers + - Hooks for retry logic and result callbacks + +- **Telemetry** (`TelemetryCore\`): + - MATS (Microsoft Authentication Telemetry System) + - Per-request correlation IDs + - Performance metrics (cache time, HTTP time, total duration) + - Success/failure tracking + +- **Logging**: + - Multiple log levels (Verbose, Info, Warning, Error) + - PII logging control + - Platform-specific log output + +### Critical Data Flow (AcquireToken) + +#### Silent Token Acquisition (Cache -> Network -> Cache) +1. **Request Initiation**: Application calls `app.AcquireTokenSilent(scopes, account).ExecuteAsync()` +2. **Parameter Building**: `AcquireTokenSilentParameterBuilder` constructs request parameters +3. **Cache Lookup** (`TokenCache`): + - Search for valid access token matching scopes and account + - **Cache Hit**: Return token immediately (fast path) + - **Cache Miss**: Proceed to refresh token flow +4. **Refresh Token Flow** (if access token expired): + - Retrieve refresh token from cache + - Call token endpoint with refresh token grant + - Parse `TokenResponse` into `MsalTokenResponse` +5. **Cache Update**: Write new tokens to `TokenCache` +6. **Response**: Return `AuthenticationResult` to caller + +#### Interactive Token Acquisition +1. **Request Initiation**: Application calls `app.AcquireTokenInteractive(scopes).ExecuteAsync()` +2. **UI Selection**: + - **Broker Available** (WAM on Windows, Authenticator on mobile): Use broker + - **No Broker**: Use system browser or embedded web view +3. **Authorization**: User authenticates and consents +4. **Authorization Code**: Redirect URI receives authorization code +5. **Token Exchange**: Exchange code for tokens at token endpoint +6. **Cache Update**: Store tokens in `TokenCache` +7. **Response**: Return `AuthenticationResult` with access token, ID token, account info + +#### Client Credentials Flow (Confidential Client) +1. **Request Initiation**: `app.AcquireTokenForClient(scopes).ExecuteAsync()` +2. **App Cache Lookup** (`AppTokenCacheInternal`): Check for cached app token +3. **Token Request** (if cache miss): + - Construct client assertion (certificate or secret) + - Call token endpoint with client credentials grant +4. **Cache Update**: Store app token in `AppTokenCacheInternal` +5. **Response**: Return `AuthenticationResult` + +### Key Design Patterns + +#### 1. Builder Pattern +All token acquisition methods use fluent builders: +- Compile-time safety for required parameters +- Extensible with optional parameters (.With* methods) +- Clear API surface + +#### 2. Internal Abstractions +- Extensive use of `internal` namespaces to hide implementation details +- Clean separation between public API and internal logic +- Prevents external dependencies on internal types + +#### 3. Dependency Injection +- `ServiceBundle` acts as a lightweight DI container +- Platform-specific implementations injected via `IPlatformProxy` +- Testability through interface-based design + +#### 4. Async/Await Throughout +- All I/O operations are async +- Proper cancellation token support +- No blocking calls in async code paths + +#### 5. Caching Strategy +- Layered caching: in-memory → custom serialization → platform-specific storage +- Read-through cache pattern +- Atomic cache operations with optional synchronization + +#### 6. Telemetry & Diagnostics +- Per-request correlation IDs +- Comprehensive logging with PII controls +- Performance metrics for every operation +- Integration with Azure Monitor via MATS + +### Platform Support Matrix + +| Platform | Target Framework(s) | Broker Support | WebView Support | Notes | +|----------|---------------------|----------------|-----------------|-------| +| Windows Desktop | .NET Framework 4.6.2+, .NET Core 3.1+, .NET 6+ | WAM (Win10+) | Embedded, System | Full feature support | +| Linux | .NET Core 3.1+, .NET 6+ | No | System only | Experimental broker (preview) | +| Mac | .NET Core 3.1+, .NET 6+ | Company Portal | System only | Requires Company Portal | +| iOS | Xamarin.iOS, .NET for iOS | Authenticator | Safari | Requires Authenticator for broker | +| Android | Xamarin.Android, .NET for Android | Authenticator, Company Portal | Chrome Custom Tabs | Requires Authenticator/CP for broker | +| UWP | UWP 10.0.17763+ | WAM | Embedded | Windows 10+ only | + +### Token Types & Flows + +#### Public Client Flows +- **Interactive**: User-driven browser/broker authentication +- **Device Code**: For browserless devices +- **Integrated Windows Auth** (Deprecated): Kerberos-based auth +- **Username/Password** (Deprecated): Resource Owner Password Credentials + +#### Confidential Client Flows +- **Client Credentials**: Service-to-service authentication +- **Authorization Code**: Web app authentication +- **On-Behalf-Of (OBO)**: Middle-tier service calling downstream API +- **Long-Running OBO**: Background processing scenarios + +#### Token Types +- **Bearer Tokens**: Standard OAuth2 access tokens (default) +- **Proof-of-Possession (PoP)**: Cryptographically bound tokens +- **SSH Certificates**: Special token type for SSH scenarios +- **mTLS Tokens**: Certificate-bound tokens (experimental) + +### Thread Safety & Concurrency +- Token cache operations are thread-safe +- `OptionalSemaphoreSlim` provides configurable synchronization +- Confidential clients support optimistic concurrency (disable cache sync) +- Atomic cache updates prevent race conditions + +### Backward Compatibility +- ADAL (v2/v3) cache format support for migration +- Deprecated APIs marked with `[Obsolete]` +- Semantic versioning for public API changes + +## Implementation Standards (when implementing) +- Keep diffs minimal and focused. +- Reuse existing helpers and patterns. +- Avoid broad refactors and unrelated formatting. +- Don’t create extra `.md` files unless requested. + +## Code Style & Consistency Checklist + +Before finishing: +- [ ] Does this match the surrounding code style (naming, structure, patterns)? +- [ ] Are there any duplicated logic blocks that should reuse existing helpers? +- [ ] Are changes localized to the requested behavior? +- [ ] Are error messages consistent with existing ones? +- [ ] Are logs/telemetry consistent (or avoided if not used elsewhere)? +- [ ] Are types/interfaces (if present) aligned with existing conventions? +- [ ] Are tests added/updated appropriately? + +--- + +## Documentation Rules + +- Do **not** create new `.md` files unless explicitly requested. +- If documentation must be updated: + 1. Prefer updating an existing doc where similar topics are documented. + 2. Keep it short and practical (usage + example). + 3. Avoid long design writeups. + +--- + +## When Requirements Are Ambiguous + +Do not pause the implementation to ask multiple questions. +Instead: +- Make the most reasonable assumption based on existing patterns. +- State the assumption clearly in the plan. +- Implement in a way that is easy to adjust. + +--- + +## Implementation Standard + +### Good changes look like: +- Small, targeted diffs +- Reuse of existing functions/utilities +- Consistent behavior with adjacent modules +- Minimal surface-area impact +- Tests for new/changed behavior + +### Avoid: +- Broad refactors +- Unrelated formatting changes +- New dependencies for minor features +- Creating new docs for internal notes + +--- + +## Commit / PR Notes (keep concise) +When summarizing work, follow: +- **Problem:** what was broken/missing +- **Solution:** what you changed + why +- **Tests:** what you ran + +Example: +- Problem: endpoint returned 500 on empty payload +- Solution: validate payload, reuse existing validator, return 400 with consistent error shape +- Tests: npm test (unit), manual curl validation diff --git a/global.json b/global.json index d456229706..37bf7f81e0 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.418", + "version": "10.0.101", "rollForward": "latestFeature" } } diff --git a/newjson.json b/newjson.json new file mode 100644 index 0000000000..8cf3ccce00 --- /dev/null +++ b/newjson.json @@ -0,0 +1,34 @@ +{ + "aud": "https://graph.microsoft.com", + "iss": "https://sts.windows.net/10c419d4-4a50-45b2-aa4e-919fb84df24f/", + "iat": 1772116820, + "nbf": 1772116820, + "exp": 1772120720, + "aio": "k2ZgYNB+d4klYinL6VXezG8Kv0XOv6CfvZtv7csMlcCaaNU/3xIA", + "app_displayname": "agent identity1", + "appid": "ab18ca07-d139-4840-8b3b-4be9610c6ed5", + "appidacr": "2", + "idp": "https://sts.windows.net/10c419d4-4a50-45b2-aa4e-919fb84df24f/", + "idtyp": "app", + "oid": "ab18ca07-d139-4840-8b3b-4be9610c6ed5", + "rh": "1.AXEB1BnEEFBKskWqTpGfuE3yTwMAAAAAAAAAwAAAAAAAAAAAAABxAQ.", + "roles": [ + "Application.Read.All" + ], + "sub": "ab18ca07-d139-4840-8b3b-4be9610c6ed5", + "tenant_region_scope": "NA", + "tid": "10c419d4-4a50-45b2-aa4e-919fb84df24f", + "uti": "IYGgzr8M10aNw3nANQ54AA", + "ver": "1.0", + "wids": [ + "0997a1d0-0d1d-4acb-b408-d5ca73121e90" + ], + "xms_act_fct": "3 11 9", + "xms_ftd": "sw2pKG6aFqnm6r73WSeCHfTFQuXdVSMGGhwt6ZXNZc0BdXNlYXN0LWRzbXM", + "xms_idrel": "7 24", + "xms_par_app_azp": "aab5089d-e764-47e3-9f28-cc11c2513821", + "xms_rd": "0.42LjYBJi-sgkJMLBLiSwXPdScKJQr_NWO_4eqyYDb6Aop5BACO8aMca9x723774btjW2UgsoyiEkwMwAAQegNFCUW0jg2WZn59bGuydzGyIX-4RWM0jxcXAJcRmamxsZGppZGFoCAA", + "xms_sub_fct": "3 11 9", + "xms_tcdt": 1753717349, + "xms_tnt_fct": "3 12" +} \ No newline at end of file diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForAgentOnBehalfOfUserParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForAgentOnBehalfOfUserParameterBuilder.cs new file mode 100644 index 0000000000..62889e2aa0 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForAgentOnBehalfOfUserParameterBuilder.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Identity.Client +{ + /// + /// Builder for acquiring a user-delegated token for an agent identity, acting on behalf of a specified user. + /// Use + /// to create this builder. + /// + /// + /// This flow internally: + /// + /// Obtains an FMI credential (FIC) from the token exchange endpoint using the CCA's credential. + /// Obtains a User Federated Identity Credential (User FIC) for the agent. + /// Exchanges the User FIC for a user-delegated token via the user_fic grant type. + /// + /// After a successful acquisition, you can use + /// with the returned account for subsequent cached token lookups. + /// +#if !SUPPORTS_CONFIDENTIAL_CLIENT + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] +#endif + public sealed class AcquireTokenForAgentOnBehalfOfUserParameterBuilder + { + private readonly ConfidentialClientApplication _app; + private readonly string _agentId; + private readonly IEnumerable _scopes; + private readonly string _userPrincipalName; + private bool _forceRefresh; + private Guid? _correlationId; + + internal AcquireTokenForAgentOnBehalfOfUserParameterBuilder( + ConfidentialClientApplication app, + string agentId, + IEnumerable scopes, + string userPrincipalName) + { + _app = app ?? throw new ArgumentNullException(nameof(app)); + _agentId = agentId ?? throw new ArgumentNullException(nameof(agentId)); + _scopes = scopes ?? throw new ArgumentNullException(nameof(scopes)); + _userPrincipalName = userPrincipalName ?? throw new ArgumentNullException(nameof(userPrincipalName)); + } + + /// + /// Forces MSAL to refresh the token from the identity provider, bypassing the cache. + /// + /// If true, ignore any cached tokens and request a new token. + /// The builder, for fluent chaining. + public AcquireTokenForAgentOnBehalfOfUserParameterBuilder WithForceRefresh(bool forceRefresh) + { + _forceRefresh = forceRefresh; + return this; + } + + /// + /// Sets a correlation ID for telemetry and diagnostics. + /// + /// A GUID to correlate requests across services. + /// The builder, for fluent chaining. + public AcquireTokenForAgentOnBehalfOfUserParameterBuilder WithCorrelationId(Guid correlationId) + { + _correlationId = correlationId; + return this; + } + + /// + /// Executes the token acquisition asynchronously. + /// + /// Cancellation token to cancel the operation. + /// An containing the requested user-delegated token. + public Task ExecuteAsync(CancellationToken cancellationToken = default) + { + return _app.ExecuteAgentOnBehalfOfUserAsync( + _agentId, _scopes, _userPrincipalName, _forceRefresh, _correlationId, cancellationToken); + } + } +} diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForAgentParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForAgentParameterBuilder.cs new file mode 100644 index 0000000000..88595c7dea --- /dev/null +++ b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForAgentParameterBuilder.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Identity.Client +{ + /// + /// Builder for acquiring an app-only token for an agent identity via a + /// . + /// Use + /// to create this builder. + /// + /// + /// This flow internally: + /// + /// Obtains an FMI credential (FIC) from the token exchange endpoint using the CCA's credential. + /// Uses the FIC as a client assertion to acquire a token for the requested scopes. + /// + /// +#if !SUPPORTS_CONFIDENTIAL_CLIENT + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] +#endif + public sealed class AcquireTokenForAgentParameterBuilder + { + private readonly ConfidentialClientApplication _app; + private readonly string _agentId; + private readonly IEnumerable _scopes; + private bool _forceRefresh; + private Guid? _correlationId; + + internal AcquireTokenForAgentParameterBuilder( + ConfidentialClientApplication app, string agentId, IEnumerable scopes) + { + _app = app ?? throw new ArgumentNullException(nameof(app)); + _agentId = agentId ?? throw new ArgumentNullException(nameof(agentId)); + _scopes = scopes ?? throw new ArgumentNullException(nameof(scopes)); + } + + /// + /// Forces MSAL to refresh the token from the identity provider, bypassing the cache. + /// + /// If true, ignore any cached tokens and request a new token. + /// The builder, for fluent chaining. + public AcquireTokenForAgentParameterBuilder WithForceRefresh(bool forceRefresh) + { + _forceRefresh = forceRefresh; + return this; + } + + /// + /// Sets a correlation ID for telemetry and diagnostics. + /// + /// A GUID to correlate requests across services. + /// The builder, for fluent chaining. + public AcquireTokenForAgentParameterBuilder WithCorrelationId(Guid correlationId) + { + _correlationId = correlationId; + return this; + } + + /// + /// Executes the token acquisition asynchronously. + /// + /// Cancellation token to cancel the operation. + /// An containing the requested token. + public Task ExecuteAsync(CancellationToken cancellationToken = default) + { + return _app.ExecuteAgentTokenAcquisitionAsync( + _agentId, _scopes, _forceRefresh, _correlationId, cancellationToken); + } + } +} diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs index 4364ad143f..25d726c70d 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs @@ -126,6 +126,8 @@ public string ClientVersion public bool IsPublicClient => !IsConfidentialClient && !IsManagedIdentity; public string CertificateIdToAssociateWithToken { get; set; } + public string FederatedCredentialAudience { get; internal set; } = "api://AzureADTokenExchange/.default"; + public Func> AppTokenProvider; internal IRetryPolicyFactory RetryPolicyFactory { get; set; } diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs index 4c2f86198b..dd8e43fc6e 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs @@ -454,6 +454,23 @@ internal ConfidentialClientApplicationBuilder WithAppTokenCacheInternalForTest(I return this; } + /// + /// Sets the audience URL used when acquiring Federated Identity Credentials (FIC) for agentic flows. + /// Defaults to api://AzureADTokenExchange/.default. Override for airgapped or sovereign clouds. + /// + /// The FIC audience URL. + /// The builder to chain the .With methods. + public ConfidentialClientApplicationBuilder WithFederatedCredentialAudience(string audience) + { + if (string.IsNullOrWhiteSpace(audience)) + { + throw new ArgumentNullException(nameof(audience)); + } + + Config.FederatedCredentialAudience = audience; + return this; + } + /// internal override void Validate() { diff --git a/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs b/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs index cea2e10ccb..85d54dc523 100644 --- a/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs +++ b/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Threading; @@ -9,6 +10,7 @@ using Microsoft.Identity.Client.ApiConfig.Executors; using Microsoft.Identity.Client.ApiConfig.Parameters; using Microsoft.Identity.Client.Core; +using Microsoft.Identity.Client.Extensibility; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.Internal.Requests; using Microsoft.Identity.Client.TelemetryCore.Internal.Events; @@ -204,6 +206,161 @@ AcquireTokenByRefreshTokenParameterBuilder IByRefreshToken.AcquireTokenByRefresh // Stores all app tokens internal ITokenCacheInternal AppTokenCacheInternal { get; } + // Cache of agent CCAs keyed by agent identity, used by AcquireTokenForAgent + private readonly ConcurrentDictionary _agentCcaCache = + new ConcurrentDictionary(); + + /// + public AcquireTokenForAgentParameterBuilder AcquireTokenForAgent( + string agentId, IEnumerable scopes) + { + if (string.IsNullOrEmpty(agentId)) + throw new ArgumentNullException(nameof(agentId)); + if (scopes == null) + throw new ArgumentNullException(nameof(scopes)); + + return new AcquireTokenForAgentParameterBuilder(this, agentId, scopes); + } + + /// + public AcquireTokenForAgentOnBehalfOfUserParameterBuilder AcquireTokenForAgentOnBehalfOfUser( + string agentId, IEnumerable scopes, string userPrincipalName) + { + if (string.IsNullOrEmpty(agentId)) + throw new ArgumentNullException(nameof(agentId)); + if (scopes == null) + throw new ArgumentNullException(nameof(scopes)); + if (string.IsNullOrEmpty(userPrincipalName)) + throw new ArgumentNullException(nameof(userPrincipalName)); + + return new AcquireTokenForAgentOnBehalfOfUserParameterBuilder(this, agentId, scopes, userPrincipalName); + } + + /// + /// Executes the two-step agentic token acquisition: + /// 1. Gets FIC from the token exchange endpoint using this CCA's credential. + /// 2. Uses the FIC as a client assertion in an agent CCA to acquire the target token. + /// + internal async Task ExecuteAgentTokenAcquisitionAsync( + string agentId, + IEnumerable scopes, + bool forceRefresh, + Guid? correlationId, + CancellationToken cancellationToken) + { + var agentCca = GetOrCreateAgentCca(agentId); + + var builder = agentCca.AcquireTokenForClient(scopes); + + if (forceRefresh) + builder = builder.WithForceRefresh(true); + + if (correlationId.HasValue) + builder = builder.WithCorrelationId(correlationId.Value); + + return await builder + .ExecuteAsync(cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Executes the three-step agentic user-delegated token acquisition: + /// 1. Gets FIC from the token exchange endpoint using this CCA's credential (via the agent CCA's assertion). + /// 2. Gets a User FIC via AcquireTokenForClient + WithFmiPathForClientAssertion on the agent CCA. + /// 3. Uses the User FIC in a user_fic grant type request to get a user-delegated token. + /// + internal async Task ExecuteAgentOnBehalfOfUserAsync( + string agentId, + IEnumerable scopes, + string userPrincipalName, + bool forceRefresh, + Guid? correlationId, + CancellationToken cancellationToken) + { + var agentCca = GetOrCreateAgentCca(agentId); + var ficAudience = ServiceBundle.Config.FederatedCredentialAudience; + + // Step 1 + 2: Get User FIC. + // The agent CCA's assertion callback already handles step 1 (getting the app FIC + // from the platform CCA). WithFmiPathForClientAssertion passes the agentId to + // that callback. The result is a User FIC token. + var userFicResult = await agentCca.AcquireTokenForClient(new[] { ficAudience }) + .WithFmiPathForClientAssertion(agentId) + .ExecuteAsync(cancellationToken) + .ConfigureAwait(false); + + string userFicAssertion = userFicResult.AccessToken; + + // Step 3: Exchange the User FIC for a user-delegated token using user_fic grant. + var usernamePasswordBuilder = ((IByUsernameAndPassword)agentCca) + .AcquireTokenByUsernamePassword(scopes, userPrincipalName, "no_password"); + + if (correlationId.HasValue) + usernamePasswordBuilder = usernamePasswordBuilder.WithCorrelationId(correlationId.Value); + + return await usernamePasswordBuilder + .OnBeforeTokenRequest(async (request) => + { + request.BodyParameters["user_federated_identity_credential"] = userFicAssertion; + request.BodyParameters["grant_type"] = "user_fic"; + + // Remove the dummy password — not needed for user_fic grant + request.BodyParameters.Remove("password"); + + // Remove client_secret if it's the default placeholder + if (request.BodyParameters.TryGetValue("client_secret", out var secret) + && secret.Equals("default", StringComparison.OrdinalIgnoreCase)) + { + request.BodyParameters.Remove("client_secret"); + } + + await Task.CompletedTask.ConfigureAwait(false); + }) + .ExecuteAsync(cancellationToken) + .ConfigureAwait(false); + } + + private IConfidentialClientApplication GetOrCreateAgentCca(string agentId) + { + return _agentCcaCache.GetOrAdd(agentId, id => + { + var config = ServiceBundle.Config; + var authorityUri = config.Authority.AuthorityInfo.CanonicalAuthority.ToString(); + var ficAudience = config.FederatedCredentialAudience; + + // The agent CCA uses a client assertion that fetches the FIC on demand + // from *this* (platform) CCA. + var platformCca = this as IConfidentialClientApplication; + + var agentBuilder = ConfidentialClientApplicationBuilder + .Create(id) + .WithAuthority(authorityUri) + .WithExperimentalFeatures(true) + .WithClientAssertion(async (AssertionRequestOptions options) => + { + string fmiPath = options.ClientAssertionFmiPath ?? id; + var result = await platformCca.AcquireTokenForClient(new[] { ficAudience }) + .WithFmiPath(fmiPath) + .ExecuteAsync() + .ConfigureAwait(false); + return result.AccessToken; + }); + + if (config.AccessorOptions != null) + agentBuilder = agentBuilder.WithCacheOptions(config.AccessorOptions); + if (config.HttpClientFactory != null) + agentBuilder = agentBuilder.WithHttpClientFactory(config.HttpClientFactory); + if (config.IdentityLogger != null) + agentBuilder = agentBuilder.WithLogging(config.IdentityLogger, config.EnablePiiLogging); + if (config.HttpManager != null) + agentBuilder = agentBuilder.WithHttpManager(config.HttpManager); + if (!config.IsInstanceDiscoveryEnabled) + agentBuilder = agentBuilder.WithInstanceDiscovery(false); + + return agentBuilder.Build(); + }); + } + internal override async Task CreateRequestParametersAsync( AcquireTokenCommonParameters commonParameters, RequestContext requestContext, diff --git a/src/client/Microsoft.Identity.Client/IConfidentialClientApplication.cs b/src/client/Microsoft.Identity.Client/IConfidentialClientApplication.cs index 90f6e7791d..46c4c752f1 100644 --- a/src/client/Microsoft.Identity.Client/IConfidentialClientApplication.cs +++ b/src/client/Microsoft.Identity.Client/IConfidentialClientApplication.cs @@ -86,6 +86,38 @@ AcquireTokenByAuthorizationCodeParameterBuilder AcquireTokenByAuthorizationCode( /// URL of the authorization endpoint with the specified parameters. GetAuthorizationRequestUrlParameterBuilder GetAuthorizationRequestUrl(IEnumerable scopes); + /// + /// Acquires an app-only token for an agent identity using Federated Managed Identity (FMI). + /// Internally, this method: + /// + /// Obtains an FMI credential (FIC) from the token exchange endpoint using the current CCA's credential. + /// Uses the FIC as a client assertion to acquire a token for the requested scopes on behalf of the agent. + /// + /// + /// The FMI path or client ID of the agent identity. + /// Scopes requested to access a protected API, e.g., https://graph.microsoft.com/.default. + /// A builder enabling you to add optional parameters before executing the token request. + AcquireTokenForAgentParameterBuilder AcquireTokenForAgent(string agentId, IEnumerable scopes); + + /// + /// Acquires a user-delegated token for an agent identity, acting on behalf of the specified user, + /// using Federated Managed Identity (FMI) and the user_fic grant type. + /// Internally, this method: + /// + /// Obtains an FMI credential (FIC) from the token exchange endpoint using the current CCA's credential. + /// Obtains a User FIC for the agent identity. + /// Exchanges the User FIC for a user-delegated token via the user_fic grant type. + /// + /// After a successful acquisition, use + /// with the returned account for subsequent cached token lookups. + /// + /// The FMI path or client ID of the agent identity. + /// Scopes requested to access a protected API, e.g., https://graph.microsoft.com/.default. + /// The UPN of the user on whose behalf the agent is acting, e.g., user@contoso.com. + /// A builder enabling you to add optional parameters before executing the token request. + AcquireTokenForAgentOnBehalfOfUserParameterBuilder AcquireTokenForAgentOnBehalfOfUser( + string agentId, IEnumerable scopes, string userPrincipalName); + /// /// In confidential client apps use instead. /// diff --git a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj index 7c08fe3762..9705dd06cc 100644 --- a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj +++ b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj @@ -80,6 +80,7 @@ + @@ -162,4 +163,8 @@ + + + + \ No newline at end of file diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt index 5f1ab1006d..a2e058bff3 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -1 +1,14 @@ -Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken \ No newline at end of file +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder.ExecuteAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder.WithCorrelationId(System.Guid correlationId) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder.ExecuteAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder.WithCorrelationId(System.Guid correlationId) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken +Microsoft.Identity.Client.ConfidentialClientApplication.AcquireTokenForAgent(string agentId, System.Collections.Generic.IEnumerable scopes) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.ConfidentialClientApplication.AcquireTokenForAgentOnBehalfOfUser(string agentId, System.Collections.Generic.IEnumerable scopes, string userPrincipalName) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithFederatedCredentialAudience(string audience) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder +Microsoft.Identity.Client.IConfidentialClientApplication.AcquireTokenForAgent(string agentId, System.Collections.Generic.IEnumerable scopes) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.IConfidentialClientApplication.AcquireTokenForAgentOnBehalfOfUser(string agentId, System.Collections.Generic.IEnumerable scopes, string userPrincipalName) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt index 49f0d092b6..a2e058bff3 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -1 +1,14 @@ +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder.ExecuteAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder.WithCorrelationId(System.Guid correlationId) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder.ExecuteAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder.WithCorrelationId(System.Guid correlationId) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken +Microsoft.Identity.Client.ConfidentialClientApplication.AcquireTokenForAgent(string agentId, System.Collections.Generic.IEnumerable scopes) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.ConfidentialClientApplication.AcquireTokenForAgentOnBehalfOfUser(string agentId, System.Collections.Generic.IEnumerable scopes, string userPrincipalName) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithFederatedCredentialAudience(string audience) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder +Microsoft.Identity.Client.IConfidentialClientApplication.AcquireTokenForAgent(string agentId, System.Collections.Generic.IEnumerable scopes) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.IConfidentialClientApplication.AcquireTokenForAgentOnBehalfOfUser(string agentId, System.Collections.Generic.IEnumerable scopes, string userPrincipalName) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt index 49f0d092b6..a2e058bff3 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt @@ -1 +1,14 @@ +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder.ExecuteAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder.WithCorrelationId(System.Guid correlationId) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder.ExecuteAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder.WithCorrelationId(System.Guid correlationId) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken +Microsoft.Identity.Client.ConfidentialClientApplication.AcquireTokenForAgent(string agentId, System.Collections.Generic.IEnumerable scopes) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.ConfidentialClientApplication.AcquireTokenForAgentOnBehalfOfUser(string agentId, System.Collections.Generic.IEnumerable scopes, string userPrincipalName) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithFederatedCredentialAudience(string audience) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder +Microsoft.Identity.Client.IConfidentialClientApplication.AcquireTokenForAgent(string agentId, System.Collections.Generic.IEnumerable scopes) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.IConfidentialClientApplication.AcquireTokenForAgentOnBehalfOfUser(string agentId, System.Collections.Generic.IEnumerable scopes, string userPrincipalName) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt index 49f0d092b6..a2e058bff3 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt @@ -1 +1,14 @@ +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder.ExecuteAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder.WithCorrelationId(System.Guid correlationId) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder.ExecuteAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder.WithCorrelationId(System.Guid correlationId) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken +Microsoft.Identity.Client.ConfidentialClientApplication.AcquireTokenForAgent(string agentId, System.Collections.Generic.IEnumerable scopes) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.ConfidentialClientApplication.AcquireTokenForAgentOnBehalfOfUser(string agentId, System.Collections.Generic.IEnumerable scopes, string userPrincipalName) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithFederatedCredentialAudience(string audience) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder +Microsoft.Identity.Client.IConfidentialClientApplication.AcquireTokenForAgent(string agentId, System.Collections.Generic.IEnumerable scopes) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.IConfidentialClientApplication.AcquireTokenForAgentOnBehalfOfUser(string agentId, System.Collections.Generic.IEnumerable scopes, string userPrincipalName) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt index 49f0d092b6..a2e058bff3 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt @@ -1 +1,14 @@ +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder.ExecuteAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder.WithCorrelationId(System.Guid correlationId) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder.ExecuteAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder.WithCorrelationId(System.Guid correlationId) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken +Microsoft.Identity.Client.ConfidentialClientApplication.AcquireTokenForAgent(string agentId, System.Collections.Generic.IEnumerable scopes) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.ConfidentialClientApplication.AcquireTokenForAgentOnBehalfOfUser(string agentId, System.Collections.Generic.IEnumerable scopes, string userPrincipalName) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithFederatedCredentialAudience(string audience) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder +Microsoft.Identity.Client.IConfidentialClientApplication.AcquireTokenForAgent(string agentId, System.Collections.Generic.IEnumerable scopes) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.IConfidentialClientApplication.AcquireTokenForAgentOnBehalfOfUser(string agentId, System.Collections.Generic.IEnumerable scopes, string userPrincipalName) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt index 49f0d092b6..a2e058bff3 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -1 +1,14 @@ +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder.ExecuteAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder.WithCorrelationId(System.Guid correlationId) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder.ExecuteAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder.WithCorrelationId(System.Guid correlationId) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken +Microsoft.Identity.Client.ConfidentialClientApplication.AcquireTokenForAgent(string agentId, System.Collections.Generic.IEnumerable scopes) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.ConfidentialClientApplication.AcquireTokenForAgentOnBehalfOfUser(string agentId, System.Collections.Generic.IEnumerable scopes, string userPrincipalName) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder +Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithFederatedCredentialAudience(string audience) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder +Microsoft.Identity.Client.IConfidentialClientApplication.AcquireTokenForAgent(string agentId, System.Collections.Generic.IEnumerable scopes) -> Microsoft.Identity.Client.AcquireTokenForAgentParameterBuilder +Microsoft.Identity.Client.IConfidentialClientApplication.AcquireTokenForAgentOnBehalfOfUser(string agentId, System.Collections.Generic.IEnumerable scopes, string userPrincipalName) -> Microsoft.Identity.Client.AcquireTokenForAgentOnBehalfOfUserParameterBuilder diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/AgenticCcaE2ETest.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/AgenticCcaE2ETest.cs new file mode 100644 index 0000000000..444a9c1e37 --- /dev/null +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/AgenticCcaE2ETest.cs @@ -0,0 +1,324 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Extensibility; +using Microsoft.Identity.Test.LabInfrastructure; +using Microsoft.Identity.Test.Unit; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Identity.Test.Integration.HeadlessTests +{ + /// + /// E2E integration tests that validate the new CCA-based agentic API. + /// Compare with (the original baseline) to see the difference: + /// + /// OLD (Agentic.cs baseline): + /// - Manually creates a platform CCA + assertion callback to get FIC + /// - Creates a second CCA with the agent identity + FIC as client assertion + /// - Calls AcquireTokenForClient on the agent CCA + /// + /// NEW (this file): + /// - Creates a single platform CCA with certificate + /// - Calls cca.AcquireTokenForAgent(agentId, scopes) � all FIC orchestration is internal + /// + [TestClass] + public class AgenticCcaE2ETest + { + // Same constants as Agentic.cs so the flows are directly comparable + private const string PlatformClientId = "aab5089d-e764-47e3-9f28-cc11c2513821"; // platform (host) app + private const string TenantId = "10c419d4-4a50-45b2-aa4e-919fb84df24f"; + private const string AgentIdentity = "ab18ca07-d139-4840-8b3b-4be9610c6ed5"; + private const string UserUpn = "agentuser1@id4slab1.onmicrosoft.com"; + private const string TokenExchangeUrl = "api://AzureADTokenExchange/.default"; + private const string Scope = "https://graph.microsoft.com/.default"; + + #region Case 1a � Certificate credential via AcquireTokenForAgent + + /// + /// Mirrors but uses the new + /// API. + /// + /// Instead of manually wiring a platform CCA ? FIC ? agent CCA ? token, + /// the caller just does: + /// cca.AcquireTokenForAgent(agentId, scopes).ExecuteAsync() + /// + [TestMethod] + public async Task AgentGetsAppTokenWithCertificate_ViaAcquireTokenForAgentTest() + { + X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName); + + // Single platform CCA � no need for a separate agent CCA + var cca = ConfidentialClientApplicationBuilder + .Create(PlatformClientId) + .WithAuthority("https://login.microsoftonline.com/", TenantId) + .WithCertificate(cert, sendX5C: true) + .WithCacheOptions(CacheOptions.EnableSharedCacheOptions) + .WithExperimentalFeatures(true) + .Build(); + + // One call does the full two-step flow internally: + // 1. AcquireTokenForClient(api://AzureADTokenExchange/.default).WithFmiPath(agentId) + // 2. Creates an internal agent CCA that uses the FIC as client assertion + // 3. AcquireTokenForClient(scopes) on the agent CCA + var result = await cca + .AcquireTokenForAgent(AgentIdentity, [Scope]) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(result, "AuthenticationResult should not be null"); + Assert.IsNotNull(result.AccessToken, "AccessToken should not be null"); + Assert.IsTrue(result.ExpiresOn > DateTimeOffset.UtcNow, "Token should not be expired"); + + Trace.WriteLine($"[CCA Case 1a] App token acquired from: {result.AuthenticationResultMetadata.TokenSource}"); + } + + #endregion + + #region Case 1b � Client assertion credential via AcquireTokenForAgent + + /// + /// Same as Case 1a but the platform CCA authenticates with a + /// WithClientAssertion callback instead of WithCertificate. + /// + /// The callback uses to get + /// ClientID and TokenEndpoint at runtime � no need to + /// hardcode the audience or client ID in the closure. + /// + /// AcquireTokenForAgent works identically regardless of how the + /// platform CCA authenticates. + /// + [TestMethod] + public async Task AgentGetsAppTokenWithAssertion_ViaAcquireTokenForAgentTest() + { + X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName); + + // Platform CCA configured with WithClientAssertion � no WithCertificate here. + // The callback only captures the cert; everything else comes from + // AssertionRequestOptions at runtime. + var cca = ConfidentialClientApplicationBuilder + .Create(PlatformClientId) + .WithAuthority($"https://login.microsoftonline.com/{TenantId}") + .WithExperimentalFeatures(true) + .WithCacheOptions(CacheOptions.EnableSharedCacheOptions) + .WithClientAssertion((AssertionRequestOptions opts) => + { + // opts.ClientID = the platform app's client ID (issuer/subject) + // opts.TokenEndpoint = the exact token endpoint URL (audience) + // No hardcoded strings needed � MSAL tells us everything. + string jwt = CreateSignedJwt(opts.ClientID, opts.TokenEndpoint, cert); + return Task.FromResult(jwt); + }) + .Build(); + + // Same single call as Case 1a � proves the new API is + // credential-agnostic. + var result = await cca + .AcquireTokenForAgent(AgentIdentity, [Scope]) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(result, "AuthenticationResult should not be null"); + Assert.IsNotNull(result.AccessToken, "AccessToken should not be null"); + Assert.IsTrue(result.ExpiresOn > DateTimeOffset.UtcNow, "Token should not be expired"); + + Trace.WriteLine($"[CCA Case 1b] App token acquired from: {result.AuthenticationResultMetadata.TokenSource}"); + } + + #endregion + + #region E2E � Agent acts on behalf of a user (user_fic grant, same as baseline) + + /// + /// Mirrors exactly. + /// The user-delegated flow still uses the CCA + OnBeforeTokenRequest pattern. + /// + [TestMethod] + public async Task AgentUserIdentityGetsTokenForGraph_ViaOnBehalfOfUserTest() + { + var cca = ConfidentialClientApplicationBuilder + .Create(AgentIdentity) + .WithAuthority("https://login.microsoftonline.com/", TenantId) + .WithCacheOptions(CacheOptions.EnableSharedCacheOptions) + .WithExperimentalFeatures(true) + .WithExtraQueryParameters(new Dictionary { { "slice", ("first", false) } }) + .WithClientAssertion((AssertionRequestOptions _) => GetAppCredentialAsync(AgentIdentity)) + .Build(); + + var result = await (cca as IByUsernameAndPassword).AcquireTokenByUsernamePassword([Scope], UserUpn, "no_password") + .OnBeforeTokenRequest( + async (request) => + { + string userFicAssertion = await GetUserFic().ConfigureAwait(false); + request.BodyParameters["user_federated_identity_credential"] = userFicAssertion; + request.BodyParameters["grant_type"] = "user_fic"; + + // remove the password + request.BodyParameters.Remove("password"); + + if (request.BodyParameters.TryGetValue("client_secret", out var secret) + && secret.Equals("default", StringComparison.OrdinalIgnoreCase)) + { + request.BodyParameters.Remove("client_secret"); + } + } + ) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(result, "AuthenticationResult should not be null"); + Assert.IsNotNull(result.AccessToken, "AccessToken should not be null"); + Assert.IsNotNull(result.Account, "Account should not be null after user flow"); + + Trace.WriteLine($"[CCA E2E user] User token acquired from: {result.AuthenticationResultMetadata.TokenSource}"); + + // Validate silent (cached) acquisition + IAccount account = await cca.GetAccountAsync(result.Account.HomeAccountId.Identifier).ConfigureAwait(false); + Assert.IsNotNull(account, "Account retrieved from cache should not be null"); + + var result2 = await cca.AcquireTokenSilent([Scope], account).ExecuteAsync().ConfigureAwait(false); + Assert.IsTrue(result2.AuthenticationResultMetadata.TokenSource == TokenSource.Cache, "Token should be from cache"); + + Trace.WriteLine($"[CCA E2E user] Silent token acquired from: {result2.AuthenticationResultMetadata.TokenSource}"); + } + + #endregion + + #region E2E � Agent on behalf of user via AcquireTokenForAgentOnBehalfOfUser (NEW simplified API) + + /// + /// Mirrors but uses the new + /// API. + /// + /// COMPARE � OLD (Agentic.cs baseline, ~30 lines of ceremony): + /// 1. Create CCA with agent identity + assertion callback + /// 2. Cast to IByUsernameAndPassword + /// 3. Call AcquireTokenByUsernamePassword with dummy password + /// 4. OnBeforeTokenRequest ? manually get User FIC, rewrite grant_type, strip password, etc. + /// + /// NEW (this test, 1 call): + /// cca.AcquireTokenForAgentOnBehalfOfUser(agentId, scopes, upn).ExecuteAsync() + /// + /// All the User FIC acquisition, grant_type rewriting, and password stripping + /// is handled internally by the CCA. + /// + [TestMethod] + public async Task AgentUserIdentityGetsToken_ViaAcquireTokenForAgentOnBehalfOfUserTest() + { + X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName); + + // Single platform CCA with certificate � same as Case 1a + var cca = ConfidentialClientApplicationBuilder + .Create(PlatformClientId) + .WithAuthority("https://login.microsoftonline.com/", TenantId) + .WithCertificate(cert, sendX5C: true) + .WithCacheOptions(CacheOptions.EnableSharedCacheOptions) + .WithExperimentalFeatures(true) + .Build(); + + // One call hides the entire user_fic complexity: + // 1. Gets FIC from platform cert ? agent CCA (cached) + // 2. Gets User FIC via agent CCA + WithFmiPathForClientAssertion + // 3. Rewrites the token request to user_fic grant with the User FIC + var result = await cca + .AcquireTokenForAgentOnBehalfOfUser(AgentIdentity, [Scope], UserUpn) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(result, "AuthenticationResult should not be null"); + Assert.IsNotNull(result.AccessToken, "AccessToken should not be null"); + Assert.IsNotNull(result.Account, "Account should not be null after user flow"); + + Trace.WriteLine($"[CCA new user API] User token acquired from: {result.AuthenticationResultMetadata.TokenSource}"); + + // Note: Silent token acquisition via cca.AcquireTokenSilent is not available + // here because the user-delegated token lives in the internal agent CCA's cache, + // not the platform CCA's cache. Calling AcquireTokenForAgentOnBehalfOfUser again + // will benefit from caching at the internal agent CCA level. + } + + #endregion + + #region Helpers (same as Agentic.cs baseline) + + private static async Task GetAppCredentialAsync(string fmiPath) + { + Assert.IsNotNull(fmiPath, "fmiPath cannot be null"); + X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName); + + var cca1 = ConfidentialClientApplicationBuilder + .Create(PlatformClientId) + .WithAuthority("https://login.microsoftonline.com/", TenantId) + .WithCacheOptions(CacheOptions.EnableSharedCacheOptions) + .WithExperimentalFeatures(true) + .WithCertificate(cert, sendX5C: true) + .Build(); + + var result = await cca1.AcquireTokenForClient([TokenExchangeUrl]) + .WithFmiPath(fmiPath) + .ExecuteAsync() + .ConfigureAwait(false); + + Trace.WriteLine($"FMI app credential from: {result.AuthenticationResultMetadata.TokenSource}"); + + return result.AccessToken; + } + + private static async Task GetUserFic() + { + var cca1 = ConfidentialClientApplicationBuilder + .Create(AgentIdentity) + .WithAuthority("https://login.microsoftonline.com/", TenantId) + .WithExperimentalFeatures(true) + .WithCacheOptions(CacheOptions.EnableSharedCacheOptions) + .WithClientAssertion(async (AssertionRequestOptions a) => + { + Assert.AreEqual(AgentIdentity, a.ClientAssertionFmiPath); + var cred = await GetAppCredentialAsync(a.ClientAssertionFmiPath).ConfigureAwait(false); + return cred; + }) + .Build(); + + var result = await cca1.AcquireTokenForClient([TokenExchangeUrl]) + .WithFmiPathForClientAssertion(AgentIdentity) + .ExecuteAsync().ConfigureAwait(false); + + Trace.WriteLine($"User FIC credential from: {result.AuthenticationResultMetadata.TokenSource}"); + + return result.AccessToken; + } + + /// + /// Builds a signed client assertion JWT using the Wilson library. + /// This is the same JWT that WithCertificate would create internally, + /// but done manually so we can pass it via WithClientAssertion. + /// + private static string CreateSignedJwt(string clientId, string audience, X509Certificate2 cert) + { + var claims = new Dictionary + { + { "aud", audience }, + { "iss", clientId }, + { "jti", Guid.NewGuid().ToString() }, + { "sub", clientId } + }; + + var tokenDescriptor = new SecurityTokenDescriptor + { + Claims = claims, + SigningCredentials = new X509SigningCredentials(cert) + }; + + return new JsonWebTokenHandler().CreateToken(tokenDescriptor); + } + + #endregion + } +}