Merged PR 13893: Update iOS SDK Sample for new init flow

This change updates the iOS sample to use the new v1.0.0 init flow. This is a fair bit of complicated setup so it is recommended that all app developers try to follow the model laid out by this sample pretty closely. It shows how to synchronize accounts with a token library at startup, perform per account initialization as fast as possible to let incoming notifications be quickly processed and has a reusable manager object that is not tightly coupled to UI.
This commit is contained in:
Brian Bowman 2019-01-29 01:10:36 +00:00
Родитель b65575d12e
Коммит 0cf3484f2e
66 изменённых файлов: 956 добавлений и 4260 удалений

Просмотреть файл

@ -7,10 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
0017D23921E55C6300FFE2A5 /* MSAAccount.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017D22E21E55C6200FFE2A5 /* MSAAccount.m */; };
0017D23A21E55C6300FFE2A5 /* AADAccount.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017D23221E55C6200FFE2A5 /* AADAccount.m */; };
0017D23B21E55C6300FFE2A5 /* MSATokenCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017D23321E55C6200FFE2A5 /* MSATokenCache.m */; };
0017D23C21E55C6300FFE2A5 /* MSATokenRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017D23521E55C6300FFE2A5 /* MSATokenRequest.m */; };
003421A921347622007FC970 /* NotificationsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 003421A821347622007FC970 /* NotificationsManager.m */; };
00823361212F114B0055F6E4 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 00823360212F114B0055F6E4 /* AppDelegate.m */; };
00823364212F114B0055F6E4 /* RootViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 00823363212F114B0055F6E4 /* RootViewController.m */; };
@ -21,20 +17,13 @@
00823375212F114B0055F6E4 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 00823374212F114B0055F6E4 /* main.m */; };
00D6D060213720E5008E5E33 /* NotificationsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 00D6D05F213720E5008E5E33 /* NotificationsViewController.m */; };
86E3681E27114147F58BE403 /* libPods-GraphNotificationsSample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 06928972061C754C239718DC /* libPods-GraphNotificationsSample.a */; };
913BC24121FBCC1600ED6A1C /* AADAccount.m in Sources */ = {isa = PBXBuildFile; fileRef = 913BC23C21FBCC1600ED6A1C /* AADAccount.m */; };
913BC24221FBCC1600ED6A1C /* MSAAccount.m in Sources */ = {isa = PBXBuildFile; fileRef = 913BC23D21FBCC1600ED6A1C /* MSAAccount.m */; };
913BC24321FBCC1600ED6A1C /* MSATokenRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 913BC23E21FBCC1600ED6A1C /* MSATokenRequest.m */; };
913BC24421FBCC1600ED6A1C /* MSATokenCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 913BC23F21FBCC1600ED6A1C /* MSATokenCache.m */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
0017D22E21E55C6200FFE2A5 /* MSAAccount.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSAAccount.m; sourceTree = "<group>"; };
0017D22F21E55C6200FFE2A5 /* MSATokenCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSATokenCache.h; sourceTree = "<group>"; };
0017D23021E55C6200FFE2A5 /* SampleAccountActionFailureReason.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SampleAccountActionFailureReason.h; sourceTree = "<group>"; };
0017D23121E55C6200FFE2A5 /* SingleUserAccountProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SingleUserAccountProvider.h; sourceTree = "<group>"; };
0017D23221E55C6200FFE2A5 /* AADAccount.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AADAccount.m; sourceTree = "<group>"; };
0017D23321E55C6200FFE2A5 /* MSATokenCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSATokenCache.m; sourceTree = "<group>"; };
0017D23421E55C6300FFE2A5 /* MSAAccount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSAAccount.h; sourceTree = "<group>"; };
0017D23521E55C6300FFE2A5 /* MSATokenRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSATokenRequest.m; sourceTree = "<group>"; };
0017D23621E55C6300FFE2A5 /* MSATokenRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSATokenRequest.h; sourceTree = "<group>"; };
0017D23721E55C6300FFE2A5 /* AADAccount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AADAccount.h; sourceTree = "<group>"; };
0017D23821E55C6300FFE2A5 /* SignInAccount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignInAccount.h; sourceTree = "<group>"; };
003421A72130A887007FC970 /* NotificationsManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotificationsManager.h; sourceTree = "<group>"; };
003421A821347622007FC970 /* NotificationsManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationsManager.m; sourceTree = "<group>"; };
0082335C212F114B0055F6E4 /* GraphNotificationsSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GraphNotificationsSample.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -54,6 +43,16 @@
00D6D0642138519C008E5E33 /* GraphNotificationsSample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GraphNotificationsSample.entitlements; sourceTree = "<group>"; };
06928972061C754C239718DC /* libPods-GraphNotificationsSample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-GraphNotificationsSample.a"; sourceTree = BUILT_PRODUCTS_DIR; };
63E3FBAFA254E6B80A8DA76E /* Pods-GraphNotifications.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GraphNotifications.release.xcconfig"; path = "Pods/Target Support Files/Pods-GraphNotifications/Pods-GraphNotifications.release.xcconfig"; sourceTree = "<group>"; };
913BC23521FBCC1600ED6A1C /* SignInAccount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignInAccount.h; sourceTree = "<group>"; };
913BC23621FBCC1600ED6A1C /* SampleAccountActionFailureReason.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SampleAccountActionFailureReason.h; sourceTree = "<group>"; };
913BC23821FBCC1600ED6A1C /* MSAAccount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSAAccount.h; sourceTree = "<group>"; };
913BC23921FBCC1600ED6A1C /* AADAccount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AADAccount.h; sourceTree = "<group>"; };
913BC23B21FBCC1600ED6A1C /* MSATokenCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSATokenCache.h; sourceTree = "<group>"; };
913BC23C21FBCC1600ED6A1C /* AADAccount.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AADAccount.m; sourceTree = "<group>"; };
913BC23D21FBCC1600ED6A1C /* MSAAccount.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSAAccount.m; sourceTree = "<group>"; };
913BC23E21FBCC1600ED6A1C /* MSATokenRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSATokenRequest.m; sourceTree = "<group>"; };
913BC23F21FBCC1600ED6A1C /* MSATokenCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSATokenCache.m; sourceTree = "<group>"; };
913BC24021FBCC1600ED6A1C /* MSATokenRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSATokenRequest.h; sourceTree = "<group>"; };
913EB4AA217E75C700A78C79 /* Secrets.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Secrets.h; sourceTree = "<group>"; };
9B6312F300B55978A0074729 /* libPods-GraphNotifications.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-GraphNotifications.a"; sourceTree = BUILT_PRODUCTS_DIR; };
D94FF25B740D15AF01A2530D /* Pods-GraphNotificationsSample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GraphNotificationsSample.release.xcconfig"; path = "Pods/Target Support Files/Pods-GraphNotificationsSample/Pods-GraphNotificationsSample.release.xcconfig"; sourceTree = "<group>"; };
@ -73,28 +72,10 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0017A49A2135B55400EB86D8 /* SampleAccountProviders */ = {
isa = PBXGroup;
children = (
0017D23721E55C6300FFE2A5 /* AADAccount.h */,
0017D23221E55C6200FFE2A5 /* AADAccount.m */,
0017D23421E55C6300FFE2A5 /* MSAAccount.h */,
0017D22E21E55C6200FFE2A5 /* MSAAccount.m */,
0017D22F21E55C6200FFE2A5 /* MSATokenCache.h */,
0017D23321E55C6200FFE2A5 /* MSATokenCache.m */,
0017D23621E55C6300FFE2A5 /* MSATokenRequest.h */,
0017D23521E55C6300FFE2A5 /* MSATokenRequest.m */,
0017D23021E55C6200FFE2A5 /* SampleAccountActionFailureReason.h */,
0017D23821E55C6300FFE2A5 /* SignInAccount.h */,
0017D23121E55C6200FFE2A5 /* SingleUserAccountProvider.h */,
);
path = SampleAccountProviders;
sourceTree = "<group>";
};
00823353212F114A0055F6E4 = {
isa = PBXGroup;
children = (
0017A49A2135B55400EB86D8 /* SampleAccountProviders */,
913BC23321FBCC1600ED6A1C /* SignInHelpers */,
0082335E212F114B0055F6E4 /* GraphNotificationsSample */,
0082335D212F114B0055F6E4 /* Products */,
D9CF36B91E745B7CB13F10B3 /* Pods */,
@ -143,6 +124,40 @@
name = Frameworks;
sourceTree = "<group>";
};
913BC23321FBCC1600ED6A1C /* SignInHelpers */ = {
isa = PBXGroup;
children = (
913BC23421FBCC1600ED6A1C /* include */,
913BC23A21FBCC1600ED6A1C /* src */,
);
name = SignInHelpers;
path = ../SignInHelpers;
sourceTree = "<group>";
};
913BC23421FBCC1600ED6A1C /* include */ = {
isa = PBXGroup;
children = (
913BC23521FBCC1600ED6A1C /* SignInAccount.h */,
913BC23621FBCC1600ED6A1C /* SampleAccountActionFailureReason.h */,
913BC23821FBCC1600ED6A1C /* MSAAccount.h */,
913BC23921FBCC1600ED6A1C /* AADAccount.h */,
);
path = include;
sourceTree = "<group>";
};
913BC23A21FBCC1600ED6A1C /* src */ = {
isa = PBXGroup;
children = (
913BC23B21FBCC1600ED6A1C /* MSATokenCache.h */,
913BC23C21FBCC1600ED6A1C /* AADAccount.m */,
913BC23D21FBCC1600ED6A1C /* MSAAccount.m */,
913BC23E21FBCC1600ED6A1C /* MSATokenRequest.m */,
913BC23F21FBCC1600ED6A1C /* MSATokenCache.m */,
913BC24021FBCC1600ED6A1C /* MSATokenRequest.h */,
);
path = src;
sourceTree = "<group>";
};
D9CF36B91E745B7CB13F10B3 /* Pods */ = {
isa = PBXGroup;
children = (
@ -166,7 +181,6 @@
00823359212F114B0055F6E4 /* Frameworks */,
0082335A212F114B0055F6E4 /* Resources */,
534E2B6519087712F45067D1 /* [CP] Embed Pods Frameworks */,
C62E42AF81F89136AC56D4FD /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@ -267,22 +281,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n";
showEnvVarsInLog = 0;
};
C62E42AF81F89136AC56D4FD /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-GraphNotificationsSample/Pods-GraphNotificationsSample-resources.sh\"\n";
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
@ -293,15 +292,15 @@
buildActionMask = 2147483647;
files = (
00823375212F114B0055F6E4 /* main.m in Sources */,
913BC24121FBCC1600ED6A1C /* AADAccount.m in Sources */,
00823364212F114B0055F6E4 /* RootViewController.m in Sources */,
003421A921347622007FC970 /* NotificationsManager.m in Sources */,
913BC24421FBCC1600ED6A1C /* MSATokenCache.m in Sources */,
00D6D060213720E5008E5E33 /* NotificationsViewController.m in Sources */,
913BC24221FBCC1600ED6A1C /* MSAAccount.m in Sources */,
00823367212F114B0055F6E4 /* LoginViewController.m in Sources */,
0017D23B21E55C6300FFE2A5 /* MSATokenCache.m in Sources */,
00823361212F114B0055F6E4 /* AppDelegate.m in Sources */,
0017D23C21E55C6300FFE2A5 /* MSATokenRequest.m in Sources */,
0017D23A21E55C6300FFE2A5 /* AADAccount.m in Sources */,
0017D23921E55C6300FFE2A5 /* MSAAccount.m in Sources */,
913BC24321FBCC1600ED6A1C /* MSATokenRequest.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

Просмотреть файл

@ -12,7 +12,6 @@ void uncaughtExceptionHandler(NSException* uncaughtException)
@property (nonatomic) MCDConnectedDevicesAccount* pendingAccount;
@property (nonatomic) void (^pendingCallback)(BOOL,NSError*);
- (void)createNotificationRegistrationWithToken:(NSString* _Nonnull)deviceToken;
- (BOOL)processNotification:(NSDictionary* _Nonnull)userInfo;
@end
@implementation AppDelegate
@ -69,41 +68,6 @@ void uncaughtExceptionHandler(NSException* uncaughtException)
}
}
- (BOOL)processNotification:(NSDictionary* _Nonnull)userInfo
{
@try
{
if ([NSJSONSerialization isValidJSONObject:userInfo])
{
id romeData = userInfo[@"rome-data"];
if ([romeData isKindOfClass:NSDictionary.class])
{
userInfo = romeData;
}
// Forward the notification to CDP.
NSError* error;
NSData* data = [NSJSONSerialization dataWithJSONObject:userInfo options:0 error:&error];
if (data != nil && error == nil)
{
NSString* byteString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
MCDConnectedDevicesProcessNotificationOperation* result = [self.platform processNotification:byteString];
return result.connectedDevicesNotification;
}
}
else
{
NSLog(@"Notification was not valid json! %@", userInfo);
}
}
@catch (NSException* e)
{
NSLog(@"GraphNotifications Error. Processing notification failed.");
}
return NO;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
@ -137,15 +101,8 @@ void uncaughtExceptionHandler(NSException* uncaughtException)
}
else
{
@try
{
// app run in background and received the push notification, app is launched by user tapping the alert view
[self processNotification:userInfo];
}
@catch(NSException* exception)
{
NSLog(@"GraphNotifications Failed start up notification with exception %@", exception);
}
// app run in background and received the push notification, app is launched by user tapping the alert view
[self.platform processNotification:userInfo];
}
return YES;
}
@ -221,15 +178,11 @@ didRegisterUserNotificationSettings:(__unused UIUserNotificationSettings*)notifi
NSLog(@"GraphNotifications Received remote notification...");
[userInfo enumerateKeysAndObjectsUsingBlock:^(
id _Nonnull key, id _Nonnull obj, __unused BOOL* _Nonnull stop) { NSLog(@"%@: %@", key, obj); }];
@try
MCDConnectedDevicesProcessNotificationOperation* operation = [self.platform processNotification:userInfo];
if (!operation.connectedDevicesNotification)
{
if (![self processNotification:userInfo])
{
NSLog(@"GraphNotifications Received notification was not for Rome");
}
}
@catch(NSException* exception) {
NSLog(@"GraphNotifications Failed to receive notification with exception %@", exception);
NSLog(@"GraphNotifications Received notification was not for Rome");
}
}

Просмотреть файл

@ -3,8 +3,8 @@
#import <Foundation/Foundation.h>
#import <ConnectedDevices/ConnectedDevices.h>
#import <ConnectedDevices/ConnectedDevicesUserData/ConnectedDevicesUserData.h>
#import <ConnectedDevices/ConnectedDevicesUserDataUserNotifications/ConnectedDevicesUserDataUserNotifications.h>
#import <ConnectedDevicesUserData/ConnectedDevicesUserData.h>
#import <ConnectedDevicesUserDataUserNotifications/ConnectedDevicesUserDataUserNotifications.h>
#import "AADAccount.h"
#import "MSAAccount.h"

Просмотреть файл

@ -6,7 +6,9 @@ target 'GraphNotificationsSample' do
# Uncomment the next line if you're using Swift or would like to use dynamic frameworks
# use_frameworks!
# This is the main ConnectedDevices SDK. It includes all the functionality needed for RemoteSystems and UserData scenarios
pod 'ProjectRomeSdk'
pod 'AdaptiveCards'
pod 'ADAL', '2.6.6'

Просмотреть файл

@ -1,17 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#pragma once
#import <ConnectedDevices/ConnectedDevices.h>
#import "SampleAccountActionFailureReason.h"
// @brief Protocol for a MCDUserAccountProvider that supports logging into/out of a single user account.
@protocol SingleUserAccountProvider
- (void)signInWithCompletionCallback:(nonnull SampleAccountProviderCompletionBlock)callback;
- (void)signOutWithCompletionCallback:(nonnull SampleAccountProviderCompletionBlock)callback;
@property(readonly, atomic) BOOL signedIn;
@end

Просмотреть файл

@ -1,13 +1,33 @@
# Uncomment the next line to define a global platform for your project
platform :ios, "10.0"
workspace 'iOSSample'
project 'iOSSample.xcodeproj'
target 'iOSSample' do
# Uncomment the next line if you're using Swift or would like to use dynamic frameworks
# use_frameworks!
use_frameworks!
pod 'ProjectRomeSdk'
# This is the main ConnectedDevices SDK. It includes all the functionality needed for RemoteSystems and UserData scenarios
pod 'ProjectRomeSdk'
# Because the ConnectedDevices platform involves many asynchronous calls (getting user tokens, sending messages to remote apps etc.)
# it is helpful to have a strategy to sequence all of completions in a sane manner.
pod 'PromiseKit'
# Pods for iOSSample
# To help authenticate users, the ADAL library is used
pod 'ADAL', '2.6.6'
# force the sub specs in the array below to use swift version 3.2
# https://github.com/mxcl/PromiseKit/issues/722
post_install do |installer|
installer.pods_project.targets.each do |target|
if ['PromiseKit'].include? target.name
target.build_configurations.each do |config|
config.build_settings['SWIFT_VERSION'] = '4.2'
end
end
end
end
end

Просмотреть файл

@ -9,7 +9,6 @@
/* Begin PBXBuildFile section */
514E375D209D7D5A000FFC17 /* AppServiceProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 514E375B209D7D5A000FFC17 /* AppServiceProvider.m */; };
514E3763209D9594000FFC17 /* InboundRequestLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 514E3762209D9594000FFC17 /* InboundRequestLogger.m */; };
8200D3A82098E2CA00D43FA6 /* NotificationProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 8200D3A72098E2CA00D43FA6 /* NotificationProvider.m */; };
8200D3AB209A3B3900D43FA6 /* AppServiceViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8200D3AA209A3B3900D43FA6 /* AppServiceViewController.m */; };
8288D5282069A64100EAABAA /* main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8288D5272069A64000EAABAA /* main.storyboard */; };
8288D5692069A6D100EAABAA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8288D5382069A6CD00EAABAA /* Assets.xcassets */; };
@ -18,17 +17,18 @@
8288D56C2069A6D100EAABAA /* MainNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8288D53B2069A6CD00EAABAA /* MainNavigationController.m */; };
8288D56D2069A6D100EAABAA /* SdkViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8288D53C2069A6CD00EAABAA /* SdkViewController.m */; };
8288D57B2069A6D100EAABAA /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 8288D55A2069A6CD00EAABAA /* Info.plist */; };
8288D57C2069A6D100EAABAA /* AppDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 8288D5612069A6CF00EAABAA /* AppDataSource.m */; };
8288D57D2069A6D100EAABAA /* RemoteSystemViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8288D5622069A6CF00EAABAA /* RemoteSystemViewController.m */; };
8288D57E2069A6D100EAABAA /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 8288D5642069A6CF00EAABAA /* AppDelegate.m */; };
8288D5802069A6D100EAABAA /* IdentityViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8288D5662069A6D000EAABAA /* IdentityViewController.m */; };
8288D5832069A72800EAABAA /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 8288D5822069A72800EAABAA /* main.m */; };
8288D5952069A9AA00EAABAA /* MSATokenCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 8288D58E2069A9A900EAABAA /* MSATokenCache.m */; };
8288D5962069A9AA00EAABAA /* MSATokenRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8288D5902069A9A900EAABAA /* MSATokenRequest.m */; };
8288D5992069A9AA00EAABAA /* MSAAccountProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 8288D5932069A9A900EAABAA /* MSAAccountProvider.m */; };
8288D59F206AD47A00EAABAA /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8288D59E206AD47A00EAABAA /* Launch Screen.storyboard */; };
913BC22F21FB917A00ED6A1C /* AADAccount.m in Sources */ = {isa = PBXBuildFile; fileRef = 913BC22A21FB917A00ED6A1C /* AADAccount.m */; };
913BC23021FB917A00ED6A1C /* MSAAccount.m in Sources */ = {isa = PBXBuildFile; fileRef = 913BC22B21FB917A00ED6A1C /* MSAAccount.m */; };
913BC23121FB917A00ED6A1C /* MSATokenRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 913BC22C21FB917A00ED6A1C /* MSATokenRequest.m */; };
913BC23221FB917A00ED6A1C /* MSATokenCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 913BC22D21FB917A00ED6A1C /* MSATokenCache.m */; };
91943C7021F14B1100548917 /* ConnectedDevicesPlatformManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 91943C6F21F14B1100548917 /* ConnectedDevicesPlatformManager.m */; };
DE544A09209D69E700305AE0 /* LaunchUriProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = DE544A08209D69E700305AE0 /* LaunchUriProvider.m */; };
FA08AD9AF324F8DA487BAD8B /* libPods-iOSSample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 871CA6245BA8A2DED6871921 /* libPods-iOSSample.a */; };
E92438C142A5C14854D43DFD /* Pods_iOSSample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E0638CBD9D25C49221F36983 /* Pods_iOSSample.framework */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@ -50,7 +50,6 @@
514E3761209D9594000FFC17 /* InboundRequestLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InboundRequestLogger.h; sourceTree = "<group>"; };
514E3762209D9594000FFC17 /* InboundRequestLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InboundRequestLogger.m; sourceTree = "<group>"; };
5C5CDD988DA7BCECE32F5F3F /* Pods-iOSSample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOSSample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-iOSSample/Pods-iOSSample.debug.xcconfig"; sourceTree = "<group>"; };
8200D3A72098E2CA00D43FA6 /* NotificationProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NotificationProvider.m; path = iOSSample/NotificationProvider.m; sourceTree = SOURCE_ROOT; };
8200D3A9209A243000D43FA6 /* AppServiceViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppServiceViewController.h; sourceTree = "<group>"; };
8200D3AA209A3B3900D43FA6 /* AppServiceViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppServiceViewController.m; sourceTree = "<group>"; };
8288D50E2069A61A00EAABAA /* iOSSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOSSample.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -67,32 +66,29 @@
8288D55B2069A6CD00EAABAA /* IdentityViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IdentityViewController.h; sourceTree = "<group>"; };
8288D55E2069A6CE00EAABAA /* UserActivityViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserActivityViewController.h; sourceTree = "<group>"; };
8288D55F2069A6CE00EAABAA /* SdkViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SdkViewController.h; sourceTree = "<group>"; };
8288D5602069A6CE00EAABAA /* NotificationProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NotificationProvider.h; sourceTree = "<group>"; };
8288D5612069A6CF00EAABAA /* AppDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDataSource.m; sourceTree = "<group>"; };
8288D5622069A6CF00EAABAA /* RemoteSystemViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RemoteSystemViewController.m; sourceTree = "<group>"; };
8288D5632069A6CF00EAABAA /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
8288D5642069A6CF00EAABAA /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
8288D5662069A6D000EAABAA /* IdentityViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IdentityViewController.m; sourceTree = "<group>"; };
8288D5672069A6D000EAABAA /* AppDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDataSource.h; sourceTree = "<group>"; };
8288D5822069A72800EAABAA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
8288D5892069A99D00EAABAA /* SampleAccountActionFailureReason.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SampleAccountActionFailureReason.h; sourceTree = "<group>"; };
8288D58A2069A99D00EAABAA /* SingleUserAccountProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SingleUserAccountProvider.h; sourceTree = "<group>"; };
8288D58B2069A99D00EAABAA /* AADMSAAccountProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AADMSAAccountProvider.h; sourceTree = "<group>"; };
8288D58C2069A99D00EAABAA /* AADAccountProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AADAccountProvider.h; sourceTree = "<group>"; };
8288D58D2069A99D00EAABAA /* MSAAccountProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSAAccountProvider.h; sourceTree = "<group>"; };
8288D58E2069A9A900EAABAA /* MSATokenCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSATokenCache.m; sourceTree = "<group>"; };
8288D58F2069A9A900EAABAA /* MSATokenRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSATokenRequest.h; sourceTree = "<group>"; };
8288D5902069A9A900EAABAA /* MSATokenRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSATokenRequest.m; sourceTree = "<group>"; };
8288D5912069A9A900EAABAA /* AADAccountProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AADAccountProvider.m; sourceTree = "<group>"; };
8288D5922069A9A900EAABAA /* AADMSAAccountProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AADMSAAccountProvider.m; sourceTree = "<group>"; };
8288D5932069A9A900EAABAA /* MSAAccountProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSAAccountProvider.m; sourceTree = "<group>"; };
8288D5942069A9A900EAABAA /* MSATokenCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSATokenCache.h; sourceTree = "<group>"; };
8288D59E206AD47A00EAABAA /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = "<group>"; };
871CA6245BA8A2DED6871921 /* libPods-iOSSample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-iOSSample.a"; sourceTree = BUILT_PRODUCTS_DIR; };
912F2CD021F79A380094B6A1 /* ConnectedDevicesPlatformManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ConnectedDevicesPlatformManager.h; sourceTree = "<group>"; };
913BC22321FB917A00ED6A1C /* SignInAccount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignInAccount.h; sourceTree = "<group>"; };
913BC22421FB917A00ED6A1C /* SampleAccountActionFailureReason.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SampleAccountActionFailureReason.h; sourceTree = "<group>"; };
913BC22621FB917A00ED6A1C /* MSAAccount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSAAccount.h; sourceTree = "<group>"; };
913BC22721FB917A00ED6A1C /* AADAccount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AADAccount.h; sourceTree = "<group>"; };
913BC22921FB917A00ED6A1C /* MSATokenCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSATokenCache.h; sourceTree = "<group>"; };
913BC22A21FB917A00ED6A1C /* AADAccount.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AADAccount.m; sourceTree = "<group>"; };
913BC22B21FB917A00ED6A1C /* MSAAccount.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSAAccount.m; sourceTree = "<group>"; };
913BC22C21FB917A00ED6A1C /* MSATokenRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSATokenRequest.m; sourceTree = "<group>"; };
913BC22D21FB917A00ED6A1C /* MSATokenCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSATokenCache.m; sourceTree = "<group>"; };
913BC22E21FB917A00ED6A1C /* MSATokenRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSATokenRequest.h; sourceTree = "<group>"; };
913EB4A8217E722000A78C79 /* Secrets.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Secrets.h; sourceTree = "<group>"; };
91943C6F21F14B1100548917 /* ConnectedDevicesPlatformManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ConnectedDevicesPlatformManager.m; sourceTree = "<group>"; };
CC70B68D1BC172939DC18E64 /* Pods-iOSSample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOSSample.release.xcconfig"; path = "Pods/Target Support Files/Pods-iOSSample/Pods-iOSSample.release.xcconfig"; sourceTree = "<group>"; };
DE544A07209D69E700305AE0 /* LaunchUriProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LaunchUriProvider.h; sourceTree = "<group>"; };
DE544A08209D69E700305AE0 /* LaunchUriProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LaunchUriProvider.m; sourceTree = "<group>"; };
E0638CBD9D25C49221F36983 /* Pods_iOSSample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iOSSample.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -100,7 +96,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
FA08AD9AF324F8DA487BAD8B /* libPods-iOSSample.a in Frameworks */,
E92438C142A5C14854D43DFD /* Pods_iOSSample.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -110,7 +106,7 @@
2FA0F9B8E1A5FE221268DAEA /* Frameworks */ = {
isa = PBXGroup;
children = (
871CA6245BA8A2DED6871921 /* libPods-iOSSample.a */,
E0638CBD9D25C49221F36983 /* Pods_iOSSample.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -136,11 +132,9 @@
8288D5102069A61A00EAABAA /* iOSSample */ = {
isa = PBXGroup;
children = (
913BC22121FB917A00ED6A1C /* SignInHelpers */,
514E3755209D0FC9000FFC17 /* iOSSample.entitlements */,
8288D5842069A72E00EAABAA /* Supporting Files */,
8288D5292069A66A00EAABAA /* Auth Provider */,
8288D5672069A6D000EAABAA /* AppDataSource.h */,
8288D5612069A6CF00EAABAA /* AppDataSource.m */,
8288D5632069A6CF00EAABAA /* AppDelegate.h */,
8288D5642069A6CF00EAABAA /* AppDelegate.m */,
514E375C209D7D5A000FFC17 /* AppServiceProvider.h */,
@ -157,8 +151,6 @@
DE544A08209D69E700305AE0 /* LaunchUriProvider.m */,
8288D5362069A6CC00EAABAA /* MainNavigationController.h */,
8288D53B2069A6CD00EAABAA /* MainNavigationController.m */,
8288D5602069A6CE00EAABAA /* NotificationProvider.h */,
8200D3A72098E2CA00D43FA6 /* NotificationProvider.m */,
8288D5372069A6CC00EAABAA /* RemoteSystemViewController.h */,
8288D5622069A6CF00EAABAA /* RemoteSystemViewController.m */,
8288D55F2069A6CE00EAABAA /* SdkViewController.h */,
@ -166,21 +158,12 @@
8288D55E2069A6CE00EAABAA /* UserActivityViewController.h */,
8288D5392069A6CD00EAABAA /* UserActivityViewController.m */,
913EB4A8217E722000A78C79 /* Secrets.h */,
91943C6F21F14B1100548917 /* ConnectedDevicesPlatformManager.m */,
912F2CD021F79A380094B6A1 /* ConnectedDevicesPlatformManager.h */,
);
path = iOSSample;
sourceTree = "<group>";
};
8288D5292069A66A00EAABAA /* Auth Provider */ = {
isa = PBXGroup;
children = (
8288D59B2069A9D100EAABAA /* MSA */,
8288D59A2069A9CA00EAABAA /* AAD */,
8288D5892069A99D00EAABAA /* SampleAccountActionFailureReason.h */,
8288D58A2069A99D00EAABAA /* SingleUserAccountProvider.h */,
);
path = "Auth Provider";
sourceTree = "<group>";
};
8288D5842069A72E00EAABAA /* Supporting Files */ = {
isa = PBXGroup;
children = (
@ -193,28 +176,38 @@
name = "Supporting Files";
sourceTree = "<group>";
};
8288D59A2069A9CA00EAABAA /* AAD */ = {
913BC22121FB917A00ED6A1C /* SignInHelpers */ = {
isa = PBXGroup;
children = (
8288D58C2069A99D00EAABAA /* AADAccountProvider.h */,
8288D5912069A9A900EAABAA /* AADAccountProvider.m */,
8288D58B2069A99D00EAABAA /* AADMSAAccountProvider.h */,
8288D5922069A9A900EAABAA /* AADMSAAccountProvider.m */,
913BC22221FB917A00ED6A1C /* include */,
913BC22821FB917A00ED6A1C /* src */,
);
name = AAD;
name = SignInHelpers;
path = ../../SignInHelpers;
sourceTree = "<group>";
};
8288D59B2069A9D100EAABAA /* MSA */ = {
913BC22221FB917A00ED6A1C /* include */ = {
isa = PBXGroup;
children = (
8288D58D2069A99D00EAABAA /* MSAAccountProvider.h */,
8288D5932069A9A900EAABAA /* MSAAccountProvider.m */,
8288D5942069A9A900EAABAA /* MSATokenCache.h */,
8288D58E2069A9A900EAABAA /* MSATokenCache.m */,
8288D58F2069A9A900EAABAA /* MSATokenRequest.h */,
8288D5902069A9A900EAABAA /* MSATokenRequest.m */,
913BC22321FB917A00ED6A1C /* SignInAccount.h */,
913BC22421FB917A00ED6A1C /* SampleAccountActionFailureReason.h */,
913BC22621FB917A00ED6A1C /* MSAAccount.h */,
913BC22721FB917A00ED6A1C /* AADAccount.h */,
);
name = MSA;
path = include;
sourceTree = "<group>";
};
913BC22821FB917A00ED6A1C /* src */ = {
isa = PBXGroup;
children = (
913BC22921FB917A00ED6A1C /* MSATokenCache.h */,
913BC22A21FB917A00ED6A1C /* AADAccount.m */,
913BC22B21FB917A00ED6A1C /* MSAAccount.m */,
913BC22C21FB917A00ED6A1C /* MSATokenRequest.m */,
913BC22D21FB917A00ED6A1C /* MSATokenCache.m */,
913BC22E21FB917A00ED6A1C /* MSATokenRequest.h */,
);
path = src;
sourceTree = "<group>";
};
DA0D6DB22790D58322F34C05 /* Pods */ = {
@ -261,6 +254,9 @@
8288D50D2069A61A00EAABAA = {
CreatedOnToolsVersion = 9.3;
SystemCapabilities = {
com.apple.BackgroundModes = {
enabled = 1;
};
com.apple.Push = {
enabled = 1;
};
@ -326,11 +322,15 @@
);
inputPaths = (
"${SRCROOT}/Pods/Target Support Files/Pods-iOSSample/Pods-iOSSample-frameworks.sh",
"${BUILT_PRODUCTS_DIR}/ADAL/ADAL.framework",
"${PODS_ROOT}/ProjectRomeSdk/full/ConnectedDevices.framework",
"${BUILT_PRODUCTS_DIR}/PromiseKit/PromiseKit.framework",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ADAL.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ConnectedDevices.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PromiseKit.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@ -346,18 +346,18 @@
files = (
8288D57E2069A6D100EAABAA /* AppDelegate.m in Sources */,
8288D56D2069A6D100EAABAA /* SdkViewController.m in Sources */,
8288D57C2069A6D100EAABAA /* AppDataSource.m in Sources */,
514E3763209D9594000FFC17 /* InboundRequestLogger.m in Sources */,
8200D3A82098E2CA00D43FA6 /* NotificationProvider.m in Sources */,
DE544A09209D69E700305AE0 /* LaunchUriProvider.m in Sources */,
913BC23021FB917A00ED6A1C /* MSAAccount.m in Sources */,
8288D5802069A6D100EAABAA /* IdentityViewController.m in Sources */,
913BC23221FB917A00ED6A1C /* MSATokenCache.m in Sources */,
8288D57D2069A6D100EAABAA /* RemoteSystemViewController.m in Sources */,
913BC23121FB917A00ED6A1C /* MSATokenRequest.m in Sources */,
8288D56C2069A6D100EAABAA /* MainNavigationController.m in Sources */,
8288D5992069A9AA00EAABAA /* MSAAccountProvider.m in Sources */,
514E375D209D7D5A000FFC17 /* AppServiceProvider.m in Sources */,
8288D5952069A9AA00EAABAA /* MSATokenCache.m in Sources */,
913BC22F21FB917A00ED6A1C /* AADAccount.m in Sources */,
91943C7021F14B1100548917 /* ConnectedDevicesPlatformManager.m in Sources */,
8288D56B2069A6D100EAABAA /* LaunchAndMessageViewController.m in Sources */,
8288D5962069A9AA00EAABAA /* MSATokenRequest.m in Sources */,
8288D56A2069A6D100EAABAA /* UserActivityViewController.m in Sources */,
8200D3AB209A3B3900D43FA6 /* AppServiceViewController.m in Sources */,
8288D5832069A72800EAABAA /* main.m in Sources */,
@ -399,7 +399,8 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@ -457,7 +458,8 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
@ -483,8 +485,9 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = iOSSample/iOSSample.entitlements;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = SX87JX3VC9;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = 9UFF8D37DM;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -492,9 +495,13 @@
);
INFOPLIST_FILE = iOSSample/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.OneRomanApp;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.OneSDKSample;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "Public sample for OneSDK";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@ -505,8 +512,9 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = iOSSample/iOSSample.entitlements;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = SX87JX3VC9;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = 9UFF8D37DM;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -514,9 +522,13 @@
);
INFOPLIST_FILE = iOSSample/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.OneRomanApp;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.OneSDKSample;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "Public sample for OneSDK";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;

Просмотреть файл

@ -1,19 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#pragma once
#import "InboundRequestLogger.h"
#import "MSAAccountProvider.h"
#import "NotificationProvider.h"
#import <ConnectedDevices/Core/MCDPlatform.h>
#import <Foundation/Foundation.h>
@interface AppDataSource : NSObject
+ (AppDataSource*)sharedInstance;
@property(nonatomic) NotificationProvider* notificationProvider;
@property(nonatomic) MSAAccountProvider* accountProvider;
@property(nonatomic) InboundRequestLogger* inboundRequestLogger;
@property(nonatomic) MCDPlatform* platform;
@end

Просмотреть файл

@ -1,40 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#import "AppDataSource.h"
#import "Secrets.h"
@implementation AppDataSource
+ (AppDataSource*)sharedInstance
{
static dispatch_once_t onceToken;
static AppDataSource* sharedInstance;
dispatch_once(&onceToken, ^{ sharedInstance = [[AppDataSource alloc] init]; });
return sharedInstance;
}
- (instancetype)init
{
if (self = [super init])
{
_notificationProvider = [NotificationProvider new];
// You will need a valid clientId to initialize the platform
// Register your app with Microsoft - https://apps.dev.microsoft.com/ to get clientId
// The platform requires a valid OAuth token to initialize
// This sample provides the source files that are used to acquired the token under the 'Auth Provider' directory
// The only requirement is to obtain a valid OAuth token
// This sample shows one way it can be done; we are giving you the option to use the sample auth code or use your own
// scopeOverrides allows you to override scopes that are requested by the auth provider. Apps do not normally need to override scopes.
NSDictionary<NSString*, NSArray<NSString*>*>* scopeOverrides = @{};
_accountProvider = [[MSAAccountProvider alloc] initWithClientId:CLIENT_ID scopeOverrides:scopeOverrides];
_inboundRequestLogger = [InboundRequestLogger new];
}
return self;
}
@end

Просмотреть файл

@ -2,8 +2,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#import "AppDataSource.h"
#import <ConnectedDevices/Core/MCDPlatform.h>
#import <UIKit/UIKit.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate>

Просмотреть файл

@ -3,12 +3,21 @@
//
#import "AppDelegate.h"
#import "AppDataSource.h"
#import <ConnectedDevices/RemoteSystems.Commanding/RemoteSystems.Commanding.h>
#import <ConnectedDevices/Core/MCDNotificationRegistration.h>
#import <ConnectedDevices/Core/MCDPlatform.h>
#import "ConnectedDevicesPlatformManager.h"
#import <ConnectedDevicesRemoteSystemsCommanding/ConnectedDevicesRemoteSystemsCommanding.h>
#import <ConnectedDevices/MCDConnectedDevicesNotificationRegistration.h>
#import <ConnectedDevices/MCDConnectedDevicesPlatform.h>
@implementation AppDelegate
@implementation AppDelegate {
ConnectedDevicesPlatformManager* _platformManager;
}
-(instancetype)init {
if (self = [super init]) {
_platformManager = [ConnectedDevicesPlatformManager sharedInstance];
}
return self;
}
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
@ -23,8 +32,10 @@
}
else
{
// app run in background and received the push notification, app is launched by user tapping the alert view
[MCDNotificationReceiver receiveNotification:notificationInfo];
// Once all accounts that are in good standing have their subcomponents initialized, its safe to pump the notification information into the platform. Before that point, a notification
// may be for an account that isn't fully set up yet. This is more likely to happen when the app is launched as a result of the notification so there
// isn't much time to start the platform before needing to process the notification.
[_platformManager.platform processNotification:notificationInfo];
}
return YES;
@ -46,16 +57,8 @@
{
[deviceTokenStr appendFormat:@"%02X", (unsigned int)byteBuffer[i]];
}
NSLog(@"APNs token: %@", deviceTokenStr);
// invoke notificationProvider with new notification registration
[[AppDataSource sharedInstance].notificationProvider
updateNotificationRegistration:[MCDNotificationRegistration
registrationWithNotificationType:MCDNotificationTypeAPN
token:deviceTokenStr
appId:[[NSBundle mainBundle] bundleIdentifier]
appDisplayName:(NSString*)[[NSBundle mainBundle]
objectForInfoDictionaryKey:@"CFBundleDisplayName"]]];
NSLog(@"APNS token: %@", deviceTokenStr);
[_platformManager setNotificationRegistration:deviceTokenStr];
}
- (void)applicationWillResignActive:(__unused UIApplication*)application
@ -67,18 +70,29 @@
// pause the game.
}
- (void)application:(__unused UIApplication*)application didReceiveRemoteNotification:(nonnull NSDictionary*)notificationInfo
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notificationInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler
{
// app run in foreground and received the push notification, pump notification into CDPPlatform
NSLog(@"Received remote notification...");
[notificationInfo enumerateKeysAndObjectsUsingBlock:^(
id _Nonnull key, id _Nonnull obj, __unused BOOL* _Nonnull stop) { NSLog(@"%@: %@", key, obj); }];
if (![MCDNotificationReceiver receiveNotification:notificationInfo])
{
NSLog(@"Received notification was not for Rome");
}
// Once all accounts that are in good standing have their subcomponents initialized, its safe to pump the notification information into the platform. Before that point, a notification
// may be for an account that isn't fully set up yet. This is more likely to happen when the app is launched as a result of the notification so there
// isn't much time to start the platform before needing to process the notification.
MCDConnectedDevicesProcessNotificationOperation* processOperation = [_platformManager.platform processNotification:notificationInfo];
[AnyPromise promiseWithAdapterBlock:^(PMKAdapter _Nonnull adapter) {
[processOperation waitForCompletionAsync:^(NSError* error){
adapter(nil, error);
}];
}].then(^{
completionHandler(UIBackgroundFetchResultNewData);
}).catch(^{
completionHandler(UIBackgroundFetchResultNoData);
});
}
- (void)applicationDidEnterBackground:(__unused UIApplication*)application
{
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to
@ -101,8 +115,6 @@
- (void)applicationWillTerminate:(__unused UIApplication*)application
{
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
// PlatformManager* platformManager = [PlatformManager sharedInstance];
//[platformManager shutdownPlatform];
}
void uncaughtExceptionHandler(NSException* uncaughtException)

Просмотреть файл

@ -4,7 +4,7 @@
#pragma once
#import <ConnectedDevices/RemoteSystems.Commanding/RemoteSystems.Commanding.h>
#import <ConnectedDevicesRemoteSystemsCommanding/ConnectedDevicesRemoteSystemsCommanding.h>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

Просмотреть файл

@ -3,7 +3,8 @@
//
#import "AppServiceViewController.h"
#import "AppDataSource.h"
#import "ConnectedDevicesPlatformManager.h"
#import "InboundRequestLogger.h"
#import <Foundation/Foundation.h>
@implementation AppServiceViewController
@ -12,12 +13,12 @@
{
[super viewDidLoad];
_messages.text = [AppDataSource sharedInstance].inboundRequestLogger.log;
_messages.text = [InboundRequestLogger sharedInstance].log;
if ([_messages.text length] == 0)
{
_messages.text = @"No message received yet";
}
[AppDataSource sharedInstance].inboundRequestLogger.delegate = self;
[InboundRequestLogger sharedInstance].delegate = self;
}
#pragma mark - InboundRequestLogger Delegate

Просмотреть файл

@ -1,25 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#pragma once
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <ConnectedDevices/Core/Core.h>
#import "SingleUserAccountProvider.h"
// @brief MCDUserAccountProvider that performs a log in/out flow using ADAL.
// Supports a single AAD user account.
// For getAccessTokenForUserAccountIdAsync: and onAccessTokenError:, because of ADAL limitations, only the first scope in scopes[] is used
@interface AADAccountProvider : NSObject <SingleUserAccountProvider>
// @brief clientId is a guid from the app's registration in the azure portal
// redirectUri is a Uri specified in the same portal
- (nullable instancetype)initWithClientId:(nonnull NSString*)clientId redirectUri:(nonnull NSURL*)redirectUri;
@property(readonly, nonatomic, copy, nonnull) NSString* clientId;
@property(readonly, nonatomic, copy, nonnull) NSURL* redirectUri;
@end

Просмотреть файл

@ -1,329 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#import "AADAccountProvider.h"
#import <ADAL/ADAL.h>
static NSString* const AADAccountProviderExceptionName = @"AADAccountProviderException";
/**
* Notes about AAD/ADAL:
* - Resource An Azure web service/app, such as https://graph.windows.net, or a CDP service.
* - Scope Individual permissions within a resource
* - Access Token A standard JSON web token for a given scope.
* This is the actual token/user ticket used to authenticate with CDP services.
* https://oauth.net/2/
* https://www.oauth.com/oauth2-servers/access-tokens/
* - Refresh token: A standard OAuth refresh token.
* Lasts longer than access tokens, and is used to request new access tokens/refresh access tokens when they expire.
* ADAL manages this automatically.
* https://oauth.net/2/grant-types/refresh-token/
* - MRRT Multiresource refresh token. A refresh token that can be used to fetch access tokens for more than one resource.
* Getting one requires the user consent to all the covered resources. ADAL manages this automatically.
*/
@interface AADAccountProvider ()
{
ADAuthenticationContext* _authContext;
ADTokenCacheItem* _tokenCacheItem;
}
@end
@implementation AADAccountProvider
@synthesize userAccountChanged = _userAccountChanged;
- (instancetype)initWithClientId:(NSString*)clientId redirectUri:(NSURL*)redirectUri
{
if (self = [super init])
{
_clientId = [clientId copy];
_redirectUri = [redirectUri copy];
_userAccountChanged = [MCDUserAccountChangedEvent new];
#if TARGET_OS_IPHONE
// Don't share token cache between applications, only need them to be cached for this application
// Without this, the MRRT is not cached, and the acquireTokenSilentWithResource: in getAccessToken
// always fails with AD_ERROR_SERVER_USER_INPUT_NEEDED
[[ADAuthenticationSettings sharedInstance] setDefaultKeychainGroup:nil];
#endif
ADAuthenticationError* error = nil;
_authContext =
[ADAuthenticationContext authenticationContextWithAuthority:@"https://login.microsoftonline.com/common" error:&error];
if (error)
{
NSLog(@"Error creating ADAuthenticationContext for AADAccountProvider: %@.", error);
return nil;
}
NSLog(@"Checking if previous AADAccountProvider session can be loaded...");
#if TARGET_OS_IPHONE
NSArray<ADTokenCacheItem*>* tokenCacheItems = [[ADKeychainTokenCache defaultKeychainCache] allItems:nil];
#else
NSArray<ADTokenCacheItem*>* tokenCacheItems = [[ADTokenCache defaultCache] allItems:nil];
#endif
if (tokenCacheItems.count > 0)
{
for (ADTokenCacheItem* item in tokenCacheItems)
{
if (item.isMultiResourceRefreshToken && [_clientId isEqualToString:item.clientId])
{
_tokenCacheItem = item;
break;
}
}
if (_tokenCacheItem)
{
NSLog(@"Loaded previous AADAccountProvider session, starting as signed in.");
}
else
{
NSLog(@"No previous AADAccountProvider session could be loaded, starting as signed out.");
}
}
}
return self;
}
- (void)_raiseAccountChangedEvent
{
NSLog(@"Raise Account changed event");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// fire event on a different thread
[self.userAccountChanged raise];
});
}
- (BOOL)signedIn
{
@synchronized(self)
{
return _tokenCacheItem != nil;
}
}
- (void)signInWithCompletionCallback:(SampleAccountProviderCompletionBlock)callback
{
if (self.signedIn)
{
callback(NO, SampleAccountActionFailureReasonAlreadySignedIn);
return;
}
// If the user has not previously consented for this default resource for this app,
// the interactive flow will ask for user consent for all resources used by the app.
// If the user previously consented to this resource on this app, and more resources are added to the app later on,
// a consent prompt for all app resources will be raised when an access token for a new resource is requested -
// see getAccessTokenForUserAccountIdAsync:
NSString* defaultResource = @"https://graph.windows.net";
[_authContext acquireTokenWithResource:defaultResource
clientId:_clientId
redirectUri:_redirectUri
completionBlock:^(ADAuthenticationResult* result) {
switch (result.status)
{
case AD_SUCCEEDED:
{
@synchronized(self)
{
_tokenCacheItem = result.tokenCacheItem;
}
[self _raiseAccountChangedEvent];
callback(YES, SampleAccountActionNoFailure);
break;
}
case AD_USER_CANCELLED:
{
callback(NO, SampleAccountActionFailureReasonUserCancelled);
break;
}
case AD_FAILED:
default:
{
NSLog(@"Error occurred in ADAL when signing in to an AAD account. Status: %u, Error: %@", result.status,
result.error);
callback(NO, SampleAccountActionFailureReasonADAL);
break;
}
}
}];
}
- (void)signOutWithCompletionCallback:(SampleAccountProviderCompletionBlock)callback
{
@synchronized(self)
{
if (!self.signedIn)
{
callback(NO, SampleAccountActionFailureReasonAlreadySignedOut);
return;
}
ADAuthenticationError* error;
#if TARGET_OS_IPHONE
BOOL removed = [[ADKeychainTokenCache defaultKeychainCache] removeAllForClientId:_clientId error:&error];
#else
// The above convenience method does not exist on OSX
BOOL removed;
NSArray<ADTokenCacheItem*>* tokenCacheItems = [[ADTokenCache defaultCache] allItems:&error];
if (!error)
{
for (ADTokenCacheItem* item in tokenCacheItems)
{
if ([item.clientId isEqualToString:_clientId])
{
removed = [[ADTokenCache defaultCache] removeItem:item error:&error];
if (!removed || error)
{
break;
}
}
}
}
#endif
if (!removed || error)
{
NSLog(@"Failed to remove token from ADAL cache, error %@", error);
callback(NO, SampleAccountActionFailureReasonADAL);
return;
}
// Delete cookies
NSArray<NSString*>* cookieNamesToDelete =
@[ @"SignInStateCookie", @"ESTSAUTHPERSISTENT", @"ESTSAUTHLIGHT", @"ESTSAUTH", @"ESTSSC" ];
NSHTTPCookieStorage* cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie* cookie in [cookieJar cookies])
{
if ([cookieNamesToDelete containsObject:cookie.name])
{
[cookieJar deleteCookie:cookie];
}
}
_tokenCacheItem = nil;
}
[self _raiseAccountChangedEvent];
callback(YES, SampleAccountActionNoFailure);
}
- (void)getAccessTokenForUserAccountIdAsync:(NSString*)accountId
scopes:(NSArray<NSString*>*)scopes
completion:(void (^)(MCDAccessTokenResult*, NSError*))completionBlock
{
@synchronized(self)
{
if (!self.signedIn || ![accountId isEqualToString:_tokenCacheItem.userInformation.uniqueId])
{
completionBlock(nil, [NSError errorWithDomain:@"AADAccountProvider"
code:0
userInfo:@{
@"Reason" : @"AADAccountProvider does not provide this account."
}]);
return;
}
// Try to fetch the token silently in the background, escalating to the ui thread if needed for a unique case (see below)
__weak __block void (^weakAdalCallback)(ADAuthenticationResult*); // __weak __block is needed for recursive blocks under ARC
__block void (^adalCallback)(ADAuthenticationResult*) = ^void(ADAuthenticationResult* adalResult) {
MCDAccessTokenResult* result;
NSError* error;
switch (adalResult.status)
{
case AD_SUCCEEDED:
{
result =
[[MCDAccessTokenResult alloc] initWithAccessToken:adalResult.accessToken status:MCDAccessTokenRequestStatusSuccess];
break;
}
case AD_USER_CANCELLED:
{
error = [NSError errorWithDomain:@"AADAccountProvider" code:0 userInfo:@{ @"Reason" : @"Cancelled by user." }];
break;
}
case AD_FAILED:
default:
{
if (adalResult.error.code == AD_ERROR_SERVER_USER_INPUT_NEEDED)
{
// This error only returns from acquireTokenSilentWithResource: when an interactive prompt is needed.
// ADAL has an MRRT, but the user has not consented for this resource/the MRRT does not cover this resource.
// Usually, users consent for all resources the app needs during the interactive flow in signInWith...:
// However, if the app adds new resources after the user consented previously, signIn will not prompt.
// Escalate to the UI thread and do an interactive flow,
// which should raise a new consent prompt for all current app resources.
NSLog(@"A resource was requested that the user did not previously consent to. "
@"Attempting to raise an interactive consent prompt.");
dispatch_async(dispatch_get_main_queue(), ^{
[_authContext acquireTokenWithResource:scopes[0]
clientId:_clientId
redirectUri:_redirectUri
completionBlock:weakAdalCallback];
});
return;
}
error = [NSError errorWithDomain:@"AADAccountProvider" code:0 userInfo:@{ @"Reason" : @"Unknown ADAL error." }];
break;
}
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ completionBlock(result, error); });
};
weakAdalCallback = adalCallback;
[_authContext acquireTokenSilentWithResource:scopes[0]
clientId:_clientId
redirectUri:_redirectUri
userId:_tokenCacheItem.userInformation.userId
completionBlock:adalCallback];
}
}
- (NSArray<MCDUserAccount*>*)getUserAccounts
{
@synchronized(self)
{
return _tokenCacheItem ?
@[ [[MCDUserAccount alloc] initWithAccountId:_tokenCacheItem.userInformation.uniqueId type:MCDUserAccountTypeAAD] ] :
nil;
}
}
- (void)onAccessTokenError:(NSString*)accountId scopes:(NSArray<NSString*>*)scopes isPermanentError:(BOOL)isPermanentError
{
@synchronized(self)
{
if ([accountId isEqualToString:_tokenCacheItem.userInformation.uniqueId])
{
if (isPermanentError)
{
_tokenCacheItem = nil;
[self _raiseAccountChangedEvent];
}
else
{
// If not a permanent error, try just refreshing the token by calling ADAL's acquireToken: again
[_authContext acquireTokenWithResource:scopes[0]
clientId:_clientId
redirectUri:_redirectUri
completionBlock:^(__unused ADAuthenticationResult* result){}];
}
}
else
{
NSLog(@"accountId was not found in AADAccountProvider.");
}
}
}
@end

Просмотреть файл

@ -1,44 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#pragma once
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <ConnectedDevices/Core/Core.h>
#import "SampleAccountActionFailureReason.h"
typedef NS_ENUM(NSInteger, AADMSAAccountProviderSignInState)
{
AADMSAAccountProviderSignInStateSignedOut,
AADMSAAccountProviderSignInStateSignedInMSA,
AADMSAAccountProviderSignInStateSignedInAAD,
};
// @brief A sample MCDUserAccountProvider that wraps around an AAD provider and an MSA provider.
// Supports only a single user account at a time - trying to log into more than one account at once will throw an exception.
// Any accounts logged into will be made available through the MCDUserAccountProvider interface.
//
// When signed into an AAD account, because of AAD limitations,
// only the first scope in scopes[] passed to for getAccessTokenForUserAccountIdAsync: and onAccessTokenError:, is used
//
// msaClientId is a guid from the app's registration in the msa apps portal
// msaScopeOverrides is a map for the app to specify special scopes to replace the default ones
// aadApplicationId is a guid from the app's registration in the azure portal
// aadRedirectUri is a Uri specified in the azure portal
@interface AADMSAAccountProvider : NSObject <MCDUserAccountProvider>
@property(readonly, atomic) AADMSAAccountProviderSignInState signInState;
@property(readonly, nonatomic, copy, nonnull) NSString* msaClientId;
@property(readonly, nonatomic, copy, nonnull) NSString* aadApplicationId;
- (nullable instancetype)initWithMsaClientId:(nonnull NSString*)msaClientId
msaScopeOverrides:(nullable NSDictionary<NSString*, NSArray<NSString*>*>*) scopes
aadApplicationId:(nonnull NSString*)aadApplicationId
aadRedirectUri:(nonnull NSURL*)aadRedirectUri;
- (void)signInMSAWithCompletionCallback:(nonnull SampleAccountProviderCompletionBlock)callback;
- (void)signInAADWithCompletionCallback:(nonnull SampleAccountProviderCompletionBlock)callback;
- (void)signOutWithCompletionCallback:(nonnull SampleAccountProviderCompletionBlock)callback;
@end

Просмотреть файл

@ -1,135 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#import "AADMSAAccountProvider.h"
#import "AADAccountProvider.h"
#import "MSAAccountProvider.h"
static NSString* const AADMSAAccountProviderExceptionName = @"AADMSAAccountProviderException";
@interface AADMSAAccountProvider ()
@property(readonly, nonatomic, strong) MSAAccountProvider* msaProvider;
@property(readonly, nonatomic, strong) AADAccountProvider* aadProvider;
@end
@implementation AADMSAAccountProvider
@synthesize userAccountChanged = _userAccountChanged;
- (instancetype)initWithMsaClientId:(NSString*)msaClientId
msaScopeOverrides:(NSDictionary<NSString*, NSArray<NSString*>*>*)scopes
aadApplicationId:(NSString*)aadApplicationId
aadRedirectUri:(NSURL*)aadRedirectUri
{
if (self = [super init])
{
_userAccountChanged = [MCDUserAccountChangedEvent new];
_msaProvider = [[MSAAccountProvider alloc] initWithClientId:msaClientId scopeOverrides:scopes];
_aadProvider = [[AADAccountProvider alloc] initWithClientId:aadApplicationId redirectUri:aadRedirectUri];
if (_msaProvider.signedIn && _aadProvider.signedIn)
{
// Shouldn't ever happen, but if it does, sign out of AAD
[_aadProvider signOutWithCompletionCallback:^(__unused BOOL success, __unused SampleAccountActionFailureReason reason){}];
}
[_msaProvider.userAccountChanged subscribe:^void() { [self.userAccountChanged raise]; }];
[_aadProvider.userAccountChanged subscribe:^void() { [self.userAccountChanged raise]; }];
}
return self;
}
- (AADMSAAccountProviderSignInState)signInState
{
@synchronized(self)
{
if (_msaProvider.signedIn)
{
return AADMSAAccountProviderSignInStateSignedInMSA;
}
else if (_aadProvider.signedIn)
{
return AADMSAAccountProviderSignInStateSignedInAAD;
}
return AADMSAAccountProviderSignInStateSignedOut;
}
}
- (NSString*)msaClientId
{
return _msaProvider.clientId;
}
- (NSString*)aadApplicationId
{
return _aadProvider.clientId;
}
- (id<SingleUserAccountProvider>)_signedInProvider
{
switch (self.signInState)
{
case AADMSAAccountProviderSignInStateSignedInMSA: return _msaProvider;
case AADMSAAccountProviderSignInStateSignedInAAD: return _aadProvider;
default: return nil;
}
}
- (void)signInMSAWithCompletionCallback:(SampleAccountProviderCompletionBlock)callback
{
if (self.signInState != AADMSAAccountProviderSignInStateSignedOut)
{
[NSException raise:AADMSAAccountProviderExceptionName format:@"Already signed into an account!"];
}
[_msaProvider signInWithCompletionCallback:callback];
}
- (void)signInAADWithCompletionCallback:(SampleAccountProviderCompletionBlock)callback
{
if (self.signInState != AADMSAAccountProviderSignInStateSignedOut)
{
[NSException raise:AADMSAAccountProviderExceptionName format:@"Already signed into an account!"];
}
[_aadProvider signInWithCompletionCallback:callback];
}
- (void)signOutWithCompletionCallback:(__unused SampleAccountProviderCompletionBlock)callback
{
id<SingleUserAccountProvider> signedInProvider = [self _signedInProvider];
if (!signedInProvider)
{
[NSException raise:AADMSAAccountProviderExceptionName format:@"Not signed into an account!"];
}
[signedInProvider signOutWithCompletionCallback:callback];
}
- (void)getAccessTokenForUserAccountIdAsync:(NSString*)accountId
scopes:(NSArray<NSString*>*)scopes
completion:(void (^)(MCDAccessTokenResult*, NSError*))completionBlock
{
id<SingleUserAccountProvider> signedInProvider = [self _signedInProvider];
if (!signedInProvider)
{
[NSException raise:AADMSAAccountProviderExceptionName format:@"Not signed into an account!"];
}
[signedInProvider getAccessTokenForUserAccountIdAsync:accountId scopes:scopes completion:completionBlock];
}
- (NSArray<MCDUserAccount*>*)getUserAccounts
{
return [[self _signedInProvider] getUserAccounts];
}
- (void)onAccessTokenError:(NSString*)accountId scopes:(NSArray<NSString*>*)scopes isPermanentError:(BOOL)isPermanentError
{
id<SingleUserAccountProvider> signedInProvider = [self _signedInProvider];
if (!signedInProvider)
{
[NSException raise:AADMSAAccountProviderExceptionName format:@"Not signed into an account!"];
}
[signedInProvider onAccessTokenError:accountId scopes:scopes isPermanentError:isPermanentError];
}
@end

Просмотреть файл

@ -1,28 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#pragma once
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <ConnectedDevices/Core/Core.h>
#import "SingleUserAccountProvider.h"
/**
* @brief
* Sample implementation of MCDUserAccountProvider.
* Exposes a single MSA account, that the user logs into via UIWebView, to CDP.
* Follows OAuth2.0 protocol, but automatically refreshes tokens when they are close to expiring.
*/
@interface MSAAccountProvider : NSObject <SingleUserAccountProvider>
// @brief clientId is a guid from the app's registration in the msa portal
// scopeOverrides is a map for the app to specify special scopes to replace the default ones
- (nullable instancetype)initWithClientId:(nonnull NSString*)clientId
scopeOverrides:(nullable NSDictionary<NSString*, NSArray<NSString*>*>*)scopes;
@property(readonly, nonatomic, copy, nonnull) NSString* clientId;
@end

Просмотреть файл

@ -1,492 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#import "MSAAccountProvider.h"
#import "MSATokenCache.h"
#import "MSATokenRequest.h"
/**
* Terms:
* - Scope: OAuth feature, limits what a token actually gives permissions to.
* https://www.oauth.com/oauth2-servers/scope/
* - Access token: A standard JSON web token for a given scope.
* This is the actual token/user ticket used to authenticate with CDP services.
* https://oauth.net/2/
* https://www.oauth.com/oauth2-servers/access-tokens/
* - Refresh token: A standard OAuth refresh token.
* Lasts longer than access tokens, and is used to request new access tokens/refresh access tokens when they expire.
* This library caches one refresh token per user.
* As such, the refresh token must already be authorized/consented to for all CDP scopes that will be used in the app.
* https://oauth.net/2/grant-types/refresh-token/
* - Grant type: Type of OAuth authorization request to make (ie: token, password, auth code)
* https://oauth.net/2/grant-types/
* - Auth code: OAuth auth code, can be exchanged for a token.
* This library has the user sign in interactively for the auth code grant type,
* then retrieves the auth code from the return URL.
* https://oauth.net/2/grant-types/authorization-code/
* - Client ID: ID of an app's registration in the MSA portal. As of the time of writing, the portal uses GUIDs.
*
* The flow of this library is described below:
* Signing in
* 1. signInWithCompletionCallback: is called
* 2. UIWebView is presented to the user for sign in
* 3. Use authcode returned from user's sign in to fetch refresh token
* 4. Refresh token is cached - if the user does not sign out, but the app is restarted,
* the user will not need to enter their credentials/consent again when signInWithCompletionCallback: is called.
* 4. Now treated as signed in. Account is exposed to CDP. userAccountChanged event is fired.
*
* While signed in
* CDP asks for access tokens
* 1. Check if access token is in cache
* 2. If not in cache, request a new access token using the cached refresh token.
* 3. If in cache but close to expiry, the access token is refreshed using the refresh token.
* The refreshed access token is returned.
* 4. If in cache and not close to expiry, just return it.
*
* Signing out
* 1. signOutWithCompletionCallback: is called
* 2. UIWebView is quickly popped up to go through the sign out URL
* 3. Cache is cleared.
* 4. Now treated as signed out. Account is no longer exposed to CDP. userAccountChanged event is fired.
*/
#pragma mark - Constants
// CDP's SDK currently requires authorization for all features, otherwise platform initialization will fail.
// As such, the user must sign in/consent for the following scopes. This may change to become more modular in the future.
static NSString* const MsaRequiredScopes = //
@"wl.offline_access+" // read and update user info at any time
@"ccs.ReadWrite+" // device commanding scope
@"dds.read+" // device discovery scope (discover other devices)
@"dds.register+" // device discovery scope (allow discovering this device)
@"wns.connect+" // push notification scope
@"asimovrome.telemetry+" // asimov token scope
@"https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp"; // default userdata.useractivities scope
// OAuth URLs
static NSString* const MsaRedirectUrl = @"https://login.live.com/oauth20_desktop.srf";
static NSString* const MsaAuthorizeUrl = @"https://login.live.com/oauth20_authorize.srf";
static NSString* const MsaLogoutUrl = @"https://login.live.com/oauth20_logout.srf";
// NSError constants
static NSString* const MsaAccountProviderErrorDomain = @"MSAAccountProvider";
static const NSInteger MsaAccountProviderErrorInvalidAccountId = 100;
static const NSInteger MsaAccountProviderErrorAccessTokenTemporaryError = 101;
static const NSInteger MsaAccountProviderErrorAccessTokenPermanentError = 102;
#pragma mark - Static Helpers
// Helper function - gets the NSURLQueryItem matching name
static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems, NSString* name)
{
NSUInteger index = [queryItems indexOfObjectPassingTest:^BOOL(NSURLQueryItem* queryItem, __unused NSUInteger idx, __unused BOOL* stop) {
return [queryItem.name isEqualToString:name];
}];
return (index != NSNotFound) ? queryItems[index] : nil;
}
#pragma mark - Private Members
@interface MSAAccountProvider () <MSATokenCacheDelegate, UIWebViewDelegate>
{
NSString* _clientId;
NSDictionary<NSString*, NSArray<NSString*>*>* _scopeOverrides;
MCDUserAccount* _account;
MSATokenCache* _tokenCache;
BOOL _signInSignOutInProgress;
SampleAccountProviderCompletionBlock _signInSignOutCallback;
UIWebView* _webView;
}
@end
#pragma mark - Implementation
@implementation MSAAccountProvider
@synthesize userAccountChanged = _userAccountChanged;
- (instancetype)initWithClientId:(NSString*)clientId
scopeOverrides:(NSDictionary<NSString*, NSArray<NSString*>*>*)scopes
{
NSLog(@"MSAAccountProvider initWithClientId");
if (self = [super init])
{
_clientId = [clientId copy];
_scopeOverrides = [scopes copy];
_tokenCache = [MSATokenCache cacheWithClientId:_clientId delegate:self];
_userAccountChanged = [MCDUserAccountChangedEvent new];
_signInSignOutInProgress = NO;
_signInSignOutCallback = nil;
if ([_tokenCache loadSavedRefreshToken])
{
NSLog(@"Loaded previous session for MSAAccountProvider. Starting as signed in.");
_account = [[MCDUserAccount alloc] initWithAccountId:[[NSUUID UUID] UUIDString] type:MCDUserAccountTypeMSA];
}
else
{
NSLog(@"No previous session could be loaded for MSAAccountProvider. Starting as signed out.");
}
}
return self;
}
#pragma mark - Private Helpers
- (NSString*)_getAuthScopes: (NSArray<NSString*>*) incoming
{
NSMutableArray<NSString*>* scopes = [NSMutableArray new];
for (NSString* scope in incoming)
{
NSArray<NSString*>* replacements = [_scopeOverrides objectForKey:scope];
if (replacements)
{
[scopes addObjectsFromArray:replacements];
}
else
{
[scopes addObject:scope];
}
}
return [scopes componentsJoinedByString:@"+"];
}
- (void)_raiseAccountChangedEvent
{
NSLog(@"Raise Account changed event");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// fire event on a different thread
[self.userAccountChanged raise];
});
}
- (void)_addAccount
{
@synchronized(self)
{
NSLog(@"Adding an account.");
_account = [[MCDUserAccount alloc] initWithAccountId:[[NSUUID UUID] UUIDString] type:MCDUserAccountTypeMSA];
[self _raiseAccountChangedEvent];
}
}
- (void)_removeAccount
{
@synchronized(self)
{
// clean account states
if (self.signedIn)
{
NSLog(@"Removing account.");
_account = nil;
[_tokenCache clearTokens];
[self _raiseAccountChangedEvent];
}
}
}
- (void)_loadWebRequest:(NSString*)requestUri
{
@synchronized(self)
{
UIViewController* rootVC = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
// lazy init
if (!_webView)
{
_webView = [[UIWebView alloc] initWithFrame:rootVC.view.bounds];
_webView.delegate = self;
}
[rootVC.view addSubview:_webView];
NSURLRequest* urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:requestUri]];
[_webView loadRequest:urlRequest];
}
}
- (void)_signInSignOutSucceededAsync:(BOOL)successful reason:(SampleAccountActionFailureReason)reason
{
dispatch_async(dispatch_get_main_queue(), ^{ [_webView removeFromSuperview]; });
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
_signInSignOutCallback(successful, reason);
_signInSignOutCallback = nil;
_signInSignOutInProgress = NO;
});
}
/**
* Asynchronously requests a new access token for the provided scope(s) and caches it.
* This assumes that the sign in helper is currently signed in.
*/
- (void)_requestNewAccessTokenAsync:(NSString*)scope callback:(void (^)(MCDAccessTokenResult*, NSError*))completionBlock
{
// Need the refresh token first, then can use it to request an access token
[_tokenCache getRefreshTokenAsync:^void(NSString* refreshToken) {
NSLog(@"Fetching access token for scope:%@", scope);
[MSATokenRequest
doAsyncRequestWithClientId:_clientId
grantType:MsaTokenRequestGrantTypeRefresh
scope:scope
redirectUri:nil
token:refreshToken
callback:^void(MSATokenRequestResult* result) {
switch (result.status)
{
case MSATokenRequestStatusSuccess:
{
NSLog(@"Successfully fetched access token.");
[_tokenCache setAccessToken:result.accessToken forScope:scope expiresIn:result.expiresIn];
completionBlock([[MCDAccessTokenResult alloc] initWithAccessToken:result.accessToken
status:MCDAccessTokenRequestStatusSuccess],
nil);
break;
}
case MSATokenRequestStatusTransientFailure:
{
NSLog(@"Requesting new access token failed temporarily, please try again.");
completionBlock(nil, [NSError errorWithDomain:MsaAccountProviderErrorDomain
code:MsaAccountProviderErrorAccessTokenTemporaryError
userInfo:nil]);
break;
}
default: // PermanentFailure
{
NSLog(@"Permanent error occurred while fetching access token.");
[self onAccessTokenError:_account.accountId scopes:@[ scope ] isPermanentError:YES];
completionBlock(nil, [NSError errorWithDomain:MsaAccountProviderErrorDomain
code:MsaAccountProviderErrorAccessTokenPermanentError
userInfo:nil]);
break;
}
}
}];
}];
}
#pragma mark - Interactive Sign In/Out
- (BOOL)signedIn
{
@synchronized(self)
{
return _account != nil;
}
}
/**
* Pops up a webview for the user to sign in with their MSA, then uses the auth code returned to cache a refresh token for the user.
* If a refresh token was already cached from a previous session, it will be used instead, and no webview will be displayed.
*/
- (void)signInWithCompletionCallback:(SampleAccountProviderCompletionBlock)signInCallback
{
@synchronized(self)
{
_signInSignOutCallback = signInCallback;
if (self.signedIn || _signInSignOutInProgress)
{
// if already signed in or in the process, callback immediately with failure and reason
[self _signInSignOutSucceededAsync:NO
reason:(self.signedIn ? SampleAccountActionFailureReasonAlreadySignedIn :
SampleAccountActionFailureReasonSigninSignOutInProgress)];
return;
}
_signInSignOutInProgress = YES;
// issue request to sign in
NSArray* scopes = [MsaRequiredScopes componentsSeparatedByString:@"+"];
[self _loadWebRequest:[NSString stringWithFormat:@"%@?redirect_uri=%@&response_type=code&client_id=%@&scope=%@", MsaAuthorizeUrl,
MsaRedirectUrl, _clientId, [self _getAuthScopes:scopes]]];
}
}
/**
* Signs the user out by going through the webview, then clears the cache and current state.
*/
- (void)signOutWithCompletionCallback:(SampleAccountProviderCompletionBlock)signOutCallback
{
@synchronized(self)
{
_signInSignOutCallback = signOutCallback;
if (!self.signedIn || _signInSignOutInProgress)
{
// if already signed out or in the process, callback immediately with failure and reason
[self _signInSignOutSucceededAsync:NO
reason:(self.signedIn ? SampleAccountActionFailureReasonSigninSignOutInProgress :
SampleAccountActionFailureReasonAlreadySignedOut)];
return;
}
_signInSignOutInProgress = YES;
// issue request to sign out
[self _loadWebRequest:[NSString stringWithFormat:@"%@?client_id=%@&redirect_uri=%@", MsaLogoutUrl, _clientId, MsaRedirectUrl]];
}
}
/**
* Continuation for signIn/signOut after the webview completes.
*/
- (void)webViewDidFinishLoad:(UIWebView*)webView
{
@synchronized(self)
{
// Validate the URL
NSURLComponents* tokenURLComponents = [NSURLComponents componentsWithURL:webView.request.URL resolvingAgainstBaseURL:nil];
if (![tokenURLComponents.path containsString:@"oauth20_desktop.srf"])
{
// finishing off loading intermediate pages,
// e.g., input username/password page, consent interrupt page, wrong username/password page etc.
// no need to handle them, return early.
return;
}
NSArray<NSURLQueryItem*>* tokenURLQueryItems = tokenURLComponents.queryItems;
if (GetQueryItemForName(tokenURLQueryItems, @"error"))
{
// sign in or sign out ending in failure
[self _signInSignOutSucceededAsync:NO reason:SampleAccountActionFailureReasonUnknown];
return;
}
NSString* authCode = GetQueryItemForName(tokenURLQueryItems, @"code").value;
if (!authCode)
{
// sign out ended in success
[self _removeAccount];
[self _signInSignOutSucceededAsync:YES reason:SampleAccountActionNoFailure];
}
else
{
// sign in ended in success
if (authCode.length <= 0)
{
// very unusual
[self _signInSignOutSucceededAsync:NO reason:SampleAccountActionFailureReasonFailToRetrieveAuthCode];
return;
}
// Fetch a refresh token using the auth code
void (^requestRefreshTokenTokenCallback)(MSATokenRequestResult*) = ^void(MSATokenRequestResult* result) {
if (result.status == MSATokenRequestStatusSuccess)
{
NSString* newRefreshToken = result.refreshToken;
NSAssert(newRefreshToken != nil, @"refresh token can not be null when refreshing refresh token succeeded");
NSLog(@"Successfully fetch the root refresh token.");
[_tokenCache setRefreshToken:newRefreshToken];
[self _addAccount];
[self _signInSignOutSucceededAsync:YES reason:SampleAccountActionNoFailure];
}
else
{
NSLog(@"Failed to fetch root refresh token using authcode.");
[self _signInSignOutSucceededAsync:NO reason:SampleAccountActionFailureReasonFailToRetrieveRefreshToken];
}
};
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"Fetch root refresh token using authcode.");
[MSATokenRequest doAsyncRequestWithClientId:_clientId
grantType:MsaTokenRequestGrantTypeCode
scope:nil
redirectUri:MsaRedirectUrl
token:authCode
callback:requestRefreshTokenTokenCallback];
});
}
}
}
/**
* Continuation for signIn/signOut after the webview completes with a failure.
*/
- (void)webView:(UIWebView*)__unused webView didFailLoadWithError:(NSError*)error
{
@synchronized(self)
{
// This gets invoked when we interrupt/cancel because we saw the oauth complete page.
int WebKitErrorFrameLoadInterruptedByPolicyChange = 102;
if (error.code != WebKitErrorFrameLoadInterruptedByPolicyChange /*interrupted*/
&& error.code != NSURLErrorCancelled)
{
[self _signInSignOutSucceededAsync:NO reason:SampleAccountActionFailureReasonUserCancelled];
}
}
}
#pragma mark - MCDUserAccountProvider Overrides
- (void)getAccessTokenForUserAccountIdAsync:(NSString*)accountId
scopes:(NSArray<NSString*>*)scopes
completion:(void (^)(MCDAccessTokenResult*, NSError*))completionBlock
{
if (![accountId isEqualToString:_account.accountId])
{
NSLog(@"accountId did not match logged in account - is the user signed in?");
completionBlock(
nil, [NSError errorWithDomain:MsaAccountProviderErrorDomain code:MsaAccountProviderErrorInvalidAccountId userInfo:nil]);
return;
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(self)
{
// check if access token cache already has a valid token
NSString* accessTokenScope = [self _getAuthScopes:scopes];
// clang-format off
[_tokenCache getAccessTokenForScopeAsync:accessTokenScope callback:^void(NSString* accessToken)
{
if (accessToken.length > 0)
{
NSLog(@"Found valid access token for scope %@ in cache, return early", accessTokenScope);
completionBlock(
[[MCDAccessTokenResult alloc] initWithAccessToken:accessToken status:MCDAccessTokenRequestStatusSuccess], nil);
return;
}
NSLog(@"Didn't find valid access token for scope %@ in cache, try to fetch it", accessTokenScope);
[self _requestNewAccessTokenAsync:accessTokenScope callback:completionBlock];
}];
// clang-format on
}
});
}
- (NSArray<MCDUserAccount*>*)getUserAccounts
{
@synchronized(self)
{
return _account ? @[ _account ] : nil;
}
}
- (void)onAccessTokenError:(NSString*)__unused accountId scopes:(NSArray<NSString*>*)__unused scopes isPermanentError:(BOOL)isPermanentError
{
@synchronized(self)
{
if (isPermanentError)
{
[self _removeAccount];
}
else
{
[_tokenCache markAllTokensExpired];
}
}
}
#pragma mark - MSATokenCache Delegate
- (void)onTokenCachePermanentFailure
{
if (_account)
{
[self onAccessTokenError:_account.accountId scopes:[_tokenCache allScopes] isPermanentError:YES];
}
}
@end

Просмотреть файл

@ -1,42 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#pragma once
#import <Foundation/Foundation.h>
// @brief Receives callback from the cache for any permanent failures
@protocol MSATokenCacheDelegate <NSObject>
- (void)onTokenCachePermanentFailure;
@end
// @brief Interface for caching and automatically refreshing MSA refresh and access tokens.
// Refresh tokens are automatically saved to disk.
// On permanent failure (cannot retry), a callback is sent to the delegate.
// These interfaces currently only support one user. forUser: will be added after platform support for multi-user is enabled.
@interface MSATokenCache : NSObject
+ (nullable instancetype)cacheWithClientId:(nonnull NSString*)clientId delegate:(nullable id<MSATokenCacheDelegate>)delegate;
- (nullable instancetype)initWithClientId:(nonnull NSString*)clientId delegate:(nullable id<MSATokenCacheDelegate>)delegate;
// @brief Adds/gets tokens to/from the cache, automatically refreshing them once expired.
- (void)setRefreshToken:(nonnull NSString*)refreshToken;
- (void)setAccessToken:(nonnull NSString*)accessToken forScope:(nonnull NSString*)scope expiresIn:(NSTimeInterval)expiry;
- (void)getRefreshTokenAsync:(nonnull void (^)(NSString* _Nullable accessToken))callback;
- (void)getAccessTokenForScopeAsync:(nonnull NSString*)scope callback:(nonnull void (^)(NSString* _Nullable accessToken))callback;
// @brief Returns the scopes for which there are currently access tokens cached.
- (nonnull NSArray<NSString*>*)allScopes;
// @brief Attempts to load a refresh token that was previously saved, and returns the success value of the operation.
// If successful, the loaded refresh token can be retrieved from getRefreshTokenAsync:
- (BOOL)loadSavedRefreshToken;
// @brief Clears the cache, including the saved refresh token.
- (void)clearTokens;
// @brief Marks all tokens as expired, such that a refresh will be attempted before the next time any token is returned.
- (void)markAllTokensExpired;
@property(nonatomic, readwrite, nullable, strong) id<MSATokenCacheDelegate> delegate;
@end

Просмотреть файл

@ -1,500 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#import "MSATokenCache.h"
#import "MSATokenRequest.h"
static NSString* const MsaOfflineAccessScope = @"wl.offline_access";
static NSString* const JsonTokenKey = @"refresh_token";
static NSString* const JsonExpirationKey = @"expires";
// Max number of times to try to refresh a token through transient failures
static const NSUInteger MsaTokenRefreshMaxRetries = 3;
// How quickly to retry refresh token refreshes on transient failure; 30 minutes.
static const int64_t MsaRefreshTokenRetryInterval = 30 * 60;
// How quickly to retry access token refreshes on transient failure; 3 minutes.
static const int64_t MsaAccessTokenRetryInterval = 3 * 60;
// How long a refresh token is expected to last without expiring; 10 days.
static const NSTimeInterval MsaRefreshTokenExpirationInterval = 10 * 24 * 60 * 60;
// Time from expiration at which a refresh token is considered 'close to expiring'; 7 days.
// (This value is intended to be aggressive and keep the refresh token relatively far from expiry)
static const NSTimeInterval MsaRefreshTokenCloseToExpiryInterval = 7 * 24 * 60 * 60;
// Time from expiration at which an access token is considered 'close to expiring'; 5 minutes.
static const NSTimeInterval MsaAccessTokenCloseToExpiryInterval = 5 * 60;
// @brief Private helper class, encapsulates a single MSA token to be cached, and how to refresh it.
@interface MSATokenCacheItem : NSObject
+ (nullable instancetype)cacheItemWithToken:(nonnull NSString*)token
expiresIn:(NSTimeInterval)expiry
refreshWith:(nonnull MSATokenRequest*)refreshRequest
parent:(nonnull MSATokenCache*)parent;
- (nullable instancetype)initWithToken:(nonnull NSString*)token
expiresIn:(NSTimeInterval)expiry
refreshWith:(nonnull MSATokenRequest*)refreshRequest
parent:(nonnull MSATokenCache*)parent;
// Asynchronously fetches the token held by this item, refreshing it if necessary.
- (void)getTokenAsync:(nonnull void (^)(NSString* _Nullable token))callback;
@property(readwrite, nonnull, nonatomic, copy) NSString* token;
@property(readwrite, nonnull, nonatomic, strong) NSDate* expirationDate;
@property(readwrite, nonnull, nonatomic, strong) MSATokenRequest* refreshRequest;
@property(readwrite, nonnull, nonatomic, strong) MSATokenCache* parent;
@property(readonly, nonatomic) NSTimeInterval closeToExpiryInterval;
@property(readonly, nonatomic) int64_t retryInterval;
// Private helper for refreshing this token. Only to be used by this class and its subclass.
// Returns the refresh token needed to refresh the token held by this item.
// For access tokens, this gets the refresh token held by the cache.
// For refresh tokens, just return the currently-held token.
- (void)getRefreshTokenAsync:(nonnull void (^)(NSString* _Nullable token))callback;
// Private helper for refreshing this token. Only to be used by this class and its subclass.
// For access tokens, sets the new token and expiration.
// For refresh tokens, marks current access tokens as expired, and caches the refresh token in persistent storage.
- (void)onSuccessfulRefresh:(nonnull MSATokenRequestResult*)result;
@end
// @brief Subclass of MSATokenCacheItem for refresh tokens
@interface MSARefreshTokenCacheItem : MSATokenCacheItem
+ (nullable instancetype)loadSavedRefreshTokenWithParent:(nonnull MSATokenCache*)parent;
- (void)saveRefreshToken;
@end
// MSATokenCache privates
@interface MSATokenCache ()
@property(readonly, nonnull, nonatomic, copy) NSString* clientId;
@property(readonly, nonnull, nonatomic, strong) NSMutableDictionary<NSString*, MSATokenCacheItem*>* cachedAccessTokens; // keyed on scopes
@property(readwrite, nullable, nonatomic, strong) MSARefreshTokenCacheItem* cachedRefreshToken;
- (void)markAccessTokensExpired;
@end
@implementation MSATokenCacheItem
+ (instancetype)cacheItemWithToken:(NSString*)token
expiresIn:(NSTimeInterval)expiry
refreshWith:(MSATokenRequest*)refreshRequest
parent:(MSATokenCache*)parent
{
return [[self alloc] initWithToken:token expiresIn:expiry refreshWith:refreshRequest parent:parent];
}
- (instancetype)initWithToken:(NSString*)token
expiresIn:(NSTimeInterval)expiry
refreshWith:(MSATokenRequest*)refreshRequest
parent:(MSATokenCache*)parent
{
if (self = [super init])
{
_token = [token copy];
_expirationDate = [NSDate dateWithTimeIntervalSinceNow:expiry];
_refreshRequest = refreshRequest;
_parent = parent;
}
return self;
}
- (void)getTokenAsync:(void (^)(NSString*))callback
{
[self getTokenAsync:callback maxRetries:MsaTokenRefreshMaxRetries];
}
- (void)getTokenAsync:(void (^)(NSString*))callback maxRetries:(NSUInteger)maxRetries
{
if ([_expirationDate timeIntervalSinceNow] >= self.closeToExpiryInterval)
{
// If expiration date is sufficiently far away
callback(self.token);
}
else
{
// If expired or close to it, get the refresh token and attempt to refresh with it
[self getRefreshTokenAsync:^void(NSString* refreshToken) {
if (!refreshToken)
{
// Unable to get the refresh token even after retrying
// Consider as a permanent failure and call back with no tokens
NSLog(@"Unable to get refresh token. Cancelling refresh and removing all tokens from cache.");
[_parent clearTokens];
callback(nil);
}
NSLog(@"Refreshing token...");
[_refreshRequest
requestAsyncWithToken:refreshToken
callback:^void(MSATokenRequestResult* result) {
switch (result.status)
{
case MSATokenRequestStatusSuccess:
{
[self onSuccessfulRefresh:result];
callback(self.token);
break;
}
case MSATokenRequestStatusTransientFailure:
{
if (maxRetries > 0)
{
// Retry the refresh
NSLog(@"Encountered transient error when refreshing token, retrying in %lld seconds...",
self.retryInterval);
dispatch_time_t retryTime = dispatch_time(DISPATCH_TIME_NOW, self.retryInterval * NSEC_PER_SEC);
dispatch_after(retryTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{ [self getTokenAsync:callback maxRetries:(maxRetries - 1)]; });
}
else
{
// Reached max number of retries
NSLog(@"Reached max number of retries for refreshing token.");
callback(nil);
}
break;
}
default: // PermanentFailure
{
NSLog(@"Permanent error occurred while refreshing token. Clearing the cache...");
[_parent clearTokens];
[_parent.delegate onTokenCachePermanentFailure];
callback(nil);
break;
}
}
}];
}];
}
}
- (NSTimeInterval)closeToExpiryInterval
{
return MsaAccessTokenCloseToExpiryInterval; // Base class expects access tokens
}
- (int64_t)retryInterval
{
return MsaAccessTokenRetryInterval; // Base class expects access tokens
}
- (void)getRefreshTokenAsync:(void (^)(NSString*))callback
{
[_parent getRefreshTokenAsync:callback]; // Base class expects access tokens, grab the refresh token from the parent
}
- (void)onSuccessfulRefresh:(MSATokenRequestResult*)result
{
@synchronized(self)
{
NSString* newToken = result.accessToken;
NSAssert(newToken.length > 0, @"UNEXPECTED: Refresh access token succeeded but access token was empty.");
NSLog(@"Successfully refreshed access token.");
self.token = newToken;
self.expirationDate = [NSDate dateWithTimeIntervalSinceNow:result.expiresIn];
}
}
@end
@implementation MSARefreshTokenCacheItem
+ (instancetype)loadSavedRefreshTokenWithParent:(MSATokenCache*)parent
{
NSLog(@"Loading refresh token from keychain...");
// clang-format off
NSDictionary* keychainMatchQuery = @{
(id) kSecClass : (id) kSecClassGenericPassword,
(id) kSecAttrGeneric : parent.clientId,
(id) kSecMatchLimit : (id) kSecMatchLimitOne, // Only match one keychain item
(id) kSecReturnData : @YES // Return the data itself rather than a ref
};
// clang-format on
CFTypeRef keychainItems = NULL;
OSStatus keychainStatus = SecItemCopyMatching((CFDictionaryRef)keychainMatchQuery, &keychainItems);
if (keychainStatus == errSecItemNotFound)
{
NSLog(@"No refresh token found in keychain.");
return nil;
}
else if (keychainStatus != errSecSuccess)
{
NSLog(@"Unable to load refresh token from keychain with OSStatus %d", (int)keychainStatus);
return nil;
}
NSError* jsonError = nil;
CFDataRef tokenData = (CFDataRef)keychainItems;
id deserializedTokenData = [NSJSONSerialization JSONObjectWithData:(__bridge NSData*)tokenData options:0 error:&jsonError];
if (jsonError)
{
NSLog(@"Encountered JSON error \'%@\' while trying to load refresh token from keychain.", jsonError);
return nil;
}
else if (![deserializedTokenData isKindOfClass:[NSDictionary class]])
{
NSLog(@"Loaded refresh token data from keychain was in an unexpected format. Will not load.");
return nil;
}
NSDictionary* tokenDict = (NSDictionary*)deserializedTokenData;
NSString* loadedRefreshToken = (NSString*)(tokenDict[JsonTokenKey]);
NSDateFormatter* dateFormatter = [NSDateFormatter new];
dateFormatter.dateStyle = NSDateFormatterFullStyle;
dateFormatter.timeStyle = NSDateFormatterFullStyle;
NSDate* loadedRefreshTokenExpiry = [dateFormatter dateFromString:(NSString*)(tokenDict[JsonExpirationKey])];
if (!loadedRefreshToken || !loadedRefreshTokenExpiry)
{
NSLog(@"Loaded refresh token data from keychain was incomplete or corrupted.");
return nil;
}
NSTimeInterval timeUntilExpiration = [loadedRefreshTokenExpiry timeIntervalSinceDate:[NSDate date]];
MSATokenRequest* refreshRequest = [MSATokenRequest tokenRequestWithClientId:parent.clientId
grantType:MsaTokenRequestGrantTypeRefresh
scope:MsaOfflineAccessScope
redirectUri:nil];
MSARefreshTokenCacheItem* ret =
[self cacheItemWithToken:loadedRefreshToken expiresIn:timeUntilExpiration refreshWith:refreshRequest parent:parent];
NSLog(@"Successfully loaded refresh token from keychain.");
return ret;
}
- (void)saveRefreshToken
{
NSLog(@"Saving refresh token to keychain...");
NSDateFormatter* dateFormatter = [NSDateFormatter new];
dateFormatter.dateStyle = NSDateFormatterFullStyle;
dateFormatter.timeStyle = NSDateFormatterFullStyle;
NSDictionary* tokenDict = @{ JsonTokenKey : self.token, JsonExpirationKey : [dateFormatter stringFromDate:self.expirationDate] };
NSError* jsonError = nil;
NSData* tokenData = [NSJSONSerialization dataWithJSONObject:tokenDict options:0 error:&jsonError];
if (jsonError)
{
NSLog(@"Encountered JSON error \'%@\' while trying to save refresh token to keychain. Will not save.", jsonError);
return;
}
// clang-format off
NSDictionary* keychainSearchQuery = @{
(id) kSecClass : (id) kSecClassGenericPassword,
(id) kSecAttrGeneric : self.parent.clientId
};
// clang-format on
OSStatus keychainStatus = SecItemUpdate((CFDictionaryRef)keychainSearchQuery, (CFDictionaryRef) @{ (id) kSecValueData : tokenData });
if (keychainStatus == errSecItemNotFound)
{
// After a device restart, this keychain item is only accessible after the device is unlocked at least once.
// This keychain item is not migrated when restoring a backup from another device.
id accessAttribute = (id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
NSMutableDictionary* keychainAddQuery = [keychainSearchQuery mutableCopy];
[keychainAddQuery addEntriesFromDictionary:@{ (id) kSecAttrAccessible : accessAttribute, (id) kSecValueData : tokenData }];
keychainStatus = SecItemAdd((CFDictionaryRef)keychainAddQuery, NULL);
}
if (keychainStatus != errSecSuccess)
{
NSLog(@"Failed to save refresh token data to keychain with OSStatus %d.", (int)keychainStatus);
}
NSLog(@"Successfully saved refresh token data to keychain.");
}
- (NSTimeInterval)closeToExpiryInterval
{
return MsaRefreshTokenCloseToExpiryInterval;
}
- (int64_t)retryInterval
{
return MsaRefreshTokenRetryInterval;
}
- (void)getRefreshTokenAsync:(void (^)(NSString*))callback
{
callback(self.token); // Since this cache item holds a refresh token, just return it
}
- (void)onSuccessfulRefresh:(MSATokenRequestResult*)result
{
@synchronized(self)
{
NSString* newToken = result.refreshToken;
NSAssert(newToken.length > 0, @"UNEXPECTED: Refresh refresh token succeeded but access token was empty.");
NSLog(@"Successfully refreshed refresh token.");
self.token = newToken;
self.expirationDate = [NSDate dateWithTimeIntervalSinceNow:MsaRefreshTokenExpirationInterval];
[self saveRefreshToken];
[self.parent markAccessTokensExpired];
}
}
@end
// MSATokenCache implementation
@implementation MSATokenCache
+ (instancetype)cacheWithClientId:(NSString*)clientId delegate:(id<MSATokenCacheDelegate>)delegate
{
return [[self alloc] initWithClientId:clientId delegate:delegate];
}
- (instancetype)initWithClientId:(NSString*)clientId delegate:(id<MSATokenCacheDelegate>)delegate
{
if (self = [super init])
{
_clientId = [clientId copy];
_delegate = delegate;
_cachedAccessTokens = [NSMutableDictionary<NSString*, MSATokenCacheItem*> new];
}
return self;
}
- (void)setRefreshToken:(NSString*)refreshToken
{
MSATokenRequest* refreshRequest = [MSATokenRequest tokenRequestWithClientId:_clientId
grantType:MsaTokenRequestGrantTypeRefresh
scope:MsaOfflineAccessScope
redirectUri:nil];
@synchronized(self)
{
_cachedRefreshToken = [MSARefreshTokenCacheItem cacheItemWithToken:refreshToken
expiresIn:MsaRefreshTokenExpirationInterval
refreshWith:refreshRequest
parent:self];
[_cachedRefreshToken saveRefreshToken];
[self markAccessTokensExpired];
}
}
- (void)setAccessToken:(NSString*)accessToken forScope:(NSString*)scope expiresIn:(NSTimeInterval)expiry
{
MSATokenRequest* refreshRequest =
[MSATokenRequest tokenRequestWithClientId:_clientId grantType:MsaTokenRequestGrantTypeRefresh scope:scope redirectUri:nil];
@synchronized(self)
{
[_cachedAccessTokens
setValue:[MSATokenCacheItem cacheItemWithToken:accessToken expiresIn:expiry refreshWith:refreshRequest parent:self]
forKey:scope];
}
}
- (void)getRefreshTokenAsync:(void (^)(NSString*))callback
{
@synchronized(self)
{
if (_cachedRefreshToken)
{
[_cachedRefreshToken getTokenAsync:callback];
}
else
{
callback(nil);
}
}
}
- (void)getAccessTokenForScopeAsync:(NSString*)scope callback:(void (^)(NSString*))callback
{
@synchronized(self)
{
MSATokenCacheItem* item = [_cachedAccessTokens valueForKey:scope];
if (item)
{
[item getTokenAsync:callback];
}
else
{
callback(nil);
}
}
}
- (NSArray<NSString*>*)allScopes
{
return [_cachedAccessTokens allKeys];
}
- (BOOL)loadSavedRefreshToken
{
MSARefreshTokenCacheItem* loadedRefreshToken = [MSARefreshTokenCacheItem loadSavedRefreshTokenWithParent:self];
if (loadedRefreshToken)
{
if ([loadedRefreshToken.expirationDate compare:[NSDate date]] != NSOrderedDescending)
{
NSLog(@"Refresh token loaded from keychain was expired. Ignoring.");
return NO;
}
@synchronized(self)
{
_cachedRefreshToken = loadedRefreshToken;
[self markAllTokensExpired]; // Force a refresh on everything on first use
}
}
return (loadedRefreshToken != nil);
}
- (void)clearTokens
{
NSLog(@"Clearing token data from cache...");
@synchronized(self)
{
[_cachedAccessTokens removeAllObjects];
_cachedRefreshToken = nil;
}
// clang-format off
NSDictionary* keychainDeleteQuery = @{
(id) kSecClass : (id) kSecClassGenericPassword,
(id) kSecAttrGeneric : _clientId
};
// clang-format on
OSStatus keychainStatus = SecItemDelete((CFDictionaryRef)keychainDeleteQuery);
if (keychainStatus != errSecSuccess)
{
NSLog(@"Unable to clear token data from keychain with OSStatus %d. Data might still be loaded on next run.", (int)keychainStatus);
}
NSLog(@"Done clearing token data from cache.");
}
- (void)markAccessTokensExpired
{
@synchronized(self)
{
for (MSATokenCacheItem* cachedAccessToken in _cachedAccessTokens.allValues)
{
cachedAccessToken.expirationDate = [NSDate distantPast];
}
}
}
- (void)markAllTokensExpired
{
@synchronized(self)
{
_cachedRefreshToken.expirationDate = [NSDate distantPast];
[self markAccessTokensExpired];
}
}
@end

Просмотреть файл

@ -1,66 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#pragma once
#import <Foundation/Foundation.h>
extern NSString* _Nonnull const MsaTokenRequestGrantTypeCode;
extern NSString* _Nonnull const MsaTokenRequestGrantTypeRefresh;
typedef NS_ENUM(NSInteger, MSATokenRequestStatus)
{
MSATokenRequestStatusSuccess,
MSATokenRequestStatusTransientFailure,
MSATokenRequestStatusPermanentFailure
};
@interface MSATokenRequestResult : NSObject
@property(readwrite, nonatomic) MSATokenRequestStatus status;
@property(readwrite, nullable, nonatomic, copy) NSString* accessToken;
@property(readwrite, nullable, nonatomic, copy) NSString* refreshToken;
@property(readwrite, nonatomic) NSInteger expiresIn;
@end
/**
* @brief Encapsulates a noninteractive request for an MSA token.
* This request may be performed multiple times.
*/
@interface MSATokenRequest : NSObject
/**
* Fetches Token (Access or Refresh Token).
* clientId - clientId of the app's registration in the MSA portal
* grantType - one of the MsaTokenRequestGrantType constants
* scope
* redirectUri
* token - authCode for MsaTokenRequestGrantTypeCode, or refresh token for MsaTokenRequestGrantTypeRefresh
*/
+ (void)doAsyncRequestWithClientId:(nonnull NSString*)clientId
grantType:(nonnull NSString*)grantType
scope:(nullable NSString*)scope
redirectUri:(nullable NSString*)redirectUri
token:(nonnull NSString*)token
callback:(nonnull void (^)(MSATokenRequestResult* _Nonnull result))callback;
+ (nullable instancetype)tokenRequestWithClientId:(nonnull NSString*)clientId
grantType:(nonnull NSString*)grantType
scope:(nullable NSString*)scope
redirectUri:(nullable NSString*)redirectUri;
- (nullable instancetype)initWithClientId:(nonnull NSString*)clientId
grantType:(nonnull NSString*)grantType
scope:(nullable NSString*)scope
redirectUri:(nullable NSString*)redirectUri;
@property(readonly, nonnull, nonatomic, copy) NSString* clientId;
@property(readonly, nonnull, nonatomic, copy) NSString* grantType;
@property(readonly, nullable, nonatomic, copy) NSString* scope;
@property(readonly, nullable, nonatomic, copy) NSString* redirectUri;
- (void)requestAsyncWithToken:(nonnull NSString*)token callback:(nonnull void (^)(MSATokenRequestResult* _Nonnull result))callback;
@end

Просмотреть файл

@ -1,165 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#import "MSATokenRequest.h"
NSString* const MsaTokenRequestGrantTypeCode = @"authorization_code";
NSString* const MsaTokenRequestGrantTypeRefresh = @"refresh_token";
static const NSTimeInterval MsaTokenRequestTimeout = 30.0;
// Helper function - encodes an NSDictionary to be usable as POST data in an NSURLRequest
static NSData* EncodeDictionary(NSDictionary<NSString*, NSString*>* dictionary)
{
NSMutableArray<NSString*>* parts = [NSMutableArray<NSString*> new];
for (NSString* key in dictionary)
{
NSString* encodedValue = [[dictionary objectForKey:key] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSString* encodedKey = [key stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
[parts addObject:[NSString stringWithFormat:@"%@=%@", encodedKey, encodedValue]];
}
NSString* encodedDictionary = [parts componentsJoinedByString:@"&"];
return [encodedDictionary dataUsingEncoding:NSUTF8StringEncoding];
}
@interface MSATokenRequestResult ()
+ (instancetype)resultWithStatus:(MSATokenRequestStatus)status responseDictionary:(nullable NSDictionary*)responseDict;
@end
@implementation MSATokenRequestResult
+ (instancetype)resultWithStatus:(MSATokenRequestStatus)status responseDictionary:(NSDictionary*)responseDict
{
MSATokenRequestResult* ret = [self new];
if (ret)
{
ret.status = status;
if (responseDict)
{
ret.accessToken = [responseDict valueForKey:@"access_token"];
ret.refreshToken = [responseDict valueForKey:@"refresh_token"];
ret.expiresIn = [[responseDict valueForKey:@"expires_in"] integerValue];
}
}
return ret;
}
@end
@implementation MSATokenRequest
+ (void)doAsyncRequestWithClientId:(NSString*)clientId
grantType:(NSString*)grantType
scope:(NSString*)scope
redirectUri:(NSString*)redirectUri
token:(NSString*)token
callback:(void (^)(MSATokenRequestResult*))callback
{
NSLog(@"Requesting token for scope %@", scope);
NSURL* url = [NSURL URLWithString:@"https://login.live.com/oauth20_token.srf"];
NSMutableURLRequest* request =
[NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:MsaTokenRequestTimeout];
[request addValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
NSMutableDictionary<NSString*, NSString*>* params = [NSMutableDictionary<NSString*, NSString*> new];
[params setObject:clientId forKey:@"client_id"];
[params setObject:grantType forKey:@"grant_type"];
if ([grantType isEqualToString:MsaTokenRequestGrantTypeCode])
{
[params setObject:redirectUri forKey:@"redirect_uri"];
[params setObject:token forKey:@"code"];
}
else if ([grantType isEqualToString:MsaTokenRequestGrantTypeRefresh])
{
if (scope)
{
[params setObject:scope forKey:@"scope"];
}
[params setObject:token forKey:MsaTokenRequestGrantTypeRefresh];
}
request.HTTPBody = EncodeDictionary(params);
request.HTTPMethod = @"POST";
static NSOperationQueue* queue = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ queue = [NSOperationQueue new]; });
NSLog(@"MSATokenRequest issuing HTTP token request.");
[NSURLConnection sendAsynchronousRequest:request
queue:queue
completionHandler:^void(NSURLResponse* response, NSData* data, NSError* error) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response; // This cast should always work
NSLog(@"MSATokenRequest response code %ld.", (long)httpResponse.statusCode);
MSATokenRequestStatus status = MSATokenRequestStatusTransientFailure;
if (httpResponse.statusCode >= 500)
{
status = MSATokenRequestStatusTransientFailure;
}
else if (httpResponse.statusCode >= 400)
{
status = MSATokenRequestStatusPermanentFailure;
}
else if ((httpResponse.statusCode >= 200 && httpResponse.statusCode < 300) || httpResponse.statusCode == 304)
{
status = MSATokenRequestStatusSuccess;
}
else
{
status = MSATokenRequestStatusTransientFailure;
}
if (data)
{
NSDictionary* responseDict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(@"MSATokenRequest data:%@", responseDict);
callback([MSATokenRequestResult resultWithStatus:status responseDictionary:responseDict]);
}
else
{
NSLog(@"MSATokenRequest error:%@", error);
callback([MSATokenRequestResult resultWithStatus:status responseDictionary:nil]);
}
}];
}
+ (instancetype)tokenRequestWithClientId:(NSString*)clientId
grantType:(NSString*)grantType
scope:(NSString*)scope
redirectUri:(NSString*)redirectUri
{
return [[self alloc] initWithClientId:clientId grantType:grantType scope:scope redirectUri:redirectUri];
}
- (instancetype)initWithClientId:(NSString*)clientId
grantType:(NSString*)grantType
scope:(NSString*)scope
redirectUri:(NSString*)redirectUri
{
if (self = [super init])
{
_clientId = [clientId copy];
_grantType = [grantType copy];
_scope = [scope copy];
_redirectUri = [redirectUri copy];
}
return self;
}
- (void)requestAsyncWithToken:(NSString*)token callback:(void (^)(MSATokenRequestResult*))callback
{
[[self class] doAsyncRequestWithClientId:_clientId
grantType:_grantType
scope:_scope
redirectUri:_redirectUri
token:token
callback:callback];
}
@end

Просмотреть файл

@ -1,23 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#pragma once
#import <Foundation/Foundation.h>
// @brief MSA failure reason for sign in or sign out action
typedef NS_ENUM(NSInteger, SampleAccountActionFailureReason)
{
SampleAccountActionNoFailure,
SampleAccountActionFailureReasonAlreadySignedIn,
SampleAccountActionFailureReasonAlreadySignedOut,
SampleAccountActionFailureReasonUserCancelled,
SampleAccountActionFailureReasonFailToRetrieveAuthCode,
SampleAccountActionFailureReasonFailToRetrieveRefreshToken,
SampleAccountActionFailureReasonSigninSignOutInProgress,
SampleAccountActionFailureReasonUnknown,
SampleAccountActionFailureReasonADAL,
};
typedef void (^SampleAccountProviderCompletionBlock)(BOOL successful, SampleAccountActionFailureReason reason);

Просмотреть файл

@ -1,17 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#pragma once
#import <ConnectedDevices/Core/Core.h>
#import "SampleAccountActionFailureReason.h"
// @brief Protocol for a MCDUserAccountProvider that supports logging into/out of a single user account.
@protocol SingleUserAccountProvider <MCDUserAccountProvider>
- (void)signInWithCompletionCallback:(nonnull SampleAccountProviderCompletionBlock)callback;
- (void)signOutWithCompletionCallback:(nonnull SampleAccountProviderCompletionBlock)callback;
@property(readonly, atomic) BOOL signedIn;
@end

Просмотреть файл

@ -0,0 +1,67 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#import <ConnectedDevices/MCDConnectedDevicesPlatform.h>
#import <Foundation/Foundation.h>
#import <PromiseKit/PromiseKit.h>
#import "MSAAccount.h"
#ifndef ConnectedDevicesPlatformManager_h
#define ConnectedDevicesPlatformManager_h
@class Account;
@class ConnectedDevicesPlatformManager;
@interface APNSManager : NSObject
- (AnyPromise*)getNotificationRegistrationAsync:(Account*)account;
- (void)setNotificationRegistration:(MCDConnectedDevicesNotificationRegistration*)registration accounts:(NSArray<Account*>*)accounts;
@end
typedef NS_ENUM(NSInteger, AccountRegistrationState) {
AccountRegistrationStateInAppCacheAndSdkCache,
AccountRegistrationStateInAppCacheOnly,
AccountRegistrationStateInSdkCacheOnly
};
@interface Account : NSObject
- (instancetype)initWithMSAAccount:(MSAAccount*)msaAccount platform:(MCDConnectedDevicesPlatform*)platform apnsManager:(APNSManager*)apnsManager;
- (instancetype)initWithMCDAccount:(MCDConnectedDevicesAccount*)account state:(AccountRegistrationState)state platform:(MCDConnectedDevicesPlatform*)platform apnsManager:(APNSManager*)apnsManager;
- (AnyPromise*)prepareAccountAsync:(ConnectedDevicesPlatformManager*)platformManager;
- (AnyPromise*)getAccessTokenAsync:(NSArray<NSString*>*)scopes;
- (AnyPromise*)registerWithSdkAsync;
- (AnyPromise*)signOutAsync;
@property(nonatomic) AccountRegistrationState state;
@property(nonatomic) MCDConnectedDevicesAccount* mcdAccount;
@property(nonatomic) MCDConnectedDevicesPlatform* platform;
@property(nonatomic) APNSManager* apnsManager;
@end
@protocol ConnectedDevicesPlatformManagerDelegate
- (void)accountListDidUpdate:(NSArray<Account*>*)accounts;
@end
// This is a singleton object which holds onto the app's ConnectedDevicesPlatform and
// handles account management.
@interface ConnectedDevicesPlatformManager : NSObject
+ (instancetype)sharedInstance;
@property(nonatomic) MCDConnectedDevicesPlatform* platform;
@property(atomic) NSMutableArray<Account*>* accounts;
@property(nonatomic, weak) id<ConnectedDevicesPlatformManagerDelegate> delegate;
@property(nonatomic) APNSManager* apnsManager;
@property(nonatomic) AnyPromise* accountsPromise;
- (AnyPromise*)signInMsaAsync;
- (AnyPromise*)signOutAsync:(Account*)account;
- (NSMutableArray<Account*>*)deserializeAccounts;
- (AnyPromise*)prepareAccountsAsync:(NSMutableArray<Account*>*)accountList;
- (void)setNotificationRegistration:(MCDConnectedDevicesNotificationRegistration*)registration;
@end
#endif /* ConnectedDevicesPlatformManager_h */

Просмотреть файл

@ -0,0 +1,496 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#import "ConnectedDevicesPlatformManager.h"
#import <ConnectedDevicesRemoteSystems/ConnectedDevicesRemoteSystems.h>
#import <ConnectedDevicesRemoteSystemsCommanding/ConnectedDevicesRemoteSystemsCommanding.h>
#import <ConnectedDevicesUserData/ConnectedDevicesUserData.h>
#import <ConnectedDevicesUserDataUserActivities/ConnectedDevicesUserDataUserActivities.h>
#import "InboundRequestLogger.h"
#import "MSAAccount.h"
#import "Secrets.h"
@implementation APNSManager {
NSMutableDictionary<NSString*, PMKAdapter>* _pendingOperationBlocks;
MCDConnectedDevicesNotificationRegistration* _notificationRegistration;
}
- (instancetype)init {
if (self = [super init]) {
_pendingOperationBlocks = [NSMutableDictionary new];
}
return self;
}
- (AnyPromise*)getNotificationRegistrationAsync:(Account*)account {
@synchronized (self) {
if (_notificationRegistration != nil) {
return [AnyPromise promiseWithValue:_notificationRegistration];
} else {
// If there isn't already a notification registration available, set up a pending operation to complete when one does
// become available. NOTE: this code uses the accountId as the key which is not guaranteed to be unique across account types.
// If your app uses multiple account types that may have conflicting ids, use a different unique key.
AnyPromise* pendingOperation = [AnyPromise promiseWithAdapterBlock:^(PMKAdapter _Nonnull adapter) {
[self->_pendingOperationBlocks setObject:adapter forKey:account.mcdAccount.accountId];
}];
return pendingOperation;
}
}
}
- (void)setNotificationRegistration:(MCDConnectedDevicesNotificationRegistration*)registration accounts:(NSArray<Account*>*)accounts{
NSMutableDictionary<NSString*, PMKAdapter>* pendingOperationBlocksToComplete;
BOOL needsUpdating = NO;
@synchronized (self) {
// NOTE: this code assumes that the token is the only piece of the notificaiton registration that is changing.
// If your app uses multiple notification providers, wants to change other information, etc. this logic needs
// to be updated to accomdate those situations.
needsUpdating = [_notificationRegistration.token isEqualToString:registration.token];
pendingOperationBlocksToComplete = _pendingOperationBlocks;
_pendingOperationBlocks = [NSMutableDictionary new];
_notificationRegistration = registration;
}
// Complete any pending requests to get the notificaiton registration.
for (NSString* accountKey in pendingOperationBlocksToComplete) {
PMKAdapter adapter = pendingOperationBlocksToComplete[accountKey];
adapter(registration, nil);
}
if (needsUpdating) {
for (Account* account in accounts) {
// Only reregister the accounts that didn't have pending operations get completed just above.
// Also make sure that only accounts in good standing are registered.
//
// NOTE: this code uses the accountId as the key which is not guaranteed to be unique across account types.
// If your app uses multiple account types that may have conflicting ids, use a different unique key.
if (account.state == AccountRegistrationStateInAppCacheAndSdkCache && nil == [pendingOperationBlocksToComplete objectForKey:account.mcdAccount.accountId]) {
[account registerWithSdkAsync];
}
}
}
}
@end
@interface Account () {
MSAAccount* _msaAccount;
}
- (void)clearSubcomponents;
@end
@implementation Account
- (instancetype)initWithMCDAccount:(MCDConnectedDevicesAccount*)mcdAccount state:(AccountRegistrationState)state platform:(MCDConnectedDevicesPlatform*)platform apnsManager:(APNSManager*)apnsManager {
if (self = [super init]) {
self.mcdAccount = mcdAccount;
self.state = state;
self.platform = platform;
self.apnsManager = apnsManager;
}
return self;
}
- (instancetype)initWithMSAAccount:(MSAAccount *)msaAccount platform:(MCDConnectedDevicesPlatform*)platform apnsManager:(APNSManager*)apnsManager {
if (self = [super init]) {
_msaAccount = msaAccount;
if (!_msaAccount.isSignedIn) {
return nil;
}
self.mcdAccount = _msaAccount.mcdAccount;
self.platform = platform;
self.apnsManager = apnsManager;
}
return self;
}
- (AnyPromise*)prepareAccountAsync:(ConnectedDevicesPlatformManager*)platformManager {
// Accounts can be in 3 different scenarios:
// 1: cached account in good standing (initialized in the SDK and our token cache).
// 2: account missing from the SDK but present in our cache: Add and initialize account.
// 3: account missing from our cache but present in the SDK. Log the account out async.
// Subcomponents (e.g. UserDataFeed or RemoteSystemAppRegistration) should only be initialized when an account is in both the app cache
// and the SDK cache.
// For scenario 1, initialize our subcomponents. This is the only case that can have incoming notifications and thus
// the asynchronous portion of preparing an account need not be waited before processing incoming notifications.
// For scenario 2, subcomponents will be initialized after InitializeAccountAsync registers the account with the SDK. Because this is a new account, an incoming notification could only
// be for this account if was an account that was previously added, then removed, and now being added back. In this case its just a race condition that
// does not matter if it can't be routed as the sender is using out of date information that just happens to align with what is soon to be the new information.
// For scenario 3, InitializeAccountAsync will unregister the account and subcomponents will never be initialized (and therefore any notifications for it will be dropped).
if (self.state == AccountRegistrationStateInAppCacheAndSdkCache) {
// Scenario 1
[self initializeSubcomponents];
return [self registerWithSdkAsync];
} else if (self.state == AccountRegistrationStateInAppCacheOnly){
// Scenario 2, add the account to the SDK
AnyPromise* addAccountPromise = [AnyPromise promiseWithAdapterBlock:^(PMKAdapter _Nonnull adapter) {
[self.platform.accountManager addAccountAsync:self.mcdAccount callback:adapter];
}];
addAccountPromise.catch(^ {
[platformManager.accounts removeObject:self];
});
return addAccountPromise.then(^(MCDConnectedDevicesAddAccountResult* result) {
switch (result.status) {
case MCDConnectedDevicesAccountAddedStatusSuccess:
self.state = AccountRegistrationStateInAppCacheAndSdkCache;
[self initializeSubcomponents];
return [self registerWithSdkAsync];
case MCDConnectedDevicesAccountAddedStatusErrorServiceFailed:
// If the service failed, ideally this could be tried again later but this
// simple app will just fail.
break;
case MCDConnectedDevicesAccountAddedStatusErrorNoNetwork:
// If there is no network, ideally this could be tried again later but this
// simple app will just fail.
break;
case MCDConnectedDevicesAccountAddedStatusErrorTokenRequestFailed:
// Token request failed! Make sure that the token library is properly configured.
break;
case MCDConnectedDevicesAccountAddedStatusErrorNoTokenRequestSubscriber:
// This means that the event is no longer being listened to and therefore can't complete the
// request. This indidcates a programming error.
break;
case MCDConnectedDevicesAccountAddedStatusErrorUnknown:
// This means that something totally unknown happened. This should not happen in normal operation.
break;
}
@throw NSGenericException;
});
} else {
// Scenario 3, remove the account from the SDK
return [AnyPromise promiseWithAdapterBlock:^(PMKAdapter _Nonnull adapter) {
[self.platform.accountManager removeAccountAsync:self.mcdAccount callback:adapter];
}].ensure(^{
[platformManager.accounts removeObject:self];
});
}
}
- (AnyPromise*)registerWithSdkAsync
{
if (self.state != AccountRegistrationStateInAppCacheAndSdkCache) {
return [AnyPromise promiseWithValue:[NSError errorWithDomain:@"AccountException" code:0 userInfo:nil]];
}
return [self.apnsManager getNotificationRegistrationAsync:self].then(^(MCDConnectedDevicesNotificationRegistration* registration) {
NSLog(@"Registering APNS with ConnectedDevicesPlatform");
return [AnyPromise promiseWithBooleanAdapterBlock:^(PMKBooleanAdapter _Nonnull adapter) {
[self.platform.notificationRegistrationManager registerForAccountAsync:self.mcdAccount registration:registration callback:adapter];
}];
}).then(^ {
// Do operations that require notification registration now. Like saving the RemoteSystemAppRegistration or saving UserDataFeed sync scopes
// Until the RemoteSystemAppRegistration is successfully saved, any outgoing communication to remote apps may
// not receive responses as the remote app will not necessarily know who it is communicating with.
MCDRemoteSystemAppRegistration* registration =
[MCDRemoteSystemAppRegistration getForAccount:self.mcdAccount platform:self.platform];
[registration saveAsync:^(BOOL completed, NSError* error) {
// A more complete sample would properly gate any outbound communication until this point.
if (!completed || error)
{
NSLog(@"Failed to register remote system");
}
else
{
NSLog(@"Successfully registered remote system.");
}
}];
// For UserDataFeed, adjust the sync scopes so that the types the app cares about synced down. Until this completes, the app will not
// get data of the desired type (UserActivities vs UserNotifications) and will not receive notifications when the data changes.
MCDUserDataFeed* userDataFeed = [MCDUserDataFeed getForAccount:self.mcdAccount
platform:self.platform
activitySourceHost:CROSS_PLATFORM_APP_ID];
NSArray<id<MCDUserDataFeedSyncScope>>* syncScopes = @[ [MCDUserActivityChannel syncScope] ];
[userDataFeed subscribeToSyncScopesAsync:syncScopes
callback:^(BOOL success __unused, NSError* _Nullable error __unused) {
// Based on your app's needs this could be a good place to start syncing down activity feeds etc.
}];
});
}
- (AnyPromise*)signOutAsync
{
// First remove the account out from the ConnectedDevices SDK. The SDK may call back for access tokens to perform
// unregistration with services
[self clearSubcomponents];
return [AnyPromise promiseWithAdapterBlock:^(PMKAdapter _Nonnull adapter) {
[self.platform.accountManager removeAccountAsync:self.mcdAccount callback:adapter];
}].then(^{
// After its gone from the sdk, it is safe to sign out from the token library and clean up the account list.
self.state = AccountRegistrationStateInAppCacheOnly;
return [AnyPromise promiseWithAdapterBlock:^(PMKAdapter _Nonnull adapter) {
[_msaAccount signOutWithCompletionCallback:adapter];
}];
});
}
- (void)initializeSubcomponents {
// Do initial per account immediate initialization work. Like creating the RemoteSystemAppRegistration or getting a UserDataFeed.
// First up is the RemoteSystemAppRegistration
MCDRemoteSystemAppRegistration* registration =
[MCDRemoteSystemAppRegistration getForAccount:self.mcdAccount platform:self.platform];
[registration setAttributes:@{ @"sample_key" : @"sample_value" }];
NSMutableArray* providers = [NSMutableArray array];
// Add each of the implemented AppServicesProviders to the registration
AppServiceProvider* appServiceProvider = [[AppServiceProvider alloc] initWithDelegate:[InboundRequestLogger sharedInstance]];
[providers addObject:appServiceProvider];
[registration setLaunchUriProvider:[[LaunchUriProvider alloc] initWithDelegate:[InboundRequestLogger sharedInstance]]];
[registration setAppServiceProviders:providers];
// Now handle UserDataFeed
MCDUserDataFeed* userDataFeed = [MCDUserDataFeed getForAccount:self.mcdAccount
platform:self.platform
activitySourceHost:CROSS_PLATFORM_APP_ID];
[userDataFeed.syncStatusChanged subscribe:^(MCDUserDataFeed* _Nonnull sender, MCDUserDataFeedSyncStatusChangedEventArgs* _Nonnull __unused args) {
NSLog(@"SyncStatus is %ld", (long)sender.syncStatus);
}];
}
- (void)clearSubcomponents {
// If your app needs to stop using a sub component for some reason, this would be a good place to reset a user data feed for instance.
}
- (AnyPromise*)getAccessTokenAsync:(NSArray<NSString*>*)scopes {
return [AnyPromise promiseWithAdapterBlock:^(PMKAdapter _Nonnull adapter) {
[_msaAccount getAccessTokenForUserAccountIdAsync:self.mcdAccount.accountId scopes:scopes completion:adapter];
}];
}
@end
@interface ConnectedDevicesPlatformManager ()
- (void)serializeAccountsToCache;
- (void)accountListChanged;
@end
@implementation ConnectedDevicesPlatformManager
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
static ConnectedDevicesPlatformManager* sharedInstance;
dispatch_once(&onceToken, ^{ sharedInstance = [[ConnectedDevicesPlatformManager alloc] init]; });
return sharedInstance;
}
- (instancetype)init {
if (self = [super init]) {
self.accounts = [NSMutableArray new];
self.apnsManager = [APNSManager new];
// Construct and initialize a platform. All we are doing here is hooking up event handlers before
// calling ConnectedDevicesPlatform Start. After Start is called events may begin to fire.
self.platform = [MCDConnectedDevicesPlatform new];
__weak ConnectedDevicesPlatformManager* weakSelf = self;
[weakSelf.platform.accountManager.accessTokenRequested subscribe:^(MCDConnectedDevicesAccountManager* _Nonnull manager, MCDConnectedDevicesAccessTokenRequestedEventArgs* _Nonnull args) {
NSLog(@"Token requested by platform for %@ and %@", args.request.account.accountId, [args.request.scopes componentsJoinedByString:@","]);
Account* account = nil;
if (weakSelf.accounts.count > 0) {
NSInteger index = [weakSelf.accounts indexOfObjectPassingTest:^BOOL (Account* account, NSUInteger index, BOOL* stop) {
return ([account.mcdAccount.accountId isEqualToString: args.request.account.accountId] && account.mcdAccount.type == args.request.account.type) ? YES : NO;
}];
if (index != NSNotFound) {
account = [weakSelf.accounts objectAtIndex:index];
}
}
if (account != nil) {
[account getAccessTokenAsync:args.request.scopes].then(^(NSString* token) {
NSLog(@"Token Request Succeeded");
[args.request completeWithAccessToken:token];
}).catch(^(NSError* error) {
NSLog(@"Token Request Failed. Could not get token.");
[args.request completeWithErrorMessage:error.localizedDescription];
});
} else {
NSLog(@"Token Request Failed. Could not find account.");
[args.request completeWithErrorMessage:@"Token Request Failed. Could not find account."];
}
}];
[self.platform.accountManager.accessTokenInvalidated
subscribe:^(MCDConnectedDevicesAccountManager* _Nonnull manager __unused,
MCDConnectedDevicesAccessTokenInvalidatedEventArgs* _Nonnull request) {
NSLog(@"Token invalidated for account: %@", request.account.accountId);
}];
[self.platform.notificationRegistrationManager.notificationRegistrationStateChanged subscribe:^(MCDConnectedDevicesNotificationRegistrationManager* _Nonnull manager __unused, MCDConnectedDevicesNotificationRegistrationStateChangedEventArgs* _Nonnull args) {
switch (args.state) {
case MCDConnectedDevicesNotificationRegistrationStateRegistered:
NSLog(@"Notifications registered for account ID: %@", args.account.accountId);
break;
case MCDConnectedDevicesNotificationRegistrationStateUnregistered:
NSLog(@"Notifications unregistered for account ID: %@", args.account.accountId);
break;
case MCDConnectedDevicesNotificationRegistrationStateExpired:
case MCDConnectedDevicesNotificationRegistrationStateExpiring:
{
// Because the notificaiton registration is expiring, the per account registration work needs to be kicked off again.
// This means registering with the NotificationRegistrationManager as well as any sub component work like RemoteSystemAppRegistration.
NSLog(@"Notifications expiring / expired for account ID: %@", args.account.accountId);
Account* account = nil;
if (weakSelf.accounts.count > 0) {
account = [weakSelf.accounts objectAtIndex:[weakSelf.accounts indexOfObjectPassingTest:^BOOL (Account* account, NSUInteger index, BOOL* stop) {
return ([account.mcdAccount.accountId isEqualToString: args.account.accountId] && account.mcdAccount.type == args.account.type) ? YES : NO;
}]];
}
if (account != nil && account.state == AccountRegistrationStateInAppCacheAndSdkCache) {
[account registerWithSdkAsync];
}
}
break;
default:
break;
}
}];
[self.platform start];
// Pull the accounts from our app's cache and synchronize the list with the apps cached by
// ConnectedDevicesPlatform AccountManager.
self.accounts = [self deserializeAccounts];
// Finally initialize the accounts. This will refresh registrations when needed, add missing accounts,
// and remove stale accounts from the ConnectedDevicesPlatform AccountManager. The promise associated
// with all of this asynchronous work need not be waited on as any sub component work will be accomplished
// in the synchronous portion of the call. If your app needs to sequence when other apps can see this app's registration
// (i.e. when RemoteSystemAppRegistration SaveAsync completes) then it would be useful to use the promise returned by
// prepareAccountsAsync
self.accountsPromise = [self prepareAccountsAsync];
}
return self;
}
- (NSMutableArray<Account*>*)deserializeAccounts {
// Add all cached accounts from the platform.
NSMutableArray<MCDConnectedDevicesAccount*>* sdkCachedAccounts = [NSMutableArray arrayWithArray:self.platform.accountManager.accounts];
// Ideally the token library would support multiple app cached accounts; If this is nil then there is no account the app knows about.
Account* appCachedAccount = [[Account alloc] initWithMSAAccount:[[MSAAccount alloc] initWithClientId:CLIENT_ID scopeOverrides:@{}] platform:self.platform apnsManager:self.apnsManager];
NSMutableArray<Account*>* accountList = [NSMutableArray new];
if (appCachedAccount != nil) {
MCDConnectedDevicesAccount* matchingAccount = nil;
for (MCDConnectedDevicesAccount* account in sdkCachedAccounts) {
if ([appCachedAccount.mcdAccount.accountId isEqualToString:account.accountId] && appCachedAccount.mcdAccount.type == account.type) {
matchingAccount = account;
break;
}
}
if (matchingAccount != nil) {
appCachedAccount.state = AccountRegistrationStateInAppCacheAndSdkCache;
[sdkCachedAccounts removeObject:matchingAccount];
} else {
appCachedAccount.state = AccountRegistrationStateInAppCacheOnly;
}
[accountList addObject:appCachedAccount];
}
// Add the remaining SDK only accounts (these need to be removed from the SDK)
for (MCDConnectedDevicesAccount* account in sdkCachedAccounts) {
[accountList addObject:[[Account alloc] initWithMCDAccount:account state:AccountRegistrationStateInSdkCacheOnly platform:self.platform apnsManager:self.apnsManager]];
}
return accountList;
}
- (AnyPromise*)prepareAccountsAsync {
NSMutableArray<AnyPromise*>* promises = [NSMutableArray new];
for (Account* account in self.accounts) {
[promises addObject:[account prepareAccountAsync:self]];
}
return PMKWhen(promises).then(^(NSArray* __unused promises) {
[self accountListChanged];
});
}
- (void)accountListChanged {
[self.delegate accountListDidUpdate:self.accounts];
[self serializeAccountsToCache];
}
- (void)serializeAccountsToCache {
// Unlike the samples on other platforms, since the token helpers don't support multiple accounts and handle persisting the token,
// there is nothing to do here. At most the app can know about one account. If your token library does support multiple accounts, it would
// be ideal to make sure that accounts are always persisted out to disk when the set of accounts changes.
}
- (AnyPromise*)signInMsaAsync {
MSAAccount* msaAccount = [[MSAAccount alloc] initWithClientId:CLIENT_ID scopeOverrides:@{}];
return [AnyPromise promiseWithAdapterBlock:^(PMKAdapter _Nonnull adapter) {
[msaAccount signInWithCompletionCallback:adapter];
}].then(^{
Account* account = [[Account alloc] initWithMSAAccount:msaAccount platform:self.platform apnsManager:self.apnsManager];
account.state = AccountRegistrationStateInAppCacheOnly;
[self.accounts addObject:account];
return [account prepareAccountAsync:self];
}).then(^{
[self accountListChanged];
});
}
- (AnyPromise*)signOutAsync:(Account*)account {
return [account signOutAsync].then(^{
[self.accounts removeObjectAtIndex:[self.accounts indexOfObjectPassingTest:^BOOL(Account * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
return [obj.mcdAccount.accountId isEqualToString:account.mcdAccount.accountId] && obj.mcdAccount.type == account.mcdAccount.type;
}]];
[self accountListChanged];
});
}
- (void)setNotificationRegistration:(NSString*)tokenString {
MCDConnectedDevicesNotificationRegistration* registration = [MCDConnectedDevicesNotificationRegistration new];
if ([[UIApplication sharedApplication] isRegisteredForRemoteNotifications])
{
registration.type = MCDNotificationTypeAPN;
}
else
{
registration.type = MCDNotificationTypePolling;
}
registration.appId = [[NSBundle mainBundle] bundleIdentifier];
registration.appDisplayName = (NSString*)[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
registration.token = tokenString;
// The two cases of receiving a new notification token are:
// 1. A notification registration is asked for and now it is available. In this case there is a pending promise that was made
// at the time of requesting the information. It now needs completed.
// 2. The account is already registered but for whatever reason the registration changes (APNS gives the app a new token)
//
// In order to most cleany handle both cases set the new notification information and then trigger a re registration of all accounts
// that are in good standing.
[self.apnsManager setNotificationRegistration:registration accounts:self.accounts];
}
@end

Просмотреть файл

@ -4,12 +4,10 @@
#pragma once
#import <ConnectedDevices/Core/MCDPlatform.h>
#import <UIKit/UIKit.h>
@interface IdentityViewController : UIViewController
@property(weak, nonatomic) IBOutlet UILabel* loginStatusLabel;
@property(weak, nonatomic) IBOutlet UIButton* loginButton;
@end

Просмотреть файл

@ -3,72 +3,102 @@
//
#import "IdentityViewController.h"
#import "AppDataSource.h"
#import "MSAAccountProvider.h"
#import "ConnectedDevicesPlatformManager.h"
#import "MainNavigationController.h"
#import <ConnectedDevices/Core/MCDPlatform.h>
#import <UIKit/UIKit.h>
@interface IdentityViewController ()
@property(nonatomic, strong) UITextView* textView;
@end
@implementation IdentityViewController
@implementation IdentityViewController {
ConnectedDevicesPlatformManager* _platformManager;
}
- (instancetype)initWithNibName:(NSString *)nibNameOrNil
bundle:(NSBundle *)nibBundleOrNil {
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
_platformManager = [ConnectedDevicesPlatformManager sharedInstance];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
_platformManager = [ConnectedDevicesPlatformManager sharedInstance];
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
if ([AppDataSource sharedInstance].accountProvider.signedIn)
{
// Wait just long enough for this ViewController to be added to the stack before trying to transition
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 10), dispatch_get_main_queue(), ^{ [self _transitionToMainViewController]; });
}
[self _setStatusText:@"Checking accounts"];
}
- (void)viewWillAppear:(BOOL)animated
{
if ([AppDataSource sharedInstance].accountProvider.signedIn)
{
[self _setStatusText:@"Currently signed in"];
[_loginButton setTitle:(@"Sign Out") forState:UIControlStateNormal];
}
else
{
[self _setStatusText:@"Currently signed out"];
[_loginButton setTitle:(@"Sign In") forState:UIControlStateNormal];
}
_platformManager.accountsPromise.then(^{
// This logic would be better if the UI supported more than one account. Since this is a simple app, just check for one account
// that is synced to key off of for sign in state.
NSArray<Account*>* accounts = _platformManager.accounts;
Account* account = nil;
if (accounts.count > 0) {
account = [accounts objectAtIndex:[accounts indexOfObjectPassingTest:^BOOL (Account* account, NSUInteger index, BOOL* stop) {
return account.state == AccountRegistrationStateInAppCacheAndSdkCache;
}]];
}
if (account != nil)
{
[self _setStatusText:@"Currently signed in"];
[_loginButton setTitle:(@"Sign Out") forState:UIControlStateNormal];
[self _transitionToMainViewController];
}
else
{
[self _setStatusText:@"Currently signed out"];
[_loginButton setTitle:(@"Sign In") forState:UIControlStateNormal];
}
});
}
- (IBAction)loginButtonPressed:(id)sender
{
// Currently, this sample only supports MSA accounts
if (![AppDataSource sharedInstance].accountProvider.signedIn)
{
[self _setStatusText:@"Signing in.."];
// Sign in
[[AppDataSource sharedInstance].accountProvider
signInWithCompletionCallback:^(BOOL successful, SampleAccountActionFailureReason reason) {
[self _setStatusText:[NSString stringWithFormat:@"%@ [%ld]", (successful ? @"Currently signed in" : @"Sign in failed"),
(long)reason]];
[self.loginButton setTitle:(@"Sign Out") forState:UIControlStateNormal];
if (successful)
{
// Once sign-in has completed successfully, it's time to initialize the platform in sdkViewController
[self _transitionToMainViewController];
}
}];
- (IBAction)loginButtonPressed:(id)sender {
// Currently, this sample only supports a single MSA account. Just find the first account in good standing to log out.
Account* account = nil;
if (_platformManager.accounts.count > 0) {
NSInteger index = [_platformManager.accounts indexOfObjectPassingTest:^BOOL (Account* account, NSUInteger index, BOOL* stop) {
return account.state == AccountRegistrationStateInAppCacheAndSdkCache;
}];
if (index != NSNotFound) {
account = [_platformManager.accounts objectAtIndex:index];
}
}
else
{
[[AppDataSource sharedInstance].accountProvider
signOutWithCompletionCallback:^(BOOL successful, SampleAccountActionFailureReason reason) {
[self _setStatusText:[NSString stringWithFormat:@"%@ [%ld]", (successful ? @"Currently signed out" : @"Sign out failed"),
(long)reason]];
[self.loginButton setTitle:(@"Sign In") forState:UIControlStateNormal];
}];
if (account == nil) {
[self _setStatusText:@"Signing in.."];
// Perform actual sign in
[_platformManager signInMsaAsync].then(^{
[self _setStatusText:[NSString stringWithFormat:@"Currently signed in"]];
[self.loginButton setTitle:(@"Sign Out") forState:UIControlStateNormal];
[self _transitionToMainViewController];
}).catch(^(NSError* error){
NSLog(@"%@", error);
[self _setStatusText:[NSString stringWithFormat:@"Sign in failed!"]];
});
} else {
[_platformManager signOutAsync:account].then(^{
[self _setStatusText:[NSString stringWithFormat:@"Currently signed out"]];
[self.loginButton setTitle:(@"Sign In") forState:UIControlStateNormal];
}).catch(^(NSError* error){
NSLog(@"%@", error);
[self _setStatusText:[NSString stringWithFormat:@"Sign out failed!"]];
});
}
}

Просмотреть файл

@ -12,6 +12,7 @@
@end
@interface InboundRequestLogger : NSObject <LaunchUriProviderDelegate, AppServiceProviderDelegate>
+ (instancetype)sharedInstance;
@property(nonatomic, weak) id<InboundRequestLoggerDelegate> delegate;
@property(nonatomic, readonly, copy) NSString* log;
@end

Просмотреть файл

@ -6,6 +6,14 @@
@implementation InboundRequestLogger
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
static InboundRequestLogger* sharedInstance;
dispatch_once(&onceToken, ^{ sharedInstance = [[InboundRequestLogger alloc] init]; });
return sharedInstance;
}
- (instancetype)init
{
if (self = [super init])

Просмотреть файл

@ -24,6 +24,10 @@
<string></string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>Launch Screen.storyboard</string>
<key>UIMainStoryboardFile</key>

Просмотреть файл

@ -4,7 +4,7 @@
#pragma once
#import <ConnectedDevices/RemoteSystems/MCDRemoteSystem.h>
#import <ConnectedDevicesRemoteSystems/MCDRemoteSystem.h>
#import <UIKit/UIKit.h>
@interface LaunchAndMessageViewController : UIViewController

Просмотреть файл

@ -4,8 +4,8 @@
#import "LaunchAndMessageViewController.h"
#import "Secrets.h"
#import <ConnectedDevices/RemoteSystems.Commanding/RemoteSystems.Commanding.h>
#import <ConnectedDevices/RemoteSystems/MCDRemoteSystemApp.h>
#import <ConnectedDevicesRemoteSystemsCommanding/ConnectedDevicesRemoteSystemsCommanding.h>
#import <ConnectedDevicesRemoteSystems/MCDRemoteSystemApp.h>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@ -80,13 +80,14 @@
MCDAppServiceConnection* connection = nil;
@synchronized(self)
{
connection = _appServiceConnection;
if (!connection)
if (!_appServiceConnection)
{
connection = _appServiceConnection = [MCDAppServiceConnection new];
connection.appServiceInfo = [MCDAppServiceInfo infoWithName:APP_SERVICE_NAME packageId:PACKAGE_ID];
_serviceClosedRegistration = [connection.serviceClosed subscribe:^(__unused MCDAppServiceConnection* connection,
MCDAppServiceClosedEventArgs* args) { [self appServiceConnection:connection closedWithStatus:args.status]; }];
_appServiceConnection = _appServiceConnection = [MCDAppServiceConnection new];
_appServiceConnection.appServiceInfo = [MCDAppServiceInfo infoWithName:APP_SERVICE_NAME packageId:PACKAGE_ID];
__weak LaunchAndMessageViewController* weakSelf = self;
[_appServiceConnection.serviceClosed subscribe:^(__unused MCDAppServiceConnection* connection,
MCDAppServiceClosedEventArgs* args) { [weakSelf appServiceConnection:connection closedWithStatus:args.status]; }];
}
}
@ -167,7 +168,7 @@
return @{
@"Type" : @"ping",
@"CreationDate" : [_dateFormatter stringFromDate:[NSDate date]],
@"TargetId" : _selectedApplication.identifier
@"TargetId" : _selectedApplication.appId
};
}

Просмотреть файл

@ -4,7 +4,7 @@
#pragma once
#import <ConnectedDevices/RemoteSystems.Commanding/RemoteSystems.Commanding.h>
#import <ConnectedDevicesRemoteSystemsCommanding/ConnectedDevicesRemoteSystemsCommanding.h>
@class LaunchUriProvider;

Просмотреть файл

@ -1,14 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#pragma once
#import <ConnectedDevices/Core/Core.h>
#import <UIKit/UIKit.h>
// @brief provides an sample implementation of MCDNotificationProvider
@interface NotificationProvider : NSObject <MCDNotificationProvider>
// @brief class method update the notification registration provider with new notification registration
- (void)updateNotificationRegistration:(MCDNotificationRegistration*)notificationRegistration;
@end

Просмотреть файл

@ -1,48 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#import "NotificationProvider.h"
#import <ConnectedDevices/Core/MCDPlatform.h>
#import <UIKit/UIKit.h>
@implementation NotificationProvider
{
MCDRegistrationUpdatedEvent* _registrationUpdated;
MCDNotificationRegistration* _notificationRegistration;
}
- (instancetype)init
{
if (self = [super init])
{
_registrationUpdated = [MCDRegistrationUpdatedEvent new];
}
return self;
}
- (void)updateNotificationRegistration:(MCDNotificationRegistration*)notificationRegistration
{
NotificationProvider* __weak weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{ [weakSelf _updateNotificationRegistration:notificationRegistration]; });
}
#pragma mark - MCDNotificationProvider Protocol Requirements
@synthesize registrationUpdated = _registrationUpdated;
- (void)getNotificationRegistrationAsync:(nonnull void (^)(MCDNotificationRegistration* _Nullable, NSError* _Nullable))completionBlock
{
dispatch_async(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ completionBlock(self->_notificationRegistration, nil); });
}
- (void)_updateNotificationRegistration:(MCDNotificationRegistration*)notificationRegistration
{
_notificationRegistration = notificationRegistration;
[_registrationUpdated raiseWithNotificationRegistration:notificationRegistration];
}
@end

Просмотреть файл

@ -4,8 +4,8 @@
#pragma once
#import <ConnectedDevices/RemoteSystems/MCDRemoteSystemFilter.h>
#import <ConnectedDevices/RemoteSystems/MCDRemoteSystemWatcher.h>
#import <ConnectedDevicesRemoteSystems/MCDRemoteSystemFilter.h>
#import <ConnectedDevicesRemoteSystems/MCDRemoteSystemWatcher.h>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

Просмотреть файл

@ -5,14 +5,14 @@
#import "RemoteSystemViewController.h"
#import "IdentityViewController.h"
#import "LaunchAndMessageViewController.h"
#import <ConnectedDevices/Core/MCDPlatform.h>
#import <ConnectedDevices/RemoteSystems/MCDRemoteSystemApp.h>
#import <ConnectedDevices/RemoteSystems/MCDRemoteSystemAuthorizationKindFilter.h>
#import <ConnectedDevices/RemoteSystems/MCDRemoteSystemDiscoveryTypeFilter.h>
#import <ConnectedDevices/RemoteSystems/MCDRemoteSystemKindFilter.h>
#import <ConnectedDevices/RemoteSystems/MCDRemoteSystemKinds.h>
#import <ConnectedDevices/RemoteSystems/MCDRemoteSystemLocalVisibilityKindFilter.h>
#import <ConnectedDevices/RemoteSystems/MCDRemoteSystemStatusTypeFilter.h>
#import <ConnectedDevices/MCDConnectedDevicesPlatform.h>
#import <ConnectedDevicesRemoteSystems/MCDRemoteSystemApp.h>
#import <ConnectedDevicesRemoteSystems/MCDRemoteSystemAuthorizationKindFilter.h>
#import <ConnectedDevicesRemoteSystems/MCDRemoteSystemDiscoveryTypeFilter.h>
#import <ConnectedDevicesRemoteSystems/MCDRemoteSystemKindFilter.h>
#import <ConnectedDevicesRemoteSystems/MCDRemoteSystemKinds.h>
#import <ConnectedDevicesRemoteSystems/MCDRemoteSystemLocalVisibilityKindFilter.h>
#import <ConnectedDevicesRemoteSystems/MCDRemoteSystemStatusTypeFilter.h>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@ -187,7 +187,7 @@
cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail;
cell.textLabel.text = [NSString stringWithFormat:@"%@\nproximal:%@\nspatial:%@\nid:%@", application.displayName,
application.isAvailableByProximity ? @"YES" : @"NO",
application.isAvailableBySpatialProximity ? @"YES" : @"NO", application.identifier];
application.isAvailableBySpatialProximity ? @"YES" : @"NO", application.appId];
return cell;
}

Просмотреть файл

@ -4,7 +4,6 @@
#pragma once
#import <ConnectedDevices/Core/MCDPlatform.h>
#import <UIKit/UIKit.h>
@interface SdkViewController : UIViewController

Просмотреть файл

@ -3,100 +3,59 @@
//
#import "SdkViewController.h"
#import "AppDataSource.h"
#import "ConnectedDevicesPlatformManager.h"
#import "AppServiceProvider.h"
#import "IdentityViewController.h"
#import "LaunchUriProvider.h"
#import "NotificationProvider.h"
#import <ConnectedDevices/Core/MCDPlatform.h>
#import <ConnectedDevices/RemoteSystems.Commanding/MCDRemoteSystemAppHostingRegistration.h>
#import <ConnectedDevices/MCDConnectedDevicesPlatform.h>
#import <ConnectedDevicesRemoteSystemsCommanding/MCDRemoteSystemAppRegistration.h>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface SdkViewController() {
ConnectedDevicesPlatformManager* _platformManager;
}
@end
@implementation SdkViewController : UIViewController
- (instancetype)initWithNibName:(NSString *)nibNameOrNil
bundle:(NSBundle *)nibBundleOrNil {
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
_platformManager = [ConnectedDevicesPlatformManager sharedInstance];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
_platformManager = [ConnectedDevicesPlatformManager sharedInstance];
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Wait to enable the buttons until platform is initialized
[self.deviceRelayButton setEnabled:NO];
[self.activityFeedButton setEnabled:NO];
UIBarButtonItem* signOutButton =
[[UIBarButtonItem alloc] initWithTitle:@"Sign Out" style:UIBarButtonItemStyleDone target:self action:@selector(_signOutClicked:)];
self.navigationItem.rightBarButtonItem = signOutButton;
[self initializePlatform];
}
- (void)initializePlatform
{
// Only register for APNs if this app is enabled for push notifications
NotificationProvider* notificationProvider;
if ([[UIApplication sharedApplication] isRegisteredForRemoteNotifications])
{
notificationProvider = [AppDataSource sharedInstance].notificationProvider;
}
else
{
NSLog(@"Initializing platform without a notification provider!");
notificationProvider = nil;
}
// Initialize platform
[AppDataSource sharedInstance].platform = [MCDPlatform platformWithAccountProvider:[AppDataSource sharedInstance].accountProvider notificationProvider:notificationProvider];
// App is registered asynchronously.
MCDRemoteSystemAppHostingRegistration* registration = [MCDRemoteSystemAppHostingRegistration new];
[registration setLaunchUriProvider:[[LaunchUriProvider alloc] initWithDelegate:[AppDataSource sharedInstance].inboundRequestLogger]];
[registration addAppServiceProvider:[[AppServiceProvider alloc] initWithDelegate:[AppDataSource sharedInstance].inboundRequestLogger]];
[registration addAttribute:@"ExampleAttribute" forName:@"ExampleName"];
[registration.statusChanged subscribe:^(__unused MCDRemoteSystemAppHostingRegistration* reg, MCDRemoteSystemAppRegistrationStatusChangedEventArgs* args) {
NSLog(@"Registration Status Changed listener");
switch (args.status) {
case MCDRemoteSystemAppRegistrationStatusFailed:
NSLog(@"Registration completed with status Failed");
break;
case MCDRemoteSystemAppRegistrationStatusInProgress:
NSLog(@"Registration in progress");
break;
case MCDRemoteSystemAppRegistrationStatusNotStarted:
NSLog(@"Registration not started");
break;
case MCDRemoteSystemAppRegistrationStatusSucceeded:
dispatch_async(dispatch_get_main_queue(), ^{
// The app has been registered. It is safe to enable button.
[self.deviceRelayButton setEnabled:YES];
[self.activityFeedButton setEnabled:YES];
});
break;
}
}];
[registration save];
}
- (void)_signOutClicked:(id)sender
{
// Disable buttons when starting sign-out
[self.deviceRelayButton setEnabled:NO];
[self.activityFeedButton setEnabled:NO];
if ([AppDataSource sharedInstance].accountProvider.signedIn)
{
[[AppDataSource sharedInstance].accountProvider
signOutWithCompletionCallback:^(BOOL successful, SampleAccountActionFailureReason reason) {
NSLog(@"%@", (successful ? @"Currently signed out" : @"Sign out failed"));
dispatch_async(dispatch_get_main_queue(), ^{ [self dismissViewControllerAnimated:YES completion:nil]; });
}];
NSMutableArray<AnyPromise*>* logoutPromises = [NSMutableArray new];
for (Account* account in _platformManager.accounts) {
[logoutPromises addObject:[_platformManager signOutAsync:account]];
}
else
{
// If we're somehow already signed out, just dismiss
PMKWhen(logoutPromises).then(^{
NSLog(@"Currently signed out");
[self dismissViewControllerAnimated:YES completion:nil];
}
}).catch(^(NSError* e){
NSLog(@"Sign out failed with error: %@", e);
});
}
@end

Просмотреть файл

@ -4,9 +4,9 @@
#pragma once
#import "MSAAccountProvider.h"
#import <ConnectedDevices/Core/Core.h>
#import <ConnectedDevices/UserData.UserActivities/UserData.UserActivities.h>
#import "MSAAccount.h"
#import <ConnectedDevices/ConnectedDevices.h>
#import <ConnectedDevicesUserDataUserActivities/ConnectedDevicesUserDataUserActivities.h>
#import <UIKit/UIKit.h>
@interface UserActivityViewController : UIViewController

Просмотреть файл

@ -3,9 +3,9 @@
//
#import "UserActivityViewController.h"
#import "AppDataSource.h"
#import <ConnectedDevices/UserData.UserActivities/UserData.UserActivities.h>
#import <ConnectedDevices/UserData/MCDUserDataFeed.h>
#import "ConnectedDevicesPlatformManager.h"
#import <ConnectedDevicesUserDataUserActivities/ConnectedDevicesUserDataUserActivities.h>
#import <ConnectedDevicesUserData/MCDUserDataFeed.h>
#import "Secrets.h"
#import <UIKit/UIKit.h>
@ -23,17 +23,24 @@
[super viewDidLoad];
// You must be logged in to use UserActivities
NSArray<MCDUserAccount*>* accounts = [[AppDataSource sharedInstance].accountProvider getUserAccounts];
if (accounts.count > 0)
ConnectedDevicesPlatformManager* platformManager = [ConnectedDevicesPlatformManager sharedInstance];
NSArray<Account*>* accounts = platformManager.accounts;
Account* account = nil;
if (accounts.count > 0) {
account = [accounts objectAtIndex:[accounts indexOfObjectPassingTest:^BOOL (Account* account, NSUInteger index, BOOL* stop) {
return account.state == AccountRegistrationStateInAppCacheAndSdkCache;
}]];
}
// This logic would be better if the UI supported more than one account. Since this is a simple app, just check for one account
// that is synced to key off of for UserDataFeed.
if (account != nil)
{
// Step #1: Get a UserActivity channel, getting the default channel
NSLog(@"Creating UserActivityChannel");
NSArray<MCDUserAccount*>* accounts = [[AppDataSource sharedInstance].accountProvider getUserAccounts];
MCDUserDataFeed* userDataFeed = [MCDUserDataFeed getForAccount:accounts[0]
platform:[AppDataSource sharedInstance].platform
MCDUserDataFeed* userDataFeed = [MCDUserDataFeed getForAccount:account.mcdAccount
platform:platformManager.platform
activitySourceHost:CROSS_PLATFORM_APP_ID];
NSArray<id<MCDUserDataFeedSyncScope>>* syncScopes = @[ [MCDUserActivityChannel syncScope] ];
[userDataFeed addSyncScopes:syncScopes];
self.channel = [MCDUserActivityChannel channelWithUserDataFeed:userDataFeed];
}
else

Просмотреть файл

@ -141,7 +141,7 @@
<color key="textColor" red="0.28812987200000001" green="0.42992432310000001" blue="0.53264653500000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" text="ms-windows-store://pdp/?productid=9NBLGGH4NNQJ" borderStyle="roundedRect" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="gSN-PK-RsD">
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" text="http://www.bing.com" borderStyle="roundedRect" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="gSN-PK-RsD">
<rect key="frame" x="22" y="253" width="304" height="30"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="ZnF-UU-m5C"/>

Просмотреть файл

@ -7,7 +7,7 @@
#import <Foundation/Foundation.h>
#import "SignInAccount.h"
#import <ADAL/ADAL.h>
#import <ConnectedDevices/ConnectedDevices/ConnectedDevices.h>
#import <ConnectedDevices/ConnectedDevices.h>
#import "SignInAccount.h"
@interface AADAccount : NSObject <SignInAccount>

Просмотреть файл

@ -6,7 +6,7 @@
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <ConnectedDevices/ConnectedDevices/ConnectedDevices.h>
#import <ConnectedDevices/ConnectedDevices.h>
#import "SignInAccount.h"
@interface MSAAccount : NSObject <SignInAccount>

Просмотреть файл

@ -6,7 +6,7 @@
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <ConnectedDevices/ConnectedDevices/ConnectedDevices.h>
#import <ConnectedDevices/ConnectedDevices.h>
#import "SampleAccountActionFailureReason.h"
@ -20,5 +20,6 @@
completion:(nonnull void (^)(NSString* _Nonnull, NSError* _Nullable))scompletionBlock;
- (BOOL)isSignedIn;
@property(readonly, nonatomic, nonnull) MCDConnectedDevicesAccount* mcdAccount;
@end

Просмотреть файл

@ -46,7 +46,8 @@ static NSString* const AADAccountProviderErrorDomain = @"AADAccount";
// Don't share token cache between applications, only need them to be cached for this application
// Without this, the MRRT is not cached, and the acquireTokenSilentWithResource: in getAccessToken
// always fails with AD_ERROR_SERVER_USER_INPUT_NEEDED
[[ADAuthenticationSettings sharedInstance] setDefaultKeychainGroup:nil];
static dispatch_once_t initKeyChainGroup;
dispatch_once(&initKeyChainGroup, ^{ [[ADAuthenticationSettings sharedInstance] setDefaultKeychainGroup:nil]; });
#endif
ADAuthenticationError* error = nil;
@ -113,7 +114,6 @@ static NSString* const AADAccountProviderErrorDomain = @"AADAccount";
// a consent prompt for all app resources will be raised when an access token for a new resource is requested -
// see getAccessTokenForUserAccountIdAsync:
NSString* defaultResource = @"https://graph.windows.net";
[_authContext
acquireTokenWithResource:defaultResource
clientId:_clientId

Просмотреть файл

@ -315,9 +315,7 @@ static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems,
[self removeAccount];
[self _signInSignOutSucceededAsync:YES
error:[NSError errorWithDomain:MsaAccountProviderErrorDomain
code:SampleAccountActionFailureReasonFailToRetrieveAuthCode
userInfo:nil]];
error:nil];
}
else
{
@ -485,20 +483,20 @@ static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems,
case MSATokenRequestStatusTransientFailure:
{
NSLog(@"Requesting new access token failed temporarily, please try again.");
completionBlock(
@"fail2", [NSError errorWithDomain:MsaAccountProviderErrorDomain
code:SampleAccountActionFailureReasonAccessTokenTemporaryError
userInfo:nil]);
completionBlock(@"Requesting new access token failed. Try again.",
[NSError errorWithDomain:MsaAccountProviderErrorDomain
code:SampleAccountActionFailureReasonAccessTokenTemporaryError
userInfo:nil]);
break;
}
default: // PermanentFailure
{
NSLog(@"Permanent error occurred while fetching access token.");
[self onAccessTokenError:_mcdAccount.accountId scopes:@[ scope ] isPermanentError:YES];
completionBlock(
@"fail3", [NSError errorWithDomain:MsaAccountProviderErrorDomain
code:SampleAccountActionFailureReasonAccessTokenPermanentError
userInfo:nil]);
completionBlock(@"Permanent error occurred fetching access token.",
[NSError errorWithDomain:MsaAccountProviderErrorDomain
code:SampleAccountActionFailureReasonAccessTokenPermanentError
userInfo:nil]);
break;
}
}

Просмотреть файл

@ -1,25 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#pragma once
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <ConnectedDevices/Core/Core.h>
#import "SingleUserAccountProvider.h"
// @brief MCDUserAccountProvider that performs a log in/out flow using ADAL.
// Supports a single AAD user account.
// For getAccessTokenForUserAccountIdAsync: and onAccessTokenError:, because of ADAL limitations, only the first scope in scopes[] is used
@interface AADAccountProvider : NSObject <SingleUserAccountProvider>
// @brief clientId is a guid from the app's registration in the azure portal
// redirectUri is a Uri specified in the same portal
- (nullable instancetype)initWithClientId:(nonnull NSString*)clientId redirectUri:(nonnull NSURL*)redirectUri;
@property(readonly, nonatomic, copy, nonnull) NSString* clientId;
@property(readonly, nonatomic, copy, nonnull) NSURL* redirectUri;
@end

Просмотреть файл

@ -1,44 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#pragma once
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <ConnectedDevices/Core/Core.h>
#import "SampleAccountActionFailureReason.h"
typedef NS_ENUM(NSInteger, AADMSAAccountProviderSignInState)
{
AADMSAAccountProviderSignInStateSignedOut,
AADMSAAccountProviderSignInStateSignedInMSA,
AADMSAAccountProviderSignInStateSignedInAAD,
};
// @brief A sample MCDUserAccountProvider that wraps around an AAD provider and an MSA provider.
// Supports only a single user account at a time - trying to log into more than one account at once will throw an exception.
// Any accounts logged into will be made available through the MCDUserAccountProvider interface.
//
// When signed into an AAD account, because of AAD limitations,
// only the first scope in scopes[] passed to for getAccessTokenForUserAccountIdAsync: and onAccessTokenError:, is used
//
// msaClientId is a guid from the app's registration in the msa apps portal
// msaScopeOverrides is a map for the app to specify special scopes to replace the default ones
// aadApplicationId is a guid from the app's registration in the azure portal
// aadRedirectUri is a Uri specified in the azure portal
@interface AADMSAAccountProvider : NSObject <MCDUserAccountProvider>
@property(readonly, atomic) AADMSAAccountProviderSignInState signInState;
@property(readonly, nonatomic, copy, nonnull) NSString* msaClientId;
@property(readonly, nonatomic, copy, nonnull) NSString* aadApplicationId;
- (nullable instancetype)initWithMsaClientId:(nonnull NSString*)msaClientId
msaScopeOverrides:(nullable NSDictionary<NSString*, NSArray<NSString*>*>*) scopes
aadApplicationId:(nonnull NSString*)aadApplicationId
aadRedirectUri:(nonnull NSURL*)aadRedirectUri;
- (void)signInMSAWithCompletionCallback:(nonnull SampleAccountProviderCompletionBlock)callback;
- (void)signInAADWithCompletionCallback:(nonnull SampleAccountProviderCompletionBlock)callback;
- (void)signOutWithCompletionCallback:(nonnull SampleAccountProviderCompletionBlock)callback;
@end

Просмотреть файл

@ -1,28 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#pragma once
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <ConnectedDevices/Core/Core.h>
#import "SingleUserAccountProvider.h"
/**
* @brief
* Sample implementation of MCDUserAccountProvider.
* Exposes a single MSA account, that the user logs into via UIWebView, to CDP.
* Follows OAuth2.0 protocol, but automatically refreshes tokens when they are close to expiring.
*/
@interface MSAAccountProvider : NSObject <SingleUserAccountProvider>
// @brief clientId is a guid from the app's registration in the msa portal
// scopeOverrides is a map for the app to specify special scopes to replace the default ones
- (nullable instancetype)initWithClientId:(nonnull NSString*)clientId
scopeOverrides:(nullable NSDictionary<NSString*, NSArray<NSString*>*>*)scopes;
@property(readonly, nonatomic, copy, nonnull) NSString* clientId;
@end

Просмотреть файл

@ -1,23 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#pragma once
#import <Foundation/Foundation.h>
// @brief MSA failure reason for sign in or sign out action
typedef NS_ENUM(NSInteger, SampleAccountActionFailureReason)
{
SampleAccountActionNoFailure,
SampleAccountActionFailureReasonAlreadySignedIn,
SampleAccountActionFailureReasonAlreadySignedOut,
SampleAccountActionFailureReasonUserCancelled,
SampleAccountActionFailureReasonFailToRetrieveAuthCode,
SampleAccountActionFailureReasonFailToRetrieveRefreshToken,
SampleAccountActionFailureReasonSigninSignOutInProgress,
SampleAccountActionFailureReasonUnknown,
SampleAccountActionFailureReasonADAL,
};
typedef void (^SampleAccountProviderCompletionBlock)(BOOL successful, SampleAccountActionFailureReason reason);

Просмотреть файл

@ -1,17 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#pragma once
#import <ConnectedDevices/Core/Core.h>
#import "SampleAccountActionFailureReason.h"
// @brief Protocol for a MCDUserAccountProvider that supports logging into/out of a single user account.
@protocol SingleUserAccountProvider <MCDUserAccountProvider>
- (void)signInWithCompletionCallback:(nonnull SampleAccountProviderCompletionBlock)callback;
- (void)signOutWithCompletionCallback:(nonnull SampleAccountProviderCompletionBlock)callback;
@property(readonly, atomic) BOOL signedIn;
@end

Просмотреть файл

@ -1,329 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#import "AADAccountProvider.h"
#import <ADAL/ADAL.h>
static NSString* const AADAccountProviderExceptionName = @"AADAccountProviderException";
/**
* Notes about AAD/ADAL:
* - Resource An Azure web service/app, such as https://graph.windows.net, or a CDP service.
* - Scope Individual permissions within a resource
* - Access Token A standard JSON web token for a given scope.
* This is the actual token/user ticket used to authenticate with CDP services.
* https://oauth.net/2/
* https://www.oauth.com/oauth2-servers/access-tokens/
* - Refresh token: A standard OAuth refresh token.
* Lasts longer than access tokens, and is used to request new access tokens/refresh access tokens when they expire.
* ADAL manages this automatically.
* https://oauth.net/2/grant-types/refresh-token/
* - MRRT Multiresource refresh token. A refresh token that can be used to fetch access tokens for more than one resource.
* Getting one requires the user consent to all the covered resources. ADAL manages this automatically.
*/
@interface AADAccountProvider ()
{
ADAuthenticationContext* _authContext;
ADTokenCacheItem* _tokenCacheItem;
}
@end
@implementation AADAccountProvider
@synthesize userAccountChanged = _userAccountChanged;
- (instancetype)initWithClientId:(NSString*)clientId redirectUri:(NSURL*)redirectUri
{
if (self = [super init])
{
_clientId = [clientId copy];
_redirectUri = [redirectUri copy];
_userAccountChanged = [MCDUserAccountChangedEvent new];
#if TARGET_OS_IPHONE
// Don't share token cache between apps, only need them to be cached for this application
// Without this, the MRRT is not cached, and the acquireTokenSilentWithResource: in getAccessToken
// always fails with AD_ERROR_SERVER_USER_INPUT_NEEDED
[[ADAuthenticationSettings sharedInstance] setDefaultKeychainGroup:nil];
#endif
ADAuthenticationError* error = nil;
_authContext =
[ADAuthenticationContext authenticationContextWithAuthority:@"https://login.microsoftonline.com/common" error:&error];
if (error)
{
NSLog(@"Error creating ADAuthenticationContext for AADAccountProvider: %@.", error);
return nil;
}
NSLog(@"Checking if previous AADAccountProvider session can be loaded...");
#if TARGET_OS_IPHONE
NSArray<ADTokenCacheItem*>* tokenCacheItems = [[ADKeychainTokenCache defaultKeychainCache] allItems:nil];
#else
NSArray<ADTokenCacheItem*>* tokenCacheItems = [[ADTokenCache defaultCache] allItems:nil];
#endif
if (tokenCacheItems.count > 0)
{
for (ADTokenCacheItem* item in tokenCacheItems)
{
if (item.isMultiResourceRefreshToken && [_clientId isEqualToString:item.clientId])
{
_tokenCacheItem = item;
break;
}
}
if (_tokenCacheItem)
{
NSLog(@"Loaded previous AADAccountProvider session, starting as signed in.");
}
else
{
NSLog(@"No previous AADAccountProvider session could be loaded, starting as signed out.");
}
}
}
return self;
}
- (void)_raiseAccountChangedEvent
{
NSLog(@"Raise Account changed event");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// fire event on a different thread
[self.userAccountChanged raise];
});
}
- (BOOL)signedIn
{
@synchronized(self)
{
return _tokenCacheItem != nil;
}
}
- (void)signInWithCompletionCallback:(SampleAccountProviderCompletionBlock)callback
{
if (self.signedIn)
{
callback(NO, SampleAccountActionFailureReasonAlreadySignedIn);
return;
}
// If the user has not previously consented for this default resource for this app,
// the interactive flow will ask for user consent for all resources used by the app.
// If the user previously consented to this resource on this app, and more resources are added to the app later on,
// a consent prompt for all app resources will be raised when an access token for a new resource is requested -
// see getAccessTokenForUserAccountIdAsync:
NSString* defaultResource = @"https://graph.windows.net";
[_authContext acquireTokenWithResource:defaultResource
clientId:_clientId
redirectUri:_redirectUri
completionBlock:^(ADAuthenticationResult* result) {
switch (result.status)
{
case AD_SUCCEEDED:
{
@synchronized(self)
{
_tokenCacheItem = result.tokenCacheItem;
}
[self _raiseAccountChangedEvent];
callback(YES, SampleAccountActionNoFailure);
break;
}
case AD_USER_CANCELLED:
{
callback(NO, SampleAccountActionFailureReasonUserCancelled);
break;
}
case AD_FAILED:
default:
{
NSLog(@"Error occurred in ADAL when signing in to an AAD account. Status: %u, Error: %@", result.status,
result.error);
callback(NO, SampleAccountActionFailureReasonADAL);
break;
}
}
}];
}
- (void)signOutWithCompletionCallback:(SampleAccountProviderCompletionBlock)callback
{
@synchronized(self)
{
if (!self.signedIn)
{
callback(NO, SampleAccountActionFailureReasonAlreadySignedOut);
return;
}
ADAuthenticationError* error;
#if TARGET_OS_IPHONE
BOOL removed = [[ADKeychainTokenCache defaultKeychainCache] removeAllForClientId:_clientId error:&error];
#else
// The above convenience method does not exist on OSX
BOOL removed;
NSArray<ADTokenCacheItem*>* tokenCacheItems = [[ADTokenCache defaultCache] allItems:&error];
if (!error)
{
for (ADTokenCacheItem* item in tokenCacheItems)
{
if ([item.clientId isEqualToString:_clientId])
{
removed = [[ADTokenCache defaultCache] removeItem:item error:&error];
if (!removed || error)
{
break;
}
}
}
}
#endif
if (!removed || error)
{
NSLog(@"Failed to remove token from ADAL cache, error %@", error);
callback(NO, SampleAccountActionFailureReasonADAL);
return;
}
// Delete cookies
NSArray<NSString*>* cookieNamesToDelete =
@[ @"SignInStateCookie", @"ESTSAUTHPERSISTENT", @"ESTSAUTHLIGHT", @"ESTSAUTH", @"ESTSSC" ];
NSHTTPCookieStorage* cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie* cookie in [cookieJar cookies])
{
if ([cookieNamesToDelete containsObject:cookie.name])
{
[cookieJar deleteCookie:cookie];
}
}
_tokenCacheItem = nil;
}
[self _raiseAccountChangedEvent];
callback(YES, SampleAccountActionNoFailure);
}
- (void)getAccessTokenForUserAccountIdAsync:(NSString*)accountId
scopes:(NSArray<NSString*>*)scopes
completion:(void (^)(MCDAccessTokenResult*, NSError*))completionBlock
{
@synchronized(self)
{
if (!self.signedIn || ![accountId isEqualToString:_tokenCacheItem.userInformation.uniqueId])
{
completionBlock(nil, [NSError errorWithDomain:@"AADAccountProvider"
code:0
userInfo:@{
@"Reason" : @"AADAccountProvider does not provide this account."
}]);
return;
}
// Try to fetch the token silently in the background, escalating to the ui thread if needed for a unique case (see below)
__weak __block void (^weakAdalCallback)(ADAuthenticationResult*); // __weak __block is needed for recursive blocks under ARC
__block void (^adalCallback)(ADAuthenticationResult*) = ^void(ADAuthenticationResult* adalResult) {
MCDAccessTokenResult* result;
NSError* error;
switch (adalResult.status)
{
case AD_SUCCEEDED:
{
result =
[[MCDAccessTokenResult alloc] initWithAccessToken:adalResult.accessToken status:MCDAccessTokenRequestStatusSuccess];
break;
}
case AD_USER_CANCELLED:
{
error = [NSError errorWithDomain:@"AADAccountProvider" code:0 userInfo:@{ @"Reason" : @"Cancelled by user." }];
break;
}
case AD_FAILED:
default:
{
if (adalResult.error.code == AD_ERROR_SERVER_USER_INPUT_NEEDED)
{
// This error only returns from acquireTokenSilentWithResource: when an interactive prompt is needed.
// ADAL has an MRRT, but the user has not consented for this resource/the MRRT does not cover this resource.
// Usually, users consent for all resources the app needs during the interactive flow in signInWith...:
// However, if the app adds new resources after the user consented previously, signIn will not prompt.
// Escalate to the UI thread and do an interactive flow,
// which should raise a new consent prompt for all current app resources.
NSLog(@"A resource was requested that the user did not previously consent to. "
@"Attempting to raise an interactive consent prompt.");
dispatch_async(dispatch_get_main_queue(), ^{
[_authContext acquireTokenWithResource:scopes[0]
clientId:_clientId
redirectUri:_redirectUri
completionBlock:weakAdalCallback];
});
return;
}
error = [NSError errorWithDomain:@"AADAccountProvider" code:0 userInfo:@{ @"Reason" : @"Unknown ADAL error." }];
break;
}
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ completionBlock(result, error); });
};
weakAdalCallback = adalCallback;
[_authContext acquireTokenSilentWithResource:scopes[0]
clientId:_clientId
redirectUri:_redirectUri
userId:_tokenCacheItem.userInformation.userId
completionBlock:adalCallback];
}
}
- (NSArray<MCDUserAccount*>*)getUserAccounts
{
@synchronized(self)
{
return _tokenCacheItem ?
@[ [[MCDUserAccount alloc] initWithAccountId:_tokenCacheItem.userInformation.uniqueId type:MCDUserAccountTypeAAD] ] :
nil;
}
}
- (void)onAccessTokenError:(NSString*)accountId scopes:(NSArray<NSString*>*)scopes isPermanentError:(BOOL)isPermanentError
{
@synchronized(self)
{
if ([accountId isEqualToString:_tokenCacheItem.userInformation.uniqueId])
{
if (isPermanentError)
{
_tokenCacheItem = nil;
[self _raiseAccountChangedEvent];
}
else
{
// If not a permanent error, try just refreshing the token by calling ADAL's acquireToken: again
[_authContext acquireTokenWithResource:scopes[0]
clientId:_clientId
redirectUri:_redirectUri
completionBlock:^(__unused ADAuthenticationResult* result){}];
}
}
else
{
NSLog(@"accountId was not found in AADAccountProvider.");
}
}
}
@end

Просмотреть файл

@ -1,135 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#import "AADMSAAccountProvider.h"
#import "AADAccountProvider.h"
#import "MSAAccountProvider.h"
static NSString* const AADMSAAccountProviderExceptionName = @"AADMSAAccountProviderException";
@interface AADMSAAccountProvider ()
@property(readonly, nonatomic, strong) MSAAccountProvider* msaProvider;
@property(readonly, nonatomic, strong) AADAccountProvider* aadProvider;
@end
@implementation AADMSAAccountProvider
@synthesize userAccountChanged = _userAccountChanged;
- (instancetype)initWithMsaClientId:(NSString*)msaClientId
msaScopeOverrides:(NSDictionary<NSString*, NSArray<NSString*>*>*)scopes
aadApplicationId:(NSString*)aadApplicationId
aadRedirectUri:(NSURL*)aadRedirectUri
{
if (self = [super init])
{
_userAccountChanged = [MCDUserAccountChangedEvent new];
_msaProvider = [[MSAAccountProvider alloc] initWithClientId:msaClientId scopeOverrides:scopes];
_aadProvider = [[AADAccountProvider alloc] initWithClientId:aadApplicationId redirectUri:aadRedirectUri];
if (_msaProvider.signedIn && _aadProvider.signedIn)
{
// Shouldn't ever happen, but if it does, sign out of AAD
[_aadProvider signOutWithCompletionCallback:^(__unused BOOL success, __unused SampleAccountActionFailureReason reason){}];
}
[_msaProvider.userAccountChanged subscribe:^void() { [self.userAccountChanged raise]; }];
[_aadProvider.userAccountChanged subscribe:^void() { [self.userAccountChanged raise]; }];
}
return self;
}
- (AADMSAAccountProviderSignInState)signInState
{
@synchronized(self)
{
if (_msaProvider.signedIn)
{
return AADMSAAccountProviderSignInStateSignedInMSA;
}
else if (_aadProvider.signedIn)
{
return AADMSAAccountProviderSignInStateSignedInAAD;
}
return AADMSAAccountProviderSignInStateSignedOut;
}
}
- (NSString*)msaClientId
{
return _msaProvider.clientId;
}
- (NSString*)aadApplicationId
{
return _aadProvider.clientId;
}
- (id<SingleUserAccountProvider>)_signedInProvider
{
switch (self.signInState)
{
case AADMSAAccountProviderSignInStateSignedInMSA: return _msaProvider;
case AADMSAAccountProviderSignInStateSignedInAAD: return _aadProvider;
default: return nil;
}
}
- (void)signInMSAWithCompletionCallback:(SampleAccountProviderCompletionBlock)callback
{
if (self.signInState != AADMSAAccountProviderSignInStateSignedOut)
{
[NSException raise:AADMSAAccountProviderExceptionName format:@"Already signed into an account!"];
}
[_msaProvider signInWithCompletionCallback:callback];
}
- (void)signInAADWithCompletionCallback:(SampleAccountProviderCompletionBlock)callback
{
if (self.signInState != AADMSAAccountProviderSignInStateSignedOut)
{
[NSException raise:AADMSAAccountProviderExceptionName format:@"Already signed into an account!"];
}
[_aadProvider signInWithCompletionCallback:callback];
}
- (void)signOutWithCompletionCallback:(__unused SampleAccountProviderCompletionBlock)callback
{
id<SingleUserAccountProvider> signedInProvider = [self _signedInProvider];
if (!signedInProvider)
{
[NSException raise:AADMSAAccountProviderExceptionName format:@"Not signed into an account!"];
}
[signedInProvider signOutWithCompletionCallback:callback];
}
- (void)getAccessTokenForUserAccountIdAsync:(NSString*)accountId
scopes:(NSArray<NSString*>*)scopes
completion:(void (^)(MCDAccessTokenResult*, NSError*))completionBlock
{
id<SingleUserAccountProvider> signedInProvider = [self _signedInProvider];
if (!signedInProvider)
{
[NSException raise:AADMSAAccountProviderExceptionName format:@"Not signed into an account!"];
}
[signedInProvider getAccessTokenForUserAccountIdAsync:accountId scopes:scopes completion:completionBlock];
}
- (NSArray<MCDUserAccount*>*)getUserAccounts
{
return [[self _signedInProvider] getUserAccounts];
}
- (void)onAccessTokenError:(NSString*)accountId scopes:(NSArray<NSString*>*)scopes isPermanentError:(BOOL)isPermanentError
{
id<SingleUserAccountProvider> signedInProvider = [self _signedInProvider];
if (!signedInProvider)
{
[NSException raise:AADMSAAccountProviderExceptionName format:@"Not signed into an account!"];
}
[signedInProvider onAccessTokenError:accountId scopes:scopes isPermanentError:isPermanentError];
}
@end

Просмотреть файл

@ -1,492 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#import "MSAAccountProvider.h"
#import "MSATokenCache.h"
#import "MSATokenRequest.h"
/**
* Terms:
* - Scope: OAuth feature, limits what a token actually gives permissions to.
* https://www.oauth.com/oauth2-servers/scope/
* - Access token: A standard JSON web token for a given scope.
* This is the actual token/user ticket used to authenticate with CDP services.
* https://oauth.net/2/
* https://www.oauth.com/oauth2-servers/access-tokens/
* - Refresh token: A standard OAuth refresh token.
* Lasts longer than access tokens, and is used to request new access tokens/refresh access tokens when they expire.
* This library caches one refresh token per user.
* As such, the refresh token must already be authorized/consented to for all CDP scopes that will be used in the app.
* https://oauth.net/2/grant-types/refresh-token/
* - Grant type: Type of OAuth authorization request to make (ie: token, password, auth code)
* https://oauth.net/2/grant-types/
* - Auth code: OAuth auth code, can be exchanged for a token.
* This library has the user sign in interactively for the auth code grant type,
* then retrieves the auth code from the return URL.
* https://oauth.net/2/grant-types/authorization-code/
* - Client ID: ID of an app's registration in the MSA portal. As of the time of writing, the portal uses GUIDs.
*
* The flow of this library is described below:
* Signing in
* 1. signInWithCompletionCallback: is called
* 2. UIWebView is presented to the user for sign in
* 3. Use authcode returned from user's sign in to fetch refresh token
* 4. Refresh token is cached - if the user does not sign out, but the app is restarted,
* the user will not need to enter their credentials/consent again when signInWithCompletionCallback: is called.
* 4. Now treated as signed in. Account is exposed to CDP. userAccountChanged event is fired.
*
* While signed in
* CDP asks for access tokens
* 1. Check if access token is in cache
* 2. If not in cache, request a new access token using the cached refresh token.
* 3. If in cache but close to expiry, the access token is refreshed using the refresh token.
* The refreshed access token is returned.
* 4. If in cache and not close to expiry, just return it.
*
* Signing out
* 1. signOutWithCompletionCallback: is called
* 2. UIWebView is quickly popped up to go through the sign out URL
* 3. Cache is cleared.
* 4. Now treated as signed out. Account is no longer exposed to CDP. userAccountChanged event is fired.
*/
#pragma mark - Constants
// CDP's SDK currently requires authorization for all features, otherwise platform initialization will fail.
// As such, the user must sign in/consent for the following scopes. This may change to become more modular in the future.
static NSString* const MsaRequiredScopes = //
@"wl.offline_access+" // read and update user info at any time
@"ccs.ReadWrite+" // device commanding scope
@"dds.read+" // device discovery scope (discover other devices)
@"dds.register+" // device discovery scope (allow discovering this device)
@"wns.connect+" // push notification scope
@"asimovrome.telemetry+" // asimov token scope
@"https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp"; // default userdata.useractivities scope
// OAuth URLs
static NSString* const MsaRedirectUrl = @"https://login.live.com/oauth20_desktop.srf";
static NSString* const MsaAuthorizeUrl = @"https://login.live.com/oauth20_authorize.srf";
static NSString* const MsaLogoutUrl = @"https://login.live.com/oauth20_logout.srf";
// NSError constants
static NSString* const MsaAccountProviderErrorDomain = @"MSAAccountProvider";
static const NSInteger MsaAccountProviderErrorInvalidAccountId = 100;
static const NSInteger MsaAccountProviderErrorAccessTokenTemporaryError = 101;
static const NSInteger MsaAccountProviderErrorAccessTokenPermanentError = 102;
#pragma mark - Static Helpers
// Helper function - gets the NSURLQueryItem matching name
static NSURLQueryItem* GetQueryItemForName(NSArray<NSURLQueryItem*>* queryItems, NSString* name)
{
NSUInteger index = [queryItems indexOfObjectPassingTest:^BOOL(NSURLQueryItem* queryItem, __unused NSUInteger idx, __unused BOOL* stop) {
return [queryItem.name isEqualToString:name];
}];
return (index != NSNotFound) ? queryItems[index] : nil;
}
#pragma mark - Private Members
@interface MSAAccountProvider () <MSATokenCacheDelegate, UIWebViewDelegate>
{
NSString* _clientId;
NSDictionary<NSString*, NSArray<NSString*>*>* _scopeOverrides;
MCDUserAccount* _account;
MSATokenCache* _tokenCache;
BOOL _signInSignOutInProgress;
SampleAccountProviderCompletionBlock _signInSignOutCallback;
UIWebView* _webView;
}
@end
#pragma mark - Implementation
@implementation MSAAccountProvider
@synthesize userAccountChanged = _userAccountChanged;
- (instancetype)initWithClientId:(NSString*)clientId
scopeOverrides:(NSDictionary<NSString*, NSArray<NSString*>*>*)scopes
{
NSLog(@"MSAAccountProvider initWithClientId");
if (self = [super init])
{
_clientId = [clientId copy];
_scopeOverrides = [scopes copy];
_tokenCache = [MSATokenCache cacheWithClientId:_clientId delegate:self];
_userAccountChanged = [MCDUserAccountChangedEvent new];
_signInSignOutInProgress = NO;
_signInSignOutCallback = nil;
if ([_tokenCache loadSavedRefreshToken])
{
NSLog(@"Loaded previous session for MSAAccountProvider. Starting as signed in.");
_account = [[MCDUserAccount alloc] initWithAccountId:[[NSUUID UUID] UUIDString] type:MCDUserAccountTypeMSA];
}
else
{
NSLog(@"No previous session could be loaded for MSAAccountProvider. Starting as signed out.");
}
}
return self;
}
#pragma mark - Private Helpers
- (NSString*)_getAuthScopes: (NSArray<NSString*>*) incoming
{
NSMutableArray<NSString*>* scopes = [NSMutableArray new];
for (NSString* scope in incoming)
{
NSArray<NSString*>* replacements = [_scopeOverrides objectForKey:scope];
if (replacements)
{
[scopes addObjectsFromArray:replacements];
}
else
{
[scopes addObject:scope];
}
}
return [scopes componentsJoinedByString:@"+"];
}
- (void)_raiseAccountChangedEvent
{
NSLog(@"Raise Account changed event");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// fire event on a different thread
[self.userAccountChanged raise];
});
}
- (void)_addAccount
{
@synchronized(self)
{
NSLog(@"Adding an account.");
_account = [[MCDUserAccount alloc] initWithAccountId:[[NSUUID UUID] UUIDString] type:MCDUserAccountTypeMSA];
[self _raiseAccountChangedEvent];
}
}
- (void)_removeAccount
{
@synchronized(self)
{
// clean account states
if (self.signedIn)
{
NSLog(@"Removing account.");
_account = nil;
[_tokenCache clearTokens];
[self _raiseAccountChangedEvent];
}
}
}
- (void)_loadWebRequest:(NSString*)requestUri
{
@synchronized(self)
{
UIViewController* rootVC = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
// lazy init
if (!_webView)
{
_webView = [[UIWebView alloc] initWithFrame:rootVC.view.bounds];
_webView.delegate = self;
}
[rootVC.view addSubview:_webView];
NSURLRequest* urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:requestUri]];
[_webView loadRequest:urlRequest];
}
}
- (void)_signInSignOutSucceededAsync:(BOOL)successful reason:(SampleAccountActionFailureReason)reason
{
dispatch_async(dispatch_get_main_queue(), ^{ [_webView removeFromSuperview]; });
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
_signInSignOutCallback(successful, reason);
_signInSignOutCallback = nil;
_signInSignOutInProgress = NO;
});
}
/**
* Asynchronously requests a new access token for the provided scope(s) and caches it.
* This assumes that the sign in helper is currently signed in.
*/
- (void)_requestNewAccessTokenAsync:(NSString*)scope callback:(void (^)(MCDAccessTokenResult*, NSError*))completionBlock
{
// Need the refresh token first, then can use it to request an access token
[_tokenCache getRefreshTokenAsync:^void(NSString* refreshToken) {
NSLog(@"Fetching access token for scope:%@", scope);
[MSATokenRequest
doAsyncRequestWithClientId:_clientId
grantType:MsaTokenRequestGrantTypeRefresh
scope:scope
redirectUri:nil
token:refreshToken
callback:^void(MSATokenRequestResult* result) {
switch (result.status)
{
case MSATokenRequestStatusSuccess:
{
NSLog(@"Successfully fetched access token.");
[_tokenCache setAccessToken:result.accessToken forScope:scope expiresIn:result.expiresIn];
completionBlock([[MCDAccessTokenResult alloc] initWithAccessToken:result.accessToken
status:MCDAccessTokenRequestStatusSuccess],
nil);
break;
}
case MSATokenRequestStatusTransientFailure:
{
NSLog(@"Requesting new access token failed temporarily, please try again.");
completionBlock(nil, [NSError errorWithDomain:MsaAccountProviderErrorDomain
code:MsaAccountProviderErrorAccessTokenTemporaryError
userInfo:nil]);
break;
}
default: // PermanentFailure
{
NSLog(@"Permanent error occurred while fetching access token.");
[self onAccessTokenError:_account.accountId scopes:@[ scope ] isPermanentError:YES];
completionBlock(nil, [NSError errorWithDomain:MsaAccountProviderErrorDomain
code:MsaAccountProviderErrorAccessTokenPermanentError
userInfo:nil]);
break;
}
}
}];
}];
}
#pragma mark - Interactive Sign In/Out
- (BOOL)signedIn
{
@synchronized(self)
{
return _account != nil;
}
}
/**
* Pops up a webview for the user to sign in with their MSA, then uses the auth code returned to cache a refresh token for the user.
* If a refresh token was already cached from a previous session, it will be used instead, and no webview will be displayed.
*/
- (void)signInWithCompletionCallback:(SampleAccountProviderCompletionBlock)signInCallback
{
@synchronized(self)
{
_signInSignOutCallback = signInCallback;
if (self.signedIn || _signInSignOutInProgress)
{
// if already signed in or in the process, callback immediately with failure and reason
[self _signInSignOutSucceededAsync:NO
reason:(self.signedIn ? SampleAccountActionFailureReasonAlreadySignedIn :
SampleAccountActionFailureReasonSigninSignOutInProgress)];
return;
}
_signInSignOutInProgress = YES;
// issue request to sign in
NSArray* scopes = [MsaRequiredScopes componentsSeparatedByString:@"+"];
[self _loadWebRequest:[NSString stringWithFormat:@"%@?redirect_uri=%@&response_type=code&client_id=%@&scope=%@", MsaAuthorizeUrl,
MsaRedirectUrl, _clientId, [self _getAuthScopes:scopes]]];
}
}
/**
* Signs the user out by going through the webview, then clears the cache and current state.
*/
- (void)signOutWithCompletionCallback:(SampleAccountProviderCompletionBlock)signOutCallback
{
@synchronized(self)
{
_signInSignOutCallback = signOutCallback;
if (!self.signedIn || _signInSignOutInProgress)
{
// if already signed out or in the process, callback immediately with failure and reason
[self _signInSignOutSucceededAsync:NO
reason:(self.signedIn ? SampleAccountActionFailureReasonSigninSignOutInProgress :
SampleAccountActionFailureReasonAlreadySignedOut)];
return;
}
_signInSignOutInProgress = YES;
// issue request to sign out
[self _loadWebRequest:[NSString stringWithFormat:@"%@?client_id=%@&redirect_uri=%@", MsaLogoutUrl, _clientId, MsaRedirectUrl]];
}
}
/**
* Continuation for signIn/signOut after the webview completes.
*/
- (void)webViewDidFinishLoad:(UIWebView*)webView
{
@synchronized(self)
{
// Validate the URL
NSURLComponents* tokenURLComponents = [NSURLComponents componentsWithURL:webView.request.URL resolvingAgainstBaseURL:nil];
if (![tokenURLComponents.path containsString:@"oauth20_desktop.srf"])
{
// finishing off loading intermediate pages,
// e.g., input username/password page, consent interrupt page, wrong username/password page etc.
// no need to handle them, return early.
return;
}
NSArray<NSURLQueryItem*>* tokenURLQueryItems = tokenURLComponents.queryItems;
if (GetQueryItemForName(tokenURLQueryItems, @"error"))
{
// sign in or sign out ending in failure
[self _signInSignOutSucceededAsync:NO reason:SampleAccountActionFailureReasonUnknown];
return;
}
NSString* authCode = GetQueryItemForName(tokenURLQueryItems, @"code").value;
if (!authCode)
{
// sign out ended in success
[self _removeAccount];
[self _signInSignOutSucceededAsync:YES reason:SampleAccountActionNoFailure];
}
else
{
// sign in ended in success
if (authCode.length <= 0)
{
// very unusual
[self _signInSignOutSucceededAsync:NO reason:SampleAccountActionFailureReasonFailToRetrieveAuthCode];
return;
}
// Fetch a refresh token using the auth code
void (^requestRefreshTokenTokenCallback)(MSATokenRequestResult*) = ^void(MSATokenRequestResult* result) {
if (result.status == MSATokenRequestStatusSuccess)
{
NSString* newRefreshToken = result.refreshToken;
NSAssert(newRefreshToken != nil, @"refresh token can not be null when refreshing refresh token succeeded");
NSLog(@"Successfully fetch the root refresh token.");
[_tokenCache setRefreshToken:newRefreshToken];
[self _addAccount];
[self _signInSignOutSucceededAsync:YES reason:SampleAccountActionNoFailure];
}
else
{
NSLog(@"Failed to fetch root refresh token using authcode.");
[self _signInSignOutSucceededAsync:NO reason:SampleAccountActionFailureReasonFailToRetrieveRefreshToken];
}
};
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"Fetch root refresh token using authcode.");
[MSATokenRequest doAsyncRequestWithClientId:_clientId
grantType:MsaTokenRequestGrantTypeCode
scope:nil
redirectUri:MsaRedirectUrl
token:authCode
callback:requestRefreshTokenTokenCallback];
});
}
}
}
/**
* Continuation for signIn/signOut after the webview completes with a failure.
*/
- (void)webView:(UIWebView*)__unused webView didFailLoadWithError:(NSError*)error
{
@synchronized(self)
{
// This gets invoked when we interrupt/cancel because we saw the oauth complete page.
int WebKitErrorFrameLoadInterruptedByPolicyChange = 102;
if (error.code != WebKitErrorFrameLoadInterruptedByPolicyChange /*interrupted*/
&& error.code != NSURLErrorCancelled)
{
[self _signInSignOutSucceededAsync:NO reason:SampleAccountActionFailureReasonUserCancelled];
}
}
}
#pragma mark - MCDUserAccountProvider Overrides
- (void)getAccessTokenForUserAccountIdAsync:(NSString*)accountId
scopes:(NSArray<NSString*>*)scopes
completion:(void (^)(MCDAccessTokenResult*, NSError*))completionBlock
{
if (![accountId isEqualToString:_account.accountId])
{
NSLog(@"accountId did not match logged in account - is the user signed in?");
completionBlock(
nil, [NSError errorWithDomain:MsaAccountProviderErrorDomain code:MsaAccountProviderErrorInvalidAccountId userInfo:nil]);
return;
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(self)
{
// check if access token cache already has a valid token
NSString* accessTokenScope = [self _getAuthScopes:scopes];
// clang-format off
[_tokenCache getAccessTokenForScopeAsync:accessTokenScope callback:^void(NSString* accessToken)
{
if (accessToken.length > 0)
{
NSLog(@"Found valid access token for scope %@ in cache, return early", accessTokenScope);
completionBlock(
[[MCDAccessTokenResult alloc] initWithAccessToken:accessToken status:MCDAccessTokenRequestStatusSuccess], nil);
return;
}
NSLog(@"Didn't find valid access token for scope %@ in cache, try to fetch it", accessTokenScope);
[self _requestNewAccessTokenAsync:accessTokenScope callback:completionBlock];
}];
// clang-format on
}
});
}
- (NSArray<MCDUserAccount*>*)getUserAccounts
{
@synchronized(self)
{
return _account ? @[ _account ] : nil;
}
}
- (void)onAccessTokenError:(NSString*)__unused accountId scopes:(NSArray<NSString*>*)__unused scopes isPermanentError:(BOOL)isPermanentError
{
@synchronized(self)
{
if (isPermanentError)
{
[self _removeAccount];
}
else
{
[_tokenCache markAllTokensExpired];
}
}
}
#pragma mark - MSATokenCache Delegate
- (void)onTokenCachePermanentFailure
{
if (_account)
{
[self onAccessTokenError:_account.accountId scopes:[_tokenCache allScopes] isPermanentError:YES];
}
}
@end

Просмотреть файл

@ -1,42 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#pragma once
#import <Foundation/Foundation.h>
// @brief Receives callback from the cache for any permanent failures
@protocol MSATokenCacheDelegate <NSObject>
- (void)onTokenCachePermanentFailure;
@end
// @brief Interface for caching and automatically refreshing MSA refresh and access tokens.
// Refresh tokens are automatically saved to disk.
// On permanent failure (cannot retry), a callback is sent to the delegate.
// These interfaces currently only support one user. forUser: will be added after platform support for multi-user is enabled.
@interface MSATokenCache : NSObject
+ (nullable instancetype)cacheWithClientId:(nonnull NSString*)clientId delegate:(nullable id<MSATokenCacheDelegate>)delegate;
- (nullable instancetype)initWithClientId:(nonnull NSString*)clientId delegate:(nullable id<MSATokenCacheDelegate>)delegate;
// @brief Adds/gets tokens to/from the cache, automatically refreshing them once expired.
- (void)setRefreshToken:(nonnull NSString*)refreshToken;
- (void)setAccessToken:(nonnull NSString*)accessToken forScope:(nonnull NSString*)scope expiresIn:(NSTimeInterval)expiry;
- (void)getRefreshTokenAsync:(nonnull void (^)(NSString* _Nullable accessToken))callback;
- (void)getAccessTokenForScopeAsync:(nonnull NSString*)scope callback:(nonnull void (^)(NSString* _Nullable accessToken))callback;
// @brief Returns the scopes for which there are currently access tokens cached.
- (nonnull NSArray<NSString*>*)allScopes;
// @brief Attempts to load a refresh token that was previously saved, and returns the success value of the operation.
// If successful, the loaded refresh token can be retrieved from getRefreshTokenAsync:
- (BOOL)loadSavedRefreshToken;
// @brief Clears the cache, including the saved refresh token.
- (void)clearTokens;
// @brief Marks all tokens as expired, such that a refresh will be attempted before the next time any token is returned.
- (void)markAllTokensExpired;
@property(nonatomic, readwrite, nullable, strong) id<MSATokenCacheDelegate> delegate;
@end

Просмотреть файл

@ -1,500 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#import "MSATokenCache.h"
#import "MSATokenRequest.h"
static NSString* const MsaOfflineAccessScope = @"wl.offline_access";
static NSString* const JsonTokenKey = @"refresh_token";
static NSString* const JsonExpirationKey = @"expires";
// Max number of times to try to refresh a token through transient failures
static const NSUInteger MsaTokenRefreshMaxRetries = 3;
// How quickly to retry refresh token refreshes on transient failure; 30 minutes.
static const int64_t MsaRefreshTokenRetryInterval = 30 * 60;
// How quickly to retry access token refreshes on transient failure; 3 minutes.
static const int64_t MsaAccessTokenRetryInterval = 3 * 60;
// How long a refresh token is expected to last without expiring; 10 days.
static const NSTimeInterval MsaRefreshTokenExpirationInterval = 10 * 24 * 60 * 60;
// Time from expiration at which a refresh token is considered 'close to expiring'; 7 days.
// (This value is intended to be aggressive and keep the refresh token relatively far from expiry)
static const NSTimeInterval MsaRefreshTokenCloseToExpiryInterval = 7 * 24 * 60 * 60;
// Time from expiration at which an access token is considered 'close to expiring'; 5 minutes.
static const NSTimeInterval MsaAccessTokenCloseToExpiryInterval = 5 * 60;
// @brief Private helper class, encapsulates a single MSA token to be cached, and how to refresh it.
@interface MSATokenCacheItem : NSObject
+ (nullable instancetype)cacheItemWithToken:(nonnull NSString*)token
expiresIn:(NSTimeInterval)expiry
refreshWith:(nonnull MSATokenRequest*)refreshRequest
parent:(nonnull MSATokenCache*)parent;
- (nullable instancetype)initWithToken:(nonnull NSString*)token
expiresIn:(NSTimeInterval)expiry
refreshWith:(nonnull MSATokenRequest*)refreshRequest
parent:(nonnull MSATokenCache*)parent;
// Asynchronously fetches the token held by this item, refreshing it if necessary.
- (void)getTokenAsync:(nonnull void (^)(NSString* _Nullable token))callback;
@property(readwrite, nonnull, nonatomic, copy) NSString* token;
@property(readwrite, nonnull, nonatomic, strong) NSDate* expirationDate;
@property(readwrite, nonnull, nonatomic, strong) MSATokenRequest* refreshRequest;
@property(readwrite, nonnull, nonatomic, strong) MSATokenCache* parent;
@property(readonly, nonatomic) NSTimeInterval closeToExpiryInterval;
@property(readonly, nonatomic) int64_t retryInterval;
// Private helper for refreshing this token. Only to be used by this class and its subclass.
// Returns the refresh token needed to refresh the token held by this item.
// For access tokens, this gets the refresh token held by the cache.
// For refresh tokens, just return the currently-held token.
- (void)getRefreshTokenAsync:(nonnull void (^)(NSString* _Nullable token))callback;
// Private helper for refreshing this token. Only to be used by this class and its subclass.
// For access tokens, sets the new token and expiration.
// For refresh tokens, marks current access tokens as expired, and caches the refresh token in persistent storage.
- (void)onSuccessfulRefresh:(nonnull MSATokenRequestResult*)result;
@end
// @brief Subclass of MSATokenCacheItem for refresh tokens
@interface MSARefreshTokenCacheItem : MSATokenCacheItem
+ (nullable instancetype)loadSavedRefreshTokenWithParent:(nonnull MSATokenCache*)parent;
- (void)saveRefreshToken;
@end
// MSATokenCache privates
@interface MSATokenCache ()
@property(readonly, nonnull, nonatomic, copy) NSString* clientId;
@property(readonly, nonnull, nonatomic, strong) NSMutableDictionary<NSString*, MSATokenCacheItem*>* cachedAccessTokens; // keyed on scopes
@property(readwrite, nullable, nonatomic, strong) MSARefreshTokenCacheItem* cachedRefreshToken;
- (void)markAccessTokensExpired;
@end
@implementation MSATokenCacheItem
+ (instancetype)cacheItemWithToken:(NSString*)token
expiresIn:(NSTimeInterval)expiry
refreshWith:(MSATokenRequest*)refreshRequest
parent:(MSATokenCache*)parent
{
return [[self alloc] initWithToken:token expiresIn:expiry refreshWith:refreshRequest parent:parent];
}
- (instancetype)initWithToken:(NSString*)token
expiresIn:(NSTimeInterval)expiry
refreshWith:(MSATokenRequest*)refreshRequest
parent:(MSATokenCache*)parent
{
if (self = [super init])
{
_token = [token copy];
_expirationDate = [NSDate dateWithTimeIntervalSinceNow:expiry];
_refreshRequest = refreshRequest;
_parent = parent;
}
return self;
}
- (void)getTokenAsync:(void (^)(NSString*))callback
{
[self getTokenAsync:callback maxRetries:MsaTokenRefreshMaxRetries];
}
- (void)getTokenAsync:(void (^)(NSString*))callback maxRetries:(NSUInteger)maxRetries
{
if ([_expirationDate timeIntervalSinceNow] >= self.closeToExpiryInterval)
{
// If expiration date is sufficiently far away
callback(self.token);
}
else
{
// If expired or close to it, get the refresh token and attempt to refresh with it
[self getRefreshTokenAsync:^void(NSString* refreshToken) {
if (!refreshToken)
{
// Unable to get the refresh token even after retrying
// Consider as a permanent failure and call back with no tokens
NSLog(@"Unable to get refresh token. Cancelling refresh and removing all tokens from cache.");
[_parent clearTokens];
callback(nil);
}
NSLog(@"Refreshing token...");
[_refreshRequest
requestAsyncWithToken:refreshToken
callback:^void(MSATokenRequestResult* result) {
switch (result.status)
{
case MSATokenRequestStatusSuccess:
{
[self onSuccessfulRefresh:result];
callback(self.token);
break;
}
case MSATokenRequestStatusTransientFailure:
{
if (maxRetries > 0)
{
// Retry the refresh
NSLog(@"Encountered transient error when refreshing token, retrying in %lld seconds...",
self.retryInterval);
dispatch_time_t retryTime = dispatch_time(DISPATCH_TIME_NOW, self.retryInterval * NSEC_PER_SEC);
dispatch_after(retryTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{ [self getTokenAsync:callback maxRetries:(maxRetries - 1)]; });
}
else
{
// Reached max number of retries
NSLog(@"Reached max number of retries for refreshing token.");
callback(nil);
}
break;
}
default: // PermanentFailure
{
NSLog(@"Permanent error occurred while refreshing token. Clearing the cache...");
[_parent clearTokens];
[_parent.delegate onTokenCachePermanentFailure];
callback(nil);
break;
}
}
}];
}];
}
}
- (NSTimeInterval)closeToExpiryInterval
{
return MsaAccessTokenCloseToExpiryInterval; // Base class expects access tokens
}
- (int64_t)retryInterval
{
return MsaAccessTokenRetryInterval; // Base class expects access tokens
}
- (void)getRefreshTokenAsync:(void (^)(NSString*))callback
{
[_parent getRefreshTokenAsync:callback]; // Base class expects access tokens, grab the refresh token from the parent
}
- (void)onSuccessfulRefresh:(MSATokenRequestResult*)result
{
@synchronized(self)
{
NSString* newToken = result.accessToken;
NSAssert(newToken.length > 0, @"UNEXPECTED: Refresh access token succeeded but access token was empty.");
NSLog(@"Successfully refreshed access token.");
self.token = newToken;
self.expirationDate = [NSDate dateWithTimeIntervalSinceNow:result.expiresIn];
}
}
@end
@implementation MSARefreshTokenCacheItem
+ (instancetype)loadSavedRefreshTokenWithParent:(MSATokenCache*)parent
{
NSLog(@"Loading refresh token from keychain...");
// clang-format off
NSDictionary* keychainMatchQuery = @{
(id) kSecClass : (id) kSecClassGenericPassword,
(id) kSecAttrGeneric : parent.clientId,
(id) kSecMatchLimit : (id) kSecMatchLimitOne, // Only match one keychain item
(id) kSecReturnData : @YES // Return the data itself rather than a ref
};
// clang-format on
CFTypeRef keychainItems = NULL;
OSStatus keychainStatus = SecItemCopyMatching((CFDictionaryRef)keychainMatchQuery, &keychainItems);
if (keychainStatus == errSecItemNotFound)
{
NSLog(@"No refresh token found in keychain.");
return nil;
}
else if (keychainStatus != errSecSuccess)
{
NSLog(@"Unable to load refresh token from keychain with OSStatus %d", (int)keychainStatus);
return nil;
}
NSError* jsonError = nil;
CFDataRef tokenData = (CFDataRef)keychainItems;
id deserializedTokenData = [NSJSONSerialization JSONObjectWithData:(__bridge NSData*)tokenData options:0 error:&jsonError];
if (jsonError)
{
NSLog(@"Encountered JSON error \'%@\' while trying to load refresh token from keychain.", jsonError);
return nil;
}
else if (![deserializedTokenData isKindOfClass:[NSDictionary class]])
{
NSLog(@"Loaded refresh token data from keychain was in an unexpected format. Will not load.");
return nil;
}
NSDictionary* tokenDict = (NSDictionary*)deserializedTokenData;
NSString* loadedRefreshToken = (NSString*)(tokenDict[JsonTokenKey]);
NSDateFormatter* dateFormatter = [NSDateFormatter new];
dateFormatter.dateStyle = NSDateFormatterFullStyle;
dateFormatter.timeStyle = NSDateFormatterFullStyle;
NSDate* loadedRefreshTokenExpiry = [dateFormatter dateFromString:(NSString*)(tokenDict[JsonExpirationKey])];
if (!loadedRefreshToken || !loadedRefreshTokenExpiry)
{
NSLog(@"Loaded refresh token data from keychain was incomplete or corrupted.");
return nil;
}
NSTimeInterval timeUntilExpiration = [loadedRefreshTokenExpiry timeIntervalSinceDate:[NSDate date]];
MSATokenRequest* refreshRequest = [MSATokenRequest tokenRequestWithClientId:parent.clientId
grantType:MsaTokenRequestGrantTypeRefresh
scope:MsaOfflineAccessScope
redirectUri:nil];
MSARefreshTokenCacheItem* ret =
[self cacheItemWithToken:loadedRefreshToken expiresIn:timeUntilExpiration refreshWith:refreshRequest parent:parent];
NSLog(@"Successfully loaded refresh token from keychain.");
return ret;
}
- (void)saveRefreshToken
{
NSLog(@"Saving refresh token to keychain...");
NSDateFormatter* dateFormatter = [NSDateFormatter new];
dateFormatter.dateStyle = NSDateFormatterFullStyle;
dateFormatter.timeStyle = NSDateFormatterFullStyle;
NSDictionary* tokenDict = @{ JsonTokenKey : self.token, JsonExpirationKey : [dateFormatter stringFromDate:self.expirationDate] };
NSError* jsonError = nil;
NSData* tokenData = [NSJSONSerialization dataWithJSONObject:tokenDict options:0 error:&jsonError];
if (jsonError)
{
NSLog(@"Encountered JSON error \'%@\' while trying to save refresh token to keychain. Will not save.", jsonError);
return;
}
// clang-format off
NSDictionary* keychainSearchQuery = @{
(id) kSecClass : (id) kSecClassGenericPassword,
(id) kSecAttrGeneric : self.parent.clientId
};
// clang-format on
OSStatus keychainStatus = SecItemUpdate((CFDictionaryRef)keychainSearchQuery, (CFDictionaryRef) @{ (id) kSecValueData : tokenData });
if (keychainStatus == errSecItemNotFound)
{
// After a device restart, this keychain item is only accessible after the device is unlocked at least once.
// This keychain item is not migrated when restoring a backup from another device.
id accessAttribute = (id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
NSMutableDictionary* keychainAddQuery = [keychainSearchQuery mutableCopy];
[keychainAddQuery addEntriesFromDictionary:@{ (id) kSecAttrAccessible : accessAttribute, (id) kSecValueData : tokenData }];
keychainStatus = SecItemAdd((CFDictionaryRef)keychainAddQuery, NULL);
}
if (keychainStatus != errSecSuccess)
{
NSLog(@"Failed to save refresh token data to keychain with OSStatus %d.", (int)keychainStatus);
}
NSLog(@"Successfully saved refresh token data to keychain.");
}
- (NSTimeInterval)closeToExpiryInterval
{
return MsaRefreshTokenCloseToExpiryInterval;
}
- (int64_t)retryInterval
{
return MsaRefreshTokenRetryInterval;
}
- (void)getRefreshTokenAsync:(void (^)(NSString*))callback
{
callback(self.token); // Since this cache item holds a refresh token, just return it
}
- (void)onSuccessfulRefresh:(MSATokenRequestResult*)result
{
@synchronized(self)
{
NSString* newToken = result.refreshToken;
NSAssert(newToken.length > 0, @"UNEXPECTED: Refresh refresh token succeeded but access token was empty.");
NSLog(@"Successfully refreshed refresh token.");
self.token = newToken;
self.expirationDate = [NSDate dateWithTimeIntervalSinceNow:MsaRefreshTokenExpirationInterval];
[self saveRefreshToken];
[self.parent markAccessTokensExpired];
}
}
@end
// MSATokenCache implementation
@implementation MSATokenCache
+ (instancetype)cacheWithClientId:(NSString*)clientId delegate:(id<MSATokenCacheDelegate>)delegate
{
return [[self alloc] initWithClientId:clientId delegate:delegate];
}
- (instancetype)initWithClientId:(NSString*)clientId delegate:(id<MSATokenCacheDelegate>)delegate
{
if (self = [super init])
{
_clientId = [clientId copy];
_delegate = delegate;
_cachedAccessTokens = [NSMutableDictionary<NSString*, MSATokenCacheItem*> new];
}
return self;
}
- (void)setRefreshToken:(NSString*)refreshToken
{
MSATokenRequest* refreshRequest = [MSATokenRequest tokenRequestWithClientId:_clientId
grantType:MsaTokenRequestGrantTypeRefresh
scope:MsaOfflineAccessScope
redirectUri:nil];
@synchronized(self)
{
_cachedRefreshToken = [MSARefreshTokenCacheItem cacheItemWithToken:refreshToken
expiresIn:MsaRefreshTokenExpirationInterval
refreshWith:refreshRequest
parent:self];
[_cachedRefreshToken saveRefreshToken];
[self markAccessTokensExpired];
}
}
- (void)setAccessToken:(NSString*)accessToken forScope:(NSString*)scope expiresIn:(NSTimeInterval)expiry
{
MSATokenRequest* refreshRequest =
[MSATokenRequest tokenRequestWithClientId:_clientId grantType:MsaTokenRequestGrantTypeRefresh scope:scope redirectUri:nil];
@synchronized(self)
{
[_cachedAccessTokens
setValue:[MSATokenCacheItem cacheItemWithToken:accessToken expiresIn:expiry refreshWith:refreshRequest parent:self]
forKey:scope];
}
}
- (void)getRefreshTokenAsync:(void (^)(NSString*))callback
{
@synchronized(self)
{
if (_cachedRefreshToken)
{
[_cachedRefreshToken getTokenAsync:callback];
}
else
{
callback(nil);
}
}
}
- (void)getAccessTokenForScopeAsync:(NSString*)scope callback:(void (^)(NSString*))callback
{
@synchronized(self)
{
MSATokenCacheItem* item = [_cachedAccessTokens valueForKey:scope];
if (item)
{
[item getTokenAsync:callback];
}
else
{
callback(nil);
}
}
}
- (NSArray<NSString*>*)allScopes
{
return [_cachedAccessTokens allKeys];
}
- (BOOL)loadSavedRefreshToken
{
MSARefreshTokenCacheItem* loadedRefreshToken = [MSARefreshTokenCacheItem loadSavedRefreshTokenWithParent:self];
if (loadedRefreshToken)
{
if ([loadedRefreshToken.expirationDate compare:[NSDate date]] != NSOrderedDescending)
{
NSLog(@"Refresh token loaded from keychain was expired. Ignoring.");
return NO;
}
@synchronized(self)
{
_cachedRefreshToken = loadedRefreshToken;
[self markAllTokensExpired]; // Force a refresh on everything on first use
}
}
return (loadedRefreshToken != nil);
}
- (void)clearTokens
{
NSLog(@"Clearing token data from cache...");
@synchronized(self)
{
[_cachedAccessTokens removeAllObjects];
_cachedRefreshToken = nil;
}
// clang-format off
NSDictionary* keychainDeleteQuery = @{
(id) kSecClass : (id) kSecClassGenericPassword,
(id) kSecAttrGeneric : _clientId
};
// clang-format on
OSStatus keychainStatus = SecItemDelete((CFDictionaryRef)keychainDeleteQuery);
if (keychainStatus != errSecSuccess)
{
NSLog(@"Unable to clear token data from keychain with OSStatus %d. Data might still be loaded on next run.", (int)keychainStatus);
}
NSLog(@"Done clearing token data from cache.");
}
- (void)markAccessTokensExpired
{
@synchronized(self)
{
for (MSATokenCacheItem* cachedAccessToken in _cachedAccessTokens.allValues)
{
cachedAccessToken.expirationDate = [NSDate distantPast];
}
}
}
- (void)markAllTokensExpired
{
@synchronized(self)
{
_cachedRefreshToken.expirationDate = [NSDate distantPast];
[self markAccessTokensExpired];
}
}
@end

Просмотреть файл

@ -1,66 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#pragma once
#import <Foundation/Foundation.h>
extern NSString* _Nonnull const MsaTokenRequestGrantTypeCode;
extern NSString* _Nonnull const MsaTokenRequestGrantTypeRefresh;
typedef NS_ENUM(NSInteger, MSATokenRequestStatus)
{
MSATokenRequestStatusSuccess,
MSATokenRequestStatusTransientFailure,
MSATokenRequestStatusPermanentFailure
};
@interface MSATokenRequestResult : NSObject
@property(readwrite, nonatomic) MSATokenRequestStatus status;
@property(readwrite, nullable, nonatomic, copy) NSString* accessToken;
@property(readwrite, nullable, nonatomic, copy) NSString* refreshToken;
@property(readwrite, nonatomic) NSInteger expiresIn;
@end
/**
* @brief Encapsulates a noninteractive request for an MSA token.
* This request may be performed multiple times.
*/
@interface MSATokenRequest : NSObject
/**
* Fetches Token (Access or Refresh Token).
* clientId - clientId of the app's registration in the MSA portal
* grantType - one of the MsaTokenRequestGrantType constants
* scope
* redirectUri
* token - authCode for MsaTokenRequestGrantTypeCode, or refresh token for MsaTokenRequestGrantTypeRefresh
*/
+ (void)doAsyncRequestWithClientId:(nonnull NSString*)clientId
grantType:(nonnull NSString*)grantType
scope:(nullable NSString*)scope
redirectUri:(nullable NSString*)redirectUri
token:(nonnull NSString*)token
callback:(nonnull void (^)(MSATokenRequestResult* _Nonnull result))callback;
+ (nullable instancetype)tokenRequestWithClientId:(nonnull NSString*)clientId
grantType:(nonnull NSString*)grantType
scope:(nullable NSString*)scope
redirectUri:(nullable NSString*)redirectUri;
- (nullable instancetype)initWithClientId:(nonnull NSString*)clientId
grantType:(nonnull NSString*)grantType
scope:(nullable NSString*)scope
redirectUri:(nullable NSString*)redirectUri;
@property(readonly, nonnull, nonatomic, copy) NSString* clientId;
@property(readonly, nonnull, nonatomic, copy) NSString* grantType;
@property(readonly, nullable, nonatomic, copy) NSString* scope;
@property(readonly, nullable, nonatomic, copy) NSString* redirectUri;
- (void)requestAsyncWithToken:(nonnull NSString*)token callback:(nonnull void (^)(MSATokenRequestResult* _Nonnull result))callback;
@end

Просмотреть файл

@ -1,165 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#import "MSATokenRequest.h"
NSString* const MsaTokenRequestGrantTypeCode = @"authorization_code";
NSString* const MsaTokenRequestGrantTypeRefresh = @"refresh_token";
static const NSTimeInterval MsaTokenRequestTimeout = 30.0;
// Helper function - encodes an NSDictionary to be usable as POST data in an NSURLRequest
static NSData* EncodeDictionary(NSDictionary<NSString*, NSString*>* dictionary)
{
NSMutableArray<NSString*>* parts = [NSMutableArray<NSString*> new];
for (NSString* key in dictionary)
{
NSString* encodedValue = [[dictionary objectForKey:key] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSString* encodedKey = [key stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
[parts addObject:[NSString stringWithFormat:@"%@=%@", encodedKey, encodedValue]];
}
NSString* encodedDictionary = [parts componentsJoinedByString:@"&"];
return [encodedDictionary dataUsingEncoding:NSUTF8StringEncoding];
}
@interface MSATokenRequestResult ()
+ (instancetype)resultWithStatus:(MSATokenRequestStatus)status responseDictionary:(nullable NSDictionary*)responseDict;
@end
@implementation MSATokenRequestResult
+ (instancetype)resultWithStatus:(MSATokenRequestStatus)status responseDictionary:(NSDictionary*)responseDict
{
MSATokenRequestResult* ret = [self new];
if (ret)
{
ret.status = status;
if (responseDict)
{
ret.accessToken = [responseDict valueForKey:@"access_token"];
ret.refreshToken = [responseDict valueForKey:@"refresh_token"];
ret.expiresIn = [[responseDict valueForKey:@"expires_in"] integerValue];
}
}
return ret;
}
@end
@implementation MSATokenRequest
+ (void)doAsyncRequestWithClientId:(NSString*)clientId
grantType:(NSString*)grantType
scope:(NSString*)scope
redirectUri:(NSString*)redirectUri
token:(NSString*)token
callback:(void (^)(MSATokenRequestResult*))callback
{
NSLog(@"Requesting token for scope %@", scope);
NSURL* url = [NSURL URLWithString:@"https://login.live.com/oauth20_token.srf"];
NSMutableURLRequest* request =
[NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:MsaTokenRequestTimeout];
[request addValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
NSMutableDictionary<NSString*, NSString*>* params = [NSMutableDictionary<NSString*, NSString*> new];
[params setObject:clientId forKey:@"client_id"];
[params setObject:grantType forKey:@"grant_type"];
if ([grantType isEqualToString:MsaTokenRequestGrantTypeCode])
{
[params setObject:redirectUri forKey:@"redirect_uri"];
[params setObject:token forKey:@"code"];
}
else if ([grantType isEqualToString:MsaTokenRequestGrantTypeRefresh])
{
if (scope)
{
[params setObject:scope forKey:@"scope"];
}
[params setObject:token forKey:MsaTokenRequestGrantTypeRefresh];
}
request.HTTPBody = EncodeDictionary(params);
request.HTTPMethod = @"POST";
static NSOperationQueue* queue = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ queue = [NSOperationQueue new]; });
NSLog(@"MSATokenRequest issuing HTTP token request.");
[NSURLConnection sendAsynchronousRequest:request
queue:queue
completionHandler:^void(NSURLResponse* response, NSData* data, NSError* error) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response; // This cast should always work
NSLog(@"MSATokenRequest response code %ld.", (long)httpResponse.statusCode);
MSATokenRequestStatus status = MSATokenRequestStatusTransientFailure;
if (httpResponse.statusCode >= 500)
{
status = MSATokenRequestStatusTransientFailure;
}
else if (httpResponse.statusCode >= 400)
{
status = MSATokenRequestStatusPermanentFailure;
}
else if ((httpResponse.statusCode >= 200 && httpResponse.statusCode < 300) || httpResponse.statusCode == 304)
{
status = MSATokenRequestStatusSuccess;
}
else
{
status = MSATokenRequestStatusTransientFailure;
}
if (data)
{
NSDictionary* responseDict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(@"MSATokenRequest data:%@", responseDict);
callback([MSATokenRequestResult resultWithStatus:status responseDictionary:responseDict]);
}
else
{
NSLog(@"MSATokenRequest error:%@", error);
callback([MSATokenRequestResult resultWithStatus:status responseDictionary:nil]);
}
}];
}
+ (instancetype)tokenRequestWithClientId:(NSString*)clientId
grantType:(NSString*)grantType
scope:(NSString*)scope
redirectUri:(NSString*)redirectUri
{
return [[self alloc] initWithClientId:clientId grantType:grantType scope:scope redirectUri:redirectUri];
}
- (instancetype)initWithClientId:(NSString*)clientId
grantType:(NSString*)grantType
scope:(NSString*)scope
redirectUri:(NSString*)redirectUri
{
if (self = [super init])
{
_clientId = [clientId copy];
_grantType = [grantType copy];
_scope = [scope copy];
_redirectUri = [redirectUri copy];
}
return self;
}
- (void)requestAsyncWithToken:(NSString*)token callback:(void (^)(MSATokenRequestResult*))callback
{
[[self class] doAsyncRequestWithClientId:_clientId
grantType:_grantType
scope:_scope
redirectUri:_redirectUri
token:token
callback:callback];
}
@end