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/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 new file mode 100644 index 0000000000..edc68ac48b --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment.xcodeproj/project.pbxproj @@ -0,0 +1,969 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 2394A84C2FC5208100B9FF0C /* MSALNativeCredManagment.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2394A8412FC5208100B9FF0C /* MSALNativeCredManagment.framework */; }; + 2394A88B2FC7C14100B9FF0C /* MSAL in Frameworks */ = {isa = PBXBuildFile; productRef = 2394A88A2FC7C14100B9FF0C /* MSAL */; }; + 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 */ + 2394A84D2FC5208100B9FF0C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2394A8382FC5208100B9FF0C /* Project object */; + proxyType = 1; + 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; + }; + 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; }; + 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 */ + 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 = ""; + }; + 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 */ + 2394A83E2FC5208100B9FF0C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2394A88B2FC7C14100B9FF0C /* MSAL in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2394A8482FC5208100B9FF0C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2394A84C2FC5208100B9FF0C /* MSALNativeCredManagment.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2394A89F2FC7FE7200B9FF0C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2394A8E52FCA000200B9FF0C /* MSAL in Frameworks */, + 2394A8E02FCA000100B9FF0C /* MSALNativeCredManagment.framework in Frameworks */, + ); + 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 */ + 2394A8372FC5208100B9FF0C = { + isa = PBXGroup; + children = ( + 2394A8432FC5208100B9FF0C /* MSALNativeCredManagment */, + 2394A84F2FC5208100B9FF0C /* MSALNativeCredManagmentTests */, + 2394A8A32FC7FE7200B9FF0C /* MSALNativeCredManagmentSampleApp */, + 2394A8B52FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppTests */, + 2394A8BF2FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppUITests */, + 2394A8422FC5208100B9FF0C /* Products */, + ); + sourceTree = ""; + }; + 2394A8422FC5208100B9FF0C /* Products */ = { + isa = PBXGroup; + children = ( + 2394A8412FC5208100B9FF0C /* MSALNativeCredManagment.framework */, + 2394A84B2FC5208100B9FF0C /* MSALNativeCredManagmentTests.xctest */, + 2394A8A22FC7FE7200B9FF0C /* MSALNativeCredManagmentSampleApp.app */, + 2394A8B22FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppTests.xctest */, + 2394A8BC2FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppUITests.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 = ( + 2394A88A2FC7C14100B9FF0C /* MSAL */, + ); + 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"; + }; + 2394A8A12FC7FE7200B9FF0C /* MSALNativeCredManagmentSampleApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2394A8C42FC7FE7400B9FF0C /* Build configuration list for PBXNativeTarget "MSALNativeCredManagmentSampleApp" */; + buildPhases = ( + 2394A89E2FC7FE7200B9FF0C /* Sources */, + 2394A89F2FC7FE7200B9FF0C /* Frameworks */, + 2394A8A02FC7FE7200B9FF0C /* Resources */, + 2394A8E32FCA000100B9FF0C /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 2394A8E42FCA000100B9FF0C /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 2394A8A32FC7FE7200B9FF0C /* MSALNativeCredManagmentSampleApp */, + ); + name = MSALNativeCredManagmentSampleApp; + packageProductDependencies = ( + 2394A8E62FCA000200B9FF0C /* MSAL */, + ); + 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 */ + 2394A8382FC5208100B9FF0C /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1620; + LastUpgradeCheck = 1620; + TargetAttributes = { + 2394A8402FC5208100B9FF0C = { + CreatedOnToolsVersion = 16.2; + }; + 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" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 2394A8372FC5208100B9FF0C; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 2394A8892FC7C14100B9FF0C /* XCLocalSwiftPackageReference "../../../../microsoft-authentication-library-for-objc" */, + 2394A8D52FC8032400B9FF0C /* XCLocalSwiftPackageReference "../MSALNativeCredManagment" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 2394A8422FC5208100B9FF0C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2394A8402FC5208100B9FF0C /* MSALNativeCredManagment */, + 2394A84A2FC5208100B9FF0C /* MSALNativeCredManagmentTests */, + 2394A8A12FC7FE7200B9FF0C /* MSALNativeCredManagmentSampleApp */, + 2394A8B12FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppTests */, + 2394A8BB2FC7FE7400B9FF0C /* MSALNativeCredManagmentSampleAppUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 2394A83F2FC5208100B9FF0C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2394A8492FC5208100B9FF0C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + 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 */ + 2394A83D2FC5208100B9FF0C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2394A8472FC5208100B9FF0C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + 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 */ + 2394A84E2FC5208100B9FF0C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + 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 */; + }; + 2394A8E42FCA000100B9FF0C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2394A8402FC5208100B9FF0C /* MSALNativeCredManagment */; + targetProxy = 2394A8E22FCA000100B9FF0C /* 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; + HEADER_SEARCH_PATHS = "$(SRCROOT)/../../../MSAL/IdentityCore/IdentityCore/src/**"; + 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_INCLUDE_PATHS = "$(SRCROOT)/CMSAL_Private/include"; + 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; + HEADER_SEARCH_PATHS = "$(SRCROOT)/../../../MSAL/IdentityCore/IdentityCore/src/**"; + 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_INCLUDE_PATHS = "$(SRCROOT)/CMSAL_Private/include"; + 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; + }; + 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.azureauthenticator; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = com.microsoft.CompanyPortalMac; + 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.azureauthenticator; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = com.microsoft.CompanyPortalMac; + 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 */ + 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; + }; + 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 */ + 2394A8892FC7C14100B9FF0C /* XCLocalSwiftPackageReference "../../../../microsoft-authentication-library-for-objc" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../../../../microsoft-authentication-library-for-objc"; + }; + 2394A8D52FC8032400B9FF0C /* XCLocalSwiftPackageReference "../MSALNativeCredManagment" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../MSALNativeCredManagment; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 2394A88A2FC7C14100B9FF0C /* MSAL */ = { + isa = XCSwiftPackageProductDependency; + productName = MSAL; + }; + 2394A8E62FCA000200B9FF0C /* MSAL */ = { + isa = XCSwiftPackageProductDependency; + productName = MSAL; + }; +/* End XCSwiftPackageProductDependency 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/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift new file mode 100644 index 0000000000..8b4e20b152 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSALNativeCredentialMethodsClient+Internal.swift @@ -0,0 +1,290 @@ +// +// 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 + +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 + } + + let scopes = ["https://graph.microsoft.com/.default"] + + tokenProvider.getAccessToken(scopes: scopes) + { accessToken, error in + completion(accessToken, error) + } + } + + /// Async wrapper around the callback-based token acquisition. + internal func acquireTokenAsync( + correlationId: UUID + ) async -> Result + { + return await withCheckedContinuation + { continuation in + self.acquireToken(correlationId: correlationId) + { accessToken, tokenError in + if let tokenError = tokenError + { + let credError = MSALNativeCredentialManagementError( + type: .unauthorized, + message: "Failed to acquire access token.", + 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)) + } + } + } + + // MARK: - API Client Access + + /// Returns or creates the internal API client for server communication. + /// + /// Client selection priority: + /// 1. Mock API client — when `UserDefaults` key + /// `com.microsoft.identity.credentialmanagement.useMockAPI` is `true`. + /// 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 + /// environment changes). + internal func getAPIClient() -> Result + { + let useMock = CredentialManagementEnvironment.isMockAPIEnabled + + // Invalidate cached client if mock state changed + if let existing = apiClient + { + let cachedIsMock = existing is CredentialManagementMockNetworkClient + 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 = CredentialManagementMockNetworkClient() + self.apiClient = mockClient + return .success(mockClient) + } + + guard let tenantSubdomain = config.tenantSubdomain, + let baseURL = URL(string: "https://\(tenantSubdomain).ciamlogin.com") else + { + return .failure(MSALNativeCredentialManagementError( + type: .invalidConfiguration, + message: "tenantSubdomain must be set on MSALNativeCredentialManagementConfig." + )) + } + + let requestSerializer = CredentialManagementRequestSerializer( + urlResolver: CredentialManagementURLResolver(baseURL: baseURL) + ) + + let client = CredentialManagementServerNetworkClient( + requestSerializer: requestSerializer, + requestInterceptor: config.requestInterceptor + ) + + self.apiClient = client + return .success(client) + } + + // MARK: - Challenge Handling + + internal func submitRegistrationChallenge( + code: String, + continuationToken: String, + correlationId: UUID + ) async -> Result + { + guard !code.isEmpty else + { + return .failure(MSALNativeCredentialManagementError( + type: .invalidInput, + message: "Verification code cannot be empty.", + correlationId: correlationId + )) + } + + // Acquire a fresh token for the activation call + let tokenResult = await acquireTokenAsync(correlationId: correlationId) + guard case .success(let accessToken) = tokenResult else + { + return .failure({ if case .failure(let e) = tokenResult { return e }; fatalError("Unreachable") }()) + } + + let clientResult = getAPIClient() + guard case .success(let apiClientInstance) = clientResult else + { + if case .failure(let error) = clientResult { return .failure(error) } + fatalError("Unreachable") + } + + let result = await apiClientInstance.activateEnrollment( + params: OTPActivationParams(continuationToken: continuationToken, code: code), + accessToken: accessToken, + correlationId: correlationId + ) + + switch result + { + case .success(let method): + self.pendingEnrollmentType = nil + return .success(method) + case .failure(let error): + return .failure(error) + } + } + + internal func resendRegistrationChallenge( + continuationToken: String, + correlationId: UUID + ) async -> Result + { + // Acquire token for the re-send call + let tokenResult = await acquireTokenAsync(correlationId: correlationId) + guard case .success(let accessToken) = tokenResult else + { + return .failure({ if case .failure(let e) = tokenResult { return e }; fatalError("Unreachable") }()) + } + + 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 + )) + } + + // 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 = PhoneEnrollmentParams(phoneNumber: "") + case .password: + enrollmentParams = PasswordEnrollmentParams(password: "") + default: + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Resend not supported for type: \(pendingType.rawValue)", + correlationId: correlationId + )) + } + + let result = await apiClientInstance.beginEnrollment( + params: enrollmentParams, + accessToken: accessToken, + correlationId: correlationId + ) + + switch result + { + case .success(let response): + switch response + { + 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) + + 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/MSIDLogger+CredentialManagement.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/MSIDLogger+CredentialManagement.swift new file mode 100644 index 0000000000..2f8435c46c --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/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/credential_method/MSALCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/MSALCredentialMethod.swift new file mode 100644 index 0000000000..1c74e79aea --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/MSALCredentialMethod.swift @@ -0,0 +1,73 @@ +// +// 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 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 a concrete subclass +/// such as `MSALPasskeyCredentialMethod`, `MSALPhoneCredentialMethod`, or +/// `MSALPasswordCredentialMethod`. +@objcMembers +public class MSALCredentialMethod: NSObject, MSALCredentialMethodProtocol { + + /// Unique identifier of the credential method (set by the server). + public internal(set) var id: 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 internal(set) var displayName: String? + + /// 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 + /// can call this via `super.init(...)`. + /// + /// - Parameters: + /// - id: Unique identifier from the server. + /// - credentialType: The type for this credential. + /// - displayName: A user-facing display name or hint. + /// - createdAt: The creation timestamp. + internal init( + id: String, + credentialType: MSALCredentialType, + displayName: String?, + createdAt: Date? + ) + { + self.id = id + self.credentialType = credentialType + self.displayName = displayName + self.createdAt = createdAt + super.init() + } +} 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 new file mode 100644 index 0000000000..be34a70c05 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALNativeCredentialMethodsClient+RegisterPasskey.swift @@ -0,0 +1,189 @@ +// +// 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 +import AuthenticationServices +@_implementationOnly import MSAL_Private + +extension MSALNativeCredentialMethodsClient +{ + /// Performs the full passkey registration flow: + /// 1. Acquires an access token. + /// 2. Calls beginEnrollment to get WebAuthn creation options from the server. + /// 3. Invokes the platform authenticator via ASAuthorization. + /// 4. Calls activateEnrollment with the attestation. + /// 5. Returns the registered credential method. + internal func performRegisterPasskey( + params: MSALRegisterPasskeyParams + ) async -> Result + { + let correlationId = params.correlationId ?? UUID() + + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "performRegisterPasskey: starting") + + // Step 1: Acquire access token + let tokenResult = await acquireTokenAsync(correlationId: correlationId) + guard case .success(let accessToken) = tokenResult else + { + return .failure({ if case .failure(let e) = tokenResult { return e }; fatalError("Unreachable") }()) + } + + // Step 2: Begin enrollment to get creation options from server + switch getAPIClient() + { + case .failure(let error): + return .failure(error) + case .success(let client): + let enrollResult = await client.beginEnrollment( + params: PasskeyEnrollmentParams(), + accessToken: accessToken, + correlationId: correlationId + ) + + guard case .success(let response) = enrollResult else + { + if case .failure(let resultError) = enrollResult { return .failure(resultError) } + fatalError("Unreachable") + } + + // 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 passkey creation options.", + correlationId: correlationId + )) + } + + // Parse creation options from server response + let creationOptions = parseCreationOptions(from: passkeyInfo.publicKey) + + // 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 + let activateResult = await client.activateEnrollment( + params: PasskeyActivationParams( + continuationToken: passkeyInfo.continuationToken, + displayName: params.displayName ?? "Passkey", + credentialId: attestation.credentialId, + attestationObject: attestation.rawAttestationObject, + clientDataJSON: attestation.rawClientDataJSON + ), + accessToken: accessToken, + correlationId: correlationId + ) + + switch activateResult + { + case .success(let method): + MSIDLogger.shared().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/passkey/MSALPasskeyAuthorizationHandler.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/passkey/MSALPasskeyAuthorizationHandler.swift new file mode 100644 index 0000000000..a1fdf519fe --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/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/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift new file mode 100644 index 0000000000..9171850073 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/password/MSALNativeCredentialMethodsClient+RegisterPassword.swift @@ -0,0 +1,92 @@ +// +// 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 +extension MSALNativeCredentialMethodsClient +{ + internal func performRegisterPassword( + params: MSALRegisterPasswordParams + ) async -> Result + { + let correlationId = params.correlationId ?? UUID() + + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "performRegisterPassword: starting") + + // Acquire access token + let tokenResult = await acquireTokenAsync(correlationId: correlationId) + guard case .success(let accessToken) = tokenResult else + { + return .failure({ if case .failure(let e) = tokenResult { return e }; fatalError("Unreachable") }()) + } + + switch getAPIClient() + { + case .failure(let error): + return .failure(error) + case .success(let client): + let enrollResult = await client.beginEnrollment( + params: PasswordEnrollmentParams(password: params.password), + accessToken: accessToken, + correlationId: correlationId + ) + + switch enrollResult + { + case .success(let response): + switch response + { + case .completed(let method): + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "performRegisterPassword: completed") + return .success(.completed(method)) + + 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: "Unexpected passkey creation response for password enrollment.", + correlationId: correlationId + )) + } + + 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 new file mode 100644 index 0000000000..0e12551fcf --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/credential_method/phone/MSALNativeCredentialMethodsClient+RegisterPhone.swift @@ -0,0 +1,97 @@ +// +// 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 +extension MSALNativeCredentialMethodsClient +{ + internal func performRegisterPhoneNumber( + params: MSALRegisterPhoneNumberParams + ) async -> Result + { + let correlationId = params.correlationId ?? UUID() + + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "performRegisterPhoneNumber: starting") + + // Acquire access token + let tokenResult = await acquireTokenAsync(correlationId: correlationId) + guard case .success(let accessToken) = tokenResult else + { + return .failure({ if case .failure(let e) = tokenResult { return e }; fatalError("Unreachable") }()) + } + + switch getAPIClient() + { + case .failure(let error): + return .failure(error) + case .success(let client): + let enrollResult = await client.beginEnrollment( + params: PhoneEnrollmentParams(phoneNumber: params.phoneNumber), + accessToken: accessToken, + correlationId: correlationId + ) + + switch enrollResult + { + case .success(let response): + switch response + { + case .completed(let method): + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "performRegisterPhoneNumber: completed immediately") + return .success(.completed(method)) + + 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: "Unexpected passkey creation response for phone enrollment.", + correlationId: correlationId + )) + } + + case .failure(let error): + return .failure(error) + } + } + } +} 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..32c089a4b5 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/CredentialManagementNetworkClientProtocol.swift @@ -0,0 +1,71 @@ +// +// 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 + +// 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, serialization, 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( + params: EnrollmentParams, + accessToken: String, + correlationId: UUID + ) 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( + params: ActivationParams, + accessToken: String, + correlationId: UUID + ) async -> Result + + /// Delete a credential method. + func deleteMethod( + type: MSALCredentialType, + methodId: String, + accessToken: String, + correlationId: UUID + ) async -> Result +} + diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/mock/CredentialManagementEnvironment.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/mock/CredentialManagementEnvironment.swift new file mode 100644 index 0000000000..507960dc4b --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/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/network/providers/mock/CredentialManagementMockNetworkClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/mock/CredentialManagementMockNetworkClient.swift new file mode 100644 index 0000000000..e72109f08b --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/mock/CredentialManagementMockNetworkClient.swift @@ -0,0 +1,277 @@ +// +// 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( + params: EnrollmentParams, + accessToken: String, + correlationId: UUID + ) async -> Result + { + await simulateNetworkDelay() + + MSIDLogger.shared().log( + level: .info, + correlationId: correlationId, + message: "[Mock] beginEnrollment for params=\(params)" + ) + + switch params + { + case let phoneParams as PhoneEnrollmentParams: + let token = "mock-ct-\(UUID().uuidString.prefix(8))" + pendingEnrollments[token] = PendingEnrollment(type: .phone, phoneNumber: phoneParams.phoneNumber) + + let maskedPhone = maskPhone(phoneParams.phoneNumber) + let challengeInfo = EnrollmentChallengeInfo( + sentTo: maskedPhone, + channelType: "sms", + codeLength: 6, + continuationToken: token + ) + return .success(.challengeRequired(challengeInfo)) + + 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) + + 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)) + + default: + return .failure(MSALNativeCredentialManagementError( + type: .generalError, + message: "Unsupported enrollment params type.", + correlationId: correlationId + )) + } + } + + // MARK: - Activate Enrollment + + func activateEnrollment( + params: ActivationParams, + accessToken: String, + correlationId: UUID + ) async -> Result + { + await simulateNetworkDelay() + + MSIDLogger.shared().log( + level: .info, + correlationId: correlationId, + message: "[Mock] activateEnrollment" + ) + + guard let pending = pendingEnrollments.removeValue(forKey: params.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 method: any MSALCredentialMethodProtocol + switch pending.type + { + case .phone: + method = MSALPhoneCredentialMethod( + id: newId, + createdAt: Date(), + phoneNumber: pending.phoneNumber ?? "+1 (555) 000-0000" + ) + default: + let displayName: String + if let passkeyParams = params as? PasskeyActivationParams + { + displayName = passkeyParams.displayName + } + else + { + displayName = "Passkey" + } + method = MSALPasskeyCredentialMethod( + id: newId, + displayName: displayName, + 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 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/providers/server/CredentialManagementErrorHandler.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialManagementErrorHandler.swift new file mode 100644 index 0000000000..899d8ac4a3 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/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/providers/server/CredentialManagementInterceptorBridge.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialManagementInterceptorBridge.swift new file mode 100644 index 0000000000..4710af3d25 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialManagementInterceptorBridge.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 +import MSAL +@_implementationOnly import MSAL_Private + +/// 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 interceptor: MSALNativeAuthRequestInterceptor + + init(interceptor: MSALNativeAuthRequestInterceptor) + { + self.interceptor = interceptor + } + + func addAdditionalHeaderFields( + for requestUrl: URL?, + with completionBlock: @escaping MSIDHttpRequestInterceptorAddHeaderCompletionBlock + ) + { + interceptor.addAdditionalHeaderFields(requestUrl) { additionalHeaders in + completionBlock(additionalHeaders) + } + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialManagementRequestConfigurator.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialManagementRequestConfigurator.swift new file mode 100644 index 0000000000..93fe589350 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/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/providers/server/CredentialManagementServerNetworkClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialManagementServerNetworkClient.swift new file mode 100644 index 0000000000..d905db7b0d --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialManagementServerNetworkClient.swift @@ -0,0 +1,405 @@ +// +// 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 + +/// Server-backed implementation of `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 + self.requestInterceptor = requestInterceptor + } + + // MARK: - List Methods + + func listMethods( + accessToken: String, + correlationId: UUID + ) async -> Result<[any MSALCredentialMethodProtocol], MSALNativeCredentialManagementError> + { + let typedRequest = ListMethodsRequest(accessToken: accessToken, correlationId: correlationId) + + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "Credential management: listing methods") + + let sendResult = await send(typedRequest) + + switch sendResult + { + 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 + } + } + + // MARK: - Begin Enrollment + + func beginEnrollment( + params: EnrollmentParams, + accessToken: String, + correlationId: UUID + ) async -> Result + { + let body: Data? + + switch params + { + 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: params.type, + accessToken: accessToken, + body: body, + correlationId: correlationId + ) + + MSIDLogger.shared().log( + level: .info, + correlationId: correlationId, + message: "Credential management: beginning enrollment for type '\(CredentialMethodMapper.serverType(from: params.type))'" + ) + + let sendResult = await send(typedRequest) + + switch sendResult + { + case .failure(let e): + return .failure(e) + case .success(let response): + return mapEnrollmentResponse(response, type: params.type, correlationId: correlationId) + } + } + + // MARK: - Activate Enrollment + + func activateEnrollment( + params: ActivationParams, + accessToken: String, + correlationId: UUID + ) async -> Result + { + let body: Data + + switch params + { + 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( + type: .generalError, + message: "Failed to encode OTP activation body.", + correlationId: correlationId + )) + } + body = encoded + + case let passkeyParams as PasskeyActivationParams: + let bodyDict: [String: Any] = [ + "continuationToken": passkeyParams.continuationToken, + "displayName": passkeyParams.displayName, + "publicKeyCredential": [ + "id": passkeyParams.credentialId.base64EncodedString(), + "response": [ + "attestationObject": passkeyParams.attestationObject.base64EncodedString(), + "clientDataJSON": passkeyParams.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 + + 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: params.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, + body: body, + correlationId: correlationId + ) + + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "Credential management: activating enrollment") + + let sendResult = await send(typedRequest) + + switch sendResult + { + case .failure(let e): + return .failure(e) + case .success(let response): + return mapActivationResponse(response, correlationId: correlationId) + } + } + + // MARK: - Delete Method + + func deleteMethod( + type: MSALCredentialType, + methodId: String, + accessToken: String, + correlationId: UUID + ) async -> Result + { + let typedRequest = DeleteMethodRequest( + type: type, + methodId: methodId, + accessToken: accessToken, + correlationId: correlationId + ) + + MSIDLogger.shared().log( + level: .info, + correlationId: correlationId, + message: "Credential management: deleting method of type '\(CredentialMethodMapper.serverType(from: type))'" + ) + + let sendResult = await send(typedRequest) + + switch sendResult + { + case .failure(let e): + return .failure(e) + case .success: + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "Credential management: method deleted successfully") + return .success(()) + } + } + + // 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 + + 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 + { + case .success(let r): msidRequest = r + case .failure(let e): return .failure(e) + } + + 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/providers/server/CredentialManagementURLResolver.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialManagementURLResolver.swift new file mode 100644 index 0000000000..add04a1e6d --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/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/providers/server/CredentialMethodMapper.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialMethodMapper.swift new file mode 100644 index 0000000000..f82c69bd23 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/CredentialMethodMapper.swift @@ -0,0 +1,112 @@ +// +// 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 + { + 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/network/providers/server/HALResource.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/HALResource.swift new file mode 100644 index 0000000000..e08a48a9d7 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/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/providers/server/request/ActivateEnrollmentRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request/ActivateEnrollmentRequest.swift new file mode 100644 index 0000000000..dd6b7fd814 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/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/providers/server/request/BeginEnrollmentRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request/BeginEnrollmentRequest.swift new file mode 100644 index 0000000000..3f52d24c8a --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/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/providers/server/request/CredentialManagementRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request/CredentialManagementRequest.swift new file mode 100644 index 0000000000..28d62a3f17 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/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/providers/server/request/DeleteMethodRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request/DeleteMethodRequest.swift new file mode 100644 index 0000000000..73c04f91be --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/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/providers/server/request/ListMethodsRequest.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request/ListMethodsRequest.swift new file mode 100644 index 0000000000..f08a927922 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/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/providers/server/request_serializer/CredentialManagementRequestSerializer.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request_serializer/CredentialManagementRequestSerializer.swift new file mode 100644 index 0000000000..d4feaf5ef0 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/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/providers/server/request_serializer/CredentialManagementRequestSerializing.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/request_serializer/CredentialManagementRequestSerializing.swift new file mode 100644 index 0000000000..d4b10f5c0a --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/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/providers/server/response/CredentialManagementResponse.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response/CredentialManagementResponse.swift new file mode 100644 index 0000000000..a5402eb8b4 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response/CredentialManagementResponse.swift @@ -0,0 +1,41 @@ +// +// 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 + +/// Typed response model for credential management HTTP responses. +/// Contains the parsed status code, headers, and optional JSON body. +internal struct CredentialManagementResponse +{ + let statusCode: Int + let headers: [String: String] + let jsonBody: [String: Any]? + let rawData: Data? + + /// Whether the response indicates success (2xx). + var isSuccess: Bool + { + return (200...299).contains(statusCode) + } +} diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response_mapper/EnrollmentResponseMapper.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response_mapper/EnrollmentResponseMapper.swift new file mode 100644 index 0000000000..a2db91a166 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/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/providers/server/response_mapper/ListMethodsResponseMapper.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response_mapper/ListMethodsResponseMapper.swift new file mode 100644 index 0000000000..c1489d441b --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/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/providers/server/response_serializer/CredentialManagementHALResponseSerializer.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response_serializer/CredentialManagementHALResponseSerializer.swift new file mode 100644 index 0000000000..05c9aa4fbf --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/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/providers/server/response_serializer/CredentialManagementResponseSerializing.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response_serializer/CredentialManagementResponseSerializing.swift new file mode 100644 index 0000000000..e4fca1af7e --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/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/providers/server/response_serializer/MSIDResponseSerializerAdapter.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/response_serializer/MSIDResponseSerializerAdapter.swift new file mode 100644 index 0000000000..bcc0720734 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/internal/network/providers/server/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/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/MSALNativeCredManagment/src/public/MSALCredentialMethodRegistrationResult.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALCredentialMethodRegistrationResult.swift new file mode 100644 index 0000000000..a53cff380f --- /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(any MSALCredentialMethodProtocol) + + /// 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/MSALNativeCredentialManagementConfig.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift new file mode 100644 index 0000000000..191bd0b212 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementConfig.swift @@ -0,0 +1,65 @@ +// +// 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 tenant subdomain. +/// +/// Example: +/// ```swift +/// let credConfig = MSALNativeCredentialManagementConfig() +/// credConfig.requestInterceptor = sharedRequestInterceptor +/// credConfig.tokenProvider = MyCustomTokenProvider() +/// credConfig.tenantSubdomain = "contoso" +/// 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 tenant subdomain for the CIAM tenant (e.g., "contoso" for contoso.ciamlogin.com). + /// + /// The credential management API base URL is derived from this value. + public var tenantSubdomain: String? + + public override init() + { + super.init() + } +} 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..349e46b4ad --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialManagementError.swift @@ -0,0 +1,101 @@ +// +// 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 + /// The input provided is invalid. + case invalidInput = 9 +} + +/// 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/MSALNativeCredentialMethodsClient.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift new file mode 100644 index 0000000000..7d9f2c4cfb --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/MSALNativeCredentialMethodsClient.swift @@ -0,0 +1,172 @@ +// +// 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 +/// Client for managing credential methods of an authenticated CIAM user. +/// +/// This client provides APIs to list, register, and delete credential methods +/// (e.g., phone, passkey, password) for the currently signed-in user. +/// +/// Example: +/// ```swift +/// let credConfig = MSALNativeCredentialManagementConfig() +/// credConfig.requestInterceptor = sharedRequestInterceptor +/// credConfig.tokenProvider = myTokenProvider +/// let credClient = try MSALNativeCredentialMethodsClient(config: credConfig) +/// ``` +@objcMembers +public class MSALNativeCredentialMethodsClient: NSObject { + + // MARK: - Public: Initialization + + /// 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." + ) + } + guard config.tenantSubdomain != nil else + { + throw MSALNativeCredentialManagementError( + type: .invalidConfiguration, + message: "A tenantSubdomain must be set on MSALNativeCredentialManagementConfig before initializing the client." + ) + } + self.config = config + self.operationQueue = DispatchQueue( + label: "com.microsoft.identity.credentialmanagement", + qos: .userInitiated + ) + + self.apiClient = nil + self.pendingEnrollmentType = nil + + super.init() + } + + // MARK: - Public: List Credential Methods + + /// 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( + correlationId: UUID? = nil + ) async -> Result<[any MSALCredentialMethodProtocol], MSALNativeCredentialManagementError> + { + let correlationId = correlationId ?? UUID() + + MSIDLogger.shared().log(level: .info, correlationId: correlationId, message: "listCredentialMethods: starting") + + let tokenResult = await acquireTokenAsync(correlationId: correlationId) + guard case .success(let accessToken) = tokenResult else + { + return .failure({ if case .failure(let e) = tokenResult { return e }; fatalError("Unreachable") }()) + } + + switch getAPIClient() + { + case .failure(let error): + return .failure(error) + case .success(let client): + return await client.listMethods( + accessToken: accessToken, + correlationId: correlationId + ) + } + } + + // MARK: - Public: Register Operations + + /// Namespace grouping for method-specific registration flows. + /// + /// Each credential type has its own function because registration inputs + /// and activation flows differ per type. + /// + /// Usage: + /// ```swift + /// 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")) + /// ``` + public private(set) lazy var register: MSALRegisterMethods = MSALRegisterMethods(client: self) + + // MARK: - Public: Delete Credential Method + + /// Delete a credential method. + /// + /// - 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: any MSALCredentialMethodProtocol, + correlationId: UUID? = nil + ) async -> Result + { + let correlationId = correlationId ?? UUID() + + MSIDLogger.shared().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({ if case .failure(let e) = tokenResult { return e }; fatalError("Unreachable") }()) + } + + switch getAPIClient() + { + case .failure(let error): + return .failure(error) + case .success(let client): + return await client.deleteMethod( + type: credentialMethod.credentialType, + methodId: credentialMethod.id, + accessToken: accessToken, + correlationId: correlationId + ) + } + } + + // MARK: - Internal: Properties (accessible by extensions) + + internal let config: MSALNativeCredentialManagementConfig + internal let operationQueue: DispatchQueue + internal var apiClient: (any CredentialManagementNetworkClientProtocol)? + internal var pendingEnrollmentType: MSALCredentialType? +} 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/MSALCredentialMethodProtocol.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethodProtocol.swift new file mode 100644 index 0000000000..4388237899 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/MSALCredentialMethodProtocol.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 + +// 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 (e.g., `.passkey`, `.phone`, `.password`). + var credentialType: MSALCredentialType { get } + + /// Display-friendly name or hint (e.g., masked phone "+1 ***-***-1234"). + var displayName: String? { get } + + /// Timestamp of when this method was registered. + var createdAt: Date? { 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/passkey/MSALPasskeyCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyCredentialMethod.swift new file mode 100644 index 0000000000..14bebcd3cd --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALPasskeyCredentialMethod.swift @@ -0,0 +1,76 @@ +// +// 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 MSALCredentialType { + /// Passkey (FIDO2/WebAuthn) credential type. + public static let passkey = MSALCredentialType("fido") +} + +/// 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 AAGUID of the authenticator that created this passkey. + public let aaguid: String? + + public init( + displayName: String?, + credentialID: String?, + aaguid: String? = nil + ) + { + self.credentialID = credentialID + 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?, + credentialID: String?, + aaguid: String? = nil + ) + { + self.credentialID = credentialID + self.aaguid = aaguid + super.init( + id: id, + credentialType: .passkey, + displayName: displayName, + createdAt: createdAt + ) + } +} 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..b129540131 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterMethods+Passkey.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 + +extension MSALRegisterMethods +{ + /// Registers a new passkey (FIDO2/WebAuthn) credential. + /// + /// 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: 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 + ) 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..84946af89d --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/passkey/MSALRegisterPasskeyParams.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 AuthenticationServices +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// Parameters for `client.register.passkey()`. +/// +/// 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? + + /// 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/MSALNativeCredManagment/src/public/credential_method/password/MSALPasswordCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/password/MSALPasswordCredentialMethod.swift new file mode 100644 index 0000000000..a570cf1ac8 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/password/MSALPasswordCredentialMethod.swift @@ -0,0 +1,59 @@ +// +// 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 MSALCredentialType { + /// Password credential type. + public static let password = MSALCredentialType("password") +} + +/// Represents a password credential method. +@objcMembers +public class MSALPasswordCredentialMethod: MSALCredentialMethod { + + public init() + { + super.init( + id: "", + credentialType: .password, + displayName: nil, + createdAt: nil + ) + } + + /// Internal initializer used by the SDK when hydrating from server responses. + internal init( + id: String, + createdAt: Date? + ) + { + super.init( + id: id, + credentialType: .password, + displayName: nil, + createdAt: createdAt + ) + } +} 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/MSALPhoneCredentialMethod.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/phone/MSALPhoneCredentialMethod.swift new file mode 100644 index 0000000000..4ffabe04c7 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/credential_method/phone/MSALPhoneCredentialMethod.swift @@ -0,0 +1,65 @@ +// +// 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 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 { + + /// The masked phone number (e.g., "+1 ***-***-1234"). + public let phoneNumber: String? + + public init(phoneNumber: String?) + { + self.phoneNumber = phoneNumber + 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? + ) + { + self.phoneNumber = phoneNumber + super.init( + id: id, + credentialType: .phone, + displayName: phoneNumber, + createdAt: createdAt + ) + } +} 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/MSALNativeCredManagment/src/public/token_provider/MSALNativeAuthTokenProvider.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/token_provider/MSALNativeAuthTokenProvider.swift new file mode 100644 index 0000000000..c1fa70643d --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/token_provider/MSALNativeAuthTokenProvider.swift @@ -0,0 +1,221 @@ +// +// 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 +#if os(iOS) +import UIKit +#elseif os(macOS) +import AppKit +#endif + +/// A built-in token provider that uses MSAL's web-based interactive flow to acquire tokens. +/// +/// 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 = try MSALNativeAuthTokenProvider(clientId: "your-client-id") +/// credConfig.tokenProvider = tokenProvider +/// ``` +@objcMembers +public class MSALNativeAuthTokenProvider: NSObject, MSALNativeCredentialManagementTokenProvider { + + private let application: MSALPublicClientApplication + private var cachedAccount: MSALAccount? + + /// Initialize with a client ID. Uses the default MSAL authority. + /// + /// - Parameter clientId: The application (client) ID registered in the identity platform. + /// - Throws: If the MSAL configuration is invalid. + public init(clientId: String) throws + { + 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 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. + /// - completionBlock: Called with the access token on success, or nil and an error on failure. + public func getAccessToken( + scopes: [String], + completionBlock: @escaping MSALNativeCredentialManagementTokenCompletionBlock + ) + { + // When mock mode is ON, return a fake token immediately without hitting the network. + if CredentialManagementEnvironment.isMockAPIEnabled + { + completionBlock("mock-access-token-for-testing", nil) + return + } + +// if let account = cachedAccount ?? (try? application.allAccounts().first) +// { +// acquireTokenSilent(scopes: scopes, account: account, completionBlock: completionBlock) +// } +// else +// { + acquireTokenInteractive(scopes: scopes, completionBlock: completionBlock) +// } + } + + /// Clear the cached account so the next token request triggers interactive sign-in. + public func signOut() + { + cachedAccount = nil + } + + // MARK: - Private + + private func acquireTokenSilent( + scopes: [String], + account: MSALAccount, + completionBlock: @escaping MSALNativeCredentialManagementTokenCompletionBlock + ) + { + 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) + } + } + + private func acquireTokenInteractive( + scopes: [String], + completionBlock: @escaping MSALNativeCredentialManagementTokenCompletionBlock + ) + { + 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) + } + } + } + + #if os(iOS) + private func topViewController() -> UIViewController? + { + 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/MSALNativeCredManagment/src/public/token_provider/MSALNativeCredentialManagementTokenProvider.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/token_provider/MSALNativeCredentialManagementTokenProvider.swift new file mode 100644 index 0000000000..a85693002a --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagment/src/public/token_provider/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/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/Configuration.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/Configuration.swift new file mode 100644 index 0000000000..bd2ad85283 --- /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 = "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/ContentView.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift new file mode 100644 index 0000000000..2113257bf6 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/ContentView.swift @@ -0,0 +1,293 @@ +// +// ContentView.swift +// MSALNativeCredManagmentSampleApp +// +// Created by Serhii Demchenko on 2026-05-27. +// + +import SwiftUI +import MSALNativeCredManagment + +struct ContentView: View { + + @EnvironmentObject var viewModel: CredentialManagementViewModel + + @State private var email = "" + @State private var password = "" + @State private var newCredentialType = "phone" + @State private var newCredentialValue = "" + @State private var challengeCode = "" + + var body: some View { + 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: - 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) + #if os(iOS) + .keyboardType(.numberPad) + #endif + .multilineTextAlignment(.center) + .font(.title3) + + Button("Verify") { + viewModel.submitChallenge(code: challengeCode) + challengeCode = "" + } + .buttonStyle(.borderedProminent) + .disabled(challengeCode.isEmpty) + } + .padding() + .navigationTitle("Verify") + #if os(iOS) + .navigationBarTitleDisplayMode(.inline) + #endif + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + viewModel.showChallengeInput = false + challengeCode = "" + } + } + } + } + } + + // 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) + + // 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) + #if os(iOS) + .autocapitalization(.none) + #endif + + 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) + + // 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 + 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("Phone").tag("phone") + Text("Passkey").tag("passkey") + Text("Password").tag("password") + } + + 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(password: newCredentialValue) + default: + viewModel.registerPhone(phoneNumber: newCredentialValue) + } + newCredentialValue = "" + } + .disabled( + (newCredentialType == "phone" && newCredentialValue.isEmpty) || + (newCredentialType == "password" && 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) { + Text(method.credentialType.rawValue.capitalized) + .font(.subheadline) + .bold() + if method.credentialType == .password, let createdAt = method.createdAt { + 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 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) + .foregroundStyle(.secondary) + } + } + Spacer() + Button(role: .destructive) { + viewModel.deleteCredentialMethod(method) + } label: { + Image(systemName: "trash") + } + .buttonStyle(.borderless) + } + } + + private func relativeTimeString(from date: Date) -> String { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .full + return formatter.localizedString(for: date, relativeTo: Date()) + } + + // MARK: - Challenge Verification View + +} + +#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..10aba4cc92 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/CredentialManagementViewModel.swift @@ -0,0 +1,289 @@ +// +// CredentialManagementViewModel.swift +// MSALNativeCredManagmentSampleApp +// +// Created by Serhii Demchenko on 2026-05-27. +// + +import Foundation +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 +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 = "" + + // 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() + } + } + + // MARK: - Private Properties + + private var credClient: MSALNativeCredentialMethodsClient? + private var tokenProvider: MSALNativeAuthTokenProvider? + private var pendingChallengeState: MSALCredentialMethodChallengeState? + + // MARK: - Initialization + + func initialize() { + 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) + MSALGlobalConfig.loggerConfig.logLevel = .verbose + MSALGlobalConfig.loggerConfig.setLogCallback { _, message, containsPII in + if !containsPII { + print("MSAL: \(message ?? "")") + } + } + + // 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() + + // 4. Initialize Credential Management Client + let credConfig = MSALNativeCredentialManagementConfig() + credConfig.requestInterceptor = sharedRequestInterceptor + 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: - Credential Management Operations + + func listCredentialMethods() { + guard let credClient = credClient else { + errorMessage = "Credential client not initialized." + return + } + + isLoading = true + statusMessage = "Loading credential methods..." + errorMessage = nil + + Task { + let result = await credClient.listCredentialMethods() + switch result { + case .success(let methods): + isLoading = false + credentialMethods = methods.compactMap { $0 as? MSALCredentialMethod } + statusMessage = "Loaded \(methods.count) credential method(s)." + case .failure(let error): + isLoading = false + errorMessage = "List failed: \(error.message ?? "Unknown error")" + } + } + } + + // MARK: - Register Phone + + func registerPhone(phoneNumber: String) { + guard let credClient = credClient else { + errorMessage = "Credential client not initialized." + return + } + + isLoading = true + statusMessage = "Registering phone..." + errorMessage = nil + + let params = MSALRegisterPhoneNumberParams(phoneNumber: phoneNumber) + + Task { + let result = await credClient.register.phoneNumber(params: params) + handleRegistrationResult(result) + } + } + + // MARK: - Register Password + + 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: password) + + Task { + let result = await credClient.register.password(params: params) + handleRegistrationResult(result) + } + } + + // MARK: - Register Passkey + + func registerPasskey(displayName: String? = nil) { + guard let credClient = self.credClient else { return } + isLoading = true + + Task { @MainActor in + guard let anchor = Self.resolveAnchor() else + { + self.isLoading = false + self.errorMessage = "No window available." + return + } + + let params = MSALRegisterPasskeyParams( + presentationAnchor: anchor, + displayName: displayName + ) + + let result = await credClient.register.passkey(params: params) + handleRegistrationResult(result) + } + } + + 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 { + 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.rawValue) successfully." + listCredentialMethods() + case .failure(let error): + isLoading = false + errorMessage = "Verification failed: \(error.message ?? "Unknown error")" + } + } + } + + func deleteCredentialMethod(_ method: MSALCredentialMethod) { + guard let credClient = credClient else { + errorMessage = "Credential client not initialized." + return + } + + isLoading = true + statusMessage = "Deleting credential method..." + errorMessage = nil + + Task { + let result = await credClient.deleteCredentialMethod(method) + switch result { + case .success: + isLoading = false + statusMessage = "Credential method deleted." + listCredentialMethods() + case .failure(let error): + isLoading = false + errorMessage = "Delete failed: \(error.message ?? "Unknown error")" + } + } + } + + // MARK: - Sign Out + + func signOut() { + tokenProvider?.signOut() + isSignedIn = false + userName = "" + 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.rawValue) 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) diff --git a/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleApp.entitlements b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleApp.entitlements new file mode 100644 index 0000000000..e5b94ba7c0 --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleApp.entitlements @@ -0,0 +1,22 @@ + + + + + com.apple.developer.associated-domains + + webcredentials:login.microsoft.com + + com.apple.security.app-sandbox + + 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/MSALNativeCredManagmentSampleAppApp.swift b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleAppApp.swift new file mode 100644 index 0000000000..aff6ad513f --- /dev/null +++ b/lib/native/MSALNativeCredManagment/MSALNativeCredManagmentSampleApp/MSALNativeCredManagmentSampleAppApp.swift @@ -0,0 +1,24 @@ +// +// MSALNativeCredManagmentSampleAppApp.swift +// MSALNativeCredManagmentSampleApp +// +// Created by Serhii Demchenko on 2026-05-27. +// + +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/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/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/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) + } +} 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. + } + +} diff --git a/lib/native/MSALNativeCredManagment/Package.swift b/lib/native/MSALNativeCredManagment/Package.swift new file mode 100644 index 0000000000..fddb7f65ca --- /dev/null +++ b/lib/native/MSALNativeCredManagment/Package.swift @@ -0,0 +1,67 @@ +// swift-tools-version: 5.9 +// 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") + +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", + swiftSettings: [ + .unsafeFlags( + ["-Xcc", "-fmodule-map-file=\(packageDir)/CMSAL_Private/include/module.modulemap"] + + allHeaderDirs.flatMap { ["-Xcc", "-I\($0)"] } + ) + ] + ), + .testTarget( + name: "MSALNativeCredManagmentTests", + dependencies: ["MSALNativeCredManagment"], + path: "MSALNativeCredManagmentTests" + ) + ] +)