From e4f7096ab558a35f44c2a657644194a18241be24 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Mon, 25 May 2026 17:30:28 -0700 Subject: [PATCH 01/42] Setup empty cred managment project. --- .../project.pbxproj | 512 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../MSALNativeCredManagment.md | 13 + .../MSALNativeCredManagment.h | 18 + .../MSALNativeCredManagmentTests.swift | 17 + 5 files changed, 567 insertions(+) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/MSALNativeCredManagment.docc/MSALNativeCredManagment.md create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/MSALNativeCredManagment.h create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentTests/MSALNativeCredManagmentTests.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..2086796319 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj @@ -0,0 +1,512 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 2394A84C2FC5208100B9FF0C /* MSALNativeCredManagment.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2394A8412FC5208100B9FF0C /* MSALNativeCredManagment.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 2394A84D2FC5208100B9FF0C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2394A8382FC5208100B9FF0C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2394A8402FC5208100B9FF0C; + remoteInfo = MSALNativeCredManagment; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 2394A8412FC5208100B9FF0C /* MSALNativeCredManagment.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MSALNativeCredManagment.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2394A84B2FC5208100B9FF0C /* MSALNativeCredManagmentTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MSALNativeCredManagmentTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 2394A8532FC5208100B9FF0C /* Exceptions for "MSALNativeCredManagment" folder in "MSALNativeCredManagment" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + publicHeaders = ( + MSALNativeCredManagment.h, + ); + target = 2394A8402FC5208100B9FF0C /* MSALNativeCredManagment */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 2394A8432FC5208100B9FF0C /* MSALNativeCredManagment */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 2394A8532FC5208100B9FF0C /* Exceptions for "MSALNativeCredManagment" folder in "MSALNativeCredManagment" target */, + ); + path = MSALNativeCredManagment; + sourceTree = ""; + }; + 2394A84F2FC5208100B9FF0C /* MSALNativeCredManagmentTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = MSALNativeCredManagmentTests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2394A83E2FC5208100B9FF0C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2394A8482FC5208100B9FF0C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2394A84C2FC5208100B9FF0C /* MSALNativeCredManagment.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2394A8372FC5208100B9FF0C = { + isa = PBXGroup; + children = ( + 2394A8432FC5208100B9FF0C /* MSALNativeCredManagment */, + 2394A84F2FC5208100B9FF0C /* MSALNativeCredManagmentTests */, + 2394A8422FC5208100B9FF0C /* Products */, + ); + sourceTree = ""; + }; + 2394A8422FC5208100B9FF0C /* Products */ = { + isa = PBXGroup; + children = ( + 2394A8412FC5208100B9FF0C /* MSALNativeCredManagment.framework */, + 2394A84B2FC5208100B9FF0C /* MSALNativeCredManagmentTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 2394A83C2FC5208100B9FF0C /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 2394A8402FC5208100B9FF0C /* MSALNativeCredManagment */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2394A8542FC5208100B9FF0C /* Build configuration list for PBXNativeTarget "MSALNativeCredManagment" */; + buildPhases = ( + 2394A83C2FC5208100B9FF0C /* Headers */, + 2394A83D2FC5208100B9FF0C /* Sources */, + 2394A83E2FC5208100B9FF0C /* Frameworks */, + 2394A83F2FC5208100B9FF0C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 2394A8432FC5208100B9FF0C /* MSALNativeCredManagment */, + ); + name = MSALNativeCredManagment; + packageProductDependencies = ( + ); + productName = MSALNativeCredManagment; + productReference = 2394A8412FC5208100B9FF0C /* MSALNativeCredManagment.framework */; + productType = "com.apple.product-type.framework"; + }; + 2394A84A2FC5208100B9FF0C /* MSALNativeCredManagmentTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2394A8592FC5208100B9FF0C /* Build configuration list for PBXNativeTarget "MSALNativeCredManagmentTests" */; + buildPhases = ( + 2394A8472FC5208100B9FF0C /* Sources */, + 2394A8482FC5208100B9FF0C /* Frameworks */, + 2394A8492FC5208100B9FF0C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2394A84E2FC5208100B9FF0C /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 2394A84F2FC5208100B9FF0C /* MSALNativeCredManagmentTests */, + ); + name = MSALNativeCredManagmentTests; + packageProductDependencies = ( + ); + productName = MSALNativeCredManagmentTests; + productReference = 2394A84B2FC5208100B9FF0C /* MSALNativeCredManagmentTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 2394A8382FC5208100B9FF0C /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1620; + LastUpgradeCheck = 1620; + TargetAttributes = { + 2394A8402FC5208100B9FF0C = { + CreatedOnToolsVersion = 16.2; + }; + 2394A84A2FC5208100B9FF0C = { + CreatedOnToolsVersion = 16.2; + }; + }; + }; + buildConfigurationList = 2394A83B2FC5208100B9FF0C /* Build configuration list for PBXProject "MSALNativeCredManagment" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 2394A8372FC5208100B9FF0C; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 2394A8422FC5208100B9FF0C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2394A8402FC5208100B9FF0C /* MSALNativeCredManagment */, + 2394A84A2FC5208100B9FF0C /* MSALNativeCredManagmentTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 2394A83F2FC5208100B9FF0C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2394A8492FC5208100B9FF0C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 2394A83D2FC5208100B9FF0C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2394A8472FC5208100B9FF0C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 2394A84E2FC5208100B9FF0C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2394A8402FC5208100B9FF0C /* MSALNativeCredManagment */; + targetProxy = 2394A84D2FC5208100B9FF0C /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 2394A8552FC5208100B9FF0C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = UBF8T346G9; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LD_RUNPATH_SEARCH_PATHS = ( + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.MSALNativeCredManagment.MSALNativeCredManagment; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = auto; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + XROS_DEPLOYMENT_TARGET = 2.2; + }; + name = Debug; + }; + 2394A8562FC5208100B9FF0C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = UBF8T346G9; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LD_RUNPATH_SEARCH_PATHS = ( + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.MSALNativeCredManagment.MSALNativeCredManagment; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = auto; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + XROS_DEPLOYMENT_TARGET = 2.2; + }; + name = Release; + }; + 2394A8572FC5208100B9FF0C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 2394A8582FC5208100B9FF0C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 2394A85A2FC5208100B9FF0C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = UBF8T346G9; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + MACOSX_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.MSALNativeCredManagment.MSALNativeCredManagmentTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + XROS_DEPLOYMENT_TARGET = 2.2; + }; + name = Debug; + }; + 2394A85B2FC5208100B9FF0C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = UBF8T346G9; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + MACOSX_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.MSALNativeCredManagment.MSALNativeCredManagmentTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + XROS_DEPLOYMENT_TARGET = 2.2; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2394A83B2FC5208100B9FF0C /* Build configuration list for PBXProject "MSALNativeCredManagment" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2394A8572FC5208100B9FF0C /* Debug */, + 2394A8582FC5208100B9FF0C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2394A8542FC5208100B9FF0C /* Build configuration list for PBXNativeTarget "MSALNativeCredManagment" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2394A8552FC5208100B9FF0C /* Debug */, + 2394A8562FC5208100B9FF0C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2394A8592FC5208100B9FF0C /* Build configuration list for PBXNativeTarget "MSALNativeCredManagmentTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2394A85A2FC5208100B9FF0C /* Debug */, + 2394A85B2FC5208100B9FF0C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 2394A8382FC5208100B9FF0C /* Project object */; +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/MSALNativeCredManagment.docc/MSALNativeCredManagment.md b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/MSALNativeCredManagment.docc/MSALNativeCredManagment.md new file mode 100644 index 0000000000..769c241024 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/MSALNativeCredManagment.docc/MSALNativeCredManagment.md @@ -0,0 +1,13 @@ +# ``MSALNativeCredManagment`` + +Summary + +## Overview + +Text + +## Topics + +### Group + +- ``Symbol`` \ No newline at end of file diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/MSALNativeCredManagment.h b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/MSALNativeCredManagment.h new file mode 100644 index 0000000000..408f37adb6 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/MSALNativeCredManagment.h @@ -0,0 +1,18 @@ +// +// MSALNativeCredManagment.h +// MSALNativeCredManagment +// +// Created by Serhii Demchenko on 2026-05-25. +// + +#import + +//! Project version number for MSALNativeCredManagment. +FOUNDATION_EXPORT double MSALNativeCredManagmentVersionNumber; + +//! Project version string for MSALNativeCredManagment. +FOUNDATION_EXPORT const unsigned char MSALNativeCredManagmentVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentTests/MSALNativeCredManagmentTests.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentTests/MSALNativeCredManagmentTests.swift new file mode 100644 index 0000000000..e655fc5cbd --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentTests/MSALNativeCredManagmentTests.swift @@ -0,0 +1,17 @@ +// +// MSALNativeCredManagmentTests.swift +// MSALNativeCredManagmentTests +// +// Created by Serhii Demchenko on 2026-05-25. +// + +import Testing +@testable import MSALNativeCredManagment + +struct MSALNativeCredManagmentTests { + + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } + +} From 3e96c2c6216de7622cd8e61ad8c1e64a90f2e8eb Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Wed, 27 May 2026 17:26:37 -0700 Subject: [PATCH 02/42] Import msal SDK. --- .../project.pbxproj | 20 +++++++++++++++ .../MSALNativeCredentialMethodsClient.swift | 25 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj index 2086796319..92e5337ebc 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 2394A84C2FC5208100B9FF0C /* MSALNativeCredManagment.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2394A8412FC5208100B9FF0C /* MSALNativeCredManagment.framework */; }; + 2394A88B2FC7C14100B9FF0C /* MSAL in Frameworks */ = {isa = PBXBuildFile; productRef = 2394A88A2FC7C14100B9FF0C /* MSAL */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -56,6 +57,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 2394A88B2FC7C14100B9FF0C /* MSAL in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -119,6 +121,7 @@ ); name = MSALNativeCredManagment; packageProductDependencies = ( + 2394A88A2FC7C14100B9FF0C /* MSAL */, ); productName = MSALNativeCredManagment; productReference = 2394A8412FC5208100B9FF0C /* MSALNativeCredManagment.framework */; @@ -174,6 +177,9 @@ ); mainGroup = 2394A8372FC5208100B9FF0C; minimizedProjectReferenceProxies = 1; + packageReferences = ( + 2394A8892FC7C14100B9FF0C /* XCLocalSwiftPackageReference "../../../../microsoft-authentication-library-for-objc" */, + ); preferredProjectObjectVersion = 77; productRefGroup = 2394A8422FC5208100B9FF0C /* Products */; projectDirPath = ""; @@ -507,6 +513,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 2394A8892FC7C14100B9FF0C /* XCLocalSwiftPackageReference "../../../../microsoft-authentication-library-for-objc" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../../../../microsoft-authentication-library-for-objc"; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 2394A88A2FC7C14100B9FF0C /* MSAL */ = { + isa = XCSwiftPackageProductDependency; + productName = MSAL; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 2394A8382FC5208100B9FF0C /* Project object */; } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift new file mode 100644 index 0000000000..8ae4162598 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -0,0 +1,25 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import MSAL From 6358fd6e767ebbde43055375ecc18538172806ff Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Wed, 27 May 2026 21:36:51 -0700 Subject: [PATCH 03/42] Draft public api. --- .../src/public/MSALCredentialMethod.swift | 69 ++++ .../MSALCredentialMethodChallengeState.swift | 128 ++++++++ .../public/MSALNativeAuthTokenProvider.swift | 106 ++++++ ...MSALNativeCredentialManagementConfig.swift | 67 ++++ ...LNativeCredentialManagementDelegates.swift | 77 +++++ .../MSALNativeCredentialManagementError.swift | 99 ++++++ ...iveCredentialManagementTokenProvider.swift | 50 +++ .../MSALNativeCredentialMethodsClient.swift | 306 +++++++++++++++++- 8 files changed, 901 insertions(+), 1 deletion(-) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethod.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethodChallengeState.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeAuthTokenProvider.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementDelegates.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementError.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementTokenProvider.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethod.swift new file mode 100644 index 0000000000..ed3539fb20 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethod.swift @@ -0,0 +1,69 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents a single credential method registered by the user. +/// +/// Instances of this class are returned by `MSALNativeCredentialMethodsClient` when +/// listing or registering credential methods. +@objcMembers +public class MSALCredentialMethod: NSObject { + + /// Unique identifier of the credential method. + public let id: String + + /// The type of credential (e.g., "password", "email", "phone", "passkey"). + public let credentialType: String + + /// Display-friendly name or hint (e.g., masked email "j***@contoso.com"). + public let displayName: String? + + /// Whether this is the default/primary method. + public let isDefault: Bool + + /// Timestamp of when this method was registered. + public let createdAt: Date? + + /// Additional metadata associated with this credential method. + public let metadata: [String: String]? + + internal init( + id: String, + credentialType: String, + displayName: String?, + isDefault: Bool, + createdAt: Date?, + metadata: [String: String]? + ) + { + self.id = id + self.credentialType = credentialType + self.displayName = displayName + self.isDefault = isDefault + self.createdAt = createdAt + self.metadata = metadata + super.init() + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethodChallengeState.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethodChallengeState.swift new file mode 100644 index 0000000000..b1d1797db5 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethodChallengeState.swift @@ -0,0 +1,128 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// The channel type through which a challenge was sent. +@objc public enum MSALCredentialMethodChallengeChannel: Int { + /// Challenge sent via email. + case email = 0 + /// Challenge sent via SMS/phone. + case phone = 1 + /// Challenge requires authenticator app. + case authenticatorApp = 2 + /// Unknown or unspecified channel. + case unknown = 99 +} + +/// Represents the state of a credential method registration that requires challenge verification. +/// +/// When registering a new credential method, the server may require the user to verify ownership +/// (e.g., enter an OTP sent to the new email/phone). This state object provides the context +/// and methods to complete or resend the challenge. +@objcMembers +public class MSALCredentialMethodChallengeState: NSObject { + + /// The channel through which the challenge was sent. + public let challengeChannel: MSALCredentialMethodChallengeChannel + + /// A display hint for the challenge target (e.g., masked email or phone number). + public let sentTo: String? + + /// The length of the expected code, if applicable. + public let codeLength: Int + + internal let continuationToken: String + internal weak var client: MSALNativeCredentialMethodsClient? + + internal init( + challengeChannel: MSALCredentialMethodChallengeChannel, + sentTo: String?, + codeLength: Int, + continuationToken: String, + client: MSALNativeCredentialMethodsClient? + ) + { + self.challengeChannel = challengeChannel + self.sentTo = sentTo + self.codeLength = codeLength + self.continuationToken = continuationToken + self.client = client + super.init() + } + + /// Submit the challenge code to complete credential method registration. + /// + /// - Parameters: + /// - code: The verification code entered by the user. + /// - delegate: Receives the registration result or error. + public func submitChallenge( + code: String, + delegate: MSALCredentialMethodRegisterDelegate + ) + { + guard let client = client else + { + let error = MSALNativeCredentialManagementError( + type: .sessionExpired, + message: "Client reference has been released. Please restart the registration flow." + ) + DispatchQueue.main.async + { + delegate.onCredentialMethodRegistrationError(error: error) + } + return + } + client.submitRegistrationChallenge( + code: code, + continuationToken: continuationToken, + delegate: delegate + ) + } + + /// Request the server to resend the challenge code. + /// + /// - Parameter delegate: Receives a new challenge state or error. + public func resendChallenge( + delegate: MSALCredentialMethodRegisterDelegate + ) + { + guard let client = client else + { + let error = MSALNativeCredentialManagementError( + type: .sessionExpired, + message: "Client reference has been released. Please restart the registration flow." + ) + DispatchQueue.main.async + { + delegate.onCredentialMethodRegistrationError(error: error) + } + return + } + client.resendRegistrationChallenge( + continuationToken: continuationToken, + delegate: delegate + ) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeAuthTokenProvider.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeAuthTokenProvider.swift new file mode 100644 index 0000000000..1af9f938e2 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeAuthTokenProvider.swift @@ -0,0 +1,106 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import MSAL + +/// A built-in token provider that wraps `MSALNativeAuthUserAccountResult` to obtain +/// access tokens using the existing MSAL Native Auth cached session. +/// +/// Use this provider (P1) when you want automatic token management through MSAL: +/// ```swift +/// let tokenProvider = MSALNativeAuthTokenProvider(userAccountResult: userAccountResult) +/// credConfig.tokenProvider = tokenProvider +/// ``` +/// +/// - Important: This provider holds a weak reference to `userAccountResult`. If the user +/// signs out or the result is deallocated, the provider will return a `sessionExpired` error. +@objcMembers +public class MSALNativeAuthTokenProvider: NSObject, MSALNativeCredentialManagementTokenProvider { + + private weak var userAccountResult: MSALNativeAuthUserAccountResult? + + /// Initialize with the user account result obtained from a successful MSAL Native Auth sign-in. + /// + /// - Parameter userAccountResult: The account result containing cached tokens and account info. + public init(userAccountResult: MSALNativeAuthUserAccountResult) + { + self.userAccountResult = userAccountResult + super.init() + } + + /// Retrieve an access token by delegating to MSAL Native Auth's silent token retrieval. + /// + /// - Parameters: + /// - scopes: The scopes required by the credential management operation. + /// - completionBlock: Called with the access token on success, or nil and an error on failure. + public func getAccessToken( + scopes: [String], + completionBlock: @escaping MSALNativeCredentialManagementTokenCompletionBlock + ) + { + guard let accountResult = userAccountResult else + { + let error = MSALNativeCredentialManagementError( + type: .sessionExpired, + message: "User account result is no longer available. The user may have signed out. " + + "Please re-authenticate and create a new MSALNativeAuthTokenProvider instance." + ) + completionBlock(nil, error) + return + } + + let params = MSALNativeAuthGetAccessTokenParameters() + params.scopes = scopes + + accountResult.getAccessToken(parameters: params, delegate: TokenProviderCredentialsDelegate(completionBlock: completionBlock)) + } +} + +/// Internal delegate that bridges the MSAL CredentialsDelegate pattern to a completion block. +private class TokenProviderCredentialsDelegate: NSObject, CredentialsDelegate { + + private let completionBlock: MSALNativeCredentialManagementTokenCompletionBlock + + init(completionBlock: @escaping MSALNativeCredentialManagementTokenCompletionBlock) + { + self.completionBlock = completionBlock + super.init() + } + + @MainActor func onAccessTokenRetrieveError(error: RetrieveAccessTokenError) + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Failed to retrieve access token from MSAL: \(error.errorDescription ?? "Unknown error")", + correlationId: error.correlationId + ) + completionBlock(nil, credError) + } + + @MainActor func onAccessTokenRetrieveCompleted(result: MSALNativeAuthTokenResult) + { + completionBlock(result.accessToken, nil) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift new file mode 100644 index 0000000000..4798fca48d --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift @@ -0,0 +1,67 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import MSAL + +/// Configuration for the credential management client. +/// +/// Use this class to configure the `MSALNativeCredentialMethodsClient` with a token provider, +/// optional request interceptor (shared with MSAL), and API base URL. +/// +/// Example: +/// ```swift +/// let credConfig = MSALNativeCredentialManagementConfig() +/// credConfig.requestInterceptor = sharedRequestInterceptor +/// credConfig.tokenProvider = MyCustomTokenProvider() +/// let credClient = try MSALNativeCredentialMethodsClient(config: credConfig) +/// ``` +@objcMembers +public class MSALNativeCredentialManagementConfig: NSObject { + + /// An optional interceptor for injecting custom HTTP headers into credential management requests. + /// + /// Can be shared with `MSALNativeAuthPublicClientApplicationConfig.requestInterceptor` + /// for consistent header injection across both MSAL and credential management calls. + public var requestInterceptor: MSALNativeAuthRequestInterceptor? + + /// The token provider used to obtain access tokens for credential management API calls. + /// + /// - P0: Assign a custom implementation conforming to `MSALNativeCredentialManagementTokenProvider`. + /// - P1: Assign `MSALNativeAuthTokenProvider(userAccountResult:)` for automatic MSAL-based token retrieval. + public var tokenProvider: MSALNativeCredentialManagementTokenProvider? + + /// The base URL for the credential management API. + /// + /// When nil, the client derives the endpoint from the tenant configuration. + public var baseURL: URL? + + /// The correlation ID to use for requests. If nil, a new one is generated per operation. + public var correlationId: UUID? + + public override init() + { + super.init() + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementDelegates.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementDelegates.swift new file mode 100644 index 0000000000..bd3a50ed8f --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementDelegates.swift @@ -0,0 +1,77 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +// MARK: - List Credential Methods Delegate + +/// Protocol that defines the delegate methods for listing credential methods. +@objc public protocol MSALCredentialMethodsListDelegate { + + /// Notifies the delegate that the list operation completed successfully. + /// - Parameter methods: The array of credential methods registered for the user. + @MainActor func onCredentialMethodsListCompleted(methods: [MSALCredentialMethod]) + + /// Notifies the delegate that the list operation resulted in an error. + /// - Parameter error: An error object indicating why the operation failed. + @MainActor func onCredentialMethodsListError(error: MSALNativeCredentialManagementError) +} + +// MARK: - Register Credential Method Delegate + +/// Protocol that defines the delegate methods for registering a credential method. +@objc public protocol MSALCredentialMethodRegisterDelegate { + + /// Notifies the delegate that the registration completed successfully. + /// - Parameter method: The newly registered credential method. + @MainActor func onCredentialMethodRegistrationCompleted(method: MSALCredentialMethod) + + /// Notifies the delegate that the registration resulted in an error. + /// - Parameter error: An error object indicating why the operation failed. + @MainActor func onCredentialMethodRegistrationError(error: MSALNativeCredentialManagementError) + + /// Notifies the delegate that a challenge verification is required to complete registration. + /// + /// The user must provide a verification code (e.g., OTP sent to the new email/phone). + /// Call `state.submitChallenge(code:delegate:)` to continue. + /// + /// - Note: If this optional method is not implemented, `onCredentialMethodRegistrationError(error:)` will be called instead. + /// - Parameter state: The challenge state containing information about the sent challenge and methods to respond. + @MainActor @objc optional func onCredentialMethodChallengeRequired( + state: MSALCredentialMethodChallengeState + ) +} + +// MARK: - Delete Credential Method Delegate + +/// Protocol that defines the delegate methods for deleting a credential method. +@objc public protocol MSALCredentialMethodDeleteDelegate { + + /// Notifies the delegate that the delete operation completed successfully. + @MainActor func onCredentialMethodDeleteCompleted() + + /// Notifies the delegate that the delete operation resulted in an error. + /// - Parameter error: An error object indicating why the operation failed. + @MainActor func onCredentialMethodDeleteError(error: MSALNativeCredentialManagementError) +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementError.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementError.swift new file mode 100644 index 0000000000..da4158c746 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementError.swift @@ -0,0 +1,99 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Error domain for credential management operations. +public let MSALNativeCredentialManagementErrorDomain = "MSALNativeCredentialManagementErrorDomain" + +/// Error types for credential management operations. +@objc public enum MSALNativeCredentialManagementErrorType: Int { + /// A general, unclassified error occurred. + case generalError = 0 + /// A network error occurred (timeout, connectivity, etc.). + case networkError = 1 + /// The access token is invalid or expired. + case unauthorized = 2 + /// The user lacks permission for this operation. + case forbidden = 3 + /// The specified credential method was not found. + case notFound = 4 + /// A conflict occurred (e.g., method already registered). + case conflict = 5 + /// Challenge verification failed. + case challengeFailed = 6 + /// The token provider reports no valid session. + case sessionExpired = 7 + /// The client configuration is invalid. + case invalidConfiguration = 8 +} + +/// Error class for credential management operations. +/// +/// Contains a typed error code, human-readable message, and optional correlation ID +/// for diagnostics. +@objcMembers +public class MSALNativeCredentialManagementError: NSObject, Error { + + /// The type of error that occurred. + public let type: MSALNativeCredentialManagementErrorType + + /// A human-readable error message describing what went wrong. + public let message: String? + + /// The correlation ID associated with this error for diagnostic purposes. + public let correlationId: UUID? + + /// The underlying error, if any. + public let underlyingError: Error? + + internal init( + type: MSALNativeCredentialManagementErrorType, + message: String? = nil, + correlationId: UUID? = nil, + underlyingError: Error? = nil + ) + { + self.type = type + self.message = message + self.correlationId = correlationId + self.underlyingError = underlyingError + super.init() + } + + public override var description: String + { + var desc = "MSALNativeCredentialManagementError(type: \(type)" + if let message = message + { + desc += ", message: \(message)" + } + if let correlationId = correlationId + { + desc += ", correlationId: \(correlationId)" + } + desc += ")" + return desc + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementTokenProvider.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementTokenProvider.swift new file mode 100644 index 0000000000..a85693002a --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementTokenProvider.swift @@ -0,0 +1,50 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Completion block type for token retrieval. +public typealias MSALNativeCredentialManagementTokenCompletionBlock = @convention(block) (String?, Error?) -> Void + +/// Protocol for providing access tokens to the credential management client. +/// +/// Implement this protocol to supply access tokens for credential management API calls. +/// - P0: Provide a custom implementation that calls your own backend. +/// - P1: Use the built-in `MSALNativeAuthTokenProvider` that wraps MSAL Native Auth. +@objc public protocol MSALNativeCredentialManagementTokenProvider: NSObjectProtocol { + + /// Retrieve an access token suitable for calling the credential management API. + /// + /// - Important: `completionBlock` **must always be called**, regardless of whether a token is available. + /// - On success, call `completionBlock(accessToken, nil)`. + /// - On failure, call `completionBlock(nil, error)`. + /// + /// - Parameters: + /// - scopes: The scopes required by the credential management operation. + /// - completionBlock: Must be called with an access token string on success, or nil and an error on failure. + func getAccessToken( + scopes: [String], + completionBlock: @escaping MSALNativeCredentialManagementTokenCompletionBlock + ) +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index 8ae4162598..f9ded6dac1 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -16,10 +16,314 @@ // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +import Foundation import MSAL + +/// Client for managing credential methods of an authenticated CIAM user. +/// +/// This client provides APIs to list, register, and delete credential methods +/// (e.g., email, phone, passkey) for the currently signed-in user. +/// +/// Example: +/// ```swift +/// let credConfig = MSALNativeCredentialManagementConfig() +/// credConfig.requestInterceptor = sharedRequestInterceptor +/// credConfig.tokenProvider = myTokenProvider +/// let credClient = try MSALNativeCredentialMethodsClient(config: credConfig) +/// credClient.listCredentialMethods(delegate: self) +/// ``` +@objcMembers +public class MSALNativeCredentialMethodsClient: NSObject { + + private let config: MSALNativeCredentialManagementConfig + private let operationQueue: DispatchQueue + + /// Initialize the credential methods client. + /// + /// - Parameter config: Configuration including token provider and optional interceptor. + /// - Throws: `MSALNativeCredentialManagementError` if the configuration is invalid (e.g., no token provider set). + public init(config: MSALNativeCredentialManagementConfig) throws + { + guard config.tokenProvider != nil else + { + throw MSALNativeCredentialManagementError( + type: .invalidConfiguration, + message: "A token provider must be set on MSALNativeCredentialManagementConfig before initializing the client." + ) + } + self.config = config + self.operationQueue = DispatchQueue( + label: "com.microsoft.identity.credentialmanagement", + qos: .userInitiated + ) + super.init() + } + + // MARK: - List Credential Methods + + /// Retrieve the list of credential methods registered for the current user. + /// + /// - Parameter delegate: Receives the result or error callback on the main thread. + public func listCredentialMethods(delegate: MSALCredentialMethodsListDelegate) + { + let correlationId = config.correlationId ?? UUID() + + operationQueue.async + { [weak self] in + guard let self = self else { return } + + self.acquireToken(correlationId: correlationId) + { accessToken, error in + if let error = error + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Failed to acquire access token for listing credential methods.", + correlationId: correlationId, + underlyingError: error + ) + DispatchQueue.main.async + { + delegate.onCredentialMethodsListError(error: credError) + } + return + } + + guard let accessToken = accessToken else + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Token provider returned nil access token.", + correlationId: correlationId + ) + DispatchQueue.main.async + { + delegate.onCredentialMethodsListError(error: credError) + } + return + } + + // TODO: Implement network call to credential management API + // Placeholder: call delegate with empty array until API integration is complete + _ = accessToken + DispatchQueue.main.async + { + delegate.onCredentialMethodsListCompleted(methods: []) + } + } + } + } + + // MARK: - Register Credential Method + + /// Begin registration of a new credential method. + /// + /// - Parameters: + /// - type: The credential type to register (e.g., "email", "phone", "passkey"). + /// - parameters: Type-specific parameters (e.g., email address, phone number). + /// - delegate: Receives state transitions (challenge required, completed, error) on the main thread. + public func registerCredentialMethod( + type: String, + parameters: [String: Any]?, + delegate: MSALCredentialMethodRegisterDelegate + ) + { + let correlationId = config.correlationId ?? UUID() + + operationQueue.async + { [weak self] in + guard let self = self else { return } + + self.acquireToken(correlationId: correlationId) + { accessToken, error in + if let error = error + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Failed to acquire access token for registering credential method.", + correlationId: correlationId, + underlyingError: error + ) + DispatchQueue.main.async + { + delegate.onCredentialMethodRegistrationError(error: credError) + } + return + } + + guard accessToken != nil else + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Token provider returned nil access token.", + correlationId: correlationId + ) + DispatchQueue.main.async + { + delegate.onCredentialMethodRegistrationError(error: credError) + } + return + } + + // TODO: Implement network call to credential management API + let credError = MSALNativeCredentialManagementError( + type: .generalError, + message: "Registration not yet implemented.", + correlationId: correlationId + ) + DispatchQueue.main.async + { + delegate.onCredentialMethodRegistrationError(error: credError) + } + } + } + } + + // MARK: - Delete Credential Method + + /// Delete a credential method by its identifier. + /// + /// - Parameters: + /// - credentialMethodId: The ID of the credential method to remove. + /// - delegate: Receives completion or error callback on the main thread. + public func deleteCredentialMethod( + credentialMethodId: String, + delegate: MSALCredentialMethodDeleteDelegate + ) + { + let correlationId = config.correlationId ?? UUID() + + operationQueue.async + { [weak self] in + guard let self = self else { return } + + self.acquireToken(correlationId: correlationId) + { accessToken, error in + if let error = error + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Failed to acquire access token for deleting credential method.", + correlationId: correlationId, + underlyingError: error + ) + DispatchQueue.main.async + { + delegate.onCredentialMethodDeleteError(error: credError) + } + return + } + + guard accessToken != nil else + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Token provider returned nil access token.", + correlationId: correlationId + ) + DispatchQueue.main.async + { + delegate.onCredentialMethodDeleteError(error: credError) + } + return + } + + // TODO: Implement network call to credential management API + let credError = MSALNativeCredentialManagementError( + type: .generalError, + message: "Deletion not yet implemented.", + correlationId: correlationId + ) + DispatchQueue.main.async + { + delegate.onCredentialMethodDeleteError(error: credError) + } + } + } + } + + // MARK: - Internal: Challenge Handling + + internal func submitRegistrationChallenge( + code: String, + continuationToken: String, + delegate: MSALCredentialMethodRegisterDelegate + ) + { + let correlationId = config.correlationId ?? UUID() + + operationQueue.async + { [weak self] in + guard self != nil else { return } + + // TODO: Implement network call to submit challenge verification + let credError = MSALNativeCredentialManagementError( + type: .generalError, + message: "Challenge submission not yet implemented.", + correlationId: correlationId + ) + DispatchQueue.main.async + { + delegate.onCredentialMethodRegistrationError(error: credError) + } + } + } + + internal func resendRegistrationChallenge( + continuationToken: String, + delegate: MSALCredentialMethodRegisterDelegate + ) + { + let correlationId = config.correlationId ?? UUID() + + operationQueue.async + { [weak self] in + guard self != nil else { return } + + // TODO: Implement network call to resend challenge + let credError = MSALNativeCredentialManagementError( + type: .generalError, + message: "Challenge resend not yet implemented.", + correlationId: correlationId + ) + DispatchQueue.main.async + { + delegate.onCredentialMethodRegistrationError(error: credError) + } + } + } + + // MARK: - Private: Token Acquisition + + private func acquireToken( + correlationId: UUID, + completion: @escaping (String?, Error?) -> Void + ) + { + guard let tokenProvider = config.tokenProvider else + { + let error = MSALNativeCredentialManagementError( + type: .invalidConfiguration, + message: "Token provider is not configured.", + correlationId: correlationId + ) + completion(nil, error) + return + } + + // TODO: Replace with actual scopes for credential management API once defined + let scopes = ["openid", "offline_access"] + + tokenProvider.getAccessToken(scopes: scopes) + { accessToken, error in + completion(accessToken, error) + } + } +} + From d18fd00235f8fbafb23d79bbed4187dc123c08de Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Wed, 27 May 2026 21:41:51 -0700 Subject: [PATCH 04/42] Add sample app. --- .../project.pbxproj | 388 ++++++++++++++++++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 85 ++++ .../Assets.xcassets/Contents.json | 6 + .../ContentView.swift | 24 ++ ...LNativeCredManagmentSampleApp.entitlements | 10 + .../MSALNativeCredManagmentSampleAppApp.swift | 17 + .../Preview Assets.xcassets/Contents.json | 6 + ...SALNativeCredManagmentSampleAppTests.swift | 16 + ...LNativeCredManagmentSampleAppUITests.swift | 43 ++ ...ManagmentSampleAppUITestsLaunchTests.swift | 33 ++ 11 files changed, 639 insertions(+) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Assets.xcassets/Contents.json create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleApp.entitlements create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleAppApp.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleAppTests/MSALNativeCredManagmentSampleAppTests.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleAppUITests/MSALNativeCredManagmentSampleAppUITests.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleAppUITests/MSALNativeCredManagmentSampleAppUITestsLaunchTests.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj index 92e5337ebc..0767942338 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj @@ -19,11 +19,28 @@ remoteGlobalIDString = 2394A8402FC5208100B9FF0C; remoteInfo = MSALNativeCredManagment; }; + 2394A8B32FC7FE7400B9FF0C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2394A8382FC5208100B9FF0C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2394A8A12FC7FE7200B9FF0C; + remoteInfo = MSALNativeCredManagmentSampleApp; + }; + 2394A8BD2FC7FE7400B9FF0C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2394A8382FC5208100B9FF0C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2394A8A12FC7FE7200B9FF0C; + remoteInfo = MSALNativeCredManagmentSampleApp; + }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 2394A8412FC5208100B9FF0C /* MSALNativeCredManagment.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MSALNativeCredManagment.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2394A84B2FC5208100B9FF0C /* MSALNativeCredManagmentTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MSALNativeCredManagmentTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2394A8A22FC7FE7200B9FF0C /* MSALNativeCredManagmentSampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MSALNativeCredManagmentSampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2394A8B22FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MSALNativeCredManagmentSampleAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2394A8BC2FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MSALNativeCredManagmentSampleAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -50,6 +67,21 @@ path = MSALNativeCredManagmentTests; sourceTree = ""; }; + 2394A8A32FC7FE7200B9FF0C /* MSALNativeCredManagmentSampleApp */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = MSALNativeCredManagmentSampleApp; + sourceTree = ""; + }; + 2394A8B52FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = MSALNativeCredManagmentSampleAppTests; + sourceTree = ""; + }; + 2394A8BF2FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = MSALNativeCredManagmentSampleAppUITests; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -69,6 +101,27 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 2394A89F2FC7FE7200B9FF0C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2394A8AF2FC7FE7400B9FF0C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2394A8B92FC7FE7400B9FF0C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -77,6 +130,9 @@ children = ( 2394A8432FC5208100B9FF0C /* MSALNativeCredManagment */, 2394A84F2FC5208100B9FF0C /* MSALNativeCredManagmentTests */, + 2394A8A32FC7FE7200B9FF0C /* MSALNativeCredManagmentSampleApp */, + 2394A8B52FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppTests */, + 2394A8BF2FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppUITests */, 2394A8422FC5208100B9FF0C /* Products */, ); sourceTree = ""; @@ -86,6 +142,9 @@ children = ( 2394A8412FC5208100B9FF0C /* MSALNativeCredManagment.framework */, 2394A84B2FC5208100B9FF0C /* MSALNativeCredManagmentTests.xctest */, + 2394A8A22FC7FE7200B9FF0C /* MSALNativeCredManagmentSampleApp.app */, + 2394A8B22FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppTests.xctest */, + 2394A8BC2FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppUITests.xctest */, ); name = Products; sourceTree = ""; @@ -150,6 +209,74 @@ productReference = 2394A84B2FC5208100B9FF0C /* MSALNativeCredManagmentTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + 2394A8A12FC7FE7200B9FF0C /* MSALNativeCredManagmentSampleApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2394A8C42FC7FE7400B9FF0C /* Build configuration list for PBXNativeTarget "MSALNativeCredManagmentSampleApp" */; + buildPhases = ( + 2394A89E2FC7FE7200B9FF0C /* Sources */, + 2394A89F2FC7FE7200B9FF0C /* Frameworks */, + 2394A8A02FC7FE7200B9FF0C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 2394A8A32FC7FE7200B9FF0C /* MSALNativeCredManagmentSampleApp */, + ); + name = MSALNativeCredManagmentSampleApp; + packageProductDependencies = ( + ); + productName = MSALNativeCredManagmentSampleApp; + productReference = 2394A8A22FC7FE7200B9FF0C /* MSALNativeCredManagmentSampleApp.app */; + productType = "com.apple.product-type.application"; + }; + 2394A8B12FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2394A8C72FC7FE7400B9FF0C /* Build configuration list for PBXNativeTarget "MSALNativeCredManagmentSampleAppTests" */; + buildPhases = ( + 2394A8AE2FC7FE7400B9FF0C /* Sources */, + 2394A8AF2FC7FE7400B9FF0C /* Frameworks */, + 2394A8B02FC7FE7400B9FF0C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2394A8B42FC7FE7400B9FF0C /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 2394A8B52FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppTests */, + ); + name = MSALNativeCredManagmentSampleAppTests; + packageProductDependencies = ( + ); + productName = MSALNativeCredManagmentSampleAppTests; + productReference = 2394A8B22FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 2394A8BB2FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2394A8CA2FC7FE7400B9FF0C /* Build configuration list for PBXNativeTarget "MSALNativeCredManagmentSampleAppUITests" */; + buildPhases = ( + 2394A8B82FC7FE7400B9FF0C /* Sources */, + 2394A8B92FC7FE7400B9FF0C /* Frameworks */, + 2394A8BA2FC7FE7400B9FF0C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2394A8BE2FC7FE7400B9FF0C /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 2394A8BF2FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppUITests */, + ); + name = MSALNativeCredManagmentSampleAppUITests; + packageProductDependencies = ( + ); + productName = MSALNativeCredManagmentSampleAppUITests; + productReference = 2394A8BC2FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -166,6 +293,17 @@ 2394A84A2FC5208100B9FF0C = { CreatedOnToolsVersion = 16.2; }; + 2394A8A12FC7FE7200B9FF0C = { + CreatedOnToolsVersion = 16.2; + }; + 2394A8B12FC7FE7400B9FF0C = { + CreatedOnToolsVersion = 16.2; + TestTargetID = 2394A8A12FC7FE7200B9FF0C; + }; + 2394A8BB2FC7FE7400B9FF0C = { + CreatedOnToolsVersion = 16.2; + TestTargetID = 2394A8A12FC7FE7200B9FF0C; + }; }; }; buildConfigurationList = 2394A83B2FC5208100B9FF0C /* Build configuration list for PBXProject "MSALNativeCredManagment" */; @@ -187,6 +325,9 @@ targets = ( 2394A8402FC5208100B9FF0C /* MSALNativeCredManagment */, 2394A84A2FC5208100B9FF0C /* MSALNativeCredManagmentTests */, + 2394A8A12FC7FE7200B9FF0C /* MSALNativeCredManagmentSampleApp */, + 2394A8B12FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppTests */, + 2394A8BB2FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppUITests */, ); }; /* End PBXProject section */ @@ -206,6 +347,27 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 2394A8A02FC7FE7200B9FF0C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2394A8B02FC7FE7400B9FF0C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2394A8BA2FC7FE7400B9FF0C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -223,6 +385,27 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 2394A89E2FC7FE7200B9FF0C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2394A8AE2FC7FE7400B9FF0C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2394A8B82FC7FE7400B9FF0C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -231,6 +414,16 @@ target = 2394A8402FC5208100B9FF0C /* MSALNativeCredManagment */; targetProxy = 2394A84D2FC5208100B9FF0C /* PBXContainerItemProxy */; }; + 2394A8B42FC7FE7400B9FF0C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2394A8A12FC7FE7200B9FF0C /* MSALNativeCredManagmentSampleApp */; + targetProxy = 2394A8B32FC7FE7400B9FF0C /* PBXContainerItemProxy */; + }; + 2394A8BE2FC7FE7400B9FF0C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2394A8A12FC7FE7200B9FF0C /* MSALNativeCredManagmentSampleApp */; + targetProxy = 2394A8BD2FC7FE7400B9FF0C /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -482,6 +675,174 @@ }; name = Release; }; + 2394A8C52FC7FE7400B9FF0C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleApp.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"MSALNativeCredManagmentSampleApp/Preview Content\""; + DEVELOPMENT_TEAM = UBF8T346G9; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.MSALNativeCredManagment.MSALNativeCredManagmentSampleApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + XROS_DEPLOYMENT_TARGET = 2.2; + }; + name = Debug; + }; + 2394A8C62FC7FE7400B9FF0C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleApp.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"MSALNativeCredManagmentSampleApp/Preview Content\""; + DEVELOPMENT_TEAM = UBF8T346G9; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.MSALNativeCredManagment.MSALNativeCredManagmentSampleApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + XROS_DEPLOYMENT_TARGET = 2.2; + }; + name = Release; + }; + 2394A8C82FC7FE7400B9FF0C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = UBF8T346G9; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + MACOSX_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.MSALNativeCredManagment.MSALNativeCredManagmentSampleAppTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MSALNativeCredManagmentSampleApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MSALNativeCredManagmentSampleApp"; + XROS_DEPLOYMENT_TARGET = 2.2; + }; + name = Debug; + }; + 2394A8C92FC7FE7400B9FF0C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = UBF8T346G9; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + MACOSX_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.MSALNativeCredManagment.MSALNativeCredManagmentSampleAppTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MSALNativeCredManagmentSampleApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MSALNativeCredManagmentSampleApp"; + XROS_DEPLOYMENT_TARGET = 2.2; + }; + name = Release; + }; + 2394A8CB2FC7FE7400B9FF0C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = UBF8T346G9; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + MACOSX_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.MSALNativeCredManagment.MSALNativeCredManagmentSampleAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_TARGET_NAME = MSALNativeCredManagmentSampleApp; + XROS_DEPLOYMENT_TARGET = 2.2; + }; + name = Debug; + }; + 2394A8CC2FC7FE7400B9FF0C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = UBF8T346G9; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + MACOSX_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.MSALNativeCredManagment.MSALNativeCredManagmentSampleAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_TARGET_NAME = MSALNativeCredManagmentSampleApp; + XROS_DEPLOYMENT_TARGET = 2.2; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -512,6 +873,33 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 2394A8C42FC7FE7400B9FF0C /* Build configuration list for PBXNativeTarget "MSALNativeCredManagmentSampleApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2394A8C52FC7FE7400B9FF0C /* Debug */, + 2394A8C62FC7FE7400B9FF0C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2394A8C72FC7FE7400B9FF0C /* Build configuration list for PBXNativeTarget "MSALNativeCredManagmentSampleAppTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2394A8C82FC7FE7400B9FF0C /* Debug */, + 2394A8C92FC7FE7400B9FF0C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2394A8CA2FC7FE7400B9FF0C /* Build configuration list for PBXNativeTarget "MSALNativeCredManagmentSampleAppUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2394A8CB2FC7FE7400B9FF0C /* Debug */, + 2394A8CC2FC7FE7400B9FF0C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Assets.xcassets/AccentColor.colorset/Contents.json b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..ffdfe150be --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,85 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Assets.xcassets/Contents.json b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift new file mode 100644 index 0000000000..714ed4878f --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift @@ -0,0 +1,24 @@ +// +// ContentView.swift +// MSALNativeCredManagmentSampleApp +// +// Created by Serhii Demchenko on 2026-05-27. +// + +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} + +#Preview { + ContentView() +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleApp.entitlements b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleApp.entitlements new file mode 100644 index 0000000000..f2ef3ae026 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleApp.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleAppApp.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleAppApp.swift new file mode 100644 index 0000000000..455b921f1c --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleAppApp.swift @@ -0,0 +1,17 @@ +// +// MSALNativeCredManagmentSampleAppApp.swift +// MSALNativeCredManagmentSampleApp +// +// Created by Serhii Demchenko on 2026-05-27. +// + +import SwiftUI + +@main +struct MSALNativeCredManagmentSampleAppApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Preview Content/Preview Assets.xcassets/Contents.json b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleAppTests/MSALNativeCredManagmentSampleAppTests.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleAppTests/MSALNativeCredManagmentSampleAppTests.swift new file mode 100644 index 0000000000..811eeb7824 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleAppTests/MSALNativeCredManagmentSampleAppTests.swift @@ -0,0 +1,16 @@ +// +// MSALNativeCredManagmentSampleAppTests.swift +// MSALNativeCredManagmentSampleAppTests +// +// Created by Serhii Demchenko on 2026-05-27. +// + +import Testing + +struct MSALNativeCredManagmentSampleAppTests { + + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } + +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleAppUITests/MSALNativeCredManagmentSampleAppUITests.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleAppUITests/MSALNativeCredManagmentSampleAppUITests.swift new file mode 100644 index 0000000000..d7215971fa --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleAppUITests/MSALNativeCredManagmentSampleAppUITests.swift @@ -0,0 +1,43 @@ +// +// MSALNativeCredManagmentSampleAppUITests.swift +// MSALNativeCredManagmentSampleAppUITests +// +// Created by Serhii Demchenko on 2026-05-27. +// + +import XCTest + +final class MSALNativeCredManagmentSampleAppUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + @MainActor + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + @MainActor + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleAppUITests/MSALNativeCredManagmentSampleAppUITestsLaunchTests.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleAppUITests/MSALNativeCredManagmentSampleAppUITestsLaunchTests.swift new file mode 100644 index 0000000000..6c79bfd71c --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleAppUITests/MSALNativeCredManagmentSampleAppUITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// MSALNativeCredManagmentSampleAppUITestsLaunchTests.swift +// MSALNativeCredManagmentSampleAppUITests +// +// Created by Serhii Demchenko on 2026-05-27. +// + +import XCTest + +final class MSALNativeCredManagmentSampleAppUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} From 30e9602e1959f21ecb537c3c9bcbbe890a4f8b67 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Thu, 28 May 2026 15:06:46 -0700 Subject: [PATCH 05/42] Use fake AT in sample app. --- .../project.pbxproj | 12 + .../Configuration.swift | 18 ++ .../ContentView.swift | 228 +++++++++++++++++- .../CredentialManagementViewModel.swift | 211 ++++++++++++++++ .../MSALNativeCredManagmentSampleAppApp.swift | 7 + .../SampleRequestInterceptor.swift | 26 ++ .../SampleTokenProvider.swift | 41 ++++ .../MSALNativeCredManagment/Package.swift | 37 +++ 8 files changed, 575 insertions(+), 5 deletions(-) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/SampleRequestInterceptor.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/SampleTokenProvider.swift create mode 100644 lib/native/MSALNativeCredManagment/Package.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj index 0767942338..7a4edb8215 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 2394A84C2FC5208100B9FF0C /* MSALNativeCredManagment.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2394A8412FC5208100B9FF0C /* MSALNativeCredManagment.framework */; }; 2394A88B2FC7C14100B9FF0C /* MSAL in Frameworks */ = {isa = PBXBuildFile; productRef = 2394A88A2FC7C14100B9FF0C /* MSAL */; }; + 2394A8D72FC8032400B9FF0C /* MSALNativeCredManagment in Frameworks */ = {isa = PBXBuildFile; productRef = 2394A8D62FC8032400B9FF0C /* MSALNativeCredManagment */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -105,6 +106,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 2394A8D72FC8032400B9FF0C /* MSALNativeCredManagment in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -226,6 +228,7 @@ ); name = MSALNativeCredManagmentSampleApp; packageProductDependencies = ( + 2394A8D62FC8032400B9FF0C /* MSALNativeCredManagment */, ); productName = MSALNativeCredManagmentSampleApp; productReference = 2394A8A22FC7FE7200B9FF0C /* MSALNativeCredManagmentSampleApp.app */; @@ -317,6 +320,7 @@ minimizedProjectReferenceProxies = 1; packageReferences = ( 2394A8892FC7C14100B9FF0C /* XCLocalSwiftPackageReference "../../../../microsoft-authentication-library-for-objc" */, + 2394A8D52FC8032400B9FF0C /* XCLocalSwiftPackageReference "../MSALNativeCredManagment" */, ); preferredProjectObjectVersion = 77; productRefGroup = 2394A8422FC5208100B9FF0C /* Products */; @@ -907,6 +911,10 @@ isa = XCLocalSwiftPackageReference; relativePath = "../../../../microsoft-authentication-library-for-objc"; }; + 2394A8D52FC8032400B9FF0C /* XCLocalSwiftPackageReference "../MSALNativeCredManagment" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../MSALNativeCredManagment; + }; /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -914,6 +922,10 @@ isa = XCSwiftPackageProductDependency; productName = MSAL; }; + 2394A8D62FC8032400B9FF0C /* MSALNativeCredManagment */ = { + isa = XCSwiftPackageProductDependency; + productName = MSALNativeCredManagment; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 2394A8382FC5208100B9FF0C /* Project object */; diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift new file mode 100644 index 0000000000..1ed82464af --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift @@ -0,0 +1,18 @@ +// +// Configuration.swift +// MSALNativeCredManagmentSampleApp +// +// Created by Serhii Demchenko on 2026-05-27. +// + +import Foundation + +/// Sample app configuration. +/// Replace these values with your own CIAM tenant settings. +enum Configuration { + /// The client ID of the application registered in the CIAM tenant. + static let clientId = "Enter_the_Application_Id_Here" + + /// The tenant subdomain (e.g., "contoso" for contoso.ciamlogin.com). + static let tenantSubdomain = "Enter_the_Tenant_Subdomain_Here" +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift index 714ed4878f..7985045df1 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift @@ -6,19 +6,237 @@ // import SwiftUI +import MSALNativeCredManagment struct ContentView: View { + + @EnvironmentObject var viewModel: CredentialManagementViewModel + + @State private var email = "" + @State private var password = "" + @State private var newCredentialType = "email" + @State private var newCredentialValue = "" + @State private var challengeCode = "" + var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") + NavigationStack { + Group { + if viewModel.isSignedIn { + signedInView + } else { + signInView + } + } + .navigationTitle("Cred Management") + .overlay { + if viewModel.isLoading { + ProgressView() + .scaleEffect(1.5) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.black.opacity(0.2)) + } + } + .alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) { + Button("OK") { viewModel.errorMessage = nil } + } message: { + Text(viewModel.errorMessage ?? "") + } + .sheet(isPresented: $viewModel.showChallengeInput) { + challengeView + } + } + } + + // MARK: - Sign In View + + private var signInView: some View { + VStack(spacing: 20) { + Image(systemName: "person.badge.key") + .font(.system(size: 60)) + .foregroundStyle(.blue) + + Text("Sign in to manage credentials") + .font(.headline) + + TextField("Email", text: $email) + .textFieldStyle(.roundedBorder) + .textContentType(.emailAddress) + .autocapitalization(.none) + + SecureField("Password", text: $password) + .textFieldStyle(.roundedBorder) + .textContentType(.password) + + Button("Sign In") { + viewModel.signIn(email: email, password: password) + } + .buttonStyle(.borderedProminent) + .disabled(email.isEmpty || password.isEmpty) + + if !viewModel.statusMessage.isEmpty { + Text(viewModel.statusMessage) + .font(.caption) + .foregroundStyle(.secondary) + } } .padding() } + + // MARK: - Signed In View + + private var signedInView: some View { + VStack(spacing: 16) { + // User info header + HStack { + VStack(alignment: .leading) { + Text("Signed in as") + .font(.caption) + .foregroundStyle(.secondary) + Text(viewModel.userName) + .font(.headline) + } + Spacer() + Button("Sign Out") { + viewModel.signOut() + } + .buttonStyle(.bordered) + .tint(.red) + } + .padding(.horizontal) + + Divider() + + // Status + if !viewModel.statusMessage.isEmpty { + Text(viewModel.statusMessage) + .font(.caption) + .foregroundStyle(.secondary) + .padding(.horizontal) + } + + // Credential methods list + List { + Section("Registered Methods") { + if viewModel.credentialMethods.isEmpty { + Text("No credential methods found.") + .foregroundStyle(.secondary) + } else { + ForEach(viewModel.credentialMethods, id: \.id) { method in + credentialMethodRow(method) + } + } + } + + Section("Add New Method") { + Picker("Type", selection: $newCredentialType) { + Text("Email").tag("email") + Text("Phone").tag("phone") + Text("Passkey").tag("passkey") + } + + TextField("Value (email or phone)", text: $newCredentialValue) + .textFieldStyle(.roundedBorder) + + Button("Register") { + viewModel.registerCredentialMethod( + type: newCredentialType, + value: newCredentialValue + ) + newCredentialValue = "" + } + .disabled(newCredentialValue.isEmpty) + } + } + .refreshable { + viewModel.listCredentialMethods() + } + } + .onAppear { + viewModel.listCredentialMethods() + } + } + + // MARK: - Credential Method Row + + private func credentialMethodRow(_ method: MSALCredentialMethod) -> some View { + HStack { + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(method.credentialType.capitalized) + .font(.subheadline) + .bold() + if method.isDefault { + Text("Default") + .font(.caption2) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.blue.opacity(0.2)) + .cornerRadius(4) + } + } + if let displayName = method.displayName { + Text(displayName) + .font(.caption) + .foregroundStyle(.secondary) + } + } + Spacer() + Button(role: .destructive) { + viewModel.deleteCredentialMethod(id: method.id) + } label: { + Image(systemName: "trash") + } + .buttonStyle(.borderless) + } + } + + // MARK: - Challenge Verification View + + private var challengeView: some View { + NavigationStack { + VStack(spacing: 20) { + Image(systemName: "lock.shield") + .font(.system(size: 50)) + .foregroundStyle(.orange) + + Text("Verification Required") + .font(.title2) + .bold() + + Text("A code was sent to \(viewModel.challengeHint). Enter it below to complete registration.") + .multilineTextAlignment(.center) + .foregroundStyle(.secondary) + + TextField("Verification Code", text: $challengeCode) + .textFieldStyle(.roundedBorder) + .keyboardType(.numberPad) + .multilineTextAlignment(.center) + .font(.title3) + + Button("Verify") { + viewModel.submitChallenge(code: challengeCode) + challengeCode = "" + } + .buttonStyle(.borderedProminent) + .disabled(challengeCode.isEmpty) + } + .padding() + .navigationTitle("Verify") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + viewModel.showChallengeInput = false + challengeCode = "" + } + } + } + } + } } #Preview { ContentView() + .environmentObject(CredentialManagementViewModel()) } + diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift new file mode 100644 index 0000000000..8da26d2a0d --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -0,0 +1,211 @@ +// +// CredentialManagementViewModel.swift +// MSALNativeCredManagmentSampleApp +// +// Created by Serhii Demchenko on 2026-05-27. +// + +import Foundation +import MSAL +import MSALNativeCredManagment +import SwiftUI + +/// Main view model that demonstrates the credential management SDK integration. +@MainActor +class CredentialManagementViewModel: ObservableObject { + + // MARK: - Published State + + @Published var isSignedIn = false + @Published var userName: String = "" + @Published var credentialMethods: [MSALCredentialMethod] = [] + @Published var isLoading = false + @Published var statusMessage: String = "" + @Published var errorMessage: String? + + // Challenge state + @Published var showChallengeInput = false + @Published var challengeHint: String = "" + + // MARK: - Private Properties + + private var credClient: MSALNativeCredentialMethodsClient? + private var tokenProvider: SampleTokenProvider? + private var pendingChallengeState: MSALCredentialMethodChallengeState? + + // MARK: - Initialization + + func initialize() { + do { + // 1. Create token provider + tokenProvider = SampleTokenProvider() + + // 2. Create shared request interceptor + let sharedRequestInterceptor = SampleRequestInterceptor() + + // 3. Initialize Credential Management Client + let credConfig = MSALNativeCredentialManagementConfig() + credConfig.requestInterceptor = sharedRequestInterceptor + credConfig.tokenProvider = tokenProvider + credClient = try MSALNativeCredentialMethodsClient(config: credConfig) + + statusMessage = "SDK initialized successfully." + } catch { + errorMessage = "Failed to initialize: \(error.localizedDescription)" + } + } + + // MARK: - Sign In (Fake for POC) + + func signIn(email: String, password: String) { + isLoading = true + statusMessage = "Signing in..." + errorMessage = nil + + // Simulate a brief network delay, then return fake token + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + guard let self = self else { return } + self.tokenProvider?.setSignedIn(true) + self.isSignedIn = true + self.userName = email + self.isLoading = false + self.statusMessage = "Signed in successfully (POC - fake token)." + } + } + + // MARK: - Credential Management Operations + + func listCredentialMethods() { + guard let credClient = credClient else { + errorMessage = "Credential client not initialized." + return + } + + isLoading = true + statusMessage = "Loading credential methods..." + errorMessage = nil + + credClient.listCredentialMethods(delegate: self) + } + + func registerCredentialMethod(type: String, value: String) { + guard let credClient = credClient else { + errorMessage = "Credential client not initialized." + return + } + + isLoading = true + statusMessage = "Registering \(type)..." + errorMessage = nil + + let parameters: [String: Any] = ["value": value] + credClient.registerCredentialMethod(type: type, parameters: parameters, delegate: self) + } + + func deleteCredentialMethod(id: String) { + guard let credClient = credClient else { + errorMessage = "Credential client not initialized." + return + } + + isLoading = true + statusMessage = "Deleting credential method..." + errorMessage = nil + + credClient.deleteCredentialMethod(credentialMethodId: id, delegate: self) + } + + func submitChallenge(code: String) { + guard let state = pendingChallengeState else { + errorMessage = "No pending challenge." + return + } + + isLoading = true + statusMessage = "Verifying code..." + showChallengeInput = false + + state.submitChallenge(code: code, delegate: self) + pendingChallengeState = nil + } + + // MARK: - Sign Out + + func signOut() { + tokenProvider?.setSignedIn(false) + isSignedIn = false + userName = "" + credentialMethods = [] + statusMessage = "Signed out." + } +} + +// MARK: - MSALCredentialMethodsListDelegate + +extension CredentialManagementViewModel: MSALCredentialMethodsListDelegate { + + nonisolated func onCredentialMethodsListCompleted(methods: [MSALCredentialMethod]) { + Task { @MainActor in + isLoading = false + credentialMethods = methods + statusMessage = "Loaded \(methods.count) credential method(s)." + } + } + + nonisolated func onCredentialMethodsListError(error: MSALNativeCredentialManagementError) { + Task { @MainActor in + isLoading = false + errorMessage = "List failed: \(error.message ?? "Unknown error")" + } + } +} + +// MARK: - MSALCredentialMethodRegisterDelegate + +extension CredentialManagementViewModel: MSALCredentialMethodRegisterDelegate { + + nonisolated func onCredentialMethodRegistrationCompleted(method: MSALCredentialMethod) { + Task { @MainActor in + isLoading = false + statusMessage = "Registered \(method.credentialType) successfully." + listCredentialMethods() + } + } + + nonisolated func onCredentialMethodRegistrationError(error: MSALNativeCredentialManagementError) { + Task { @MainActor in + isLoading = false + errorMessage = "Registration failed: \(error.message ?? "Unknown error")" + } + } + + nonisolated func onCredentialMethodChallengeRequired(state: MSALCredentialMethodChallengeState) { + Task { @MainActor in + isLoading = false + pendingChallengeState = state + challengeHint = state.sentTo ?? "your registered contact" + showChallengeInput = true + statusMessage = "Verification code sent to \(challengeHint)." + } + } +} + +// MARK: - MSALCredentialMethodDeleteDelegate + +extension CredentialManagementViewModel: MSALCredentialMethodDeleteDelegate { + + nonisolated func onCredentialMethodDeleteCompleted() { + Task { @MainActor in + isLoading = false + statusMessage = "Credential method deleted." + listCredentialMethods() + } + } + + nonisolated func onCredentialMethodDeleteError(error: MSALNativeCredentialManagementError) { + Task { @MainActor in + isLoading = false + errorMessage = "Delete failed: \(error.message ?? "Unknown error")" + } + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleAppApp.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleAppApp.swift index 455b921f1c..aff6ad513f 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleAppApp.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleAppApp.swift @@ -9,9 +9,16 @@ import SwiftUI @main struct MSALNativeCredManagmentSampleAppApp: App { + + @StateObject private var viewModel = CredentialManagementViewModel() + var body: some Scene { WindowGroup { ContentView() + .environmentObject(viewModel) + .onAppear { + viewModel.initialize() + } } } } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/SampleRequestInterceptor.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/SampleRequestInterceptor.swift new file mode 100644 index 0000000000..39f7107355 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/SampleRequestInterceptor.swift @@ -0,0 +1,26 @@ +// +// SampleRequestInterceptor.swift +// MSALNativeCredManagmentSampleApp +// +// Created by Serhii Demchenko on 2026-05-27. +// + +import Foundation +import MSAL +import MSALNativeCredManagment + +/// Sample request interceptor shared between MSAL and the Credential Management client. +/// +/// Demonstrates how to inject custom headers into credential management requests. +class SampleRequestInterceptor: NSObject, MSALNativeAuthRequestInterceptor { + + func addAdditionalHeaderFields( + _ requestUrl: URL?, + completionBlock: @escaping MSALNativeAuthRequestInterceptorAddHeaderCompletionBlock + ) { + let headers: [String: String] = [ + "x-sample-app-version": "1.0.0" + ] + completionBlock(headers) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/SampleTokenProvider.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/SampleTokenProvider.swift new file mode 100644 index 0000000000..31aa0f4c95 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/SampleTokenProvider.swift @@ -0,0 +1,41 @@ +// +// SampleTokenProvider.swift +// MSALNativeCredManagmentSampleApp +// +// Created by Serhii Demchenko on 2026-05-27. +// + +import Foundation +import MSAL +import MSALNativeCredManagment + +/// A sample fake token provider for POC purposes. +/// Returns a hardcoded access token without calling MSAL. +class SampleTokenProvider: NSObject, MSALNativeCredentialManagementTokenProvider { + + private var isSignedIn = false + + func setSignedIn(_ signedIn: Bool) { + isSignedIn = signedIn + } + + func getAccessToken( + scopes: [String], + completionBlock: @escaping MSALNativeCredentialManagementTokenCompletionBlock + ) { + guard isSignedIn else { + let error = NSError( + domain: "SampleTokenProvider", + code: -1, + userInfo: [NSLocalizedDescriptionKey: "No user signed in. Please sign in first."] + ) + completionBlock(nil, error) + return + } + + // Return a fake access token for POC + let fakeToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.fake-poc-access-token" + completionBlock(fakeToken, nil) + } +} + diff --git a/lib/native/MSALNativeCredManagment/Package.swift b/lib/native/MSALNativeCredManagment/Package.swift new file mode 100644 index 0000000000..6e8be0e859 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/Package.swift @@ -0,0 +1,37 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "MSALNativeCredManagment", + platforms: [ + .iOS(.v16), + .macOS(.v13) + ], + products: [ + .library( + name: "MSALNativeCredManagment", + targets: ["MSALNativeCredManagment"] + ) + ], + dependencies: [ + // MSAL SDK as a local dependency (relative path to the root of microsoft-authentication-library-for-objc) + .package(path: "../../..") + ], + targets: [ + .target( + name: "MSALNativeCredManagment", + dependencies: [ + .product(name: "MSAL", package: "microsoft-authentication-library-for-objc") + ], + path: "MSALNativeCredManagment/src", + sources: ["public"] + ), + .testTarget( + name: "MSALNativeCredManagmentTests", + dependencies: ["MSALNativeCredManagment"], + path: "MSALNativeCredManagmentTests" + ) + ] +) From 2bd31d51143ae55e2376a30f39a94b8eaf56937f Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Thu, 28 May 2026 15:17:02 -0700 Subject: [PATCH 06/42] Add logger config. --- .../CredentialManagementViewModel.swift | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 8da26d2a0d..9c80dba10d 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -37,13 +37,21 @@ class CredentialManagementViewModel: ObservableObject { func initialize() { do { - // 1. Create token provider + // 1. Configure shared logger (used by both MSAL and Credential Management) + MSALGlobalConfig.loggerConfig.logLevel = .verbose + MSALGlobalConfig.loggerConfig.setLogCallback { _, message, containsPII in + if !containsPII { + print("MSAL: \(message ?? "")") + } + } + + // 2. Create token provider tokenProvider = SampleTokenProvider() - // 2. Create shared request interceptor + // 3. Create shared request interceptor let sharedRequestInterceptor = SampleRequestInterceptor() - // 3. Initialize Credential Management Client + // 4. Initialize Credential Management Client let credConfig = MSALNativeCredentialManagementConfig() credConfig.requestInterceptor = sharedRequestInterceptor credConfig.tokenProvider = tokenProvider From 5dbea488619191dc4562bbb50651641b39c8730a Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Thu, 28 May 2026 15:22:57 -0700 Subject: [PATCH 07/42] add token_provider folder. --- .../public/{ => token_provider}/MSALNativeAuthTokenProvider.swift | 0 .../MSALNativeCredentialManagementTokenProvider.swift | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/{ => token_provider}/MSALNativeAuthTokenProvider.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/{ => token_provider}/MSALNativeCredentialManagementTokenProvider.swift (100%) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeAuthTokenProvider.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/token_provider/MSALNativeAuthTokenProvider.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeAuthTokenProvider.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/token_provider/MSALNativeAuthTokenProvider.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementTokenProvider.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/token_provider/MSALNativeCredentialManagementTokenProvider.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementTokenProvider.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/token_provider/MSALNativeCredentialManagementTokenProvider.swift From 363e7f5018a65d3ae7b45f05e305c72108206cd5 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Thu, 28 May 2026 15:48:21 -0700 Subject: [PATCH 08/42] mock cred storage. --- .../MSALNativeCredentialMethodsClient.swift | 81 +++++++++++++++---- .../MSALNativeCredManagment/Package.swift | 3 +- 2 files changed, 65 insertions(+), 19 deletions(-) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index f9ded6dac1..58a60aaa1f 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -44,6 +44,9 @@ public class MSALNativeCredentialMethodsClient: NSObject { private let config: MSALNativeCredentialManagementConfig private let operationQueue: DispatchQueue + // Mock storage simulating server-side credential methods + private var mockCredentialMethods: [MSALCredentialMethod] + /// Initialize the credential methods client. /// /// - Parameter config: Configuration including token provider and optional interceptor. @@ -62,6 +65,35 @@ public class MSALNativeCredentialMethodsClient: NSObject { label: "com.microsoft.identity.credentialmanagement", qos: .userInitiated ) + + // Seed with default credential methods for POC + self.mockCredentialMethods = [ + MSALCredentialMethod( + id: "fido-001", + credentialType: "fido2", + displayName: "Security Key (YubiKey 5)", + isDefault: true, + createdAt: Date(timeIntervalSinceNow: -86400 * 30), + metadata: ["aaguid": "2fc0579f-8113-47ea-b116-bb5a8db9202a"] + ), + MSALCredentialMethod( + id: "phone-001", + credentialType: "phone", + displayName: "+1 *** ***-4589", + isDefault: false, + createdAt: Date(timeIntervalSinceNow: -86400 * 60), + metadata: nil + ), + MSALCredentialMethod( + id: "password-001", + credentialType: "password", + displayName: "Password", + isDefault: false, + createdAt: Date(timeIntervalSinceNow: -86400 * 90), + metadata: nil + ) + ] + super.init() } @@ -109,12 +141,12 @@ public class MSALNativeCredentialMethodsClient: NSObject { return } - // TODO: Implement network call to credential management API - // Placeholder: call delegate with empty array until API integration is complete + // Mock: return current in-memory credential methods _ = accessToken + let methods = self.mockCredentialMethods DispatchQueue.main.async { - delegate.onCredentialMethodsListCompleted(methods: []) + delegate.onCredentialMethodsListCompleted(methods: methods) } } } @@ -171,15 +203,19 @@ public class MSALNativeCredentialMethodsClient: NSObject { return } - // TODO: Implement network call to credential management API - let credError = MSALNativeCredentialManagementError( - type: .generalError, - message: "Registration not yet implemented.", - correlationId: correlationId + // Mock: add new credential method to in-memory storage + let newMethod = MSALCredentialMethod( + id: "\(type)-\(UUID().uuidString.prefix(8))", + credentialType: type, + displayName: (parameters?["value"] as? String) ?? type, + isDefault: false, + createdAt: Date(), + metadata: nil ) + self.mockCredentialMethods.append(newMethod) DispatchQueue.main.async { - delegate.onCredentialMethodRegistrationError(error: credError) + delegate.onCredentialMethodRegistrationCompleted(method: newMethod) } } } @@ -234,15 +270,26 @@ public class MSALNativeCredentialMethodsClient: NSObject { return } - // TODO: Implement network call to credential management API - let credError = MSALNativeCredentialManagementError( - type: .generalError, - message: "Deletion not yet implemented.", - correlationId: correlationId - ) - DispatchQueue.main.async + // Mock: remove credential method from in-memory storage + if let index = self.mockCredentialMethods.firstIndex(where: { $0.id == credentialMethodId }) + { + self.mockCredentialMethods.remove(at: index) + DispatchQueue.main.async + { + delegate.onCredentialMethodDeleteCompleted() + } + } + else { - delegate.onCredentialMethodDeleteError(error: credError) + let credError = MSALNativeCredentialManagementError( + type: .notFound, + message: "Credential method with id '\(credentialMethodId)' not found.", + correlationId: correlationId + ) + DispatchQueue.main.async + { + delegate.onCredentialMethodDeleteError(error: credError) + } } } } diff --git a/lib/native/MSALNativeCredManagment/Package.swift b/lib/native/MSALNativeCredManagment/Package.swift index 6e8be0e859..0825285c73 100644 --- a/lib/native/MSALNativeCredManagment/Package.swift +++ b/lib/native/MSALNativeCredManagment/Package.swift @@ -25,8 +25,7 @@ let package = Package( dependencies: [ .product(name: "MSAL", package: "microsoft-authentication-library-for-objc") ], - path: "MSALNativeCredManagment/src", - sources: ["public"] + path: "MSALNativeCredManagment/src/public" ), .testTarget( name: "MSALNativeCredManagmentTests", From 14cee69b60f73e531884c7a887de9e0607016bd9 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Thu, 28 May 2026 16:29:16 -0700 Subject: [PATCH 09/42] use callback for delete method. --- ...LNativeCredentialManagementDelegates.swift | 12 --- .../MSALNativeCredentialMethodsClient.swift | 96 +++++++++---------- .../CredentialManagementViewModel.swift | 33 +++---- 3 files changed, 60 insertions(+), 81 deletions(-) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementDelegates.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementDelegates.swift index bd3a50ed8f..e9c1d8ce04 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementDelegates.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementDelegates.swift @@ -63,15 +63,3 @@ import Foundation ) } -// MARK: - Delete Credential Method Delegate - -/// Protocol that defines the delegate methods for deleting a credential method. -@objc public protocol MSALCredentialMethodDeleteDelegate { - - /// Notifies the delegate that the delete operation completed successfully. - @MainActor func onCredentialMethodDeleteCompleted() - - /// Notifies the delegate that the delete operation resulted in an error. - /// - Parameter error: An error object indicating why the operation failed. - @MainActor func onCredentialMethodDeleteError(error: MSALNativeCredentialManagementError) -} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index 58a60aaa1f..8c1749f46f 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -225,70 +225,68 @@ public class MSALNativeCredentialMethodsClient: NSObject { /// Delete a credential method by its identifier. /// - /// - Parameters: - /// - credentialMethodId: The ID of the credential method to remove. - /// - delegate: Receives completion or error callback on the main thread. + /// - Parameter credentialMethod: The ID of the credential method to remove. + /// - Returns: A `Result` indicating success or containing an error. public func deleteCredentialMethod( - credentialMethodId: String, - delegate: MSALCredentialMethodDeleteDelegate - ) + credentialMethod: String + ) async -> Result { let correlationId = config.correlationId ?? UUID() - operationQueue.async - { [weak self] in - guard let self = self else { return } - - self.acquireToken(correlationId: correlationId) - { accessToken, error in - if let error = error + return await withCheckedContinuation + { continuation in + self.operationQueue.async + { [weak self] in + guard let self = self else { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Failed to acquire access token for deleting credential method.", - correlationId: correlationId, - underlyingError: error + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "Client was deallocated.", + correlationId: correlationId ) - DispatchQueue.main.async - { - delegate.onCredentialMethodDeleteError(error: credError) - } + continuation.resume(returning: .failure(error)) return } - guard accessToken != nil else - { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Token provider returned nil access token.", - correlationId: correlationId - ) - DispatchQueue.main.async + self.acquireToken(correlationId: correlationId) + { accessToken, tokenError in + if let tokenError = tokenError { - delegate.onCredentialMethodDeleteError(error: credError) + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Failed to acquire access token for deleting credential method.", + correlationId: correlationId, + underlyingError: tokenError + ) + continuation.resume(returning: .failure(credError)) + return } - return - } - // Mock: remove credential method from in-memory storage - if let index = self.mockCredentialMethods.firstIndex(where: { $0.id == credentialMethodId }) - { - self.mockCredentialMethods.remove(at: index) - DispatchQueue.main.async + guard accessToken != nil else { - delegate.onCredentialMethodDeleteCompleted() + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Token provider returned nil access token.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(credError)) + return } - } - else - { - let credError = MSALNativeCredentialManagementError( - type: .notFound, - message: "Credential method with id '\(credentialMethodId)' not found.", - correlationId: correlationId - ) - DispatchQueue.main.async + + // Mock: remove credential method from in-memory storage + if let index = self.mockCredentialMethods.firstIndex(where: { $0.id == credentialMethod }) + { + self.mockCredentialMethods.remove(at: index) + continuation.resume(returning: .success(())) + } + else { - delegate.onCredentialMethodDeleteError(error: credError) + let credError = MSALNativeCredentialManagementError( + type: .notFound, + message: "Credential method with id '\(credentialMethod)' not found.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(credError)) } } } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 9c80dba10d..425a3c6157 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -120,7 +120,18 @@ class CredentialManagementViewModel: ObservableObject { statusMessage = "Deleting credential method..." errorMessage = nil - credClient.deleteCredentialMethod(credentialMethodId: id, delegate: self) + Task { + let result = await credClient.deleteCredentialMethod(credentialMethod: id) + switch result { + case .success: + isLoading = false + statusMessage = "Credential method deleted." + listCredentialMethods() + case .failure(let error): + isLoading = false + errorMessage = "Delete failed: \(error.message ?? "Unknown error")" + } + } } func submitChallenge(code: String) { @@ -198,22 +209,4 @@ extension CredentialManagementViewModel: MSALCredentialMethodRegisterDelegate { } } -// MARK: - MSALCredentialMethodDeleteDelegate - -extension CredentialManagementViewModel: MSALCredentialMethodDeleteDelegate { - - nonisolated func onCredentialMethodDeleteCompleted() { - Task { @MainActor in - isLoading = false - statusMessage = "Credential method deleted." - listCredentialMethods() - } - } - - nonisolated func onCredentialMethodDeleteError(error: MSALNativeCredentialManagementError) { - Task { @MainActor in - isLoading = false - errorMessage = "Delete failed: \(error.message ?? "Unknown error")" - } - } -} +// MARK: - MSALCredentialMethodDeleteDelegate is no longer used — delete uses async/await From 9eeb2563b0aa1823679ba2e9a6e1e585c85f5ea3 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Thu, 28 May 2026 16:41:15 -0700 Subject: [PATCH 10/42] use callback for other methods as well. --- .../MSALCredentialMethodChallengeState.swift | 128 ----------- ...LNativeCredentialManagementDelegates.swift | 65 ------ .../MSALNativeCredentialMethodsClient.swift | 214 +++++++----------- .../ContentView.swift | 45 ---- .../CredentialManagementViewModel.swift | 99 +++----- 5 files changed, 109 insertions(+), 442 deletions(-) delete mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethodChallengeState.swift delete mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementDelegates.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethodChallengeState.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethodChallengeState.swift deleted file mode 100644 index b1d1797db5..0000000000 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethodChallengeState.swift +++ /dev/null @@ -1,128 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. -// All rights reserved. -// -// This code is licensed under the MIT License. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -/// The channel type through which a challenge was sent. -@objc public enum MSALCredentialMethodChallengeChannel: Int { - /// Challenge sent via email. - case email = 0 - /// Challenge sent via SMS/phone. - case phone = 1 - /// Challenge requires authenticator app. - case authenticatorApp = 2 - /// Unknown or unspecified channel. - case unknown = 99 -} - -/// Represents the state of a credential method registration that requires challenge verification. -/// -/// When registering a new credential method, the server may require the user to verify ownership -/// (e.g., enter an OTP sent to the new email/phone). This state object provides the context -/// and methods to complete or resend the challenge. -@objcMembers -public class MSALCredentialMethodChallengeState: NSObject { - - /// The channel through which the challenge was sent. - public let challengeChannel: MSALCredentialMethodChallengeChannel - - /// A display hint for the challenge target (e.g., masked email or phone number). - public let sentTo: String? - - /// The length of the expected code, if applicable. - public let codeLength: Int - - internal let continuationToken: String - internal weak var client: MSALNativeCredentialMethodsClient? - - internal init( - challengeChannel: MSALCredentialMethodChallengeChannel, - sentTo: String?, - codeLength: Int, - continuationToken: String, - client: MSALNativeCredentialMethodsClient? - ) - { - self.challengeChannel = challengeChannel - self.sentTo = sentTo - self.codeLength = codeLength - self.continuationToken = continuationToken - self.client = client - super.init() - } - - /// Submit the challenge code to complete credential method registration. - /// - /// - Parameters: - /// - code: The verification code entered by the user. - /// - delegate: Receives the registration result or error. - public func submitChallenge( - code: String, - delegate: MSALCredentialMethodRegisterDelegate - ) - { - guard let client = client else - { - let error = MSALNativeCredentialManagementError( - type: .sessionExpired, - message: "Client reference has been released. Please restart the registration flow." - ) - DispatchQueue.main.async - { - delegate.onCredentialMethodRegistrationError(error: error) - } - return - } - client.submitRegistrationChallenge( - code: code, - continuationToken: continuationToken, - delegate: delegate - ) - } - - /// Request the server to resend the challenge code. - /// - /// - Parameter delegate: Receives a new challenge state or error. - public func resendChallenge( - delegate: MSALCredentialMethodRegisterDelegate - ) - { - guard let client = client else - { - let error = MSALNativeCredentialManagementError( - type: .sessionExpired, - message: "Client reference has been released. Please restart the registration flow." - ) - DispatchQueue.main.async - { - delegate.onCredentialMethodRegistrationError(error: error) - } - return - } - client.resendRegistrationChallenge( - continuationToken: continuationToken, - delegate: delegate - ) - } -} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementDelegates.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementDelegates.swift deleted file mode 100644 index e9c1d8ce04..0000000000 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementDelegates.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. -// All rights reserved. -// -// This code is licensed under the MIT License. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -// MARK: - List Credential Methods Delegate - -/// Protocol that defines the delegate methods for listing credential methods. -@objc public protocol MSALCredentialMethodsListDelegate { - - /// Notifies the delegate that the list operation completed successfully. - /// - Parameter methods: The array of credential methods registered for the user. - @MainActor func onCredentialMethodsListCompleted(methods: [MSALCredentialMethod]) - - /// Notifies the delegate that the list operation resulted in an error. - /// - Parameter error: An error object indicating why the operation failed. - @MainActor func onCredentialMethodsListError(error: MSALNativeCredentialManagementError) -} - -// MARK: - Register Credential Method Delegate - -/// Protocol that defines the delegate methods for registering a credential method. -@objc public protocol MSALCredentialMethodRegisterDelegate { - - /// Notifies the delegate that the registration completed successfully. - /// - Parameter method: The newly registered credential method. - @MainActor func onCredentialMethodRegistrationCompleted(method: MSALCredentialMethod) - - /// Notifies the delegate that the registration resulted in an error. - /// - Parameter error: An error object indicating why the operation failed. - @MainActor func onCredentialMethodRegistrationError(error: MSALNativeCredentialManagementError) - - /// Notifies the delegate that a challenge verification is required to complete registration. - /// - /// The user must provide a verification code (e.g., OTP sent to the new email/phone). - /// Call `state.submitChallenge(code:delegate:)` to continue. - /// - /// - Note: If this optional method is not implemented, `onCredentialMethodRegistrationError(error:)` will be called instead. - /// - Parameter state: The challenge state containing information about the sent challenge and methods to respond. - @MainActor @objc optional func onCredentialMethodChallengeRequired( - state: MSALCredentialMethodChallengeState - ) -} - diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index 8c1749f46f..175d823e8d 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -101,52 +101,53 @@ public class MSALNativeCredentialMethodsClient: NSObject { /// Retrieve the list of credential methods registered for the current user. /// - /// - Parameter delegate: Receives the result or error callback on the main thread. - public func listCredentialMethods(delegate: MSALCredentialMethodsListDelegate) + /// - Returns: A `Result` containing the array of credential methods or an error. + public func listCredentialMethods() async -> Result<[MSALCredentialMethod], MSALNativeCredentialManagementError> { let correlationId = config.correlationId ?? UUID() - operationQueue.async - { [weak self] in - guard let self = self else { return } - - self.acquireToken(correlationId: correlationId) - { accessToken, error in - if let error = error + return await withCheckedContinuation + { continuation in + self.operationQueue.async + { [weak self] in + guard let self = self else { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Failed to acquire access token for listing credential methods.", - correlationId: correlationId, - underlyingError: error + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "Client was deallocated.", + correlationId: correlationId ) - DispatchQueue.main.async - { - delegate.onCredentialMethodsListError(error: credError) - } + continuation.resume(returning: .failure(error)) return } - guard let accessToken = accessToken else - { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Token provider returned nil access token.", - correlationId: correlationId - ) - DispatchQueue.main.async + self.acquireToken(correlationId: correlationId) + { accessToken, tokenError in + if let tokenError = tokenError { - delegate.onCredentialMethodsListError(error: credError) + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Failed to acquire access token for listing credential methods.", + correlationId: correlationId, + underlyingError: tokenError + ) + continuation.resume(returning: .failure(credError)) + return } - return - } - // Mock: return current in-memory credential methods - _ = accessToken - let methods = self.mockCredentialMethods - DispatchQueue.main.async - { - delegate.onCredentialMethodsListCompleted(methods: methods) + guard accessToken != nil else + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Token provider returned nil access token.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(credError)) + return + } + + // Mock: return current in-memory credential methods + continuation.resume(returning: .success(self.mockCredentialMethods)) } } } @@ -154,68 +155,70 @@ public class MSALNativeCredentialMethodsClient: NSObject { // MARK: - Register Credential Method - /// Begin registration of a new credential method. + /// Register a new credential method. /// /// - Parameters: /// - type: The credential type to register (e.g., "email", "phone", "passkey"). /// - parameters: Type-specific parameters (e.g., email address, phone number). - /// - delegate: Receives state transitions (challenge required, completed, error) on the main thread. + /// - Returns: A `Result` containing the newly registered credential method or an error. public func registerCredentialMethod( type: String, - parameters: [String: Any]?, - delegate: MSALCredentialMethodRegisterDelegate - ) + parameters: [String: Any]? + ) async -> Result { let correlationId = config.correlationId ?? UUID() - operationQueue.async - { [weak self] in - guard let self = self else { return } - - self.acquireToken(correlationId: correlationId) - { accessToken, error in - if let error = error + return await withCheckedContinuation + { continuation in + self.operationQueue.async + { [weak self] in + guard let self = self else { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Failed to acquire access token for registering credential method.", - correlationId: correlationId, - underlyingError: error + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "Client was deallocated.", + correlationId: correlationId ) - DispatchQueue.main.async - { - delegate.onCredentialMethodRegistrationError(error: credError) - } + continuation.resume(returning: .failure(error)) return } - guard accessToken != nil else - { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Token provider returned nil access token.", - correlationId: correlationId - ) - DispatchQueue.main.async + self.acquireToken(correlationId: correlationId) + { accessToken, tokenError in + if let tokenError = tokenError { - delegate.onCredentialMethodRegistrationError(error: credError) + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Failed to acquire access token for registering credential method.", + correlationId: correlationId, + underlyingError: tokenError + ) + continuation.resume(returning: .failure(credError)) + return } - return - } - // Mock: add new credential method to in-memory storage - let newMethod = MSALCredentialMethod( - id: "\(type)-\(UUID().uuidString.prefix(8))", - credentialType: type, - displayName: (parameters?["value"] as? String) ?? type, - isDefault: false, - createdAt: Date(), - metadata: nil - ) - self.mockCredentialMethods.append(newMethod) - DispatchQueue.main.async - { - delegate.onCredentialMethodRegistrationCompleted(method: newMethod) + guard accessToken != nil else + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Token provider returned nil access token.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(credError)) + return + } + + // Mock: add new credential method to in-memory storage + let newMethod = MSALCredentialMethod( + id: "\(type)-\(UUID().uuidString.prefix(8))", + credentialType: type, + displayName: (parameters?["value"] as? String) ?? type, + isDefault: false, + createdAt: Date(), + metadata: nil + ) + self.mockCredentialMethods.append(newMethod) + continuation.resume(returning: .success(newMethod)) } } } @@ -293,57 +296,6 @@ public class MSALNativeCredentialMethodsClient: NSObject { } } - // MARK: - Internal: Challenge Handling - - internal func submitRegistrationChallenge( - code: String, - continuationToken: String, - delegate: MSALCredentialMethodRegisterDelegate - ) - { - let correlationId = config.correlationId ?? UUID() - - operationQueue.async - { [weak self] in - guard self != nil else { return } - - // TODO: Implement network call to submit challenge verification - let credError = MSALNativeCredentialManagementError( - type: .generalError, - message: "Challenge submission not yet implemented.", - correlationId: correlationId - ) - DispatchQueue.main.async - { - delegate.onCredentialMethodRegistrationError(error: credError) - } - } - } - - internal func resendRegistrationChallenge( - continuationToken: String, - delegate: MSALCredentialMethodRegisterDelegate - ) - { - let correlationId = config.correlationId ?? UUID() - - operationQueue.async - { [weak self] in - guard self != nil else { return } - - // TODO: Implement network call to resend challenge - let credError = MSALNativeCredentialManagementError( - type: .generalError, - message: "Challenge resend not yet implemented.", - correlationId: correlationId - ) - DispatchQueue.main.async - { - delegate.onCredentialMethodRegistrationError(error: credError) - } - } - } - // MARK: - Private: Token Acquisition private func acquireToken( diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift index 7985045df1..f43d2c6bcb 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift @@ -16,7 +16,6 @@ struct ContentView: View { @State private var password = "" @State private var newCredentialType = "email" @State private var newCredentialValue = "" - @State private var challengeCode = "" var body: some View { NavigationStack { @@ -41,9 +40,6 @@ struct ContentView: View { } message: { Text(viewModel.errorMessage ?? "") } - .sheet(isPresented: $viewModel.showChallengeInput) { - challengeView - } } } @@ -192,47 +188,6 @@ struct ContentView: View { // MARK: - Challenge Verification View - private var challengeView: some View { - NavigationStack { - VStack(spacing: 20) { - Image(systemName: "lock.shield") - .font(.system(size: 50)) - .foregroundStyle(.orange) - - Text("Verification Required") - .font(.title2) - .bold() - - Text("A code was sent to \(viewModel.challengeHint). Enter it below to complete registration.") - .multilineTextAlignment(.center) - .foregroundStyle(.secondary) - - TextField("Verification Code", text: $challengeCode) - .textFieldStyle(.roundedBorder) - .keyboardType(.numberPad) - .multilineTextAlignment(.center) - .font(.title3) - - Button("Verify") { - viewModel.submitChallenge(code: challengeCode) - challengeCode = "" - } - .buttonStyle(.borderedProminent) - .disabled(challengeCode.isEmpty) - } - .padding() - .navigationTitle("Verify") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - viewModel.showChallengeInput = false - challengeCode = "" - } - } - } - } - } } #Preview { diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 425a3c6157..aec2078239 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -23,15 +23,10 @@ class CredentialManagementViewModel: ObservableObject { @Published var statusMessage: String = "" @Published var errorMessage: String? - // Challenge state - @Published var showChallengeInput = false - @Published var challengeHint: String = "" - // MARK: - Private Properties private var credClient: MSALNativeCredentialMethodsClient? private var tokenProvider: SampleTokenProvider? - private var pendingChallengeState: MSALCredentialMethodChallengeState? // MARK: - Initialization @@ -93,7 +88,18 @@ class CredentialManagementViewModel: ObservableObject { statusMessage = "Loading credential methods..." errorMessage = nil - credClient.listCredentialMethods(delegate: self) + Task { + let result = await credClient.listCredentialMethods() + switch result { + case .success(let methods): + isLoading = false + credentialMethods = methods + statusMessage = "Loaded \(methods.count) credential method(s)." + case .failure(let error): + isLoading = false + errorMessage = "List failed: \(error.message ?? "Unknown error")" + } + } } func registerCredentialMethod(type: String, value: String) { @@ -106,8 +112,19 @@ class CredentialManagementViewModel: ObservableObject { statusMessage = "Registering \(type)..." errorMessage = nil - let parameters: [String: Any] = ["value": value] - credClient.registerCredentialMethod(type: type, parameters: parameters, delegate: self) + Task { + let parameters: [String: Any] = ["value": value] + let result = await credClient.registerCredentialMethod(type: type, parameters: parameters) + switch result { + case .success(let method): + isLoading = false + statusMessage = "Registered \(method.credentialType) successfully." + listCredentialMethods() + case .failure(let error): + isLoading = false + errorMessage = "Registration failed: \(error.message ?? "Unknown error")" + } + } } func deleteCredentialMethod(id: String) { @@ -134,20 +151,6 @@ class CredentialManagementViewModel: ObservableObject { } } - func submitChallenge(code: String) { - guard let state = pendingChallengeState else { - errorMessage = "No pending challenge." - return - } - - isLoading = true - statusMessage = "Verifying code..." - showChallengeInput = false - - state.submitChallenge(code: code, delegate: self) - pendingChallengeState = nil - } - // MARK: - Sign Out func signOut() { @@ -159,54 +162,4 @@ class CredentialManagementViewModel: ObservableObject { } } -// MARK: - MSALCredentialMethodsListDelegate - -extension CredentialManagementViewModel: MSALCredentialMethodsListDelegate { - - nonisolated func onCredentialMethodsListCompleted(methods: [MSALCredentialMethod]) { - Task { @MainActor in - isLoading = false - credentialMethods = methods - statusMessage = "Loaded \(methods.count) credential method(s)." - } - } - - nonisolated func onCredentialMethodsListError(error: MSALNativeCredentialManagementError) { - Task { @MainActor in - isLoading = false - errorMessage = "List failed: \(error.message ?? "Unknown error")" - } - } -} - -// MARK: - MSALCredentialMethodRegisterDelegate - -extension CredentialManagementViewModel: MSALCredentialMethodRegisterDelegate { - - nonisolated func onCredentialMethodRegistrationCompleted(method: MSALCredentialMethod) { - Task { @MainActor in - isLoading = false - statusMessage = "Registered \(method.credentialType) successfully." - listCredentialMethods() - } - } - - nonisolated func onCredentialMethodRegistrationError(error: MSALNativeCredentialManagementError) { - Task { @MainActor in - isLoading = false - errorMessage = "Registration failed: \(error.message ?? "Unknown error")" - } - } - - nonisolated func onCredentialMethodChallengeRequired(state: MSALCredentialMethodChallengeState) { - Task { @MainActor in - isLoading = false - pendingChallengeState = state - challengeHint = state.sentTo ?? "your registered contact" - showChallengeInput = true - statusMessage = "Verification code sent to \(challengeHint)." - } - } -} - -// MARK: - MSALCredentialMethodDeleteDelegate is no longer used — delete uses async/await +// MARK: - All credential operations use async/await (no delegates needed) From 08f0b776005efd4c7e7fb6948f705ede8eb0a974 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Thu, 28 May 2026 16:48:28 -0700 Subject: [PATCH 11/42] add challendge back to register method. --- ...ALCredentialMethodRegistrationResult.swift | 120 ++++++++++++++++ .../MSALNativeCredentialManagementError.swift | 2 + .../MSALNativeCredentialMethodsClient.swift | 135 ++++++++++++++++-- .../ContentView.swift | 48 +++++++ .../CredentialManagementViewModel.swift | 41 +++++- 5 files changed, 333 insertions(+), 13 deletions(-) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethodRegistrationResult.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethodRegistrationResult.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethodRegistrationResult.swift new file mode 100644 index 0000000000..03ec8e3b91 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethodRegistrationResult.swift @@ -0,0 +1,120 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// The result of a credential method registration attempt. +/// +/// Registration may complete immediately or require a verification challenge (e.g., OOB code). +public enum MSALCredentialMethodRegistrationResult +{ + /// Registration completed successfully. + case completed(MSALCredentialMethod) + + /// A verification challenge is required to complete registration. + /// Use the provided `MSALCredentialMethodChallengeState` to submit the code or resend. + case challengeRequired(MSALCredentialMethodChallengeState) +} + +/// Represents the state of a pending challenge during credential registration. +/// +/// Use `submitChallenge(code:)` to verify or `resendChallenge()` to request a new code. +public class MSALCredentialMethodChallengeState +{ + /// The channel the code was sent to (e.g., email address or phone number hint). + public let sentTo: String? + + /// The channel type (e.g., "email", "phone"). + public let channelType: String? + + /// The number of digits in the expected code. + public let codeLength: Int? + + // MARK: - Internal + + private let continuationToken: String + private weak var client: MSALNativeCredentialMethodsClient? + private let correlationId: UUID + + internal init( + sentTo: String?, + channelType: String?, + codeLength: Int?, + continuationToken: String, + client: MSALNativeCredentialMethodsClient, + correlationId: UUID + ) + { + self.sentTo = sentTo + self.channelType = channelType + self.codeLength = codeLength + self.continuationToken = continuationToken + self.client = client + self.correlationId = correlationId + } + + /// Submit the verification code to complete registration. + /// + /// - Parameter code: The verification code received by the user. + /// - Returns: A `Result` containing the registered credential method or an error. + public func submitChallenge(code: String) async -> Result + { + guard let client = client else + { + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "Client was deallocated.", + correlationId: correlationId + ) + return .failure(error) + } + + return await client.submitRegistrationChallenge( + code: code, + continuationToken: continuationToken, + correlationId: correlationId + ) + } + + /// Request a new verification code. + /// + /// - Returns: A new `MSALCredentialMethodChallengeState` with updated delivery info, or an error. + public func resendChallenge() async -> Result + { + guard let client = client else + { + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "Client was deallocated.", + correlationId: correlationId + ) + return .failure(error) + } + + return await client.resendRegistrationChallenge( + continuationToken: continuationToken, + correlationId: correlationId + ) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementError.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementError.swift index da4158c746..349e46b4ad 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementError.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementError.swift @@ -47,6 +47,8 @@ public let MSALNativeCredentialManagementErrorDomain = "MSALNativeCredentialMana case sessionExpired = 7 /// The client configuration is invalid. case invalidConfiguration = 8 + /// The input provided is invalid. + case invalidInput = 9 } /// Error class for credential management operations. diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index 175d823e8d..df9b27ed49 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -160,11 +160,11 @@ public class MSALNativeCredentialMethodsClient: NSObject { /// - Parameters: /// - type: The credential type to register (e.g., "email", "phone", "passkey"). /// - parameters: Type-specific parameters (e.g., email address, phone number). - /// - Returns: A `Result` containing the newly registered credential method or an error. + /// - Returns: A `Result` containing the registration outcome (completed or challenge required) or an error. public func registerCredentialMethod( type: String, parameters: [String: Any]? - ) async -> Result + ) async -> Result { let correlationId = config.correlationId ?? UUID() @@ -208,18 +208,129 @@ public class MSALNativeCredentialMethodsClient: NSObject { return } - // Mock: add new credential method to in-memory storage - let newMethod = MSALCredentialMethod( - id: "\(type)-\(UUID().uuidString.prefix(8))", - credentialType: type, - displayName: (parameters?["value"] as? String) ?? type, - isDefault: false, - createdAt: Date(), - metadata: nil + // Mock: simulate challenge required for email/phone, immediate for passkey + if type == "passkey" || type == "fido2" + { + let newMethod = MSALCredentialMethod( + id: "\(type)-\(UUID().uuidString.prefix(8))", + credentialType: type, + displayName: (parameters?["value"] as? String) ?? type, + isDefault: false, + createdAt: Date(), + metadata: nil + ) + self.mockCredentialMethods.append(newMethod) + continuation.resume(returning: .success(.completed(newMethod))) + } + else + { + // Simulate challenge required + let challengeState = MSALCredentialMethodChallengeState( + sentTo: (parameters?["value"] as? String) ?? "***", + channelType: type, + codeLength: 6, + continuationToken: "mock-continuation-\(UUID().uuidString.prefix(8))", + client: self, + correlationId: correlationId + ) + // Store pending registration info for when challenge is submitted + self.pendingRegistrationType = type + self.pendingRegistrationParameters = parameters + continuation.resume(returning: .success(.challengeRequired(challengeState))) + } + } + } + } + } + + // MARK: - Internal: Challenge Handling + + /// Pending registration state for mock challenge flow. + private var pendingRegistrationType: String? + private var pendingRegistrationParameters: [String: Any]? + + internal func submitRegistrationChallenge( + code: String, + continuationToken: String, + correlationId: UUID + ) async -> Result + { + return await withCheckedContinuation + { continuation in + self.operationQueue.async + { [weak self] in + guard let self = self else + { + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "Client was deallocated.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(error)) + return + } + + // Mock: accept any non-empty code + guard !code.isEmpty else + { + let error = MSALNativeCredentialManagementError( + type: .invalidInput, + message: "Verification code cannot be empty.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(error)) + return + } + + let type = self.pendingRegistrationType ?? "unknown" + let params = self.pendingRegistrationParameters + + let newMethod = MSALCredentialMethod( + id: "\(type)-\(UUID().uuidString.prefix(8))", + credentialType: type, + displayName: (params?["value"] as? String) ?? type, + isDefault: false, + createdAt: Date(), + metadata: nil + ) + self.mockCredentialMethods.append(newMethod) + self.pendingRegistrationType = nil + self.pendingRegistrationParameters = nil + continuation.resume(returning: .success(newMethod)) + } + } + } + + internal func resendRegistrationChallenge( + continuationToken: String, + correlationId: UUID + ) async -> Result + { + return await withCheckedContinuation + { continuation in + self.operationQueue.async + { [weak self] in + guard let self = self else + { + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "Client was deallocated.", + correlationId: correlationId ) - self.mockCredentialMethods.append(newMethod) - continuation.resume(returning: .success(newMethod)) + continuation.resume(returning: .failure(error)) + return } + + // Mock: return a new challenge state + let newState = MSALCredentialMethodChallengeState( + sentTo: (self.pendingRegistrationParameters?["value"] as? String) ?? "***", + channelType: self.pendingRegistrationType, + codeLength: 6, + continuationToken: "mock-continuation-\(UUID().uuidString.prefix(8))", + client: self, + correlationId: correlationId + ) + continuation.resume(returning: .success(newState)) } } } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift index f43d2c6bcb..2eeb1c9df6 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift @@ -16,6 +16,7 @@ struct ContentView: View { @State private var password = "" @State private var newCredentialType = "email" @State private var newCredentialValue = "" + @State private var challengeCode = "" var body: some View { NavigationStack { @@ -40,6 +41,53 @@ struct ContentView: View { } message: { Text(viewModel.errorMessage ?? "") } + .sheet(isPresented: $viewModel.showChallengeInput) { + challengeView + } + } + } + + // MARK: - Challenge View + + private var challengeView: some View { + NavigationStack { + VStack(spacing: 20) { + Image(systemName: "lock.shield") + .font(.system(size: 50)) + .foregroundStyle(.orange) + + Text("Verification Required") + .font(.title2) + .bold() + + Text("A code was sent to \(viewModel.challengeHint). Enter it below to complete registration.") + .multilineTextAlignment(.center) + .foregroundStyle(.secondary) + + TextField("Verification Code", text: $challengeCode) + .textFieldStyle(.roundedBorder) + .keyboardType(.numberPad) + .multilineTextAlignment(.center) + .font(.title3) + + Button("Verify") { + viewModel.submitChallenge(code: challengeCode) + challengeCode = "" + } + .buttonStyle(.borderedProminent) + .disabled(challengeCode.isEmpty) + } + .padding() + .navigationTitle("Verify") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + viewModel.showChallengeInput = false + challengeCode = "" + } + } + } } } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index aec2078239..98261cc5a2 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -23,10 +23,15 @@ class CredentialManagementViewModel: ObservableObject { @Published var statusMessage: String = "" @Published var errorMessage: String? + // Challenge state + @Published var showChallengeInput = false + @Published var challengeHint: String = "" + // MARK: - Private Properties private var credClient: MSALNativeCredentialMethodsClient? private var tokenProvider: SampleTokenProvider? + private var pendingChallengeState: MSALCredentialMethodChallengeState? // MARK: - Initialization @@ -116,13 +121,47 @@ class CredentialManagementViewModel: ObservableObject { let parameters: [String: Any] = ["value": value] let result = await credClient.registerCredentialMethod(type: type, parameters: parameters) switch result { + case .success(let registrationResult): + switch registrationResult { + case .completed(let method): + isLoading = false + statusMessage = "Registered \(method.credentialType) successfully." + listCredentialMethods() + case .challengeRequired(let state): + isLoading = false + pendingChallengeState = state + challengeHint = state.sentTo ?? "your registered contact" + showChallengeInput = true + statusMessage = "Verification code sent to \(challengeHint)." + } + case .failure(let error): + isLoading = false + errorMessage = "Registration failed: \(error.message ?? "Unknown error")" + } + } + } + + func submitChallenge(code: String) { + guard let state = pendingChallengeState else { + errorMessage = "No pending challenge." + return + } + + isLoading = true + statusMessage = "Verifying code..." + showChallengeInput = false + + Task { + let result = await state.submitChallenge(code: code) + switch result { case .success(let method): isLoading = false + pendingChallengeState = nil statusMessage = "Registered \(method.credentialType) successfully." listCredentialMethods() case .failure(let error): isLoading = false - errorMessage = "Registration failed: \(error.message ?? "Unknown error")" + errorMessage = "Verification failed: \(error.message ?? "Unknown error")" } } } From 84ebb4656eb0645c45048181d69c623581cdf931 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Thu, 28 May 2026 17:10:47 -0700 Subject: [PATCH 12/42] add code to use OS API for passkey creation. --- .../Configuration.swift | 4 + .../ContentView.swift | 6 ++ .../CredentialManagementViewModel.swift | 76 +++++++++++++++++++ .../PasskeyAuthorizationDelegate.swift | 67 ++++++++++++++++ 4 files changed, 153 insertions(+) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/PasskeyAuthorizationDelegate.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift index 1ed82464af..85003925b6 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift @@ -15,4 +15,8 @@ enum Configuration { /// The tenant subdomain (e.g., "contoso" for contoso.ciamlogin.com). static let tenantSubdomain = "Enter_the_Tenant_Subdomain_Here" + + /// The relying party identifier for passkey registration (WebAuthn). + /// Must match the domain associated with your app (via apple-app-site-association). + static let relyingPartyIdentifier = "Enter_the_Relying_Party_Here" } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift index 2eeb1c9df6..fc704f7304 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift @@ -66,7 +66,9 @@ struct ContentView: View { TextField("Verification Code", text: $challengeCode) .textFieldStyle(.roundedBorder) + #if os(iOS) .keyboardType(.numberPad) + #endif .multilineTextAlignment(.center) .font(.title3) @@ -79,7 +81,9 @@ struct ContentView: View { } .padding() .navigationTitle("Verify") + #if os(iOS) .navigationBarTitleDisplayMode(.inline) + #endif .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { @@ -105,7 +109,9 @@ struct ContentView: View { TextField("Email", text: $email) .textFieldStyle(.roundedBorder) .textContentType(.emailAddress) + #if os(iOS) .autocapitalization(.none) + #endif SecureField("Password", text: $password) .textFieldStyle(.roundedBorder) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 98261cc5a2..776ccaf641 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -9,6 +9,7 @@ import Foundation import MSAL import MSALNativeCredManagment import SwiftUI +import AuthenticationServices /// Main view model that demonstrates the credential management SDK integration. @MainActor @@ -117,6 +118,11 @@ class CredentialManagementViewModel: ObservableObject { statusMessage = "Registering \(type)..." errorMessage = nil + if type == "passkey" || type == "fido2" { + registerPasskey() + return + } + Task { let parameters: [String: Any] = ["value": value] let result = await credClient.registerCredentialMethod(type: type, parameters: parameters) @@ -141,6 +147,76 @@ class CredentialManagementViewModel: ObservableObject { } } + // MARK: - Passkey Registration + + private func registerPasskey() { + let relyingPartyIdentifier = Configuration.relyingPartyIdentifier + + // Mock: generate a random challenge (in production, this comes from the server) + var challengeBytes = [UInt8](repeating: 0, count: 32) + _ = SecRandomCopyBytes(kSecRandomDefault, challengeBytes.count, &challengeBytes) + let challenge = Data(challengeBytes) + + // Mock: use a random user ID (in production, this comes from the server) + let userId = Data(UUID().uuidString.utf8) + + let provider = ASAuthorizationPlatformPublicKeyCredentialProvider( + relyingPartyIdentifier: relyingPartyIdentifier + ) + + let registrationRequest = provider.createCredentialRegistrationRequest( + challenge: challenge, + name: userName.isEmpty ? "user@example.com" : userName, + userID: userId + ) + + let authController = ASAuthorizationController(authorizationRequests: [registrationRequest]) + passkeyDelegate = PasskeyAuthorizationDelegate { [weak self] result in + Task { @MainActor in + guard let self = self else { return } + switch result { + case .success(let credential): + // Register the passkey in the credential management client + let credentialIdString = credential.credentialID.base64EncodedString() + let parameters: [String: Any] = [ + "value": "Passkey (\(String(credentialIdString.prefix(8)))...)", + "credentialId": credentialIdString + ] + guard let credClient = self.credClient else { return } + let registerResult = await credClient.registerCredentialMethod( + type: "passkey", + parameters: parameters + ) + switch registerResult { + case .success(let registrationResult): + switch registrationResult { + case .completed(let method): + self.isLoading = false + self.statusMessage = "Passkey registered successfully." + self.listCredentialMethods() + _ = method + case .challengeRequired: + self.isLoading = false + self.statusMessage = "Passkey registered (unexpected challenge)." + self.listCredentialMethods() + } + case .failure(let error): + self.isLoading = false + self.errorMessage = "Passkey registration failed: \(error.message ?? "Unknown")" + } + case .failure(let error): + self.isLoading = false + self.errorMessage = "Passkey creation failed: \(error.localizedDescription)" + } + } + } + authController.delegate = passkeyDelegate + authController.presentationContextProvider = passkeyDelegate + authController.performRequests() + } + + private var passkeyDelegate: PasskeyAuthorizationDelegate? + func submitChallenge(code: String) { guard let state = pendingChallengeState else { errorMessage = "No pending challenge." diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/PasskeyAuthorizationDelegate.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/PasskeyAuthorizationDelegate.swift new file mode 100644 index 0000000000..6002bc9850 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/PasskeyAuthorizationDelegate.swift @@ -0,0 +1,67 @@ +// +// PasskeyAuthorizationDelegate.swift +// MSALNativeCredManagmentSampleApp +// +// Created by Serhii Demchenko on 2026-05-28. +// + +import AuthenticationServices +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// Delegate that handles passkey (platform public key credential) authorization results. +class PasskeyAuthorizationDelegate: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { + + enum PasskeyResult { + case success(ASAuthorizationPlatformPublicKeyCredentialRegistration) + case failure(Error) + } + + private let completion: (PasskeyResult) -> Void + + init(completion: @escaping (PasskeyResult) -> Void) { + self.completion = completion + } + + // MARK: - ASAuthorizationControllerPresentationContextProviding + + func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + #if os(macOS) + return NSApplication.shared.keyWindow ?? NSWindow() + #else + return UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .flatMap { $0.windows } + .first(where: { $0.isKeyWindow }) ?? UIWindow() + #endif + } + + // MARK: - ASAuthorizationControllerDelegate + + func authorizationController( + controller: ASAuthorizationController, + didCompleteWithAuthorization authorization: ASAuthorization + ) { + guard let credential = authorization.credential + as? ASAuthorizationPlatformPublicKeyCredentialRegistration else { + let error = NSError( + domain: "PasskeyError", + code: -1, + userInfo: [NSLocalizedDescriptionKey: "Unexpected credential type returned."] + ) + completion(.failure(error)) + return + } + completion(.success(credential)) + } + + func authorizationController( + controller: ASAuthorizationController, + didCompleteWithError error: Error + ) { + completion(.failure(error)) + } +} From d3f33bcc41b07f3042d9909ae3efa51866044001 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Thu, 28 May 2026 18:03:17 -0700 Subject: [PATCH 13/42] use authenticator bundle id to test passkey. --- .../project.pbxproj | 4 ++-- .../Configuration.swift | 2 +- .../MSALNativeCredManagmentSampleApp.entitlements | 12 ++++++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj index 7a4edb8215..fc0ba30b11 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj @@ -707,7 +707,7 @@ "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 15.2; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.MSALNativeCredManagment.MSALNativeCredManagmentSampleApp; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.azureauthenticator; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; @@ -746,7 +746,7 @@ "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 15.2; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.MSALNativeCredManagment.MSALNativeCredManagmentSampleApp; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.azureauthenticator; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift index 85003925b6..6d6f610ef2 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift @@ -18,5 +18,5 @@ enum Configuration { /// The relying party identifier for passkey registration (WebAuthn). /// Must match the domain associated with your app (via apple-app-site-association). - static let relyingPartyIdentifier = "Enter_the_Relying_Party_Here" + static let relyingPartyIdentifier = "login.microsoft.com" } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleApp.entitlements b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleApp.entitlements index f2ef3ae026..a021bf3f46 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleApp.entitlements +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleApp.entitlements @@ -2,9 +2,13 @@ - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - + com.apple.developer.associated-domains + + webcredentials:login.microsoft.com + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + From a3c08b5379790da8b2fad4db0196e62a3baebda2 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Fri, 29 May 2026 12:25:12 -0700 Subject: [PATCH 14/42] use CP bundle ID on mac. --- .../MSALNativeCredManagment.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj index fc0ba30b11..56ee8c141e 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj @@ -708,6 +708,7 @@ MACOSX_DEPLOYMENT_TARGET = 15.2; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.azureauthenticator; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = com.microsoft.CompanyPortalMac; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; @@ -747,6 +748,7 @@ MACOSX_DEPLOYMENT_TARGET = 15.2; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.azureauthenticator; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = com.microsoft.CompanyPortalMac; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; From e37ac16db6c4a6a68f13a6e72101b1e6204d6ea9 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Fri, 29 May 2026 12:42:05 -0700 Subject: [PATCH 15/42] Split cred methods. --- .../MSALNativeCredentialMethodsClient.swift | 93 +++++++++++++++---- .../MSALCredentialMethod.swift | 32 ++++--- .../MSALPasskeyCredentialMethod.swift | 61 ++++++++++++ .../MSALPasswordCredentialMethod.swift | 46 +++++++++ .../MSALPhoneCredentialMethod.swift | 61 ++++++++++++ .../ContentView.swift | 3 +- 6 files changed, 261 insertions(+), 35 deletions(-) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/{ => credential_method}/MSALCredentialMethod.swift (65%) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index df9b27ed49..8f2dc411c6 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -68,29 +68,27 @@ public class MSALNativeCredentialMethodsClient: NSObject { // Seed with default credential methods for POC self.mockCredentialMethods = [ - MSALCredentialMethod( + MSALPasskeyCredentialMethod( id: "fido-001", - credentialType: "fido2", displayName: "Security Key (YubiKey 5)", isDefault: true, createdAt: Date(timeIntervalSinceNow: -86400 * 30), - metadata: ["aaguid": "2fc0579f-8113-47ea-b116-bb5a8db9202a"] + credentialID: "abc123base64", + authenticatorAttachment: "cross-platform", + aaguid: "2fc0579f-8113-47ea-b116-bb5a8db9202a" ), - MSALCredentialMethod( + MSALPhoneCredentialMethod( id: "phone-001", - credentialType: "phone", displayName: "+1 *** ***-4589", isDefault: false, createdAt: Date(timeIntervalSinceNow: -86400 * 60), - metadata: nil + phoneNumber: "+1 *** ***-4589", + phoneType: "mobile" ), - MSALCredentialMethod( + MSALPasswordCredentialMethod( id: "password-001", - credentialType: "password", - displayName: "Password", isDefault: false, - createdAt: Date(timeIntervalSinceNow: -86400 * 90), - metadata: nil + createdAt: Date(timeIntervalSinceNow: -86400 * 90) ) ] @@ -211,13 +209,13 @@ public class MSALNativeCredentialMethodsClient: NSObject { // Mock: simulate challenge required for email/phone, immediate for passkey if type == "passkey" || type == "fido2" { - let newMethod = MSALCredentialMethod( + let newMethod = MSALPasskeyCredentialMethod( id: "\(type)-\(UUID().uuidString.prefix(8))", - credentialType: type, - displayName: (parameters?["value"] as? String) ?? type, + displayName: (parameters?["value"] as? String) ?? "Passkey", isDefault: false, createdAt: Date(), - metadata: nil + credentialID: parameters?["credentialId"] as? String, + authenticatorAttachment: "platform" ) self.mockCredentialMethods.append(newMethod) continuation.resume(returning: .success(.completed(newMethod))) @@ -285,13 +283,11 @@ public class MSALNativeCredentialMethodsClient: NSObject { let type = self.pendingRegistrationType ?? "unknown" let params = self.pendingRegistrationParameters - let newMethod = MSALCredentialMethod( + let newMethod = Self.createCredentialMethod( + type: type, id: "\(type)-\(UUID().uuidString.prefix(8))", - credentialType: type, displayName: (params?["value"] as? String) ?? type, - isDefault: false, - createdAt: Date(), - metadata: nil + parameters: params ) self.mockCredentialMethods.append(newMethod) self.pendingRegistrationType = nil @@ -407,6 +403,63 @@ public class MSALNativeCredentialMethodsClient: NSObject { } } + // MARK: - Private: Credential Method Factory + + /// Creates the appropriate `MSALCredentialMethod` subclass based on the type string. + /// New credential types can be supported by adding a new case here (or via a registry pattern). + private static func createCredentialMethod( + type: String, + id: String, + displayName: String?, + parameters: [String: Any]? + ) -> MSALCredentialMethod + { + switch type + { + case "passkey", "fido2": + return MSALPasskeyCredentialMethod( + id: id, + displayName: displayName, + isDefault: false, + createdAt: Date(), + credentialID: parameters?["credentialId"] as? String, + authenticatorAttachment: "platform" + ) + case "phone": + return MSALPhoneCredentialMethod( + id: id, + displayName: displayName, + isDefault: false, + createdAt: Date(), + phoneNumber: parameters?["value"] as? String, + phoneType: "mobile" + ) + case "email": + return MSALPhoneCredentialMethod( + id: id, + displayName: displayName, + isDefault: false, + createdAt: Date(), + phoneNumber: parameters?["value"] as? String, + phoneType: "mobile" + ) + case "password": + return MSALPasswordCredentialMethod( + id: id, + isDefault: false, + createdAt: Date() + ) + default: + return MSALCredentialMethod( + id: id, + credentialType: type, + displayName: displayName, + isDefault: false, + createdAt: Date() + ) + } + } + // MARK: - Private: Token Acquisition private func acquireToken( diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethod.swift similarity index 65% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethod.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethod.swift index ed3539fb20..28d6877814 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethod.swift @@ -24,20 +24,23 @@ import Foundation -/// Represents a single credential method registered by the user. +/// Base class for all credential methods. /// -/// Instances of this class are returned by `MSALNativeCredentialMethodsClient` when -/// listing or registering credential methods. +/// Each credential type (passkey, phone, password, etc.) is represented +/// by a concrete subclass. New credential types can be added by subclassing +/// without modifying existing classes (Open/Closed Principle). +/// +/// Do not instantiate `MSALCredentialMethod` directly — use the appropriate subclass. @objcMembers -public class MSALCredentialMethod: NSObject { +open class MSALCredentialMethod: NSObject { /// Unique identifier of the credential method. public let id: String - /// The type of credential (e.g., "password", "email", "phone", "passkey"). + /// The type identifier string (e.g., "passkey", "phone", "password"). public let credentialType: String - /// Display-friendly name or hint (e.g., masked email "j***@contoso.com"). + /// Display-friendly name or hint (e.g., masked phone "+1 ***-***-1234"). public let displayName: String? /// Whether this is the default/primary method. @@ -46,16 +49,20 @@ public class MSALCredentialMethod: NSObject { /// Timestamp of when this method was registered. public let createdAt: Date? - /// Additional metadata associated with this credential method. - public let metadata: [String: String]? - - internal init( + /// Subclasses must override to provide type-specific initialization from server response. + /// + /// - Parameters: + /// - id: Unique identifier from the server. + /// - credentialType: The type string for this credential. + /// - displayName: A user-facing display name or hint. + /// - isDefault: Whether this is the user's default method. + /// - createdAt: The creation timestamp. + public init( id: String, credentialType: String, displayName: String?, isDefault: Bool, - createdAt: Date?, - metadata: [String: String]? + createdAt: Date? ) { self.id = id @@ -63,7 +70,6 @@ public class MSALCredentialMethod: NSObject { self.displayName = displayName self.isDefault = isDefault self.createdAt = createdAt - self.metadata = metadata super.init() } } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift new file mode 100644 index 0000000000..196869e9bf --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift @@ -0,0 +1,61 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents a passkey (FIDO2/WebAuthn) credential method. +@objcMembers +public class MSALPasskeyCredentialMethod: MSALCredentialMethod { + + /// The base64-encoded credential ID from WebAuthn registration. + public let credentialID: String? + + /// The authenticator attachment type (e.g., "platform", "cross-platform"). + public let authenticatorAttachment: String? + + /// The AAGUID of the authenticator that created this passkey. + public let aaguid: String? + + public init( + id: String, + displayName: String?, + isDefault: Bool, + createdAt: Date?, + credentialID: String?, + authenticatorAttachment: String? = "platform", + aaguid: String? = nil + ) + { + self.credentialID = credentialID + self.authenticatorAttachment = authenticatorAttachment + self.aaguid = aaguid + super.init( + id: id, + credentialType: "passkey", + displayName: displayName, + isDefault: isDefault, + createdAt: createdAt + ) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift new file mode 100644 index 0000000000..9936a7b384 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift @@ -0,0 +1,46 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents a password credential method. +@objcMembers +public class MSALPasswordCredentialMethod: MSALCredentialMethod { + + public init( + id: String, + displayName: String? = "Password", + isDefault: Bool, + createdAt: Date? + ) + { + super.init( + id: id, + credentialType: "password", + displayName: displayName, + isDefault: isDefault, + createdAt: createdAt + ) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift new file mode 100644 index 0000000000..7c0807de53 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift @@ -0,0 +1,61 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents a phone number credential method (SMS or voice call OTP). +@objcMembers +public class MSALPhoneCredentialMethod: MSALCredentialMethod { + + /// The masked phone number (e.g., "+1 ***-***-1234"). + public let phoneNumber: String? + + /// The phone type — "mobile" or "office". + public let phoneType: String? + + /// The delivery channel — "sms" or "voice". + public let smsSignInState: String? + + public init( + id: String, + displayName: String?, + isDefault: Bool, + createdAt: Date?, + phoneNumber: String?, + phoneType: String? = "mobile", + smsSignInState: String? = nil + ) + { + self.phoneNumber = phoneNumber + self.phoneType = phoneType + self.smsSignInState = smsSignInState + super.init( + id: id, + credentialType: "phone", + displayName: displayName, + isDefault: isDefault, + createdAt: createdAt + ) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift index fc704f7304..2c20fa7d04 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift @@ -14,7 +14,7 @@ struct ContentView: View { @State private var email = "" @State private var password = "" - @State private var newCredentialType = "email" + @State private var newCredentialType = "phone" @State private var newCredentialValue = "" @State private var challengeCode = "" @@ -179,7 +179,6 @@ struct ContentView: View { Section("Add New Method") { Picker("Type", selection: $newCredentialType) { - Text("Email").tag("email") Text("Phone").tag("phone") Text("Passkey").tag("passkey") } From cb3c8f75fe6a8da212ccf1fc0f357856bed878b5 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Fri, 29 May 2026 12:50:30 -0700 Subject: [PATCH 16/42] update code to create exact cred class. --- ...ALCredentialMethodRegistrationResult.swift | 4 +- .../MSALNativeCredentialMethodsClient.swift | 76 ++++++++----------- .../MSALCredentialMethod.swift | 40 ++++++++-- .../ContentView.swift | 1 + .../CredentialManagementViewModel.swift | 43 ++++++++--- 5 files changed, 102 insertions(+), 62 deletions(-) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethodRegistrationResult.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethodRegistrationResult.swift index 03ec8e3b91..a53cff380f 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethodRegistrationResult.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethodRegistrationResult.swift @@ -30,7 +30,7 @@ import Foundation public enum MSALCredentialMethodRegistrationResult { /// Registration completed successfully. - case completed(MSALCredentialMethod) + case completed(any MSALCredentialMethodProtocol) /// A verification challenge is required to complete registration. /// Use the provided `MSALCredentialMethodChallengeState` to submit the code or resend. @@ -78,7 +78,7 @@ public class MSALCredentialMethodChallengeState /// /// - Parameter code: The verification code received by the user. /// - Returns: A `Result` containing the registered credential method or an error. - public func submitChallenge(code: String) async -> Result + public func submitChallenge(code: String) async -> Result { guard let client = client else { diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index 8f2dc411c6..05dca050ed 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -45,7 +45,7 @@ public class MSALNativeCredentialMethodsClient: NSObject { private let operationQueue: DispatchQueue // Mock storage simulating server-side credential methods - private var mockCredentialMethods: [MSALCredentialMethod] + private var mockCredentialMethods: [any MSALCredentialMethodProtocol] /// Initialize the credential methods client. /// @@ -100,7 +100,7 @@ public class MSALNativeCredentialMethodsClient: NSObject { /// Retrieve the list of credential methods registered for the current user. /// /// - Returns: A `Result` containing the array of credential methods or an error. - public func listCredentialMethods() async -> Result<[MSALCredentialMethod], MSALNativeCredentialManagementError> + public func listCredentialMethods() async -> Result<[any MSALCredentialMethodProtocol], MSALNativeCredentialManagementError> { let correlationId = config.correlationId ?? UUID() @@ -155,13 +155,11 @@ public class MSALNativeCredentialMethodsClient: NSObject { /// Register a new credential method. /// - /// - Parameters: - /// - type: The credential type to register (e.g., "email", "phone", "passkey"). - /// - parameters: Type-specific parameters (e.g., email address, phone number). + /// - Parameter credentialMethod: The credential method instance to register. + /// Pass a concrete subclass such as `MSALPasskeyCredentialMethod` or `MSALPhoneCredentialMethod`. /// - Returns: A `Result` containing the registration outcome (completed or challenge required) or an error. public func registerCredentialMethod( - type: String, - parameters: [String: Any]? + _ credentialMethod: any MSALCredentialMethodProtocol ) async -> Result { let correlationId = config.correlationId ?? UUID() @@ -206,34 +204,26 @@ public class MSALNativeCredentialMethodsClient: NSObject { return } - // Mock: simulate challenge required for email/phone, immediate for passkey - if type == "passkey" || type == "fido2" + // Mock: simulate challenge required for phone, immediate for passkey/password + let type = credentialMethod.credentialType + if type == "passkey" || type == "password" { - let newMethod = MSALPasskeyCredentialMethod( - id: "\(type)-\(UUID().uuidString.prefix(8))", - displayName: (parameters?["value"] as? String) ?? "Passkey", - isDefault: false, - createdAt: Date(), - credentialID: parameters?["credentialId"] as? String, - authenticatorAttachment: "platform" - ) - self.mockCredentialMethods.append(newMethod) - continuation.resume(returning: .success(.completed(newMethod))) + self.mockCredentialMethods.append(credentialMethod) + continuation.resume(returning: .success(.completed(credentialMethod))) } else { - // Simulate challenge required + // Simulate challenge required for phone + let sentTo = credentialMethod.displayName ?? "***" let challengeState = MSALCredentialMethodChallengeState( - sentTo: (parameters?["value"] as? String) ?? "***", + sentTo: sentTo, channelType: type, codeLength: 6, continuationToken: "mock-continuation-\(UUID().uuidString.prefix(8))", client: self, correlationId: correlationId ) - // Store pending registration info for when challenge is submitted - self.pendingRegistrationType = type - self.pendingRegistrationParameters = parameters + self.pendingRegistrationCredential = credentialMethod continuation.resume(returning: .success(.challengeRequired(challengeState))) } } @@ -244,14 +234,13 @@ public class MSALNativeCredentialMethodsClient: NSObject { // MARK: - Internal: Challenge Handling /// Pending registration state for mock challenge flow. - private var pendingRegistrationType: String? - private var pendingRegistrationParameters: [String: Any]? + private var pendingRegistrationCredential: (any MSALCredentialMethodProtocol)? internal func submitRegistrationChallenge( code: String, continuationToken: String, correlationId: UUID - ) async -> Result + ) async -> Result { return await withCheckedContinuation { continuation in @@ -280,19 +269,20 @@ public class MSALNativeCredentialMethodsClient: NSObject { return } - let type = self.pendingRegistrationType ?? "unknown" - let params = self.pendingRegistrationParameters + guard let credential = self.pendingRegistrationCredential else + { + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "No pending registration found.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(error)) + return + } - let newMethod = Self.createCredentialMethod( - type: type, - id: "\(type)-\(UUID().uuidString.prefix(8))", - displayName: (params?["value"] as? String) ?? type, - parameters: params - ) - self.mockCredentialMethods.append(newMethod) - self.pendingRegistrationType = nil - self.pendingRegistrationParameters = nil - continuation.resume(returning: .success(newMethod)) + self.mockCredentialMethods.append(credential) + self.pendingRegistrationCredential = nil + continuation.resume(returning: .success(credential)) } } } @@ -319,8 +309,8 @@ public class MSALNativeCredentialMethodsClient: NSObject { // Mock: return a new challenge state let newState = MSALCredentialMethodChallengeState( - sentTo: (self.pendingRegistrationParameters?["value"] as? String) ?? "***", - channelType: self.pendingRegistrationType, + sentTo: self.pendingRegistrationCredential?.displayName ?? "***", + channelType: self.pendingRegistrationCredential?.credentialType, codeLength: 6, continuationToken: "mock-continuation-\(UUID().uuidString.prefix(8))", client: self, @@ -412,11 +402,11 @@ public class MSALNativeCredentialMethodsClient: NSObject { id: String, displayName: String?, parameters: [String: Any]? - ) -> MSALCredentialMethod + ) -> any MSALCredentialMethodProtocol { switch type { - case "passkey", "fido2": + case "passkey": return MSALPasskeyCredentialMethod( id: id, displayName: displayName, diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethod.swift index 28d6877814..9a5bbf9973 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethod.swift @@ -24,15 +24,43 @@ import Foundation -/// Base class for all credential methods. +// MARK: - Protocol + +/// Protocol defining the contract for all credential method types. +/// +/// All credential method classes must conform to this protocol. +/// Use this protocol when you need to work with credential methods generically. +public protocol MSALCredentialMethodProtocol: AnyObject { + + /// Unique identifier of the credential method. + var id: String { get } + + /// The type identifier string (e.g., "passkey", "phone", "password"). + var credentialType: String { get } + + /// Display-friendly name or hint (e.g., masked phone "+1 ***-***-1234"). + var displayName: String? { get } + + /// Whether this is the default/primary method. + var isDefault: Bool { get } + + /// Timestamp of when this method was registered. + var createdAt: Date? { get } +} + +// MARK: - Abstract Base Class + +/// Abstract base class for all credential methods. /// /// Each credential type (passkey, phone, password, etc.) is represented /// by a concrete subclass. New credential types can be added by subclassing /// without modifying existing classes (Open/Closed Principle). /// -/// Do not instantiate `MSALCredentialMethod` directly — use the appropriate subclass. +/// **Do not instantiate `MSALCredentialMethod` directly** — use a concrete subclass +/// such as `MSALPasskeyCredentialMethod`, `MSALPhoneCredentialMethod`, or +/// `MSALPasswordCredentialMethod`. @objcMembers -open class MSALCredentialMethod: NSObject { +open class MSALCredentialMethod: NSObject, MSALCredentialMethodProtocol { /// Unique identifier of the credential method. public let id: String @@ -49,7 +77,9 @@ open class MSALCredentialMethod: NSObject { /// Timestamp of when this method was registered. public let createdAt: Date? - /// Subclasses must override to provide type-specific initialization from server response. + /// Internal initializer — prevents external consumers from creating + /// `MSALCredentialMethod` directly. Only subclasses within this module + /// (or subclasses in consuming code) can call this via `super.init(...)`. /// /// - Parameters: /// - id: Unique identifier from the server. @@ -57,7 +87,7 @@ open class MSALCredentialMethod: NSObject { /// - displayName: A user-facing display name or hint. /// - isDefault: Whether this is the user's default method. /// - createdAt: The creation timestamp. - public init( + internal init( id: String, credentialType: String, displayName: String?, diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift index 2c20fa7d04..a78f51d469 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift @@ -181,6 +181,7 @@ struct ContentView: View { Picker("Type", selection: $newCredentialType) { Text("Phone").tag("phone") Text("Passkey").tag("passkey") + Text("Password").tag("password") } TextField("Value (email or phone)", text: $newCredentialValue) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 776ccaf641..a32f0756b1 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -99,7 +99,7 @@ class CredentialManagementViewModel: ObservableObject { switch result { case .success(let methods): isLoading = false - credentialMethods = methods + credentialMethods = methods.compactMap { $0 as? MSALCredentialMethod } statusMessage = "Loaded \(methods.count) credential method(s)." case .failure(let error): isLoading = false @@ -118,14 +118,32 @@ class CredentialManagementViewModel: ObservableObject { statusMessage = "Registering \(type)..." errorMessage = nil - if type == "passkey" || type == "fido2" { + if type == "passkey" { registerPasskey() return } + // Create the appropriate credential method instance + let credentialMethod: any MSALCredentialMethodProtocol + switch type { + case "phone": + credentialMethod = MSALPhoneCredentialMethod( + id: "phone-\(UUID().uuidString.prefix(8))", + displayName: value, + isDefault: false, + createdAt: Date(), + phoneNumber: value + ) + default: + credentialMethod = MSALPasswordCredentialMethod( + id: "password-\(UUID().uuidString.prefix(8))", + isDefault: false, + createdAt: Date() + ) + } + Task { - let parameters: [String: Any] = ["value": value] - let result = await credClient.registerCredentialMethod(type: type, parameters: parameters) + let result = await credClient.registerCredentialMethod(credentialMethod) switch result { case .success(let registrationResult): switch registrationResult { @@ -178,15 +196,16 @@ class CredentialManagementViewModel: ObservableObject { case .success(let credential): // Register the passkey in the credential management client let credentialIdString = credential.credentialID.base64EncodedString() - let parameters: [String: Any] = [ - "value": "Passkey (\(String(credentialIdString.prefix(8)))...)", - "credentialId": credentialIdString - ] - guard let credClient = self.credClient else { return } - let registerResult = await credClient.registerCredentialMethod( - type: "passkey", - parameters: parameters + let passkeyMethod = MSALPasskeyCredentialMethod( + id: "passkey-\(UUID().uuidString.prefix(8))", + displayName: "Passkey (\(String(credentialIdString.prefix(8)))...)", + isDefault: false, + createdAt: Date(), + credentialID: credentialIdString, + authenticatorAttachment: "platform" ) + guard let credClient = self.credClient else { return } + let registerResult = await credClient.registerCredentialMethod(passkeyMethod) switch registerResult { case .success(let registrationResult): switch registrationResult { From d1861f4025765106147c362cf7b76577276d243d Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Fri, 29 May 2026 14:27:26 -0700 Subject: [PATCH 17/42] refactor code. --- .../MSALNativeCredentialMethodsClient.swift | 221 +++++++----------- .../ContentView.swift | 14 +- .../CredentialManagementViewModel.swift | 120 +++++----- 3 files changed, 151 insertions(+), 204 deletions(-) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index 05dca050ed..0c722c436f 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -28,7 +28,7 @@ import MSAL /// Client for managing credential methods of an authenticated CIAM user. /// /// This client provides APIs to list, register, and delete credential methods -/// (e.g., email, phone, passkey) for the currently signed-in user. +/// (e.g., phone, passkey, password) for the currently signed-in user. /// /// Example: /// ```swift @@ -36,16 +36,11 @@ import MSAL /// credConfig.requestInterceptor = sharedRequestInterceptor /// credConfig.tokenProvider = myTokenProvider /// let credClient = try MSALNativeCredentialMethodsClient(config: credConfig) -/// credClient.listCredentialMethods(delegate: self) /// ``` @objcMembers public class MSALNativeCredentialMethodsClient: NSObject { - private let config: MSALNativeCredentialManagementConfig - private let operationQueue: DispatchQueue - - // Mock storage simulating server-side credential methods - private var mockCredentialMethods: [any MSALCredentialMethodProtocol] + // MARK: - Public: Initialization /// Initialize the credential methods client. /// @@ -95,7 +90,7 @@ public class MSALNativeCredentialMethodsClient: NSObject { super.init() } - // MARK: - List Credential Methods + // MARK: - Public: List Credential Methods /// Retrieve the list of credential methods registered for the current user. /// @@ -151,7 +146,7 @@ public class MSALNativeCredentialMethodsClient: NSObject { } } - // MARK: - Register Credential Method + // MARK: - Public: Register Credential Method /// Register a new credential method. /// @@ -231,10 +226,79 @@ public class MSALNativeCredentialMethodsClient: NSObject { } } - // MARK: - Internal: Challenge Handling + // MARK: - Public: Delete Credential Method - /// Pending registration state for mock challenge flow. - private var pendingRegistrationCredential: (any MSALCredentialMethodProtocol)? + /// Delete a credential method by its identifier. + /// + /// - Parameter credentialMethod: The ID of the credential method to remove. + /// - Returns: A `Result` indicating success or containing an error. + public func deleteCredentialMethod( + credentialMethod: String + ) async -> Result + { + let correlationId = config.correlationId ?? UUID() + + return await withCheckedContinuation + { continuation in + self.operationQueue.async + { [weak self] in + guard let self = self else + { + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "Client was deallocated.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(error)) + return + } + + self.acquireToken(correlationId: correlationId) + { accessToken, tokenError in + if let tokenError = tokenError + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Failed to acquire access token for deleting credential method.", + correlationId: correlationId, + underlyingError: tokenError + ) + continuation.resume(returning: .failure(credError)) + return + } + + guard accessToken != nil else + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Token provider returned nil access token.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(credError)) + return + } + + // Mock: remove credential method from in-memory storage + if let index = self.mockCredentialMethods.firstIndex(where: { $0.id == credentialMethod }) + { + self.mockCredentialMethods.remove(at: index) + continuation.resume(returning: .success(())) + } + else + { + let credError = MSALNativeCredentialManagementError( + type: .notFound, + message: "Credential method with id '\(credentialMethod)' not found.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(credError)) + } + } + } + } + } + + // MARK: - Internal: Challenge Handling internal func submitRegistrationChallenge( code: String, @@ -321,134 +385,12 @@ public class MSALNativeCredentialMethodsClient: NSObject { } } - // MARK: - Delete Credential Method + // MARK: - Private: Properties - /// Delete a credential method by its identifier. - /// - /// - Parameter credentialMethod: The ID of the credential method to remove. - /// - Returns: A `Result` indicating success or containing an error. - public func deleteCredentialMethod( - credentialMethod: String - ) async -> Result - { - let correlationId = config.correlationId ?? UUID() - - return await withCheckedContinuation - { continuation in - self.operationQueue.async - { [weak self] in - guard let self = self else - { - let error = MSALNativeCredentialManagementError( - type: .generalError, - message: "Client was deallocated.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(error)) - return - } - - self.acquireToken(correlationId: correlationId) - { accessToken, tokenError in - if let tokenError = tokenError - { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Failed to acquire access token for deleting credential method.", - correlationId: correlationId, - underlyingError: tokenError - ) - continuation.resume(returning: .failure(credError)) - return - } - - guard accessToken != nil else - { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Token provider returned nil access token.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(credError)) - return - } - - // Mock: remove credential method from in-memory storage - if let index = self.mockCredentialMethods.firstIndex(where: { $0.id == credentialMethod }) - { - self.mockCredentialMethods.remove(at: index) - continuation.resume(returning: .success(())) - } - else - { - let credError = MSALNativeCredentialManagementError( - type: .notFound, - message: "Credential method with id '\(credentialMethod)' not found.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(credError)) - } - } - } - } - } - - // MARK: - Private: Credential Method Factory - - /// Creates the appropriate `MSALCredentialMethod` subclass based on the type string. - /// New credential types can be supported by adding a new case here (or via a registry pattern). - private static func createCredentialMethod( - type: String, - id: String, - displayName: String?, - parameters: [String: Any]? - ) -> any MSALCredentialMethodProtocol - { - switch type - { - case "passkey": - return MSALPasskeyCredentialMethod( - id: id, - displayName: displayName, - isDefault: false, - createdAt: Date(), - credentialID: parameters?["credentialId"] as? String, - authenticatorAttachment: "platform" - ) - case "phone": - return MSALPhoneCredentialMethod( - id: id, - displayName: displayName, - isDefault: false, - createdAt: Date(), - phoneNumber: parameters?["value"] as? String, - phoneType: "mobile" - ) - case "email": - return MSALPhoneCredentialMethod( - id: id, - displayName: displayName, - isDefault: false, - createdAt: Date(), - phoneNumber: parameters?["value"] as? String, - phoneType: "mobile" - ) - case "password": - return MSALPasswordCredentialMethod( - id: id, - isDefault: false, - createdAt: Date() - ) - default: - return MSALCredentialMethod( - id: id, - credentialType: type, - displayName: displayName, - isDefault: false, - createdAt: Date() - ) - } - } + private let config: MSALNativeCredentialManagementConfig + private let operationQueue: DispatchQueue + private var mockCredentialMethods: [any MSALCredentialMethodProtocol] + private var pendingRegistrationCredential: (any MSALCredentialMethodProtocol)? // MARK: - Private: Token Acquisition @@ -477,4 +419,3 @@ public class MSALNativeCredentialMethodsClient: NSObject { } } } - diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift index a78f51d469..3822316eb5 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift @@ -188,13 +188,17 @@ struct ContentView: View { .textFieldStyle(.roundedBorder) Button("Register") { - viewModel.registerCredentialMethod( - type: newCredentialType, - value: newCredentialValue - ) + switch newCredentialType { + case "passkey": + viewModel.registerPasskey() + case "password": + viewModel.registerPassword() + default: + viewModel.registerPhone(phoneNumber: newCredentialValue) + } newCredentialValue = "" } - .disabled(newCredentialValue.isEmpty) + .disabled(newCredentialType == "phone" && newCredentialValue.isEmpty) } } .refreshable { diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index a32f0756b1..3e1d9a4660 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -108,66 +108,59 @@ class CredentialManagementViewModel: ObservableObject { } } - func registerCredentialMethod(type: String, value: String) { + // MARK: - Register Phone + + func registerPhone(phoneNumber: String) { guard let credClient = credClient else { errorMessage = "Credential client not initialized." return } isLoading = true - statusMessage = "Registering \(type)..." + statusMessage = "Registering phone..." errorMessage = nil - if type == "passkey" { - registerPasskey() - return + let credentialMethod = MSALPhoneCredentialMethod( + id: "phone-\(UUID().uuidString.prefix(8))", + displayName: phoneNumber, + isDefault: false, + createdAt: Date(), + phoneNumber: phoneNumber + ) + + Task { + let result = await credClient.registerCredentialMethod(credentialMethod) + handleRegistrationResult(result) } + } + + // MARK: - Register Password - // Create the appropriate credential method instance - let credentialMethod: any MSALCredentialMethodProtocol - switch type { - case "phone": - credentialMethod = MSALPhoneCredentialMethod( - id: "phone-\(UUID().uuidString.prefix(8))", - displayName: value, - isDefault: false, - createdAt: Date(), - phoneNumber: value - ) - default: - credentialMethod = MSALPasswordCredentialMethod( - id: "password-\(UUID().uuidString.prefix(8))", - isDefault: false, - createdAt: Date() - ) + func registerPassword() { + guard let credClient = credClient else { + errorMessage = "Credential client not initialized." + return } + isLoading = true + statusMessage = "Registering password..." + errorMessage = nil + + let credentialMethod = MSALPasswordCredentialMethod( + id: "password-\(UUID().uuidString.prefix(8))", + isDefault: false, + createdAt: Date() + ) + Task { let result = await credClient.registerCredentialMethod(credentialMethod) - switch result { - case .success(let registrationResult): - switch registrationResult { - case .completed(let method): - isLoading = false - statusMessage = "Registered \(method.credentialType) successfully." - listCredentialMethods() - case .challengeRequired(let state): - isLoading = false - pendingChallengeState = state - challengeHint = state.sentTo ?? "your registered contact" - showChallengeInput = true - statusMessage = "Verification code sent to \(challengeHint)." - } - case .failure(let error): - isLoading = false - errorMessage = "Registration failed: \(error.message ?? "Unknown error")" - } + handleRegistrationResult(result) } } - // MARK: - Passkey Registration + // MARK: - Register Passkey - private func registerPasskey() { + func registerPasskey() { let relyingPartyIdentifier = Configuration.relyingPartyIdentifier // Mock: generate a random challenge (in production, this comes from the server) @@ -206,23 +199,7 @@ class CredentialManagementViewModel: ObservableObject { ) guard let credClient = self.credClient else { return } let registerResult = await credClient.registerCredentialMethod(passkeyMethod) - switch registerResult { - case .success(let registrationResult): - switch registrationResult { - case .completed(let method): - self.isLoading = false - self.statusMessage = "Passkey registered successfully." - self.listCredentialMethods() - _ = method - case .challengeRequired: - self.isLoading = false - self.statusMessage = "Passkey registered (unexpected challenge)." - self.listCredentialMethods() - } - case .failure(let error): - self.isLoading = false - self.errorMessage = "Passkey registration failed: \(error.message ?? "Unknown")" - } + self.handleRegistrationResult(registerResult) case .failure(let error): self.isLoading = false self.errorMessage = "Passkey creation failed: \(error.localizedDescription)" @@ -294,6 +271,31 @@ class CredentialManagementViewModel: ObservableObject { credentialMethods = [] statusMessage = "Signed out." } + + // MARK: - Private Helpers + + private func handleRegistrationResult( + _ result: Result + ) { + switch result { + case .success(let registrationResult): + switch registrationResult { + case .completed(let method): + isLoading = false + statusMessage = "Registered \(method.credentialType) successfully." + listCredentialMethods() + case .challengeRequired(let state): + isLoading = false + pendingChallengeState = state + challengeHint = state.sentTo ?? "your registered contact" + showChallengeInput = true + statusMessage = "Verification code sent to \(challengeHint)." + } + case .failure(let error): + isLoading = false + errorMessage = "Registration failed: \(error.message ?? "Unknown error")" + } + } } // MARK: - All credential operations use async/await (no delegates needed) From d879b946bb73c0bc1512c88dfbcdad594d68a552 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Fri, 29 May 2026 17:14:40 -0700 Subject: [PATCH 18/42] tc. --- .../src/public/MSALNativeCredentialMethodsClient.swift | 1 - .../credential_method/MSALPasswordCredentialMethod.swift | 3 +-- .../public/credential_method/MSALPhoneCredentialMethod.swift | 3 +-- .../CredentialManagementViewModel.swift | 1 - 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index 0c722c436f..a567c8f92f 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -74,7 +74,6 @@ public class MSALNativeCredentialMethodsClient: NSObject { ), MSALPhoneCredentialMethod( id: "phone-001", - displayName: "+1 *** ***-4589", isDefault: false, createdAt: Date(timeIntervalSinceNow: -86400 * 60), phoneNumber: "+1 *** ***-4589", diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift index 9936a7b384..6ed5cdd723 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift @@ -30,7 +30,6 @@ public class MSALPasswordCredentialMethod: MSALCredentialMethod { public init( id: String, - displayName: String? = "Password", isDefault: Bool, createdAt: Date? ) @@ -38,7 +37,7 @@ public class MSALPasswordCredentialMethod: MSALCredentialMethod { super.init( id: id, credentialType: "password", - displayName: displayName, + displayName: "Password", isDefault: isDefault, createdAt: createdAt ) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift index 7c0807de53..6553ec7179 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift @@ -39,7 +39,6 @@ public class MSALPhoneCredentialMethod: MSALCredentialMethod { public init( id: String, - displayName: String?, isDefault: Bool, createdAt: Date?, phoneNumber: String?, @@ -53,7 +52,7 @@ public class MSALPhoneCredentialMethod: MSALCredentialMethod { super.init( id: id, credentialType: "phone", - displayName: displayName, + displayName: phoneNumber, isDefault: isDefault, createdAt: createdAt ) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 3e1d9a4660..2c1214aff8 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -122,7 +122,6 @@ class CredentialManagementViewModel: ObservableObject { let credentialMethod = MSALPhoneCredentialMethod( id: "phone-\(UUID().uuidString.prefix(8))", - displayName: phoneNumber, isDefault: false, createdAt: Date(), phoneNumber: phoneNumber From 351c616b972d983ebef0aaded730c633cd30eed6 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Fri, 29 May 2026 19:05:59 -0700 Subject: [PATCH 19/42] move MSALCredentialMethod to internal folder. --- .../MSALCredentialMethod.swift | 32 ++---------- .../MSALCredentialMethodProtocol.swift | 49 +++++++++++++++++++ .../CredentialManagementViewModel.swift | 1 - .../MSALNativeCredManagment/Package.swift | 2 +- 4 files changed, 53 insertions(+), 31 deletions(-) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/{public/credential_method => internal}/MSALCredentialMethod.swift (74%) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethodProtocol.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift similarity index 74% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethod.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift index 9a5bbf9973..d7949632c9 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift @@ -24,33 +24,7 @@ import Foundation -// MARK: - Protocol - -/// Protocol defining the contract for all credential method types. -/// -/// All credential method classes must conform to this protocol. -/// Use this protocol when you need to work with credential methods generically. -public protocol MSALCredentialMethodProtocol: AnyObject { - - /// Unique identifier of the credential method. - var id: String { get } - - /// The type identifier string (e.g., "passkey", "phone", "password"). - var credentialType: String { get } - - /// Display-friendly name or hint (e.g., masked phone "+1 ***-***-1234"). - var displayName: String? { get } - - /// Whether this is the default/primary method. - var isDefault: Bool { get } - - /// Timestamp of when this method was registered. - var createdAt: Date? { get } -} - -// MARK: - Abstract Base Class - -/// Abstract base class for all credential methods. +/// Internal abstract base class for all credential methods. /// /// Each credential type (passkey, phone, password, etc.) is represented /// by a concrete subclass. New credential types can be added by subclassing @@ -60,7 +34,7 @@ public protocol MSALCredentialMethodProtocol: AnyObject { /// such as `MSALPasskeyCredentialMethod`, `MSALPhoneCredentialMethod`, or /// `MSALPasswordCredentialMethod`. @objcMembers -open class MSALCredentialMethod: NSObject, MSALCredentialMethodProtocol { +public class MSALCredentialMethod: NSObject, MSALCredentialMethodProtocol { /// Unique identifier of the credential method. public let id: String @@ -79,7 +53,7 @@ open class MSALCredentialMethod: NSObject, MSALCredentialMethodProtocol { /// Internal initializer — prevents external consumers from creating /// `MSALCredentialMethod` directly. Only subclasses within this module - /// (or subclasses in consuming code) can call this via `super.init(...)`. + /// can call this via `super.init(...)`. /// /// - Parameters: /// - id: Unique identifier from the server. diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethodProtocol.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethodProtocol.swift new file mode 100644 index 0000000000..dd5ada5c79 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethodProtocol.swift @@ -0,0 +1,49 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +// MARK: - Protocol + +/// Protocol defining the contract for all credential method types. +/// +/// All credential method classes must conform to this protocol. +/// Use this protocol when you need to work with credential methods generically. +public protocol MSALCredentialMethodProtocol: AnyObject { + + /// Unique identifier of the credential method. + var id: String { get } + + /// The type identifier string (e.g., "passkey", "phone", "password"). + var credentialType: String { get } + + /// Display-friendly name or hint (e.g., masked phone "+1 ***-***-1234"). + var displayName: String? { get } + + /// Whether this is the default/primary method. + var isDefault: Bool { get } + + /// Timestamp of when this method was registered. + var createdAt: Date? { get } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 2c1214aff8..7002dade69 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -140,7 +140,6 @@ class CredentialManagementViewModel: ObservableObject { errorMessage = "Credential client not initialized." return } - isLoading = true statusMessage = "Registering password..." errorMessage = nil diff --git a/lib/native/MSALNativeCredManagment/Package.swift b/lib/native/MSALNativeCredManagment/Package.swift index 0825285c73..dd92e28d31 100644 --- a/lib/native/MSALNativeCredManagment/Package.swift +++ b/lib/native/MSALNativeCredManagment/Package.swift @@ -25,7 +25,7 @@ let package = Package( dependencies: [ .product(name: "MSAL", package: "microsoft-authentication-library-for-objc") ], - path: "MSALNativeCredManagment/src/public" + path: "MSALNativeCredManagment/src" ), .testTarget( name: "MSALNativeCredManagmentTests", From a1a0cdf39e7955d69e8aa1aca4b3d3db56319e4b Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Fri, 29 May 2026 19:33:21 -0700 Subject: [PATCH 20/42] Removed isDefault property. --- .../src/internal/MSALCredentialMethod.swift | 6 ------ .../MSALNativeCredentialMethodsClient.swift | 3 --- .../MSALCredentialMethodProtocol.swift | 3 --- .../MSALPasskeyCredentialMethod.swift | 2 -- .../MSALPasswordCredentialMethod.swift | 2 -- .../MSALPhoneCredentialMethod.swift | 2 -- .../ContentView.swift | 16 +++------------- .../CredentialManagementViewModel.swift | 3 --- 8 files changed, 3 insertions(+), 34 deletions(-) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift index d7949632c9..7fb663cb1d 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift @@ -45,9 +45,6 @@ public class MSALCredentialMethod: NSObject, MSALCredentialMethodProtocol { /// Display-friendly name or hint (e.g., masked phone "+1 ***-***-1234"). public let displayName: String? - /// Whether this is the default/primary method. - public let isDefault: Bool - /// Timestamp of when this method was registered. public let createdAt: Date? @@ -59,20 +56,17 @@ public class MSALCredentialMethod: NSObject, MSALCredentialMethodProtocol { /// - id: Unique identifier from the server. /// - credentialType: The type string for this credential. /// - displayName: A user-facing display name or hint. - /// - isDefault: Whether this is the user's default method. /// - createdAt: The creation timestamp. internal init( id: String, credentialType: String, displayName: String?, - isDefault: Bool, createdAt: Date? ) { self.id = id self.credentialType = credentialType self.displayName = displayName - self.isDefault = isDefault self.createdAt = createdAt super.init() } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index a567c8f92f..d17a20fd7b 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -66,7 +66,6 @@ public class MSALNativeCredentialMethodsClient: NSObject { MSALPasskeyCredentialMethod( id: "fido-001", displayName: "Security Key (YubiKey 5)", - isDefault: true, createdAt: Date(timeIntervalSinceNow: -86400 * 30), credentialID: "abc123base64", authenticatorAttachment: "cross-platform", @@ -74,14 +73,12 @@ public class MSALNativeCredentialMethodsClient: NSObject { ), MSALPhoneCredentialMethod( id: "phone-001", - isDefault: false, createdAt: Date(timeIntervalSinceNow: -86400 * 60), phoneNumber: "+1 *** ***-4589", phoneType: "mobile" ), MSALPasswordCredentialMethod( id: "password-001", - isDefault: false, createdAt: Date(timeIntervalSinceNow: -86400 * 90) ) ] diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethodProtocol.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethodProtocol.swift index dd5ada5c79..f15ca5023b 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethodProtocol.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethodProtocol.swift @@ -41,9 +41,6 @@ public protocol MSALCredentialMethodProtocol: AnyObject { /// Display-friendly name or hint (e.g., masked phone "+1 ***-***-1234"). var displayName: String? { get } - /// Whether this is the default/primary method. - var isDefault: Bool { get } - /// Timestamp of when this method was registered. var createdAt: Date? { get } } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift index 196869e9bf..9661097fce 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift @@ -40,7 +40,6 @@ public class MSALPasskeyCredentialMethod: MSALCredentialMethod { public init( id: String, displayName: String?, - isDefault: Bool, createdAt: Date?, credentialID: String?, authenticatorAttachment: String? = "platform", @@ -54,7 +53,6 @@ public class MSALPasskeyCredentialMethod: MSALCredentialMethod { id: id, credentialType: "passkey", displayName: displayName, - isDefault: isDefault, createdAt: createdAt ) } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift index 6ed5cdd723..5b324b9005 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift @@ -30,7 +30,6 @@ public class MSALPasswordCredentialMethod: MSALCredentialMethod { public init( id: String, - isDefault: Bool, createdAt: Date? ) { @@ -38,7 +37,6 @@ public class MSALPasswordCredentialMethod: MSALCredentialMethod { id: id, credentialType: "password", displayName: "Password", - isDefault: isDefault, createdAt: createdAt ) } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift index 6553ec7179..a3de19a1d8 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift @@ -39,7 +39,6 @@ public class MSALPhoneCredentialMethod: MSALCredentialMethod { public init( id: String, - isDefault: Bool, createdAt: Date?, phoneNumber: String?, phoneType: String? = "mobile", @@ -53,7 +52,6 @@ public class MSALPhoneCredentialMethod: MSALCredentialMethod { id: id, credentialType: "phone", displayName: phoneNumber, - isDefault: isDefault, createdAt: createdAt ) } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift index 3822316eb5..33788df473 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift @@ -215,19 +215,9 @@ struct ContentView: View { private func credentialMethodRow(_ method: MSALCredentialMethod) -> some View { HStack { VStack(alignment: .leading, spacing: 4) { - HStack { - Text(method.credentialType.capitalized) - .font(.subheadline) - .bold() - if method.isDefault { - Text("Default") - .font(.caption2) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.blue.opacity(0.2)) - .cornerRadius(4) - } - } + Text(method.credentialType.capitalized) + .font(.subheadline) + .bold() if let displayName = method.displayName { Text(displayName) .font(.caption) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 7002dade69..4d9ac64ba0 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -122,7 +122,6 @@ class CredentialManagementViewModel: ObservableObject { let credentialMethod = MSALPhoneCredentialMethod( id: "phone-\(UUID().uuidString.prefix(8))", - isDefault: false, createdAt: Date(), phoneNumber: phoneNumber ) @@ -146,7 +145,6 @@ class CredentialManagementViewModel: ObservableObject { let credentialMethod = MSALPasswordCredentialMethod( id: "password-\(UUID().uuidString.prefix(8))", - isDefault: false, createdAt: Date() ) @@ -190,7 +188,6 @@ class CredentialManagementViewModel: ObservableObject { let passkeyMethod = MSALPasskeyCredentialMethod( id: "passkey-\(UUID().uuidString.prefix(8))", displayName: "Passkey (\(String(credentialIdString.prefix(8)))...)", - isDefault: false, createdAt: Date(), credentialID: credentialIdString, authenticatorAttachment: "platform" From 87f0358b255f64a773d3b5a0703b788833d2bea8 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Fri, 29 May 2026 19:42:40 -0700 Subject: [PATCH 21/42] hide some properties. --- .../src/internal/MSALCredentialMethod.swift | 8 ++++---- .../MSALNativeCredentialMethodsClient.swift | 7 +++++++ .../MSALPasskeyCredentialMethod.swift | 19 +++++++++++++++++++ .../MSALPasswordCredentialMethod.swift | 13 ++++++++++++- .../MSALPhoneCredentialMethod.swift | 18 ++++++++++++++++++ .../CredentialManagementViewModel.swift | 9 +-------- 6 files changed, 61 insertions(+), 13 deletions(-) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift index 7fb663cb1d..1239525d26 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift @@ -36,8 +36,8 @@ import Foundation @objcMembers public class MSALCredentialMethod: NSObject, MSALCredentialMethodProtocol { - /// Unique identifier of the credential method. - public let id: String + /// Unique identifier of the credential method (set by the server). + public internal(set) var id: String /// The type identifier string (e.g., "passkey", "phone", "password"). public let credentialType: String @@ -45,8 +45,8 @@ public class MSALCredentialMethod: NSObject, MSALCredentialMethodProtocol { /// Display-friendly name or hint (e.g., masked phone "+1 ***-***-1234"). public let displayName: String? - /// Timestamp of when this method was registered. - public let createdAt: Date? + /// Timestamp of when this method was registered (set by the server). + public internal(set) var createdAt: Date? /// Internal initializer — prevents external consumers from creating /// `MSALCredentialMethod` directly. Only subclasses within this module diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index d17a20fd7b..7f8efed05e 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -197,6 +197,13 @@ public class MSALNativeCredentialMethodsClient: NSObject { // Mock: simulate challenge required for phone, immediate for passkey/password let type = credentialMethod.credentialType + + // Assign server-generated ID + if let method = credentialMethod as? MSALCredentialMethod + { + method.id = "\(type)-\(UUID().uuidString.prefix(8))" + } + if type == "passkey" || type == "password" { self.mockCredentialMethods.append(credentialMethod) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift index 9661097fce..73be4fdd42 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift @@ -38,6 +38,25 @@ public class MSALPasskeyCredentialMethod: MSALCredentialMethod { public let aaguid: String? public init( + displayName: String?, + credentialID: String?, + authenticatorAttachment: String? = "platform", + aaguid: String? = nil + ) + { + self.credentialID = credentialID + self.authenticatorAttachment = authenticatorAttachment + self.aaguid = aaguid + super.init( + id: "", + credentialType: "passkey", + displayName: displayName, + createdAt: nil + ) + } + + /// Internal initializer used by the SDK when hydrating from server responses. + internal init( id: String, displayName: String?, createdAt: Date?, diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift index 5b324b9005..e9ecb180c5 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift @@ -28,7 +28,18 @@ import Foundation @objcMembers public class MSALPasswordCredentialMethod: MSALCredentialMethod { - public init( + public init() + { + super.init( + id: "", + credentialType: "password", + displayName: "Password", + createdAt: nil + ) + } + + /// Internal initializer used by the SDK when hydrating from server responses. + internal init( id: String, createdAt: Date? ) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift index a3de19a1d8..e587161965 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift @@ -38,6 +38,24 @@ public class MSALPhoneCredentialMethod: MSALCredentialMethod { public let smsSignInState: String? public init( + phoneNumber: String?, + phoneType: String? = "mobile", + smsSignInState: String? = nil + ) + { + self.phoneNumber = phoneNumber + self.phoneType = phoneType + self.smsSignInState = smsSignInState + super.init( + id: "", + credentialType: "phone", + displayName: phoneNumber, + createdAt: nil + ) + } + + /// Internal initializer used by the SDK when hydrating from server responses. + internal init( id: String, createdAt: Date?, phoneNumber: String?, diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 4d9ac64ba0..3296390b80 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -121,8 +121,6 @@ class CredentialManagementViewModel: ObservableObject { errorMessage = nil let credentialMethod = MSALPhoneCredentialMethod( - id: "phone-\(UUID().uuidString.prefix(8))", - createdAt: Date(), phoneNumber: phoneNumber ) @@ -143,10 +141,7 @@ class CredentialManagementViewModel: ObservableObject { statusMessage = "Registering password..." errorMessage = nil - let credentialMethod = MSALPasswordCredentialMethod( - id: "password-\(UUID().uuidString.prefix(8))", - createdAt: Date() - ) + let credentialMethod = MSALPasswordCredentialMethod() Task { let result = await credClient.registerCredentialMethod(credentialMethod) @@ -186,9 +181,7 @@ class CredentialManagementViewModel: ObservableObject { // Register the passkey in the credential management client let credentialIdString = credential.credentialID.base64EncodedString() let passkeyMethod = MSALPasskeyCredentialMethod( - id: "passkey-\(UUID().uuidString.prefix(8))", displayName: "Passkey (\(String(credentialIdString.prefix(8)))...)", - createdAt: Date(), credentialID: credentialIdString, authenticatorAttachment: "platform" ) From 7851857959e9a3e977fe17c2be0c690d3b73cf1b Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Fri, 29 May 2026 20:16:09 -0700 Subject: [PATCH 22/42] refactor credential type. --- .../src/internal/MSALCredentialMethod.swift | 8 +-- .../MSALNativeCredentialMethodsClient.swift | 11 ++-- .../MSALCredentialMethodProtocol.swift | 4 +- .../MSALCredentialType.swift | 51 +++++++++++++++++++ .../MSALPasskeyCredentialMethod.swift | 9 +++- .../MSALPasswordCredentialMethod.swift | 9 +++- .../MSALPhoneCredentialMethod.swift | 29 ++++------- .../ContentView.swift | 2 +- .../CredentialManagementViewModel.swift | 4 +- 9 files changed, 88 insertions(+), 39 deletions(-) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialType.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift index 1239525d26..5619ef3e6f 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift @@ -39,8 +39,8 @@ public class MSALCredentialMethod: NSObject, MSALCredentialMethodProtocol { /// Unique identifier of the credential method (set by the server). public internal(set) var id: String - /// The type identifier string (e.g., "passkey", "phone", "password"). - public let credentialType: String + /// The type identifier (e.g., `.passkey`, `.phone`, `.password`). + public let credentialType: MSALCredentialType /// Display-friendly name or hint (e.g., masked phone "+1 ***-***-1234"). public let displayName: String? @@ -54,12 +54,12 @@ public class MSALCredentialMethod: NSObject, MSALCredentialMethodProtocol { /// /// - Parameters: /// - id: Unique identifier from the server. - /// - credentialType: The type string for this credential. + /// - credentialType: The type for this credential. /// - displayName: A user-facing display name or hint. /// - createdAt: The creation timestamp. internal init( id: String, - credentialType: String, + credentialType: MSALCredentialType, displayName: String?, createdAt: Date? ) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index 7f8efed05e..18980e072b 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -74,8 +74,7 @@ public class MSALNativeCredentialMethodsClient: NSObject { MSALPhoneCredentialMethod( id: "phone-001", createdAt: Date(timeIntervalSinceNow: -86400 * 60), - phoneNumber: "+1 *** ***-4589", - phoneType: "mobile" + phoneNumber: "+1 *** ***-4589" ), MSALPasswordCredentialMethod( id: "password-001", @@ -201,10 +200,10 @@ public class MSALNativeCredentialMethodsClient: NSObject { // Assign server-generated ID if let method = credentialMethod as? MSALCredentialMethod { - method.id = "\(type)-\(UUID().uuidString.prefix(8))" + method.id = "\(type.rawValue)-\(UUID().uuidString.prefix(8))" } - if type == "passkey" || type == "password" + if type == .passkey || type == .password { self.mockCredentialMethods.append(credentialMethod) continuation.resume(returning: .success(.completed(credentialMethod))) @@ -215,7 +214,7 @@ public class MSALNativeCredentialMethodsClient: NSObject { let sentTo = credentialMethod.displayName ?? "***" let challengeState = MSALCredentialMethodChallengeState( sentTo: sentTo, - channelType: type, + channelType: type.rawValue, codeLength: 6, continuationToken: "mock-continuation-\(UUID().uuidString.prefix(8))", client: self, @@ -377,7 +376,7 @@ public class MSALNativeCredentialMethodsClient: NSObject { // Mock: return a new challenge state let newState = MSALCredentialMethodChallengeState( sentTo: self.pendingRegistrationCredential?.displayName ?? "***", - channelType: self.pendingRegistrationCredential?.credentialType, + channelType: self.pendingRegistrationCredential?.credentialType.rawValue, codeLength: 6, continuationToken: "mock-continuation-\(UUID().uuidString.prefix(8))", client: self, diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethodProtocol.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethodProtocol.swift index f15ca5023b..4388237899 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethodProtocol.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethodProtocol.swift @@ -35,8 +35,8 @@ public protocol MSALCredentialMethodProtocol: AnyObject { /// Unique identifier of the credential method. var id: String { get } - /// The type identifier string (e.g., "passkey", "phone", "password"). - var credentialType: String { get } + /// The type identifier (e.g., `.passkey`, `.phone`, `.password`). + var credentialType: MSALCredentialType { get } /// Display-friendly name or hint (e.g., masked phone "+1 ***-***-1234"). var displayName: String? { get } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialType.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialType.swift new file mode 100644 index 0000000000..43aff303d5 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialType.swift @@ -0,0 +1,51 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// A type-safe, extensible identifier for credential method types. +/// +/// New credential types are defined by adding a `static let` extension +/// in the corresponding credential class file — no central registry needed. +/// +/// Example (in a new credential file): +/// ```swift +/// extension MSALCredentialType { +/// public static let myNewType = MSALCredentialType("myNewType") +/// } +/// ``` +public struct MSALCredentialType: RawRepresentable, Hashable, Sendable { + + public let rawValue: String + + public init(rawValue: String) + { + self.rawValue = rawValue + } + + public init(_ rawValue: String) + { + self.rawValue = rawValue + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift index 73be4fdd42..09212a15ff 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift @@ -24,6 +24,11 @@ import Foundation +extension MSALCredentialType { + /// Passkey (FIDO2/WebAuthn) credential type. + public static let passkey = MSALCredentialType("passkey") +} + /// Represents a passkey (FIDO2/WebAuthn) credential method. @objcMembers public class MSALPasskeyCredentialMethod: MSALCredentialMethod { @@ -49,7 +54,7 @@ public class MSALPasskeyCredentialMethod: MSALCredentialMethod { self.aaguid = aaguid super.init( id: "", - credentialType: "passkey", + credentialType: .passkey, displayName: displayName, createdAt: nil ) @@ -70,7 +75,7 @@ public class MSALPasskeyCredentialMethod: MSALCredentialMethod { self.aaguid = aaguid super.init( id: id, - credentialType: "passkey", + credentialType: .passkey, displayName: displayName, createdAt: createdAt ) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift index e9ecb180c5..744012bb4b 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift @@ -24,6 +24,11 @@ import Foundation +extension MSALCredentialType { + /// Password credential type. + public static let password = MSALCredentialType("password") +} + /// Represents a password credential method. @objcMembers public class MSALPasswordCredentialMethod: MSALCredentialMethod { @@ -32,7 +37,7 @@ public class MSALPasswordCredentialMethod: MSALCredentialMethod { { super.init( id: "", - credentialType: "password", + credentialType: .password, displayName: "Password", createdAt: nil ) @@ -46,7 +51,7 @@ public class MSALPasswordCredentialMethod: MSALCredentialMethod { { super.init( id: id, - credentialType: "password", + credentialType: .password, displayName: "Password", createdAt: createdAt ) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift index e587161965..4ffabe04c7 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift @@ -24,6 +24,11 @@ import Foundation +extension MSALCredentialType { + /// Phone number credential type (SMS or voice call OTP). + public static let phone = MSALCredentialType("phone") +} + /// Represents a phone number credential method (SMS or voice call OTP). @objcMembers public class MSALPhoneCredentialMethod: MSALCredentialMethod { @@ -31,24 +36,12 @@ public class MSALPhoneCredentialMethod: MSALCredentialMethod { /// The masked phone number (e.g., "+1 ***-***-1234"). public let phoneNumber: String? - /// The phone type — "mobile" or "office". - public let phoneType: String? - - /// The delivery channel — "sms" or "voice". - public let smsSignInState: String? - - public init( - phoneNumber: String?, - phoneType: String? = "mobile", - smsSignInState: String? = nil - ) + public init(phoneNumber: String?) { self.phoneNumber = phoneNumber - self.phoneType = phoneType - self.smsSignInState = smsSignInState super.init( id: "", - credentialType: "phone", + credentialType: .phone, displayName: phoneNumber, createdAt: nil ) @@ -58,17 +51,13 @@ public class MSALPhoneCredentialMethod: MSALCredentialMethod { internal init( id: String, createdAt: Date?, - phoneNumber: String?, - phoneType: String? = "mobile", - smsSignInState: String? = nil + phoneNumber: String? ) { self.phoneNumber = phoneNumber - self.phoneType = phoneType - self.smsSignInState = smsSignInState super.init( id: id, - credentialType: "phone", + credentialType: .phone, displayName: phoneNumber, createdAt: createdAt ) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift index 33788df473..756815a84c 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift @@ -215,7 +215,7 @@ struct ContentView: View { private func credentialMethodRow(_ method: MSALCredentialMethod) -> some View { HStack { VStack(alignment: .leading, spacing: 4) { - Text(method.credentialType.capitalized) + Text(method.credentialType.rawValue.capitalized) .font(.subheadline) .bold() if let displayName = method.displayName { diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 3296390b80..5d8f18f73b 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -217,7 +217,7 @@ class CredentialManagementViewModel: ObservableObject { case .success(let method): isLoading = false pendingChallengeState = nil - statusMessage = "Registered \(method.credentialType) successfully." + statusMessage = "Registered \(method.credentialType.rawValue) successfully." listCredentialMethods() case .failure(let error): isLoading = false @@ -270,7 +270,7 @@ class CredentialManagementViewModel: ObservableObject { switch registrationResult { case .completed(let method): isLoading = false - statusMessage = "Registered \(method.credentialType) successfully." + statusMessage = "Registered \(method.credentialType.rawValue) successfully." listCredentialMethods() case .challengeRequired(let state): isLoading = false From 4b57d8b4c73dcb935f6fb9943f72e0bf47aeebce Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Fri, 29 May 2026 20:27:19 -0700 Subject: [PATCH 23/42] update display name. --- .../src/internal/MSALCredentialMethod.swift | 2 +- .../public/MSALNativeCredentialMethodsClient.swift | 3 ++- .../MSALPasswordCredentialMethod.swift | 4 ++-- .../ContentView.swift | 12 +++++++++++- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift index 5619ef3e6f..1c74e79aea 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift @@ -43,7 +43,7 @@ public class MSALCredentialMethod: NSObject, MSALCredentialMethodProtocol { public let credentialType: MSALCredentialType /// Display-friendly name or hint (e.g., masked phone "+1 ***-***-1234"). - public let displayName: String? + public internal(set) var displayName: String? /// Timestamp of when this method was registered (set by the server). public internal(set) var createdAt: Date? diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index 18980e072b..501a24d086 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -197,10 +197,11 @@ public class MSALNativeCredentialMethodsClient: NSObject { // Mock: simulate challenge required for phone, immediate for passkey/password let type = credentialMethod.credentialType - // Assign server-generated ID + // Assign server-generated ID and metadata if let method = credentialMethod as? MSALCredentialMethod { method.id = "\(type.rawValue)-\(UUID().uuidString.prefix(8))" + method.createdAt = Date() } if type == .passkey || type == .password diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift index 744012bb4b..a570cf1ac8 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift @@ -38,7 +38,7 @@ public class MSALPasswordCredentialMethod: MSALCredentialMethod { super.init( id: "", credentialType: .password, - displayName: "Password", + displayName: nil, createdAt: nil ) } @@ -52,7 +52,7 @@ public class MSALPasswordCredentialMethod: MSALCredentialMethod { super.init( id: id, credentialType: .password, - displayName: "Password", + displayName: nil, createdAt: createdAt ) } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift index 756815a84c..6a91300465 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift @@ -218,7 +218,11 @@ struct ContentView: View { Text(method.credentialType.rawValue.capitalized) .font(.subheadline) .bold() - if let displayName = method.displayName { + if method.credentialType == .password, let createdAt = method.createdAt { + Text("Last updated: \(relativeTimeString(from: createdAt))") + .font(.caption) + .foregroundStyle(.secondary) + } else if let displayName = method.displayName { Text(displayName) .font(.caption) .foregroundStyle(.secondary) @@ -234,6 +238,12 @@ struct ContentView: View { } } + private func relativeTimeString(from date: Date) -> String { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .full + return formatter.localizedString(for: date, relativeTo: Date()) + } + // MARK: - Challenge Verification View } From d7d6294ceff891c53fc0eabdfa2f5b261f649b2a Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Fri, 29 May 2026 20:37:32 -0700 Subject: [PATCH 24/42] move correlation id. --- ...MSALNativeCredentialManagementConfig.swift | 3 --- .../MSALNativeCredentialMethodsClient.swift | 19 +++++++++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift index 4798fca48d..24fdf4fc85 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift @@ -57,9 +57,6 @@ public class MSALNativeCredentialManagementConfig: NSObject { /// When nil, the client derives the endpoint from the tenant configuration. public var baseURL: URL? - /// The correlation ID to use for requests. If nil, a new one is generated per operation. - public var correlationId: UUID? - public override init() { super.init() diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index 501a24d086..a56b91d40b 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -89,10 +89,13 @@ public class MSALNativeCredentialMethodsClient: NSObject { /// Retrieve the list of credential methods registered for the current user. /// + /// - Parameter correlationId: Optional correlation ID for request tracing. A new UUID is generated if nil. /// - Returns: A `Result` containing the array of credential methods or an error. - public func listCredentialMethods() async -> Result<[any MSALCredentialMethodProtocol], MSALNativeCredentialManagementError> + public func listCredentialMethods( + correlationId: UUID? = nil + ) async -> Result<[any MSALCredentialMethodProtocol], MSALNativeCredentialManagementError> { - let correlationId = config.correlationId ?? UUID() + let correlationId = correlationId ?? UUID() return await withCheckedContinuation { continuation in @@ -147,12 +150,14 @@ public class MSALNativeCredentialMethodsClient: NSObject { /// /// - Parameter credentialMethod: The credential method instance to register. /// Pass a concrete subclass such as `MSALPasskeyCredentialMethod` or `MSALPhoneCredentialMethod`. + /// - Parameter correlationId: Optional correlation ID for request tracing. A new UUID is generated if nil. /// - Returns: A `Result` containing the registration outcome (completed or challenge required) or an error. public func registerCredentialMethod( - _ credentialMethod: any MSALCredentialMethodProtocol + _ credentialMethod: any MSALCredentialMethodProtocol, + correlationId: UUID? = nil ) async -> Result { - let correlationId = config.correlationId ?? UUID() + let correlationId = correlationId ?? UUID() return await withCheckedContinuation { continuation in @@ -234,12 +239,14 @@ public class MSALNativeCredentialMethodsClient: NSObject { /// Delete a credential method by its identifier. /// /// - Parameter credentialMethod: The ID of the credential method to remove. + /// - Parameter correlationId: Optional correlation ID for request tracing. A new UUID is generated if nil. /// - Returns: A `Result` indicating success or containing an error. public func deleteCredentialMethod( - credentialMethod: String + credentialMethod: String, + correlationId: UUID? = nil ) async -> Result { - let correlationId = config.correlationId ?? UUID() + let correlationId = correlationId ?? UUID() return await withCheckedContinuation { continuation in From 1d32508efbe37eb6d47e027eec3c91f0b7628fca Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Fri, 29 May 2026 20:41:02 -0700 Subject: [PATCH 25/42] refactor delete method. --- .../public/MSALNativeCredentialMethodsClient.swift | 11 ++++++----- .../ContentView.swift | 2 +- .../CredentialManagementViewModel.swift | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index a56b91d40b..c0d8f7349a 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -236,13 +236,13 @@ public class MSALNativeCredentialMethodsClient: NSObject { // MARK: - Public: Delete Credential Method - /// Delete a credential method by its identifier. + /// Delete a credential method. /// - /// - Parameter credentialMethod: The ID of the credential method to remove. + /// - Parameter credentialMethod: The credential method to remove. /// - Parameter correlationId: Optional correlation ID for request tracing. A new UUID is generated if nil. /// - Returns: A `Result` indicating success or containing an error. public func deleteCredentialMethod( - credentialMethod: String, + _ credentialMethod: any MSALCredentialMethodProtocol, correlationId: UUID? = nil ) async -> Result { @@ -289,7 +289,8 @@ public class MSALNativeCredentialMethodsClient: NSObject { } // Mock: remove credential method from in-memory storage - if let index = self.mockCredentialMethods.firstIndex(where: { $0.id == credentialMethod }) + let methodId = credentialMethod.id + if let index = self.mockCredentialMethods.firstIndex(where: { $0.id == methodId }) { self.mockCredentialMethods.remove(at: index) continuation.resume(returning: .success(())) @@ -298,7 +299,7 @@ public class MSALNativeCredentialMethodsClient: NSObject { { let credError = MSALNativeCredentialManagementError( type: .notFound, - message: "Credential method with id '\(credentialMethod)' not found.", + message: "Credential method with id '\(methodId)' not found.", correlationId: correlationId ) continuation.resume(returning: .failure(credError)) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift index 6a91300465..71b9ad9c2c 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift @@ -230,7 +230,7 @@ struct ContentView: View { } Spacer() Button(role: .destructive) { - viewModel.deleteCredentialMethod(id: method.id) + viewModel.deleteCredentialMethod(method) } label: { Image(systemName: "trash") } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 5d8f18f73b..6a91eccd78 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -226,7 +226,7 @@ class CredentialManagementViewModel: ObservableObject { } } - func deleteCredentialMethod(id: String) { + func deleteCredentialMethod(_ method: MSALCredentialMethod) { guard let credClient = credClient else { errorMessage = "Credential client not initialized." return @@ -237,7 +237,7 @@ class CredentialManagementViewModel: ObservableObject { errorMessage = nil Task { - let result = await credClient.deleteCredentialMethod(credentialMethod: id) + let result = await credClient.deleteCredentialMethod(method) switch result { case .success: isLoading = false From 03c586bc6278b132fe502faa9644f497ee0f81a3 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Fri, 29 May 2026 20:46:09 -0700 Subject: [PATCH 26/42] show phone number. --- .../MSALNativeCredManagmentSampleApp/ContentView.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift index 71b9ad9c2c..19a39de9fd 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift @@ -222,6 +222,11 @@ struct ContentView: View { Text("Last updated: \(relativeTimeString(from: createdAt))") .font(.caption) .foregroundStyle(.secondary) + } else if method.credentialType == .phone, + let phone = (method as? MSALPhoneCredentialMethod)?.phoneNumber { + Text(phone) + .font(.caption) + .foregroundStyle(.secondary) } else if let displayName = method.displayName { Text(displayName) .font(.caption) From c4f11945173d2746cc3ff88fc0d045696f5ec0f7 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Fri, 29 May 2026 20:50:30 -0700 Subject: [PATCH 27/42] show passkey name. --- .../MSALNativeCredManagmentSampleApp/ContentView.swift | 9 +++++++-- .../CredentialManagementViewModel.swift | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift index 19a39de9fd..97e57b9630 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift @@ -184,13 +184,13 @@ struct ContentView: View { Text("Password").tag("password") } - TextField("Value (email or phone)", text: $newCredentialValue) + TextField(newCredentialType == "passkey" ? "Passkey name" : "Value (phone number)", text: $newCredentialValue) .textFieldStyle(.roundedBorder) Button("Register") { switch newCredentialType { case "passkey": - viewModel.registerPasskey() + viewModel.registerPasskey(displayName: newCredentialValue.isEmpty ? nil : newCredentialValue) case "password": viewModel.registerPassword() default: @@ -227,6 +227,11 @@ struct ContentView: View { Text(phone) .font(.caption) .foregroundStyle(.secondary) + } else if method.credentialType == .passkey, + let name = (method as? MSALPasskeyCredentialMethod)?.displayName { + Text(name) + .font(.caption) + .foregroundStyle(.secondary) } else if let displayName = method.displayName { Text(displayName) .font(.caption) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 6a91eccd78..3f101daa77 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -151,7 +151,7 @@ class CredentialManagementViewModel: ObservableObject { // MARK: - Register Passkey - func registerPasskey() { + func registerPasskey(displayName: String? = nil) { let relyingPartyIdentifier = Configuration.relyingPartyIdentifier // Mock: generate a random challenge (in production, this comes from the server) @@ -181,7 +181,7 @@ class CredentialManagementViewModel: ObservableObject { // Register the passkey in the credential management client let credentialIdString = credential.credentialID.base64EncodedString() let passkeyMethod = MSALPasskeyCredentialMethod( - displayName: "Passkey (\(String(credentialIdString.prefix(8)))...)", + displayName: displayName ?? "Passkey (\(String(credentialIdString.prefix(8)))...)", credentialID: credentialIdString, authenticatorAttachment: "platform" ) From 7c66e13a9a5aa7fbe6459250a198753373cdc1e6 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Fri, 29 May 2026 21:50:48 -0700 Subject: [PATCH 28/42] remove unused property. --- .../src/public/MSALNativeCredentialMethodsClient.swift | 1 - .../credential_method/MSALPasskeyCredentialMethod.swift | 7 ------- .../CredentialManagementViewModel.swift | 3 +-- 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index c0d8f7349a..12b92ebd54 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -68,7 +68,6 @@ public class MSALNativeCredentialMethodsClient: NSObject { displayName: "Security Key (YubiKey 5)", createdAt: Date(timeIntervalSinceNow: -86400 * 30), credentialID: "abc123base64", - authenticatorAttachment: "cross-platform", aaguid: "2fc0579f-8113-47ea-b116-bb5a8db9202a" ), MSALPhoneCredentialMethod( diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift index 09212a15ff..028b25799f 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift @@ -36,21 +36,16 @@ public class MSALPasskeyCredentialMethod: MSALCredentialMethod { /// The base64-encoded credential ID from WebAuthn registration. public let credentialID: String? - /// The authenticator attachment type (e.g., "platform", "cross-platform"). - public let authenticatorAttachment: String? - /// The AAGUID of the authenticator that created this passkey. public let aaguid: String? public init( displayName: String?, credentialID: String?, - authenticatorAttachment: String? = "platform", aaguid: String? = nil ) { self.credentialID = credentialID - self.authenticatorAttachment = authenticatorAttachment self.aaguid = aaguid super.init( id: "", @@ -66,12 +61,10 @@ public class MSALPasskeyCredentialMethod: MSALCredentialMethod { displayName: String?, createdAt: Date?, credentialID: String?, - authenticatorAttachment: String? = "platform", aaguid: String? = nil ) { self.credentialID = credentialID - self.authenticatorAttachment = authenticatorAttachment self.aaguid = aaguid super.init( id: id, diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 3f101daa77..8c9b068e28 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -182,8 +182,7 @@ class CredentialManagementViewModel: ObservableObject { let credentialIdString = credential.credentialID.base64EncodedString() let passkeyMethod = MSALPasskeyCredentialMethod( displayName: displayName ?? "Passkey (\(String(credentialIdString.prefix(8)))...)", - credentialID: credentialIdString, - authenticatorAttachment: "platform" + credentialID: credentialIdString ) guard let credClient = self.credClient else { return } let registerResult = await credClient.registerCredentialMethod(passkeyMethod) From e22a6f92bb47d816022708920fdb8d4500bedf8f Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Mon, 1 Jun 2026 18:01:54 -0700 Subject: [PATCH 29/42] refactor. --- .../MSALNativeCredentialMethodsClient.swift | 115 +++--------------- .../src/public/MSALRegisterMethods.swift | 46 +++++++ .../src/public/MSALRegisterParams.swift | 46 +++++++ ...dentialMethodsClient+RegisterPasskey.swift | 89 ++++++++++++++ .../MSALPasskeyCredentialMethod.swift | 0 .../passkey/MSALRegisterMethods+Passkey.swift | 49 ++++++++ .../passkey/MSALRegisterPasskeyParams.swift | 46 +++++++ ...entialMethodsClient+RegisterPassword.swift | 86 +++++++++++++ .../MSALPasswordCredentialMethod.swift | 0 .../MSALRegisterMethods+Password.swift | 49 ++++++++ .../password/MSALRegisterPasswordParams.swift | 39 ++++++ ...redentialMethodsClient+RegisterPhone.swift | 95 +++++++++++++++ .../MSALPhoneCredentialMethod.swift | 0 .../phone/MSALRegisterMethods+Phone.swift | 49 ++++++++ .../phone/MSALRegisterPhoneNumberParams.swift | 39 ++++++ .../CredentialManagementViewModel.swift | 17 ++- 16 files changed, 660 insertions(+), 105 deletions(-) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALRegisterMethods.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALRegisterParams.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/{ => passkey}/MSALPasskeyCredentialMethod.swift (100%) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterMethods+Passkey.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterPasskeyParams.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/{ => password}/MSALPasswordCredentialMethod.swift (100%) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/password/MSALRegisterMethods+Password.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/password/MSALRegisterPasswordParams.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/{ => phone}/MSALPhoneCredentialMethod.swift (100%) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/phone/MSALRegisterMethods+Phone.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/phone/MSALRegisterPhoneNumberParams.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index 12b92ebd54..359955d74e 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -64,7 +64,7 @@ public class MSALNativeCredentialMethodsClient: NSObject { // Seed with default credential methods for POC self.mockCredentialMethods = [ MSALPasskeyCredentialMethod( - id: "fido-001", + id: "passkey-001", displayName: "Security Key (YubiKey 5)", createdAt: Date(timeIntervalSinceNow: -86400 * 30), credentialID: "abc123base64", @@ -143,95 +143,20 @@ public class MSALNativeCredentialMethodsClient: NSObject { } } - // MARK: - Public: Register Credential Method + // MARK: - Public: Register Operations - /// Register a new credential method. + /// Namespace grouping for method-specific registration flows. /// - /// - Parameter credentialMethod: The credential method instance to register. - /// Pass a concrete subclass such as `MSALPasskeyCredentialMethod` or `MSALPhoneCredentialMethod`. - /// - Parameter correlationId: Optional correlation ID for request tracing. A new UUID is generated if nil. - /// - Returns: A `Result` containing the registration outcome (completed or challenge required) or an error. - public func registerCredentialMethod( - _ credentialMethod: any MSALCredentialMethodProtocol, - correlationId: UUID? = nil - ) async -> Result - { - let correlationId = correlationId ?? UUID() - - return await withCheckedContinuation - { continuation in - self.operationQueue.async - { [weak self] in - guard let self = self else - { - let error = MSALNativeCredentialManagementError( - type: .generalError, - message: "Client was deallocated.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(error)) - return - } - - self.acquireToken(correlationId: correlationId) - { accessToken, tokenError in - if let tokenError = tokenError - { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Failed to acquire access token for registering credential method.", - correlationId: correlationId, - underlyingError: tokenError - ) - continuation.resume(returning: .failure(credError)) - return - } - - guard accessToken != nil else - { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Token provider returned nil access token.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(credError)) - return - } - - // Mock: simulate challenge required for phone, immediate for passkey/password - let type = credentialMethod.credentialType - - // Assign server-generated ID and metadata - if let method = credentialMethod as? MSALCredentialMethod - { - method.id = "\(type.rawValue)-\(UUID().uuidString.prefix(8))" - method.createdAt = Date() - } - - if type == .passkey || type == .password - { - self.mockCredentialMethods.append(credentialMethod) - continuation.resume(returning: .success(.completed(credentialMethod))) - } - else - { - // Simulate challenge required for phone - let sentTo = credentialMethod.displayName ?? "***" - let challengeState = MSALCredentialMethodChallengeState( - sentTo: sentTo, - channelType: type.rawValue, - codeLength: 6, - continuationToken: "mock-continuation-\(UUID().uuidString.prefix(8))", - client: self, - correlationId: correlationId - ) - self.pendingRegistrationCredential = credentialMethod - continuation.resume(returning: .success(.challengeRequired(challengeState))) - } - } - } - } - } + /// Each credential type has its own function because registration inputs + /// and activation flows differ per type. + /// + /// Usage: + /// ```swift + /// let result = await client.register.passkey(params: MSALRegisterPasskeyParams(displayName: "My Key")) + /// let result = await client.register.phoneNumber(params: MSALRegisterPhoneNumberParams(phoneNumber: "+1234567890")) + /// let result = await client.register.password(params: MSALRegisterPasswordParams(password: "secret")) + /// ``` + public private(set) lazy var register: MSALRegisterMethods = MSALRegisterMethods(client: self) // MARK: - Public: Delete Credential Method @@ -395,16 +320,16 @@ public class MSALNativeCredentialMethodsClient: NSObject { } } - // MARK: - Private: Properties + // MARK: - Internal: Properties - private let config: MSALNativeCredentialManagementConfig - private let operationQueue: DispatchQueue - private var mockCredentialMethods: [any MSALCredentialMethodProtocol] - private var pendingRegistrationCredential: (any MSALCredentialMethodProtocol)? + internal let config: MSALNativeCredentialManagementConfig + internal let operationQueue: DispatchQueue + internal var mockCredentialMethods: [any MSALCredentialMethodProtocol] + internal var pendingRegistrationCredential: (any MSALCredentialMethodProtocol)? - // MARK: - Private: Token Acquisition + // MARK: - Internal: Token Acquisition - private func acquireToken( + internal func acquireToken( correlationId: UUID, completion: @escaping (String?, Error?) -> Void ) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALRegisterMethods.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALRegisterMethods.swift new file mode 100644 index 0000000000..3eeef7b9d0 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALRegisterMethods.swift @@ -0,0 +1,46 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Namespace for credential method registration operations. +/// +/// Access via `client.register.passkey(...)`, `client.register.phoneNumber(...)`, +/// or `client.register.password(...)`. +/// +/// Each credential type adds its registration method via an extension in its +/// own folder under `credential_method//`. This follows the Open/Closed +/// Principle — adding a new credential type requires only new files, without +/// modifying existing code. +public class MSALRegisterMethods +{ + // MARK: - Internal + + internal weak var client: MSALNativeCredentialMethodsClient? + + internal init(client: MSALNativeCredentialMethodsClient) + { + self.client = client + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALRegisterParams.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALRegisterParams.swift new file mode 100644 index 0000000000..d280c6e719 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALRegisterParams.swift @@ -0,0 +1,46 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Base class for all credential method registration parameters. +/// +/// Subclasses add credential-specific fields (e.g., display name, phone number, password). +@objcMembers +public class MSALRegisterParams: NSObject +{ + /// Optional correlation ID for request tracing. A new UUID is generated if nil. + public var correlationId: UUID? + + public override init() + { + super.init() + } + + public init(correlationId: UUID?) + { + self.correlationId = correlationId + super.init() + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift new file mode 100644 index 0000000000..ba73d00c20 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift @@ -0,0 +1,89 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +extension MSALNativeCredentialMethodsClient +{ + internal func performRegisterPasskey( + params: MSALRegisterPasskeyParams? + ) async -> Result + { + let correlationId = params?.correlationId ?? UUID() + + return await withCheckedContinuation + { continuation in + self.operationQueue.async + { [weak self] in + guard let self = self else + { + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "Client was deallocated.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(error)) + return + } + + self.acquireToken(correlationId: correlationId) + { accessToken, tokenError in + if let tokenError = tokenError + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Failed to acquire access token for registering credential method.", + correlationId: correlationId, + underlyingError: tokenError + ) + continuation.resume(returning: .failure(credError)) + return + } + + guard accessToken != nil else + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Token provider returned nil access token.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(credError)) + return + } + + // Mock: passkey registration completes immediately + let method = MSALPasskeyCredentialMethod( + id: "passkey-\(UUID().uuidString.prefix(8))", + displayName: params?.displayName, + createdAt: Date(), + credentialID: nil, + aaguid: nil + ) + self.mockCredentialMethods.append(method) + continuation.resume(returning: .success(.completed(method))) + } + } + } + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyCredentialMethod.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasskeyCredentialMethod.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyCredentialMethod.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterMethods+Passkey.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterMethods+Passkey.swift new file mode 100644 index 0000000000..8c165867de --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterMethods+Passkey.swift @@ -0,0 +1,49 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +extension MSALRegisterMethods +{ + /// Begins passkey (FIDO2/WebAuthn) registration. + /// + /// - Parameter params: Optional parameters including display name and correlation ID. + /// - Returns: A `Result` containing the registration outcome or an error. + public func passkey( + params: MSALRegisterPasskeyParams? = nil + ) async -> Result + { + guard let client = client else + { + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "Client was deallocated.", + correlationId: params?.correlationId ?? UUID() + ) + return .failure(error) + } + + return await client.performRegisterPasskey(params: params) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterPasskeyParams.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterPasskeyParams.swift new file mode 100644 index 0000000000..10965cddf4 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterPasskeyParams.swift @@ -0,0 +1,46 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Parameters for `client.register.passkey()`. +/// +/// No fields are strictly required — the server generates the WebAuthn challenge. +@objcMembers +public class MSALRegisterPasskeyParams: MSALRegisterParams +{ + /// Optional human-readable label for the passkey (e.g. "Work YubiKey"). + public var displayName: String? + + public override init() + { + super.init() + } + + public init(displayName: String? = nil, correlationId: UUID? = nil) + { + self.displayName = displayName + super.init(correlationId: correlationId) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift new file mode 100644 index 0000000000..2179135920 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift @@ -0,0 +1,86 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +extension MSALNativeCredentialMethodsClient +{ + internal func performRegisterPassword( + params: MSALRegisterPasswordParams + ) async -> Result + { + let correlationId = params.correlationId ?? UUID() + + return await withCheckedContinuation + { continuation in + self.operationQueue.async + { [weak self] in + guard let self = self else + { + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "Client was deallocated.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(error)) + return + } + + self.acquireToken(correlationId: correlationId) + { accessToken, tokenError in + if let tokenError = tokenError + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Failed to acquire access token for registering credential method.", + correlationId: correlationId, + underlyingError: tokenError + ) + continuation.resume(returning: .failure(credError)) + return + } + + guard accessToken != nil else + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Token provider returned nil access token.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(credError)) + return + } + + // Mock: password registration completes immediately + let method = MSALPasswordCredentialMethod( + id: "password-\(UUID().uuidString.prefix(8))", + createdAt: Date() + ) + self.mockCredentialMethods.append(method) + continuation.resume(returning: .success(.completed(method))) + } + } + } + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/password/MSALPasswordCredentialMethod.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPasswordCredentialMethod.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/password/MSALPasswordCredentialMethod.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/password/MSALRegisterMethods+Password.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/password/MSALRegisterMethods+Password.swift new file mode 100644 index 0000000000..dd1efcc895 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/password/MSALRegisterMethods+Password.swift @@ -0,0 +1,49 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +extension MSALRegisterMethods +{ + /// Registers a password. Typically completes in a single step (no activation needed). + /// + /// - Parameter params: Parameters including the password value and optional correlation ID. + /// - Returns: A `Result` containing the registration outcome or an error. + public func password( + params: MSALRegisterPasswordParams + ) async -> Result + { + guard let client = client else + { + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "Client was deallocated.", + correlationId: params.correlationId ?? UUID() + ) + return .failure(error) + } + + return await client.performRegisterPassword(params: params) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/password/MSALRegisterPasswordParams.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/password/MSALRegisterPasswordParams.swift new file mode 100644 index 0000000000..73eb487c7f --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/password/MSALRegisterPasswordParams.swift @@ -0,0 +1,39 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Parameters for `client.register.password()`. +@objcMembers +public class MSALRegisterPasswordParams: MSALRegisterParams +{ + /// The password value to register. + public var password: String + + public init(password: String, correlationId: UUID? = nil) + { + self.password = password + super.init(correlationId: correlationId) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift new file mode 100644 index 0000000000..7c01a81758 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift @@ -0,0 +1,95 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +extension MSALNativeCredentialMethodsClient +{ + internal func performRegisterPhoneNumber( + params: MSALRegisterPhoneNumberParams + ) async -> Result + { + let correlationId = params.correlationId ?? UUID() + + return await withCheckedContinuation + { continuation in + self.operationQueue.async + { [weak self] in + guard let self = self else + { + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "Client was deallocated.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(error)) + return + } + + self.acquireToken(correlationId: correlationId) + { accessToken, tokenError in + if let tokenError = tokenError + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Failed to acquire access token for registering credential method.", + correlationId: correlationId, + underlyingError: tokenError + ) + continuation.resume(returning: .failure(credError)) + return + } + + guard accessToken != nil else + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Token provider returned nil access token.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(credError)) + return + } + + // Mock: phone registration requires OTP challenge + let method = MSALPhoneCredentialMethod( + id: "phone-\(UUID().uuidString.prefix(8))", + createdAt: Date(), + phoneNumber: params.phoneNumber + ) + let challengeState = MSALCredentialMethodChallengeState( + sentTo: params.phoneNumber, + channelType: MSALCredentialType.phone.rawValue, + codeLength: 6, + continuationToken: "mock-continuation-\(UUID().uuidString.prefix(8))", + client: self, + correlationId: correlationId + ) + self.pendingRegistrationCredential = method + continuation.resume(returning: .success(.challengeRequired(challengeState))) + } + } + } + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/phone/MSALPhoneCredentialMethod.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALPhoneCredentialMethod.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/phone/MSALPhoneCredentialMethod.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/phone/MSALRegisterMethods+Phone.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/phone/MSALRegisterMethods+Phone.swift new file mode 100644 index 0000000000..e3600e051b --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/phone/MSALRegisterMethods+Phone.swift @@ -0,0 +1,49 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +extension MSALRegisterMethods +{ + /// Begins phone number registration with the phone number to enroll. + /// + /// - Parameter params: Parameters including the phone number (E.164 format) and optional correlation ID. + /// - Returns: A `Result` containing the registration outcome (completed or challenge required) or an error. + public func phoneNumber( + params: MSALRegisterPhoneNumberParams + ) async -> Result + { + guard let client = client else + { + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "Client was deallocated.", + correlationId: params.correlationId ?? UUID() + ) + return .failure(error) + } + + return await client.performRegisterPhoneNumber(params: params) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/phone/MSALRegisterPhoneNumberParams.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/phone/MSALRegisterPhoneNumberParams.swift new file mode 100644 index 0000000000..208866b691 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/phone/MSALRegisterPhoneNumberParams.swift @@ -0,0 +1,39 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Parameters for `client.register.phoneNumber()`. +@objcMembers +public class MSALRegisterPhoneNumberParams: MSALRegisterParams +{ + /// The phone number to enroll, in E.164 format (e.g. "+35312345678"). + public var phoneNumber: String + + public init(phoneNumber: String, correlationId: UUID? = nil) + { + self.phoneNumber = phoneNumber + super.init(correlationId: correlationId) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 8c9b068e28..01482c9147 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -120,12 +120,10 @@ class CredentialManagementViewModel: ObservableObject { statusMessage = "Registering phone..." errorMessage = nil - let credentialMethod = MSALPhoneCredentialMethod( - phoneNumber: phoneNumber - ) + let params = MSALRegisterPhoneNumberParams(phoneNumber: phoneNumber) Task { - let result = await credClient.registerCredentialMethod(credentialMethod) + let result = await credClient.register.phoneNumber(params: params) handleRegistrationResult(result) } } @@ -141,10 +139,10 @@ class CredentialManagementViewModel: ObservableObject { statusMessage = "Registering password..." errorMessage = nil - let credentialMethod = MSALPasswordCredentialMethod() + let params = MSALRegisterPasswordParams(password: "") Task { - let result = await credClient.registerCredentialMethod(credentialMethod) + let result = await credClient.register.password(params: params) handleRegistrationResult(result) } } @@ -180,12 +178,11 @@ class CredentialManagementViewModel: ObservableObject { case .success(let credential): // Register the passkey in the credential management client let credentialIdString = credential.credentialID.base64EncodedString() - let passkeyMethod = MSALPasskeyCredentialMethod( - displayName: displayName ?? "Passkey (\(String(credentialIdString.prefix(8)))...)", - credentialID: credentialIdString + let params = MSALRegisterPasskeyParams( + displayName: displayName ?? "Passkey (\(String(credentialIdString.prefix(8)))...)" ) guard let credClient = self.credClient else { return } - let registerResult = await credClient.registerCredentialMethod(passkeyMethod) + let registerResult = await credClient.register.passkey(params: params) self.handleRegistrationResult(registerResult) case .failure(let error): self.isLoading = false From 46ffdd7e3fabd7ba3f67a8afbc969b9d7d54cfd7 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Mon, 1 Jun 2026 18:19:03 -0700 Subject: [PATCH 30/42] refactor passkey registration. --- .../MSALNativeCredentialMethodsClient.swift | 1 + ...dentialMethodsClient+RegisterPasskey.swift | 91 ++++++++++- .../MSALPasskeyRegistrationState.swift | 154 ++++++++++++++++++ .../passkey/MSALRegisterMethods+Passkey.swift | 10 +- .../CredentialManagementViewModel.swift | 96 ++++++----- 5 files changed, 303 insertions(+), 49 deletions(-) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyRegistrationState.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index 359955d74e..99f57c7522 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -153,6 +153,7 @@ public class MSALNativeCredentialMethodsClient: NSObject { /// Usage: /// ```swift /// let result = await client.register.passkey(params: MSALRegisterPasskeyParams(displayName: "My Key")) + /// // Use result.creationOptions with ASAuthorization, then call state.complete(attestation:) /// let result = await client.register.phoneNumber(params: MSALRegisterPhoneNumberParams(phoneNumber: "+1234567890")) /// let result = await client.register.password(params: MSALRegisterPasswordParams(password: "secret")) /// ``` diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift index ba73d00c20..c6d3910c2a 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift @@ -26,9 +26,10 @@ import Foundation extension MSALNativeCredentialMethodsClient { + /// Step 1: Request passkey creation options from the server. internal func performRegisterPasskey( params: MSALRegisterPasskeyParams? - ) async -> Result + ) async -> Result { let correlationId = params?.correlationId ?? UUID() @@ -53,7 +54,7 @@ extension MSALNativeCredentialMethodsClient { let credError = MSALNativeCredentialManagementError( type: .unauthorized, - message: "Failed to acquire access token for registering credential method.", + message: "Failed to acquire access token for passkey registration.", correlationId: correlationId, underlyingError: tokenError ) @@ -72,16 +73,94 @@ extension MSALNativeCredentialMethodsClient return } - // Mock: passkey registration completes immediately + // Mock server response: generate WebAuthn creation options + // In production, this would be a POST to the credential management API + // which returns PublicKeyCredentialCreationOptions. + var challengeBytes = [UInt8](repeating: 0, count: 32) + _ = SecRandomCopyBytes(kSecRandomDefault, challengeBytes.count, &challengeBytes) + + let creationOptions = MSALPasskeyCreationOptions( + challenge: Data(challengeBytes), + userId: Data(UUID().uuidString.utf8), + userName: params?.displayName ?? "user", + relyingPartyIdentifier: "login.microsoft.com" + ) + + let continuationToken = "passkey-reg-\(UUID().uuidString)" + + let state = MSALPasskeyRegistrationState( + creationOptions: creationOptions, + continuationToken: continuationToken, + client: self, + correlationId: correlationId + ) + + continuation.resume(returning: .success(state)) + } + } + } + } + + /// Step 2: Submit the attestation from the platform authenticator to the server. + internal func completePasskeyRegistration( + attestation: MSALPasskeyAttestation, + continuationToken: String, + correlationId: UUID + ) async -> Result + { + return await withCheckedContinuation + { continuation in + self.operationQueue.async + { [weak self] in + guard let self = self else + { + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "Client was deallocated.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(error)) + return + } + + self.acquireToken(correlationId: correlationId) + { accessToken, tokenError in + if let tokenError = tokenError + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Failed to acquire access token for completing passkey registration.", + correlationId: correlationId, + underlyingError: tokenError + ) + continuation.resume(returning: .failure(credError)) + return + } + + guard accessToken != nil else + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Token provider returned nil access token.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(credError)) + return + } + + // Mock server response: validate attestation and store credential. + // In production, this would POST the attestation to the server + // which validates and returns the registered credential method. + let credentialIdString = attestation.credentialId.base64EncodedString() let method = MSALPasskeyCredentialMethod( id: "passkey-\(UUID().uuidString.prefix(8))", - displayName: params?.displayName, + displayName: "Passkey (\(String(credentialIdString.prefix(8)))...)", createdAt: Date(), - credentialID: nil, + credentialID: credentialIdString, aaguid: nil ) self.mockCredentialMethods.append(method) - continuation.resume(returning: .success(.completed(method))) + continuation.resume(returning: .success(method)) } } } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyRegistrationState.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyRegistrationState.swift new file mode 100644 index 0000000000..b934ddb68f --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyRegistrationState.swift @@ -0,0 +1,154 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// WebAuthn creation options returned by the server for passkey registration. +/// +/// Use these values to configure `ASAuthorizationPlatformPublicKeyCredentialProvider` +/// and create a credential registration request. +@objcMembers +public class MSALPasskeyCreationOptions: NSObject +{ + /// The WebAuthn challenge (random bytes from the server). + public let challenge: Data + + /// The user identifier assigned by the server. + public let userId: Data + + /// The user display name (e.g., email or friendly name) for the passkey prompt. + public let userName: String + + /// The relying party identifier (e.g., "login.microsoft.com"). + public let relyingPartyIdentifier: String + + internal init( + challenge: Data, + userId: Data, + userName: String, + relyingPartyIdentifier: String + ) + { + self.challenge = challenge + self.userId = userId + self.userName = userName + self.relyingPartyIdentifier = relyingPartyIdentifier + super.init() + } +} + +/// State object returned after requesting passkey registration from the server. +/// +/// Contains the WebAuthn creation options needed to invoke the platform authenticator, +/// and a `complete(attestation:)` method to finalize registration. +/// +/// Usage: +/// ```swift +/// let result = await client.register.passkey(params: params) +/// switch result { +/// case .success(let state): +/// // Use state.creationOptions to drive ASAuthorization +/// let credential = try await performPlatformPasskeyCreation(with: state.creationOptions) +/// let finalResult = await state.complete(attestation: attestation) +/// case .failure(let error): +/// // handle error +/// } +/// ``` +public class MSALPasskeyRegistrationState +{ + /// The WebAuthn creation options to use with the platform authenticator. + public let creationOptions: MSALPasskeyCreationOptions + + // MARK: - Internal + + private let continuationToken: String + private weak var client: MSALNativeCredentialMethodsClient? + private let correlationId: UUID + + internal init( + creationOptions: MSALPasskeyCreationOptions, + continuationToken: String, + client: MSALNativeCredentialMethodsClient, + correlationId: UUID + ) + { + self.creationOptions = creationOptions + self.continuationToken = continuationToken + self.client = client + self.correlationId = correlationId + } + + /// Complete passkey registration by submitting the attestation from the platform authenticator. + /// + /// - Parameter attestation: The attestation data from the credential creation response. + /// - Returns: A `Result` containing the registered passkey credential method or an error. + public func complete( + attestation: MSALPasskeyAttestation + ) async -> Result + { + guard let client = client else + { + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "Client was deallocated.", + correlationId: correlationId + ) + return .failure(error) + } + + return await client.completePasskeyRegistration( + attestation: attestation, + continuationToken: continuationToken, + correlationId: correlationId + ) + } +} + +/// Attestation data from the platform authenticator after passkey creation. +/// +/// Populate this from the `ASAuthorizationPlatformPublicKeyCredentialRegistration` response. +@objcMembers +public class MSALPasskeyAttestation: NSObject +{ + /// The credential ID assigned by the authenticator. + public let credentialId: Data + + /// The raw attestation object (CBOR-encoded). + public let rawAttestationObject: Data + + /// The raw client data JSON. + public let rawClientDataJSON: Data + + public init( + credentialId: Data, + rawAttestationObject: Data, + rawClientDataJSON: Data + ) + { + self.credentialId = credentialId + self.rawAttestationObject = rawAttestationObject + self.rawClientDataJSON = rawClientDataJSON + super.init() + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterMethods+Passkey.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterMethods+Passkey.swift index 8c165867de..b97861c53b 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterMethods+Passkey.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterMethods+Passkey.swift @@ -26,13 +26,17 @@ import Foundation extension MSALRegisterMethods { - /// Begins passkey (FIDO2/WebAuthn) registration. + /// Begins passkey (FIDO2/WebAuthn) registration by requesting creation options from the server. + /// + /// The returned `MSALPasskeyRegistrationState` contains the WebAuthn challenge and parameters + /// needed to invoke the platform authenticator. After the user creates the passkey, + /// call `state.complete(attestation:)` to finalize registration. /// /// - Parameter params: Optional parameters including display name and correlation ID. - /// - Returns: A `Result` containing the registration outcome or an error. + /// - Returns: A `Result` containing the registration state (with creation options) or an error. public func passkey( params: MSALRegisterPasskeyParams? = nil - ) async -> Result + ) async -> Result { guard let client = client else { diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 01482c9147..f8bbf93b6e 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -150,49 +150,65 @@ class CredentialManagementViewModel: ObservableObject { // MARK: - Register Passkey func registerPasskey(displayName: String? = nil) { - let relyingPartyIdentifier = Configuration.relyingPartyIdentifier - - // Mock: generate a random challenge (in production, this comes from the server) - var challengeBytes = [UInt8](repeating: 0, count: 32) - _ = SecRandomCopyBytes(kSecRandomDefault, challengeBytes.count, &challengeBytes) - let challenge = Data(challengeBytes) - - // Mock: use a random user ID (in production, this comes from the server) - let userId = Data(UUID().uuidString.utf8) - - let provider = ASAuthorizationPlatformPublicKeyCredentialProvider( - relyingPartyIdentifier: relyingPartyIdentifier - ) - - let registrationRequest = provider.createCredentialRegistrationRequest( - challenge: challenge, - name: userName.isEmpty ? "user@example.com" : userName, - userID: userId - ) - - let authController = ASAuthorizationController(authorizationRequests: [registrationRequest]) - passkeyDelegate = PasskeyAuthorizationDelegate { [weak self] result in - Task { @MainActor in - guard let self = self else { return } - switch result { - case .success(let credential): - // Register the passkey in the credential management client - let credentialIdString = credential.credentialID.base64EncodedString() - let params = MSALRegisterPasskeyParams( - displayName: displayName ?? "Passkey (\(String(credentialIdString.prefix(8)))...)" - ) - guard let credClient = self.credClient else { return } - let registerResult = await credClient.register.passkey(params: params) - self.handleRegistrationResult(registerResult) - case .failure(let error): - self.isLoading = false - self.errorMessage = "Passkey creation failed: \(error.localizedDescription)" + guard let credClient = self.credClient else { return } + isLoading = true + + Task { @MainActor in + // Step 1: Request creation options from the server + let params = MSALRegisterPasskeyParams(displayName: displayName) + let beginResult = await credClient.register.passkey(params: params) + + switch beginResult { + case .failure(let error): + self.isLoading = false + self.errorMessage = "Passkey registration failed: \(error.message ?? "unknown error")" + return + case .success(let state): + // Step 2: Use creation options to invoke platform authenticator + let options = state.creationOptions + let provider = ASAuthorizationPlatformPublicKeyCredentialProvider( + relyingPartyIdentifier: options.relyingPartyIdentifier + ) + + let registrationRequest = provider.createCredentialRegistrationRequest( + challenge: options.challenge, + name: options.userName, + userID: options.userId + ) + + let authController = ASAuthorizationController(authorizationRequests: [registrationRequest]) + passkeyDelegate = PasskeyAuthorizationDelegate { [weak self] result in + Task { @MainActor in + guard let self = self else { return } + switch result { + case .success(let credential): + // Step 3: Submit attestation back to the server + let attestation = MSALPasskeyAttestation( + credentialId: credential.credentialID, + rawAttestationObject: credential.rawAttestationObject ?? Data(), + rawClientDataJSON: credential.rawClientDataJSON + ) + let completeResult = await state.complete(attestation: attestation) + switch completeResult { + case .success(let method): + self.isLoading = false + self.statusMessage = "Passkey registered: \(method.displayName ?? "unknown")" + self.listCredentialMethods() + case .failure(let error): + self.isLoading = false + self.errorMessage = "Passkey registration failed: \(error.message ?? "unknown error")" + } + case .failure(let error): + self.isLoading = false + self.errorMessage = "Passkey creation failed: \(error.localizedDescription)" + } + } } + authController.delegate = passkeyDelegate + authController.presentationContextProvider = passkeyDelegate + authController.performRequests() } } - authController.delegate = passkeyDelegate - authController.presentationContextProvider = passkeyDelegate - authController.performRequests() } private var passkeyDelegate: PasskeyAuthorizationDelegate? From 8085ca66640733c0957087dc2f1233e46da92749 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Mon, 1 Jun 2026 19:16:31 -0700 Subject: [PATCH 31/42] change passkey api. --- .../MSALNativeCredentialMethodsClient.swift | 5 +- ...dentialMethodsClient+RegisterPasskey.swift | 161 +++++++++--------- .../MSALPasskeyAuthorizationHandler.swift | 145 ++++++++++++++++ .../MSALPasskeyRegistrationState.swift | 154 ----------------- .../passkey/MSALRegisterMethods+Passkey.swift | 34 +++- .../passkey/MSALRegisterPasskeyParams.swift | 31 +++- .../CredentialManagementViewModel.swift | 79 +++------ .../PasskeyAuthorizationDelegate.swift | 67 -------- 8 files changed, 301 insertions(+), 375 deletions(-) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyAuthorizationHandler.swift delete mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyRegistrationState.swift delete mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/PasskeyAuthorizationDelegate.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index 99f57c7522..4a7e9e7573 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -152,8 +152,9 @@ public class MSALNativeCredentialMethodsClient: NSObject { /// /// Usage: /// ```swift - /// let result = await client.register.passkey(params: MSALRegisterPasskeyParams(displayName: "My Key")) - /// // Use result.creationOptions with ASAuthorization, then call state.complete(attestation:) + /// let params = MSALRegisterPasskeyParams(presentationAnchor: window, displayName: "My Key") + /// let result = await client.register.passkey(params: params) + /// // All register methods return Result /// let result = await client.register.phoneNumber(params: MSALRegisterPhoneNumberParams(phoneNumber: "+1234567890")) /// let result = await client.register.password(params: MSALRegisterPasswordParams(password: "secret")) /// ``` diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift index c6d3910c2a..c9dff748ef 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift @@ -23,90 +23,97 @@ // THE SOFTWARE. import Foundation +import AuthenticationServices extension MSALNativeCredentialMethodsClient { - /// Step 1: Request passkey creation options from the server. + /// Performs the full passkey registration flow: + /// 1. Acquires an access token. + /// 2. Requests WebAuthn creation options from the server (mocked). + /// 3. Invokes the platform authenticator via ASAuthorization. + /// 4. Submits the attestation back to the server (mocked). + /// 5. Returns the registered credential method. internal func performRegisterPasskey( - params: MSALRegisterPasskeyParams? - ) async -> Result + params: MSALRegisterPasskeyParams + ) async -> Result { - let correlationId = params?.correlationId ?? UUID() + let correlationId = params.correlationId ?? UUID() - return await withCheckedContinuation - { continuation in - self.operationQueue.async - { [weak self] in - guard let self = self else - { - let error = MSALNativeCredentialManagementError( - type: .generalError, - message: "Client was deallocated.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(error)) - return - } + // Step 1: Acquire access token + let tokenResult = await acquireTokenAsync(correlationId: correlationId) + switch tokenResult + { + case .failure(let error): + return .failure(error) + case .success: + break + } - self.acquireToken(correlationId: correlationId) - { accessToken, tokenError in - if let tokenError = tokenError - { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Failed to acquire access token for passkey registration.", - correlationId: correlationId, - underlyingError: tokenError - ) - continuation.resume(returning: .failure(credError)) - return - } + // Step 2: Request creation options from the server (mock) + let creationOptions = requestCreationOptions(displayName: params.displayName) - guard accessToken != nil else - { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Token provider returned nil access token.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(credError)) - return - } + // Step 3: Invoke platform authenticator + let handler = MSALPasskeyAuthorizationHandler(anchor: params.presentationAnchor) + let attestation: MSALPasskeyAttestation + do + { + attestation = try await handler.performRegistration(options: creationOptions) + } + catch + { + let credError = MSALNativeCredentialManagementError( + type: .generalError, + message: "Passkey creation was cancelled or failed.", + correlationId: correlationId, + underlyingError: error + ) + return .failure(credError) + } - // Mock server response: generate WebAuthn creation options - // In production, this would be a POST to the credential management API - // which returns PublicKeyCredentialCreationOptions. - var challengeBytes = [UInt8](repeating: 0, count: 32) - _ = SecRandomCopyBytes(kSecRandomDefault, challengeBytes.count, &challengeBytes) + // Step 4: Submit attestation to server (mock) + return submitAttestation(attestation, correlationId: correlationId) + } - let creationOptions = MSALPasskeyCreationOptions( - challenge: Data(challengeBytes), - userId: Data(UUID().uuidString.utf8), - userName: params?.displayName ?? "user", - relyingPartyIdentifier: "login.microsoft.com" - ) + // MARK: - Private Helpers - let continuationToken = "passkey-reg-\(UUID().uuidString)" + /// Requests WebAuthn creation options from the server. + /// In production, this would be a POST to the credential management API. + private func requestCreationOptions(displayName: String?) -> MSALPasskeyCreationOptions + { + var challengeBytes = [UInt8](repeating: 0, count: 32) + _ = SecRandomCopyBytes(kSecRandomDefault, challengeBytes.count, &challengeBytes) - let state = MSALPasskeyRegistrationState( - creationOptions: creationOptions, - continuationToken: continuationToken, - client: self, - correlationId: correlationId - ) + return MSALPasskeyCreationOptions( + challenge: Data(challengeBytes), + userId: Data(UUID().uuidString.utf8), + userName: displayName ?? "user", + relyingPartyIdentifier: "login.microsoft.com" + ) + } - continuation.resume(returning: .success(state)) - } - } - } + /// Submits the attestation to the server for validation. + /// In production, this would POST the attestation and receive the registered credential. + private func submitAttestation( + _ attestation: MSALPasskeyAttestation, + correlationId: UUID + ) -> Result + { + let credentialIdString = attestation.credentialId.base64EncodedString() + let method = MSALPasskeyCredentialMethod( + id: "passkey-\(UUID().uuidString.prefix(8))", + displayName: "Passkey (\(String(credentialIdString.prefix(8)))...)", + createdAt: Date(), + credentialID: credentialIdString, + aaguid: nil + ) + mockCredentialMethods.append(method) + return .success(.completed(method)) } - /// Step 2: Submit the attestation from the platform authenticator to the server. - internal func completePasskeyRegistration( - attestation: MSALPasskeyAttestation, - continuationToken: String, + /// Async wrapper around the token acquisition callback. + private func acquireTokenAsync( correlationId: UUID - ) async -> Result + ) async -> Result { return await withCheckedContinuation { continuation in @@ -129,7 +136,7 @@ extension MSALNativeCredentialMethodsClient { let credError = MSALNativeCredentialManagementError( type: .unauthorized, - message: "Failed to acquire access token for completing passkey registration.", + message: "Failed to acquire access token for passkey registration.", correlationId: correlationId, underlyingError: tokenError ) @@ -137,7 +144,7 @@ extension MSALNativeCredentialMethodsClient return } - guard accessToken != nil else + guard let accessToken = accessToken else { let credError = MSALNativeCredentialManagementError( type: .unauthorized, @@ -148,19 +155,7 @@ extension MSALNativeCredentialMethodsClient return } - // Mock server response: validate attestation and store credential. - // In production, this would POST the attestation to the server - // which validates and returns the registered credential method. - let credentialIdString = attestation.credentialId.base64EncodedString() - let method = MSALPasskeyCredentialMethod( - id: "passkey-\(UUID().uuidString.prefix(8))", - displayName: "Passkey (\(String(credentialIdString.prefix(8)))...)", - createdAt: Date(), - credentialID: credentialIdString, - aaguid: nil - ) - self.mockCredentialMethods.append(method) - continuation.resume(returning: .success(method)) + continuation.resume(returning: .success(accessToken)) } } } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyAuthorizationHandler.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyAuthorizationHandler.swift new file mode 100644 index 0000000000..a1fdf519fe --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyAuthorizationHandler.swift @@ -0,0 +1,145 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import AuthenticationServices +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +// MARK: - Internal Types + +/// WebAuthn creation options returned by the server for passkey registration. +internal struct MSALPasskeyCreationOptions +{ + let challenge: Data + let userId: Data + let userName: String + let relyingPartyIdentifier: String +} + +/// Attestation data from the platform authenticator after passkey creation. +internal struct MSALPasskeyAttestation +{ + let credentialId: Data + let rawAttestationObject: Data + let rawClientDataJSON: Data +} + +// MARK: - Internal Authorization Handler + +/// Handles the ASAuthorization flow for passkey creation. +/// +/// This class encapsulates all platform authenticator interactions so that +/// developers never need to work with `ASAuthorizationController` directly. +internal class MSALPasskeyAuthorizationHandler: NSObject, + ASAuthorizationControllerDelegate, + ASAuthorizationControllerPresentationContextProviding +{ + private let anchor: ASPresentationAnchor + private var continuation: CheckedContinuation? + + init(anchor: ASPresentationAnchor) + { + self.anchor = anchor + super.init() + } + + /// Performs the platform passkey creation and returns the attestation. + func performRegistration( + options: MSALPasskeyCreationOptions + ) async throws -> MSALPasskeyAttestation + { + return try await withCheckedThrowingContinuation + { continuation in + self.continuation = continuation + + let provider = ASAuthorizationPlatformPublicKeyCredentialProvider( + relyingPartyIdentifier: options.relyingPartyIdentifier + ) + + let request = provider.createCredentialRegistrationRequest( + challenge: options.challenge, + name: options.userName, + userID: options.userId + ) + + let controller = ASAuthorizationController(authorizationRequests: [request]) + controller.delegate = self + controller.presentationContextProvider = self + DispatchQueue.main.async + { + controller.performRequests() + } + } + } + + // MARK: - ASAuthorizationControllerPresentationContextProviding + + func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor + { + return anchor + } + + // MARK: - ASAuthorizationControllerDelegate + + func authorizationController( + controller: ASAuthorizationController, + didCompleteWithAuthorization authorization: ASAuthorization + ) + { + guard let credential = authorization.credential + as? ASAuthorizationPlatformPublicKeyCredentialRegistration + else + { + let error = NSError( + domain: "MSALPasskeyError", + code: -1, + userInfo: [NSLocalizedDescriptionKey: "Unexpected credential type returned."] + ) + continuation?.resume(throwing: error) + continuation = nil + return + } + + let attestation = MSALPasskeyAttestation( + credentialId: credential.credentialID, + rawAttestationObject: credential.rawAttestationObject ?? Data(), + rawClientDataJSON: credential.rawClientDataJSON + ) + continuation?.resume(returning: attestation) + continuation = nil + } + + func authorizationController( + controller: ASAuthorizationController, + didCompleteWithError error: Error + ) + { + continuation?.resume(throwing: error) + continuation = nil + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyRegistrationState.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyRegistrationState.swift deleted file mode 100644 index b934ddb68f..0000000000 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyRegistrationState.swift +++ /dev/null @@ -1,154 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. -// All rights reserved. -// -// This code is licensed under the MIT License. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -/// WebAuthn creation options returned by the server for passkey registration. -/// -/// Use these values to configure `ASAuthorizationPlatformPublicKeyCredentialProvider` -/// and create a credential registration request. -@objcMembers -public class MSALPasskeyCreationOptions: NSObject -{ - /// The WebAuthn challenge (random bytes from the server). - public let challenge: Data - - /// The user identifier assigned by the server. - public let userId: Data - - /// The user display name (e.g., email or friendly name) for the passkey prompt. - public let userName: String - - /// The relying party identifier (e.g., "login.microsoft.com"). - public let relyingPartyIdentifier: String - - internal init( - challenge: Data, - userId: Data, - userName: String, - relyingPartyIdentifier: String - ) - { - self.challenge = challenge - self.userId = userId - self.userName = userName - self.relyingPartyIdentifier = relyingPartyIdentifier - super.init() - } -} - -/// State object returned after requesting passkey registration from the server. -/// -/// Contains the WebAuthn creation options needed to invoke the platform authenticator, -/// and a `complete(attestation:)` method to finalize registration. -/// -/// Usage: -/// ```swift -/// let result = await client.register.passkey(params: params) -/// switch result { -/// case .success(let state): -/// // Use state.creationOptions to drive ASAuthorization -/// let credential = try await performPlatformPasskeyCreation(with: state.creationOptions) -/// let finalResult = await state.complete(attestation: attestation) -/// case .failure(let error): -/// // handle error -/// } -/// ``` -public class MSALPasskeyRegistrationState -{ - /// The WebAuthn creation options to use with the platform authenticator. - public let creationOptions: MSALPasskeyCreationOptions - - // MARK: - Internal - - private let continuationToken: String - private weak var client: MSALNativeCredentialMethodsClient? - private let correlationId: UUID - - internal init( - creationOptions: MSALPasskeyCreationOptions, - continuationToken: String, - client: MSALNativeCredentialMethodsClient, - correlationId: UUID - ) - { - self.creationOptions = creationOptions - self.continuationToken = continuationToken - self.client = client - self.correlationId = correlationId - } - - /// Complete passkey registration by submitting the attestation from the platform authenticator. - /// - /// - Parameter attestation: The attestation data from the credential creation response. - /// - Returns: A `Result` containing the registered passkey credential method or an error. - public func complete( - attestation: MSALPasskeyAttestation - ) async -> Result - { - guard let client = client else - { - let error = MSALNativeCredentialManagementError( - type: .generalError, - message: "Client was deallocated.", - correlationId: correlationId - ) - return .failure(error) - } - - return await client.completePasskeyRegistration( - attestation: attestation, - continuationToken: continuationToken, - correlationId: correlationId - ) - } -} - -/// Attestation data from the platform authenticator after passkey creation. -/// -/// Populate this from the `ASAuthorizationPlatformPublicKeyCredentialRegistration` response. -@objcMembers -public class MSALPasskeyAttestation: NSObject -{ - /// The credential ID assigned by the authenticator. - public let credentialId: Data - - /// The raw attestation object (CBOR-encoded). - public let rawAttestationObject: Data - - /// The raw client data JSON. - public let rawClientDataJSON: Data - - public init( - credentialId: Data, - rawAttestationObject: Data, - rawClientDataJSON: Data - ) - { - self.credentialId = credentialId - self.rawAttestationObject = rawAttestationObject - self.rawClientDataJSON = rawClientDataJSON - super.init() - } -} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterMethods+Passkey.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterMethods+Passkey.swift index b97861c53b..b129540131 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterMethods+Passkey.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterMethods+Passkey.swift @@ -26,24 +26,40 @@ import Foundation extension MSALRegisterMethods { - /// Begins passkey (FIDO2/WebAuthn) registration by requesting creation options from the server. + /// Registers a new passkey (FIDO2/WebAuthn) credential. /// - /// The returned `MSALPasskeyRegistrationState` contains the WebAuthn challenge and parameters - /// needed to invoke the platform authenticator. After the user creates the passkey, - /// call `state.complete(attestation:)` to finalize registration. + /// This single call handles the entire flow: + /// 1. Requests creation options from the server. + /// 2. Presents the system passkey sheet to the user. + /// 3. Submits the attestation back to the server. /// - /// - Parameter params: Optional parameters including display name and correlation ID. - /// - Returns: A `Result` containing the registration state (with creation options) or an error. + /// - Parameter params: Parameters including the presentation anchor and optional display name. + /// - Returns: A `Result` containing the registration result or an error. + /// + /// Example: + /// ```swift + /// let params = MSALRegisterPasskeyParams( + /// presentationAnchor: view.window!, + /// displayName: "My iPhone" + /// ) + /// let result = await client.register.passkey(params: params) + /// switch result { + /// case .success(.completed(let method)): + /// print("Registered: \(method.id)") + /// case .failure(let error): + /// print("Failed: \(error.message ?? "")") + /// } + /// ``` public func passkey( - params: MSALRegisterPasskeyParams? = nil - ) async -> Result + params: MSALRegisterPasskeyParams + ) async -> Result { guard let client = client else { let error = MSALNativeCredentialManagementError( type: .generalError, message: "Client was deallocated.", - correlationId: params?.correlationId ?? UUID() + correlationId: params.correlationId ?? UUID() ) return .failure(error) } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterPasskeyParams.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterPasskeyParams.swift index 10965cddf4..84946af89d 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterPasskeyParams.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterPasskeyParams.swift @@ -22,24 +22,39 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import Foundation +import AuthenticationServices +#if os(macOS) +import AppKit +#else +import UIKit +#endif /// Parameters for `client.register.passkey()`. /// -/// No fields are strictly required — the server generates the WebAuthn challenge. +/// The `presentationAnchor` is required — it provides the window in which the system +/// passkey sheet is presented. @objcMembers public class MSALRegisterPasskeyParams: MSALRegisterParams { + /// The window used to present the passkey authorization sheet. + public var presentationAnchor: ASPresentationAnchor + /// Optional human-readable label for the passkey (e.g. "Work YubiKey"). public var displayName: String? - public override init() - { - super.init() - } - - public init(displayName: String? = nil, correlationId: UUID? = nil) + /// Creates passkey registration parameters. + /// + /// - Parameters: + /// - presentationAnchor: The window that will present the system passkey UI. + /// - displayName: Optional friendly name for the passkey. + /// - correlationId: Optional correlation ID for logging/diagnostics. + public init( + presentationAnchor: ASPresentationAnchor, + displayName: String? = nil, + correlationId: UUID? = nil + ) { + self.presentationAnchor = presentationAnchor self.displayName = displayName super.init(correlationId: correlationId) } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index f8bbf93b6e..83885f0a22 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -10,6 +10,11 @@ import MSAL import MSALNativeCredManagment import SwiftUI import AuthenticationServices +#if os(macOS) +import AppKit +#else +import UIKit +#endif /// Main view model that demonstrates the credential management SDK integration. @MainActor @@ -154,64 +159,34 @@ class CredentialManagementViewModel: ObservableObject { isLoading = true Task { @MainActor in - // Step 1: Request creation options from the server - let params = MSALRegisterPasskeyParams(displayName: displayName) - let beginResult = await credClient.register.passkey(params: params) - - switch beginResult { - case .failure(let error): + guard let anchor = Self.resolveAnchor() else + { self.isLoading = false - self.errorMessage = "Passkey registration failed: \(error.message ?? "unknown error")" + self.errorMessage = "No window available." return - case .success(let state): - // Step 2: Use creation options to invoke platform authenticator - let options = state.creationOptions - let provider = ASAuthorizationPlatformPublicKeyCredentialProvider( - relyingPartyIdentifier: options.relyingPartyIdentifier - ) - - let registrationRequest = provider.createCredentialRegistrationRequest( - challenge: options.challenge, - name: options.userName, - userID: options.userId - ) - - let authController = ASAuthorizationController(authorizationRequests: [registrationRequest]) - passkeyDelegate = PasskeyAuthorizationDelegate { [weak self] result in - Task { @MainActor in - guard let self = self else { return } - switch result { - case .success(let credential): - // Step 3: Submit attestation back to the server - let attestation = MSALPasskeyAttestation( - credentialId: credential.credentialID, - rawAttestationObject: credential.rawAttestationObject ?? Data(), - rawClientDataJSON: credential.rawClientDataJSON - ) - let completeResult = await state.complete(attestation: attestation) - switch completeResult { - case .success(let method): - self.isLoading = false - self.statusMessage = "Passkey registered: \(method.displayName ?? "unknown")" - self.listCredentialMethods() - case .failure(let error): - self.isLoading = false - self.errorMessage = "Passkey registration failed: \(error.message ?? "unknown error")" - } - case .failure(let error): - self.isLoading = false - self.errorMessage = "Passkey creation failed: \(error.localizedDescription)" - } - } - } - authController.delegate = passkeyDelegate - authController.presentationContextProvider = passkeyDelegate - authController.performRequests() } + + let params = MSALRegisterPasskeyParams( + presentationAnchor: anchor, + displayName: displayName + ) + + let result = await credClient.register.passkey(params: params) + handleRegistrationResult(result) } } - private var passkeyDelegate: PasskeyAuthorizationDelegate? + private static func resolveAnchor() -> ASPresentationAnchor? + { + #if os(macOS) + return NSApplication.shared.keyWindow + #else + return UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .flatMap { $0.windows } + .first(where: { $0.isKeyWindow }) + #endif + } func submitChallenge(code: String) { guard let state = pendingChallengeState else { diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/PasskeyAuthorizationDelegate.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/PasskeyAuthorizationDelegate.swift deleted file mode 100644 index 6002bc9850..0000000000 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/PasskeyAuthorizationDelegate.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// PasskeyAuthorizationDelegate.swift -// MSALNativeCredManagmentSampleApp -// -// Created by Serhii Demchenko on 2026-05-28. -// - -import AuthenticationServices -#if os(macOS) -import AppKit -#else -import UIKit -#endif - -/// Delegate that handles passkey (platform public key credential) authorization results. -class PasskeyAuthorizationDelegate: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { - - enum PasskeyResult { - case success(ASAuthorizationPlatformPublicKeyCredentialRegistration) - case failure(Error) - } - - private let completion: (PasskeyResult) -> Void - - init(completion: @escaping (PasskeyResult) -> Void) { - self.completion = completion - } - - // MARK: - ASAuthorizationControllerPresentationContextProviding - - func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - #if os(macOS) - return NSApplication.shared.keyWindow ?? NSWindow() - #else - return UIApplication.shared.connectedScenes - .compactMap { $0 as? UIWindowScene } - .flatMap { $0.windows } - .first(where: { $0.isKeyWindow }) ?? UIWindow() - #endif - } - - // MARK: - ASAuthorizationControllerDelegate - - func authorizationController( - controller: ASAuthorizationController, - didCompleteWithAuthorization authorization: ASAuthorization - ) { - guard let credential = authorization.credential - as? ASAuthorizationPlatformPublicKeyCredentialRegistration else { - let error = NSError( - domain: "PasskeyError", - code: -1, - userInfo: [NSLocalizedDescriptionKey: "Unexpected credential type returned."] - ) - completion(.failure(error)) - return - } - completion(.success(credential)) - } - - func authorizationController( - controller: ASAuthorizationController, - didCompleteWithError error: Error - ) { - completion(.failure(error)) - } -} From 46683b42217bc1ddef04340f6f5e5ad1add5603f Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Mon, 1 Jun 2026 19:20:26 -0700 Subject: [PATCH 32/42] refactor new apis. --- .../MSALNativeCredentialMethodsClient+RegisterPasskey.swift | 0 .../passkey/MSALPasskeyAuthorizationHandler.swift | 0 .../MSALNativeCredentialMethodsClient+RegisterPassword.swift | 0 .../phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/{public => internal}/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/{public => internal}/credential_method/passkey/MSALPasskeyAuthorizationHandler.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/{public => internal}/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/{public => internal}/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift (100%) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyAuthorizationHandler.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALPasskeyAuthorizationHandler.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyAuthorizationHandler.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALPasskeyAuthorizationHandler.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift From f0413290b6da9bb84c384aa2c431d888cc97d6f7 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Tue, 2 Jun 2026 14:59:59 -0700 Subject: [PATCH 33/42] hide internal code. --- ...tiveCredentialMethodsClient+Internal.swift | 164 ++++++++++++++++++ .../MSALNativeCredentialMethodsClient.swift | 142 +-------------- 2 files changed, 169 insertions(+), 137 deletions(-) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift new file mode 100644 index 0000000000..1dc1591204 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift @@ -0,0 +1,164 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +extension MSALNativeCredentialMethodsClient +{ + // MARK: - Token Acquisition + + internal func acquireToken( + correlationId: UUID, + completion: @escaping (String?, Error?) -> Void + ) + { + guard let tokenProvider = config.tokenProvider else + { + let error = MSALNativeCredentialManagementError( + type: .invalidConfiguration, + message: "Token provider is not configured.", + correlationId: correlationId + ) + completion(nil, error) + return + } + + // TODO: Replace with actual scopes for credential management API once defined + let scopes = ["openid", "offline_access"] + + tokenProvider.getAccessToken(scopes: scopes) + { accessToken, error in + completion(accessToken, error) + } + } + + // MARK: - Challenge Handling + + internal func submitRegistrationChallenge( + code: String, + continuationToken: String, + correlationId: UUID + ) async -> Result + { + return await withCheckedContinuation + { continuation in + self.operationQueue.async + { [weak self] in + guard let self = self else + { + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "Client was deallocated.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(error)) + return + } + + guard !code.isEmpty else + { + let error = MSALNativeCredentialManagementError( + type: .invalidInput, + message: "Verification code cannot be empty.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(error)) + return + } + + guard let credential = self.pendingRegistrationCredential else + { + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "No pending registration found.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(error)) + return + } + + self.mockCredentialMethods.append(credential) + self.pendingRegistrationCredential = nil + continuation.resume(returning: .success(credential)) + } + } + } + + internal func resendRegistrationChallenge( + continuationToken: String, + correlationId: UUID + ) async -> Result + { + return await withCheckedContinuation + { continuation in + self.operationQueue.async + { [weak self] in + guard let self = self else + { + let error = MSALNativeCredentialManagementError( + type: .generalError, + message: "Client was deallocated.", + correlationId: correlationId + ) + continuation.resume(returning: .failure(error)) + return + } + + let newState = MSALCredentialMethodChallengeState( + sentTo: self.pendingRegistrationCredential?.displayName ?? "***", + channelType: self.pendingRegistrationCredential?.credentialType.rawValue, + codeLength: 6, + continuationToken: "mock-continuation-\(UUID().uuidString.prefix(8))", + client: self, + correlationId: correlationId + ) + continuation.resume(returning: .success(newState)) + } + } + } + + // MARK: - Mock Data + + internal func seedMockData() + { + mockCredentialMethods = [ + MSALPasskeyCredentialMethod( + id: "passkey-001", + displayName: "Security Key (YubiKey 5)", + createdAt: Date(timeIntervalSinceNow: -86400 * 30), + credentialID: "abc123base64", + aaguid: "2fc0579f-8113-47ea-b116-bb5a8db9202a" + ), + MSALPhoneCredentialMethod( + id: "phone-001", + createdAt: Date(timeIntervalSinceNow: -86400 * 60), + phoneNumber: "+1 *** ***-4589" + ), + MSALPasswordCredentialMethod( + id: "password-001", + createdAt: Date(timeIntervalSinceNow: -86400 * 90) + ) + ] + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index 4a7e9e7573..ae30eae924 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -60,28 +60,12 @@ public class MSALNativeCredentialMethodsClient: NSObject { label: "com.microsoft.identity.credentialmanagement", qos: .userInitiated ) - - // Seed with default credential methods for POC - self.mockCredentialMethods = [ - MSALPasskeyCredentialMethod( - id: "passkey-001", - displayName: "Security Key (YubiKey 5)", - createdAt: Date(timeIntervalSinceNow: -86400 * 30), - credentialID: "abc123base64", - aaguid: "2fc0579f-8113-47ea-b116-bb5a8db9202a" - ), - MSALPhoneCredentialMethod( - id: "phone-001", - createdAt: Date(timeIntervalSinceNow: -86400 * 60), - phoneNumber: "+1 *** ***-4589" - ), - MSALPasswordCredentialMethod( - id: "password-001", - createdAt: Date(timeIntervalSinceNow: -86400 * 90) - ) - ] + self.mockCredentialMethods = [] + self.pendingRegistrationCredential = nil super.init() + + seedMockData() } // MARK: - Public: List Credential Methods @@ -136,7 +120,6 @@ public class MSALNativeCredentialMethodsClient: NSObject { return } - // Mock: return current in-memory credential methods continuation.resume(returning: .success(self.mockCredentialMethods)) } } @@ -214,7 +197,6 @@ public class MSALNativeCredentialMethodsClient: NSObject { return } - // Mock: remove credential method from in-memory storage let methodId = credentialMethod.id if let index = self.mockCredentialMethods.firstIndex(where: { $0.id == methodId }) { @@ -235,124 +217,10 @@ public class MSALNativeCredentialMethodsClient: NSObject { } } - // MARK: - Internal: Challenge Handling - - internal func submitRegistrationChallenge( - code: String, - continuationToken: String, - correlationId: UUID - ) async -> Result - { - return await withCheckedContinuation - { continuation in - self.operationQueue.async - { [weak self] in - guard let self = self else - { - let error = MSALNativeCredentialManagementError( - type: .generalError, - message: "Client was deallocated.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(error)) - return - } - - // Mock: accept any non-empty code - guard !code.isEmpty else - { - let error = MSALNativeCredentialManagementError( - type: .invalidInput, - message: "Verification code cannot be empty.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(error)) - return - } - - guard let credential = self.pendingRegistrationCredential else - { - let error = MSALNativeCredentialManagementError( - type: .generalError, - message: "No pending registration found.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(error)) - return - } - - self.mockCredentialMethods.append(credential) - self.pendingRegistrationCredential = nil - continuation.resume(returning: .success(credential)) - } - } - } - - internal func resendRegistrationChallenge( - continuationToken: String, - correlationId: UUID - ) async -> Result - { - return await withCheckedContinuation - { continuation in - self.operationQueue.async - { [weak self] in - guard let self = self else - { - let error = MSALNativeCredentialManagementError( - type: .generalError, - message: "Client was deallocated.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(error)) - return - } - - // Mock: return a new challenge state - let newState = MSALCredentialMethodChallengeState( - sentTo: self.pendingRegistrationCredential?.displayName ?? "***", - channelType: self.pendingRegistrationCredential?.credentialType.rawValue, - codeLength: 6, - continuationToken: "mock-continuation-\(UUID().uuidString.prefix(8))", - client: self, - correlationId: correlationId - ) - continuation.resume(returning: .success(newState)) - } - } - } - - // MARK: - Internal: Properties + // MARK: - Internal: Properties (accessible by extensions) internal let config: MSALNativeCredentialManagementConfig internal let operationQueue: DispatchQueue internal var mockCredentialMethods: [any MSALCredentialMethodProtocol] internal var pendingRegistrationCredential: (any MSALCredentialMethodProtocol)? - - // MARK: - Internal: Token Acquisition - - internal func acquireToken( - correlationId: UUID, - completion: @escaping (String?, Error?) -> Void - ) - { - guard let tokenProvider = config.tokenProvider else - { - let error = MSALNativeCredentialManagementError( - type: .invalidConfiguration, - message: "Token provider is not configured.", - correlationId: correlationId - ) - completion(nil, error) - return - } - - // TODO: Replace with actual scopes for credential management API once defined - let scopes = ["openid", "offline_access"] - - tokenProvider.getAccessToken(scopes: scopes) - { accessToken, error in - completion(accessToken, error) - } - } } From 8e69ba37566d5ad58ef3489a5f782385a65e2c58 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Tue, 2 Jun 2026 15:33:02 -0700 Subject: [PATCH 34/42] use passkey name. --- .../MSALNativeCredentialMethodsClient+RegisterPasskey.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift index c9dff748ef..2678fc7452 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift @@ -71,7 +71,7 @@ extension MSALNativeCredentialMethodsClient } // Step 4: Submit attestation to server (mock) - return submitAttestation(attestation, correlationId: correlationId) + return submitAttestation(attestation, displayName: params.displayName, correlationId: correlationId) } // MARK: - Private Helpers @@ -95,13 +95,15 @@ extension MSALNativeCredentialMethodsClient /// In production, this would POST the attestation and receive the registered credential. private func submitAttestation( _ attestation: MSALPasskeyAttestation, + displayName: String?, correlationId: UUID ) -> Result { let credentialIdString = attestation.credentialId.base64EncodedString() + let resolvedName = displayName ?? "Passkey (\(String(credentialIdString.prefix(8)))...)" let method = MSALPasskeyCredentialMethod( id: "passkey-\(UUID().uuidString.prefix(8))", - displayName: "Passkey (\(String(credentialIdString.prefix(8)))...)", + displayName: resolvedName, createdAt: Date(), credentialID: credentialIdString, aaguid: nil From bd5fae170baff8f6ef07749a8660223f3bd4a34d Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Tue, 2 Jun 2026 16:09:47 -0700 Subject: [PATCH 35/42] fix password entry. --- .../ContentView.swift | 16 ++++++++++++---- .../CredentialManagementViewModel.swift | 8 ++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift index 97e57b9630..8bc5a69536 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift @@ -184,21 +184,29 @@ struct ContentView: View { Text("Password").tag("password") } - TextField(newCredentialType == "passkey" ? "Passkey name" : "Value (phone number)", text: $newCredentialValue) - .textFieldStyle(.roundedBorder) + if newCredentialType == "password" { + SecureField("Password", text: $newCredentialValue) + .textFieldStyle(.roundedBorder) + } else { + TextField(newCredentialType == "passkey" ? "Passkey name" : "Phone number", text: $newCredentialValue) + .textFieldStyle(.roundedBorder) + } Button("Register") { switch newCredentialType { case "passkey": viewModel.registerPasskey(displayName: newCredentialValue.isEmpty ? nil : newCredentialValue) case "password": - viewModel.registerPassword() + viewModel.registerPassword(password: newCredentialValue) default: viewModel.registerPhone(phoneNumber: newCredentialValue) } newCredentialValue = "" } - .disabled(newCredentialType == "phone" && newCredentialValue.isEmpty) + .disabled( + (newCredentialType == "phone" && newCredentialValue.isEmpty) || + (newCredentialType == "password" && newCredentialValue.isEmpty) + ) } } .refreshable { diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 83885f0a22..7ac053d34b 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -135,16 +135,20 @@ class CredentialManagementViewModel: ObservableObject { // MARK: - Register Password - func registerPassword() { + func registerPassword(password: String) { guard let credClient = credClient else { errorMessage = "Credential client not initialized." return } + guard !password.isEmpty else { + errorMessage = "Password cannot be empty." + return + } isLoading = true statusMessage = "Registering password..." errorMessage = nil - let params = MSALRegisterPasswordParams(password: "") + let params = MSALRegisterPasswordParams(password: password) Task { let result = await credClient.register.password(params: params) From b5240005b7de6288f6cae8b11eaf392241676e4c Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Tue, 2 Jun 2026 17:56:37 -0700 Subject: [PATCH 36/42] initial server api implementation. --- ...tiveCredentialMethodsClient+Internal.swift | 260 ++++++++++++----- ...dentialMethodsClient+RegisterPasskey.swift | 270 +++++++++++------- ...entialMethodsClient+RegisterPassword.swift | 116 +++++--- ...redentialMethodsClient+RegisterPhone.swift | 127 ++++---- .../internal/hal/CredentialMethodMapper.swift | 122 ++++++++ .../src/internal/hal/HALResource.swift | 146 ++++++++++ .../CredentialManagementAPIClient.swift | 262 +++++++++++++++++ .../CredentialManagementNetworkClient.swift | 70 +++++ .../CredentialManagementRequestBuilder.swift | 139 +++++++++ .../CredentialManagementResponseMapper.swift | 144 ++++++++++ ...CredentialManagementURLSessionClient.swift | 190 ++++++++++++ .../MSALLogger+CredentialManagement.swift | 99 +++++++ .../network/NetworkProviderAdapter.swift | 58 ++++ .../src/internal/util/Result+Extensions.swift | 18 ++ ...MSALNativeCredentialManagementConfig.swift | 7 + ...eCredentialManagementNetworkProvider.swift | 96 +++++++ .../MSALNativeCredentialMethodsClient.swift | 174 +++++------ .../Configuration.swift | 4 + .../ContentView.swift | 20 ++ .../CredentialManagementViewModel.swift | 25 +- .../MockNetworkProvider.swift | 163 +++++++++++ 21 files changed, 2128 insertions(+), 382 deletions(-) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/hal/CredentialMethodMapper.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/hal/HALResource.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementAPIClient.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClient.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementRequestBuilder.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementResponseMapper.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementURLSessionClient.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/MSALLogger+CredentialManagement.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/NetworkProviderAdapter.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/util/Result+Extensions.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementNetworkProvider.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MockNetworkProvider.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift index 1dc1591204..4a3d22cf05 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift @@ -23,6 +23,7 @@ // THE SOFTWARE. import Foundation +import MSAL extension MSALNativeCredentialMethodsClient { @@ -44,8 +45,7 @@ extension MSALNativeCredentialMethodsClient return } - // TODO: Replace with actual scopes for credential management API once defined - let scopes = ["openid", "offline_access"] + let scopes = ["https://graph.microsoft.com/.default"] tokenProvider.getAccessToken(scopes: scopes) { accessToken, error in @@ -53,112 +53,214 @@ extension MSALNativeCredentialMethodsClient } } - // MARK: - Challenge Handling - - internal func submitRegistrationChallenge( - code: String, - continuationToken: String, + /// Async wrapper around the callback-based token acquisition. + internal func acquireTokenAsync( correlationId: UUID - ) async -> Result + ) async -> Result { return await withCheckedContinuation { continuation in - self.operationQueue.async - { [weak self] in - guard let self = self else - { - let error = MSALNativeCredentialManagementError( - type: .generalError, - message: "Client was deallocated.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(error)) - return - } - - guard !code.isEmpty else + self.acquireToken(correlationId: correlationId) + { accessToken, tokenError in + if let tokenError = tokenError { - let error = MSALNativeCredentialManagementError( - type: .invalidInput, - message: "Verification code cannot be empty.", - correlationId: correlationId + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Failed to acquire access token.", + correlationId: correlationId, + underlyingError: tokenError ) - continuation.resume(returning: .failure(error)) + continuation.resume(returning: .failure(credError)) return } - guard let credential = self.pendingRegistrationCredential else + guard let accessToken = accessToken else { - let error = MSALNativeCredentialManagementError( - type: .generalError, - message: "No pending registration found.", + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Token provider returned nil access token.", correlationId: correlationId ) - continuation.resume(returning: .failure(error)) + continuation.resume(returning: .failure(credError)) return } - self.mockCredentialMethods.append(credential) - self.pendingRegistrationCredential = nil - continuation.resume(returning: .success(credential)) + continuation.resume(returning: .success(accessToken)) } } } - internal func resendRegistrationChallenge( + // MARK: - API Client Access + + /// Returns or creates the internal API client for server communication. + internal func getAPIClient() -> Result + { + if let existing = apiClient + { + return .success(existing) + } + + guard let baseURL = config.baseURL else + { + return .failure(MSALNativeCredentialManagementError( + type: .invalidConfiguration, + message: "baseURL must be set on MSALNativeCredentialManagementConfig." + )) + } + + let client = CredentialManagementAPIClient( + baseURL: baseURL, + networkClient: networkClient + ) + self.apiClient = client + return .success(client) + } + + // MARK: - Challenge Handling + + internal func submitRegistrationChallenge( + code: String, continuationToken: String, correlationId: UUID - ) async -> Result + ) async -> Result { - return await withCheckedContinuation - { continuation in - self.operationQueue.async - { [weak self] in - guard let self = self else - { - let error = MSALNativeCredentialManagementError( - type: .generalError, - message: "Client was deallocated.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(error)) - return - } + guard !code.isEmpty else + { + return .failure(MSALNativeCredentialManagementError( + type: .invalidInput, + message: "Verification code cannot be empty.", + correlationId: correlationId + )) + } - let newState = MSALCredentialMethodChallengeState( - sentTo: self.pendingRegistrationCredential?.displayName ?? "***", - channelType: self.pendingRegistrationCredential?.credentialType.rawValue, - codeLength: 6, - continuationToken: "mock-continuation-\(UUID().uuidString.prefix(8))", - client: self, + // Acquire a fresh token for the activation call + let tokenResult = await acquireTokenAsync(correlationId: correlationId) + guard case .success(let accessToken) = tokenResult else + { + return .failure(tokenResult.failureValue!) + } + + let clientResult = getAPIClient() + guard case .success(let apiClientInstance) = clientResult else + { + if case .failure(let error) = clientResult { return .failure(error) } + fatalError("Unreachable") + } + + // Build the activation body with continuationToken and code + let activationBody: [String: Any] = [ + "continuationToken": continuationToken, + "oob": code + ] + + guard let bodyData = try? JSONSerialization.data(withJSONObject: activationBody) else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Failed to encode activation request body.", + correlationId: correlationId + )) + } + + // Use the activate href stored in the challenge state + // The activate link was captured during enrollment + guard let activateHref = pendingActivateHref else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "No pending activation link found.", + correlationId: correlationId + )) + } + + let result = await apiClientInstance.activateEnrollment( + activateHref: activateHref, + accessToken: accessToken, + body: bodyData, + correlationId: correlationId + ) + + switch result + { + case .success(let halResource): + let json = halResource.properties + guard let method = CredentialMethodMapper.parseMethod(from: json) else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Failed to parse registered method from activation response.", correlationId: correlationId - ) - continuation.resume(returning: .success(newState)) + )) } + self.pendingActivateHref = nil + return .success(method) + + case .failure(let error): + return .failure(error) } } - // MARK: - Mock Data - - internal func seedMockData() + internal func resendRegistrationChallenge( + continuationToken: String, + correlationId: UUID + ) async -> Result { - mockCredentialMethods = [ - MSALPasskeyCredentialMethod( - id: "passkey-001", - displayName: "Security Key (YubiKey 5)", - createdAt: Date(timeIntervalSinceNow: -86400 * 30), - credentialID: "abc123base64", - aaguid: "2fc0579f-8113-47ea-b116-bb5a8db9202a" - ), - MSALPhoneCredentialMethod( - id: "phone-001", - createdAt: Date(timeIntervalSinceNow: -86400 * 60), - phoneNumber: "+1 *** ***-4589" - ), - MSALPasswordCredentialMethod( - id: "password-001", - createdAt: Date(timeIntervalSinceNow: -86400 * 90) + // Acquire token for the re-send call + let tokenResult = await acquireTokenAsync(correlationId: correlationId) + guard case .success(let accessToken) = tokenResult else + { + return .failure(tokenResult.failureValue!) + } + + let clientResult = getAPIClient() + guard case .success(let apiClientInstance) = clientResult else + { + if case .failure(let error) = clientResult { return .failure(error) } + fatalError("Unreachable") + } + + // Re-enroll to get a new challenge (server re-sends OOB code) + guard let pendingType = pendingEnrollmentType else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "No pending enrollment type found for resend.", + correlationId: correlationId + )) + } + + let resendBody: [String: Any] = ["continuationToken": continuationToken] + let bodyData = try? JSONSerialization.data(withJSONObject: resendBody) + + let result = await apiClientInstance.beginEnrollment( + type: pendingType, + accessToken: accessToken, + body: bodyData, + correlationId: correlationId + ) + + switch result + { + case .success(let halResource): + let newContinuationToken = halResource.string(forKey: "continuationToken") ?? continuationToken + + if let activateLink = halResource.link(rel: "activate") + { + self.pendingActivateHref = activateLink.href + } + + let newState = MSALCredentialMethodChallengeState( + sentTo: halResource.string(forKey: "sentTo"), + channelType: halResource.string(forKey: "channelType"), + codeLength: halResource.properties["codeLength"] as? Int, + continuationToken: newContinuationToken, + client: self, + correlationId: correlationId ) - ] + return .success(newState) + + case .failure(let error): + return .failure(error) + } } } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift index 2678fc7452..ff7a3a3277 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift @@ -29,9 +29,9 @@ extension MSALNativeCredentialMethodsClient { /// Performs the full passkey registration flow: /// 1. Acquires an access token. - /// 2. Requests WebAuthn creation options from the server (mocked). + /// 2. Calls beginEnrollment to get WebAuthn creation options from the server. /// 3. Invokes the platform authenticator via ASAuthorization. - /// 4. Submits the attestation back to the server (mocked). + /// 4. Calls activateEnrollment with the attestation. /// 5. Returns the registered credential method. internal func performRegisterPasskey( params: MSALRegisterPasskeyParams @@ -39,127 +39,187 @@ extension MSALNativeCredentialMethodsClient { let correlationId = params.correlationId ?? UUID() + CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "performRegisterPasskey: starting") + // Step 1: Acquire access token let tokenResult = await acquireTokenAsync(correlationId: correlationId) - switch tokenResult + guard case .success(let accessToken) = tokenResult else { - case .failure(let error): - return .failure(error) - case .success: - break + return .failure(tokenResult.failureValue!) } - // Step 2: Request creation options from the server (mock) - let creationOptions = requestCreationOptions(displayName: params.displayName) - - // Step 3: Invoke platform authenticator - let handler = MSALPasskeyAuthorizationHandler(anchor: params.presentationAnchor) - let attestation: MSALPasskeyAttestation - do + // Step 2: Begin enrollment to get creation options from server + switch getAPIClient() { - attestation = try await handler.performRegistration(options: creationOptions) - } - catch - { - let credError = MSALNativeCredentialManagementError( - type: .generalError, - message: "Passkey creation was cancelled or failed.", - correlationId: correlationId, - underlyingError: error + case .failure(let error): + return .failure(error) + case .success(let client): + let enrollResult = await client.beginEnrollment( + type: .passkey, + accessToken: accessToken, + body: nil, + correlationId: correlationId ) - return .failure(credError) - } - // Step 4: Submit attestation to server (mock) - return submitAttestation(attestation, displayName: params.displayName, correlationId: correlationId) - } + guard case .success(let halResource) = enrollResult else + { + return .failure(enrollResult.failureValue!) + } - // MARK: - Private Helpers + // Parse the server response for WebAuthn creation options + guard let publicKeyDict = halResource.properties["publicKey"] as? [String: Any] else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Server did not return publicKey creation options.", + correlationId: correlationId + )) + } - /// Requests WebAuthn creation options from the server. - /// In production, this would be a POST to the credential management API. - private func requestCreationOptions(displayName: String?) -> MSALPasskeyCreationOptions - { - var challengeBytes = [UInt8](repeating: 0, count: 32) - _ = SecRandomCopyBytes(kSecRandomDefault, challengeBytes.count, &challengeBytes) + guard let continuationToken = halResource.string(forKey: "continuationToken") else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Server did not return continuationToken.", + correlationId: correlationId + )) + } - return MSALPasskeyCreationOptions( - challenge: Data(challengeBytes), - userId: Data(UUID().uuidString.utf8), - userName: displayName ?? "user", - relyingPartyIdentifier: "login.microsoft.com" - ) - } + guard let activateLink = halResource.link(rel: "activate") else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Server did not return activate link.", + correlationId: correlationId + )) + } - /// Submits the attestation to the server for validation. - /// In production, this would POST the attestation and receive the registered credential. - private func submitAttestation( - _ attestation: MSALPasskeyAttestation, - displayName: String?, - correlationId: UUID - ) -> Result - { - let credentialIdString = attestation.credentialId.base64EncodedString() - let resolvedName = displayName ?? "Passkey (\(String(credentialIdString.prefix(8)))...)" - let method = MSALPasskeyCredentialMethod( - id: "passkey-\(UUID().uuidString.prefix(8))", - displayName: resolvedName, - createdAt: Date(), - credentialID: credentialIdString, - aaguid: nil - ) - mockCredentialMethods.append(method) - return .success(.completed(method)) - } + // Parse creation options from server response + let creationOptions = parseCreationOptions(from: publicKeyDict) - /// Async wrapper around the token acquisition callback. - private func acquireTokenAsync( - correlationId: UUID - ) async -> Result - { - return await withCheckedContinuation - { continuation in - self.operationQueue.async - { [weak self] in - guard let self = self else + // Step 3: Invoke platform authenticator + let handler = MSALPasskeyAuthorizationHandler(anchor: params.presentationAnchor) + let attestation: MSALPasskeyAttestation + do + { + attestation = try await handler.performRegistration(options: creationOptions) + } + catch + { + let credError = MSALNativeCredentialManagementError( + type: .generalError, + message: "Passkey creation was cancelled or failed.", + correlationId: correlationId, + underlyingError: error + ) + return .failure(credError) + } + + // Step 4: Submit attestation to server via activate link + let activationBody: [String: Any] = [ + "continuationToken": continuationToken, + "displayName": params.displayName ?? "Passkey", + "publicKeyCredential": [ + "id": attestation.credentialId.base64EncodedString(), + "response": [ + "attestationObject": attestation.rawAttestationObject.base64EncodedString(), + "clientDataJSON": attestation.rawClientDataJSON.base64EncodedString() + ] + ] + ] + + guard let bodyData = try? JSONSerialization.data(withJSONObject: activationBody) else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Failed to encode passkey activation body.", + correlationId: correlationId + )) + } + + let activateResult = await client.activateEnrollment( + activateHref: activateLink.href, + accessToken: accessToken, + body: bodyData, + correlationId: correlationId + ) + + switch activateResult + { + case .success(let resultResource): + guard let method = CredentialMethodMapper.parseMethod(from: resultResource.properties) else { - let error = MSALNativeCredentialManagementError( + return .failure(MSALNativeCredentialManagementError( type: .generalError, - message: "Client was deallocated.", + message: "Failed to parse registered passkey from activation response.", correlationId: correlationId - ) - continuation.resume(returning: .failure(error)) - return - } - - self.acquireToken(correlationId: correlationId) - { accessToken, tokenError in - if let tokenError = tokenError - { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Failed to acquire access token for passkey registration.", - correlationId: correlationId, - underlyingError: tokenError - ) - continuation.resume(returning: .failure(credError)) - return - } - - guard let accessToken = accessToken else - { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Token provider returned nil access token.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(credError)) - return - } - - continuation.resume(returning: .success(accessToken)) + )) } + CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "performRegisterPasskey: completed") + return .success(.completed(method)) + case .failure(let error): + return .failure(error) } } } + + // MARK: - Private Helpers + + /// Parses the server-provided publicKey object into local creation options. + private func parseCreationOptions(from publicKeyDict: [String: Any]) -> MSALPasskeyCreationOptions + { + let challenge: Data + if let challengeString = publicKeyDict["challenge"] as? String, + let decoded = Data(base64Encoded: challengeString) + { + challenge = decoded + } + else + { + var bytes = [UInt8](repeating: 0, count: 32) + _ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) + challenge = Data(bytes) + } + + let userId: Data + if let userDict = publicKeyDict["user"] as? [String: Any], + let idString = userDict["id"] as? String, + let decoded = Data(base64Encoded: idString) + { + userId = decoded + } + else + { + userId = Data(UUID().uuidString.utf8) + } + + let userName: String + if let userDict = publicKeyDict["user"] as? [String: Any], + let name = userDict["name"] as? String + { + userName = name + } + else + { + userName = "user" + } + + let rpId: String + if let rpDict = publicKeyDict["rp"] as? [String: Any], + let id = rpDict["id"] as? String + { + rpId = id + } + else + { + rpId = "login.microsoft.com" + } + + return MSALPasskeyCreationOptions( + challenge: challenge, + userId: userId, + userName: userName, + relyingPartyIdentifier: rpId + ) + } } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift index 2179135920..214bb90c74 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift @@ -32,54 +32,88 @@ extension MSALNativeCredentialMethodsClient { let correlationId = params.correlationId ?? UUID() - return await withCheckedContinuation - { continuation in - self.operationQueue.async - { [weak self] in - guard let self = self else - { - let error = MSALNativeCredentialManagementError( - type: .generalError, - message: "Client was deallocated.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(error)) - return - } + CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "performRegisterPassword: starting") - self.acquireToken(correlationId: correlationId) - { accessToken, tokenError in - if let tokenError = tokenError - { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Failed to acquire access token for registering credential method.", - correlationId: correlationId, - underlyingError: tokenError - ) - continuation.resume(returning: .failure(credError)) - return - } + // Acquire access token + let tokenResult = await acquireTokenAsync(correlationId: correlationId) + guard case .success(let accessToken) = tokenResult else + { + return .failure(tokenResult.failureValue!) + } + + switch getAPIClient() + { + case .failure(let error): + return .failure(error) + case .success(let client): + // Build the enrollment body with password + let enrollBody: [String: Any] = ["password": params.password] + guard let bodyData = try? JSONSerialization.data(withJSONObject: enrollBody) else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Failed to encode password enrollment body.", + correlationId: correlationId + )) + } + + let enrollResult = await client.beginEnrollment( + type: .password, + accessToken: accessToken, + body: bodyData, + correlationId: correlationId + ) - guard accessToken != nil else + switch enrollResult + { + case .success(let halResource): + let state = halResource.string(forKey: "state") + + // Password registration typically completes in one step + if state == "completed" || halResource.link(rel: "activate") == nil + { + guard let method = CredentialMethodMapper.parseMethod(from: halResource.properties) else { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Token provider returned nil access token.", - correlationId: correlationId + // If server returned state=completed but no parseable method, + // create a password method from what we know + let fallbackMethod = MSALPasswordCredentialMethod( + id: halResource.string(forKey: "id") ?? UUID().uuidString, + createdAt: Date() ) - continuation.resume(returning: .failure(credError)) - return + return .success(.completed(fallbackMethod)) } + CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "performRegisterPassword: completed") + return .success(.completed(method)) + } - // Mock: password registration completes immediately - let method = MSALPasswordCredentialMethod( - id: "password-\(UUID().uuidString.prefix(8))", - createdAt: Date() - ) - self.mockCredentialMethods.append(method) - continuation.resume(returning: .success(.completed(method))) + // If server requires activation (unlikely for password but handle gracefully) + guard let continuationToken = halResource.string(forKey: "continuationToken") else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Server did not return continuationToken for password enrollment.", + correlationId: correlationId + )) } + + if let activateLink = halResource.link(rel: "activate") + { + self.pendingActivateHref = activateLink.href + } + self.pendingEnrollmentType = .password + + let challengeState = MSALCredentialMethodChallengeState( + sentTo: halResource.string(forKey: "sentTo"), + channelType: halResource.string(forKey: "channelType"), + codeLength: halResource.properties["codeLength"] as? Int, + continuationToken: continuationToken, + client: self, + correlationId: correlationId + ) + return .success(.challengeRequired(challengeState)) + + case .failure(let error): + return .failure(error) } } } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift index 7c01a81758..150f075d4a 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift @@ -32,63 +32,92 @@ extension MSALNativeCredentialMethodsClient { let correlationId = params.correlationId ?? UUID() - return await withCheckedContinuation - { continuation in - self.operationQueue.async - { [weak self] in - guard let self = self else - { - let error = MSALNativeCredentialManagementError( - type: .generalError, - message: "Client was deallocated.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(error)) - return - } + CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "performRegisterPhoneNumber: starting") - self.acquireToken(correlationId: correlationId) - { accessToken, tokenError in - if let tokenError = tokenError - { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Failed to acquire access token for registering credential method.", - correlationId: correlationId, - underlyingError: tokenError - ) - continuation.resume(returning: .failure(credError)) - return - } + // Acquire access token + let tokenResult = await acquireTokenAsync(correlationId: correlationId) + guard case .success(let accessToken) = tokenResult else + { + return .failure(tokenResult.failureValue!) + } + + switch getAPIClient() + { + case .failure(let error): + return .failure(error) + case .success(let client): + // Build the enrollment body with phone number + let enrollBody: [String: Any] = ["phoneNumber": params.phoneNumber] + guard let bodyData = try? JSONSerialization.data(withJSONObject: enrollBody) else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Failed to encode phone enrollment body.", + correlationId: correlationId + )) + } + + let enrollResult = await client.beginEnrollment( + type: .phone, + accessToken: accessToken, + body: bodyData, + correlationId: correlationId + ) + + switch enrollResult + { + case .success(let halResource): + let state = halResource.string(forKey: "state") - guard accessToken != nil else + // If server returned state=completed, the method is already registered + if state == "completed" + { + guard let method = CredentialMethodMapper.parseMethod(from: halResource.properties) else { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Token provider returned nil access token.", + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Failed to parse phone method from enrollment response.", correlationId: correlationId - ) - continuation.resume(returning: .failure(credError)) - return + )) } + return .success(.completed(method)) + } - // Mock: phone registration requires OTP challenge - let method = MSALPhoneCredentialMethod( - id: "phone-\(UUID().uuidString.prefix(8))", - createdAt: Date(), - phoneNumber: params.phoneNumber - ) - let challengeState = MSALCredentialMethodChallengeState( - sentTo: params.phoneNumber, - channelType: MSALCredentialType.phone.rawValue, - codeLength: 6, - continuationToken: "mock-continuation-\(UUID().uuidString.prefix(8))", - client: self, + // Otherwise, server sent a challenge (OTP to the phone) + guard let continuationToken = halResource.string(forKey: "continuationToken") else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Server did not return continuationToken for phone enrollment.", correlationId: correlationId - ) - self.pendingRegistrationCredential = method - continuation.resume(returning: .success(.challengeRequired(challengeState))) + )) + } + + // Store the activate link for later submission + if let activateLink = halResource.link(rel: "activate") + { + self.pendingActivateHref = activateLink.href } + self.pendingEnrollmentType = .phone + + let challengeState = MSALCredentialMethodChallengeState( + sentTo: halResource.string(forKey: "sentTo") ?? params.phoneNumber, + channelType: halResource.string(forKey: "channelType") ?? "sms", + codeLength: halResource.properties["codeLength"] as? Int ?? 6, + continuationToken: continuationToken, + client: self, + correlationId: correlationId + ) + + CredentialManagementLogger.log( + level: .info, + correlationId: correlationId, + message: "performRegisterPhoneNumber: challenge sent" + ) + return .success(.challengeRequired(challengeState)) + + case .failure(let error): + return .failure(error) } } } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/hal/CredentialMethodMapper.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/hal/CredentialMethodMapper.swift new file mode 100644 index 0000000000..b10c42dc66 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/hal/CredentialMethodMapper.swift @@ -0,0 +1,122 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Maps server method types to SDK `MSALCredentialType` values. +/// +/// The server uses different type identifiers (e.g., "fido") than the SDK +/// (e.g., `.passkey`). This mapper handles the bidirectional translation. +internal struct CredentialMethodMapper +{ + // MARK: - Server type → SDK type mapping + + /// Maps a server `type` string to an `MSALCredentialType`. + static func credentialType(fromServerType serverType: String) -> MSALCredentialType? + { + switch serverType + { + case "fido": + return .passkey + case "phone", "sms": + return .phone + case "password": + return .password + default: + return nil + } + } + + /// Maps an `MSALCredentialType` to the server's `type` string for API paths. + static func serverType(from credentialType: MSALCredentialType) -> String + { + switch credentialType + { + case .passkey: + return "fido" + case .phone: + return "phone" + case .password: + return "password" + default: + return credentialType.rawValue + } + } + + // MARK: - HAL JSON → Credential Method + + /// Parses a single credential method from a HAL-embedded method JSON object. + /// + /// - Parameter json: The method JSON from `_embedded.methods[]`. + /// - Returns: A concrete `MSALCredentialMethodProtocol` instance, or nil for unknown types. + static func parseMethod(from json: [String: Any]) -> (any MSALCredentialMethodProtocol)? + { + guard let serverType = json["type"] as? String, + let id = json["id"] as? String else + { + return nil + } + + let displayName = json["displayName"] as? String + + switch serverType + { + case "fido": + return MSALPasskeyCredentialMethod( + id: id, + displayName: displayName, + createdAt: nil, + credentialID: nil, + aaguid: json["aaGuid"] as? String + ) + + case "phone", "sms": + return MSALPhoneCredentialMethod( + id: id, + createdAt: nil, + phoneNumber: displayName + ) + + case "password": + return MSALPasswordCredentialMethod( + id: id, + createdAt: nil + ) + + default: + // Unknown method type — skip for forward compatibility + return nil + } + } + + /// Parses the list of credential methods from a `GET /me/methods` HAL response. + /// + /// - Parameter halResource: The parsed HAL resource. + /// - Returns: Array of credential method instances. + static func parseMethods(from halResource: HALResource) -> [any MSALCredentialMethodProtocol] + { + let methodsJson = halResource.embeddedResources(rel: "methods") + return methodsJson.compactMap { parseMethod(from: $0) } + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/hal/HALResource.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/hal/HALResource.swift new file mode 100644 index 0000000000..e08a48a9d7 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/hal/HALResource.swift @@ -0,0 +1,146 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents a HAL Link Object as defined by the JSON-HAL specification. +/// +/// See: https://www.ietf.org/archive/id/draft-kelly-json-hal-11.html +internal struct HALLink +{ + /// The URI of the linked resource. + let href: String + + /// Secondary key distinguishing links within the same relation. + let name: String? + + /// Whether `href` is a URI Template (RFC 6570). + let templated: Bool + + init(href: String, name: String? = nil, templated: Bool = false) + { + self.href = href + self.name = name + self.templated = templated + } + + /// Parses a HAL Link Object from a JSON dictionary. + init?(json: [String: Any]) + { + guard let href = json["href"] as? String else { return nil } + self.href = href + self.name = json["name"] as? String + self.templated = json["templated"] as? Bool ?? false + } +} + +/// Generic parser for HAL+JSON documents. +/// +/// Handles extraction of `_links` and `_embedded` sections, +/// and provides typed accessors for common HAL patterns. +internal struct HALResource +{ + /// The raw JSON properties (excluding `_links` and `_embedded`). + let properties: [String: Any] + + /// All links keyed by relation type. + let links: [String: [HALLink]] + + /// All embedded resources keyed by relation type. + let embedded: [String: [[String: Any]]] + + /// Parses a HAL resource from a JSON dictionary. + init(json: [String: Any]) + { + var props = json + var parsedLinks: [String: [HALLink]] = [:] + var parsedEmbedded: [String: [[String: Any]]] = [:] + + // Parse _links + if let linksJson = json["_links"] as? [String: Any] + { + for (rel, value) in linksJson + { + if rel == "curies" { continue } + + if let linkDict = value as? [String: Any], let link = HALLink(json: linkDict) + { + parsedLinks[rel] = [link] + } + else if let linkArray = value as? [[String: Any]] + { + parsedLinks[rel] = linkArray.compactMap { HALLink(json: $0) } + } + } + props.removeValue(forKey: "_links") + } + + // Parse _embedded + if let embeddedJson = json["_embedded"] as? [String: Any] + { + for (rel, value) in embeddedJson + { + if let array = value as? [[String: Any]] + { + parsedEmbedded[rel] = array + } + else if let single = value as? [String: Any] + { + parsedEmbedded[rel] = [single] + } + } + props.removeValue(forKey: "_embedded") + } + + self.properties = props + self.links = parsedLinks + self.embedded = parsedEmbedded + } + + // MARK: - Accessors + + /// Returns a single link for the given relation, or nil if not present. + func link(rel: String) -> HALLink? + { + return links[rel]?.first + } + + /// Returns all links for the given relation. + func allLinks(rel: String) -> [HALLink] + { + return links[rel] ?? [] + } + + /// Returns embedded resources for the given relation. + func embeddedResources(rel: String) -> [[String: Any]] + { + return embedded[rel] ?? [] + } + + /// Returns a string property value. + func string(forKey key: String) -> String? + { + return properties[key] as? String + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementAPIClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementAPIClient.swift new file mode 100644 index 0000000000..53543d0c6c --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementAPIClient.swift @@ -0,0 +1,262 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import MSAL + +/// Internal API client that orchestrates network calls and HAL response parsing +/// for the credential management service. +internal final class CredentialManagementAPIClient +{ + private let networkClient: CredentialManagementNetworkClient + private let baseURL: URL + + private static let methodsPath = "/api/v1.0/me/methods" + + init(baseURL: URL, networkClient: CredentialManagementNetworkClient) + { + self.baseURL = baseURL + self.networkClient = networkClient + } + + // MARK: - List Methods + + func listMethods( + accessToken: String, + correlationId: UUID + ) async -> Result<[any MSALCredentialMethodProtocol], MSALNativeCredentialManagementError> + { + let builder = CredentialManagementRequestBuilder( + baseURL: baseURL, + accessToken: accessToken, + correlationId: correlationId + ) + + let request: CredentialManagementRequest + switch builder.buildGET(path: Self.methodsPath) + { + case .success(let r): request = r + case .failure(let e): return .failure(e) + } + + CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "Credential management: listing methods") + + let response: CredentialManagementResponse + do + { + response = try await networkClient.perform(request: request) + } + catch + { + return .failure(MSALNativeCredentialManagementError( + type: .networkError, + message: "Network request failed for listing credential methods.", + correlationId: correlationId, + underlyingError: error + )) + } + + if let mappedError = CredentialManagementResponseMapper.mapError(from: response, correlationId: correlationId) + { + return .failure(mappedError) + } + + let json: [String: Any] + switch CredentialManagementResponseMapper.decodeJSON(from: response, correlationId: correlationId) + { + case .success(let j): json = j + case .failure(let e): return .failure(e) + } + + let halResource = HALResource(json: json) + let methods = CredentialMethodMapper.parseMethods(from: halResource) + + CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "Credential management: listed \(methods.count) method(s)") + + return .success(methods) + } + + // MARK: - Begin Enrollment + + func beginEnrollment( + type: MSALCredentialType, + accessToken: String, + body: Data?, + correlationId: UUID + ) async -> Result + { + let serverType = CredentialMethodMapper.serverType(from: type) + let path = "\(Self.methodsPath)/\(serverType)" + + let builder = CredentialManagementRequestBuilder( + baseURL: baseURL, + accessToken: accessToken, + correlationId: correlationId + ) + + let request: CredentialManagementRequest + switch builder.buildPOST(path: path, body: body) + { + case .success(let r): request = r + case .failure(let e): return .failure(e) + } + + CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "Credential management: beginning enrollment for type '\(serverType)'") + + let response: CredentialManagementResponse + do + { + response = try await networkClient.perform(request: request) + } + catch + { + return .failure(MSALNativeCredentialManagementError( + type: .networkError, + message: "Network request failed for enrollment.", + correlationId: correlationId, + underlyingError: error + )) + } + + if let mappedError = CredentialManagementResponseMapper.mapError(from: response, correlationId: correlationId) + { + return .failure(mappedError) + } + + let json: [String: Any] + switch CredentialManagementResponseMapper.decodeJSON(from: response, correlationId: correlationId) + { + case .success(let j): json = j + case .failure(let e): return .failure(e) + } + + return .success(HALResource(json: json)) + } + + // MARK: - Activate Enrollment + + func activateEnrollment( + activateHref: String, + accessToken: String, + body: Data, + correlationId: UUID + ) async -> Result + { + let builder = CredentialManagementRequestBuilder( + baseURL: baseURL, + accessToken: accessToken, + correlationId: correlationId + ) + + let request: CredentialManagementRequest + switch builder.buildPOST(path: activateHref, body: body) + { + case .success(let r): request = r + case .failure(let e): return .failure(e) + } + + CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "Credential management: activating enrollment") + + let response: CredentialManagementResponse + do + { + response = try await networkClient.perform(request: request) + } + catch + { + return .failure(MSALNativeCredentialManagementError( + type: .networkError, + message: "Network request failed for activation.", + correlationId: correlationId, + underlyingError: error + )) + } + + if let mappedError = CredentialManagementResponseMapper.mapError(from: response, correlationId: correlationId) + { + return .failure(mappedError) + } + + let json: [String: Any] + switch CredentialManagementResponseMapper.decodeJSON(from: response, correlationId: correlationId) + { + case .success(let j): json = j + case .failure(let e): return .failure(e) + } + + return .success(HALResource(json: json)) + } + + // MARK: - Delete Method + + func deleteMethod( + type: MSALCredentialType, + methodId: String, + accessToken: String, + correlationId: UUID + ) async -> Result + { + let serverType = CredentialMethodMapper.serverType(from: type) + let path = "\(Self.methodsPath)/\(serverType)/\(methodId)" + + let builder = CredentialManagementRequestBuilder( + baseURL: baseURL, + accessToken: accessToken, + correlationId: correlationId + ) + + let request: CredentialManagementRequest + switch builder.buildDELETE(path: path) + { + case .success(let r): request = r + case .failure(let e): return .failure(e) + } + + CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "Credential management: deleting method of type '\(serverType)'") + + let response: CredentialManagementResponse + do + { + response = try await networkClient.perform(request: request) + } + catch + { + return .failure(MSALNativeCredentialManagementError( + type: .networkError, + message: "Network request failed for deletion.", + correlationId: correlationId, + underlyingError: error + )) + } + + if let mappedError = CredentialManagementResponseMapper.mapError(from: response, correlationId: correlationId) + { + return .failure(mappedError) + } + + CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "Credential management: method deleted successfully") + + return .success(()) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClient.swift new file mode 100644 index 0000000000..0d363916bb --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClient.swift @@ -0,0 +1,70 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Protocol defining the HTTP transport interface for credential management API calls. +/// +/// This protocol enables injection of mock implementations for unit testing. +internal protocol CredentialManagementNetworkClient +{ + /// Performs an HTTP request and returns the raw response. + /// + /// - Parameter request: The request to execute. + /// - Returns: The response containing status code, headers, and body data. + /// - Throws: Network-level errors (connectivity, timeout). + func perform(request: CredentialManagementRequest) async throws -> CredentialManagementResponse +} + +/// Represents an HTTP request to the credential management API. +internal struct CredentialManagementRequest +{ + let url: URL + let method: HTTPMethod + let headers: [String: String] + let body: Data? + + internal enum HTTPMethod: String + { + case get = "GET" + case post = "POST" + case delete = "DELETE" + } + + init(url: URL, method: HTTPMethod, headers: [String: String] = [:], body: Data? = nil) + { + self.url = url + self.method = method + self.headers = headers + self.body = body + } +} + +/// Represents an HTTP response from the credential management API. +internal struct CredentialManagementResponse +{ + let statusCode: Int + let headers: [String: String] + let data: Data? +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementRequestBuilder.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementRequestBuilder.swift new file mode 100644 index 0000000000..dcf3ea4273 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementRequestBuilder.swift @@ -0,0 +1,139 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Builds authenticated HTTP requests for the credential management API. +/// +/// Ensures consistent headers (Authorization, Accept, Content-Type, correlation ID) +/// and validates that all target URLs belong to the trusted base URL. +internal struct CredentialManagementRequestBuilder +{ + private let baseURL: URL + private let accessToken: String + private let correlationId: UUID + + init(baseURL: URL, accessToken: String, correlationId: UUID) + { + self.baseURL = baseURL + self.accessToken = accessToken + self.correlationId = correlationId + } + + // MARK: - Request Factories + + func buildGET(path: String) -> Result + { + guard let url = resolveURL(path: path) else + { + return .failure(untrustedURLError(path: path)) + } + + return .success(CredentialManagementRequest( + url: url, + method: .get, + headers: commonHeaders() + )) + } + + func buildPOST(path: String, body: Data?) -> Result + { + guard let url = resolveURL(path: path) else + { + return .failure(untrustedURLError(path: path)) + } + + var headers = commonHeaders() + if body != nil + { + headers["Content-Type"] = "application/json" + } + + return .success(CredentialManagementRequest( + url: url, + method: .post, + headers: headers, + body: body + )) + } + + func buildDELETE(path: String) -> Result + { + guard let url = resolveURL(path: path) else + { + return .failure(untrustedURLError(path: path)) + } + + return .success(CredentialManagementRequest( + url: url, + method: .delete, + headers: commonHeaders() + )) + } + + // MARK: - URL Validation + + /// Resolves a path (absolute or relative) against the base URL. + /// Returns nil if the resolved URL does not belong to the trusted base. + func resolveURL(path: String) -> URL? + { + let resolvedURL: URL + + if path.hasPrefix("http://") || path.hasPrefix("https://") + { + guard let url = URL(string: path) else { return nil } + resolvedURL = url + } + else + { + guard let url = URL(string: path, relativeTo: baseURL)?.absoluteURL else { return nil } + resolvedURL = url + } + + guard resolvedURL.scheme == "https" else { return nil } + guard resolvedURL.host == baseURL.host else { return nil } + + return resolvedURL + } + + // MARK: - Private + + private func commonHeaders() -> [String: String] + { + return [ + "Authorization": "Bearer \(accessToken)", + "Accept": "application/hal+json", + "client-request-id": correlationId.uuidString + ] + } + + private func untrustedURLError(path: String) -> MSALNativeCredentialManagementError + { + return MSALNativeCredentialManagementError( + type: .generalError, + message: "URL validation failed: '\(path)' does not belong to the trusted service.", + correlationId: correlationId + ) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementResponseMapper.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementResponseMapper.swift new file mode 100644 index 0000000000..e45d945837 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementResponseMapper.swift @@ -0,0 +1,144 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Maps HTTP response status codes to `MSALNativeCredentialManagementError`. +/// +/// Handles success codes (200, 201, 204) and maps error codes to +/// appropriate `MSALNativeCredentialManagementErrorType` values. +internal struct CredentialManagementResponseMapper +{ + /// Validates that the response indicates success, or maps to a typed error. + /// + /// - Parameters: + /// - response: The HTTP response. + /// - correlationId: The request correlation ID for diagnostics. + /// - Returns: nil if the response indicates success, or an error describing the failure. + static func mapError( + from response: CredentialManagementResponse, + correlationId: UUID + ) -> MSALNativeCredentialManagementError? + { + switch response.statusCode + { + case 200, 201, 204: + return nil + + case 401: + return MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Server returned 401 Unauthorized. The access token may be expired or invalid.", + correlationId: correlationId + ) + + case 403: + return MSALNativeCredentialManagementError( + type: .forbidden, + message: "Server returned 403 Forbidden. The user may lack the required permissions or recent MFA.", + correlationId: correlationId + ) + + case 404: + return MSALNativeCredentialManagementError( + type: .notFound, + message: "Server returned 404 Not Found. The credential method does not exist.", + correlationId: correlationId + ) + + case 409: + return MSALNativeCredentialManagementError( + type: .conflict, + message: "Server returned 409 Conflict. The credential method may already be registered.", + correlationId: correlationId + ) + + case 429: + return MSALNativeCredentialManagementError( + type: .networkError, + message: "Server returned 429 Too Many Requests. Please try again later.", + correlationId: correlationId + ) + + default: + let message: String + if (500...599).contains(response.statusCode) + { + message = "Server returned \(response.statusCode). A server-side error occurred." + } + else + { + message = "Server returned unexpected status code \(response.statusCode)." + } + return MSALNativeCredentialManagementError( + type: .generalError, + message: message, + correlationId: correlationId + ) + } + } + + /// Decodes JSON body from a successful response. + /// + /// - Parameters: + /// - response: The HTTP response with body data. + /// - correlationId: The request correlation ID. + /// - Returns: Decoded dictionary or an error. + static func decodeJSON( + from response: CredentialManagementResponse, + correlationId: UUID + ) -> Result<[String: Any], MSALNativeCredentialManagementError> + { + guard let data = response.data, !data.isEmpty else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Response body is empty.", + correlationId: correlationId + )) + } + + do + { + guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Response body is not a valid JSON object.", + correlationId: correlationId + )) + } + return .success(json) + } + catch + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Failed to parse response JSON.", + correlationId: correlationId, + underlyingError: error + )) + } + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementURLSessionClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementURLSessionClient.swift new file mode 100644 index 0000000000..fcd99a39d1 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementURLSessionClient.swift @@ -0,0 +1,190 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import MSAL + +/// Concrete implementation of `CredentialManagementNetworkClient` using `URLSession`. +/// +/// Modeled after `MSIDHttpRequest` patterns from IdentityCore: +/// - Configurable retry logic (`retryCount`, `retryInterval`) +/// - Configurable request timeout +/// - Request interceptor support for header injection (shared with MSAL) +/// - Retry on transient failures (5xx, timeout, network errors) +internal final class CredentialManagementURLSessionClient: CredentialManagementNetworkClient +{ + // MARK: - Configuration (mirrors MSIDHttpRequest properties) + + /// Number of retry attempts for transient failures. Default is 1. + var retryCount: Int = 1 + + /// Interval between retry attempts in seconds. Default is 0.5. + var retryInterval: TimeInterval = 0.5 + + /// Request timeout in seconds. Default is 30. + var requestTimeoutInterval: TimeInterval = 30 + + /// Optional request interceptor shared with MSAL for injecting custom headers. + var requestInterceptor: MSALNativeAuthRequestInterceptor? + + // MARK: - Private + + private let urlSession: URLSession + + init(urlSession: URLSession = .shared) + { + self.urlSession = urlSession + } + + convenience init( + requestInterceptor: MSALNativeAuthRequestInterceptor?, + retryCount: Int = 1, + retryInterval: TimeInterval = 0.5, + requestTimeoutInterval: TimeInterval = 30 + ) + { + self.init(urlSession: .shared) + self.requestInterceptor = requestInterceptor + self.retryCount = retryCount + self.retryInterval = retryInterval + self.requestTimeoutInterval = requestTimeoutInterval + } + + // MARK: - CredentialManagementNetworkClient + + func perform(request: CredentialManagementRequest) async throws -> CredentialManagementResponse + { + var urlRequest = try await buildURLRequest(from: request) + urlRequest.timeoutInterval = requestTimeoutInterval + + var lastError: Error? + let maxAttempts = 1 + retryCount + + for attempt in 0.. 0 + { + CredentialManagementLogger.log( + level: .info, + message: "Retrying request (attempt \(attempt + 1)/\(maxAttempts)) after \(retryInterval)s" + ) + try await Task.sleep(nanoseconds: UInt64(retryInterval * 1_000_000_000)) + } + + do + { + let (data, response) = try await urlSession.data(for: urlRequest) + + guard let httpResponse = response as? HTTPURLResponse else + { + throw URLError(.badServerResponse) + } + + let responseHeaders = httpResponse.allHeaderFields.reduce(into: [String: String]()) + { result, pair in + if let key = pair.key as? String, let value = pair.value as? String + { + result[key] = value + } + } + + let credResponse = CredentialManagementResponse( + statusCode: httpResponse.statusCode, + headers: responseHeaders, + data: data + ) + + // Retry on transient server errors (5xx) + if isRetryableStatusCode(httpResponse.statusCode) && attempt < maxAttempts - 1 + { + lastError = URLError(.badServerResponse) + continue + } + + return credResponse + } + catch + { + lastError = error + + // Only retry on transient/network errors + if !isRetryableError(error) || attempt >= maxAttempts - 1 + { + throw error + } + } + } + + throw lastError ?? URLError(.unknown) + } + + // MARK: - Private Helpers + + private func buildURLRequest(from request: CredentialManagementRequest) async throws -> URLRequest + { + var urlRequest = URLRequest(url: request.url) + urlRequest.httpMethod = request.method.rawValue + urlRequest.httpBody = request.body + + for (key, value) in request.headers + { + urlRequest.setValue(value, forHTTPHeaderField: key) + } + + // Apply interceptor headers (mirrors MSIDHttpRequest's requestInterceptor pattern) + if let interceptor = requestInterceptor + { + let additionalHeaders = await withCheckedContinuation + { continuation in + interceptor.addAdditionalHeaderFields(request.url) + { headers in + continuation.resume(returning: headers ?? [:]) + } + } + + for (key, value) in additionalHeaders + { + urlRequest.setValue(value, forHTTPHeaderField: key) + } + } + + return urlRequest + } + + private func isRetryableStatusCode(_ statusCode: Int) -> Bool + { + return (500...599).contains(statusCode) || statusCode == 429 + } + + private func isRetryableError(_ error: Error) -> Bool + { + let nsError = error as NSError + let retryableCodes: [Int] = [ + NSURLErrorTimedOut, + NSURLErrorNetworkConnectionLost, + NSURLErrorNotConnectedToInternet + ] + return nsError.domain == NSURLErrorDomain && retryableCodes.contains(nsError.code) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/MSALLogger+CredentialManagement.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/MSALLogger+CredentialManagement.swift new file mode 100644 index 0000000000..dad97d04b4 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/MSALLogger+CredentialManagement.swift @@ -0,0 +1,99 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import os +import MSAL + +/// Lightweight logger that reuses MSAL's log level configuration. +/// +/// Uses Apple's unified logging (`os.Logger`) for output, while respecting +/// the MSAL log level set on `MSALGlobalConfig.loggerConfig.logLevel`. +/// +/// **Privacy:** This logger NEVER logs tokens, phone numbers, email addresses, +/// credential IDs, continuation tokens, or full HAL payloads. +internal enum CredentialManagementLogger +{ + private static let logger = os.Logger( + subsystem: "com.microsoft.identity.client", + category: "CredentialManagement" + ) + + /// Log a non-PII message at the specified level. + static func log( + level: MSALLogLevel, + correlationId: UUID? = nil, + message: String + ) + { + let loggerConfig = MSALGlobalConfig.loggerConfig + guard level.rawValue <= loggerConfig.logLevel.rawValue else { return } + + let formatted: String + if let correlationId = correlationId + { + formatted = "[CredMgmt][\(correlationId.uuidString)] \(message)" + } + else + { + formatted = "[CredMgmt] \(message)" + } + + switch level + { + case .error: + logger.error("\(formatted, privacy: .public)") + case .warning: + logger.warning("\(formatted, privacy: .public)") + case .verbose: + logger.debug("\(formatted, privacy: .public)") + default: + logger.info("\(formatted, privacy: .public)") + } + } + + /// Log a message containing PII (only delivered when masking allows it). + static func logPII( + level: MSALLogLevel, + correlationId: UUID? = nil, + message: String + ) + { + let loggerConfig = MSALGlobalConfig.loggerConfig + guard loggerConfig.logMaskingLevel == .settingsMaskSecretsOnly else { return } + guard level.rawValue <= loggerConfig.logLevel.rawValue else { return } + + let formatted: String + if let correlationId = correlationId + { + formatted = "[CredMgmt][\(correlationId.uuidString)][PII] \(message)" + } + else + { + formatted = "[CredMgmt][PII] \(message)" + } + + logger.debug("\(formatted, privacy: .private)") + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/NetworkProviderAdapter.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/NetworkProviderAdapter.swift new file mode 100644 index 0000000000..ff4be1e840 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/NetworkProviderAdapter.swift @@ -0,0 +1,58 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Adapts a public `MSALNativeCredentialManagementNetworkProvider` to the internal +/// `CredentialManagementNetworkClient` protocol. +/// +/// This enables injection of custom/mock network providers via configuration +/// while keeping the internal transport protocol private. +internal final class NetworkProviderAdapter: CredentialManagementNetworkClient +{ + private let provider: MSALNativeCredentialManagementNetworkProvider + + init(provider: MSALNativeCredentialManagementNetworkProvider) + { + self.provider = provider + } + + func perform(request: CredentialManagementRequest) async throws -> CredentialManagementResponse + { + let publicRequest = MSALCredentialManagementHTTPRequest( + url: request.url, + method: request.method.rawValue, + headers: request.headers, + body: request.body + ) + + let publicResponse = try await provider.performRequest(publicRequest) + + return CredentialManagementResponse( + statusCode: publicResponse.statusCode, + headers: publicResponse.headers, + data: publicResponse.data + ) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/util/Result+Extensions.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/util/Result+Extensions.swift new file mode 100644 index 0000000000..101a2ac285 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/util/Result+Extensions.swift @@ -0,0 +1,18 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. + +import Foundation + +/// Convenience extension for extracting error from Result when success was already handled. +internal extension Result +{ + /// Returns the error if this is a `.failure`, otherwise nil. + var failureValue: Failure? + { + if case .failure(let error) = self { return error } + return nil + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift index 24fdf4fc85..557b15ce5a 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift @@ -57,6 +57,13 @@ public class MSALNativeCredentialManagementConfig: NSObject { /// When nil, the client derives the endpoint from the tenant configuration. public var baseURL: URL? + /// Optional custom network provider for HTTP transport. + /// + /// When set, this replaces the default URLSession-based transport (which includes + /// retry logic, interceptor support, and configurable timeouts modeled after MSIDHttpRequest). + /// Use this to inject a mock provider for testing or local development. + public var networkProvider: MSALNativeCredentialManagementNetworkProvider? + public override init() { super.init() diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementNetworkProvider.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementNetworkProvider.swift new file mode 100644 index 0000000000..6e333aac77 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementNetworkProvider.swift @@ -0,0 +1,96 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents an HTTP request to the credential management API. +public struct MSALCredentialManagementHTTPRequest +{ + /// The target URL. + public let url: URL + + /// The HTTP method (e.g., "GET", "POST", "DELETE"). + public let method: String + + /// The request headers. + public let headers: [String: String] + + /// The request body, if any. + public let body: Data? + + public init(url: URL, method: String, headers: [String: String], body: Data?) + { + self.url = url + self.method = method + self.headers = headers + self.body = body + } +} + +/// Represents an HTTP response from the credential management API. +public struct MSALCredentialManagementHTTPResponse +{ + /// The HTTP status code. + public let statusCode: Int + + /// The response headers. + public let headers: [String: String] + + /// The response body data. + public let data: Data? + + public init(statusCode: Int, headers: [String: String], data: Data?) + { + self.statusCode = statusCode + self.headers = headers + self.data = data + } +} + +/// Protocol for providing custom HTTP transport to the credential management client. +/// +/// Implement this protocol to inject a mock or custom HTTP layer for testing. +/// When set on `MSALNativeCredentialManagementConfig.networkProvider`, it replaces +/// the default URLSession-based transport. +/// +/// Example (mock): +/// ```swift +/// class MockNetworkProvider: MSALNativeCredentialManagementNetworkProvider { +/// func performRequest(_ request: MSALCredentialManagementHTTPRequest) async throws +/// -> MSALCredentialManagementHTTPResponse { +/// // Return mock HAL+JSON responses +/// } +/// } +/// ``` +public protocol MSALNativeCredentialManagementNetworkProvider +{ + /// Performs an HTTP request and returns the response. + /// + /// - Parameter request: The request to execute. + /// - Returns: The HTTP response. + /// - Throws: Network-level errors. + func performRequest( + _ request: MSALCredentialManagementHTTPRequest + ) async throws -> MSALCredentialManagementHTTPResponse +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index ae30eae924..2cb06cd12e 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -55,17 +55,35 @@ public class MSALNativeCredentialMethodsClient: NSObject { message: "A token provider must be set on MSALNativeCredentialManagementConfig before initializing the client." ) } + guard config.baseURL != nil else + { + throw MSALNativeCredentialManagementError( + type: .invalidConfiguration, + message: "A baseURL must be set on MSALNativeCredentialManagementConfig before initializing the client." + ) + } self.config = config self.operationQueue = DispatchQueue( label: "com.microsoft.identity.credentialmanagement", qos: .userInitiated ) - self.mockCredentialMethods = [] - self.pendingRegistrationCredential = nil - super.init() + if let customProvider = config.networkProvider + { + self.networkClient = NetworkProviderAdapter(provider: customProvider) + } + else + { + self.networkClient = CredentialManagementURLSessionClient( + requestInterceptor: config.requestInterceptor + ) + } + + self.apiClient = nil + self.pendingActivateHref = nil + self.pendingEnrollmentType = nil - seedMockData() + super.init() } // MARK: - Public: List Credential Methods @@ -80,49 +98,24 @@ public class MSALNativeCredentialMethodsClient: NSObject { { let correlationId = correlationId ?? UUID() - return await withCheckedContinuation - { continuation in - self.operationQueue.async - { [weak self] in - guard let self = self else - { - let error = MSALNativeCredentialManagementError( - type: .generalError, - message: "Client was deallocated.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(error)) - return - } - - self.acquireToken(correlationId: correlationId) - { accessToken, tokenError in - if let tokenError = tokenError - { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Failed to acquire access token for listing credential methods.", - correlationId: correlationId, - underlyingError: tokenError - ) - continuation.resume(returning: .failure(credError)) - return - } - - guard accessToken != nil else - { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Token provider returned nil access token.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(credError)) - return - } - - continuation.resume(returning: .success(self.mockCredentialMethods)) - } - } + CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "listCredentialMethods: starting") + + let tokenResult = await acquireTokenAsync(correlationId: correlationId) + guard case .success(let accessToken) = tokenResult else + { + return .failure(tokenResult.failureValue!) + } + + switch getAPIClient() + { + case .failure(let error): + return .failure(error) + case .success(let client): + let result = await client.listMethods( + accessToken: accessToken, + correlationId: correlationId + ) + return result } } @@ -157,63 +150,30 @@ public class MSALNativeCredentialMethodsClient: NSObject { { let correlationId = correlationId ?? UUID() - return await withCheckedContinuation - { continuation in - self.operationQueue.async - { [weak self] in - guard let self = self else - { - let error = MSALNativeCredentialManagementError( - type: .generalError, - message: "Client was deallocated.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(error)) - return - } - - self.acquireToken(correlationId: correlationId) - { accessToken, tokenError in - if let tokenError = tokenError - { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Failed to acquire access token for deleting credential method.", - correlationId: correlationId, - underlyingError: tokenError - ) - continuation.resume(returning: .failure(credError)) - return - } - - guard accessToken != nil else - { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Token provider returned nil access token.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(credError)) - return - } - - let methodId = credentialMethod.id - if let index = self.mockCredentialMethods.firstIndex(where: { $0.id == methodId }) - { - self.mockCredentialMethods.remove(at: index) - continuation.resume(returning: .success(())) - } - else - { - let credError = MSALNativeCredentialManagementError( - type: .notFound, - message: "Credential method with id '\(methodId)' not found.", - correlationId: correlationId - ) - continuation.resume(returning: .failure(credError)) - } - } - } + CredentialManagementLogger.log( + level: .info, + correlationId: correlationId, + message: "deleteCredentialMethod: type=\(credentialMethod.credentialType.rawValue)" + ) + + let tokenResult = await acquireTokenAsync(correlationId: correlationId) + guard case .success(let accessToken) = tokenResult else + { + return .failure(tokenResult.failureValue!) + } + + switch getAPIClient() + { + case .failure(let error): + return .failure(error) + case .success(let client): + let result = await client.deleteMethod( + type: credentialMethod.credentialType, + methodId: credentialMethod.id, + accessToken: accessToken, + correlationId: correlationId + ) + return result } } @@ -221,6 +181,8 @@ public class MSALNativeCredentialMethodsClient: NSObject { internal let config: MSALNativeCredentialManagementConfig internal let operationQueue: DispatchQueue - internal var mockCredentialMethods: [any MSALCredentialMethodProtocol] - internal var pendingRegistrationCredential: (any MSALCredentialMethodProtocol)? + internal let networkClient: CredentialManagementNetworkClient + internal var apiClient: CredentialManagementAPIClient? + internal var pendingActivateHref: String? + internal var pendingEnrollmentType: MSALCredentialType? } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift index 6d6f610ef2..280ca8dd31 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift @@ -19,4 +19,8 @@ enum Configuration { /// The relying party identifier for passkey registration (WebAuthn). /// Must match the domain associated with your app (via apple-app-site-association). static let relyingPartyIdentifier = "login.microsoft.com" + + /// The base URL for the credential management API. + /// Replace with your tenant's credential management endpoint. + static let credentialManagementBaseURL = "https://\(tenantSubdomain).ciamlogin.com/api/v1.0" } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift index 8bc5a69536..2113257bf6 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift @@ -106,6 +106,16 @@ struct ContentView: View { Text("Sign in to manage credentials") .font(.headline) + // API mode toggle + Toggle(isOn: $viewModel.useMockAPI) { + HStack { + Image(systemName: viewModel.useMockAPI ? "server.rack" : "globe") + Text(viewModel.useMockAPI ? "Mock API" : "Real Server") + .font(.subheadline) + } + } + .padding(.horizontal) + TextField("Email", text: $email) .textFieldStyle(.roundedBorder) .textContentType(.emailAddress) @@ -154,6 +164,16 @@ struct ContentView: View { } .padding(.horizontal) + // API mode toggle + Toggle(isOn: $viewModel.useMockAPI) { + HStack { + Image(systemName: viewModel.useMockAPI ? "server.rack" : "globe") + Text(viewModel.useMockAPI ? "Mock API" : "Real Server") + .font(.subheadline) + } + } + .padding(.horizontal) + Divider() // Status diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 7ac053d34b..25ea3305b0 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -33,6 +33,13 @@ class CredentialManagementViewModel: ObservableObject { @Published var showChallengeInput = false @Published var challengeHint: String = "" + // Toggle: real server vs mock + @Published var useMockAPI: Bool = true { + didSet { + reinitializeClient() + } + } + // MARK: - Private Properties private var credClient: MSALNativeCredentialMethodsClient? @@ -42,6 +49,10 @@ class CredentialManagementViewModel: ObservableObject { // MARK: - Initialization func initialize() { + reinitializeClient() + } + + private func reinitializeClient() { do { // 1. Configure shared logger (used by both MSAL and Credential Management) MSALGlobalConfig.loggerConfig.logLevel = .verbose @@ -52,7 +63,9 @@ class CredentialManagementViewModel: ObservableObject { } // 2. Create token provider - tokenProvider = SampleTokenProvider() + if tokenProvider == nil { + tokenProvider = SampleTokenProvider() + } // 3. Create shared request interceptor let sharedRequestInterceptor = SampleRequestInterceptor() @@ -61,9 +74,17 @@ class CredentialManagementViewModel: ObservableObject { let credConfig = MSALNativeCredentialManagementConfig() credConfig.requestInterceptor = sharedRequestInterceptor credConfig.tokenProvider = tokenProvider + credConfig.baseURL = URL(string: Configuration.credentialManagementBaseURL) + + // 5. Inject mock or use default real network + if useMockAPI { + credConfig.networkProvider = MockNetworkProvider() + } + credClient = try MSALNativeCredentialMethodsClient(config: credConfig) - statusMessage = "SDK initialized successfully." + let mode = useMockAPI ? "Mock API" : "Real Server" + statusMessage = "SDK initialized (\(mode))." } catch { errorMessage = "Failed to initialize: \(error.localizedDescription)" } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MockNetworkProvider.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MockNetworkProvider.swift new file mode 100644 index 0000000000..96a45dbe93 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MockNetworkProvider.swift @@ -0,0 +1,163 @@ +// +// MockNetworkProvider.swift +// MSALNativeCredManagmentSampleApp +// +// Created by Serhii Demchenko on 2026-06-04. +// + +import Foundation +import MSALNativeCredManagment + +/// A local mock network provider that returns fake HAL+JSON responses. +/// +/// Used for development and testing without a live server. Toggle between +/// this and the real server implementation via the sample app settings. +class MockNetworkProvider: MSALNativeCredentialManagementNetworkProvider { + + func performRequest( + _ request: MSALCredentialManagementHTTPRequest + ) async throws -> MSALCredentialManagementHTTPResponse { + // Simulate a brief network delay + try await Task.sleep(nanoseconds: 300_000_000) // 0.3s + + let path = request.url.path + + switch (request.method, path) { + case ("GET", let p) where p.contains("/me/methods"): + return listMethodsResponse() + case ("POST", let p) where p.contains("/activate"): + return activateResponse() + case ("POST", let p) where p.contains("/me/methods/phone"): + return enrollPhoneResponse() + case ("POST", let p) where p.contains("/me/methods/fido"): + return enrollPasskeyResponse() + case ("POST", let p) where p.contains("/me/methods/password"): + return enrollPasswordResponse() + case ("DELETE", _): + return MSALCredentialManagementHTTPResponse(statusCode: 204, headers: [:], data: nil) + default: + return MSALCredentialManagementHTTPResponse(statusCode: 404, headers: [:], data: nil) + } + } + + // MARK: - Mock Responses + + private func listMethodsResponse() -> MSALCredentialManagementHTTPResponse { + let json: [String: Any] = [ + "_embedded": [ + "methods": [ + [ + "id": "mock-phone-001", + "type": "phone", + "displayName": "+1 •••• 5678", + "createdDateTime": "2025-01-15T10:30:00Z", + "_links": [ + "self": ["href": "/api/v1.0/me/methods/phone/mock-phone-001"] + ] + ], + [ + "id": "mock-passkey-001", + "type": "fido", + "displayName": "iPhone Passkey", + "createdDateTime": "2025-02-01T14:00:00Z", + "aaGuid": "00000000-0000-0000-0000-000000000001", + "_links": [ + "self": ["href": "/api/v1.0/me/methods/fido/mock-passkey-001"] + ] + ], + [ + "id": "mock-password-001", + "type": "password", + "displayName": "Password", + "createdDateTime": "2024-12-01T09:00:00Z", + "_links": [ + "self": ["href": "/api/v1.0/me/methods/password/mock-password-001"] + ] + ] + ] + ], + "_links": [ + "self": ["href": "/api/v1.0/me/methods"], + "enroll": [ + ["href": "/api/v1.0/me/methods/phone", "name": "phone"], + ["href": "/api/v1.0/me/methods/fido", "name": "fido"], + ["href": "/api/v1.0/me/methods/password", "name": "password"] + ] + ] + ] + return jsonResponse(statusCode: 200, json: json) + } + + private func enrollPhoneResponse() -> MSALCredentialManagementHTTPResponse { + let json: [String: Any] = [ + "state": "challengeRequired", + "continuationToken": "mock-continuation-token-phone", + "challengeChannel": "sms", + "challengeTargetLabel": "+1 •••• 5678", + "codeLength": 6, + "_links": [ + "activate": ["href": "/api/v1.0/me/methods/phone/mock-new-phone/activate"] + ] + ] + return jsonResponse(statusCode: 200, json: json) + } + + private func enrollPasskeyResponse() -> MSALCredentialManagementHTTPResponse { + let json: [String: Any] = [ + "state": "completed", + "method": [ + "id": "mock-passkey-new", + "type": "fido", + "displayName": "New Passkey", + "createdDateTime": "2025-06-04T12:00:00Z", + "_links": [ + "self": ["href": "/api/v1.0/me/methods/fido/mock-passkey-new"] + ] + ] + ] + return jsonResponse(statusCode: 201, json: json) + } + + private func enrollPasswordResponse() -> MSALCredentialManagementHTTPResponse { + let json: [String: Any] = [ + "state": "completed", + "method": [ + "id": "mock-password-new", + "type": "password", + "displayName": "Password", + "createdDateTime": "2025-06-04T12:00:00Z", + "_links": [ + "self": ["href": "/api/v1.0/me/methods/password/mock-password-new"] + ] + ] + ] + return jsonResponse(statusCode: 201, json: json) + } + + private func activateResponse() -> MSALCredentialManagementHTTPResponse { + let json: [String: Any] = [ + "state": "completed", + "method": [ + "id": "mock-activated-001", + "type": "phone", + "displayName": "+1 •••• 5678", + "createdDateTime": "2025-06-04T12:00:00Z", + "_links": [ + "self": ["href": "/api/v1.0/me/methods/phone/mock-activated-001"] + ] + ] + ] + return jsonResponse(statusCode: 200, json: json) + } + + // MARK: - Helpers + + private func jsonResponse(statusCode: Int, json: [String: Any]) -> MSALCredentialManagementHTTPResponse { + let data = try? JSONSerialization.data(withJSONObject: json) + return MSALCredentialManagementHTTPResponse( + statusCode: statusCode, + headers: ["Content-Type": "application/hal+json"], + data: data + ) + } +} From 145c890b2aa426fd24228908cc9ede7a0120b1f7 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Wed, 3 Jun 2026 16:35:47 -0700 Subject: [PATCH 37/42] update network layer and sample app. --- .../CMSAL_Private/include/module.modulemap | 21 ++ .../project.pbxproj | 51 ++- ...tiveCredentialMethodsClient+Internal.swift | 39 ++- ...dentialMethodsClient+RegisterPasskey.swift | 6 +- ...entialMethodsClient+RegisterPassword.swift | 7 +- ...redentialMethodsClient+RegisterPhone.swift | 7 +- .../internal/hal/CredentialMethodMapper.swift | 12 +- .../CredentialManagementEnvironment.swift | 70 ++++ .../CredentialManagementMockAPIClient.swift | 320 ++++++++++++++++++ .../CredentialManagementAPIClient.swift | 277 +++++++-------- .../CredentialManagementErrorHandler.swift | 123 +++++++ ...edentialManagementInterceptorBridge.swift} | 39 +-- ...ntialManagementNetworkClientProtocol.swift | 57 ++++ .../CredentialManagementRequestBuilder.swift | 139 -------- ...dentialManagementRequestConfigurator.swift | 89 +++++ .../CredentialManagementResponseMapper.swift | 144 -------- .../CredentialManagementURLResolver.swift | 70 ++++ ...CredentialManagementURLSessionClient.swift | 190 ----------- .../MSALLogger+CredentialManagement.swift | 99 ------ .../MSIDLogger+CredentialManagement.swift | 105 ++++++ .../request/ActivateEnrollmentRequest.swift | 44 +++ .../request/BeginEnrollmentRequest.swift | 46 +++ .../request/CredentialManagementRequest.swift | 46 +++ .../network/request/DeleteMethodRequest.swift | 45 +++ .../network/request/ListMethodsRequest.swift | 43 +++ ...redentialManagementRequestSerializer.swift | 68 ++++ ...edentialManagementRequestSerializing.swift | 33 ++ .../CredentialManagementResponse.swift} | 47 +-- .../EnrollmentResponseMapper.swift | 48 +++ .../ListMethodsResponseMapper.swift | 50 +++ ...ntialManagementHALResponseSerializer.swift | 72 ++++ ...dentialManagementResponseSerializing.swift | 32 ++ .../MSIDResponseSerializerAdapter.swift | 68 ++++ ...MSALNativeCredentialManagementConfig.swift | 7 - ...eCredentialManagementNetworkProvider.swift | 96 ------ .../MSALNativeCredentialMethodsClient.swift | 26 +- .../passkey/MSALPasskeyCredentialMethod.swift | 2 +- .../CredentialManagementViewModel.swift | 18 +- .../MockNetworkProvider.swift | 163 --------- .../MSALNativeCredManagment/Package.swift | 35 +- 40 files changed, 1733 insertions(+), 1121 deletions(-) create mode 100644 lib/native/MSALNativeCredManagment/CMSAL_Private/include/module.modulemap create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/mock/CredentialManagementEnvironment.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/mock/CredentialManagementMockAPIClient.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementErrorHandler.swift rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{NetworkProviderAdapter.swift => CredentialManagementInterceptorBridge.swift} (52%) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClientProtocol.swift delete mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementRequestBuilder.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementRequestConfigurator.swift delete mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementResponseMapper.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementURLResolver.swift delete mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementURLSessionClient.swift delete mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/MSALLogger+CredentialManagement.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/MSIDLogger+CredentialManagement.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/ActivateEnrollmentRequest.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/BeginEnrollmentRequest.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/CredentialManagementRequest.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/DeleteMethodRequest.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/ListMethodsRequest.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_serializer/CredentialManagementRequestSerializer.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_serializer/CredentialManagementRequestSerializing.swift rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{CredentialManagementNetworkClient.swift => response/CredentialManagementResponse.swift} (52%) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_mapper/EnrollmentResponseMapper.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_mapper/ListMethodsResponseMapper.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_serializer/CredentialManagementHALResponseSerializer.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_serializer/CredentialManagementResponseSerializing.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_serializer/MSIDResponseSerializerAdapter.swift delete mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementNetworkProvider.swift delete mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MockNetworkProvider.swift diff --git a/lib/native/MSALNativeCredManagment/CMSAL_Private/include/module.modulemap b/lib/native/MSALNativeCredManagment/CMSAL_Private/include/module.modulemap new file mode 100644 index 0000000000..81720a0f0a --- /dev/null +++ b/lib/native/MSALNativeCredManagment/CMSAL_Private/include/module.modulemap @@ -0,0 +1,21 @@ +// Minimal MSAL_Private module for credential management network layer. +// Contains only IdentityCore types needed by Swift network code. +module MSAL_Private { + header "/Users/antrix1989/workspace/apple/azure-activedirectory-tokenbroker-for-objc/sedemche/ciam-cred-managment/ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc/MSAL/IdentityCore/IdentityCore/src/network/MSIDHttpRequest.h" + header "/Users/antrix1989/workspace/apple/azure-activedirectory-tokenbroker-for-objc/sedemche/ciam-cred-managment/ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc/MSAL/IdentityCore/IdentityCore/src/network/MSIDHttpRequestInterceptorProtocol.h" + header "/Users/antrix1989/workspace/apple/azure-activedirectory-tokenbroker-for-objc/sedemche/ciam-cred-managment/ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc/MSAL/IdentityCore/IdentityCore/src/network/request_server_telemetry/MSIDHttpRequestServerTelemetryHandling.h" + header "/Users/antrix1989/workspace/apple/azure-activedirectory-tokenbroker-for-objc/sedemche/ciam-cred-managment/ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc/MSAL/IdentityCore/IdentityCore/src/network/request_serializer/MSIDRequestSerialization.h" + header "/Users/antrix1989/workspace/apple/azure-activedirectory-tokenbroker-for-objc/sedemche/ciam-cred-managment/ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc/MSAL/IdentityCore/IdentityCore/src/network/request_configurator/MSIDHttpRequestConfiguratorProtocol.h" + header "/Users/antrix1989/workspace/apple/azure-activedirectory-tokenbroker-for-objc/sedemche/ciam-cred-managment/ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc/MSAL/IdentityCore/IdentityCore/src/network/response_serializer/MSIDResponseSerialization.h" + header "/Users/antrix1989/workspace/apple/azure-activedirectory-tokenbroker-for-objc/sedemche/ciam-cred-managment/ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc/MSAL/IdentityCore/IdentityCore/src/network/error_handler/MSIDHttpRequestErrorHandling.h" + header "/Users/antrix1989/workspace/apple/azure-activedirectory-tokenbroker-for-objc/sedemche/ciam-cred-managment/ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc/MSAL/IdentityCore/IdentityCore/src/MSIDBasicContext.h" + header "/Users/antrix1989/workspace/apple/azure-activedirectory-tokenbroker-for-objc/sedemche/ciam-cred-managment/ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc/MSAL/IdentityCore/IdentityCore/src/logger/MSIDLogger.h" + header "/Users/antrix1989/workspace/apple/azure-activedirectory-tokenbroker-for-objc/sedemche/ciam-cred-managment/ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc/MSAL/IdentityCore/IdentityCore/src/logger/MSIDLogger+Internal.h" + header "/Users/antrix1989/workspace/apple/azure-activedirectory-tokenbroker-for-objc/sedemche/ciam-cred-managment/ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc/MSAL/IdentityCore/IdentityCore/src/logger/MSIDMaskedHashableLogParameter.h" + header "/Users/antrix1989/workspace/apple/azure-activedirectory-tokenbroker-for-objc/sedemche/ciam-cred-managment/ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc/MSAL/IdentityCore/IdentityCore/src/logger/MSIDMaskedLogParameter.h" + header "/Users/antrix1989/workspace/apple/azure-activedirectory-tokenbroker-for-objc/sedemche/ciam-cred-managment/ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc/MSAL/IdentityCore/IdentityCore/src/logger/MSIDMaskedUsernameLogParameter.h" + header "/Users/antrix1989/workspace/apple/azure-activedirectory-tokenbroker-for-objc/sedemche/ciam-cred-managment/ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc/MSAL/IdentityCore/IdentityCore/src/MSIDError.h" + header "/Users/antrix1989/workspace/apple/azure-activedirectory-tokenbroker-for-objc/sedemche/ciam-cred-managment/ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc/MSAL/IdentityCore/IdentityCore/src/oauth2/MSIDExternalSSOContext.h" + header "/Users/antrix1989/workspace/apple/azure-activedirectory-tokenbroker-for-objc/sedemche/ciam-cred-managment/ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc/MSAL/IdentityCore/IdentityCore/src/parameters/MSIDRequestParameters.h" + export * +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj index 56ee8c141e..6be4f2f795 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj @@ -9,7 +9,9 @@ /* Begin PBXBuildFile section */ 2394A84C2FC5208100B9FF0C /* MSALNativeCredManagment.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2394A8412FC5208100B9FF0C /* MSALNativeCredManagment.framework */; }; 2394A88B2FC7C14100B9FF0C /* MSAL in Frameworks */ = {isa = PBXBuildFile; productRef = 2394A88A2FC7C14100B9FF0C /* MSAL */; }; - 2394A8D72FC8032400B9FF0C /* MSALNativeCredManagment in Frameworks */ = {isa = PBXBuildFile; productRef = 2394A8D62FC8032400B9FF0C /* MSALNativeCredManagment */; }; + 2394A8E02FCA000100B9FF0C /* MSALNativeCredManagment.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2394A8412FC5208100B9FF0C /* MSALNativeCredManagment.framework */; }; + 2394A8E12FCA000100B9FF0C /* MSALNativeCredManagment.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2394A8412FC5208100B9FF0C /* MSALNativeCredManagment.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 2394A8E52FCA000200B9FF0C /* MSAL in Frameworks */ = {isa = PBXBuildFile; productRef = 2394A8E62FCA000200B9FF0C /* MSAL */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -34,8 +36,29 @@ remoteGlobalIDString = 2394A8A12FC7FE7200B9FF0C; remoteInfo = MSALNativeCredManagmentSampleApp; }; + 2394A8E22FCA000100B9FF0C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2394A8382FC5208100B9FF0C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2394A8402FC5208100B9FF0C; + remoteInfo = MSALNativeCredManagment; + }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 2394A8E32FCA000100B9FF0C /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 2394A8E12FCA000100B9FF0C /* MSALNativeCredManagment.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 2394A8412FC5208100B9FF0C /* MSALNativeCredManagment.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MSALNativeCredManagment.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2394A84B2FC5208100B9FF0C /* MSALNativeCredManagmentTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MSALNativeCredManagmentTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -106,7 +129,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 2394A8D72FC8032400B9FF0C /* MSALNativeCredManagment in Frameworks */, + 2394A8E52FCA000200B9FF0C /* MSAL in Frameworks */, + 2394A8E02FCA000100B9FF0C /* MSALNativeCredManagment.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -218,17 +242,19 @@ 2394A89E2FC7FE7200B9FF0C /* Sources */, 2394A89F2FC7FE7200B9FF0C /* Frameworks */, 2394A8A02FC7FE7200B9FF0C /* Resources */, + 2394A8E32FCA000100B9FF0C /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( + 2394A8E42FCA000100B9FF0C /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 2394A8A32FC7FE7200B9FF0C /* MSALNativeCredManagmentSampleApp */, ); name = MSALNativeCredManagmentSampleApp; packageProductDependencies = ( - 2394A8D62FC8032400B9FF0C /* MSALNativeCredManagment */, + 2394A8E62FCA000200B9FF0C /* MSAL */, ); productName = MSALNativeCredManagmentSampleApp; productReference = 2394A8A22FC7FE7200B9FF0C /* MSALNativeCredManagmentSampleApp.app */; @@ -428,6 +454,11 @@ target = 2394A8A12FC7FE7200B9FF0C /* MSALNativeCredManagmentSampleApp */; targetProxy = 2394A8BD2FC7FE7400B9FF0C /* PBXContainerItemProxy */; }; + 2394A8E42FCA000100B9FF0C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2394A8402FC5208100B9FF0C /* MSALNativeCredManagment */; + targetProxy = 2394A8E22FCA000100B9FF0C /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -445,6 +476,10 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = YES; GENERATE_INFOPLIST_FILE = YES; + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/../../../MSAL/IdentityCore/IdentityCore/src/**", + "$(SRCROOT)/../../../MSAL/src/**", + ); INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 18.2; @@ -466,6 +501,7 @@ SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INCLUDE_PATHS = "$(SRCROOT)/../../../MSAL"; SWIFT_INSTALL_OBJC_HEADER = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; @@ -487,6 +523,10 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = YES; GENERATE_INFOPLIST_FILE = YES; + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/../../../MSAL/IdentityCore/IdentityCore/src/**", + "$(SRCROOT)/../../../MSAL/src/**", + ); INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 18.2; @@ -508,6 +548,7 @@ SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INCLUDE_PATHS = "$(SRCROOT)/../../../MSAL"; SWIFT_INSTALL_OBJC_HEADER = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; @@ -924,9 +965,9 @@ isa = XCSwiftPackageProductDependency; productName = MSAL; }; - 2394A8D62FC8032400B9FF0C /* MSALNativeCredManagment */ = { + 2394A8E62FCA000200B9FF0C /* MSAL */ = { isa = XCSwiftPackageProductDependency; - productName = MSALNativeCredManagment; + productName = MSAL; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift index 4a3d22cf05..9faab17ed4 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift @@ -93,11 +93,37 @@ extension MSALNativeCredentialMethodsClient // MARK: - API Client Access /// Returns or creates the internal API client for server communication. - internal func getAPIClient() -> Result + /// + /// Client selection priority: + /// 1. Mock API client — when `UserDefaults` key + /// `com.microsoft.identity.credentialmanagement.useMockAPI` is `true`. + /// 2. Default (`CredentialManagementAPIClient`) — MSIDHttpRequest-backed URLSession transport. + /// + /// The mock switch is evaluated on every call so toggling UserDefaults at runtime + /// takes effect on the next API call (the cached client is invalidated when the + /// environment changes). + internal func getAPIClient() -> Result { + let useMock = CredentialManagementEnvironment.isMockAPIEnabled + + // Invalidate cached client if mock state changed if let existing = apiClient { - return .success(existing) + let cachedIsMock = existing is CredentialManagementMockAPIClient + if cachedIsMock == useMock + { + return .success(existing) + } + // Mock state flipped — discard cached client + self.apiClient = nil + } + + // Mock API takes precedence — no config validation needed + if useMock + { + let mockClient = CredentialManagementMockAPIClient() + self.apiClient = mockClient + return .success(mockClient) } guard let baseURL = config.baseURL else @@ -108,10 +134,15 @@ extension MSALNativeCredentialMethodsClient )) } + let requestSerializer = CredentialManagementRequestSerializer( + urlResolver: CredentialManagementURLResolver(baseURL: baseURL) + ) + let client = CredentialManagementAPIClient( - baseURL: baseURL, - networkClient: networkClient + requestSerializer: requestSerializer, + requestInterceptor: config.requestInterceptor ) + self.apiClient = client return .success(client) } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift index ff7a3a3277..33ec43fb67 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift @@ -23,7 +23,9 @@ // THE SOFTWARE. import Foundation +import MSAL import AuthenticationServices +@_implementationOnly import MSAL_Private extension MSALNativeCredentialMethodsClient { @@ -39,7 +41,7 @@ extension MSALNativeCredentialMethodsClient { let correlationId = params.correlationId ?? UUID() - CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "performRegisterPasskey: starting") + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "performRegisterPasskey: starting") // Step 1: Acquire access token let tokenResult = await acquireTokenAsync(correlationId: correlationId) @@ -155,7 +157,7 @@ extension MSALNativeCredentialMethodsClient correlationId: correlationId )) } - CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "performRegisterPasskey: completed") + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "performRegisterPasskey: completed") return .success(.completed(method)) case .failure(let error): return .failure(error) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift index 214bb90c74..9e19e0f30c 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift @@ -23,7 +23,8 @@ // THE SOFTWARE. import Foundation - +import MSAL +@_implementationOnly import MSAL_Private extension MSALNativeCredentialMethodsClient { internal func performRegisterPassword( @@ -32,7 +33,7 @@ extension MSALNativeCredentialMethodsClient { let correlationId = params.correlationId ?? UUID() - CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "performRegisterPassword: starting") + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "performRegisterPassword: starting") // Acquire access token let tokenResult = await acquireTokenAsync(correlationId: correlationId) @@ -82,7 +83,7 @@ extension MSALNativeCredentialMethodsClient ) return .success(.completed(fallbackMethod)) } - CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "performRegisterPassword: completed") + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "performRegisterPassword: completed") return .success(.completed(method)) } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift index 150f075d4a..218c29285f 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift @@ -23,7 +23,8 @@ // THE SOFTWARE. import Foundation - +import MSAL +@_implementationOnly import MSAL_Private extension MSALNativeCredentialMethodsClient { internal func performRegisterPhoneNumber( @@ -32,7 +33,7 @@ extension MSALNativeCredentialMethodsClient { let correlationId = params.correlationId ?? UUID() - CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "performRegisterPhoneNumber: starting") + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "performRegisterPhoneNumber: starting") // Acquire access token let tokenResult = await acquireTokenAsync(correlationId: correlationId) @@ -109,7 +110,7 @@ extension MSALNativeCredentialMethodsClient correlationId: correlationId ) - CredentialManagementLogger.log( + MSIDLogger.shared().log( level: .info, correlationId: correlationId, message: "performRegisterPhoneNumber: challenge sent" diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/hal/CredentialMethodMapper.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/hal/CredentialMethodMapper.swift index b10c42dc66..f82c69bd23 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/hal/CredentialMethodMapper.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/hal/CredentialMethodMapper.swift @@ -51,17 +51,7 @@ internal struct CredentialMethodMapper /// Maps an `MSALCredentialType` to the server's `type` string for API paths. static func serverType(from credentialType: MSALCredentialType) -> String { - switch credentialType - { - case .passkey: - return "fido" - case .phone: - return "phone" - case .password: - return "password" - default: - return credentialType.rawValue - } + return credentialType.rawValue } // MARK: - HAL JSON → Credential Method diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/mock/CredentialManagementEnvironment.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/mock/CredentialManagementEnvironment.swift new file mode 100644 index 0000000000..507960dc4b --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/mock/CredentialManagementEnvironment.swift @@ -0,0 +1,70 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Internal environment switch for credential management. +/// +/// Reads UserDefaults to determine whether to route API calls to the real server +/// or return mock responses. This mechanism is entirely internal and is NOT exposed +/// in any public API. +/// +/// ## UserDefaults Keys +/// +/// - `com.microsoft.identity.credentialmanagement.useMockAPI` (Bool): +/// When `true`, all credential management API calls will be routed to the mock client. +/// Default is `false` (real server). +/// +/// - `com.microsoft.identity.credentialmanagement.mockDelaySeconds` (Double): +/// Simulated network delay in seconds when using mock API. Default is 0.5. +/// +/// ## Usage (for internal testing / debug builds only) +/// +/// ```swift +/// // Enable mock API (e.g., in a debug settings screen or launch argument): +/// UserDefaults.standard.set(true, forKey: "com.microsoft.identity.credentialmanagement.useMockAPI") +/// +/// // Optionally configure simulated delay: +/// UserDefaults.standard.set(1.0, forKey: "com.microsoft.identity.credentialmanagement.mockDelaySeconds") +/// +/// // Disable mock API (back to real server): +/// UserDefaults.standard.set(false, forKey: "com.microsoft.identity.credentialmanagement.useMockAPI") +/// ``` +/// +/// ## Launch Arguments +/// +/// You can also pass the flag as a launch argument in Xcode: +/// `-com.microsoft.identity.credentialmanagement.useMockAPI YES` +/// +internal enum CredentialManagementEnvironment +{ + /// UserDefaults key that controls mock/server routing. + static let useMockAPIKey = "com.microsoft.identity.credentialmanagement.useMockAPI" + + /// Returns `true` when mock API mode is enabled via UserDefaults. + static var isMockAPIEnabled: Bool + { + return UserDefaults.standard.bool(forKey: useMockAPIKey) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/mock/CredentialManagementMockAPIClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/mock/CredentialManagementMockAPIClient.swift new file mode 100644 index 0000000000..a7ec26f2b2 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/mock/CredentialManagementMockAPIClient.swift @@ -0,0 +1,320 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import MSAL +@_implementationOnly import MSAL_Private + +/// Internal mock API client that returns simulated responses without hitting the server. +/// +/// Activated via UserDefaults key `com.microsoft.identity.credentialmanagement.useMockAPI`. +/// This is entirely internal — no public API exposes this client or the switch mechanism. +/// +/// The mock maintains in-memory state so that enrollments and deletions are reflected +/// in subsequent `listMethods` calls within the same session. +internal final class CredentialManagementMockAPIClient: CredentialManagementNetworkClientProtocol +{ + /// Simulates network latency. Configurable via UserDefaults key + /// `com.microsoft.identity.credentialmanagement.mockDelaySeconds` (default: 0.5). + private var simulatedDelay: TimeInterval + { + let delay = UserDefaults.standard.double( + forKey: "com.microsoft.identity.credentialmanagement.mockDelaySeconds" + ) + return delay > 0 ? delay : 0.5 + } + + /// In-memory store of credential methods. Mutations (enroll/delete) persist for the + /// lifetime of this client instance. + private var methods: [any MSALCredentialMethodProtocol] + + /// Tracks the pending enrollment type between beginEnrollment and activateEnrollment. + private var pendingEnrollmentType: MSALCredentialType? + + /// Tracks the user-provided phone number from the enrollment body. + private var pendingPhoneNumber: String? + + init() + { + self.methods = Self.seedMethods() + + MSIDLogger.shared().log( + level: .warning, + correlationId: UUID(), + message: "⚠️ Mock API client is active. All credential management calls will return simulated data." + ) + } + + // MARK: - List Methods + + func listMethods( + accessToken: String, + correlationId: UUID + ) async -> Result<[any MSALCredentialMethodProtocol], MSALNativeCredentialManagementError> + { + await simulateNetworkDelay() + + MSIDLogger.shared().log( + level: .info, + correlationId: correlationId, + message: "[Mock] listMethods called — returning \(methods.count) methods" + ) + + return .success(methods) + } + + // MARK: - Begin Enrollment + + func beginEnrollment( + type: MSALCredentialType, + accessToken: String, + body: Data?, + correlationId: UUID + ) async -> Result + { + await simulateNetworkDelay() + + MSIDLogger.shared().log( + level: .info, + correlationId: correlationId, + message: "[Mock] beginEnrollment called for type=\(type.rawValue)" + ) + + // Track the pending enrollment so activateEnrollment returns the correct type + pendingEnrollmentType = type + + // Extract phone number from body if present + if type == .phone, let body = body, + let json = try? JSONSerialization.jsonObject(with: body) as? [String: Any], + let phone = json["phoneNumber"] as? String + { + pendingPhoneNumber = phone + } + + let mockResponse = buildMockEnrollmentResponse(type: type) + return .success(mockResponse) + } + + // MARK: - Activate Enrollment + + func activateEnrollment( + activateHref: String, + accessToken: String, + body: Data, + correlationId: UUID + ) async -> Result + { + await simulateNetworkDelay() + + MSIDLogger.shared().log( + level: .info, + correlationId: correlationId, + message: "[Mock] activateEnrollment called" + ) + + let enrolledType = pendingEnrollmentType ?? .passkey + let newId = "mock-enrolled-\(UUID().uuidString.prefix(8))" + + // Extract displayName from the activation request body (provided by the user) + let bodyJson = (try? JSONSerialization.jsonObject(with: body)) as? [String: Any] + let userDisplayName = bodyJson?["displayName"] as? String + + // Add the correct credential type to the in-memory store + let newMethod: any MSALCredentialMethodProtocol + switch enrolledType + { + case .phone: + let phoneDisplay = pendingPhoneNumber ?? "+1 (555) ***-0000" + newMethod = MSALPhoneCredentialMethod( + id: newId, + createdAt: nil, + phoneNumber: phoneDisplay + ) + case .password: + newMethod = MSALPasswordCredentialMethod( + id: newId, + createdAt: nil + ) + default: + newMethod = MSALPasskeyCredentialMethod( + id: newId, + displayName: userDisplayName ?? "Passkey", + createdAt: nil, + credentialID: "mock-cred-\(UUID().uuidString.prefix(8))", + aaguid: nil + ) + } + methods.append(newMethod) + + // Clear pending state + let mockResponse = buildMockActivationResponse(type: enrolledType, id: newId, displayName: userDisplayName) + pendingEnrollmentType = nil + pendingPhoneNumber = nil + + return .success(mockResponse) + } + + // MARK: - Delete Method + + func deleteMethod( + type: MSALCredentialType, + methodId: String, + accessToken: String, + correlationId: UUID + ) async -> Result + { + await simulateNetworkDelay() + + MSIDLogger.shared().log( + level: .info, + correlationId: correlationId, + message: "[Mock] deleteMethod called for type=\(type.rawValue), id=\(methodId)" + ) + + // Remove from in-memory store so subsequent listMethods reflects the deletion + methods.removeAll { $0.id == methodId } + + return .success(()) + } + + // MARK: - Private: Seed Data + + private static func seedMethods() -> [any MSALCredentialMethodProtocol] + { + return [ + MSALPasskeyCredentialMethod( + id: "mock-passkey-001", + displayName: "Mock Passkey", + createdAt: nil, + credentialID: "mock-credential-id-abc", + aaguid: "00000000-0000-0000-0000-000000000001" + ), + MSALPhoneCredentialMethod( + id: "mock-phone-001", + createdAt: nil, + phoneNumber: "+1 (555) 123-4567" + ), + MSALPasswordCredentialMethod( + id: "mock-password-001", + createdAt: nil + ) + ] + } + + // MARK: - Private: Mock Response Builders + + private func buildMockEnrollmentResponse(type: MSALCredentialType) -> HALResource + { + if type == .password + { + // Password completes in one step — no challenge/activation needed + let newId = "mock-enrolled-\(UUID().uuidString.prefix(8))" + let newMethod = MSALPasswordCredentialMethod(id: newId, createdAt: nil) + methods.append(newMethod) + + let json: [String: Any] = [ + "id": newId, + "type": type.rawValue, + "state": "completed", + "displayName": "Password" + ] + return HALResource(json: json) + } + + var json: [String: Any] = [ + "continuationToken": "mock-continuation-token-\(UUID().uuidString.prefix(8))", + "_links": [ + "activate": ["href": "https://mock.credentialmanagement.microsoft.com/activate"] + ] + ] + + if type == .phone + { + // Use the actual phone number provided by the user (masked for display) + let maskedPhone: String + if let phone = pendingPhoneNumber, phone.count > 4 + { + let last4 = String(phone.suffix(4)) + maskedPhone = "••• \(last4)" + } + else + { + maskedPhone = pendingPhoneNumber ?? "+1 (555) ***-0000" + } + json["sentTo"] = maskedPhone + json["channelType"] = "sms" + json["codeLength"] = 6 + } + else if type == .passkey + { + // Provide mock WebAuthn creation options so the passkey flow can parse them + json["publicKey"] = [ + "challenge": Data("mock-challenge-\(UUID().uuidString)".utf8).base64EncodedString(), + "rp": [ + "id": "login.microsoft.com", + "name": "Microsoft" + ], + "user": [ + "id": Data("mock-user-id".utf8).base64EncodedString(), + "name": "user@contoso.com", + "displayName": "Mock User" + ] + ] + } + + return HALResource(json: json) + } + + private func buildMockActivationResponse(type: MSALCredentialType, id: String, displayName: String?) -> HALResource + { + var json: [String: Any] = [ + "id": id, + "type": type.rawValue + ] + + switch type + { + case .phone: + json["displayName"] = pendingPhoneNumber ?? "Phone" + json["phoneNumber"] = pendingPhoneNumber ?? "+1 (555) 000-0000" + case .password: + json["displayName"] = "Password" + default: + json["displayName"] = displayName ?? "Passkey" + } + + return HALResource(json: json) + } + + // MARK: - Private: Delay Simulation + + private func simulateNetworkDelay() async + { + let delay = simulatedDelay + if delay > 0 + { + try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) + } + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementAPIClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementAPIClient.swift index 53543d0c6c..b63107cf9f 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementAPIClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementAPIClient.swift @@ -24,20 +24,26 @@ import Foundation import MSAL - -/// Internal API client that orchestrates network calls and HAL response parsing -/// for the credential management service. -internal final class CredentialManagementAPIClient +@_implementationOnly import MSAL_Private + +/// Internal API client that orchestrates network calls using `MSIDHttpRequest` +/// infrastructure from IdentityCore. +/// +/// Architecture (follows IdentityCore pattern): +/// 1. Typed request objects define endpoint-specific data +/// 2. `CredentialManagementRequestConfigurator` wires serializers/handlers onto MSIDHttpRequest +/// 3. MSIDHttpRequest sends via URLSession with retry/telemetry +/// 4. `MSIDResponseSerializerAdapter` bridges to pure-Swift response parsing +/// 5. Response mappers transform parsed responses into domain objects +internal final class CredentialManagementAPIClient: CredentialManagementNetworkClientProtocol { - private let networkClient: CredentialManagementNetworkClient - private let baseURL: URL - - private static let methodsPath = "/api/v1.0/me/methods" + private let requestSerializer: CredentialManagementRequestSerializing + private let requestInterceptor: MSALNativeAuthRequestInterceptor? - init(baseURL: URL, networkClient: CredentialManagementNetworkClient) + init(requestSerializer: CredentialManagementRequestSerializing, requestInterceptor: MSALNativeAuthRequestInterceptor?) { - self.baseURL = baseURL - self.networkClient = networkClient + self.requestSerializer = requestSerializer + self.requestInterceptor = requestInterceptor } // MARK: - List Methods @@ -47,54 +53,24 @@ internal final class CredentialManagementAPIClient correlationId: UUID ) async -> Result<[any MSALCredentialMethodProtocol], MSALNativeCredentialManagementError> { - let builder = CredentialManagementRequestBuilder( - baseURL: baseURL, - accessToken: accessToken, - correlationId: correlationId - ) - - let request: CredentialManagementRequest - switch builder.buildGET(path: Self.methodsPath) - { - case .success(let r): request = r - case .failure(let e): return .failure(e) - } + let typedRequest = ListMethodsRequest(accessToken: accessToken, correlationId: correlationId) - CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "Credential management: listing methods") + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "Credential management: listing methods") - let response: CredentialManagementResponse - do - { - response = try await networkClient.perform(request: request) - } - catch - { - return .failure(MSALNativeCredentialManagementError( - type: .networkError, - message: "Network request failed for listing credential methods.", - correlationId: correlationId, - underlyingError: error - )) - } + let sendResult = await send(typedRequest) - if let mappedError = CredentialManagementResponseMapper.mapError(from: response, correlationId: correlationId) + switch sendResult { - return .failure(mappedError) + case .failure(let e): + return .failure(e) + case .success(let response): + let mapResult = ListMethodsResponseMapper.map(response, correlationId: correlationId) + if case .success(let methods) = mapResult + { + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "Credential management: listed \(methods.count) method(s)") + } + return mapResult } - - let json: [String: Any] - switch CredentialManagementResponseMapper.decodeJSON(from: response, correlationId: correlationId) - { - case .success(let j): json = j - case .failure(let e): return .failure(e) - } - - let halResource = HALResource(json: json) - let methods = CredentialMethodMapper.parseMethods(from: halResource) - - CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "Credential management: listed \(methods.count) method(s)") - - return .success(methods) } // MARK: - Begin Enrollment @@ -106,52 +82,21 @@ internal final class CredentialManagementAPIClient correlationId: UUID ) async -> Result { - let serverType = CredentialMethodMapper.serverType(from: type) - let path = "\(Self.methodsPath)/\(serverType)" - - let builder = CredentialManagementRequestBuilder( - baseURL: baseURL, + let typedRequest = BeginEnrollmentRequest( + type: type, accessToken: accessToken, + body: body, correlationId: correlationId ) - let request: CredentialManagementRequest - switch builder.buildPOST(path: path, body: body) - { - case .success(let r): request = r - case .failure(let e): return .failure(e) - } - - CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "Credential management: beginning enrollment for type '\(serverType)'") - - let response: CredentialManagementResponse - do - { - response = try await networkClient.perform(request: request) - } - catch - { - return .failure(MSALNativeCredentialManagementError( - type: .networkError, - message: "Network request failed for enrollment.", - correlationId: correlationId, - underlyingError: error - )) - } - - if let mappedError = CredentialManagementResponseMapper.mapError(from: response, correlationId: correlationId) - { - return .failure(mappedError) - } - - let json: [String: Any] - switch CredentialManagementResponseMapper.decodeJSON(from: response, correlationId: correlationId) - { - case .success(let j): json = j - case .failure(let e): return .failure(e) - } + MSIDLogger.shared().log( + level: .info, + correlationId: correlationId, + message: "Credential management: beginning enrollment for type '\(CredentialMethodMapper.serverType(from: type))'" + ) - return .success(HALResource(json: json)) + let sendResult = await send(typedRequest) + return sendResult.flatMap { EnrollmentResponseMapper.map($0, correlationId: correlationId) } } // MARK: - Activate Enrollment @@ -163,49 +108,17 @@ internal final class CredentialManagementAPIClient correlationId: UUID ) async -> Result { - let builder = CredentialManagementRequestBuilder( - baseURL: baseURL, + let typedRequest = ActivateEnrollmentRequest( + activateHref: activateHref, accessToken: accessToken, + body: body, correlationId: correlationId ) - let request: CredentialManagementRequest - switch builder.buildPOST(path: activateHref, body: body) - { - case .success(let r): request = r - case .failure(let e): return .failure(e) - } - - CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "Credential management: activating enrollment") - - let response: CredentialManagementResponse - do - { - response = try await networkClient.perform(request: request) - } - catch - { - return .failure(MSALNativeCredentialManagementError( - type: .networkError, - message: "Network request failed for activation.", - correlationId: correlationId, - underlyingError: error - )) - } - - if let mappedError = CredentialManagementResponseMapper.mapError(from: response, correlationId: correlationId) - { - return .failure(mappedError) - } + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "Credential management: activating enrollment") - let json: [String: Any] - switch CredentialManagementResponseMapper.decodeJSON(from: response, correlationId: correlationId) - { - case .success(let j): json = j - case .failure(let e): return .failure(e) - } - - return .success(HALResource(json: json)) + let sendResult = await send(typedRequest) + return sendResult.flatMap { EnrollmentResponseMapper.map($0, correlationId: correlationId) } } // MARK: - Delete Method @@ -217,46 +130,86 @@ internal final class CredentialManagementAPIClient correlationId: UUID ) async -> Result { - let serverType = CredentialMethodMapper.serverType(from: type) - let path = "\(Self.methodsPath)/\(serverType)/\(methodId)" - - let builder = CredentialManagementRequestBuilder( - baseURL: baseURL, + let typedRequest = DeleteMethodRequest( + type: type, + methodId: methodId, accessToken: accessToken, correlationId: correlationId ) - let request: CredentialManagementRequest - switch builder.buildDELETE(path: path) - { - case .success(let r): request = r - case .failure(let e): return .failure(e) - } + MSIDLogger.shared().log( + level: .info, + correlationId: correlationId, + message: "Credential management: deleting method of type '\(CredentialMethodMapper.serverType(from: type))'" + ) - CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "Credential management: deleting method of type '\(serverType)'") + let sendResult = await send(typedRequest) - let response: CredentialManagementResponse - do - { - response = try await networkClient.perform(request: request) - } - catch + switch sendResult { - return .failure(MSALNativeCredentialManagementError( - type: .networkError, - message: "Network request failed for deletion.", - correlationId: correlationId, - underlyingError: error - )) + case .failure(let e): + return .failure(e) + case .success: + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "Credential management: method deleted successfully") + return .success(()) } + } - if let mappedError = CredentialManagementResponseMapper.mapError(from: response, correlationId: correlationId) + // MARK: - Private: Send Pipeline + + /// Configures and sends a typed request through the MSIDHttpRequest pipeline. + private func send( + _ typedRequest: CredentialManagementRequestProtocol + ) async -> Result + { + let configurator = CredentialManagementRequestConfigurator( + requestSerializer: requestSerializer, + correlationId: typedRequest.correlationId, + requestInterceptor: requestInterceptor + ) + + let configResult = configurator.configure(typedRequest) + + let msidRequest: MSIDHttpRequest + switch configResult { - return .failure(mappedError) + case .success(let r): msidRequest = r + case .failure(let e): return .failure(e) } - CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "Credential management: method deleted successfully") - - return .success(()) + return await withCheckedContinuation + { continuation in + msidRequest.send + { result, error in + if let error = error + { + if let credError = error as? MSALNativeCredentialManagementError + { + continuation.resume(returning: .failure(credError)) + } + else + { + continuation.resume(returning: .failure(MSALNativeCredentialManagementError( + type: .networkError, + message: "Network request failed.", + correlationId: typedRequest.correlationId, + underlyingError: error + ))) + } + } + else if let response = result as? CredentialManagementResponse + { + continuation.resume(returning: .success(response)) + } + else + { + continuation.resume(returning: .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Unexpected response type from network layer.", + correlationId: typedRequest.correlationId + ))) + } + } + } } } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementErrorHandler.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementErrorHandler.swift new file mode 100644 index 0000000000..899d8ac4a3 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementErrorHandler.swift @@ -0,0 +1,123 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +@_implementationOnly import MSAL_Private + +/// Implements `MSIDHttpRequestErrorHandling` for credential management API errors. +/// +/// Maps HTTP error status codes to `MSALNativeCredentialManagementError` instances +/// and invokes the completion block with the appropriate typed error. +internal final class CredentialManagementErrorHandler: NSObject, MSIDHttpRequestErrorHandling +{ + private let correlationId: UUID + + init(correlationId: UUID) + { + self.correlationId = correlationId + } + + func handleError( + _ error: Error?, + httpResponse: HTTPURLResponse?, + data: Data?, + httpRequest: MSIDHttpRequestProtocol?, + responseSerializer: MSIDResponseSerialization?, + externalSSOContext ssoContext: MSIDExternalSSOContext?, + context: MSIDRequestContext?, + completionBlock: MSIDHttpRequestDidCompleteBlock? + ) + { + let statusCode = httpResponse?.statusCode ?? 0 + let mappedError: MSALNativeCredentialManagementError + + switch statusCode + { + case 401: + mappedError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Server returned 401 Unauthorized. The access token may be expired or invalid.", + correlationId: correlationId + ) + + case 403: + mappedError = MSALNativeCredentialManagementError( + type: .forbidden, + message: "Server returned 403 Forbidden. The user may lack the required permissions or recent MFA.", + correlationId: correlationId + ) + + case 404: + mappedError = MSALNativeCredentialManagementError( + type: .notFound, + message: "Server returned 404 Not Found. The credential method does not exist.", + correlationId: correlationId + ) + + case 409: + mappedError = MSALNativeCredentialManagementError( + type: .conflict, + message: "Server returned 409 Conflict. The credential method may already be registered.", + correlationId: correlationId + ) + + case 429: + mappedError = MSALNativeCredentialManagementError( + type: .networkError, + message: "Server returned 429 Too Many Requests. Please try again later.", + correlationId: correlationId + ) + + default: + if let error = error + { + mappedError = MSALNativeCredentialManagementError( + type: .networkError, + message: "Network request failed.", + correlationId: correlationId, + underlyingError: error + ) + } + else + { + let message: String + if (500...599).contains(statusCode) + { + message = "Server returned \(statusCode). A server-side error occurred." + } + else + { + message = "Server returned unexpected status code \(statusCode)." + } + mappedError = MSALNativeCredentialManagementError( + type: .generalError, + message: message, + correlationId: correlationId + ) + } + } + + completionBlock?(nil, mappedError) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/NetworkProviderAdapter.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementInterceptorBridge.swift similarity index 52% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/NetworkProviderAdapter.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementInterceptorBridge.swift index ff4be1e840..4710af3d25 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/NetworkProviderAdapter.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementInterceptorBridge.swift @@ -23,36 +23,27 @@ // THE SOFTWARE. import Foundation +import MSAL +@_implementationOnly import MSAL_Private -/// Adapts a public `MSALNativeCredentialManagementNetworkProvider` to the internal -/// `CredentialManagementNetworkClient` protocol. -/// -/// This enables injection of custom/mock network providers via configuration -/// while keeping the internal transport protocol private. -internal final class NetworkProviderAdapter: CredentialManagementNetworkClient +/// Bridges `MSALNativeAuthRequestInterceptor` (Swift protocol from MSAL public API) to +/// `MSIDHttpRequestInterceptorProtocol` (ObjC protocol used by MSIDHttpRequest in IdentityCore). +internal final class CredentialManagementInterceptorBridge: NSObject, MSIDHttpRequestInterceptorProtocol { - private let provider: MSALNativeCredentialManagementNetworkProvider + private let interceptor: MSALNativeAuthRequestInterceptor - init(provider: MSALNativeCredentialManagementNetworkProvider) + init(interceptor: MSALNativeAuthRequestInterceptor) { - self.provider = provider + self.interceptor = interceptor } - func perform(request: CredentialManagementRequest) async throws -> CredentialManagementResponse + func addAdditionalHeaderFields( + for requestUrl: URL?, + with completionBlock: @escaping MSIDHttpRequestInterceptorAddHeaderCompletionBlock + ) { - let publicRequest = MSALCredentialManagementHTTPRequest( - url: request.url, - method: request.method.rawValue, - headers: request.headers, - body: request.body - ) - - let publicResponse = try await provider.performRequest(publicRequest) - - return CredentialManagementResponse( - statusCode: publicResponse.statusCode, - headers: publicResponse.headers, - data: publicResponse.data - ) + interceptor.addAdditionalHeaderFields(requestUrl) { additionalHeaders in + completionBlock(additionalHeaders) + } } } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClientProtocol.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClientProtocol.swift new file mode 100644 index 0000000000..c425897085 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClientProtocol.swift @@ -0,0 +1,57 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import MSAL + +/// Internal protocol that unifies the MSIDHttpRequest-based client and the custom +/// network provider client behind a single interface. +internal protocol CredentialManagementNetworkClientProtocol +{ + func listMethods( + accessToken: String, + correlationId: UUID + ) async -> Result<[any MSALCredentialMethodProtocol], MSALNativeCredentialManagementError> + + func beginEnrollment( + type: MSALCredentialType, + accessToken: String, + body: Data?, + correlationId: UUID + ) async -> Result + + func activateEnrollment( + activateHref: String, + accessToken: String, + body: Data, + correlationId: UUID + ) async -> Result + + func deleteMethod( + type: MSALCredentialType, + methodId: String, + accessToken: String, + correlationId: UUID + ) async -> Result +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementRequestBuilder.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementRequestBuilder.swift deleted file mode 100644 index dcf3ea4273..0000000000 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementRequestBuilder.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. -// All rights reserved. -// -// This code is licensed under the MIT License. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -/// Builds authenticated HTTP requests for the credential management API. -/// -/// Ensures consistent headers (Authorization, Accept, Content-Type, correlation ID) -/// and validates that all target URLs belong to the trusted base URL. -internal struct CredentialManagementRequestBuilder -{ - private let baseURL: URL - private let accessToken: String - private let correlationId: UUID - - init(baseURL: URL, accessToken: String, correlationId: UUID) - { - self.baseURL = baseURL - self.accessToken = accessToken - self.correlationId = correlationId - } - - // MARK: - Request Factories - - func buildGET(path: String) -> Result - { - guard let url = resolveURL(path: path) else - { - return .failure(untrustedURLError(path: path)) - } - - return .success(CredentialManagementRequest( - url: url, - method: .get, - headers: commonHeaders() - )) - } - - func buildPOST(path: String, body: Data?) -> Result - { - guard let url = resolveURL(path: path) else - { - return .failure(untrustedURLError(path: path)) - } - - var headers = commonHeaders() - if body != nil - { - headers["Content-Type"] = "application/json" - } - - return .success(CredentialManagementRequest( - url: url, - method: .post, - headers: headers, - body: body - )) - } - - func buildDELETE(path: String) -> Result - { - guard let url = resolveURL(path: path) else - { - return .failure(untrustedURLError(path: path)) - } - - return .success(CredentialManagementRequest( - url: url, - method: .delete, - headers: commonHeaders() - )) - } - - // MARK: - URL Validation - - /// Resolves a path (absolute or relative) against the base URL. - /// Returns nil if the resolved URL does not belong to the trusted base. - func resolveURL(path: String) -> URL? - { - let resolvedURL: URL - - if path.hasPrefix("http://") || path.hasPrefix("https://") - { - guard let url = URL(string: path) else { return nil } - resolvedURL = url - } - else - { - guard let url = URL(string: path, relativeTo: baseURL)?.absoluteURL else { return nil } - resolvedURL = url - } - - guard resolvedURL.scheme == "https" else { return nil } - guard resolvedURL.host == baseURL.host else { return nil } - - return resolvedURL - } - - // MARK: - Private - - private func commonHeaders() -> [String: String] - { - return [ - "Authorization": "Bearer \(accessToken)", - "Accept": "application/hal+json", - "client-request-id": correlationId.uuidString - ] - } - - private func untrustedURLError(path: String) -> MSALNativeCredentialManagementError - { - return MSALNativeCredentialManagementError( - type: .generalError, - message: "URL validation failed: '\(path)' does not belong to the trusted service.", - correlationId: correlationId - ) - } -} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementRequestConfigurator.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementRequestConfigurator.swift new file mode 100644 index 0000000000..93fe589350 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementRequestConfigurator.swift @@ -0,0 +1,89 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import MSAL +@_implementationOnly import MSAL_Private + +/// Configures an `MSIDHttpRequest` with the credential management serializers, +/// error handler, and optional interceptor. +/// +/// Follows the IdentityCore `MSIDHttpRequestConfiguratorProtocol` pattern: +/// the configurator wires all cross-cutting concerns onto a request before sending. +internal final class CredentialManagementRequestConfigurator +{ + private let requestSerializer: CredentialManagementRequestSerializing + private let correlationId: UUID + private let requestInterceptor: MSALNativeAuthRequestInterceptor? + + init( + requestSerializer: CredentialManagementRequestSerializing, + correlationId: UUID, + requestInterceptor: MSALNativeAuthRequestInterceptor? + ) + { + self.requestSerializer = requestSerializer + self.correlationId = correlationId + self.requestInterceptor = requestInterceptor + } + + /// Configures an `MSIDHttpRequest` from a typed request. + /// Returns the configured request or an error if URL validation fails. + func configure( + _ typedRequest: CredentialManagementRequestProtocol + ) -> Result + { + guard let urlRequest = requestSerializer.serialize(typedRequest) else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "URL validation failed: '\(typedRequest.path)' does not belong to the trusted service.", + correlationId: typedRequest.correlationId + )) + } + + let request = MSIDHttpRequest() + request.urlRequest = urlRequest + + // Serializers + request.responseSerializer = MSIDResponseSerializerAdapter() + request.errorResponseSerializer = MSIDResponseSerializerAdapter() + + // Error handler + request.errorHandler = CredentialManagementErrorHandler(correlationId: correlationId) + + // Context + let context = MSIDBasicContext() + context.correlationId = correlationId + request.context = context + + // Interceptor + if let interceptor = requestInterceptor + { + request.requestInterceptor = CredentialManagementInterceptorBridge(interceptor: interceptor) + } + + return .success(request) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementResponseMapper.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementResponseMapper.swift deleted file mode 100644 index e45d945837..0000000000 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementResponseMapper.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. -// All rights reserved. -// -// This code is licensed under the MIT License. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -/// Maps HTTP response status codes to `MSALNativeCredentialManagementError`. -/// -/// Handles success codes (200, 201, 204) and maps error codes to -/// appropriate `MSALNativeCredentialManagementErrorType` values. -internal struct CredentialManagementResponseMapper -{ - /// Validates that the response indicates success, or maps to a typed error. - /// - /// - Parameters: - /// - response: The HTTP response. - /// - correlationId: The request correlation ID for diagnostics. - /// - Returns: nil if the response indicates success, or an error describing the failure. - static func mapError( - from response: CredentialManagementResponse, - correlationId: UUID - ) -> MSALNativeCredentialManagementError? - { - switch response.statusCode - { - case 200, 201, 204: - return nil - - case 401: - return MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Server returned 401 Unauthorized. The access token may be expired or invalid.", - correlationId: correlationId - ) - - case 403: - return MSALNativeCredentialManagementError( - type: .forbidden, - message: "Server returned 403 Forbidden. The user may lack the required permissions or recent MFA.", - correlationId: correlationId - ) - - case 404: - return MSALNativeCredentialManagementError( - type: .notFound, - message: "Server returned 404 Not Found. The credential method does not exist.", - correlationId: correlationId - ) - - case 409: - return MSALNativeCredentialManagementError( - type: .conflict, - message: "Server returned 409 Conflict. The credential method may already be registered.", - correlationId: correlationId - ) - - case 429: - return MSALNativeCredentialManagementError( - type: .networkError, - message: "Server returned 429 Too Many Requests. Please try again later.", - correlationId: correlationId - ) - - default: - let message: String - if (500...599).contains(response.statusCode) - { - message = "Server returned \(response.statusCode). A server-side error occurred." - } - else - { - message = "Server returned unexpected status code \(response.statusCode)." - } - return MSALNativeCredentialManagementError( - type: .generalError, - message: message, - correlationId: correlationId - ) - } - } - - /// Decodes JSON body from a successful response. - /// - /// - Parameters: - /// - response: The HTTP response with body data. - /// - correlationId: The request correlation ID. - /// - Returns: Decoded dictionary or an error. - static func decodeJSON( - from response: CredentialManagementResponse, - correlationId: UUID - ) -> Result<[String: Any], MSALNativeCredentialManagementError> - { - guard let data = response.data, !data.isEmpty else - { - return .failure(MSALNativeCredentialManagementError( - type: .generalError, - message: "Response body is empty.", - correlationId: correlationId - )) - } - - do - { - guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else - { - return .failure(MSALNativeCredentialManagementError( - type: .generalError, - message: "Response body is not a valid JSON object.", - correlationId: correlationId - )) - } - return .success(json) - } - catch - { - return .failure(MSALNativeCredentialManagementError( - type: .generalError, - message: "Failed to parse response JSON.", - correlationId: correlationId, - underlyingError: error - )) - } - } -} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementURLResolver.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementURLResolver.swift new file mode 100644 index 0000000000..add04a1e6d --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementURLResolver.swift @@ -0,0 +1,70 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Centralized URL resolution and validation for the credential management service. +/// Used by both the MSIDHttpRequest-based transport and the custom network provider transport. +internal struct CredentialManagementURLResolver +{ + private let baseURL: URL + + init(baseURL: URL) + { + self.baseURL = baseURL + } + + /// Resolves a path (absolute or relative) against the base URL. + /// Returns nil if the resolved URL does not pass security validation. + /// + /// Validation rules: + /// - Scheme must be HTTPS + /// - Host must match the trusted base URL host + func resolve(path: String) -> URL? + { + let resolvedURL: URL + + if path.hasPrefix("http://") || path.hasPrefix("https://") + { + guard let url = URL(string: path) else { return nil } + resolvedURL = url + } + else + { + guard let url = URL(string: path, relativeTo: baseURL)?.absoluteURL else { return nil } + resolvedURL = url + } + + guard resolvedURL.scheme == "https" else { return nil } + guard resolvedURL.host == baseURL.host else { return nil } + + return resolvedURL + } +} + +/// Shared endpoint path constants. +internal enum CredentialManagementEndpoints +{ + static let methodsPath = "/api/v1.0/me/methods" +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementURLSessionClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementURLSessionClient.swift deleted file mode 100644 index fcd99a39d1..0000000000 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementURLSessionClient.swift +++ /dev/null @@ -1,190 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. -// All rights reserved. -// -// This code is licensed under the MIT License. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import MSAL - -/// Concrete implementation of `CredentialManagementNetworkClient` using `URLSession`. -/// -/// Modeled after `MSIDHttpRequest` patterns from IdentityCore: -/// - Configurable retry logic (`retryCount`, `retryInterval`) -/// - Configurable request timeout -/// - Request interceptor support for header injection (shared with MSAL) -/// - Retry on transient failures (5xx, timeout, network errors) -internal final class CredentialManagementURLSessionClient: CredentialManagementNetworkClient -{ - // MARK: - Configuration (mirrors MSIDHttpRequest properties) - - /// Number of retry attempts for transient failures. Default is 1. - var retryCount: Int = 1 - - /// Interval between retry attempts in seconds. Default is 0.5. - var retryInterval: TimeInterval = 0.5 - - /// Request timeout in seconds. Default is 30. - var requestTimeoutInterval: TimeInterval = 30 - - /// Optional request interceptor shared with MSAL for injecting custom headers. - var requestInterceptor: MSALNativeAuthRequestInterceptor? - - // MARK: - Private - - private let urlSession: URLSession - - init(urlSession: URLSession = .shared) - { - self.urlSession = urlSession - } - - convenience init( - requestInterceptor: MSALNativeAuthRequestInterceptor?, - retryCount: Int = 1, - retryInterval: TimeInterval = 0.5, - requestTimeoutInterval: TimeInterval = 30 - ) - { - self.init(urlSession: .shared) - self.requestInterceptor = requestInterceptor - self.retryCount = retryCount - self.retryInterval = retryInterval - self.requestTimeoutInterval = requestTimeoutInterval - } - - // MARK: - CredentialManagementNetworkClient - - func perform(request: CredentialManagementRequest) async throws -> CredentialManagementResponse - { - var urlRequest = try await buildURLRequest(from: request) - urlRequest.timeoutInterval = requestTimeoutInterval - - var lastError: Error? - let maxAttempts = 1 + retryCount - - for attempt in 0.. 0 - { - CredentialManagementLogger.log( - level: .info, - message: "Retrying request (attempt \(attempt + 1)/\(maxAttempts)) after \(retryInterval)s" - ) - try await Task.sleep(nanoseconds: UInt64(retryInterval * 1_000_000_000)) - } - - do - { - let (data, response) = try await urlSession.data(for: urlRequest) - - guard let httpResponse = response as? HTTPURLResponse else - { - throw URLError(.badServerResponse) - } - - let responseHeaders = httpResponse.allHeaderFields.reduce(into: [String: String]()) - { result, pair in - if let key = pair.key as? String, let value = pair.value as? String - { - result[key] = value - } - } - - let credResponse = CredentialManagementResponse( - statusCode: httpResponse.statusCode, - headers: responseHeaders, - data: data - ) - - // Retry on transient server errors (5xx) - if isRetryableStatusCode(httpResponse.statusCode) && attempt < maxAttempts - 1 - { - lastError = URLError(.badServerResponse) - continue - } - - return credResponse - } - catch - { - lastError = error - - // Only retry on transient/network errors - if !isRetryableError(error) || attempt >= maxAttempts - 1 - { - throw error - } - } - } - - throw lastError ?? URLError(.unknown) - } - - // MARK: - Private Helpers - - private func buildURLRequest(from request: CredentialManagementRequest) async throws -> URLRequest - { - var urlRequest = URLRequest(url: request.url) - urlRequest.httpMethod = request.method.rawValue - urlRequest.httpBody = request.body - - for (key, value) in request.headers - { - urlRequest.setValue(value, forHTTPHeaderField: key) - } - - // Apply interceptor headers (mirrors MSIDHttpRequest's requestInterceptor pattern) - if let interceptor = requestInterceptor - { - let additionalHeaders = await withCheckedContinuation - { continuation in - interceptor.addAdditionalHeaderFields(request.url) - { headers in - continuation.resume(returning: headers ?? [:]) - } - } - - for (key, value) in additionalHeaders - { - urlRequest.setValue(value, forHTTPHeaderField: key) - } - } - - return urlRequest - } - - private func isRetryableStatusCode(_ statusCode: Int) -> Bool - { - return (500...599).contains(statusCode) || statusCode == 429 - } - - private func isRetryableError(_ error: Error) -> Bool - { - let nsError = error as NSError - let retryableCodes: [Int] = [ - NSURLErrorTimedOut, - NSURLErrorNetworkConnectionLost, - NSURLErrorNotConnectedToInternet - ] - return nsError.domain == NSURLErrorDomain && retryableCodes.contains(nsError.code) - } -} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/MSALLogger+CredentialManagement.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/MSALLogger+CredentialManagement.swift deleted file mode 100644 index dad97d04b4..0000000000 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/MSALLogger+CredentialManagement.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. -// All rights reserved. -// -// This code is licensed under the MIT License. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import os -import MSAL - -/// Lightweight logger that reuses MSAL's log level configuration. -/// -/// Uses Apple's unified logging (`os.Logger`) for output, while respecting -/// the MSAL log level set on `MSALGlobalConfig.loggerConfig.logLevel`. -/// -/// **Privacy:** This logger NEVER logs tokens, phone numbers, email addresses, -/// credential IDs, continuation tokens, or full HAL payloads. -internal enum CredentialManagementLogger -{ - private static let logger = os.Logger( - subsystem: "com.microsoft.identity.client", - category: "CredentialManagement" - ) - - /// Log a non-PII message at the specified level. - static func log( - level: MSALLogLevel, - correlationId: UUID? = nil, - message: String - ) - { - let loggerConfig = MSALGlobalConfig.loggerConfig - guard level.rawValue <= loggerConfig.logLevel.rawValue else { return } - - let formatted: String - if let correlationId = correlationId - { - formatted = "[CredMgmt][\(correlationId.uuidString)] \(message)" - } - else - { - formatted = "[CredMgmt] \(message)" - } - - switch level - { - case .error: - logger.error("\(formatted, privacy: .public)") - case .warning: - logger.warning("\(formatted, privacy: .public)") - case .verbose: - logger.debug("\(formatted, privacy: .public)") - default: - logger.info("\(formatted, privacy: .public)") - } - } - - /// Log a message containing PII (only delivered when masking allows it). - static func logPII( - level: MSALLogLevel, - correlationId: UUID? = nil, - message: String - ) - { - let loggerConfig = MSALGlobalConfig.loggerConfig - guard loggerConfig.logMaskingLevel == .settingsMaskSecretsOnly else { return } - guard level.rawValue <= loggerConfig.logLevel.rawValue else { return } - - let formatted: String - if let correlationId = correlationId - { - formatted = "[CredMgmt][\(correlationId.uuidString)][PII] \(message)" - } - else - { - formatted = "[CredMgmt][PII] \(message)" - } - - logger.debug("\(formatted, privacy: .private)") - } -} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/MSIDLogger+CredentialManagement.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/MSIDLogger+CredentialManagement.swift new file mode 100644 index 0000000000..2f8435c46c --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/MSIDLogger+CredentialManagement.swift @@ -0,0 +1,105 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import MSAL +@_implementationOnly import MSAL_Private + +/// Convenience extension for logging in the credential management module. +/// +/// Provides a simple `log(level:correlationId:message:)` API that delegates to +/// `MSIDLogger` from IdentityCore. MSAL is the base module — we use its +/// logging infrastructure directly, no wrappers. +/// +/// **Privacy:** This extension NEVER logs tokens, phone numbers, email addresses, +/// credential IDs, continuation tokens, or full HAL payloads. +extension MSIDLogger +{ + /// Log a non-PII message using `MSALLogLevel` (public SDK enum). + func log( + level: MSALLogLevel, + correlationId: UUID? = nil, + message: String, + filename: String = #fileID, + lineNumber: Int = #line, + function: String = #function + ) + { + self.log( + with: msidLogLevel(from: level), + context: nil, + correlationId: correlationId, + containsPII: false, + filename: filename, + lineNumber: UInt(lineNumber), + function: function, + format: "[CredMgmt] %@", + formatArgs: getVaList([message]) + ) + } + + /// Log a PII message (only delivered when masking allows it). + func logPII( + level: MSALLogLevel, + correlationId: UUID? = nil, + message: String, + filename: String = #fileID, + lineNumber: Int = #line, + function: String = #function + ) + { + self.log( + with: msidLogLevel(from: level), + context: nil, + correlationId: correlationId, + containsPII: true, + filename: filename, + lineNumber: UInt(lineNumber), + function: function, + format: "[CredMgmt] %@", + formatArgs: getVaList([message]) + ) + } + + private func msidLogLevel(from msalLevel: MSALLogLevel) -> MSIDLogLevel + { + switch msalLevel + { + case .error: + return .error + case .warning: + return .warning + case .info: + return .info + case .verbose: + return .verbose + case .last: + return .last + case .nothing: + return .nothing + @unknown default: + return .info + } + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/ActivateEnrollmentRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/ActivateEnrollmentRequest.swift new file mode 100644 index 0000000000..dd6b7fd814 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/ActivateEnrollmentRequest.swift @@ -0,0 +1,44 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Request to activate a pending enrollment. +/// POST {activateHref} (absolute or relative URL provided by the service) +internal struct ActivateEnrollmentRequest: CredentialManagementRequestProtocol +{ + let httpMethod = "POST" + let path: String + let accessToken: String + let correlationId: UUID + let body: Data? + + init(activateHref: String, accessToken: String, body: Data, correlationId: UUID) + { + self.path = activateHref + self.accessToken = accessToken + self.body = body + self.correlationId = correlationId + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/BeginEnrollmentRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/BeginEnrollmentRequest.swift new file mode 100644 index 0000000000..3f52d24c8a --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/BeginEnrollmentRequest.swift @@ -0,0 +1,46 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import MSAL + +/// Request to begin enrollment of a new credential method. +/// POST /api/v1.0/me/methods/{type} +internal struct BeginEnrollmentRequest: CredentialManagementRequestProtocol +{ + let httpMethod = "POST" + let path: String + let accessToken: String + let correlationId: UUID + let body: Data? + + init(type: MSALCredentialType, accessToken: String, body: Data?, correlationId: UUID) + { + let serverType = CredentialMethodMapper.serverType(from: type) + self.path = "\(CredentialManagementEndpoints.methodsPath)/\(serverType)" + self.accessToken = accessToken + self.body = body + self.correlationId = correlationId + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/CredentialManagementRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/CredentialManagementRequest.swift new file mode 100644 index 0000000000..28d62a3f17 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/CredentialManagementRequest.swift @@ -0,0 +1,46 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Base protocol for all credential management network requests. +/// Each endpoint defines a concrete type conforming to this protocol. +internal protocol CredentialManagementRequestProtocol +{ + /// HTTP method (GET, POST, DELETE, etc.) + var httpMethod: String { get } + + /// The path or absolute URL for the endpoint. + /// Relative paths are resolved against the base URL. + var path: String { get } + + /// The access token for the Bearer authorization header. + var accessToken: String { get } + + /// Correlation ID for request tracing. + var correlationId: UUID { get } + + /// Optional request body (JSON-encoded). + var body: Data? { get } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/DeleteMethodRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/DeleteMethodRequest.swift new file mode 100644 index 0000000000..73c04f91be --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/DeleteMethodRequest.swift @@ -0,0 +1,45 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import MSAL + +/// Request to delete a credential method. +/// DELETE /api/v1.0/me/methods/{type}/{methodId} +internal struct DeleteMethodRequest: CredentialManagementRequestProtocol +{ + let httpMethod = "DELETE" + let path: String + let accessToken: String + let correlationId: UUID + let body: Data? = nil + + init(type: MSALCredentialType, methodId: String, accessToken: String, correlationId: UUID) + { + let serverType = CredentialMethodMapper.serverType(from: type) + self.path = "\(CredentialManagementEndpoints.methodsPath)/\(serverType)/\(methodId)" + self.accessToken = accessToken + self.correlationId = correlationId + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/ListMethodsRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/ListMethodsRequest.swift new file mode 100644 index 0000000000..f08a927922 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/ListMethodsRequest.swift @@ -0,0 +1,43 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Request to list all credential methods for the current user. +/// GET /api/v1.0/me/methods +internal struct ListMethodsRequest: CredentialManagementRequestProtocol +{ + let httpMethod = "GET" + let path: String + let accessToken: String + let correlationId: UUID + let body: Data? = nil + + init(accessToken: String, correlationId: UUID) + { + self.path = CredentialManagementEndpoints.methodsPath + self.accessToken = accessToken + self.correlationId = correlationId + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_serializer/CredentialManagementRequestSerializer.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_serializer/CredentialManagementRequestSerializer.swift new file mode 100644 index 0000000000..d4feaf5ef0 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_serializer/CredentialManagementRequestSerializer.swift @@ -0,0 +1,68 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Concrete request serializer that transforms typed credential management requests +/// into `URLRequest` instances with appropriate headers. +/// +/// Applies: +/// - Authorization: Bearer {token} +/// - Accept: application/hal+json +/// - client-request-id: {correlationId} +/// - Content-Type: application/json (when body present) +internal final class CredentialManagementRequestSerializer: CredentialManagementRequestSerializing +{ + private let urlResolver: CredentialManagementURLResolver + + init(urlResolver: CredentialManagementURLResolver) + { + self.urlResolver = urlResolver + } + + func serialize(_ request: CredentialManagementRequestProtocol) -> URLRequest? + { + guard let url = urlResolver.resolve(path: request.path) else + { + return nil + } + + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = request.httpMethod + + // Standard headers + urlRequest.setValue("Bearer \(request.accessToken)", forHTTPHeaderField: "Authorization") + urlRequest.setValue("application/hal+json", forHTTPHeaderField: "Accept") + urlRequest.setValue(request.correlationId.uuidString, forHTTPHeaderField: "client-request-id") + + // Body + if let body = request.body + { + urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + urlRequest.httpBody = body + } + + return urlRequest + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_serializer/CredentialManagementRequestSerializing.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_serializer/CredentialManagementRequestSerializing.swift new file mode 100644 index 0000000000..d4b10f5c0a --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_serializer/CredentialManagementRequestSerializing.swift @@ -0,0 +1,33 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Protocol for serializing typed credential management requests into URLRequests. +internal protocol CredentialManagementRequestSerializing +{ + /// Transforms a typed request into a fully configured URLRequest. + /// Returns nil if the request cannot be serialized (e.g., URL validation failure). + func serialize(_ request: CredentialManagementRequestProtocol) -> URLRequest? +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response/CredentialManagementResponse.swift similarity index 52% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClient.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response/CredentialManagementResponse.swift index 0d363916bb..a5402eb8b4 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response/CredentialManagementResponse.swift @@ -24,47 +24,18 @@ import Foundation -/// Protocol defining the HTTP transport interface for credential management API calls. -/// -/// This protocol enables injection of mock implementations for unit testing. -internal protocol CredentialManagementNetworkClient -{ - /// Performs an HTTP request and returns the raw response. - /// - /// - Parameter request: The request to execute. - /// - Returns: The response containing status code, headers, and body data. - /// - Throws: Network-level errors (connectivity, timeout). - func perform(request: CredentialManagementRequest) async throws -> CredentialManagementResponse -} - -/// Represents an HTTP request to the credential management API. -internal struct CredentialManagementRequest +/// Typed response model for credential management HTTP responses. +/// Contains the parsed status code, headers, and optional JSON body. +internal struct CredentialManagementResponse { - let url: URL - let method: HTTPMethod + let statusCode: Int let headers: [String: String] - let body: Data? - - internal enum HTTPMethod: String - { - case get = "GET" - case post = "POST" - case delete = "DELETE" - } + let jsonBody: [String: Any]? + let rawData: Data? - init(url: URL, method: HTTPMethod, headers: [String: String] = [:], body: Data? = nil) + /// Whether the response indicates success (2xx). + var isSuccess: Bool { - self.url = url - self.method = method - self.headers = headers - self.body = body + return (200...299).contains(statusCode) } } - -/// Represents an HTTP response from the credential management API. -internal struct CredentialManagementResponse -{ - let statusCode: Int - let headers: [String: String] - let data: Data? -} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_mapper/EnrollmentResponseMapper.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_mapper/EnrollmentResponseMapper.swift new file mode 100644 index 0000000000..a2db91a166 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_mapper/EnrollmentResponseMapper.swift @@ -0,0 +1,48 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import MSAL + +/// Maps a successful credential management response into a `HALResource`. +/// Used for enrollment (begin and activate) responses. +internal enum EnrollmentResponseMapper +{ + static func map( + _ response: CredentialManagementResponse, + correlationId: UUID + ) -> Result + { + guard let json = response.jsonBody else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Response body is empty or not valid JSON.", + correlationId: correlationId + )) + } + + return .success(HALResource(json: json)) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_mapper/ListMethodsResponseMapper.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_mapper/ListMethodsResponseMapper.swift new file mode 100644 index 0000000000..c1489d441b --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_mapper/ListMethodsResponseMapper.swift @@ -0,0 +1,50 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import MSAL + +/// Maps a successful credential management response containing a methods list +/// into an array of `MSALCredentialMethodProtocol` objects. +internal enum ListMethodsResponseMapper +{ + static func map( + _ response: CredentialManagementResponse, + correlationId: UUID + ) -> Result<[any MSALCredentialMethodProtocol], MSALNativeCredentialManagementError> + { + guard let json = response.jsonBody else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Response body is empty or not valid JSON.", + correlationId: correlationId + )) + } + + let halResource = HALResource(json: json) + let methods = CredentialMethodMapper.parseMethods(from: halResource) + return .success(methods) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_serializer/CredentialManagementHALResponseSerializer.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_serializer/CredentialManagementHALResponseSerializer.swift new file mode 100644 index 0000000000..05c9aa4fbf --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_serializer/CredentialManagementHALResponseSerializer.swift @@ -0,0 +1,72 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Concrete response serializer that parses raw HTTP into `CredentialManagementResponse`. +/// Handles HAL+JSON content by parsing the body as JSON. +internal final class CredentialManagementHALResponseSerializer: CredentialManagementResponseSerializing +{ + func serialize(httpResponse: HTTPURLResponse?, data: Data?) -> CredentialManagementResponse? + { + guard let httpResponse = httpResponse else + { + return nil + } + + let statusCode = httpResponse.statusCode + let headers = httpResponse.allHeaderFields.reduce(into: [String: String]()) + { result, pair in + if let key = pair.key as? String, let value = pair.value as? String + { + result[key] = value + } + } + + // For 204 No Content or empty body, return response without JSON + if statusCode == 204 || data == nil || data?.isEmpty == true + { + return CredentialManagementResponse( + statusCode: statusCode, + headers: headers, + jsonBody: nil, + rawData: data + ) + } + + // Parse JSON body + var jsonBody: [String: Any]? + if let data = data + { + jsonBody = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] + } + + return CredentialManagementResponse( + statusCode: statusCode, + headers: headers, + jsonBody: jsonBody, + rawData: data + ) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_serializer/CredentialManagementResponseSerializing.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_serializer/CredentialManagementResponseSerializing.swift new file mode 100644 index 0000000000..e4fca1af7e --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_serializer/CredentialManagementResponseSerializing.swift @@ -0,0 +1,32 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Protocol for deserializing raw HTTP responses into typed response objects. +internal protocol CredentialManagementResponseSerializing +{ + /// Parses a raw HTTP response into a typed `CredentialManagementResponse`. + func serialize(httpResponse: HTTPURLResponse?, data: Data?) -> CredentialManagementResponse? +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_serializer/MSIDResponseSerializerAdapter.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_serializer/MSIDResponseSerializerAdapter.swift new file mode 100644 index 0000000000..bcc0720734 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_serializer/MSIDResponseSerializerAdapter.swift @@ -0,0 +1,68 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +@_implementationOnly import MSAL_Private + +/// Adapter that bridges the pure-Swift `CredentialManagementHALResponseSerializer` +/// to IdentityCore's `MSIDResponseSerialization` protocol. +/// +/// This is installed on `MSIDHttpRequest.responseSerializer` to integrate with +/// the IdentityCore transport pipeline while keeping parsing logic in pure Swift. +internal final class MSIDResponseSerializerAdapter: NSObject, MSIDResponseSerialization +{ + private let halSerializer = CredentialManagementHALResponseSerializer() + + func responseObject(for httpResponse: HTTPURLResponse?, data: Data?, context: MSIDRequestContext?) throws -> Any + { + guard let response = halSerializer.serialize(httpResponse: httpResponse, data: data) else + { + throw NSError( + domain: "CredentialManagementErrorDomain", + code: -1, + userInfo: [NSLocalizedDescriptionKey: "No HTTP response received."] + ) + } + + // For error status codes, throw so the error handler takes over + guard response.isSuccess else + { + var userInfo: [String: Any] = [ + "statusCode": response.statusCode, + "responseHeaders": response.headers + ] + if let data = data + { + userInfo["responseData"] = data + } + throw NSError( + domain: "CredentialManagementErrorDomain", + code: response.statusCode, + userInfo: userInfo + ) + } + + return response + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift index 557b15ce5a..24fdf4fc85 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift @@ -57,13 +57,6 @@ public class MSALNativeCredentialManagementConfig: NSObject { /// When nil, the client derives the endpoint from the tenant configuration. public var baseURL: URL? - /// Optional custom network provider for HTTP transport. - /// - /// When set, this replaces the default URLSession-based transport (which includes - /// retry logic, interceptor support, and configurable timeouts modeled after MSIDHttpRequest). - /// Use this to inject a mock provider for testing or local development. - public var networkProvider: MSALNativeCredentialManagementNetworkProvider? - public override init() { super.init() diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementNetworkProvider.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementNetworkProvider.swift deleted file mode 100644 index 6e333aac77..0000000000 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementNetworkProvider.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. -// All rights reserved. -// -// This code is licensed under the MIT License. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -/// Represents an HTTP request to the credential management API. -public struct MSALCredentialManagementHTTPRequest -{ - /// The target URL. - public let url: URL - - /// The HTTP method (e.g., "GET", "POST", "DELETE"). - public let method: String - - /// The request headers. - public let headers: [String: String] - - /// The request body, if any. - public let body: Data? - - public init(url: URL, method: String, headers: [String: String], body: Data?) - { - self.url = url - self.method = method - self.headers = headers - self.body = body - } -} - -/// Represents an HTTP response from the credential management API. -public struct MSALCredentialManagementHTTPResponse -{ - /// The HTTP status code. - public let statusCode: Int - - /// The response headers. - public let headers: [String: String] - - /// The response body data. - public let data: Data? - - public init(statusCode: Int, headers: [String: String], data: Data?) - { - self.statusCode = statusCode - self.headers = headers - self.data = data - } -} - -/// Protocol for providing custom HTTP transport to the credential management client. -/// -/// Implement this protocol to inject a mock or custom HTTP layer for testing. -/// When set on `MSALNativeCredentialManagementConfig.networkProvider`, it replaces -/// the default URLSession-based transport. -/// -/// Example (mock): -/// ```swift -/// class MockNetworkProvider: MSALNativeCredentialManagementNetworkProvider { -/// func performRequest(_ request: MSALCredentialManagementHTTPRequest) async throws -/// -> MSALCredentialManagementHTTPResponse { -/// // Return mock HAL+JSON responses -/// } -/// } -/// ``` -public protocol MSALNativeCredentialManagementNetworkProvider -{ - /// Performs an HTTP request and returns the response. - /// - /// - Parameter request: The request to execute. - /// - Returns: The HTTP response. - /// - Throws: Network-level errors. - func performRequest( - _ request: MSALCredentialManagementHTTPRequest - ) async throws -> MSALCredentialManagementHTTPResponse -} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index 2cb06cd12e..eded281aa4 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -24,7 +24,7 @@ import Foundation import MSAL - +@_implementationOnly import MSAL_Private /// Client for managing credential methods of an authenticated CIAM user. /// /// This client provides APIs to list, register, and delete credential methods @@ -68,17 +68,6 @@ public class MSALNativeCredentialMethodsClient: NSObject { qos: .userInitiated ) - if let customProvider = config.networkProvider - { - self.networkClient = NetworkProviderAdapter(provider: customProvider) - } - else - { - self.networkClient = CredentialManagementURLSessionClient( - requestInterceptor: config.requestInterceptor - ) - } - self.apiClient = nil self.pendingActivateHref = nil self.pendingEnrollmentType = nil @@ -98,7 +87,7 @@ public class MSALNativeCredentialMethodsClient: NSObject { { let correlationId = correlationId ?? UUID() - CredentialManagementLogger.log(level: .info, correlationId: correlationId, message: "listCredentialMethods: starting") + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "listCredentialMethods: starting") let tokenResult = await acquireTokenAsync(correlationId: correlationId) guard case .success(let accessToken) = tokenResult else @@ -111,11 +100,10 @@ public class MSALNativeCredentialMethodsClient: NSObject { case .failure(let error): return .failure(error) case .success(let client): - let result = await client.listMethods( + return await client.listMethods( accessToken: accessToken, correlationId: correlationId ) - return result } } @@ -150,7 +138,7 @@ public class MSALNativeCredentialMethodsClient: NSObject { { let correlationId = correlationId ?? UUID() - CredentialManagementLogger.log( + MSIDLogger.shared().log( level: .info, correlationId: correlationId, message: "deleteCredentialMethod: type=\(credentialMethod.credentialType.rawValue)" @@ -167,13 +155,12 @@ public class MSALNativeCredentialMethodsClient: NSObject { case .failure(let error): return .failure(error) case .success(let client): - let result = await client.deleteMethod( + return await client.deleteMethod( type: credentialMethod.credentialType, methodId: credentialMethod.id, accessToken: accessToken, correlationId: correlationId ) - return result } } @@ -181,8 +168,7 @@ public class MSALNativeCredentialMethodsClient: NSObject { internal let config: MSALNativeCredentialManagementConfig internal let operationQueue: DispatchQueue - internal let networkClient: CredentialManagementNetworkClient - internal var apiClient: CredentialManagementAPIClient? + internal var apiClient: (any CredentialManagementNetworkClientProtocol)? internal var pendingActivateHref: String? internal var pendingEnrollmentType: MSALCredentialType? } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyCredentialMethod.swift index 028b25799f..14bebcd3cd 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyCredentialMethod.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyCredentialMethod.swift @@ -26,7 +26,7 @@ import Foundation extension MSALCredentialType { /// Passkey (FIDO2/WebAuthn) credential type. - public static let passkey = MSALCredentialType("passkey") + public static let passkey = MSALCredentialType("fido") } /// Represents a passkey (FIDO2/WebAuthn) credential method. diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 25ea3305b0..19126fc516 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -33,9 +33,14 @@ class CredentialManagementViewModel: ObservableObject { @Published var showChallengeInput = false @Published var challengeHint: String = "" - // Toggle: real server vs mock - @Published var useMockAPI: Bool = true { + // Toggle: real server vs mock (backed by UserDefaults — SDK reads this internally) + @Published var useMockAPI: Bool = UserDefaults.standard.bool( + forKey: "com.microsoft.identity.credentialmanagement.useMockAPI" + ) { didSet { + UserDefaults.standard.set(useMockAPI, forKey: "com.microsoft.identity.credentialmanagement.useMockAPI") + // Invalidate cached client so next call picks up new mode + credClient = nil reinitializeClient() } } @@ -76,21 +81,16 @@ class CredentialManagementViewModel: ObservableObject { credConfig.tokenProvider = tokenProvider credConfig.baseURL = URL(string: Configuration.credentialManagementBaseURL) - // 5. Inject mock or use default real network - if useMockAPI { - credConfig.networkProvider = MockNetworkProvider() - } - credClient = try MSALNativeCredentialMethodsClient(config: credConfig) - let mode = useMockAPI ? "Mock API" : "Real Server" + let mode = useMockAPI ? "Mock API (UserDefaults)" : "Real Server" statusMessage = "SDK initialized (\(mode))." } catch { errorMessage = "Failed to initialize: \(error.localizedDescription)" } } - // MARK: - Sign In (Fake for POC) + // MARK: - Sign In func signIn(email: String, password: String) { isLoading = true diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MockNetworkProvider.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MockNetworkProvider.swift deleted file mode 100644 index 96a45dbe93..0000000000 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MockNetworkProvider.swift +++ /dev/null @@ -1,163 +0,0 @@ -// -// MockNetworkProvider.swift -// MSALNativeCredManagmentSampleApp -// -// Created by Serhii Demchenko on 2026-06-04. -// - -import Foundation -import MSALNativeCredManagment - -/// A local mock network provider that returns fake HAL+JSON responses. -/// -/// Used for development and testing without a live server. Toggle between -/// this and the real server implementation via the sample app settings. -class MockNetworkProvider: MSALNativeCredentialManagementNetworkProvider { - - func performRequest( - _ request: MSALCredentialManagementHTTPRequest - ) async throws -> MSALCredentialManagementHTTPResponse { - // Simulate a brief network delay - try await Task.sleep(nanoseconds: 300_000_000) // 0.3s - - let path = request.url.path - - switch (request.method, path) { - case ("GET", let p) where p.contains("/me/methods"): - return listMethodsResponse() - case ("POST", let p) where p.contains("/activate"): - return activateResponse() - case ("POST", let p) where p.contains("/me/methods/phone"): - return enrollPhoneResponse() - case ("POST", let p) where p.contains("/me/methods/fido"): - return enrollPasskeyResponse() - case ("POST", let p) where p.contains("/me/methods/password"): - return enrollPasswordResponse() - case ("DELETE", _): - return MSALCredentialManagementHTTPResponse(statusCode: 204, headers: [:], data: nil) - default: - return MSALCredentialManagementHTTPResponse(statusCode: 404, headers: [:], data: nil) - } - } - - // MARK: - Mock Responses - - private func listMethodsResponse() -> MSALCredentialManagementHTTPResponse { - let json: [String: Any] = [ - "_embedded": [ - "methods": [ - [ - "id": "mock-phone-001", - "type": "phone", - "displayName": "+1 •••• 5678", - "createdDateTime": "2025-01-15T10:30:00Z", - "_links": [ - "self": ["href": "/api/v1.0/me/methods/phone/mock-phone-001"] - ] - ], - [ - "id": "mock-passkey-001", - "type": "fido", - "displayName": "iPhone Passkey", - "createdDateTime": "2025-02-01T14:00:00Z", - "aaGuid": "00000000-0000-0000-0000-000000000001", - "_links": [ - "self": ["href": "/api/v1.0/me/methods/fido/mock-passkey-001"] - ] - ], - [ - "id": "mock-password-001", - "type": "password", - "displayName": "Password", - "createdDateTime": "2024-12-01T09:00:00Z", - "_links": [ - "self": ["href": "/api/v1.0/me/methods/password/mock-password-001"] - ] - ] - ] - ], - "_links": [ - "self": ["href": "/api/v1.0/me/methods"], - "enroll": [ - ["href": "/api/v1.0/me/methods/phone", "name": "phone"], - ["href": "/api/v1.0/me/methods/fido", "name": "fido"], - ["href": "/api/v1.0/me/methods/password", "name": "password"] - ] - ] - ] - return jsonResponse(statusCode: 200, json: json) - } - - private func enrollPhoneResponse() -> MSALCredentialManagementHTTPResponse { - let json: [String: Any] = [ - "state": "challengeRequired", - "continuationToken": "mock-continuation-token-phone", - "challengeChannel": "sms", - "challengeTargetLabel": "+1 •••• 5678", - "codeLength": 6, - "_links": [ - "activate": ["href": "/api/v1.0/me/methods/phone/mock-new-phone/activate"] - ] - ] - return jsonResponse(statusCode: 200, json: json) - } - - private func enrollPasskeyResponse() -> MSALCredentialManagementHTTPResponse { - let json: [String: Any] = [ - "state": "completed", - "method": [ - "id": "mock-passkey-new", - "type": "fido", - "displayName": "New Passkey", - "createdDateTime": "2025-06-04T12:00:00Z", - "_links": [ - "self": ["href": "/api/v1.0/me/methods/fido/mock-passkey-new"] - ] - ] - ] - return jsonResponse(statusCode: 201, json: json) - } - - private func enrollPasswordResponse() -> MSALCredentialManagementHTTPResponse { - let json: [String: Any] = [ - "state": "completed", - "method": [ - "id": "mock-password-new", - "type": "password", - "displayName": "Password", - "createdDateTime": "2025-06-04T12:00:00Z", - "_links": [ - "self": ["href": "/api/v1.0/me/methods/password/mock-password-new"] - ] - ] - ] - return jsonResponse(statusCode: 201, json: json) - } - - private func activateResponse() -> MSALCredentialManagementHTTPResponse { - let json: [String: Any] = [ - "state": "completed", - "method": [ - "id": "mock-activated-001", - "type": "phone", - "displayName": "+1 •••• 5678", - "createdDateTime": "2025-06-04T12:00:00Z", - "_links": [ - "self": ["href": "/api/v1.0/me/methods/phone/mock-activated-001"] - ] - ] - ] - return jsonResponse(statusCode: 200, json: json) - } - - // MARK: - Helpers - - private func jsonResponse(statusCode: Int, json: [String: Any]) -> MSALCredentialManagementHTTPResponse { - let data = try? JSONSerialization.data(withJSONObject: json) - return MSALCredentialManagementHTTPResponse( - statusCode: statusCode, - headers: ["Content-Type": "application/hal+json"], - data: data - ) - } -} diff --git a/lib/native/MSALNativeCredManagment/Package.swift b/lib/native/MSALNativeCredManagment/Package.swift index dd92e28d31..f5e439f273 100644 --- a/lib/native/MSALNativeCredManagment/Package.swift +++ b/lib/native/MSALNativeCredManagment/Package.swift @@ -2,6 +2,33 @@ // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription +import Foundation + +// Compute absolute path to MSAL root from this Package.swift location +let packageDir = URL(fileURLWithPath: #filePath).deletingLastPathComponent().path +let msalRoot = URL(fileURLWithPath: packageDir + "/../../../MSAL").standardized.path + +// Dynamically discover all subdirectories containing .h files. +// This is necessary because IdentityCore headers use bare `#import "filename.h"` without relative paths. +func findHeaderDirs(in basePath: String) -> [String] { + let fm = FileManager.default + guard let enumerator = fm.enumerator(atPath: basePath) else { return [] } + var dirs = Set() + dirs.insert(basePath) + while let item = enumerator.nextObject() as? String { + if item.hasSuffix(".h") { + let dir = (item as NSString).deletingLastPathComponent + if !dir.isEmpty { + dirs.insert(basePath + "/" + dir) + } + } + } + return Array(dirs).sorted() +} + +let allHeaderDirs = findHeaderDirs(in: msalRoot + "/IdentityCore/IdentityCore/src") + + findHeaderDirs(in: msalRoot + "/src") + + [msalRoot] let package = Package( name: "MSALNativeCredManagment", @@ -25,7 +52,13 @@ let package = Package( dependencies: [ .product(name: "MSAL", package: "microsoft-authentication-library-for-objc") ], - path: "MSALNativeCredManagment/src" + path: "MSALNativeCredManagment/src", + swiftSettings: [ + .unsafeFlags( + ["-Xcc", "-fmodule-map-file=\(packageDir)/CMSAL_Private/include/module.modulemap"] + + allHeaderDirs.flatMap { ["-Xcc", "-I\($0)"] } + ) + ] ), .testTarget( name: "MSALNativeCredManagmentTests", From 2a407032d0952c8b94a0fc4338ba8f64261e2bad Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Wed, 3 Jun 2026 18:06:47 -0700 Subject: [PATCH 38/42] Refactor network layer. --- ...tiveCredentialMethodsClient+Internal.swift | 85 +++-- .../MSALCredentialMethod.swift | 0 ...dentialMethodsClient+RegisterPasskey.swift | 57 +--- ...entialMethodsClient+RegisterPassword.swift | 57 ++-- ...redentialMethodsClient+RegisterPhone.swift | 68 ++-- .../CredentialManagementMockAPIClient.swift | 320 ------------------ ...ntialManagementNetworkClientProtocol.swift | 59 +++- .../CredentialManagementEnvironment.swift | 0 ...redentialManagementMockNetworkClient.swift | 276 +++++++++++++++ .../CredentialManagementErrorHandler.swift | 0 ...redentialManagementInterceptorBridge.swift | 0 ...dentialManagementRequestConfigurator.swift | 0 ...entialManagementServerNetworkClient.swift} | 161 ++++++++- .../CredentialManagementURLResolver.swift | 0 .../server}/CredentialMethodMapper.swift | 0 .../{hal => network/server}/HALResource.swift | 0 .../MSIDLogger+CredentialManagement.swift | 0 .../request/ActivateEnrollmentRequest.swift | 0 .../request/BeginEnrollmentRequest.swift | 0 .../request/CredentialManagementRequest.swift | 0 .../request/DeleteMethodRequest.swift | 0 .../request/ListMethodsRequest.swift | 0 ...redentialManagementRequestSerializer.swift | 0 ...edentialManagementRequestSerializing.swift | 0 .../CredentialManagementResponse.swift | 0 .../EnrollmentResponseMapper.swift | 0 .../ListMethodsResponseMapper.swift | 0 ...ntialManagementHALResponseSerializer.swift | 0 ...dentialManagementResponseSerializing.swift | 0 .../MSIDResponseSerializerAdapter.swift | 0 .../src/internal/util/Result+Extensions.swift | 18 - .../MSALNativeCredentialMethodsClient.swift | 6 +- 32 files changed, 582 insertions(+), 525 deletions(-) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/{ => credential_method}/MSALCredentialMethod.swift (100%) delete mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/mock/CredentialManagementMockAPIClient.swift rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/{ => network}/mock/CredentialManagementEnvironment.swift (100%) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/mock/CredentialManagementMockNetworkClient.swift rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => server}/CredentialManagementErrorHandler.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => server}/CredentialManagementInterceptorBridge.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => server}/CredentialManagementRequestConfigurator.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{CredentialManagementAPIClient.swift => server/CredentialManagementServerNetworkClient.swift} (55%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => server}/CredentialManagementURLResolver.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/{hal => network/server}/CredentialMethodMapper.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/{hal => network/server}/HALResource.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => server}/MSIDLogger+CredentialManagement.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => server}/request/ActivateEnrollmentRequest.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => server}/request/BeginEnrollmentRequest.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => server}/request/CredentialManagementRequest.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => server}/request/DeleteMethodRequest.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => server}/request/ListMethodsRequest.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => server}/request_serializer/CredentialManagementRequestSerializer.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => server}/request_serializer/CredentialManagementRequestSerializing.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => server}/response/CredentialManagementResponse.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => server}/response_mapper/EnrollmentResponseMapper.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => server}/response_mapper/ListMethodsResponseMapper.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => server}/response_serializer/CredentialManagementHALResponseSerializer.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => server}/response_serializer/CredentialManagementResponseSerializing.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => server}/response_serializer/MSIDResponseSerializerAdapter.swift (100%) delete mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/util/Result+Extensions.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift index 9faab17ed4..4b78b2d6b0 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift @@ -97,7 +97,7 @@ extension MSALNativeCredentialMethodsClient /// Client selection priority: /// 1. Mock API client — when `UserDefaults` key /// `com.microsoft.identity.credentialmanagement.useMockAPI` is `true`. - /// 2. Default (`CredentialManagementAPIClient`) — MSIDHttpRequest-backed URLSession transport. + /// 2. Default (`CredentialManagementServerNetworkClient`) — MSIDHttpRequest-backed URLSession transport. /// /// The mock switch is evaluated on every call so toggling UserDefaults at runtime /// takes effect on the next API call (the cached client is invalidated when the @@ -109,7 +109,7 @@ extension MSALNativeCredentialMethodsClient // Invalidate cached client if mock state changed if let existing = apiClient { - let cachedIsMock = existing is CredentialManagementMockAPIClient + let cachedIsMock = existing is CredentialManagementMockNetworkClient if cachedIsMock == useMock { return .success(existing) @@ -121,7 +121,7 @@ extension MSALNativeCredentialMethodsClient // Mock API takes precedence — no config validation needed if useMock { - let mockClient = CredentialManagementMockAPIClient() + let mockClient = CredentialManagementMockNetworkClient() self.apiClient = mockClient return .success(mockClient) } @@ -138,7 +138,7 @@ extension MSALNativeCredentialMethodsClient urlResolver: CredentialManagementURLResolver(baseURL: baseURL) ) - let client = CredentialManagementAPIClient( + let client = CredentialManagementServerNetworkClient( requestSerializer: requestSerializer, requestInterceptor: config.requestInterceptor ) @@ -168,7 +168,7 @@ extension MSALNativeCredentialMethodsClient let tokenResult = await acquireTokenAsync(correlationId: correlationId) guard case .success(let accessToken) = tokenResult else { - return .failure(tokenResult.failureValue!) + return .failure({ if case .failure(let e) = tokenResult { return e }; fatalError("Unreachable") }()) } let clientResult = getAPIClient() @@ -193,19 +193,8 @@ extension MSALNativeCredentialMethodsClient )) } - // Use the activate href stored in the challenge state - // The activate link was captured during enrollment - guard let activateHref = pendingActivateHref else - { - return .failure(MSALNativeCredentialManagementError( - type: .generalError, - message: "No pending activation link found.", - correlationId: correlationId - )) - } - let result = await apiClientInstance.activateEnrollment( - activateHref: activateHref, + continuationToken: continuationToken, accessToken: accessToken, body: bodyData, correlationId: correlationId @@ -213,19 +202,9 @@ extension MSALNativeCredentialMethodsClient switch result { - case .success(let halResource): - let json = halResource.properties - guard let method = CredentialMethodMapper.parseMethod(from: json) else - { - return .failure(MSALNativeCredentialManagementError( - type: .generalError, - message: "Failed to parse registered method from activation response.", - correlationId: correlationId - )) - } - self.pendingActivateHref = nil + case .success(let method): + self.pendingEnrollmentType = nil return .success(method) - case .failure(let error): return .failure(error) } @@ -240,7 +219,7 @@ extension MSALNativeCredentialMethodsClient let tokenResult = await acquireTokenAsync(correlationId: correlationId) guard case .success(let accessToken) = tokenResult else { - return .failure(tokenResult.failureValue!) + return .failure({ if case .failure(let e) = tokenResult { return e }; fatalError("Unreachable") }()) } let clientResult = getAPIClient() @@ -272,23 +251,39 @@ extension MSALNativeCredentialMethodsClient switch result { - case .success(let halResource): - let newContinuationToken = halResource.string(forKey: "continuationToken") ?? continuationToken - - if let activateLink = halResource.link(rel: "activate") + case .success(let response): + switch response { - self.pendingActivateHref = activateLink.href - } + case .challengeRequired(let challengeInfo): + let newState = MSALCredentialMethodChallengeState( + sentTo: challengeInfo.sentTo, + channelType: challengeInfo.channelType, + codeLength: challengeInfo.codeLength, + continuationToken: challengeInfo.continuationToken, + client: self, + correlationId: correlationId + ) + return .success(newState) + + case .completed(_): + // Unlikely on resend, but handle gracefully + let state = MSALCredentialMethodChallengeState( + sentTo: nil, + channelType: nil, + codeLength: nil, + continuationToken: continuationToken, + client: self, + correlationId: correlationId + ) + return .success(state) - let newState = MSALCredentialMethodChallengeState( - sentTo: halResource.string(forKey: "sentTo"), - channelType: halResource.string(forKey: "channelType"), - codeLength: halResource.properties["codeLength"] as? Int, - continuationToken: newContinuationToken, - client: self, - correlationId: correlationId - ) - return .success(newState) + case .passkeyCreationRequired: + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Unexpected passkey creation response on resend.", + correlationId: correlationId + )) + } case .failure(let error): return .failure(error) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/MSALCredentialMethod.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALCredentialMethod.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/MSALCredentialMethod.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift index 33ec43fb67..3e8a111cde 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift @@ -47,7 +47,7 @@ extension MSALNativeCredentialMethodsClient let tokenResult = await acquireTokenAsync(correlationId: correlationId) guard case .success(let accessToken) = tokenResult else { - return .failure(tokenResult.failureValue!) + return .failure({ if case .failure(let e) = tokenResult { return e }; fatalError("Unreachable") }()) } // Step 2: Begin enrollment to get creation options from server @@ -63,41 +63,28 @@ extension MSALNativeCredentialMethodsClient correlationId: correlationId ) - guard case .success(let halResource) = enrollResult else - { - return .failure(enrollResult.failureValue!) - } - - // Parse the server response for WebAuthn creation options - guard let publicKeyDict = halResource.properties["publicKey"] as? [String: Any] else - { - return .failure(MSALNativeCredentialManagementError( - type: .generalError, - message: "Server did not return publicKey creation options.", - correlationId: correlationId - )) - } - - guard let continuationToken = halResource.string(forKey: "continuationToken") else - { - return .failure(MSALNativeCredentialManagementError( - type: .generalError, - message: "Server did not return continuationToken.", - correlationId: correlationId - )) - } + guard case .success(let response) = enrollResult else + { + if case .failure(let resultError) = enrollResult { return .failure(resultError) } + fatalError("Unreachable") + } - guard let activateLink = halResource.link(rel: "activate") else + // Expect passkey creation options from the server + guard case .passkeyCreationRequired(let passkeyInfo) = response else { + if case .completed(let method) = response + { + return .success(.completed(method)) + } return .failure(MSALNativeCredentialManagementError( type: .generalError, - message: "Server did not return activate link.", + message: "Server did not return passkey creation options.", correlationId: correlationId )) } // Parse creation options from server response - let creationOptions = parseCreationOptions(from: publicKeyDict) + let creationOptions = parseCreationOptions(from: passkeyInfo.publicKey) // Step 3: Invoke platform authenticator let handler = MSALPasskeyAuthorizationHandler(anchor: params.presentationAnchor) @@ -117,9 +104,9 @@ extension MSALNativeCredentialMethodsClient return .failure(credError) } - // Step 4: Submit attestation to server via activate link + // Step 4: Submit attestation to server via activate let activationBody: [String: Any] = [ - "continuationToken": continuationToken, + "continuationToken": passkeyInfo.continuationToken, "displayName": params.displayName ?? "Passkey", "publicKeyCredential": [ "id": attestation.credentialId.base64EncodedString(), @@ -140,7 +127,7 @@ extension MSALNativeCredentialMethodsClient } let activateResult = await client.activateEnrollment( - activateHref: activateLink.href, + continuationToken: passkeyInfo.continuationToken, accessToken: accessToken, body: bodyData, correlationId: correlationId @@ -148,15 +135,7 @@ extension MSALNativeCredentialMethodsClient switch activateResult { - case .success(let resultResource): - guard let method = CredentialMethodMapper.parseMethod(from: resultResource.properties) else - { - return .failure(MSALNativeCredentialManagementError( - type: .generalError, - message: "Failed to parse registered passkey from activation response.", - correlationId: correlationId - )) - } + case .success(let method): MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "performRegisterPasskey: completed") return .success(.completed(method)) case .failure(let error): diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift index 9e19e0f30c..3a053abb49 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift @@ -39,7 +39,7 @@ extension MSALNativeCredentialMethodsClient let tokenResult = await acquireTokenAsync(correlationId: correlationId) guard case .success(let accessToken) = tokenResult else { - return .failure(tokenResult.failureValue!) + return .failure({ if case .failure(let e) = tokenResult { return e }; fatalError("Unreachable") }()) } switch getAPIClient() @@ -67,52 +67,35 @@ extension MSALNativeCredentialMethodsClient switch enrollResult { - case .success(let halResource): - let state = halResource.string(forKey: "state") - - // Password registration typically completes in one step - if state == "completed" || halResource.link(rel: "activate") == nil + case .success(let response): + switch response { - guard let method = CredentialMethodMapper.parseMethod(from: halResource.properties) else - { - // If server returned state=completed but no parseable method, - // create a password method from what we know - let fallbackMethod = MSALPasswordCredentialMethod( - id: halResource.string(forKey: "id") ?? UUID().uuidString, - createdAt: Date() - ) - return .success(.completed(fallbackMethod)) - } + case .completed(let method): MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "performRegisterPassword: completed") return .success(.completed(method)) - } - // If server requires activation (unlikely for password but handle gracefully) - guard let continuationToken = halResource.string(forKey: "continuationToken") else - { + case .challengeRequired(let challengeInfo): + // Unlikely for password but handle gracefully + self.pendingEnrollmentType = .password + + let challengeState = MSALCredentialMethodChallengeState( + sentTo: challengeInfo.sentTo, + channelType: challengeInfo.channelType, + codeLength: challengeInfo.codeLength, + continuationToken: challengeInfo.continuationToken, + client: self, + correlationId: correlationId + ) + return .success(.challengeRequired(challengeState)) + + case .passkeyCreationRequired: return .failure(MSALNativeCredentialManagementError( type: .generalError, - message: "Server did not return continuationToken for password enrollment.", + message: "Unexpected passkey creation response for password enrollment.", correlationId: correlationId )) } - if let activateLink = halResource.link(rel: "activate") - { - self.pendingActivateHref = activateLink.href - } - self.pendingEnrollmentType = .password - - let challengeState = MSALCredentialMethodChallengeState( - sentTo: halResource.string(forKey: "sentTo"), - channelType: halResource.string(forKey: "channelType"), - codeLength: halResource.properties["codeLength"] as? Int, - continuationToken: continuationToken, - client: self, - correlationId: correlationId - ) - return .success(.challengeRequired(challengeState)) - case .failure(let error): return .failure(error) } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift index 218c29285f..c7927784ee 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift @@ -39,7 +39,7 @@ extension MSALNativeCredentialMethodsClient let tokenResult = await acquireTokenAsync(correlationId: correlationId) guard case .success(let accessToken) = tokenResult else { - return .failure(tokenResult.failureValue!) + return .failure({ if case .failure(let e) = tokenResult { return e }; fatalError("Unreachable") }()) } switch getAPIClient() @@ -67,56 +67,40 @@ extension MSALNativeCredentialMethodsClient switch enrollResult { - case .success(let halResource): - let state = halResource.string(forKey: "state") - - // If server returned state=completed, the method is already registered - if state == "completed" + case .success(let response): + switch response { - guard let method = CredentialMethodMapper.parseMethod(from: halResource.properties) else - { - return .failure(MSALNativeCredentialManagementError( - type: .generalError, - message: "Failed to parse phone method from enrollment response.", - correlationId: correlationId - )) - } + case .completed(let method): + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "performRegisterPhoneNumber: completed immediately") return .success(.completed(method)) - } - // Otherwise, server sent a challenge (OTP to the phone) - guard let continuationToken = halResource.string(forKey: "continuationToken") else - { + case .challengeRequired(let challengeInfo): + self.pendingEnrollmentType = .phone + + let challengeState = MSALCredentialMethodChallengeState( + sentTo: challengeInfo.sentTo ?? params.phoneNumber, + channelType: challengeInfo.channelType ?? "sms", + codeLength: challengeInfo.codeLength ?? 6, + continuationToken: challengeInfo.continuationToken, + client: self, + correlationId: correlationId + ) + + MSIDLogger.shared().log( + level: .info, + correlationId: correlationId, + message: "performRegisterPhoneNumber: challenge sent" + ) + return .success(.challengeRequired(challengeState)) + + case .passkeyCreationRequired: return .failure(MSALNativeCredentialManagementError( type: .generalError, - message: "Server did not return continuationToken for phone enrollment.", + message: "Unexpected passkey creation response for phone enrollment.", correlationId: correlationId )) } - // Store the activate link for later submission - if let activateLink = halResource.link(rel: "activate") - { - self.pendingActivateHref = activateLink.href - } - self.pendingEnrollmentType = .phone - - let challengeState = MSALCredentialMethodChallengeState( - sentTo: halResource.string(forKey: "sentTo") ?? params.phoneNumber, - channelType: halResource.string(forKey: "channelType") ?? "sms", - codeLength: halResource.properties["codeLength"] as? Int ?? 6, - continuationToken: continuationToken, - client: self, - correlationId: correlationId - ) - - MSIDLogger.shared().log( - level: .info, - correlationId: correlationId, - message: "performRegisterPhoneNumber: challenge sent" - ) - return .success(.challengeRequired(challengeState)) - case .failure(let error): return .failure(error) } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/mock/CredentialManagementMockAPIClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/mock/CredentialManagementMockAPIClient.swift deleted file mode 100644 index a7ec26f2b2..0000000000 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/mock/CredentialManagementMockAPIClient.swift +++ /dev/null @@ -1,320 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. -// All rights reserved. -// -// This code is licensed under the MIT License. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import MSAL -@_implementationOnly import MSAL_Private - -/// Internal mock API client that returns simulated responses without hitting the server. -/// -/// Activated via UserDefaults key `com.microsoft.identity.credentialmanagement.useMockAPI`. -/// This is entirely internal — no public API exposes this client or the switch mechanism. -/// -/// The mock maintains in-memory state so that enrollments and deletions are reflected -/// in subsequent `listMethods` calls within the same session. -internal final class CredentialManagementMockAPIClient: CredentialManagementNetworkClientProtocol -{ - /// Simulates network latency. Configurable via UserDefaults key - /// `com.microsoft.identity.credentialmanagement.mockDelaySeconds` (default: 0.5). - private var simulatedDelay: TimeInterval - { - let delay = UserDefaults.standard.double( - forKey: "com.microsoft.identity.credentialmanagement.mockDelaySeconds" - ) - return delay > 0 ? delay : 0.5 - } - - /// In-memory store of credential methods. Mutations (enroll/delete) persist for the - /// lifetime of this client instance. - private var methods: [any MSALCredentialMethodProtocol] - - /// Tracks the pending enrollment type between beginEnrollment and activateEnrollment. - private var pendingEnrollmentType: MSALCredentialType? - - /// Tracks the user-provided phone number from the enrollment body. - private var pendingPhoneNumber: String? - - init() - { - self.methods = Self.seedMethods() - - MSIDLogger.shared().log( - level: .warning, - correlationId: UUID(), - message: "⚠️ Mock API client is active. All credential management calls will return simulated data." - ) - } - - // MARK: - List Methods - - func listMethods( - accessToken: String, - correlationId: UUID - ) async -> Result<[any MSALCredentialMethodProtocol], MSALNativeCredentialManagementError> - { - await simulateNetworkDelay() - - MSIDLogger.shared().log( - level: .info, - correlationId: correlationId, - message: "[Mock] listMethods called — returning \(methods.count) methods" - ) - - return .success(methods) - } - - // MARK: - Begin Enrollment - - func beginEnrollment( - type: MSALCredentialType, - accessToken: String, - body: Data?, - correlationId: UUID - ) async -> Result - { - await simulateNetworkDelay() - - MSIDLogger.shared().log( - level: .info, - correlationId: correlationId, - message: "[Mock] beginEnrollment called for type=\(type.rawValue)" - ) - - // Track the pending enrollment so activateEnrollment returns the correct type - pendingEnrollmentType = type - - // Extract phone number from body if present - if type == .phone, let body = body, - let json = try? JSONSerialization.jsonObject(with: body) as? [String: Any], - let phone = json["phoneNumber"] as? String - { - pendingPhoneNumber = phone - } - - let mockResponse = buildMockEnrollmentResponse(type: type) - return .success(mockResponse) - } - - // MARK: - Activate Enrollment - - func activateEnrollment( - activateHref: String, - accessToken: String, - body: Data, - correlationId: UUID - ) async -> Result - { - await simulateNetworkDelay() - - MSIDLogger.shared().log( - level: .info, - correlationId: correlationId, - message: "[Mock] activateEnrollment called" - ) - - let enrolledType = pendingEnrollmentType ?? .passkey - let newId = "mock-enrolled-\(UUID().uuidString.prefix(8))" - - // Extract displayName from the activation request body (provided by the user) - let bodyJson = (try? JSONSerialization.jsonObject(with: body)) as? [String: Any] - let userDisplayName = bodyJson?["displayName"] as? String - - // Add the correct credential type to the in-memory store - let newMethod: any MSALCredentialMethodProtocol - switch enrolledType - { - case .phone: - let phoneDisplay = pendingPhoneNumber ?? "+1 (555) ***-0000" - newMethod = MSALPhoneCredentialMethod( - id: newId, - createdAt: nil, - phoneNumber: phoneDisplay - ) - case .password: - newMethod = MSALPasswordCredentialMethod( - id: newId, - createdAt: nil - ) - default: - newMethod = MSALPasskeyCredentialMethod( - id: newId, - displayName: userDisplayName ?? "Passkey", - createdAt: nil, - credentialID: "mock-cred-\(UUID().uuidString.prefix(8))", - aaguid: nil - ) - } - methods.append(newMethod) - - // Clear pending state - let mockResponse = buildMockActivationResponse(type: enrolledType, id: newId, displayName: userDisplayName) - pendingEnrollmentType = nil - pendingPhoneNumber = nil - - return .success(mockResponse) - } - - // MARK: - Delete Method - - func deleteMethod( - type: MSALCredentialType, - methodId: String, - accessToken: String, - correlationId: UUID - ) async -> Result - { - await simulateNetworkDelay() - - MSIDLogger.shared().log( - level: .info, - correlationId: correlationId, - message: "[Mock] deleteMethod called for type=\(type.rawValue), id=\(methodId)" - ) - - // Remove from in-memory store so subsequent listMethods reflects the deletion - methods.removeAll { $0.id == methodId } - - return .success(()) - } - - // MARK: - Private: Seed Data - - private static func seedMethods() -> [any MSALCredentialMethodProtocol] - { - return [ - MSALPasskeyCredentialMethod( - id: "mock-passkey-001", - displayName: "Mock Passkey", - createdAt: nil, - credentialID: "mock-credential-id-abc", - aaguid: "00000000-0000-0000-0000-000000000001" - ), - MSALPhoneCredentialMethod( - id: "mock-phone-001", - createdAt: nil, - phoneNumber: "+1 (555) 123-4567" - ), - MSALPasswordCredentialMethod( - id: "mock-password-001", - createdAt: nil - ) - ] - } - - // MARK: - Private: Mock Response Builders - - private func buildMockEnrollmentResponse(type: MSALCredentialType) -> HALResource - { - if type == .password - { - // Password completes in one step — no challenge/activation needed - let newId = "mock-enrolled-\(UUID().uuidString.prefix(8))" - let newMethod = MSALPasswordCredentialMethod(id: newId, createdAt: nil) - methods.append(newMethod) - - let json: [String: Any] = [ - "id": newId, - "type": type.rawValue, - "state": "completed", - "displayName": "Password" - ] - return HALResource(json: json) - } - - var json: [String: Any] = [ - "continuationToken": "mock-continuation-token-\(UUID().uuidString.prefix(8))", - "_links": [ - "activate": ["href": "https://mock.credentialmanagement.microsoft.com/activate"] - ] - ] - - if type == .phone - { - // Use the actual phone number provided by the user (masked for display) - let maskedPhone: String - if let phone = pendingPhoneNumber, phone.count > 4 - { - let last4 = String(phone.suffix(4)) - maskedPhone = "••• \(last4)" - } - else - { - maskedPhone = pendingPhoneNumber ?? "+1 (555) ***-0000" - } - json["sentTo"] = maskedPhone - json["channelType"] = "sms" - json["codeLength"] = 6 - } - else if type == .passkey - { - // Provide mock WebAuthn creation options so the passkey flow can parse them - json["publicKey"] = [ - "challenge": Data("mock-challenge-\(UUID().uuidString)".utf8).base64EncodedString(), - "rp": [ - "id": "login.microsoft.com", - "name": "Microsoft" - ], - "user": [ - "id": Data("mock-user-id".utf8).base64EncodedString(), - "name": "user@contoso.com", - "displayName": "Mock User" - ] - ] - } - - return HALResource(json: json) - } - - private func buildMockActivationResponse(type: MSALCredentialType, id: String, displayName: String?) -> HALResource - { - var json: [String: Any] = [ - "id": id, - "type": type.rawValue - ] - - switch type - { - case .phone: - json["displayName"] = pendingPhoneNumber ?? "Phone" - json["phoneNumber"] = pendingPhoneNumber ?? "+1 (555) 000-0000" - case .password: - json["displayName"] = "Password" - default: - json["displayName"] = displayName ?? "Passkey" - } - - return HALResource(json: json) - } - - // MARK: - Private: Delay Simulation - - private func simulateNetworkDelay() async - { - let delay = simulatedDelay - if delay > 0 - { - try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) - } - } -} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClientProtocol.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClientProtocol.swift index c425897085..c5fda2fe63 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClientProtocol.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClientProtocol.swift @@ -25,29 +25,44 @@ import Foundation import MSAL -/// Internal protocol that unifies the MSIDHttpRequest-based client and the custom -/// network provider client behind a single interface. +// MARK: - Protocol + +/// Internal protocol that unifies the server-backed client and the mock client +/// behind a single HAL-free interface. +/// +/// Implementations own the transport details (HAL parsing, link tracking, etc.) +/// and expose only typed domain models to callers. internal protocol CredentialManagementNetworkClientProtocol { + /// List all credential methods for the authenticated user. func listMethods( accessToken: String, correlationId: UUID ) async -> Result<[any MSALCredentialMethodProtocol], MSALNativeCredentialManagementError> + /// Begin enrollment of a new credential method. + /// + /// Returns a typed `EnrollmentBeginResponse` that tells the caller whether + /// enrollment completed, a challenge is required, or passkey creation options are available. func beginEnrollment( type: MSALCredentialType, accessToken: String, body: Data?, correlationId: UUID - ) async -> Result + ) async -> Result + /// Activate (complete) an enrollment that required a second step. + /// + /// The `continuationToken` identifies the pending enrollment. The implementation + /// resolves any internal resource context (e.g., HAL links) from its in-memory store. func activateEnrollment( - activateHref: String, + continuationToken: String, accessToken: String, body: Data, correlationId: UUID - ) async -> Result + ) async -> Result + /// Delete a credential method. func deleteMethod( type: MSALCredentialType, methodId: String, @@ -55,3 +70,37 @@ internal protocol CredentialManagementNetworkClientProtocol correlationId: UUID ) async -> Result } + +// MARK: - Response Types + +/// HAL-free result of a `beginEnrollment` call. +/// +/// Callers use this typed response instead of parsing raw HAL resources. +/// The HAL-specific link/resource context is managed internally by the server layer. +internal enum EnrollmentBeginResponse +{ + /// Enrollment completed in one step (e.g., password). + case completed(any MSALCredentialMethodProtocol) + + /// A verification challenge was sent (e.g., OTP to phone). + case challengeRequired(EnrollmentChallengeInfo) + + /// Server returned WebAuthn creation options for passkey registration. + case passkeyCreationRequired(PasskeyCreationInfo) +} + +/// Information about a verification challenge sent during enrollment. +internal struct EnrollmentChallengeInfo +{ + let sentTo: String? + let channelType: String? + let codeLength: Int? + let continuationToken: String +} + +/// Information needed to invoke the platform authenticator for passkey creation. +internal struct PasskeyCreationInfo +{ + let publicKey: [String: Any] + let continuationToken: String +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/mock/CredentialManagementEnvironment.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/mock/CredentialManagementEnvironment.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/mock/CredentialManagementEnvironment.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/mock/CredentialManagementEnvironment.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/mock/CredentialManagementMockNetworkClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/mock/CredentialManagementMockNetworkClient.swift new file mode 100644 index 0000000000..466be75eef --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/mock/CredentialManagementMockNetworkClient.swift @@ -0,0 +1,276 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import MSAL +@_implementationOnly import MSAL_Private + +/// In-memory mock implementation of `CredentialManagementNetworkClientProtocol`. +/// +/// Operates entirely on real typed model objects — no HAL logic. +/// Stores methods in a dictionary keyed by ID for O(1) lookup/delete. +/// +/// Activated via UserDefaults key `com.microsoft.identity.credentialmanagement.useMockAPI`. +internal final class CredentialManagementMockNetworkClient: CredentialManagementNetworkClientProtocol +{ + /// In-memory store of credential methods keyed by `id`. + private var methodStore: [String: any MSALCredentialMethodProtocol] + + /// Tracks pending enrollments by continuationToken for activate step. + private var pendingEnrollments: [String: PendingEnrollment] + + private struct PendingEnrollment + { + let type: MSALCredentialType + let phoneNumber: String? + } + + private var simulatedDelay: TimeInterval + { + let delay = UserDefaults.standard.double( + forKey: "com.microsoft.identity.credentialmanagement.mockDelaySeconds" + ) + return delay > 0 ? delay : 0.5 + } + + init() + { + self.methodStore = [:] + self.pendingEnrollments = [:] + Self.seedMethods().forEach { self.methodStore[$0.id] = $0 } + + MSIDLogger.shared().log( + level: .warning, + correlationId: UUID(), + message: "⚠️ Mock network client is active. All credential management calls will return simulated data." + ) + } + + // MARK: - List Methods + + func listMethods( + accessToken: String, + correlationId: UUID + ) async -> Result<[any MSALCredentialMethodProtocol], MSALNativeCredentialManagementError> + { + await simulateNetworkDelay() + + MSIDLogger.shared().log( + level: .info, + correlationId: correlationId, + message: "[Mock] listMethods — returning \(methodStore.count) methods" + ) + + return .success(Array(methodStore.values)) + } + + // MARK: - Begin Enrollment + + func beginEnrollment( + type: MSALCredentialType, + accessToken: String, + body: Data?, + correlationId: UUID + ) async -> Result + { + await simulateNetworkDelay() + + MSIDLogger.shared().log( + level: .info, + correlationId: correlationId, + message: "[Mock] beginEnrollment for type=\(type.rawValue)" + ) + + switch type + { + case .password: + // Password completes immediately + let newId = "mock-\(UUID().uuidString.prefix(8))" + let method = MSALPasswordCredentialMethod(id: newId, createdAt: Date()) + methodStore[newId] = method + return .success(.completed(method)) + + case .phone: + let phoneNumber = extractPhoneNumber(from: body) + let token = "mock-ct-\(UUID().uuidString.prefix(8))" + pendingEnrollments[token] = PendingEnrollment(type: .phone, phoneNumber: phoneNumber) + + let maskedPhone = maskPhone(phoneNumber) + let challengeInfo = EnrollmentChallengeInfo( + sentTo: maskedPhone, + channelType: "sms", + codeLength: 6, + continuationToken: token + ) + return .success(.challengeRequired(challengeInfo)) + + default: + // Passkey — return creation options + let token = "mock-ct-\(UUID().uuidString.prefix(8))" + pendingEnrollments[token] = PendingEnrollment(type: type, phoneNumber: nil) + + let publicKey: [String: Any] = [ + "challenge": Data("mock-challenge-\(UUID().uuidString)".utf8).base64EncodedString(), + "rp": ["id": "login.microsoft.com", "name": "Microsoft"], + "user": [ + "id": Data("mock-user-id".utf8).base64EncodedString(), + "name": "user@contoso.com", + "displayName": "Mock User" + ] + ] + + let creationInfo = PasskeyCreationInfo( + publicKey: publicKey, + continuationToken: token + ) + return .success(.passkeyCreationRequired(creationInfo)) + } + } + + // MARK: - Activate Enrollment + + func activateEnrollment( + continuationToken: String, + accessToken: String, + body: Data, + correlationId: UUID + ) async -> Result + { + await simulateNetworkDelay() + + MSIDLogger.shared().log( + level: .info, + correlationId: correlationId, + message: "[Mock] activateEnrollment" + ) + + guard let pending = pendingEnrollments.removeValue(forKey: continuationToken) else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "No pending enrollment found for the given continuation token.", + correlationId: correlationId + )) + } + + let newId = "mock-\(UUID().uuidString.prefix(8))" + let bodyJson = (try? JSONSerialization.jsonObject(with: body)) as? [String: Any] + let displayName = bodyJson?["displayName"] as? String + + let method: any MSALCredentialMethodProtocol + switch pending.type + { + case .phone: + method = MSALPhoneCredentialMethod( + id: newId, + createdAt: Date(), + phoneNumber: pending.phoneNumber ?? "+1 (555) 000-0000" + ) + default: + method = MSALPasskeyCredentialMethod( + id: newId, + displayName: displayName ?? "Passkey", + createdAt: Date(), + credentialID: "mock-cred-\(UUID().uuidString.prefix(8))", + aaguid: nil + ) + } + + methodStore[newId] = method + return .success(method) + } + + // MARK: - Delete Method + + func deleteMethod( + type: MSALCredentialType, + methodId: String, + accessToken: String, + correlationId: UUID + ) async -> Result + { + await simulateNetworkDelay() + + MSIDLogger.shared().log( + level: .info, + correlationId: correlationId, + message: "[Mock] deleteMethod id=\(methodId)" + ) + + methodStore.removeValue(forKey: methodId) + return .success(()) + } + + // MARK: - Private + + private static func seedMethods() -> [any MSALCredentialMethodProtocol] + { + [ + MSALPasskeyCredentialMethod( + id: "mock-passkey-001", + displayName: "Mock Passkey", + createdAt: nil, + credentialID: "mock-credential-id-abc", + aaguid: "00000000-0000-0000-0000-000000000001" + ), + MSALPhoneCredentialMethod( + id: "mock-phone-001", + createdAt: nil, + phoneNumber: "+1 (555) 123-4567" + ), + MSALPasswordCredentialMethod( + id: "mock-password-001", + createdAt: nil + ) + ] + } + + private func extractPhoneNumber(from body: Data?) -> String? + { + guard let body = body, + let json = try? JSONSerialization.jsonObject(with: body) as? [String: Any] else + { + return nil + } + return json["phoneNumber"] as? String + } + + private func maskPhone(_ phone: String?) -> String + { + guard let phone = phone, phone.count > 4 else + { + return phone ?? "+1 (555) ***-0000" + } + return "••• \(String(phone.suffix(4)))" + } + + private func simulateNetworkDelay() async + { + let delay = simulatedDelay + if delay > 0 + { + try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) + } + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementErrorHandler.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementErrorHandler.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementErrorHandler.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementErrorHandler.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementInterceptorBridge.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementInterceptorBridge.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementInterceptorBridge.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementInterceptorBridge.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementRequestConfigurator.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementRequestConfigurator.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementRequestConfigurator.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementRequestConfigurator.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementAPIClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementServerNetworkClient.swift similarity index 55% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementAPIClient.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementServerNetworkClient.swift index b63107cf9f..a58d71c13e 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementAPIClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementServerNetworkClient.swift @@ -26,20 +26,24 @@ import Foundation import MSAL @_implementationOnly import MSAL_Private -/// Internal API client that orchestrates network calls using `MSIDHttpRequest` -/// infrastructure from IdentityCore. +/// Server-backed implementation of `CredentialManagementNetworkClientProtocol`. /// -/// Architecture (follows IdentityCore pattern): -/// 1. Typed request objects define endpoint-specific data -/// 2. `CredentialManagementRequestConfigurator` wires serializers/handlers onto MSIDHttpRequest -/// 3. MSIDHttpRequest sends via URLSession with retry/telemetry -/// 4. `MSIDResponseSerializerAdapter` bridges to pure-Swift response parsing -/// 5. Response mappers transform parsed responses into domain objects -internal final class CredentialManagementAPIClient: CredentialManagementNetworkClientProtocol +/// This layer: +/// 1. Sends requests through the MSIDHttpRequest pipeline. +/// 2. Parses HAL responses internally. +/// 3. Maintains an in-memory relation store that maps `continuationToken` → HAL link context. +/// 4. Exposes only typed domain models to callers. +/// +/// HAL concepts (links, embedded resources) never escape this layer. +internal final class CredentialManagementServerNetworkClient: CredentialManagementNetworkClientProtocol { private let requestSerializer: CredentialManagementRequestSerializing private let requestInterceptor: MSALNativeAuthRequestInterceptor? + /// In-memory relation store: maps continuationToken → activate href. + /// This keeps HAL link context hidden from callers. + private var activateHrefStore: [String: String] = [:] + init(requestSerializer: CredentialManagementRequestSerializing, requestInterceptor: MSALNativeAuthRequestInterceptor?) { self.requestSerializer = requestSerializer @@ -80,7 +84,7 @@ internal final class CredentialManagementAPIClient: CredentialManagementNetworkC accessToken: String, body: Data?, correlationId: UUID - ) async -> Result + ) async -> Result { let typedRequest = BeginEnrollmentRequest( type: type, @@ -96,18 +100,35 @@ internal final class CredentialManagementAPIClient: CredentialManagementNetworkC ) let sendResult = await send(typedRequest) - return sendResult.flatMap { EnrollmentResponseMapper.map($0, correlationId: correlationId) } + + switch sendResult + { + case .failure(let e): + return .failure(e) + case .success(let response): + return mapEnrollmentResponse(response, type: type, correlationId: correlationId) + } } // MARK: - Activate Enrollment func activateEnrollment( - activateHref: String, + continuationToken: String, accessToken: String, body: Data, correlationId: UUID - ) async -> Result + ) async -> Result { + // Resolve the activate href from our internal relation store + guard let activateHref = activateHrefStore.removeValue(forKey: continuationToken) else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "No activate link found for the given continuation token.", + correlationId: correlationId + )) + } + let typedRequest = ActivateEnrollmentRequest( activateHref: activateHref, accessToken: accessToken, @@ -118,7 +139,14 @@ internal final class CredentialManagementAPIClient: CredentialManagementNetworkC MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "Credential management: activating enrollment") let sendResult = await send(typedRequest) - return sendResult.flatMap { EnrollmentResponseMapper.map($0, correlationId: correlationId) } + + switch sendResult + { + case .failure(let e): + return .failure(e) + case .success(let response): + return mapActivationResponse(response, correlationId: correlationId) + } } // MARK: - Delete Method @@ -155,9 +183,112 @@ internal final class CredentialManagementAPIClient: CredentialManagementNetworkC } } + // MARK: - Private: Response Mapping + + /// Maps a raw enrollment response into a typed `EnrollmentBeginResponse`. + /// Stores any HAL activate link in the internal relation store. + private func mapEnrollmentResponse( + _ response: CredentialManagementResponse, + type: MSALCredentialType, + correlationId: UUID + ) -> Result + { + guard let json = response.jsonBody else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Response body is empty or not valid JSON.", + correlationId: correlationId + )) + } + + let halResource = HALResource(json: json) + + // Check if enrollment completed in one step + let state = halResource.string(forKey: "state") + if state == "completed" || halResource.link(rel: "activate") == nil + { + if let method = CredentialMethodMapper.parseMethod(from: halResource.properties) + { + return .success(.completed(method)) + } + // Fallback for password + if type == .password + { + let method = MSALPasswordCredentialMethod( + id: halResource.string(forKey: "id") ?? UUID().uuidString, + createdAt: Date() + ) + return .success(.completed(method)) + } + } + + // Multi-step: extract continuation token and store activate link + guard let continuationToken = halResource.string(forKey: "continuationToken") else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Server did not return continuationToken.", + correlationId: correlationId + )) + } + + if let activateLink = halResource.link(rel: "activate") + { + activateHrefStore[continuationToken] = activateLink.href + } + + // Passkey: return creation options + if let publicKeyDict = halResource.properties["publicKey"] as? [String: Any] + { + let info = PasskeyCreationInfo( + publicKey: publicKeyDict, + continuationToken: continuationToken + ) + return .success(.passkeyCreationRequired(info)) + } + + // Phone/other: return challenge info + let challengeInfo = EnrollmentChallengeInfo( + sentTo: halResource.string(forKey: "sentTo"), + channelType: halResource.string(forKey: "channelType"), + codeLength: halResource.properties["codeLength"] as? Int, + continuationToken: continuationToken + ) + return .success(.challengeRequired(challengeInfo)) + } + + /// Maps an activation response into a typed credential method. + private func mapActivationResponse( + _ response: CredentialManagementResponse, + correlationId: UUID + ) -> Result + { + guard let json = response.jsonBody else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Activation response body is empty or not valid JSON.", + correlationId: correlationId + )) + } + + let halResource = HALResource(json: json) + + guard let method = CredentialMethodMapper.parseMethod(from: halResource.properties) else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Failed to parse registered method from activation response.", + correlationId: correlationId + )) + } + + return .success(method) + } + // MARK: - Private: Send Pipeline - /// Configures and sends a typed request through the MSIDHttpRequest pipeline. private func send( _ typedRequest: CredentialManagementRequestProtocol ) async -> Result diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementURLResolver.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementURLResolver.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementURLResolver.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementURLResolver.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/hal/CredentialMethodMapper.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialMethodMapper.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/hal/CredentialMethodMapper.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialMethodMapper.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/hal/HALResource.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/HALResource.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/hal/HALResource.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/HALResource.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/MSIDLogger+CredentialManagement.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/MSIDLogger+CredentialManagement.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/MSIDLogger+CredentialManagement.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/MSIDLogger+CredentialManagement.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/ActivateEnrollmentRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/ActivateEnrollmentRequest.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/ActivateEnrollmentRequest.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/ActivateEnrollmentRequest.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/BeginEnrollmentRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/BeginEnrollmentRequest.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/BeginEnrollmentRequest.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/BeginEnrollmentRequest.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/CredentialManagementRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/CredentialManagementRequest.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/CredentialManagementRequest.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/CredentialManagementRequest.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/DeleteMethodRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/DeleteMethodRequest.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/DeleteMethodRequest.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/DeleteMethodRequest.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/ListMethodsRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/ListMethodsRequest.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request/ListMethodsRequest.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/ListMethodsRequest.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_serializer/CredentialManagementRequestSerializer.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request_serializer/CredentialManagementRequestSerializer.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_serializer/CredentialManagementRequestSerializer.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request_serializer/CredentialManagementRequestSerializer.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_serializer/CredentialManagementRequestSerializing.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request_serializer/CredentialManagementRequestSerializing.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_serializer/CredentialManagementRequestSerializing.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request_serializer/CredentialManagementRequestSerializing.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response/CredentialManagementResponse.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response/CredentialManagementResponse.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response/CredentialManagementResponse.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response/CredentialManagementResponse.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_mapper/EnrollmentResponseMapper.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_mapper/EnrollmentResponseMapper.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_mapper/EnrollmentResponseMapper.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_mapper/EnrollmentResponseMapper.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_mapper/ListMethodsResponseMapper.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_mapper/ListMethodsResponseMapper.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_mapper/ListMethodsResponseMapper.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_mapper/ListMethodsResponseMapper.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_serializer/CredentialManagementHALResponseSerializer.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_serializer/CredentialManagementHALResponseSerializer.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_serializer/CredentialManagementHALResponseSerializer.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_serializer/CredentialManagementHALResponseSerializer.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_serializer/CredentialManagementResponseSerializing.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_serializer/CredentialManagementResponseSerializing.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_serializer/CredentialManagementResponseSerializing.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_serializer/CredentialManagementResponseSerializing.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_serializer/MSIDResponseSerializerAdapter.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_serializer/MSIDResponseSerializerAdapter.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response_serializer/MSIDResponseSerializerAdapter.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_serializer/MSIDResponseSerializerAdapter.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/util/Result+Extensions.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/util/Result+Extensions.swift deleted file mode 100644 index 101a2ac285..0000000000 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/util/Result+Extensions.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. -// All rights reserved. -// -// This code is licensed under the MIT License. - -import Foundation - -/// Convenience extension for extracting error from Result when success was already handled. -internal extension Result -{ - /// Returns the error if this is a `.failure`, otherwise nil. - var failureValue: Failure? - { - if case .failure(let error) = self { return error } - return nil - } -} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index eded281aa4..031e1bf812 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -69,7 +69,6 @@ public class MSALNativeCredentialMethodsClient: NSObject { ) self.apiClient = nil - self.pendingActivateHref = nil self.pendingEnrollmentType = nil super.init() @@ -92,7 +91,7 @@ public class MSALNativeCredentialMethodsClient: NSObject { let tokenResult = await acquireTokenAsync(correlationId: correlationId) guard case .success(let accessToken) = tokenResult else { - return .failure(tokenResult.failureValue!) + return .failure({ if case .failure(let e) = tokenResult { return e }; fatalError("Unreachable") }()) } switch getAPIClient() @@ -147,7 +146,7 @@ public class MSALNativeCredentialMethodsClient: NSObject { let tokenResult = await acquireTokenAsync(correlationId: correlationId) guard case .success(let accessToken) = tokenResult else { - return .failure(tokenResult.failureValue!) + return .failure({ if case .failure(let e) = tokenResult { return e }; fatalError("Unreachable") }()) } switch getAPIClient() @@ -169,6 +168,5 @@ public class MSALNativeCredentialMethodsClient: NSObject { internal let config: MSALNativeCredentialManagementConfig internal let operationQueue: DispatchQueue internal var apiClient: (any CredentialManagementNetworkClientProtocol)? - internal var pendingActivateHref: String? internal var pendingEnrollmentType: MSALCredentialType? } From f6a052ee8027cf0f11b24345ecde5c020b1ef62a Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Wed, 3 Jun 2026 19:36:36 -0700 Subject: [PATCH 39/42] tt. --- ...tiveCredentialMethodsClient+Internal.swift | 39 ++++++----- ...dentialMethodsClient+RegisterPasskey.swift | 33 +++------- ...entialMethodsClient+RegisterPassword.swift | 14 +--- ...redentialMethodsClient+RegisterPhone.swift | 14 +--- ...ntialManagementNetworkClientProtocol.swift | 34 ++++++++-- ...redentialManagementMockNetworkClient.swift | 49 +++++++------- ...dentialManagementServerNetworkClient.swift | 64 +++++++++++++++++-- 7 files changed, 143 insertions(+), 104 deletions(-) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift index 4b78b2d6b0..44b8ae5156 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift @@ -178,25 +178,9 @@ extension MSALNativeCredentialMethodsClient fatalError("Unreachable") } - // Build the activation body with continuationToken and code - let activationBody: [String: Any] = [ - "continuationToken": continuationToken, - "oob": code - ] - - guard let bodyData = try? JSONSerialization.data(withJSONObject: activationBody) else - { - return .failure(MSALNativeCredentialManagementError( - type: .generalError, - message: "Failed to encode activation request body.", - correlationId: correlationId - )) - } - let result = await apiClientInstance.activateEnrollment( - continuationToken: continuationToken, + params: .otp(continuationToken: continuationToken, code: code), accessToken: accessToken, - body: bodyData, correlationId: correlationId ) @@ -239,13 +223,26 @@ extension MSALNativeCredentialMethodsClient )) } - let resendBody: [String: Any] = ["continuationToken": continuationToken] - let bodyData = try? JSONSerialization.data(withJSONObject: resendBody) + // For resend, we use the same enrollment type but the server uses + // the continuationToken to identify the pending session + let enrollmentParams: EnrollmentParams + switch pendingType + { + case .phone: + enrollmentParams = .phone(phoneNumber: "") + case .password: + enrollmentParams = .password(password: "") + default: + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Resend not supported for type: \(pendingType.rawValue)", + correlationId: correlationId + )) + } let result = await apiClientInstance.beginEnrollment( - type: pendingType, + params: enrollmentParams, accessToken: accessToken, - body: bodyData, correlationId: correlationId ) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift index 3e8a111cde..9bc989dd5b 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift @@ -57,9 +57,8 @@ extension MSALNativeCredentialMethodsClient return .failure(error) case .success(let client): let enrollResult = await client.beginEnrollment( - type: .passkey, + params: .passkey, accessToken: accessToken, - body: nil, correlationId: correlationId ) @@ -105,31 +104,15 @@ extension MSALNativeCredentialMethodsClient } // Step 4: Submit attestation to server via activate - let activationBody: [String: Any] = [ - "continuationToken": passkeyInfo.continuationToken, - "displayName": params.displayName ?? "Passkey", - "publicKeyCredential": [ - "id": attestation.credentialId.base64EncodedString(), - "response": [ - "attestationObject": attestation.rawAttestationObject.base64EncodedString(), - "clientDataJSON": attestation.rawClientDataJSON.base64EncodedString() - ] - ] - ] - - guard let bodyData = try? JSONSerialization.data(withJSONObject: activationBody) else - { - return .failure(MSALNativeCredentialManagementError( - type: .generalError, - message: "Failed to encode passkey activation body.", - correlationId: correlationId - )) - } - let activateResult = await client.activateEnrollment( - continuationToken: passkeyInfo.continuationToken, + params: .passkey( + continuationToken: passkeyInfo.continuationToken, + displayName: params.displayName ?? "Passkey", + credentialId: attestation.credentialId, + attestationObject: attestation.rawAttestationObject, + clientDataJSON: attestation.rawClientDataJSON + ), accessToken: accessToken, - body: bodyData, correlationId: correlationId ) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift index 3a053abb49..18c259c10b 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift @@ -47,21 +47,9 @@ extension MSALNativeCredentialMethodsClient case .failure(let error): return .failure(error) case .success(let client): - // Build the enrollment body with password - let enrollBody: [String: Any] = ["password": params.password] - guard let bodyData = try? JSONSerialization.data(withJSONObject: enrollBody) else - { - return .failure(MSALNativeCredentialManagementError( - type: .generalError, - message: "Failed to encode password enrollment body.", - correlationId: correlationId - )) - } - let enrollResult = await client.beginEnrollment( - type: .password, + params: .password(password: params.password), accessToken: accessToken, - body: bodyData, correlationId: correlationId ) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift index c7927784ee..32167c7b08 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift @@ -47,21 +47,9 @@ extension MSALNativeCredentialMethodsClient case .failure(let error): return .failure(error) case .success(let client): - // Build the enrollment body with phone number - let enrollBody: [String: Any] = ["phoneNumber": params.phoneNumber] - guard let bodyData = try? JSONSerialization.data(withJSONObject: enrollBody) else - { - return .failure(MSALNativeCredentialManagementError( - type: .generalError, - message: "Failed to encode phone enrollment body.", - correlationId: correlationId - )) - } - let enrollResult = await client.beginEnrollment( - type: .phone, + params: .phone(phoneNumber: params.phoneNumber), accessToken: accessToken, - body: bodyData, correlationId: correlationId ) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClientProtocol.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClientProtocol.swift index c5fda2fe63..8af2d49ea9 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClientProtocol.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClientProtocol.swift @@ -30,7 +30,7 @@ import MSAL /// Internal protocol that unifies the server-backed client and the mock client /// behind a single HAL-free interface. /// -/// Implementations own the transport details (HAL parsing, link tracking, etc.) +/// Implementations own the transport details (HAL parsing, link tracking, serialization, etc.) /// and expose only typed domain models to callers. internal protocol CredentialManagementNetworkClientProtocol { @@ -45,9 +45,8 @@ internal protocol CredentialManagementNetworkClientProtocol /// Returns a typed `EnrollmentBeginResponse` that tells the caller whether /// enrollment completed, a challenge is required, or passkey creation options are available. func beginEnrollment( - type: MSALCredentialType, + params: EnrollmentParams, accessToken: String, - body: Data?, correlationId: UUID ) async -> Result @@ -56,9 +55,8 @@ internal protocol CredentialManagementNetworkClientProtocol /// The `continuationToken` identifies the pending enrollment. The implementation /// resolves any internal resource context (e.g., HAL links) from its in-memory store. func activateEnrollment( - continuationToken: String, + params: ActivationParams, accessToken: String, - body: Data, correlationId: UUID ) async -> Result @@ -71,6 +69,32 @@ internal protocol CredentialManagementNetworkClientProtocol ) async -> Result } +// MARK: - Request Params + +/// Typed parameters for beginning enrollment of a credential method. +internal enum EnrollmentParams +{ + case phone(phoneNumber: String) + case password(password: String) + case passkey +} + +/// Typed parameters for activating (completing) a pending enrollment. +internal enum ActivationParams +{ + /// OTP-based activation (phone / password challenge). + case otp(continuationToken: String, code: String) + + /// Passkey attestation-based activation. + case passkey( + continuationToken: String, + displayName: String, + credentialId: Data, + attestationObject: Data, + clientDataJSON: Data + ) +} + // MARK: - Response Types /// HAL-free result of a `beginEnrollment` call. diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/mock/CredentialManagementMockNetworkClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/mock/CredentialManagementMockNetworkClient.swift index 466be75eef..8abaae2b63 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/mock/CredentialManagementMockNetworkClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/mock/CredentialManagementMockNetworkClient.swift @@ -88,9 +88,8 @@ internal final class CredentialManagementMockNetworkClient: CredentialManagement // MARK: - Begin Enrollment func beginEnrollment( - type: MSALCredentialType, + params: EnrollmentParams, accessToken: String, - body: Data?, correlationId: UUID ) async -> Result { @@ -99,10 +98,10 @@ internal final class CredentialManagementMockNetworkClient: CredentialManagement MSIDLogger.shared().log( level: .info, correlationId: correlationId, - message: "[Mock] beginEnrollment for type=\(type.rawValue)" + message: "[Mock] beginEnrollment for params=\(params)" ) - switch type + switch params { case .password: // Password completes immediately @@ -111,8 +110,7 @@ internal final class CredentialManagementMockNetworkClient: CredentialManagement methodStore[newId] = method return .success(.completed(method)) - case .phone: - let phoneNumber = extractPhoneNumber(from: body) + case .phone(let phoneNumber): let token = "mock-ct-\(UUID().uuidString.prefix(8))" pendingEnrollments[token] = PendingEnrollment(type: .phone, phoneNumber: phoneNumber) @@ -125,10 +123,10 @@ internal final class CredentialManagementMockNetworkClient: CredentialManagement ) return .success(.challengeRequired(challengeInfo)) - default: + case .passkey: // Passkey — return creation options let token = "mock-ct-\(UUID().uuidString.prefix(8))" - pendingEnrollments[token] = PendingEnrollment(type: type, phoneNumber: nil) + pendingEnrollments[token] = PendingEnrollment(type: .passkey, phoneNumber: nil) let publicKey: [String: Any] = [ "challenge": Data("mock-challenge-\(UUID().uuidString)".utf8).base64EncodedString(), @@ -151,9 +149,8 @@ internal final class CredentialManagementMockNetworkClient: CredentialManagement // MARK: - Activate Enrollment func activateEnrollment( - continuationToken: String, + params: ActivationParams, accessToken: String, - body: Data, correlationId: UUID ) async -> Result { @@ -165,6 +162,15 @@ internal final class CredentialManagementMockNetworkClient: CredentialManagement message: "[Mock] activateEnrollment" ) + let continuationToken: String + switch params + { + case .otp(let token, _): + continuationToken = token + case .passkey(let token, _, _, _, _): + continuationToken = token + } + guard let pending = pendingEnrollments.removeValue(forKey: continuationToken) else { return .failure(MSALNativeCredentialManagementError( @@ -175,8 +181,6 @@ internal final class CredentialManagementMockNetworkClient: CredentialManagement } let newId = "mock-\(UUID().uuidString.prefix(8))" - let bodyJson = (try? JSONSerialization.jsonObject(with: body)) as? [String: Any] - let displayName = bodyJson?["displayName"] as? String let method: any MSALCredentialMethodProtocol switch pending.type @@ -188,9 +192,18 @@ internal final class CredentialManagementMockNetworkClient: CredentialManagement phoneNumber: pending.phoneNumber ?? "+1 (555) 000-0000" ) default: + let displayName: String + if case .passkey(_, let name, _, _, _) = params + { + displayName = name + } + else + { + displayName = "Passkey" + } method = MSALPasskeyCredentialMethod( id: newId, - displayName: displayName ?? "Passkey", + displayName: displayName, createdAt: Date(), credentialID: "mock-cred-\(UUID().uuidString.prefix(8))", aaguid: nil @@ -246,16 +259,6 @@ internal final class CredentialManagementMockNetworkClient: CredentialManagement ] } - private func extractPhoneNumber(from body: Data?) -> String? - { - guard let body = body, - let json = try? JSONSerialization.jsonObject(with: body) as? [String: Any] else - { - return nil - } - return json["phoneNumber"] as? String - } - private func maskPhone(_ phone: String?) -> String { guard let phone = phone, phone.count > 4 else diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementServerNetworkClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementServerNetworkClient.swift index a58d71c13e..a3d8b09d7a 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementServerNetworkClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementServerNetworkClient.swift @@ -80,12 +80,27 @@ internal final class CredentialManagementServerNetworkClient: CredentialManageme // MARK: - Begin Enrollment func beginEnrollment( - type: MSALCredentialType, + params: EnrollmentParams, accessToken: String, - body: Data?, correlationId: UUID ) async -> Result { + let type: MSALCredentialType + let body: Data? + + switch params + { + case .phone(let phoneNumber): + type = .phone + body = try? JSONSerialization.data(withJSONObject: ["phoneNumber": phoneNumber]) + case .password(let password): + type = .password + body = try? JSONSerialization.data(withJSONObject: ["password": password]) + case .passkey: + type = .passkey + body = nil + } + let typedRequest = BeginEnrollmentRequest( type: type, accessToken: accessToken, @@ -113,12 +128,53 @@ internal final class CredentialManagementServerNetworkClient: CredentialManageme // MARK: - Activate Enrollment func activateEnrollment( - continuationToken: String, + params: ActivationParams, accessToken: String, - body: Data, correlationId: UUID ) async -> Result { + let continuationToken: String + let body: Data + + switch params + { + case .otp(let token, let code): + continuationToken = token + let bodyDict: [String: Any] = ["continuationToken": token, "oob": code] + guard let encoded = try? JSONSerialization.data(withJSONObject: bodyDict) else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Failed to encode OTP activation body.", + correlationId: correlationId + )) + } + body = encoded + + case .passkey(let token, let displayName, let credentialId, let attestationObject, let clientDataJSON): + continuationToken = token + let bodyDict: [String: Any] = [ + "continuationToken": token, + "displayName": displayName, + "publicKeyCredential": [ + "id": credentialId.base64EncodedString(), + "response": [ + "attestationObject": attestationObject.base64EncodedString(), + "clientDataJSON": clientDataJSON.base64EncodedString() + ] + ] + ] + guard let encoded = try? JSONSerialization.data(withJSONObject: bodyDict) else + { + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Failed to encode passkey activation body.", + correlationId: correlationId + )) + } + body = encoded + } + // Resolve the activate href from our internal relation store guard let activateHref = activateHrefStore.removeValue(forKey: continuationToken) else { From 6e77555d7e1827232b4e9d4133f643b816b2c491 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Wed, 3 Jun 2026 20:05:22 -0700 Subject: [PATCH 40/42] Refactor network layer protocol --- ...tiveCredentialMethodsClient+Internal.swift | 6 +- .../MSIDLogger+CredentialManagement.swift | 0 ...dentialMethodsClient+RegisterPasskey.swift | 4 +- ...entialMethodsClient+RegisterPassword.swift | 2 +- ...redentialMethodsClient+RegisterPhone.swift | 2 +- ...ntialManagementNetworkClientProtocol.swift | 59 ------------------- .../CredentialManagementEnvironment.swift | 0 ...redentialManagementMockNetworkClient.swift | 44 +++++++------- .../CredentialManagementErrorHandler.swift | 0 ...redentialManagementInterceptorBridge.swift | 0 ...dentialManagementRequestConfigurator.swift | 0 ...dentialManagementServerNetworkClient.swift | 51 ++++++++-------- .../CredentialManagementURLResolver.swift | 0 .../server/CredentialMethodMapper.swift | 0 .../{ => providers}/server/HALResource.swift | 0 .../request/ActivateEnrollmentRequest.swift | 0 .../request/BeginEnrollmentRequest.swift | 0 .../request/CredentialManagementRequest.swift | 0 .../server/request/DeleteMethodRequest.swift | 0 .../server/request/ListMethodsRequest.swift | 0 ...redentialManagementRequestSerializer.swift | 0 ...edentialManagementRequestSerializing.swift | 0 .../CredentialManagementResponse.swift | 0 .../EnrollmentResponseMapper.swift | 0 .../ListMethodsResponseMapper.swift | 0 ...ntialManagementHALResponseSerializer.swift | 0 ...dentialManagementResponseSerializing.swift | 0 .../MSIDResponseSerializerAdapter.swift | 0 .../activation_params/ActivationParams.swift | 37 ++++++++++++ .../OTPActivationParams.swift | 37 ++++++++++++ .../PasskeyActivationParams.swift | 49 +++++++++++++++ .../enrollment_params/EnrollmentParams.swift | 37 ++++++++++++ .../PasskeyEnrollmentParams.swift | 34 +++++++++++ .../PasswordEnrollmentParams.swift | 37 ++++++++++++ .../PhoneEnrollmentParams.swift | 37 ++++++++++++ .../response/EnrollmentBeginResponse.swift | 57 ++++++++++++++++++ .../Configuration.swift | 4 -- 37 files changed, 380 insertions(+), 117 deletions(-) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/{network/server => }/MSIDLogger+CredentialManagement.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/mock/CredentialManagementEnvironment.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/mock/CredentialManagementMockNetworkClient.swift (93%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/CredentialManagementErrorHandler.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/CredentialManagementInterceptorBridge.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/CredentialManagementRequestConfigurator.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/CredentialManagementServerNetworkClient.swift (90%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/CredentialManagementURLResolver.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/CredentialMethodMapper.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/HALResource.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/request/ActivateEnrollmentRequest.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/request/BeginEnrollmentRequest.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/request/CredentialManagementRequest.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/request/DeleteMethodRequest.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/request/ListMethodsRequest.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/request_serializer/CredentialManagementRequestSerializer.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/request_serializer/CredentialManagementRequestSerializing.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/response/CredentialManagementResponse.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/response_mapper/EnrollmentResponseMapper.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/response_mapper/ListMethodsResponseMapper.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/response_serializer/CredentialManagementHALResponseSerializer.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/response_serializer/CredentialManagementResponseSerializing.swift (100%) rename lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/{ => providers}/server/response_serializer/MSIDResponseSerializerAdapter.swift (100%) create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/activation_params/ActivationParams.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/activation_params/OTPActivationParams.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/activation_params/PasskeyActivationParams.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/enrollment_params/EnrollmentParams.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/enrollment_params/PasskeyEnrollmentParams.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/enrollment_params/PasswordEnrollmentParams.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/enrollment_params/PhoneEnrollmentParams.swift create mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response/EnrollmentBeginResponse.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift index 44b8ae5156..f7e1ddfabf 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift @@ -179,7 +179,7 @@ extension MSALNativeCredentialMethodsClient } let result = await apiClientInstance.activateEnrollment( - params: .otp(continuationToken: continuationToken, code: code), + params: OTPActivationParams(continuationToken: continuationToken, code: code), accessToken: accessToken, correlationId: correlationId ) @@ -229,9 +229,9 @@ extension MSALNativeCredentialMethodsClient switch pendingType { case .phone: - enrollmentParams = .phone(phoneNumber: "") + enrollmentParams = PhoneEnrollmentParams(phoneNumber: "") case .password: - enrollmentParams = .password(password: "") + enrollmentParams = PasswordEnrollmentParams(password: "") default: return .failure(MSALNativeCredentialManagementError( type: .generalError, diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/MSIDLogger+CredentialManagement.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSIDLogger+CredentialManagement.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/MSIDLogger+CredentialManagement.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSIDLogger+CredentialManagement.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift index 9bc989dd5b..be34a70c05 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift @@ -57,7 +57,7 @@ extension MSALNativeCredentialMethodsClient return .failure(error) case .success(let client): let enrollResult = await client.beginEnrollment( - params: .passkey, + params: PasskeyEnrollmentParams(), accessToken: accessToken, correlationId: correlationId ) @@ -105,7 +105,7 @@ extension MSALNativeCredentialMethodsClient // Step 4: Submit attestation to server via activate let activateResult = await client.activateEnrollment( - params: .passkey( + params: PasskeyActivationParams( continuationToken: passkeyInfo.continuationToken, displayName: params.displayName ?? "Passkey", credentialId: attestation.credentialId, diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift index 18c259c10b..9171850073 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift @@ -48,7 +48,7 @@ extension MSALNativeCredentialMethodsClient return .failure(error) case .success(let client): let enrollResult = await client.beginEnrollment( - params: .password(password: params.password), + params: PasswordEnrollmentParams(password: params.password), accessToken: accessToken, correlationId: correlationId ) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift index 32167c7b08..0e12551fcf 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift @@ -48,7 +48,7 @@ extension MSALNativeCredentialMethodsClient return .failure(error) case .success(let client): let enrollResult = await client.beginEnrollment( - params: .phone(phoneNumber: params.phoneNumber), + params: PhoneEnrollmentParams(phoneNumber: params.phoneNumber), accessToken: accessToken, correlationId: correlationId ) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClientProtocol.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClientProtocol.swift index 8af2d49ea9..32c089a4b5 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClientProtocol.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClientProtocol.swift @@ -69,62 +69,3 @@ internal protocol CredentialManagementNetworkClientProtocol ) async -> Result } -// MARK: - Request Params - -/// Typed parameters for beginning enrollment of a credential method. -internal enum EnrollmentParams -{ - case phone(phoneNumber: String) - case password(password: String) - case passkey -} - -/// Typed parameters for activating (completing) a pending enrollment. -internal enum ActivationParams -{ - /// OTP-based activation (phone / password challenge). - case otp(continuationToken: String, code: String) - - /// Passkey attestation-based activation. - case passkey( - continuationToken: String, - displayName: String, - credentialId: Data, - attestationObject: Data, - clientDataJSON: Data - ) -} - -// MARK: - Response Types - -/// HAL-free result of a `beginEnrollment` call. -/// -/// Callers use this typed response instead of parsing raw HAL resources. -/// The HAL-specific link/resource context is managed internally by the server layer. -internal enum EnrollmentBeginResponse -{ - /// Enrollment completed in one step (e.g., password). - case completed(any MSALCredentialMethodProtocol) - - /// A verification challenge was sent (e.g., OTP to phone). - case challengeRequired(EnrollmentChallengeInfo) - - /// Server returned WebAuthn creation options for passkey registration. - case passkeyCreationRequired(PasskeyCreationInfo) -} - -/// Information about a verification challenge sent during enrollment. -internal struct EnrollmentChallengeInfo -{ - let sentTo: String? - let channelType: String? - let codeLength: Int? - let continuationToken: String -} - -/// Information needed to invoke the platform authenticator for passkey creation. -internal struct PasskeyCreationInfo -{ - let publicKey: [String: Any] - let continuationToken: String -} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/mock/CredentialManagementEnvironment.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/mock/CredentialManagementEnvironment.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/mock/CredentialManagementEnvironment.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/mock/CredentialManagementEnvironment.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/mock/CredentialManagementMockNetworkClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/mock/CredentialManagementMockNetworkClient.swift similarity index 93% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/mock/CredentialManagementMockNetworkClient.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/mock/CredentialManagementMockNetworkClient.swift index 8abaae2b63..e72109f08b 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/mock/CredentialManagementMockNetworkClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/mock/CredentialManagementMockNetworkClient.swift @@ -103,18 +103,11 @@ internal final class CredentialManagementMockNetworkClient: CredentialManagement switch params { - case .password: - // Password completes immediately - let newId = "mock-\(UUID().uuidString.prefix(8))" - let method = MSALPasswordCredentialMethod(id: newId, createdAt: Date()) - methodStore[newId] = method - return .success(.completed(method)) - - case .phone(let phoneNumber): + case let phoneParams as PhoneEnrollmentParams: let token = "mock-ct-\(UUID().uuidString.prefix(8))" - pendingEnrollments[token] = PendingEnrollment(type: .phone, phoneNumber: phoneNumber) + pendingEnrollments[token] = PendingEnrollment(type: .phone, phoneNumber: phoneParams.phoneNumber) - let maskedPhone = maskPhone(phoneNumber) + let maskedPhone = maskPhone(phoneParams.phoneNumber) let challengeInfo = EnrollmentChallengeInfo( sentTo: maskedPhone, channelType: "sms", @@ -123,7 +116,14 @@ internal final class CredentialManagementMockNetworkClient: CredentialManagement ) return .success(.challengeRequired(challengeInfo)) - case .passkey: + case is PasswordEnrollmentParams: + // Password completes immediately + let newId = "mock-\(UUID().uuidString.prefix(8))" + let method = MSALPasswordCredentialMethod(id: newId, createdAt: Date()) + methodStore[newId] = method + return .success(.completed(method)) + + case is PasskeyEnrollmentParams: // Passkey — return creation options let token = "mock-ct-\(UUID().uuidString.prefix(8))" pendingEnrollments[token] = PendingEnrollment(type: .passkey, phoneNumber: nil) @@ -143,6 +143,13 @@ internal final class CredentialManagementMockNetworkClient: CredentialManagement continuationToken: token ) return .success(.passkeyCreationRequired(creationInfo)) + + default: + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Unsupported enrollment params type.", + correlationId: correlationId + )) } } @@ -162,16 +169,7 @@ internal final class CredentialManagementMockNetworkClient: CredentialManagement message: "[Mock] activateEnrollment" ) - let continuationToken: String - switch params - { - case .otp(let token, _): - continuationToken = token - case .passkey(let token, _, _, _, _): - continuationToken = token - } - - guard let pending = pendingEnrollments.removeValue(forKey: continuationToken) else + guard let pending = pendingEnrollments.removeValue(forKey: params.continuationToken) else { return .failure(MSALNativeCredentialManagementError( type: .generalError, @@ -193,9 +191,9 @@ internal final class CredentialManagementMockNetworkClient: CredentialManagement ) default: let displayName: String - if case .passkey(_, let name, _, _, _) = params + if let passkeyParams = params as? PasskeyActivationParams { - displayName = name + displayName = passkeyParams.displayName } else { diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementErrorHandler.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialManagementErrorHandler.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementErrorHandler.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialManagementErrorHandler.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementInterceptorBridge.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialManagementInterceptorBridge.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementInterceptorBridge.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialManagementInterceptorBridge.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementRequestConfigurator.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialManagementRequestConfigurator.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementRequestConfigurator.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialManagementRequestConfigurator.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementServerNetworkClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialManagementServerNetworkClient.swift similarity index 90% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementServerNetworkClient.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialManagementServerNetworkClient.swift index a3d8b09d7a..d905db7b0d 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementServerNetworkClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialManagementServerNetworkClient.swift @@ -85,24 +85,20 @@ internal final class CredentialManagementServerNetworkClient: CredentialManageme correlationId: UUID ) async -> Result { - let type: MSALCredentialType let body: Data? switch params { - case .phone(let phoneNumber): - type = .phone - body = try? JSONSerialization.data(withJSONObject: ["phoneNumber": phoneNumber]) - case .password(let password): - type = .password - body = try? JSONSerialization.data(withJSONObject: ["password": password]) - case .passkey: - type = .passkey + case let phoneParams as PhoneEnrollmentParams: + body = try? JSONSerialization.data(withJSONObject: ["phoneNumber": phoneParams.phoneNumber]) + case let passwordParams as PasswordEnrollmentParams: + body = try? JSONSerialization.data(withJSONObject: ["password": passwordParams.password]) + default: body = nil } let typedRequest = BeginEnrollmentRequest( - type: type, + type: params.type, accessToken: accessToken, body: body, correlationId: correlationId @@ -111,7 +107,7 @@ internal final class CredentialManagementServerNetworkClient: CredentialManageme MSIDLogger.shared().log( level: .info, correlationId: correlationId, - message: "Credential management: beginning enrollment for type '\(CredentialMethodMapper.serverType(from: type))'" + message: "Credential management: beginning enrollment for type '\(CredentialMethodMapper.serverType(from: params.type))'" ) let sendResult = await send(typedRequest) @@ -121,7 +117,7 @@ internal final class CredentialManagementServerNetworkClient: CredentialManageme case .failure(let e): return .failure(e) case .success(let response): - return mapEnrollmentResponse(response, type: type, correlationId: correlationId) + return mapEnrollmentResponse(response, type: params.type, correlationId: correlationId) } } @@ -133,14 +129,15 @@ internal final class CredentialManagementServerNetworkClient: CredentialManageme correlationId: UUID ) async -> Result { - let continuationToken: String let body: Data switch params { - case .otp(let token, let code): - continuationToken = token - let bodyDict: [String: Any] = ["continuationToken": token, "oob": code] + case let otpParams as OTPActivationParams: + let bodyDict: [String: Any] = [ + "continuationToken": otpParams.continuationToken, + "oob": otpParams.code + ] guard let encoded = try? JSONSerialization.data(withJSONObject: bodyDict) else { return .failure(MSALNativeCredentialManagementError( @@ -151,16 +148,15 @@ internal final class CredentialManagementServerNetworkClient: CredentialManageme } body = encoded - case .passkey(let token, let displayName, let credentialId, let attestationObject, let clientDataJSON): - continuationToken = token + case let passkeyParams as PasskeyActivationParams: let bodyDict: [String: Any] = [ - "continuationToken": token, - "displayName": displayName, + "continuationToken": passkeyParams.continuationToken, + "displayName": passkeyParams.displayName, "publicKeyCredential": [ - "id": credentialId.base64EncodedString(), + "id": passkeyParams.credentialId.base64EncodedString(), "response": [ - "attestationObject": attestationObject.base64EncodedString(), - "clientDataJSON": clientDataJSON.base64EncodedString() + "attestationObject": passkeyParams.attestationObject.base64EncodedString(), + "clientDataJSON": passkeyParams.clientDataJSON.base64EncodedString() ] ] ] @@ -173,10 +169,17 @@ internal final class CredentialManagementServerNetworkClient: CredentialManageme )) } body = encoded + + default: + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Unsupported activation params type.", + correlationId: correlationId + )) } // Resolve the activate href from our internal relation store - guard let activateHref = activateHrefStore.removeValue(forKey: continuationToken) else + guard let activateHref = activateHrefStore.removeValue(forKey: params.continuationToken) else { return .failure(MSALNativeCredentialManagementError( type: .generalError, diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementURLResolver.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialManagementURLResolver.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialManagementURLResolver.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialManagementURLResolver.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialMethodMapper.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialMethodMapper.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/CredentialMethodMapper.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialMethodMapper.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/HALResource.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/HALResource.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/HALResource.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/HALResource.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/ActivateEnrollmentRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request/ActivateEnrollmentRequest.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/ActivateEnrollmentRequest.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request/ActivateEnrollmentRequest.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/BeginEnrollmentRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request/BeginEnrollmentRequest.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/BeginEnrollmentRequest.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request/BeginEnrollmentRequest.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/CredentialManagementRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request/CredentialManagementRequest.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/CredentialManagementRequest.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request/CredentialManagementRequest.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/DeleteMethodRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request/DeleteMethodRequest.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/DeleteMethodRequest.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request/DeleteMethodRequest.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/ListMethodsRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request/ListMethodsRequest.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request/ListMethodsRequest.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request/ListMethodsRequest.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request_serializer/CredentialManagementRequestSerializer.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request_serializer/CredentialManagementRequestSerializer.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request_serializer/CredentialManagementRequestSerializer.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request_serializer/CredentialManagementRequestSerializer.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request_serializer/CredentialManagementRequestSerializing.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request_serializer/CredentialManagementRequestSerializing.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/request_serializer/CredentialManagementRequestSerializing.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request_serializer/CredentialManagementRequestSerializing.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response/CredentialManagementResponse.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response/CredentialManagementResponse.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response/CredentialManagementResponse.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response/CredentialManagementResponse.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_mapper/EnrollmentResponseMapper.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response_mapper/EnrollmentResponseMapper.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_mapper/EnrollmentResponseMapper.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response_mapper/EnrollmentResponseMapper.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_mapper/ListMethodsResponseMapper.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response_mapper/ListMethodsResponseMapper.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_mapper/ListMethodsResponseMapper.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response_mapper/ListMethodsResponseMapper.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_serializer/CredentialManagementHALResponseSerializer.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response_serializer/CredentialManagementHALResponseSerializer.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_serializer/CredentialManagementHALResponseSerializer.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response_serializer/CredentialManagementHALResponseSerializer.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_serializer/CredentialManagementResponseSerializing.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response_serializer/CredentialManagementResponseSerializing.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_serializer/CredentialManagementResponseSerializing.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response_serializer/CredentialManagementResponseSerializing.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_serializer/MSIDResponseSerializerAdapter.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response_serializer/MSIDResponseSerializerAdapter.swift similarity index 100% rename from lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/server/response_serializer/MSIDResponseSerializerAdapter.swift rename to lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response_serializer/MSIDResponseSerializerAdapter.swift diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/activation_params/ActivationParams.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/activation_params/ActivationParams.swift new file mode 100644 index 0000000000..9317ddf479 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/activation_params/ActivationParams.swift @@ -0,0 +1,37 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Base class for activation (enrollment completion) parameters. +/// Subclass to add new activation flows without modifying existing code (Open/Closed Principle). +internal class ActivationParams +{ + let continuationToken: String + + init(continuationToken: String) + { + self.continuationToken = continuationToken + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/activation_params/OTPActivationParams.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/activation_params/OTPActivationParams.swift new file mode 100644 index 0000000000..316aa1e34f --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/activation_params/OTPActivationParams.swift @@ -0,0 +1,37 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// OTP-based activation parameters (phone verification). +internal final class OTPActivationParams: ActivationParams +{ + let code: String + + init(continuationToken: String, code: String) + { + self.code = code + super.init(continuationToken: continuationToken) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/activation_params/PasskeyActivationParams.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/activation_params/PasskeyActivationParams.swift new file mode 100644 index 0000000000..3d75636f8c --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/activation_params/PasskeyActivationParams.swift @@ -0,0 +1,49 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Passkey attestation-based activation parameters. +internal final class PasskeyActivationParams: ActivationParams +{ + let displayName: String + let credentialId: Data + let attestationObject: Data + let clientDataJSON: Data + + init( + continuationToken: String, + displayName: String, + credentialId: Data, + attestationObject: Data, + clientDataJSON: Data + ) + { + self.displayName = displayName + self.credentialId = credentialId + self.attestationObject = attestationObject + self.clientDataJSON = clientDataJSON + super.init(continuationToken: continuationToken) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/enrollment_params/EnrollmentParams.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/enrollment_params/EnrollmentParams.swift new file mode 100644 index 0000000000..2937b7d899 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/enrollment_params/EnrollmentParams.swift @@ -0,0 +1,37 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Base class for enrollment parameters. +/// Subclass to add new credential types without modifying existing code (Open/Closed Principle). +internal class EnrollmentParams +{ + let type: MSALCredentialType + + init(type: MSALCredentialType) + { + self.type = type + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/enrollment_params/PasskeyEnrollmentParams.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/enrollment_params/PasskeyEnrollmentParams.swift new file mode 100644 index 0000000000..768fe12997 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/enrollment_params/PasskeyEnrollmentParams.swift @@ -0,0 +1,34 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Parameters for enrolling a passkey credential. +internal final class PasskeyEnrollmentParams: EnrollmentParams +{ + init() + { + super.init(type: .passkey) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/enrollment_params/PasswordEnrollmentParams.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/enrollment_params/PasswordEnrollmentParams.swift new file mode 100644 index 0000000000..218a8ca13c --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/enrollment_params/PasswordEnrollmentParams.swift @@ -0,0 +1,37 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Parameters for enrolling a password credential. +internal final class PasswordEnrollmentParams: EnrollmentParams +{ + let password: String + + init(password: String) + { + self.password = password + super.init(type: .password) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/enrollment_params/PhoneEnrollmentParams.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/enrollment_params/PhoneEnrollmentParams.swift new file mode 100644 index 0000000000..423aeb9182 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/request_params/enrollment_params/PhoneEnrollmentParams.swift @@ -0,0 +1,37 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Parameters for enrolling a phone number credential. +internal final class PhoneEnrollmentParams: EnrollmentParams +{ + let phoneNumber: String + + init(phoneNumber: String) + { + self.phoneNumber = phoneNumber + super.init(type: .phone) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response/EnrollmentBeginResponse.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response/EnrollmentBeginResponse.swift new file mode 100644 index 0000000000..53158a6bfe --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/response/EnrollmentBeginResponse.swift @@ -0,0 +1,57 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// HAL-free result of a `beginEnrollment` call. +/// +/// Callers use this typed response instead of parsing raw HAL resources. +/// The HAL-specific link/resource context is managed internally by the server layer. +internal enum EnrollmentBeginResponse +{ + /// Enrollment completed in one step (e.g., password). + case completed(any MSALCredentialMethodProtocol) + + /// A verification challenge was sent (e.g., OTP to phone). + case challengeRequired(EnrollmentChallengeInfo) + + /// Server returned WebAuthn creation options for passkey registration. + case passkeyCreationRequired(PasskeyCreationInfo) +} + +/// Information about a verification challenge sent during enrollment. +internal struct EnrollmentChallengeInfo +{ + let sentTo: String? + let channelType: String? + let codeLength: Int? + let continuationToken: String +} + +/// Information needed to invoke the platform authenticator for passkey creation. +internal struct PasskeyCreationInfo +{ + let publicKey: [String: Any] + let continuationToken: String +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift index 280ca8dd31..7db2128397 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift @@ -16,10 +16,6 @@ enum Configuration { /// The tenant subdomain (e.g., "contoso" for contoso.ciamlogin.com). static let tenantSubdomain = "Enter_the_Tenant_Subdomain_Here" - /// The relying party identifier for passkey registration (WebAuthn). - /// Must match the domain associated with your app (via apple-app-site-association). - static let relyingPartyIdentifier = "login.microsoft.com" - /// The base URL for the credential management API. /// Replace with your tenant's credential management endpoint. static let credentialManagementBaseURL = "https://\(tenantSubdomain).ciamlogin.com/api/v1.0" From 4057773b7baa2802ed6f30b55a3ee410acc35c53 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Wed, 3 Jun 2026 20:25:42 -0700 Subject: [PATCH 41/42] refacor config params. --- .../MSALNativeCredentialMethodsClient+Internal.swift | 5 +++-- .../public/MSALNativeCredentialManagementConfig.swift | 9 +++++---- .../src/public/MSALNativeCredentialMethodsClient.swift | 4 ++-- .../MSALNativeCredManagmentSampleApp/Configuration.swift | 4 ---- .../CredentialManagementViewModel.swift | 2 +- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift index f7e1ddfabf..8b4e20b152 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift @@ -126,11 +126,12 @@ extension MSALNativeCredentialMethodsClient return .success(mockClient) } - guard let baseURL = config.baseURL else + guard let tenantSubdomain = config.tenantSubdomain, + let baseURL = URL(string: "https://\(tenantSubdomain).ciamlogin.com") else { return .failure(MSALNativeCredentialManagementError( type: .invalidConfiguration, - message: "baseURL must be set on MSALNativeCredentialManagementConfig." + message: "tenantSubdomain must be set on MSALNativeCredentialManagementConfig." )) } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift index 24fdf4fc85..191bd0b212 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift @@ -28,13 +28,14 @@ import MSAL /// Configuration for the credential management client. /// /// Use this class to configure the `MSALNativeCredentialMethodsClient` with a token provider, -/// optional request interceptor (shared with MSAL), and API base URL. +/// optional request interceptor (shared with MSAL), and tenant subdomain. /// /// Example: /// ```swift /// let credConfig = MSALNativeCredentialManagementConfig() /// credConfig.requestInterceptor = sharedRequestInterceptor /// credConfig.tokenProvider = MyCustomTokenProvider() +/// credConfig.tenantSubdomain = "contoso" /// let credClient = try MSALNativeCredentialMethodsClient(config: credConfig) /// ``` @objcMembers @@ -52,10 +53,10 @@ public class MSALNativeCredentialManagementConfig: NSObject { /// - P1: Assign `MSALNativeAuthTokenProvider(userAccountResult:)` for automatic MSAL-based token retrieval. public var tokenProvider: MSALNativeCredentialManagementTokenProvider? - /// The base URL for the credential management API. + /// The tenant subdomain for the CIAM tenant (e.g., "contoso" for contoso.ciamlogin.com). /// - /// When nil, the client derives the endpoint from the tenant configuration. - public var baseURL: URL? + /// The credential management API base URL is derived from this value. + public var tenantSubdomain: String? public override init() { diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift index 031e1bf812..7d9f2c4cfb 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -55,11 +55,11 @@ public class MSALNativeCredentialMethodsClient: NSObject { message: "A token provider must be set on MSALNativeCredentialManagementConfig before initializing the client." ) } - guard config.baseURL != nil else + guard config.tenantSubdomain != nil else { throw MSALNativeCredentialManagementError( type: .invalidConfiguration, - message: "A baseURL must be set on MSALNativeCredentialManagementConfig before initializing the client." + message: "A tenantSubdomain must be set on MSALNativeCredentialManagementConfig before initializing the client." ) } self.config = config diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift index 7db2128397..1ed82464af 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift @@ -15,8 +15,4 @@ enum Configuration { /// The tenant subdomain (e.g., "contoso" for contoso.ciamlogin.com). static let tenantSubdomain = "Enter_the_Tenant_Subdomain_Here" - - /// The base URL for the credential management API. - /// Replace with your tenant's credential management endpoint. - static let credentialManagementBaseURL = "https://\(tenantSubdomain).ciamlogin.com/api/v1.0" } diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 19126fc516..5bc1a1f583 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -79,7 +79,7 @@ class CredentialManagementViewModel: ObservableObject { let credConfig = MSALNativeCredentialManagementConfig() credConfig.requestInterceptor = sharedRequestInterceptor credConfig.tokenProvider = tokenProvider - credConfig.baseURL = URL(string: Configuration.credentialManagementBaseURL) + credConfig.tenantSubdomain = Configuration.tenantSubdomain credClient = try MSALNativeCredentialMethodsClient(config: credConfig) From db89b787eb36a2fc02d7120dfbcc9c13f4c12df2 Mon Sep 17 00:00:00 2001 From: Sergey Demchenko Date: Fri, 5 Jun 2026 10:57:38 -0700 Subject: [PATCH 42/42] Acquire AT to call server apis. --- .../contents.xcworkspacedata | 4 + .../project.pbxproj | 14 +- .../MSALNativeAuthTokenProvider.swift | 195 ++++++++++++++---- .../Configuration.swift | 2 +- .../CredentialManagementViewModel.swift | 38 ++-- ...LNativeCredManagmentSampleApp.entitlements | 8 + .../SampleTokenProvider.swift | 41 ---- .../MSALNativeCredManagment/Package.swift | 2 - 8 files changed, 185 insertions(+), 119 deletions(-) create mode 100644 lib/native/MSALNativeCredManagment/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata delete mode 100644 lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/SampleTokenProvider.swift diff --git a/lib/native/MSALNativeCredManagment/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/lib/native/MSALNativeCredManagment/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..94b2795e22 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,4 @@ + + + diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj index 6be4f2f795..edc68ac48b 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj @@ -476,10 +476,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = YES; GENERATE_INFOPLIST_FILE = YES; - HEADER_SEARCH_PATHS = ( - "$(SRCROOT)/../../../MSAL/IdentityCore/IdentityCore/src/**", - "$(SRCROOT)/../../../MSAL/src/**", - ); + HEADER_SEARCH_PATHS = "$(SRCROOT)/../../../MSAL/IdentityCore/IdentityCore/src/**"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 18.2; @@ -501,7 +498,7 @@ SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_INCLUDE_PATHS = "$(SRCROOT)/../../../MSAL"; + SWIFT_INCLUDE_PATHS = "$(SRCROOT)/CMSAL_Private/include"; SWIFT_INSTALL_OBJC_HEADER = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; @@ -523,10 +520,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = YES; GENERATE_INFOPLIST_FILE = YES; - HEADER_SEARCH_PATHS = ( - "$(SRCROOT)/../../../MSAL/IdentityCore/IdentityCore/src/**", - "$(SRCROOT)/../../../MSAL/src/**", - ); + HEADER_SEARCH_PATHS = "$(SRCROOT)/../../../MSAL/IdentityCore/IdentityCore/src/**"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 18.2; @@ -548,7 +542,7 @@ SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_INCLUDE_PATHS = "$(SRCROOT)/../../../MSAL"; + SWIFT_INCLUDE_PATHS = "$(SRCROOT)/CMSAL_Private/include"; SWIFT_INSTALL_OBJC_HEADER = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/token_provider/MSALNativeAuthTokenProvider.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/token_provider/MSALNativeAuthTokenProvider.swift index 1af9f938e2..c1fa70643d 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/token_provider/MSALNativeAuthTokenProvider.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/token_provider/MSALNativeAuthTokenProvider.swift @@ -24,33 +24,55 @@ import Foundation import MSAL +#if os(iOS) +import UIKit +#elseif os(macOS) +import AppKit +#endif -/// A built-in token provider that wraps `MSALNativeAuthUserAccountResult` to obtain -/// access tokens using the existing MSAL Native Auth cached session. +/// A built-in token provider that uses MSAL's web-based interactive flow to acquire tokens. /// -/// Use this provider (P1) when you want automatic token management through MSAL: +/// On first call, it presents a web view for interactive sign-in. On subsequent calls, +/// it attempts silent token acquisition using the cached account, falling back to interactive +/// if the silent attempt fails with `MSALErrorInteractionRequired`. +/// +/// Usage: /// ```swift -/// let tokenProvider = MSALNativeAuthTokenProvider(userAccountResult: userAccountResult) +/// let tokenProvider = try MSALNativeAuthTokenProvider(clientId: "your-client-id") /// credConfig.tokenProvider = tokenProvider /// ``` -/// -/// - Important: This provider holds a weak reference to `userAccountResult`. If the user -/// signs out or the result is deallocated, the provider will return a `sessionExpired` error. @objcMembers public class MSALNativeAuthTokenProvider: NSObject, MSALNativeCredentialManagementTokenProvider { - private weak var userAccountResult: MSALNativeAuthUserAccountResult? + private let application: MSALPublicClientApplication + private var cachedAccount: MSALAccount? - /// Initialize with the user account result obtained from a successful MSAL Native Auth sign-in. + /// Initialize with a client ID. Uses the default MSAL authority. /// - /// - Parameter userAccountResult: The account result containing cached tokens and account info. - public init(userAccountResult: MSALNativeAuthUserAccountResult) + /// - Parameter clientId: The application (client) ID registered in the identity platform. + /// - Throws: If the MSAL configuration is invalid. + public init(clientId: String) throws { - self.userAccountResult = userAccountResult + let config = MSALPublicClientApplicationConfig(clientId: clientId) + config.cacheConfig.keychainSharingGroup = "com.microsoft.adalcache" +// config.sliceConfig?.dc = "ESTS-PUB-SCUS-FD000-TEST1-100" + +// config.authority = try MSALAuthority(url: URL(string: "https://login.microsoftonline.com/40e32adb-2fb9-4616-8604-d73950c432f1")!) + + config.authority = try MSALAuthority(url: URL(string: "https://login.microsoftonline.com/common")!) + + + // Set know authoirty to skip broker and run local flow only + config.knownAuthorities = [config.authority] + + self.application = try MSALPublicClientApplication(configuration: config) super.init() } - /// Retrieve an access token by delegating to MSAL Native Auth's silent token retrieval. + /// Retrieve an access token using MSAL web flow. + /// + /// Attempts silent acquisition first. If no cached account exists or interaction is required, + /// falls back to interactive web view sign-in. /// /// - Parameters: /// - scopes: The scopes required by the credential management operation. @@ -60,47 +82,140 @@ public class MSALNativeAuthTokenProvider: NSObject, MSALNativeCredentialManageme completionBlock: @escaping MSALNativeCredentialManagementTokenCompletionBlock ) { - guard let accountResult = userAccountResult else + // When mock mode is ON, return a fake token immediately without hitting the network. + if CredentialManagementEnvironment.isMockAPIEnabled { - let error = MSALNativeCredentialManagementError( - type: .sessionExpired, - message: "User account result is no longer available. The user may have signed out. " - + "Please re-authenticate and create a new MSALNativeAuthTokenProvider instance." - ) - completionBlock(nil, error) + completionBlock("mock-access-token-for-testing", nil) return } - let params = MSALNativeAuthGetAccessTokenParameters() - params.scopes = scopes - - accountResult.getAccessToken(parameters: params, delegate: TokenProviderCredentialsDelegate(completionBlock: completionBlock)) +// if let account = cachedAccount ?? (try? application.allAccounts().first) +// { +// acquireTokenSilent(scopes: scopes, account: account, completionBlock: completionBlock) +// } +// else +// { + acquireTokenInteractive(scopes: scopes, completionBlock: completionBlock) +// } } -} -/// Internal delegate that bridges the MSAL CredentialsDelegate pattern to a completion block. -private class TokenProviderCredentialsDelegate: NSObject, CredentialsDelegate { + /// Clear the cached account so the next token request triggers interactive sign-in. + public func signOut() + { + cachedAccount = nil + } - private let completionBlock: MSALNativeCredentialManagementTokenCompletionBlock + // MARK: - Private - init(completionBlock: @escaping MSALNativeCredentialManagementTokenCompletionBlock) + private func acquireTokenSilent( + scopes: [String], + account: MSALAccount, + completionBlock: @escaping MSALNativeCredentialManagementTokenCompletionBlock + ) { - self.completionBlock = completionBlock - super.init() + let silentParams = MSALSilentTokenParameters(scopes: scopes, account: account) + + application.acquireTokenSilent(with: silentParams) { [weak self] result, error in + guard let self = self else { return } + + if let result = result + { + self.cachedAccount = result.account + completionBlock(result.accessToken, nil) + return + } + + if let nsError = error as NSError?, + nsError.domain == MSALErrorDomain, + nsError.code == MSALError.interactionRequired.rawValue + { + self.acquireTokenInteractive(scopes: scopes, completionBlock: completionBlock) + return + } + + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Silent token acquisition failed: \(error?.localizedDescription ?? "Unknown error")" + ) + completionBlock(nil, credError) + } } - @MainActor func onAccessTokenRetrieveError(error: RetrieveAccessTokenError) + private func acquireTokenInteractive( + scopes: [String], + completionBlock: @escaping MSALNativeCredentialManagementTokenCompletionBlock + ) { - let credError = MSALNativeCredentialManagementError( - type: .unauthorized, - message: "Failed to retrieve access token from MSAL: \(error.errorDescription ?? "Unknown error")", - correlationId: error.correlationId - ) - completionBlock(nil, credError) + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + guard let viewController = self.topViewController() else + { + let error = MSALNativeCredentialManagementError( + type: .invalidConfiguration, + message: "Unable to find a view controller to present the web view from." + ) + completionBlock(nil, error) + return + } + + #if os(iOS) + let webParams = MSALWebviewParameters(authPresentationViewController: viewController) + #elseif os(macOS) + let webParams = MSALWebviewParameters(authPresentationViewController: viewController) + webParams.webviewType = .wkWebView + #endif + + let interactiveParams = MSALInteractiveTokenParameters(scopes: scopes, webviewParameters: webParams) + interactiveParams.promptType = .login + interactiveParams.scopes = ["api://02815c3e-3ef8-40a4-8f95-cfb184350d7e/Me.UserAuthenticationMethod.ReadWrite"] + interactiveParams.claimsRequest = MSALClaimsRequest(jsonString: "{\"access_token\":{\"acrs\":{\"essential\":true,\"values\":[\"urn:user:registersecurityinfo\"]},\"amr\":{\"essential\":true,\"values\":[\"ngcmfa\"]}}}", error: nil) + +// parameters.claimsRequest = [[MSALClaimsRequest alloc] initWithJsonString:kDeviceIdClaimsValue error:nil]; + + self.application.acquireToken(with: interactiveParams) { [weak self] result, error in + guard let self = self else { return } + + if let result = result + { + self.cachedAccount = result.account + completionBlock(result.accessToken, nil) + return + } + + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Interactive sign-in failed: \(error?.localizedDescription ?? "Unknown error")" + ) + completionBlock(nil, credError) + } + } } - @MainActor func onAccessTokenRetrieveCompleted(result: MSALNativeAuthTokenResult) + #if os(iOS) + private func topViewController() -> UIViewController? { - completionBlock(result.accessToken, nil) + guard let scene = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }) + .first(where: { $0.activationState == .foregroundActive }), + let rootVC = scene.windows.first(where: { $0.isKeyWindow })?.rootViewController + else + { + return nil + } + + var top = rootVC + while let presented = top.presentedViewController + { + top = presented + } + return top + } + #elseif os(macOS) + private func topViewController() -> NSViewController? + { + return NSApplication.shared.keyWindow?.contentViewController } + #endif } + diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift index 1ed82464af..bd2ad85283 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift @@ -11,7 +11,7 @@ import Foundation /// Replace these values with your own CIAM tenant settings. enum Configuration { /// The client ID of the application registered in the CIAM tenant. - static let clientId = "Enter_the_Application_Id_Here" + static let clientId = "6d0926a3-67d7-45b7-b429-9c25b0a699f7" /// The tenant subdomain (e.g., "contoso" for contoso.ciamlogin.com). static let tenantSubdomain = "Enter_the_Tenant_Subdomain_Here" diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift index 5bc1a1f583..10aba4cc92 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -48,7 +48,7 @@ class CredentialManagementViewModel: ObservableObject { // MARK: - Private Properties private var credClient: MSALNativeCredentialMethodsClient? - private var tokenProvider: SampleTokenProvider? + private var tokenProvider: MSALNativeAuthTokenProvider? private var pendingChallengeState: MSALCredentialMethodChallengeState? // MARK: - Initialization @@ -57,6 +57,10 @@ class CredentialManagementViewModel: ObservableObject { reinitializeClient() } + /// Sign in using MSAL interactive web flow. Email/password are passed as login hints. + func signIn(email: String, password: String) { + reinitializeClient() + } private func reinitializeClient() { do { // 1. Configure shared logger (used by both MSAL and Credential Management) @@ -67,10 +71,11 @@ class CredentialManagementViewModel: ObservableObject { } } - // 2. Create token provider - if tokenProvider == nil { - tokenProvider = SampleTokenProvider() - } + // 2. Create token provider using MSAL web flow + let msalTokenProvider = try MSALNativeAuthTokenProvider( + clientId: Configuration.clientId + ) + self.tokenProvider = msalTokenProvider // 3. Create shared request interceptor let sharedRequestInterceptor = SampleRequestInterceptor() @@ -78,36 +83,19 @@ class CredentialManagementViewModel: ObservableObject { // 4. Initialize Credential Management Client let credConfig = MSALNativeCredentialManagementConfig() credConfig.requestInterceptor = sharedRequestInterceptor - credConfig.tokenProvider = tokenProvider + credConfig.tokenProvider = msalTokenProvider credConfig.tenantSubdomain = Configuration.tenantSubdomain credClient = try MSALNativeCredentialMethodsClient(config: credConfig) let mode = useMockAPI ? "Mock API (UserDefaults)" : "Real Server" statusMessage = "SDK initialized (\(mode))." + isSignedIn = true } catch { errorMessage = "Failed to initialize: \(error.localizedDescription)" } } - // MARK: - Sign In - - func signIn(email: String, password: String) { - isLoading = true - statusMessage = "Signing in..." - errorMessage = nil - - // Simulate a brief network delay, then return fake token - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - guard let self = self else { return } - self.tokenProvider?.setSignedIn(true) - self.isSignedIn = true - self.userName = email - self.isLoading = false - self.statusMessage = "Signed in successfully (POC - fake token)." - } - } - // MARK: - Credential Management Operations func listCredentialMethods() { @@ -265,7 +253,7 @@ class CredentialManagementViewModel: ObservableObject { // MARK: - Sign Out func signOut() { - tokenProvider?.setSignedIn(false) + tokenProvider?.signOut() isSignedIn = false userName = "" credentialMethods = [] diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleApp.entitlements b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleApp.entitlements index a021bf3f46..e5b94ba7c0 100644 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleApp.entitlements +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleApp.entitlements @@ -10,5 +10,13 @@ com.apple.security.files.user-selected.read-only + com.apple.security.network.client + + com.apple.security.network.server + + keychain-access-groups + + $(AppIdentifierPrefix)com.microsoft.adalcache + diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/SampleTokenProvider.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/SampleTokenProvider.swift deleted file mode 100644 index 31aa0f4c95..0000000000 --- a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/SampleTokenProvider.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// SampleTokenProvider.swift -// MSALNativeCredManagmentSampleApp -// -// Created by Serhii Demchenko on 2026-05-27. -// - -import Foundation -import MSAL -import MSALNativeCredManagment - -/// A sample fake token provider for POC purposes. -/// Returns a hardcoded access token without calling MSAL. -class SampleTokenProvider: NSObject, MSALNativeCredentialManagementTokenProvider { - - private var isSignedIn = false - - func setSignedIn(_ signedIn: Bool) { - isSignedIn = signedIn - } - - func getAccessToken( - scopes: [String], - completionBlock: @escaping MSALNativeCredentialManagementTokenCompletionBlock - ) { - guard isSignedIn else { - let error = NSError( - domain: "SampleTokenProvider", - code: -1, - userInfo: [NSLocalizedDescriptionKey: "No user signed in. Please sign in first."] - ) - completionBlock(nil, error) - return - } - - // Return a fake access token for POC - let fakeToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.fake-poc-access-token" - completionBlock(fakeToken, nil) - } -} - diff --git a/lib/native/MSALNativeCredManagment/Package.swift b/lib/native/MSALNativeCredManagment/Package.swift index f5e439f273..fddb7f65ca 100644 --- a/lib/native/MSALNativeCredManagment/Package.swift +++ b/lib/native/MSALNativeCredManagment/Package.swift @@ -27,8 +27,6 @@ func findHeaderDirs(in basePath: String) -> [String] { } let allHeaderDirs = findHeaderDirs(in: msalRoot + "/IdentityCore/IdentityCore/src") - + findHeaderDirs(in: msalRoot + "/src") - + [msalRoot] let package = Package( name: "MSALNativeCredManagment",