chromium/chrome/browser/enterprise/platform_auth/extensible_enterprise_sso_provider_mac.mm

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "chrome/browser/enterprise/platform_auth/extensible_enterprise_sso_provider_mac.h"

#import <AuthenticationServices/AuthenticationServices.h>
#import <Foundation/Foundation.h>

#import <string>
#import <utility>
#import <vector>

#import "base/functional/callback.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/bind_post_task.h"
#import "chrome/browser/platform_util.h"
#import "components/policy/core/common/policy_logger.h"
#import "net/base/apple/http_response_headers_util.h"
#import "net/base/apple/url_conversions.h"
#import "net/http/http_request_headers.h"
#import "net/http/http_response_headers.h"
#import "net/http/http_util.h"
#import "net/url_request/url_request.h"
#import "url/gurl.h"

// Interface that provides a presentation context to the platform
// and a delegate for the authorization controller.
@interface SSOServiceAuthControllerDelegate
    : NSObject <ASAuthorizationControllerDelegate,
                ASAuthorizationControllerPresentationContextProviding>
@end

// Class that allows fetching authentication headers for a url if it is
// supported by any SSO extension on the device.
@implementation SSOServiceAuthControllerDelegate {
  enterprise_auth::PlatformAuthProviderManager::GetDataCallback _callback;
  ASAuthorizationController* _controller;
}

- (void)dealloc {
  VLOG_POLICY(2, EXTENSIBLE_SSO) << "[ExtensibleEnterpriseSSO] Destroying "
                                    "SSOServiceAuthControllerDelegate";
  // This is here for debugging purposes and will be removed once this code is
  // no longer experimental.
  if (_callback) {
    VLOG_POLICY(2, EXTENSIBLE_SSO)
        << "[ExtensibleEnterpriseSSO] Fetching headers aborted.";
    std::move(_callback).Run(net::HttpRequestHeaders());
  }
}

// Gets authentication headers for `url` if the device can perform
// authentication for it.
// If the device can perform the authentication, `withCallback` is called
// with headers built from the response from the device, otherwise it is called
// with empty headers.
- (void)getAuthHeaders:(GURL)url
          withCallback:
              (enterprise_auth::PlatformAuthProviderManager::GetDataCallback)
                  callback {
  _callback = std::move(callback);
  ASAuthorizationSingleSignOnProvider* auth_provider =
      [ASAuthorizationSingleSignOnProvider
          authorizationProviderWithIdentityProviderURL:net::NSURLWithGURL(url)];

  if (!auth_provider.canPerformAuthorization) {
    VLOG_POLICY(2, EXTENSIBLE_SSO)
        << "[ExtensibleEnterpriseSSO] Fetching headers for " << url
        << " NOT SUPPORTED.";
    std::move(_callback).Run(net::HttpRequestHeaders());
    return;
  }
  VLOG_POLICY(2, EXTENSIBLE_SSO)
      << "[ExtensibleEnterpriseSSO] Attempting to get headers for " << url;

  // Create a login request for `url`.
  ASAuthorizationSingleSignOnRequest* request = [auth_provider createRequest];
  request.requestedOperation = ASAuthorizationOperationLogin;
  _controller = [[ASAuthorizationController alloc]
      initWithAuthorizationRequests:[NSArray arrayWithObject:request]];
  _controller.delegate = self;
  _controller.presentationContextProvider = self;

  [_controller performRequests];
}

// ASAuthorizationControllerDelegate implementation

// Called when the authentication was successful and creates a
// HttpRequestHeaders from `authorization`.
- (void)authorizationController:(ASAuthorizationController*)controller
    didCompleteWithAuthorization:(ASAuthorization*)authorization {
  VLOG_POLICY(2, EXTENSIBLE_SSO)
      << "[ExtensibleEnterpriseSSO] Fetching headers completed.";
  ASAuthorizationSingleSignOnRequest* request =
      (ASAuthorizationSingleSignOnRequest*)
          controller.authorizationRequests.firstObject;
  if (!request || request.requestedOperation != ASAuthorizationOperationLogin) {
    VLOG_POLICY(2, EXTENSIBLE_SSO)
        << "[ExtensibleEnterpriseSSO] Fetching headers completed for non-login "
           "operation.";
    std::move(_callback).Run(net::HttpRequestHeaders());
    return;
  }
  VLOG_POLICY(2, EXTENSIBLE_SSO)
      << "[ExtensibleEnterpriseSSO] Fetching headers completed for login "
         "operation.";
  ASAuthorizationSingleSignOnCredential* credential = authorization.credential;
  NSDictionary* headers = credential.authenticatedResponse.allHeaderFields;
  net::HttpRequestHeaders request_headers;
  for (NSString* key in headers) {
    const std::string header_name = base::SysNSStringToUTF8(key);
    if (!net::HttpUtil::IsValidHeaderName(header_name)) {
      VLOG_POLICY(2, EXTENSIBLE_SSO)
          << "[ExtensibleEnterpriseSSO] Invalid header name " << header_name;
      continue;
    }

    const std::string header_value = base::SysNSStringToUTF8(
        net::FixNSStringIncorrectlyDecodedAsLatin1(headers[key]));
    if (!net::HttpUtil::IsValidHeaderValue(header_value)) {
      VLOG_POLICY(2, EXTENSIBLE_SSO)
          << "[ExtensibleEnterpriseSSO] Invalid header value " << header_value;
      continue;
    }

    request_headers.SetHeader(header_name, header_value);
  }
  std::move(_callback).Run(std::move(request_headers));
}

// Called when the authentication failed and creates a
// empty HttpRequestHeaders.
- (void)authorizationController:(ASAuthorizationController*)controller
           didCompleteWithError:(NSError*)error {
  VLOG_POLICY(2, EXTENSIBLE_SSO)
      << "[ExtensibleEnterpriseSSO] Fetching headers failed";
  std::move(_callback).Run(net::HttpRequestHeaders());
}

// ASAuthorizationControllerPresentationContextProviding implementation
- (ASPresentationAnchor)presentationAnchorForAuthorizationController:
    (ASAuthorizationController*)controller {
  // TODO(b/340868357): Pick the window where the url is being used.
  return platform_util::GetActiveWindow();
}

@end

namespace enterprise_auth {

namespace {

// Empty function used to ensure SSOServiceAuthControllerDelegate does not get
// destroyed until the data is fetched.
void OnDataFetched(SSOServiceAuthControllerDelegate*) {
  VLOG_POLICY(2, EXTENSIBLE_SSO)
      << "[ExtensibleEnterpriseSSO] Deleting SSOServiceAuthControllerDelegate";
}

}  // namespace

ExtensibleEnterpriseSSOProvider::ExtensibleEnterpriseSSOProvider() = default;

ExtensibleEnterpriseSSOProvider::~ExtensibleEnterpriseSSOProvider() = default;

bool ExtensibleEnterpriseSSOProvider::SupportsOriginFiltering() {
  return false;
}

void ExtensibleEnterpriseSSOProvider::FetchOrigins(
    FetchOriginsCallback on_fetch_complete) {
  // Origin filtering is nor supported.
  NOTREACHED();
}

void ExtensibleEnterpriseSSOProvider::GetData(
    const GURL& url,
    PlatformAuthProviderManager::GetDataCallback callback) {
  SSOServiceAuthControllerDelegate* delegate =
      [[SSOServiceAuthControllerDelegate alloc] init];
  auto final_callback = base::BindPostTaskToCurrentDefault(
      std::move(callback).Then(base::BindOnce(&OnDataFetched, delegate)));
  [delegate getAuthHeaders:url withCallback:std::move(final_callback)];
}

}  // namespace enterprise_auth