fune/netwerk/protocol/http/MicrosoftEntraSSOUtils.mm

411 lines
16 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#import <AuthenticationServices/ASAuthorizationSingleSignOnProvider.h>
#import <AuthenticationServices/AuthenticationServices.h>
#import <Foundation/Foundation.h>
#include <functional> // For std::function
#include "MicrosoftEntraSSOUtils.h"
#include "nsIURI.h"
#include "nsHttp.h"
#include "nsHttpChannel.h"
#include "nsCocoaUtils.h"
#include "nsTHashMap.h"
#include "nsHashKeys.h"
#include "nsThreadUtils.h"
#include "mozilla/Logging.h"
#include "mozilla/SyncRunnable.h"
#include "mozilla/glean/GleanMetrics.h"
namespace {
static mozilla::LazyLogModule gMacOSWebAuthnServiceLog("macOSSingleSignOn");
} // namespace
NS_ASSUME_NONNULL_BEGIN
// Delegate
API_AVAILABLE(macos(13.3))
@interface SSORequestDelegate : NSObject <ASAuthorizationControllerDelegate>
- (void)setCallback:(mozilla::net::MicrosoftEntraSSOUtils*)callback;
@end
namespace mozilla {
namespace net {
class API_AVAILABLE(macos(13.3)) MicrosoftEntraSSOUtils final {
public:
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MicrosoftEntraSSOUtils)
explicit MicrosoftEntraSSOUtils(nsHttpChannel* aChannel,
std::function<void()>&& aResultCallback);
bool AddMicrosoftEntraSSOInternal();
void AddRequestHeader(const nsACString& aKey, const nsACString& aValue);
void InvokeCallback();
private:
~MicrosoftEntraSSOUtils();
ASAuthorizationSingleSignOnProvider* mProvider;
ASAuthorizationController* mAuthorizationController;
SSORequestDelegate* mRequestDelegate;
RefPtr<nsHttpChannel> mChannel;
std::function<void()> mResultCallback;
nsTHashMap<nsCStringHashKey, nsCString> mRequestHeaders;
};
} // namespace net
} // namespace mozilla
@implementation SSORequestDelegate {
RefPtr<mozilla::net::MicrosoftEntraSSOUtils> mCallback;
}
- (void)setCallback:(mozilla::net::MicrosoftEntraSSOUtils*)callback {
mCallback = callback;
}
- (void)authorizationController:(ASAuthorizationController*)controller
didCompleteWithAuthorization:(ASAuthorization*)authorization {
if ([authorization.credential
isKindOfClass:[ASAuthorizationSingleSignOnCredential class]]) {
MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("SSORequestDelegate::didCompleteWithAuthorization: "
"got ASAuthorizationSingleSignOnCredential"));
ASAuthorizationSingleSignOnCredential* ssoCredential =
(ASAuthorizationSingleSignOnCredential*)authorization.credential;
NSHTTPURLResponse* authenticatedResponse =
ssoCredential.authenticatedResponse;
if (authenticatedResponse) {
NSDictionary* headers = authenticatedResponse.allHeaderFields;
NSMutableString* headersString = [NSMutableString string];
for (NSString* key in headers) {
[headersString appendFormat:@"%@: %@\n", key, headers[key]];
}
MOZ_LOG(
gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("SSORequestDelegate::didCompleteWithAuthorization: "
"authenticatedResponse: \nStatus Code: %ld\nHeaders:\n%s",
(long)authenticatedResponse.statusCode, [headersString UTF8String]));
// An example format of ssoCookies:
// sso_cookies:
// {"device_headers":[
// {"header":{"x-ms-DeviceCredential”:”…”},”tenant_id”:”…”}],
// ”prt_headers":[{"header":{"x-ms-RefreshTokenCredential”:”…”},
// ”home_account_id”:”….”}]}
NSString* ssoCookies = headers[@"sso_cookies"];
if (ssoCookies) {
NSError* err = nil;
NSDictionary* ssoCookiesDict = [NSJSONSerialization
JSONObjectWithData:[ssoCookies
dataUsingEncoding:NSUTF8StringEncoding]
options:0
error:&err];
if (!err) {
NSMutableArray* allHeaders = [NSMutableArray array];
nsCString entraSuccessLabel;
if (ssoCookiesDict[@"device_headers"]) {
[allHeaders addObject:ssoCookiesDict[@"device_headers"]];
} else {
MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("SSORequestDelegate::didCompleteWithAuthorization: "
"Missing device_headers"));
entraSuccessLabel = "device_headers_missing"_ns;
}
if (ssoCookiesDict[@"prt_headers"]) {
[allHeaders addObject:ssoCookiesDict[@"prt_headers"]];
} else {
MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("SSORequestDelegate::didCompleteWithAuthorization: "
"Missing prt_headers"));
entraSuccessLabel = "prt_headers_missing"_ns;
}
if (allHeaders.count == 0) {
entraSuccessLabel = "both_headers_missing"_ns;
}
// We would like to have both device_headers and prt_headers before
// attaching the headers
if (allHeaders.count == 2) {
// Append cookie headers retrieved from MS Broker
for (NSArray* headerArray in allHeaders) {
if (headerArray) {
for (NSDictionary* headerDict in headerArray) {
NSDictionary* headers = headerDict[@"header"];
if (headers) {
for (NSString* key in headers) {
NSString* value = headers[key];
if (value) {
nsAutoString nsKey;
nsAutoString nsValue;
mozilla::CopyNSStringToXPCOMString(key, nsKey);
mozilla::CopyNSStringToXPCOMString(value, nsValue);
mCallback->AddRequestHeader(
NS_ConvertUTF16toUTF8(nsKey),
NS_ConvertUTF16toUTF8(nsValue));
}
}
}
}
}
}
mozilla::glean::network_sso::entra_success.Get("success"_ns).Add(1);
} else {
mozilla::glean::network_sso::entra_success.Get(entraSuccessLabel)
.Add(1);
MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("SSORequestDelegate::didCompleteWithAuthorization: "
"sso_cookies has missing headers"));
}
} else {
MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("SSORequestDelegate::didCompleteWithAuthorization: "
"Failed to parse sso_cookies: %s",
[[err localizedDescription] UTF8String]));
mozilla::glean::network_sso::entra_success.Get("invalid_cookie"_ns)
.Add(1);
}
} else {
MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("SSORequestDelegate::didCompleteWithAuthorization: "
"sso_cookies is not present"));
mozilla::glean::network_sso::entra_success.Get("invalid_cookie"_ns)
.Add(1);
}
} else {
MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("SSORequestDelegate::didCompleteWithAuthorization: "
"authenticatedResponse is nil"));
mozilla::glean::network_sso::entra_success.Get("invalid_cookie"_ns)
.Add(1);
}
} else {
MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("SSORequestDelegate::didCompleteWithAuthorization: "
"should have ASAuthorizationSingleSignOnCredential"));
mozilla::glean::network_sso::entra_success.Get("no_credential"_ns).Add(1);
}
NS_DispatchToMainThread(NS_NewRunnableFunction(
"SSORequestDelegate::didCompleteWithAuthorization failure",
[callback(mCallback)]() {
MOZ_ASSERT(NS_IsMainThread());
callback->InvokeCallback();
}));
}
- (void)authorizationController:(ASAuthorizationController*)controller
didCompleteWithError:(NSError*)error {
nsAutoString errorDescription;
nsAutoString errorDomain;
nsCocoaUtils::GetStringForNSString(error.localizedDescription,
errorDescription);
nsCocoaUtils::GetStringForNSString(error.domain, errorDomain);
MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("SSORequestDelegate::didCompleteWithError: domain "
"'%s' code %ld (%s)",
NS_ConvertUTF16toUTF8(errorDomain).get(), error.code,
NS_ConvertUTF16toUTF8(errorDescription).get()));
if ([error.domain isEqualToString:ASAuthorizationErrorDomain]) {
switch (error.code) {
case ASAuthorizationErrorCanceled:
MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("SSORequestDelegate::didCompleteWithError: Authorization "
"error: The user canceled the authorization attempt."));
break;
case ASAuthorizationErrorFailed:
MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("SSORequestDelegate::didCompleteWithError: Authorization "
"error: The authorization attempt failed."));
break;
case ASAuthorizationErrorInvalidResponse:
MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("Authorization error: The authorization request received an "
"invalid response."));
break;
case ASAuthorizationErrorNotHandled:
MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("SSORequestDelegate::didCompleteWithError: Authorization "
"error: The authorization request wasnt handled."));
break;
case ASAuthorizationErrorUnknown:
MOZ_LOG(
gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("SSORequestDelegate::didCompleteWithError: Authorization error: "
"The authorization attempt failed for an unknown reason."));
break;
case ASAuthorizationErrorNotInteractive:
MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("SSORequestDelegate::didCompleteWithError: Authorization "
"error: The authorization request isnt interactive."));
break;
default:
MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("SSORequestDelegate::didCompleteWithError: Authorization "
"error: Unhandled error code."));
break;
}
}
mozilla::glean::network_sso::entra_success.Get("broker_error"_ns).Add(1);
NS_DispatchToMainThread(NS_NewRunnableFunction(
"SSORequestDelegate::didCompleteWithError", [callback(mCallback)]() {
MOZ_ASSERT(NS_IsMainThread());
callback->InvokeCallback();
}));
}
@end
namespace mozilla {
namespace net {
MicrosoftEntraSSOUtils::MicrosoftEntraSSOUtils(
nsHttpChannel* aChannel, std::function<void()>&& aResultCallback)
: mProvider(nullptr),
mAuthorizationController(nullptr),
mRequestDelegate(nullptr),
mChannel(aChannel),
mResultCallback(std::move(aResultCallback)) {
MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("MicrosoftEntraSSOUtils::MicrosoftEntraSSOUtils()"));
}
MicrosoftEntraSSOUtils::~MicrosoftEntraSSOUtils() {
MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("MicrosoftEntraSSOUtils::~MicrosoftEntraSSOUtils()"));
if (mRequestDelegate) {
[mRequestDelegate release];
mRequestDelegate = nil;
}
if (mAuthorizationController) {
[mAuthorizationController release];
mAuthorizationController = nil;
}
}
void MicrosoftEntraSSOUtils::AddRequestHeader(const nsACString& aKey,
const nsACString& aValue) {
mRequestHeaders.InsertOrUpdate(aKey, aValue);
}
// Used to return to nsHttpChannel::ContinuePrepareToConnect after the delegate
// completes its job
void MicrosoftEntraSSOUtils::InvokeCallback() {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mChannel,
"channel needs to be initialized for MicrosoftEntraSSOUtils");
if (!mRequestHeaders.IsEmpty()) {
for (auto iter = mRequestHeaders.Iter(); !iter.Done(); iter.Next()) {
// Passed value will be merged to any existing value.
mChannel->SetRequestHeader(iter.Key(), iter.Data(), true);
}
}
std::function<void()> callback = std::move(mResultCallback);
if (callback) {
callback();
}
}
bool MicrosoftEntraSSOUtils::AddMicrosoftEntraSSOInternal() {
MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("MicrosoftEntraSSOUtils::AddMicrosoftEntraSSO start"));
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mChannel,
"channel needs to be initialized for MicrosoftEntraSSOUtils");
NSURL* url =
[NSURL URLWithString:@"https://login.microsoftonline.com/common"];
mProvider = [ASAuthorizationSingleSignOnProvider
authorizationProviderWithIdentityProviderURL:url];
if (!mProvider) {
return false;
}
if (![mProvider canPerformAuthorization]) {
return false;
}
nsCOMPtr<nsIURI> uri;
mChannel->GetURI(getter_AddRefs(uri));
if (!url) {
return false;
}
nsAutoCString urispec;
uri->GetSpec(urispec);
NSString* urispecNSString = [NSString stringWithUTF8String:urispec.get()];
MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug,
("MicrosoftEntraSSOUtils::AddMicrosoftEntraSSO [urispec=%s]",
urispec.get()));
// Create a controller and initialize it with SSO requests
ASAuthorizationSingleSignOnRequest* ssoRequest = [mProvider createRequest];
ssoRequest.requestedOperation = @"get_sso_cookies";
ssoRequest.userInterfaceEnabled = NO;
// Set NSURLQueryItems for the MS broker
NSURLQueryItem* ssoUrl = [NSURLQueryItem queryItemWithName:@"sso_url"
value:urispecNSString];
NSURLQueryItem* typesOfHeader =
[NSURLQueryItem queryItemWithName:@"types_of_header" value:@"0"];
NSURLQueryItem* brokerKey = [NSURLQueryItem
queryItemWithName:@"broker_key"
value:@"kSiiehqi0sbYWxT2zOmV-rv8B3QRNsUKcU3YPc122121"];
NSURLQueryItem* protocolVer =
[NSURLQueryItem queryItemWithName:@"msg_protocol_ver" value:@"4"];
ssoRequest.authorizationOptions =
@[ ssoUrl, typesOfHeader, brokerKey, protocolVer ];
if (!ssoRequest) {
return false;
}
mAuthorizationController = [[ASAuthorizationController alloc]
initWithAuthorizationRequests:@[ ssoRequest ]];
if (!mAuthorizationController) {
return false;
}
mRequestDelegate = [[SSORequestDelegate alloc] init];
[mRequestDelegate setCallback:this];
mAuthorizationController.delegate = mRequestDelegate;
[mAuthorizationController performRequests];
// Return true after acknowledging that the delegate will be called
return true;
}
API_AVAILABLE(macos(13.3))
nsresult AddMicrosoftEntraSSO(nsHttpChannel* aChannel,
std::function<void()>&& aResultCallback) {
MOZ_ASSERT(XRE_IsParentProcess());
// The service is used by this function and the delegate, and it should be
// released when the delegate finishes running. It will remain alive even
// after AddMicrosoftEntraSSO returns.
RefPtr<MicrosoftEntraSSOUtils> service =
new MicrosoftEntraSSOUtils(aChannel, std::move(aResultCallback));
mozilla::glean::network_sso::total_entra_uses.Add(1);
if (!service->AddMicrosoftEntraSSOInternal()) {
mozilla::glean::network_sso::entra_success
.Get("invalid_controller_setup"_ns)
.Add(1);
return NS_ERROR_FAILURE;
}
return NS_OK;
}
} // namespace net
} // namespace mozilla
NS_ASSUME_NONNULL_END