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