tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

MicrosoftEntraSSOUtils.mm (15143B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 #import <AuthenticationServices/ASAuthorizationSingleSignOnProvider.h>
      6 #import <AuthenticationServices/AuthenticationServices.h>
      7 #import <Foundation/Foundation.h>
      8 
      9 #include <functional>  // For std::function
     10 
     11 #include "MicrosoftEntraSSOUtils.h"
     12 #include "nsIURI.h"
     13 #include "nsHttp.h"
     14 #include "nsHttpChannel.h"
     15 #include "nsCocoaUtils.h"
     16 #include "nsTHashMap.h"
     17 #include "nsHashKeys.h"
     18 #include "nsThreadUtils.h"
     19 #include "mozilla/Logging.h"
     20 #include "mozilla/glean/NetwerkMetrics.h"
     21 
     22 namespace {
     23 static mozilla::LazyLogModule gMacOSWebAuthnServiceLog("macOSSingleSignOn");
     24 }  // namespace
     25 
     26 NS_ASSUME_NONNULL_BEGIN
     27 
     28 // Delegate
     29 API_AVAILABLE(macos(13.3))
     30 @interface SSORequestDelegate : NSObject <ASAuthorizationControllerDelegate>
     31 - (void)setCallback:(mozilla::net::MicrosoftEntraSSOUtils*)callback;
     32 @end
     33 
     34 namespace mozilla {
     35 namespace net {
     36 
     37 class API_AVAILABLE(macos(13.3)) MicrosoftEntraSSOUtils final {
     38 public:
     39  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MicrosoftEntraSSOUtils)
     40 
     41  explicit MicrosoftEntraSSOUtils(nsHttpChannel* aChannel,
     42                                  std::function<void()>&& aResultCallback);
     43  bool AddMicrosoftEntraSSOInternal();
     44  void AddRequestHeader(const nsACString& aKey, const nsACString& aValue);
     45  void InvokeCallback();
     46 
     47 private:
     48  ~MicrosoftEntraSSOUtils();
     49  ASAuthorizationSingleSignOnProvider* mProvider;
     50  ASAuthorizationController* mAuthorizationController;
     51  SSORequestDelegate* mRequestDelegate;
     52  RefPtr<nsHttpChannel> mChannel;
     53  std::function<void()> mResultCallback;
     54  nsTHashMap<nsCStringHashKey, nsCString> mRequestHeaders;
     55 };
     56 }  // namespace net
     57 }  // namespace mozilla
     58 
     59 @implementation SSORequestDelegate {
     60  RefPtr<mozilla::net::MicrosoftEntraSSOUtils> mCallback;
     61 }
     62 - (void)setCallback:(mozilla::net::MicrosoftEntraSSOUtils*)callback {
     63  mCallback = callback;
     64 }
     65 - (void)authorizationController:(ASAuthorizationController*)controller
     66    didCompleteWithAuthorization:(ASAuthorization*)authorization {
     67  ASAuthorizationSingleSignOnCredential* ssoCredential =
     68      [authorization.credential
     69          isKindOfClass:[ASAuthorizationSingleSignOnCredential class]]
     70          ? (ASAuthorizationSingleSignOnCredential*)authorization.credential
     71          : nil;
     72 
     73  if (!ssoCredential) {
     74    MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
     75            ("SSORequestDelegate::didCompleteWithAuthorization: "
     76             "should have ASAuthorizationSingleSignOnCredential"));
     77    mozilla::glean::network_sso::entra_success.Get("no_credential"_ns).Add(1);
     78    [self invokeCallbackOnMainThread];
     79    return;
     80  }
     81 
     82  NSHTTPURLResponse* authenticatedResponse =
     83      ssoCredential.authenticatedResponse;
     84  if (!authenticatedResponse) {
     85    MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
     86            ("SSORequestDelegate::didCompleteWithAuthorization: "
     87             "authenticatedResponse is nil"));
     88    mozilla::glean::network_sso::entra_success.Get("invalid_cookie"_ns).Add(1);
     89    [self invokeCallbackOnMainThread];
     90    return;
     91  }
     92 
     93  NSDictionary* headers = authenticatedResponse.allHeaderFields;
     94  NSMutableString* headersString = [NSMutableString string];
     95  for (NSString* key in headers) {
     96    [headersString appendFormat:@"%@: %@\n", key, headers[key]];
     97  }
     98  MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
     99          ("SSORequestDelegate::didCompleteWithAuthorization: "
    100           "authenticatedResponse: \nStatus Code: %ld\nHeaders:\n%s",
    101           (long)authenticatedResponse.statusCode, [headersString UTF8String]));
    102 
    103  // An example format of ssoCookies:
    104  // sso_cookies:
    105  // {"device_headers":[
    106  // {"header":{"x-ms-DeviceCredential”:”…”},”tenant_id”:”…”}],
    107  // ”prt_headers":[{"header":{"x-ms-RefreshTokenCredential”:”…”},
    108  // ”home_account_id”:”….”}]}
    109  NSString* ssoCookies = headers[@"sso_cookies"];
    110  if (!ssoCookies) {
    111    MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
    112            ("SSORequestDelegate::didCompleteWithAuthorization: "
    113             "authenticatedResponse is nil"));
    114    mozilla::glean::network_sso::entra_success.Get("invalid_cookie"_ns).Add(1);
    115    [self invokeCallbackOnMainThread];
    116    return;
    117  }
    118  NSError* err = nil;
    119  NSDictionary* ssoCookiesDict = [NSJSONSerialization
    120      JSONObjectWithData:[ssoCookies dataUsingEncoding:NSUTF8StringEncoding]
    121                 options:0
    122                   error:&err];
    123 
    124  if (err) {
    125    MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
    126            ("SSORequestDelegate::didCompleteWithAuthorization: Error parsing "
    127             "JSON: %s",
    128             [[err localizedDescription] UTF8String]));
    129    mozilla::glean::network_sso::entra_success.Get("invalid_cookie"_ns).Add(1);
    130    [self invokeCallbackOnMainThread];
    131    return;
    132  }
    133 
    134  NSMutableArray* allHeaders = [NSMutableArray array];
    135  nsCString entraSuccessLabel;
    136 
    137  if (ssoCookiesDict[@"device_headers"]) {
    138    [allHeaders addObject:ssoCookiesDict[@"device_headers"]];
    139  } else {
    140    MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
    141            ("SSORequestDelegate::didCompleteWithAuthorization: "
    142             "Missing device_headers"));
    143    entraSuccessLabel = "device_headers_missing"_ns;
    144  }
    145 
    146  if (ssoCookiesDict[@"prt_headers"]) {
    147    [allHeaders addObject:ssoCookiesDict[@"prt_headers"]];
    148  } else {
    149    MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
    150            ("SSORequestDelegate::didCompleteWithAuthorization: "
    151             "Missing prt_headers"));
    152    entraSuccessLabel = "prt_headers_missing"_ns;
    153  }
    154 
    155  if (allHeaders.count == 0) {
    156    entraSuccessLabel = "both_headers_missing"_ns;
    157  }
    158 
    159  // We would like to have both device_headers and prt_headers before
    160  // attaching the headers
    161  if (allHeaders.count != 2) {
    162    MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
    163            ("SSORequestDelegate::didCompleteWithAuthorization: "
    164             "sso_cookies has missing headers"));
    165    mozilla::glean::network_sso::entra_success.Get(entraSuccessLabel).Add(1);
    166  } else {
    167    mozilla::glean::network_sso::entra_success.Get("success"_ns).Add(1);
    168  }
    169 
    170  // Append cookie headers retrieved from MS Broker
    171  for (NSArray* headerArray in allHeaders) {
    172    if (!headerArray) {
    173      continue;
    174    }
    175    for (NSDictionary* headerDict in headerArray) {
    176      NSDictionary* headers = headerDict[@"header"];
    177      if (!headers) {
    178        continue;
    179      }
    180      for (NSString* key in headers) {
    181        NSString* value = headers[key];
    182        if (!value) {
    183          continue;
    184        }
    185        nsAutoString nsKey;
    186        nsAutoString nsValue;
    187        mozilla::CopyNSStringToXPCOMString(key, nsKey);
    188        mozilla::CopyNSStringToXPCOMString(value, nsValue);
    189        mCallback->AddRequestHeader(NS_ConvertUTF16toUTF8(nsKey),
    190                                    NS_ConvertUTF16toUTF8(nsValue));
    191      }
    192    }
    193  }
    194 
    195  [self invokeCallbackOnMainThread];
    196 }
    197 - (void)invokeCallbackOnMainThread {
    198  NS_DispatchToMainThread(NS_NewRunnableFunction(
    199      "SSORequestDelegate::didCompleteWithAuthorization",
    200      [callback(mCallback)]() { callback->InvokeCallback(); }));
    201 }
    202 
    203 - (void)authorizationController:(ASAuthorizationController*)controller
    204           didCompleteWithError:(NSError*)error {
    205  nsAutoString errorDescription;
    206  nsAutoString errorDomain;
    207  nsCocoaUtils::GetStringForNSString(error.localizedDescription,
    208                                     errorDescription);
    209  nsCocoaUtils::GetStringForNSString(error.domain, errorDomain);
    210  MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
    211          ("SSORequestDelegate::didCompleteWithError: domain "
    212           "'%s' code %ld (%s)",
    213           NS_ConvertUTF16toUTF8(errorDomain).get(), error.code,
    214           NS_ConvertUTF16toUTF8(errorDescription).get()));
    215  if ([error.domain isEqualToString:ASAuthorizationErrorDomain]) {
    216    switch (error.code) {
    217      case ASAuthorizationErrorCanceled:
    218        MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
    219                ("SSORequestDelegate::didCompleteWithError: Authorization "
    220                 "error: The user canceled the authorization attempt."));
    221        break;
    222      case ASAuthorizationErrorFailed:
    223        MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
    224                ("SSORequestDelegate::didCompleteWithError: Authorization "
    225                 "error: The authorization attempt failed."));
    226        break;
    227      case ASAuthorizationErrorInvalidResponse:
    228        MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
    229                ("Authorization error: The authorization request received an "
    230                 "invalid response."));
    231        break;
    232      case ASAuthorizationErrorNotHandled:
    233        MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
    234                ("SSORequestDelegate::didCompleteWithError: Authorization "
    235                 "error: The authorization request wasn’t handled."));
    236        break;
    237      case ASAuthorizationErrorUnknown:
    238        MOZ_LOG(
    239            gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
    240            ("SSORequestDelegate::didCompleteWithError: Authorization error: "
    241             "The authorization attempt failed for an unknown reason."));
    242        break;
    243      case ASAuthorizationErrorNotInteractive:
    244        MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
    245                ("SSORequestDelegate::didCompleteWithError: Authorization "
    246                 "error: The authorization request isn’t interactive."));
    247        break;
    248      default:
    249        MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
    250                ("SSORequestDelegate::didCompleteWithError: Authorization "
    251                 "error: Unhandled error code."));
    252        break;
    253    }
    254  }
    255 
    256  mozilla::glean::network_sso::entra_success.Get("broker_error"_ns).Add(1);
    257  NS_DispatchToMainThread(NS_NewRunnableFunction(
    258      "SSORequestDelegate::didCompleteWithError", [callback(mCallback)]() {
    259        MOZ_ASSERT(NS_IsMainThread());
    260        callback->InvokeCallback();
    261      }));
    262 }
    263 @end
    264 
    265 namespace mozilla {
    266 namespace net {
    267 
    268 MicrosoftEntraSSOUtils::MicrosoftEntraSSOUtils(
    269    nsHttpChannel* aChannel, std::function<void()>&& aResultCallback)
    270    : mProvider(nullptr),
    271      mAuthorizationController(nullptr),
    272      mRequestDelegate(nullptr),
    273      mChannel(aChannel),
    274      mResultCallback(std::move(aResultCallback)) {
    275  MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
    276          ("MicrosoftEntraSSOUtils::MicrosoftEntraSSOUtils()"));
    277 }
    278 
    279 MicrosoftEntraSSOUtils::~MicrosoftEntraSSOUtils() {
    280  MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
    281          ("MicrosoftEntraSSOUtils::~MicrosoftEntraSSOUtils()"));
    282  if (mRequestDelegate) {
    283    [mRequestDelegate release];
    284    mRequestDelegate = nil;
    285  }
    286  if (mAuthorizationController) {
    287    [mAuthorizationController release];
    288    mAuthorizationController = nil;
    289  }
    290 }
    291 
    292 void MicrosoftEntraSSOUtils::AddRequestHeader(const nsACString& aKey,
    293                                              const nsACString& aValue) {
    294  mRequestHeaders.InsertOrUpdate(aKey, aValue);
    295 }
    296 
    297 // Used to return to nsHttpChannel::ContinuePrepareToConnect after the delegate
    298 // completes its job
    299 void MicrosoftEntraSSOUtils::InvokeCallback() {
    300  MOZ_ASSERT(NS_IsMainThread());
    301  MOZ_ASSERT(mChannel,
    302             "channel needs to be initialized for MicrosoftEntraSSOUtils");
    303 
    304  if (!mRequestHeaders.IsEmpty()) {
    305    for (auto iter = mRequestHeaders.Iter(); !iter.Done(); iter.Next()) {
    306      // Passed value will be merged to any existing value.
    307      mChannel->SetRequestHeader(iter.Key(), iter.Data(), true);
    308    }
    309  }
    310 
    311  std::function<void()> callback = std::move(mResultCallback);
    312  if (callback) {
    313    callback();
    314  }
    315 }
    316 
    317 bool MicrosoftEntraSSOUtils::AddMicrosoftEntraSSOInternal() {
    318  MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
    319          ("MicrosoftEntraSSOUtils::AddMicrosoftEntraSSO start"));
    320  MOZ_ASSERT(NS_IsMainThread());
    321  MOZ_ASSERT(mChannel,
    322             "channel needs to be initialized for MicrosoftEntraSSOUtils");
    323 
    324  NSURL* url =
    325      [NSURL URLWithString:@"https://login.microsoftonline.com/common"];
    326 
    327  mProvider = [ASAuthorizationSingleSignOnProvider
    328      authorizationProviderWithIdentityProviderURL:url];
    329  if (!mProvider) {
    330    return false;
    331  }
    332 
    333  if (![mProvider canPerformAuthorization]) {
    334    return false;
    335  }
    336 
    337  nsCOMPtr<nsIURI> uri;
    338  mChannel->GetURI(getter_AddRefs(uri));
    339  if (!url) {
    340    return false;
    341  }
    342 
    343  nsAutoCString urispec;
    344  uri->GetSpec(urispec);
    345  NSString* urispecNSString = [NSString stringWithUTF8String:urispec.get()];
    346  MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
    347          ("MicrosoftEntraSSOUtils::AddMicrosoftEntraSSO [urispec=%s]",
    348           urispec.get()));
    349 
    350  // Create a controller and initialize it with SSO requests
    351  ASAuthorizationSingleSignOnRequest* ssoRequest = [mProvider createRequest];
    352  ssoRequest.requestedOperation = @"get_sso_cookies";
    353  ssoRequest.userInterfaceEnabled = NO;
    354 
    355  // Set NSURLQueryItems for the MS broker
    356  NSURLQueryItem* ssoUrl = [NSURLQueryItem queryItemWithName:@"sso_url"
    357                                                       value:urispecNSString];
    358  NSURLQueryItem* typesOfHeader =
    359      [NSURLQueryItem queryItemWithName:@"types_of_header" value:@"0"];
    360  NSURLQueryItem* brokerKey = [NSURLQueryItem
    361      queryItemWithName:@"broker_key"
    362                  value:@"kSiiehqi0sbYWxT2zOmV-rv8B3QRNsUKcU3YPc122121"];
    363  NSURLQueryItem* protocolVer =
    364      [NSURLQueryItem queryItemWithName:@"msg_protocol_ver" value:@"4"];
    365  ssoRequest.authorizationOptions =
    366      @[ ssoUrl, typesOfHeader, brokerKey, protocolVer ];
    367 
    368  if (!ssoRequest) {
    369    return false;
    370  }
    371 
    372  mAuthorizationController = [[ASAuthorizationController alloc]
    373      initWithAuthorizationRequests:@[ ssoRequest ]];
    374  if (!mAuthorizationController) {
    375    return false;
    376  }
    377 
    378  mRequestDelegate = [[SSORequestDelegate alloc] init];
    379  [mRequestDelegate setCallback:this];
    380  mAuthorizationController.delegate = mRequestDelegate;
    381 
    382  [mAuthorizationController performRequests];
    383 
    384  // Return true after acknowledging that the delegate will be called
    385  return true;
    386 }
    387 
    388 API_AVAILABLE(macos(13.3))
    389 nsresult AddMicrosoftEntraSSO(nsHttpChannel* aChannel,
    390                              std::function<void()>&& aResultCallback) {
    391  MOZ_ASSERT(XRE_IsParentProcess());
    392 
    393  // The service is used by this function and the delegate, and it should be
    394  // released when the delegate finishes running. It will remain alive even
    395  // after AddMicrosoftEntraSSO returns.
    396  RefPtr<MicrosoftEntraSSOUtils> service =
    397      new MicrosoftEntraSSOUtils(aChannel, std::move(aResultCallback));
    398 
    399  mozilla::glean::network_sso::total_entra_uses.Add(1);
    400 
    401  if (!service->AddMicrosoftEntraSSOInternal()) {
    402    mozilla::glean::network_sso::entra_success
    403        .Get("invalid_controller_setup"_ns)
    404        .Add(1);
    405    return NS_ERROR_FAILURE;
    406  }
    407 
    408  return NS_OK;
    409 }
    410 }  // namespace net
    411 }  // namespace mozilla
    412 
    413 NS_ASSUME_NONNULL_END