Skip to content

Add generic webview session manager for enrollment and registration flows#1672

Draft
Copilot wants to merge 20 commits intodevfrom
copilot/update-intune-enrollment-flow
Draft

Add generic webview session manager for enrollment and registration flows#1672
Copilot wants to merge 20 commits intodevfrom
copilot/update-intune-enrollment-flow

Conversation

Copy link
Contributor

Copilot AI commented Jan 24, 2026

Overview

Implements extensible webview session management for enrollment flows (Intune, MDM, device registration). Enables code sharing between MSIDLocalInteractiveController (IdentityCore) and MSIDBrokerInteractiveController (broker repo) via composition.

Changes

Core Implementation

  • MSIDWebviewSessionManager - Standalone manager class with all session logic
  • MSIDWebviewSessionControlling - Protocol for controller integration
  • Direct usage pattern (no category wrapper) - same code in local and broker contexts

Helper Types

  • MSIDWebviewAction - Action object returned from async URL callbacks
  • MSIDWebviewResponseEvent - Structured response events from webview navigation
  • MSIDBRTAttemptTracker - Tracks BRT acquisition attempts (max 2 per session)
  • MSIDResponseHeaderStore - Session-level header storage with case-insensitive lookup

Webview Extensions

  • MSIDAADOAuthEmbeddedWebviewController - HTTP 302 response header capture, async action callbacks for msauth:// and browser:// URLs
  • MSIDASWebAuthenticationSessionHandler - Additional header support (iOS 17.4+)
  • MSIDSystemWebViewControllerFactory - Header injection plumbing

Configuration

  • capturedHeaderKeys - Configurable header capture (defaults: x-ms-clitelem, x-install-url, authorization)
  • customURLActionHandler - Pluggable block for custom URL handling
  • Built-in handlers for common patterns: enroll, installProfile, profileInstalled

Usage

// In any controller (local or broker)
@interface YourController : MSIDBaseRequestController <MSIDWebviewSessionControlling>
@property (nonatomic, strong) MSIDWebviewSessionManager *webviewSessionManager;
@end

// Init
_webviewSessionManager = [[MSIDWebviewSessionManager alloc] initWithController:self];

// Configure webview
[self.webviewSessionManager configureWebview:webviewController];

// Optional: Custom headers
self.webviewSessionManager.capturedHeaderKeys = [NSSet setWithArray:@[@"x-custom-token"]];

// Optional: Custom URL handler
self.webviewSessionManager.customURLActionHandler = ^(NSURL *url, void(^completion)(MSIDWebviewAction *)) {
    // Custom logic with fallback to built-in handlers
};

// Access captured state
NSString *token = [self.webviewSessionManager.responseHeaderStore headerForKey:@"x-custom-token"];

Architecture

MSIDWebviewSessionManager (455 lines)
├── BRT attempt tracking (max 2 per session)
├── Header capture from HTTP 302 responses
├── Custom URL action routing
└── Webview callback wiring

Used directly by:
├── MSIDLocalInteractiveController
└── MSIDBrokerInteractiveController (broker repo)

Testing

  • Unit tests for all helper types
  • Manager independently testable
  • Integration pattern validated

Documentation

  • docs/intune-enrollment-webview-flow.md - Architecture and sequence diagrams
  • MANAGER_USAGE_GUIDE.md - Integration guide
  • FINAL_ARCHITECTURE.md - Design overview
Original prompt

Implement updates to support Intune enrollment flow requirements in embedded/system webviews:

Requirements:

  1. Best-effort BRT acquisition attempts (non-broker context only):

    • Attempt BRT acquisition on the first redirect with scheme msauth:// or browser:// in a single token acquisition request session.
    • If that first attempt fails and another msauth:// or browser:// redirect happens in the same session, attempt BRT acquisition once more.
    • Total maximum 2 attempts per token acquisition session.
    • BRT acquisition failure must NOT block the flow; proceed regardless while recording telemetry/state.
    • BRT logic must be handled by InteractiveController (not webview controller).
  2. Response header capture:

    • Capture selected headers across ALL WKWebView navigation responses throughout the flow: x-ms-clitelem, X-Intune-AuthToken, X-Install-Url.
    • Headers are present on HTTP 302 responses; store them in a session-level header store so they can be used later when custom-scheme redirects occur.
    • Webview controller should forward structured response events to InteractiveController; InteractiveController updates telemetry and stores headers.
  3. Special URL semantics:

    • Intercept msauth:// and browser:// navigation actions in embedded WKWebView and delegate handling to InteractiveController via async callback that returns an action object.
    • Implement handlers for msauth://enroll?cpurl=..., msauth://installProfile (uses stored 302 headers for install URL + token), and msauth://profileInstalled (from ASWebAuthenticationSession).
    • For installProfile: open ASWebAuthenticationSession using URL from X-Install-Url and attach X-Intune-AuthToken as an additional header field.
    • For profileInstalled: if broker context is available, continue broker flow; otherwise retry token request in broker context (per existing architecture).
  4. System webview headers:

    • Extend MSIDASWebAuthenticationSessionHandler and MSIDSystemWebViewControllerFactory plumbing to accept optional additional header fields and apply them to ASWebAuthenticationSession when supported by OS.
  5. Add a comprehensive design document with:

    • High-level architecture diagrams (ASCII/mermaid)
    • Sequence/flow diagrams for token request, enrollment, install profile handoff, and profileInstalled continuation.
    • Wiring information: which callbacks, which classes own which state, session lifetime.
    • Notes on header capture across 302 responses and correlation strategy.
    • BRT attempt state machine.

Repository: AzureAD/microsoft-authentication-library-common-for-objc
Base branch: dev
Key files (existing):

  • IdentityCore/src/webview/embeddedWebview/MSIDAADOAuthEmbeddedWebviewController.m
  • IdentityCore/src/webview/systemWebview/session/MSIDASWebAuthenticationSessionHandler.h/.m
  • IdentityCore/src/webview/systemWebview/session/MSIDSystemWebViewControllerFactory.m

Implement new helper types as needed:

  • MSIDWebviewAction (+ additionalHeaders)
  • MSIDWebviewResponseEvent
  • MSIDBRTAttemptTracker
  • MSIDResponseHeaderStore

Also update InteractiveControllers (MSIDLocalInteractiveController and broker interactive controller as applicable) to wire callbacks and implement logic.

Include a Markdown design doc under a suitable docs/ path (e.g., docs/intune-enrollment-webview-flow.md).

This pull request was created from Copilot chat.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Co-authored-by: Veena11 <9446116+Veena11@users.noreply.github.com>
@@ -0,0 +1,61 @@
//------------------------------------------------------------------------------

Choose a reason for hiding this comment

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

This pull request does not update changelog.txt.

Please consider if this change would be noticeable to a partner or user and either update changelog.txt or resolve this conversation.

Copilot AI and others added 6 commits January 24, 2026 02:49
Co-authored-by: Veena11 <9446116+Veena11@users.noreply.github.com>
Co-authored-by: Veena11 <9446116+Veena11@users.noreply.github.com>
…ent callbacks

Co-authored-by: Veena11 <9446116+Veena11@users.noreply.github.com>
Co-authored-by: Veena11 <9446116+Veena11@users.noreply.github.com>
Co-authored-by: Veena11 <9446116+Veena11@users.noreply.github.com>
…Session header injection

Co-authored-by: Veena11 <9446116+Veena11@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement updates for Intune enrollment flow requirements Add Intune enrollment flow support with header capture and async action callbacks Jan 24, 2026
Copilot AI requested a review from Veena11 January 24, 2026 03:01
Copilot AI and others added 4 commits January 24, 2026 03:05
Co-authored-by: Veena11 <9446116+Veena11@users.noreply.github.com>
Co-authored-by: Veena11 <9446116+Veena11@users.noreply.github.com>
Co-authored-by: Veena11 <9446116+Veena11@users.noreply.github.com>
Co-authored-by: Veena11 <9446116+Veena11@users.noreply.github.com>
Copilot AI changed the title Add Intune enrollment flow support with header capture and async action callbacks Refactor webview extensions from vendor-specific to generic framework Jan 24, 2026
Copilot AI and others added 3 commits January 24, 2026 03:18
…lers

Co-authored-by: Veena11 <9446116+Veena11@users.noreply.github.com>
Co-authored-by: Veena11 <9446116+Veena11@users.noreply.github.com>
Co-authored-by: Veena11 <9446116+Veena11@users.noreply.github.com>
Copilot AI changed the title Refactor webview extensions from vendor-specific to generic framework Refactor webview extensions to manager-based composition for cross-controller code sharing Jan 24, 2026
Co-authored-by: Veena11 <9446116+Veena11@users.noreply.github.com>
Co-authored-by: Veena11 <9446116+Veena11@users.noreply.github.com>
Copilot AI changed the title Refactor webview extensions to manager-based composition for cross-controller code sharing Refactor webview session management to enable cross-repository code sharing Jan 24, 2026
Copilot AI and others added 3 commits January 24, 2026 03:45
Co-authored-by: Veena11 <9446116+Veena11@users.noreply.github.com>
Co-authored-by: Veena11 <9446116+Veena11@users.noreply.github.com>
Co-authored-by: Veena11 <9446116+Veena11@users.noreply.github.com>
Copilot AI changed the title Refactor webview session management to enable cross-repository code sharing Add generic webview session manager for enrollment and registration flows Jan 24, 2026
@agubuzomaximus agubuzomaximus marked this pull request as ready for review January 27, 2026 20:04
@agubuzomaximus agubuzomaximus requested a review from a team as a code owner January 27, 2026 20:04
@agubuzomaximus agubuzomaximus marked this pull request as draft January 27, 2026 20:05
Copy link
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

This PR implements a generic webview session manager to support device enrollment and registration flows (such as Intune MDM enrollment). The implementation introduces MSIDWebviewSessionManager, a reusable component that can be shared between local and broker contexts via composition.

Changes:

  • Core manager class (MSIDWebviewSessionManager) with header capture, BRT attempt tracking, and custom URL action handling
  • Helper types for actions, response events, BRT tracking, and header storage
  • Extensions to embedded and system webview controllers to support async action callbacks and header injection
  • Comprehensive documentation and unit tests for helper types

Reviewed changes

Copilot reviewed 27 out of 27 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
IdentityCore/src/webview/MSIDWebviewSessionManager.h/m Core manager class with session logic, header capture, and URL routing
IdentityCore/src/webview/MSIDWebviewAction.h/m Action object returned from async URL callbacks
IdentityCore/src/webview/MSIDWebviewResponseEvent.h/m Structured response events from webview navigation
IdentityCore/src/webview/MSIDBRTAttemptTracker.h/m Tracks BRT acquisition attempts (max 2 per session)
IdentityCore/src/webview/MSIDResponseHeaderStore.h/m Session-level header storage
IdentityCore/src/webview/embeddedWebview/MSIDAADOAuthEmbeddedWebviewController.m Async action callback support for msauth:// and browser:// URLs
IdentityCore/src/webview/embeddedWebview/MSIDOAuth2EmbeddedWebviewController.m HTTP response header capture
IdentityCore/src/webview/systemWebview/session/MSIDASWebAuthenticationSessionHandler.h/m Header injection for iOS 17.4+, macOS 14.4+
IdentityCore/src/webview/systemWebview/session/MSIDSystemWebViewControllerFactory.m Factory method accepting additional headers
IdentityCore/tests/MSID*Tests.m Unit tests for helper types
docs/intune-enrollment-webview-flow.md Comprehensive architecture and design documentation
Various markdown files Usage guides and architecture documentation

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

Comment on lines +98 to +136
// Use new async action callback if available for Intune enrollment flow
if (self.webviewActionDecisionBlock)
{
decisionHandler(WKNavigationActionPolicyCancel);

__weak typeof(self) weakSelf = self;
self.webviewActionDecisionBlock(requestURL, ^(MSIDWebviewAction *action) {
__strong typeof(self) strongSelf = weakSelf;
if (!strongSelf) return;

switch (action.actionType)
{
case MSIDWebviewActionTypeCancel:
// Cancel the navigation
break;

case MSIDWebviewActionTypeContinue:
// Continue with default behavior (complete auth)
[strongSelf completeWebAuthWithURL:requestURL];
break;

case MSIDWebviewActionTypeLoadRequest:
if (action.request)
{
[strongSelf loadRequest:action.request];
}
break;

case MSIDWebviewActionTypeComplete:
if (action.completeURL)
{
[strongSelf completeWebAuthWithURL:action.completeURL];
}
break;
}
});

return YES;
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The action decision block is called with requestURL after decisionHandler has already been invoked with WKNavigationActionPolicyCancel (line 101). This is the correct approach for async handling, but there's a potential issue: if the action completion block is never called or called with an error, the webview navigation will be stuck in a cancelled state. Consider adding a timeout or error handling mechanism to ensure the flow can recover if the completion handler is not invoked within a reasonable timeframe.

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +119
- (void)testAllHeaders_shouldReturnAllStoredHeaders
{
MSIDResponseHeaderStore *store = [[MSIDResponseHeaderStore alloc] init];

[store setHeader:@"token123" forKey:@"X-Intune-AuthToken"];
[store setHeader:@"https://install.com" forKey:@"X-Install-Url"];
[store setHeader:@"telemetry" forKey:@"x-ms-clitelem"];

NSDictionary *headers = store.allHeaders;

XCTAssertEqual(headers.count, 3);
XCTAssertEqualObjects(headers[@"X-Intune-AuthToken"], @"token123");
XCTAssertEqualObjects(headers[@"X-Install-Url"], @"https://install.com");
XCTAssertEqualObjects(headers[@"x-ms-clitelem"], @"telemetry");
}

- (void)testClearHeaders_shouldRemoveAllHeaders
{
MSIDResponseHeaderStore *store = [[MSIDResponseHeaderStore alloc] init];

[store setHeader:@"token123" forKey:@"X-Intune-AuthToken"];
[store setHeader:@"https://install.com" forKey:@"X-Install-Url"];
XCTAssertEqual(store.allHeaders.count, 2);

[store clearHeaders];

XCTAssertEqual(store.allHeaders.count, 0);
XCTAssertNil([store headerForKey:@"X-Intune-AuthToken"]);
XCTAssertNil([store headerForKey:@"X-Install-Url"]);
}

@end
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The unit tests for MSIDResponseHeaderStore do not test case-insensitive header retrieval, which is claimed in the documentation. HTTP headers should be case-insensitive per RFC 2616, and the documentation states this behavior is supported. Add test cases that verify headers can be retrieved regardless of case, such as storing with "X-Custom-Header" and retrieving with "x-custom-header".

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +58
self.headers[key] = value;
}
}

- (NSString *)headerForKey:(NSString *)key
{
return self.headers[key];
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The documentation claims case-insensitive header lookup, but the implementation stores and retrieves headers with exact key matching. HTTP headers are case-insensitive per RFC 2616, so retrieval should also be case-insensitive. Consider normalizing keys to lowercase when storing (in setHeader) and retrieving (in headerForKey) to match the behavior described in the documentation at line 508 of docs/intune-enrollment-webview-flow.md.

Suggested change
self.headers[key] = value;
}
}
- (NSString *)headerForKey:(NSString *)key
{
return self.headers[key];
NSString *normalizedKey = [key lowercaseString];
self.headers[normalizedKey] = value;
}
}
- (NSString *)headerForKey:(NSString *)key
{
if (!key)
{
return nil;
}
NSString *normalizedKey = [key lowercaseString];
return self.headers[normalizedKey];

Copilot uses AI. Check for mistakes.
Comment on lines +278 to +301
@"Profile installation completed, determining next action");

// Check if broker context is available
// In production, this would check broker availability and context
BOOL hasBrokerContext = NO; // TODO: Implement actual broker context check

if (hasBrokerContext)
{
// Continue broker flow with profile installed indication
MSID_LOG_WITH_CTX(MSIDLogLevelInfo, self.controller.requestParameters,
@"Continuing broker flow after profile installation");
completionHandler([MSIDWebviewAction completeAction:url]);
}
else
{
// Retry entire token request in broker context
// This is handled by existing architecture - the error will trigger broker retry
MSID_LOG_WITH_CTX(MSIDLogLevelInfo, self.controller.requestParameters,
@"Retrying token request in broker context after profile installation");

// TODO: In production, this would set up state to retry in broker context
// For now, complete with the URL and let existing logic handle it
completionHandler([MSIDWebviewAction completeAction:url]);
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The hasBrokerContext check is hardcoded to NO with a TODO comment, making the conditional logic on lines 284-301 unreachable. Since hasBrokerContext is always false, the code will always take the else branch. This incomplete implementation could lead to incorrect behavior in production. Either implement the broker context check or provide a mechanism for callers to inject this logic, such as through a delegate method or configuration property.

Suggested change
@"Profile installation completed, determining next action");
// Check if broker context is available
// In production, this would check broker availability and context
BOOL hasBrokerContext = NO; // TODO: Implement actual broker context check
if (hasBrokerContext)
{
// Continue broker flow with profile installed indication
MSID_LOG_WITH_CTX(MSIDLogLevelInfo, self.controller.requestParameters,
@"Continuing broker flow after profile installation");
completionHandler([MSIDWebviewAction completeAction:url]);
}
else
{
// Retry entire token request in broker context
// This is handled by existing architecture - the error will trigger broker retry
MSID_LOG_WITH_CTX(MSIDLogLevelInfo, self.controller.requestParameters,
@"Retrying token request in broker context after profile installation");
// TODO: In production, this would set up state to retry in broker context
// For now, complete with the URL and let existing logic handle it
completionHandler([MSIDWebviewAction completeAction:url]);
}
@"Profile installation completed");
// Currently, after profile installation we simply continue with the returned URL.
// Any broker-specific retry/continuation logic should be implemented by callers
// or higher-level components that invoke this manager.
completionHandler([MSIDWebviewAction completeAction:url]);

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +304
//------------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation.
// All rights reserved.
//
// This code is licensed under the MIT License.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files(the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions :
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
//------------------------------------------------------------------------------

#import "MSIDWebviewSessionManager.h"
#import "MSIDBRTAttemptTracker.h"
#import "MSIDResponseHeaderStore.h"
#import "MSIDWebviewAction.h"
#import "MSIDWebviewResponseEvent.h"
#import "MSIDOAuth2EmbeddedWebviewController.h"
#import "NSURL+MSIDExtensions.h"
#import "MSIDLogger+Internal.h"

@implementation MSIDWebviewSessionManager

- (instancetype)initWithController:(id<MSIDWebviewSessionControlling>)controller
{
self = [super init];
if (self)
{
_controller = controller;
_brtAttemptTracker = [[MSIDBRTAttemptTracker alloc] init];
_responseHeaderStore = [[MSIDResponseHeaderStore alloc] init];
}
return self;
}

- (instancetype)init
{
return [self initWithController:nil];
}

#pragma mark - Configuration

- (void)configureWebview:(id)webviewController
{
if (![webviewController isKindOfClass:[MSIDOAuth2EmbeddedWebviewController class]])
{
// Not an embedded webview controller, skip configuration
return;
}

MSIDOAuth2EmbeddedWebviewController *controller = (MSIDOAuth2EmbeddedWebviewController *)webviewController;

// Set response event block to capture headers
__weak typeof(self) weakSelf = self;
controller.webviewResponseEventBlock = ^(MSIDWebviewResponseEvent *event) {
__strong typeof(self) strongSelf = weakSelf;
if (!strongSelf) return;

[strongSelf captureHeadersFromResponseEvent:event];
};

// Set action decision block for custom URL handling
controller.webviewActionDecisionBlock = ^(NSURL *url, void(^completionHandler)(MSIDWebviewAction *action)) {
__strong typeof(self) strongSelf = weakSelf;
if (!strongSelf)
{
completionHandler([MSIDWebviewAction continueAction]);
return;
}

// Use custom handler if provided, otherwise use default
if (strongSelf.customURLActionHandler)
{
strongSelf.customURLActionHandler(url, completionHandler);
}
else
{
[strongSelf handleCustomURLAction:url completion:completionHandler];
}
};
}

#pragma mark - Header Capture

- (void)captureHeadersFromResponseEvent:(MSIDWebviewResponseEvent *)event
{
NSDictionary *headers = event.httpHeaders;
if (!headers || headers.count == 0) return;

// Determine which headers to capture
NSSet<NSString *> *headerKeys = self.capturedHeaderKeys;

// If not configured, use default common headers for backwards compatibility
// These represent typical headers used in enrollment/registration flows
if (headerKeys == nil)
{
headerKeys = [NSSet setWithArray:@[@"x-ms-clitelem", @"x-install-url", @"authorization"]];
}

// Empty set means no capture
if (headerKeys.count == 0) return;

// Capture headers (case-insensitive matching)
for (NSString *key in headers)
{
NSString *lowerKey = [key lowercaseString];
for (NSString *captureKey in headerKeys)
{
if ([lowerKey isEqualToString:[captureKey lowercaseString]])
{
[self.responseHeaderStore setHeader:headers[key] forKey:captureKey];
break;
}
}
}
}

#pragma mark - Custom URL Action Handling

- (void)handleCustomURLAction:(NSURL *)url
completion:(void(^)(MSIDWebviewAction *action))completionHandler
{
if (!url || !completionHandler)
{
if (completionHandler) completionHandler([MSIDWebviewAction cancelAction]);
return;
}

NSString *host = [url.host lowercaseString];

// Handle common enrollment/registration patterns
if ([host isEqualToString:@"enroll"])
{
[self handleEnrollURLAction:url completion:completionHandler];
}
else if ([host isEqualToString:@"installprofile"])
{
[self handleInstallProfileURLAction:url completion:completionHandler];
}
else if ([host isEqualToString:@"profileinstalled"])
{
[self handleProfileInstalledURLAction:url completion:completionHandler];
}
else
{
// Default: complete with the URL
completionHandler([MSIDWebviewAction completeAction:url]);
}
}

#pragma mark - Specific URL Handlers

- (void)handleEnrollURLAction:(NSURL *)url
completion:(void(^)(MSIDWebviewAction *action))completionHandler
{
// Extract continuation URL parameter (typically "cpurl")
NSDictionary *params = [url msidQueryParameters];
NSString *continuationURL = params[@"cpurl"];

if (!continuationURL)
{
MSID_LOG_WITH_CTX(MSIDLogLevelError, self.controller.requestParameters,
@"Enroll URL missing continuation parameter (cpurl)");
completionHandler([MSIDWebviewAction cancelAction]);
return;
}

// Attempt BRT acquisition if allowed (best-effort, non-blocking)
if (self.brtAttemptTracker.canAttemptBRT)
{
[self.brtAttemptTracker recordAttempt];

MSID_LOG_WITH_CTX(MSIDLogLevelInfo, self.controller.requestParameters,
@"Attempting BRT acquisition (attempt %ld)", (long)self.brtAttemptTracker.attemptCount);

// TODO: Implement actual BRT acquisition logic
// This should be async and non-blocking. For now, we just log and continue.
// In production, this would:
// 1. Check if broker is available
// 2. Attempt to acquire BRT
// 3. Store result in telemetry
// 4. Continue regardless of success/failure
}

// Create request to load continuation URL
NSURL *cpurlURL = [NSURL URLWithString:continuationURL];
if (!cpurlURL)
{
MSID_LOG_WITH_CTX(MSIDLogLevelError, self.controller.requestParameters,
@"Invalid continuation URL: %@", continuationURL);
completionHandler([MSIDWebviewAction cancelAction]);
return;
}

NSURLRequest *request = [NSURLRequest requestWithURL:cpurlURL];
completionHandler([MSIDWebviewAction loadRequestAction:request]);
}

- (void)handleInstallProfileURLAction:(NSURL *)url
completion:(void(^)(MSIDWebviewAction *action))completionHandler
{
// Retrieve stored headers (using generic header store)
// Common header names: x-install-url for the installation URL
// authorization or similar header for authentication
NSString *installURL = [self.responseHeaderStore headerForKey:@"x-install-url"];

// Try common auth header names (authorization is standard, but could be custom)
NSString *authToken = [self.responseHeaderStore headerForKey:@"authorization"];
if (!authToken) {
// Fall back to checking all stored headers for any auth-related header
NSDictionary *allHeaders = [self.responseHeaderStore allHeaders];
for (NSString *key in allHeaders) {
if ([[key lowercaseString] containsString:@"auth"] ||
[[key lowercaseString] containsString:@"token"]) {
authToken = allHeaders[key];
break;
}
}
}

if (!installURL)
{
MSID_LOG_WITH_CTX(MSIDLogLevelError, self.controller.requestParameters,
@"Install profile action called but install URL not found in header store");
completionHandler([MSIDWebviewAction cancelAction]);
return;
}

MSID_LOG_WITH_CTX(MSIDLogLevelInfo, self.controller.requestParameters,
@"Opening system webview for profile installation");

NSURL *profileInstallURL = [NSURL URLWithString:installURL];
if (!profileInstallURL)
{
MSID_LOG_WITH_CTX(MSIDLogLevelError, self.controller.requestParameters,
@"Invalid install URL: %@", installURL);
completionHandler([MSIDWebviewAction cancelAction]);
return;
}

// Create additional headers if auth token is available
NSDictionary<NSString *, NSString *> *additionalHeaders = nil;
if (authToken)
{
// Use Authorization header (standard) or the original key if found
additionalHeaders = @{@"Authorization": authToken};
}

// In production, this would create and start an ASWebAuthenticationSession
// For reference implementation, we log and cancel the embedded webview
MSID_LOG_WITH_CTX(MSIDLogLevelInfo, self.controller.requestParameters,
@"Would open system webview with URL: %@ and headers: %@",
profileInstallURL, additionalHeaders);

// Return cancel action since embedded webview flow is done
completionHandler([MSIDWebviewAction cancelAction]);
}

- (void)handleProfileInstalledURLAction:(NSURL *)url
completion:(void(^)(MSIDWebviewAction *action))completionHandler
{
MSID_LOG_WITH_CTX(MSIDLogLevelInfo, self.controller.requestParameters,
@"Profile installation completed, determining next action");

// Check if broker context is available
// In production, this would check broker availability and context
BOOL hasBrokerContext = NO; // TODO: Implement actual broker context check

if (hasBrokerContext)
{
// Continue broker flow with profile installed indication
MSID_LOG_WITH_CTX(MSIDLogLevelInfo, self.controller.requestParameters,
@"Continuing broker flow after profile installation");
completionHandler([MSIDWebviewAction completeAction:url]);
}
else
{
// Retry entire token request in broker context
// This is handled by existing architecture - the error will trigger broker retry
MSID_LOG_WITH_CTX(MSIDLogLevelInfo, self.controller.requestParameters,
@"Retrying token request in broker context after profile installation");

// TODO: In production, this would set up state to retry in broker context
// For now, complete with the URL and let existing logic handle it
completionHandler([MSIDWebviewAction completeAction:url]);
}
}

@end
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

There are no unit tests for the core MSIDWebviewSessionManager class, which contains 304 lines of complex logic including header capture, URL routing, and action handling. The PR only includes tests for the helper types (Action, ResponseEvent, BRTAttemptTracker, ResponseHeaderStore). Add comprehensive unit tests for MSIDWebviewSessionManager covering configuration, header capture with various scenarios, custom URL handling for all supported patterns (enroll, installProfile, profileInstalled), and edge cases.

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +128
// Capture headers (case-insensitive matching)
for (NSString *key in headers)
{
NSString *lowerKey = [key lowercaseString];
for (NSString *captureKey in headerKeys)
{
if ([lowerKey isEqualToString:[captureKey lowercaseString]])
{
[self.responseHeaderStore setHeader:headers[key] forKey:captureKey];
break;
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The nested loop for case-insensitive header matching has O(n*m) complexity where n is the number of response headers and m is the number of capture keys. For typical scenarios this is acceptable, but consider optimizing by creating a lowercase-normalized lookup set of capture keys once, rather than calling lowercaseString repeatedly inside the nested loop. This would reduce the complexity for the inner operation.

Suggested change
// Capture headers (case-insensitive matching)
for (NSString *key in headers)
{
NSString *lowerKey = [key lowercaseString];
for (NSString *captureKey in headerKeys)
{
if ([lowerKey isEqualToString:[captureKey lowercaseString]])
{
[self.responseHeaderStore setHeader:headers[key] forKey:captureKey];
break;
}
// Precompute a lowercase-to-original mapping for capture keys for efficient lookups
NSMutableDictionary<NSString *, NSString *> *normalizedCaptureKeys = [NSMutableDictionary new];
for (NSString *captureKey in headerKeys)
{
NSString *lowerCaptureKey = [captureKey lowercaseString];
if (lowerCaptureKey)
{
normalizedCaptureKeys[lowerCaptureKey] = captureKey;
}
}
// Capture headers (case-insensitive matching)
for (NSString *key in headers)
{
NSString *lowerKey = [key lowercaseString];
if (!lowerKey) continue;
NSString *normalizedKey = normalizedCaptureKeys[lowerKey];
if (normalizedKey)
{
[self.responseHeaderStore setHeader:headers[key] forKey:normalizedKey];

Copilot uses AI. Check for mistakes.
Comment on lines +222 to +234
// Try common auth header names (authorization is standard, but could be custom)
NSString *authToken = [self.responseHeaderStore headerForKey:@"authorization"];
if (!authToken) {
// Fall back to checking all stored headers for any auth-related header
NSDictionary *allHeaders = [self.responseHeaderStore allHeaders];
for (NSString *key in allHeaders) {
if ([[key lowercaseString] containsString:@"auth"] ||
[[key lowercaseString] containsString:@"token"]) {
authToken = allHeaders[key];
break;
}
}
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The fallback logic for finding auth-related headers searches for any key containing "auth" or "token". This is overly broad and could match unintended headers like "authentication-state", "token-type", etc. The current implementation also only retrieves the first match and breaks, which may not be the desired header. Consider being more specific about which auth header names to check, or document this fallback behavior and its priority order clearly.

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +71
@implementation MSIDResponseHeaderStore

- (instancetype)init
{
self = [super init];
if (self)
{
_headers = [NSMutableDictionary new];
}
return self;
}

- (void)setHeader:(NSString *)value forKey:(NSString *)key
{
if (key && value)
{
self.headers[key] = value;
}
}

- (NSString *)headerForKey:(NSString *)key
{
return self.headers[key];
}

- (NSDictionary<NSString *, NSString *> *)allHeaders
{
return [self.headers copy];
}

- (void)clearHeaders
{
[self.headers removeAllObjects];
}

@end
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

Sensitive headers (such as authorization tokens) are stored in plain text in memory without any encryption or secure storage mechanism. According to the documentation in docs/intune-enrollment-webview-flow.md line 596, "Header encryption" is listed as a future enhancement. For production use, consider implementing secure storage for sensitive headers, especially authentication tokens, to mitigate the risk of memory dumps or debugging tools exposing these values. At minimum, document the security implications of storing sensitive headers in plain text.

Copilot uses AI. Check for mistakes.
Comment on lines +125 to +131
/**
Configure webview with response event and action decision callbacks.
This should be called after the webview controller is created but before it is started.

@param webviewController The webview controller to configure
*/
- (void)configureWebview:(id)webviewController;
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The configureWebview method accepts an untyped 'id' parameter and silently returns if the object is not an MSIDOAuth2EmbeddedWebviewController. This design makes it easy to accidentally pass the wrong type without any compile-time or runtime error notification. Consider either: (1) accepting a protocol type that webview controllers must conform to, (2) returning a BOOL or error to indicate configuration success/failure, or (3) at minimum logging a warning when an unsupported webview type is passed.

Copilot uses AI. Check for mistakes.
Comment on lines +266 to +268
MSID_LOG_WITH_CTX(MSIDLogLevelInfo, self.controller.requestParameters,
@"Would open system webview with URL: %@ and headers: %@",
profileInstallURL, additionalHeaders);
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

handleInstallProfileURLAction logs the additionalHeaders dictionary via MSID_LOG_WITH_CTX, which can include sensitive authentication data such as Authorization tokens captured from response headers. An attacker who gains access to logs (e.g., via device compromise, log forwarding, or support dumps) could retrieve these tokens and impersonate users or enroll devices without authorization. Avoid logging header values that may contain secrets (especially Authorization and *AuthToken), and restrict logs to non-sensitive metadata such as the presence of headers or high-level status information.

Suggested change
MSID_LOG_WITH_CTX(MSIDLogLevelInfo, self.controller.requestParameters,
@"Would open system webview with URL: %@ and headers: %@",
profileInstallURL, additionalHeaders);
NSUInteger headerCount = additionalHeaders.count;
MSID_LOG_WITH_CTX(MSIDLogLevelInfo, self.controller.requestParameters,
@"Would open system webview with URL: %@ and %lu additional header(s) (header values not logged)",
profileInstallURL, (unsigned long)headerCount);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants