diff --git a/IdentityCore/IdentityCore.xcodeproj/project.pbxproj b/IdentityCore/IdentityCore.xcodeproj/project.pbxproj index e4b25509ec..31fc06c1aa 100644 --- a/IdentityCore/IdentityCore.xcodeproj/project.pbxproj +++ b/IdentityCore/IdentityCore.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 0570FE80219B8C8C00958ECF /* MSIDCredentialCacheItem+MSIDBaseToken.h in Headers */ = {isa = PBXBuildFile; fileRef = 0570FE7D219B8C8C00958ECF /* MSIDCredentialCacheItem+MSIDBaseToken.h */; }; 0570FE81219E33FB00958ECF /* MSIDCredentialCacheItem+MSIDBaseToken.m in Sources */ = {isa = PBXBuildFile; fileRef = 0570FE7C219B8C8C00958ECF /* MSIDCredentialCacheItem+MSIDBaseToken.m */; }; 0CC830A9C664A75FFE7C032C /* MSIDDIContainerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E356C396133919B12FB4F01 /* MSIDDIContainerTests.m */; }; + 131989824FC049AFB96F9E3E /* MSIDThrottlingRefreshing.h in Headers */ = {isa = PBXBuildFile; fileRef = CACE5D66E6234506B9936BC5 /* MSIDThrottlingRefreshing.h */; }; 1E00D281248F27ED006E4BAE /* MSIDAuthScheme.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E00D27F248F27ED006E4BAE /* MSIDAuthScheme.h */; }; 1E00D282248F27ED006E4BAE /* MSIDAuthScheme.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E00D280248F27ED006E4BAE /* MSIDAuthScheme.m */; }; 1E00D283248F27ED006E4BAE /* MSIDAuthScheme.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E00D280248F27ED006E4BAE /* MSIDAuthScheme.m */; }; @@ -551,6 +552,7 @@ 2A24814F2CB06A1A006FCB34 /* MSIDSSORemoteSilentTokenRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A24814C2CB06A1A006FCB34 /* MSIDSSORemoteSilentTokenRequest.m */; }; 2A2481582CB08050006FCB34 /* MSIDXpcSingleSignOnProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A2481562CB08050006FCB34 /* MSIDXpcSingleSignOnProvider.m */; }; 2A2481592CB08050006FCB34 /* MSIDXpcSingleSignOnProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 2A2481552CB08050006FCB34 /* MSIDXpcSingleSignOnProvider.h */; }; + CA0FEA110000000000000001 /* MSIDXpcCanPerformFailureReason.h in Headers */ = {isa = PBXBuildFile; fileRef = CA0FEA110000000000000002 /* MSIDXpcCanPerformFailureReason.h */; }; 2A24815F2CB08344006FCB34 /* MSIDXpcSilentTokenRequestController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A24815D2CB08344006FCB34 /* MSIDXpcSilentTokenRequestController.m */; }; 2A2481602CB08344006FCB34 /* MSIDXpcSilentTokenRequestController.h in Headers */ = {isa = PBXBuildFile; fileRef = 2A24815C2CB08344006FCB34 /* MSIDXpcSilentTokenRequestController.h */; }; 2A294C292F2D56300042AEA0 /* MSIDExecutionFlowConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 2A294C282F2D56300042AEA0 /* MSIDExecutionFlowConstants.h */; }; @@ -597,9 +599,12 @@ 2A886D702ECBE3D600675D31 /* MSIDGCDStarvationDetector.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A886D6D2ECBE3D600675D31 /* MSIDGCDStarvationDetector.m */; }; 2AADDAC72DADB84D00CB7740 /* MSIDSSOXpcSilentTokenRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2AADDAC62DADB84D00CB7740 /* MSIDSSOXpcSilentTokenRequest.m */; }; 2AADDAC82DADB84D00CB7740 /* MSIDSSOXpcSilentTokenRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 2AADDAC52DADB84D00CB7740 /* MSIDSSOXpcSilentTokenRequest.h */; }; + 2D9C4F18A6B941579F0D8C36 /* MSIDThrottlingMetaDataReading.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A3F1B92D04C45E8A9C16384 /* MSIDThrottlingMetaDataReading.h */; }; 3A7F0C6025AB453BB48081FB /* MSIDDeviceTokenResponseHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DB2B5B443CD84503A7A0A8F5 /* MSIDDeviceTokenResponseHandlerTests.m */; }; 3AA5A4B540B84C95881A3197 /* MSIDDeviceTokenGrantRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 96B39696EBC44368B865FB1E /* MSIDDeviceTokenGrantRequestTests.m */; }; + FE01A1B2C3D4E5F600000002 /* MSIDDeviceTokenUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = FE01A1B2C3D4E5F600000001 /* MSIDDeviceTokenUtilTests.m */; }; 43F7552B93054DDE99785519 /* MSIDDeviceTokenGrantRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 96B39696EBC44368B865FB1E /* MSIDDeviceTokenGrantRequestTests.m */; }; + FE01A1B2C3D4E5F600000003 /* MSIDDeviceTokenUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = FE01A1B2C3D4E5F600000001 /* MSIDDeviceTokenUtilTests.m */; }; 4B6D22262E831B0B00546EC8 /* MSIDFlightManagerQueryKeyDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4B6D22252E831AEA00546EC8 /* MSIDFlightManagerQueryKeyDelegate.h */; }; 4B6D222C2E8342C200546EC8 /* MSIDFlightManagerQueryKeyType.h in Headers */ = {isa = PBXBuildFile; fileRef = 4B6D222A2E8342C200546EC8 /* MSIDFlightManagerQueryKeyType.h */; }; 4B6D222D2E8342C200546EC8 /* MSIDFlightManagerQueryKeyType.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B6D222B2E8342C200546EC8 /* MSIDFlightManagerQueryKeyType.m */; }; @@ -724,8 +729,6 @@ 606830102098E94100CCA6AB /* MSIDCertificateChooser.m in Sources */ = {isa = PBXBuildFile; fileRef = 6068300F2098E94100CCA6AB /* MSIDCertificateChooser.m */; }; 6068303A20A3560F00CCA6AB /* MSIDPKeyAuthHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 6068303820A33A9000CCA6AB /* MSIDPKeyAuthHandler.m */; }; 606B108C20D084B600B34224 /* MSIDAADV1WebviewFactoryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 606B108A20D084B600B34224 /* MSIDAADV1WebviewFactoryTests.m */; }; - B427657B2F3EC2A100F79587 /* MSIDWebviewNavigationHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B427657A2F3EC2A100F79587 /* MSIDWebviewNavigationHandlerTests.m */; }; - B427657C2F3EC2A100F79587 /* MSIDWebviewNavigationHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B427657A2F3EC2A100F79587 /* MSIDWebviewNavigationHandlerTests.m */; }; 606B108E20D08C9500B34224 /* MSIDOAuth2EmbeddedWebviewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 606B108D20D08C9500B34224 /* MSIDOAuth2EmbeddedWebviewControllerTests.m */; }; 606B108F20D08C9500B34224 /* MSIDOAuth2EmbeddedWebviewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 606B108D20D08C9500B34224 /* MSIDOAuth2EmbeddedWebviewControllerTests.m */; }; 607123C1210FCAAD00B91068 /* MSIDAADAuthorityValidationRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 607123C0210FCAAD00B91068 /* MSIDAADAuthorityValidationRequest.m */; }; @@ -802,6 +805,9 @@ 720B5B562DD57D6800318FE5 /* MSIDEcdhApv.m in Sources */ = {isa = PBXBuildFile; fileRef = 720B5B542DD57D6600318FE5 /* MSIDEcdhApv.m */; }; 720B5B582DD58A7F00318FE5 /* MSIDJWECryptoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 720B5B572DD58A6A00318FE5 /* MSIDJWECryptoTests.m */; }; 720B5B592DD58A7F00318FE5 /* MSIDJWECryptoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 720B5B572DD58A6A00318FE5 /* MSIDJWECryptoTests.m */; }; + 7222DA3B2FFEE4020076ED4F /* MSIDDeviceTokenGrantRequestMock.h in Headers */ = {isa = PBXBuildFile; fileRef = 7222DA3A2FFEE3F40076ED4F /* MSIDDeviceTokenGrantRequestMock.h */; }; + 7222DA3E2FFEE42D0076ED4F /* MSIDDeviceTokenGrantRequestMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 7222DA3D2FFEE42B0076ED4F /* MSIDDeviceTokenGrantRequestMock.m */; }; + 7222DA3F2FFEE42D0076ED4F /* MSIDDeviceTokenGrantRequestMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 7222DA3D2FFEE42B0076ED4F /* MSIDDeviceTokenGrantRequestMock.m */; }; 722AC5172F0EF1C9005BE6A5 /* MSIDTestBoundAppRefreshTokenRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 722AC5162F0EF1AC005BE6A5 /* MSIDTestBoundAppRefreshTokenRequest.h */; }; 722AC5192F0EF277005BE6A5 /* MSIDTestBoundAppRefreshTokenRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 722AC5182F0EF275005BE6A5 /* MSIDTestBoundAppRefreshTokenRequest.m */; }; 722AC51A2F0EF277005BE6A5 /* MSIDTestBoundAppRefreshTokenRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 722AC5182F0EF275005BE6A5 /* MSIDTestBoundAppRefreshTokenRequest.m */; }; @@ -870,9 +876,13 @@ 72D961AE2DE12F1F005DED66 /* MSIDCachedNonce.h in Headers */ = {isa = PBXBuildFile; fileRef = 72D961AD2DE12F19005DED66 /* MSIDCachedNonce.h */; }; 72D961B02DE12F30005DED66 /* MSIDCachedNonce.m in Sources */ = {isa = PBXBuildFile; fileRef = 72D961AF2DE12F2E005DED66 /* MSIDCachedNonce.m */; }; 72D961B12DE12F30005DED66 /* MSIDCachedNonce.m in Sources */ = {isa = PBXBuildFile; fileRef = 72D961AF2DE12F2E005DED66 /* MSIDCachedNonce.m */; }; + 72DB19C530070A08008AE594 /* MSIDDeviceTokenUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = 72DB19C4300709E5008AE594 /* MSIDDeviceTokenUtil.h */; }; + 72DB19C630070A08008AE594 /* MSIDDeviceTokenUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = 72DB19C4300709E5008AE594 /* MSIDDeviceTokenUtil.h */; }; + 72DB19C830070A11008AE594 /* MSIDDeviceTokenUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 72DB19C730070A0F008AE594 /* MSIDDeviceTokenUtil.m */; }; + 72DB19C930070A11008AE594 /* MSIDDeviceTokenUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 72DB19C730070A0F008AE594 /* MSIDDeviceTokenUtil.m */; }; + 72DB19CC30072079008AE594 /* MSIDDeviceTokenUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 72DB19CB30072077008AE594 /* MSIDDeviceTokenUtilTests.m */; }; + 72DB19CD30072079008AE594 /* MSIDDeviceTokenUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 72DB19CB30072077008AE594 /* MSIDDeviceTokenUtilTests.m */; }; 73D7C94FBD27C5CA3480B739 /* MSIDDIContainer.h in Headers */ = {isa = PBXBuildFile; fileRef = 30CDFCBD388F4440556637F9 /* MSIDDIContainer.h */; settings = {ATTRIBUTES = (Project, ); }; }; - AE6838E26F5648EFAB74A481 /* MSIDThrottlingRefreshing.h in Headers */ = {isa = PBXBuildFile; fileRef = CACE5D66E6234506B9936BC5 /* MSIDThrottlingRefreshing.h */; }; - B5E8A92C146D4F38B5C92E17 /* MSIDThrottlingMetaDataReading.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A3F1B92D04C45E8A9C16384 /* MSIDThrottlingMetaDataReading.h */; }; 740340B92460E5C400DFCF27 /* MSIDCurrentRequestTelemetrySerializedItem.h in Headers */ = {isa = PBXBuildFile; fileRef = 740340B72460E5C400DFCF27 /* MSIDCurrentRequestTelemetrySerializedItem.h */; }; 740340BA2460E5C400DFCF27 /* MSIDCurrentRequestTelemetrySerializedItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 740340B82460E5C400DFCF27 /* MSIDCurrentRequestTelemetrySerializedItem.m */; }; 740340BB2460E5C400DFCF27 /* MSIDCurrentRequestTelemetrySerializedItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 740340B82460E5C400DFCF27 /* MSIDCurrentRequestTelemetrySerializedItem.m */; }; @@ -1042,6 +1052,7 @@ A0E541D425CDDAB30016E167 /* MSIDThrottlingMetaDataCache.h in Headers */ = {isa = PBXBuildFile; fileRef = A0E541D325CDDAB30016E167 /* MSIDThrottlingMetaDataCache.h */; }; A0E541EE25CDDAFD0016E167 /* MSIDThrottlingMetaDataCache.m in Sources */ = {isa = PBXBuildFile; fileRef = A0E541ED25CDDAFD0016E167 /* MSIDThrottlingMetaDataCache.m */; }; A0E541EF25CDDAFD0016E167 /* MSIDThrottlingMetaDataCache.m in Sources */ = {isa = PBXBuildFile; fileRef = A0E541ED25CDDAFD0016E167 /* MSIDThrottlingMetaDataCache.m */; }; + AE6838E26F5648EFAB74A481 /* MSIDThrottlingRefreshing.h in Headers */ = {isa = PBXBuildFile; fileRef = CACE5D66E6234506B9936BC5 /* MSIDThrottlingRefreshing.h */; }; B2000C8D20EC62D70092790A /* MSIDAADV1IdTokenClaims.m in Sources */ = {isa = PBXBuildFile; fileRef = B2CDB5841FE3427F003A4B5C /* MSIDAADV1IdTokenClaims.m */; }; B2000C8E20EC62DF0092790A /* MSIDAADV1IdTokenClaims.m in Sources */ = {isa = PBXBuildFile; fileRef = B2CDB5841FE3427F003A4B5C /* MSIDAADV1IdTokenClaims.m */; }; B2000C8F20EC63210092790A /* MSIDDefaultCredentialCacheKey.m in Sources */ = {isa = PBXBuildFile; fileRef = B251CC1F2040F6C6005E0179 /* MSIDDefaultCredentialCacheKey.m */; }; @@ -1457,7 +1468,6 @@ B286B9A52389DD07007833AD /* MSIDAuthorizeWebRequestConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = 96A2D5A5209D102900F80E3A /* MSIDAuthorizeWebRequestConfiguration.h */; }; B286B9A62389DD1E007833AD /* MSIDSystemWebviewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 96A3E9B7208941D700BE5262 /* MSIDSystemWebviewController.h */; }; B286B9A72389DD2E007833AD /* MSIDAADOAuthEmbeddedWebviewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 6057EE8E20B5FCF8007976EB /* MSIDAADOAuthEmbeddedWebviewController.h */; }; - VC00000000000000000F2001 /* MSIDOpenIdVcHandling.h in Headers */ = {isa = PBXBuildFile; fileRef = VC00000000000000000F2003 /* MSIDOpenIdVcHandling.h */; }; B286B9A82389DD34007833AD /* MSIDWebviewUIController.h in Headers */ = {isa = PBXBuildFile; fileRef = 60B3855C20A96DAA00D546D0 /* MSIDWebviewUIController.h */; }; B286B9A92389DD37007833AD /* MSIDNTLMUIPrompt.h in Headers */ = {isa = PBXBuildFile; fileRef = 600D199C20963AD50004CD43 /* MSIDNTLMUIPrompt.h */; }; B286B9AA2389DD43007833AD /* MSIDCertificateChooser.h in Headers */ = {isa = PBXBuildFile; fileRef = 6068300E2098E92E00CCA6AB /* MSIDCertificateChooser.h */; }; @@ -1935,6 +1945,13 @@ B41163B929BAC9BF00E64619 /* MSIDWKNavigationActionMock.m in Sources */ = {isa = PBXBuildFile; fileRef = B41163B829BAC9BF00E64619 /* MSIDWKNavigationActionMock.m */; }; B41163BA29BAC9BF00E64619 /* MSIDWKNavigationActionMock.m in Sources */ = {isa = PBXBuildFile; fileRef = B41163B829BAC9BF00E64619 /* MSIDWKNavigationActionMock.m */; }; B41163BC29BAC9EE00E64619 /* MSIDWKNavigationActionMock.h in Headers */ = {isa = PBXBuildFile; fileRef = B41163BB29BAC9DE00E64619 /* MSIDWKNavigationActionMock.h */; }; + B4134C1A2FEA3E410037FE68 /* MSIDMobileOnboardingState.h in Headers */ = {isa = PBXBuildFile; fileRef = B4134C182FEA3E410037FE68 /* MSIDMobileOnboardingState.h */; }; + B4134C1B2FEA3E410037FE68 /* MSIDMobileOnboardingState.m in Sources */ = {isa = PBXBuildFile; fileRef = B4134C192FEA3E410037FE68 /* MSIDMobileOnboardingState.m */; }; + B4134C1C2FEA3E410037FE68 /* MSIDMobileOnboardingState.h in Headers */ = {isa = PBXBuildFile; fileRef = B4134C182FEA3E410037FE68 /* MSIDMobileOnboardingState.h */; }; + B4134C1D2FEA3E410037FE68 /* MSIDMobileOnboardingState.m in Sources */ = {isa = PBXBuildFile; fileRef = B4134C192FEA3E410037FE68 /* MSIDMobileOnboardingState.m */; }; + B4134C442FEC495C0037FE68 /* MSIDMockUXCallbackProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = B4134C422FEC495C0037FE68 /* MSIDMockUXCallbackProvider.h */; }; + B4134C452FEC495C0037FE68 /* MSIDMockUXCallbackProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = B4134C432FEC495C0037FE68 /* MSIDMockUXCallbackProvider.m */; }; + B4134C462FEC495C0037FE68 /* MSIDMockUXCallbackProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = B4134C432FEC495C0037FE68 /* MSIDMockUXCallbackProvider.m */; }; B41DD0A22FB4187500F81A9A /* MSIDIntuneDeviceIdCache.h in Headers */ = {isa = PBXBuildFile; fileRef = B41DD0A12FB4187200F81A9A /* MSIDIntuneDeviceIdCache.h */; }; B41DD0A42FB4187B00F81A9A /* MSIDIntuneDeviceIdCache.m in Sources */ = {isa = PBXBuildFile; fileRef = B41DD0A32FB4187900F81A9A /* MSIDIntuneDeviceIdCache.m */; }; B41DD0A52FB4187B00F81A9A /* MSIDIntuneDeviceIdCache.m in Sources */ = {isa = PBXBuildFile; fileRef = B41DD0A32FB4187900F81A9A /* MSIDIntuneDeviceIdCache.m */; }; @@ -1943,17 +1960,22 @@ B41F0CD62F871F260029E631 /* MSIDWebMDMEnrollmentCompletionResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = B41F0CD52F871F230029E631 /* MSIDWebMDMEnrollmentCompletionResponse.h */; }; B41F0CD82F871F320029E631 /* MSIDWebMDMEnrollmentCompletionResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = B41F0CD72F871F2E0029E631 /* MSIDWebMDMEnrollmentCompletionResponse.m */; }; B41F0CD92F871F320029E631 /* MSIDWebMDMEnrollmentCompletionResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = B41F0CD72F871F2E0029E631 /* MSIDWebMDMEnrollmentCompletionResponse.m */; }; - B427654F2F3EC29200F79587 /* MSIDWebviewNavigationHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = B427654E2F3EC27600F79587 /* MSIDWebviewNavigationHandler.h */; }; - B42765512F3EC2A100F79587 /* MSIDWebviewNavigationHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = B42765502F3EC29F00F79587 /* MSIDWebviewNavigationHandler.m */; }; - B42765522F3EC2A100F79587 /* MSIDWebviewNavigationHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = B42765502F3EC29F00F79587 /* MSIDWebviewNavigationHandler.m */; }; B42558B62F57A6620024523D /* MSIDOnboardingBlobBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = B42558B52F57A65A0024523D /* MSIDOnboardingBlobBuilder.h */; }; B42558B82F57ADFD0024523D /* MSIDOnboardingBlobBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = B42558B72F57ADFD0024523D /* MSIDOnboardingBlobBuilder.m */; }; B42558B92F57ADFD0024523D /* MSIDOnboardingBlobBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = B42558B72F57ADFD0024523D /* MSIDOnboardingBlobBuilder.m */; }; B42558BB2F57AE340024523D /* MSIDOnboardingBlobBuilderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B42558BA2F57AE340024523D /* MSIDOnboardingBlobBuilderTests.m */; }; B42558BC2F57AE340024523D /* MSIDOnboardingBlobBuilderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B42558BA2F57AE340024523D /* MSIDOnboardingBlobBuilderTests.m */; }; + B427654F2F3EC29200F79587 /* MSIDWebviewNavigationHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = B427654E2F3EC27600F79587 /* MSIDWebviewNavigationHandler.h */; }; + B42765512F3EC2A100F79587 /* MSIDWebviewNavigationHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = B42765502F3EC29F00F79587 /* MSIDWebviewNavigationHandler.m */; }; + B42765522F3EC2A100F79587 /* MSIDWebviewNavigationHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = B42765502F3EC29F00F79587 /* MSIDWebviewNavigationHandler.m */; }; + B427657B2F3EC2A100F79587 /* MSIDWebviewNavigationHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B427657A2F3EC2A100F79587 /* MSIDWebviewNavigationHandlerTests.m */; }; + B427657C2F3EC2A100F79587 /* MSIDWebviewNavigationHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B427657A2F3EC2A100F79587 /* MSIDWebviewNavigationHandlerTests.m */; }; B42C16012CE7E54800553316 /* MSIDFamilyRefreshToken.h in Headers */ = {isa = PBXBuildFile; fileRef = B42C16002CE7E53800553316 /* MSIDFamilyRefreshToken.h */; }; B42C16032CE7E55200553316 /* MSIDFamilyRefreshToken.m in Sources */ = {isa = PBXBuildFile; fileRef = B42C16022CE7E54C00553316 /* MSIDFamilyRefreshToken.m */; }; B42C16042CE7E55200553316 /* MSIDFamilyRefreshToken.m in Sources */ = {isa = PBXBuildFile; fileRef = B42C16022CE7E54C00553316 /* MSIDFamilyRefreshToken.m */; }; + B42DA4843008B0E80098AE41 /* MSIDOnboardingBlobBuilder+MSIDTestUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = B42DA4833008B0E80098AE41 /* MSIDOnboardingBlobBuilder+MSIDTestUtil.m */; }; + B42DA4853008B0E80098AE41 /* MSIDOnboardingBlobBuilder+MSIDTestUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = B42DA4833008B0E80098AE41 /* MSIDOnboardingBlobBuilder+MSIDTestUtil.m */; }; + B42DA4863008B0E80098AE41 /* MSIDOnboardingBlobBuilder+MSIDTestUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = B42DA4823008B0E80098AE41 /* MSIDOnboardingBlobBuilder+MSIDTestUtil.h */; }; B431B5232AF040450020CD3D /* MSIDBrokerOperationPasskeyAssertionRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B431B5222AF040450020CD3D /* MSIDBrokerOperationPasskeyAssertionRequestTests.m */; }; B431B5242AF040450020CD3D /* MSIDBrokerOperationPasskeyAssertionRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B431B5222AF040450020CD3D /* MSIDBrokerOperationPasskeyAssertionRequestTests.m */; }; B431B5262AF05B3F0020CD3D /* MSIDBrokerOperationPasskeyCredentialRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B431B5252AF05B3F0020CD3D /* MSIDBrokerOperationPasskeyCredentialRequestTests.m */; }; @@ -2041,6 +2063,19 @@ B4EC850F2EF65C58005567DA /* MSIDJweResponse+EcdhAesGcm.m in Sources */ = {isa = PBXBuildFile; fileRef = B4EC850C2EF65C58005567DA /* MSIDJweResponse+EcdhAesGcm.m */; }; B4EC85102EF65C58005567DA /* MSIDJweResponse+EcdhAesGcm.h in Headers */ = {isa = PBXBuildFile; fileRef = B4EC850B2EF65C58005567DA /* MSIDJweResponse+EcdhAesGcm.h */; }; B4EC85122EF65C58005567DA /* MSIDJweResponse+EcdhAesGcm.m in Sources */ = {isa = PBXBuildFile; fileRef = B4EC850C2EF65C58005567DA /* MSIDJweResponse+EcdhAesGcm.m */; }; + B4F104BF2FE3A16500EBEB5F /* MSIDUXCallbackProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = B4F104BE2FE3A16500EBEB5F /* MSIDUXCallbackProvider.m */; }; + B4F104C02FE3A16500EBEB5F /* MSIDUXCallbackProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = B4F104BC2FE3A16500EBEB5F /* MSIDUXCallbackProtocol.h */; }; + B4F104C12FE3A16500EBEB5F /* MSIDUXCallbackProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = B4F104BD2FE3A16500EBEB5F /* MSIDUXCallbackProvider.h */; }; + B4F104C22FE3A16500EBEB5F /* MSIDUXCallbackProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = B4F104BE2FE3A16500EBEB5F /* MSIDUXCallbackProvider.m */; }; + B4F104C32FE3A16500EBEB5F /* MSIDUXCallbackProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = B4F104BC2FE3A16500EBEB5F /* MSIDUXCallbackProtocol.h */; }; + B4F104C42FE3A16500EBEB5F /* MSIDUXCallbackProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = B4F104BD2FE3A16500EBEB5F /* MSIDUXCallbackProvider.h */; }; + B4FBF0332EF33A7600EDE1E9 /* MSIDOnboardingStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = B4FBF0322EF33A7600EDE1E9 /* MSIDOnboardingStatus.m */; }; + B4FBF0342EF33A7600EDE1E9 /* MSIDOnboardingStatus.h in Headers */ = {isa = PBXBuildFile; fileRef = B4FBF0312EF33A7600EDE1E9 /* MSIDOnboardingStatus.h */; }; + B4FBF0352EF33A7600EDE1E9 /* MSIDOnboardingStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = B4FBF0322EF33A7600EDE1E9 /* MSIDOnboardingStatus.m */; }; + B4FBF0372EF33AAA00EDE1E9 /* MSIDOnboardingStatusTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4FBF0362EF33AAA00EDE1E9 /* MSIDOnboardingStatusTests.m */; }; + B4FBF0382EF33AAA00EDE1E9 /* MSIDOnboardingStatusTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4FBF0362EF33AAA00EDE1E9 /* MSIDOnboardingStatusTests.m */; }; + B4FBF0402EF44BBB00EDE1E9 /* MSIDOnboardingStatusCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4FBF03F2EF44BBB00EDE1E9 /* MSIDOnboardingStatusCacheTests.m */; }; + B4FBF0412EF44BBB00EDE1E9 /* MSIDOnboardingStatusCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4FBF03F2EF44BBB00EDE1E9 /* MSIDOnboardingStatusCacheTests.m */; }; B4FDC6F72F3EFFD300091B6C /* MSIDWebviewNavigationDecisionResolver.h in Headers */ = {isa = PBXBuildFile; fileRef = B4FDC6F62F3EFFB600091B6C /* MSIDWebviewNavigationDecisionResolver.h */; }; B4FDC6F92F3EFFDB00091B6C /* MSIDWebviewNavigationDecisionResolver.m in Sources */ = {isa = PBXBuildFile; fileRef = B4FDC6F82F3EFFD800091B6C /* MSIDWebviewNavigationDecisionResolver.m */; }; B500F7322F1144A900E64911 /* MSIDBrokerOperationGetDefaultAccountRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B500F7302F1144A900E64911 /* MSIDBrokerOperationGetDefaultAccountRequestTests.m */; }; @@ -2058,13 +2093,7 @@ B5AAE11E2F03DADD0026B21B /* MSIDBrokerOperationGetDefaultAccountRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = B5AAE11D2F03DADD0026B21B /* MSIDBrokerOperationGetDefaultAccountRequest.m */; }; B5AAE11F2F03DADD0026B21B /* MSIDBrokerOperationGetDefaultAccountRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = B5AAE11C2F03DADD0026B21B /* MSIDBrokerOperationGetDefaultAccountRequest.h */; }; B5AAE1202F03DADD0026B21B /* MSIDBrokerOperationGetDefaultAccountRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = B5AAE11D2F03DADD0026B21B /* MSIDBrokerOperationGetDefaultAccountRequest.m */; }; - B4FBF0332EF33A7600EDE1E9 /* MSIDOnboardingStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = B4FBF0322EF33A7600EDE1E9 /* MSIDOnboardingStatus.m */; }; - B4FBF0342EF33A7600EDE1E9 /* MSIDOnboardingStatus.h in Headers */ = {isa = PBXBuildFile; fileRef = B4FBF0312EF33A7600EDE1E9 /* MSIDOnboardingStatus.h */; }; - B4FBF0352EF33A7600EDE1E9 /* MSIDOnboardingStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = B4FBF0322EF33A7600EDE1E9 /* MSIDOnboardingStatus.m */; }; - B4FBF0372EF33AAA00EDE1E9 /* MSIDOnboardingStatusTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4FBF0362EF33AAA00EDE1E9 /* MSIDOnboardingStatusTests.m */; }; - B4FBF0382EF33AAA00EDE1E9 /* MSIDOnboardingStatusTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4FBF0362EF33AAA00EDE1E9 /* MSIDOnboardingStatusTests.m */; }; - B4FBF0402EF44BBB00EDE1E9 /* MSIDOnboardingStatusCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4FBF03F2EF44BBB00EDE1E9 /* MSIDOnboardingStatusCacheTests.m */; }; - B4FBF0412EF44BBB00EDE1E9 /* MSIDOnboardingStatusCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4FBF03F2EF44BBB00EDE1E9 /* MSIDOnboardingStatusCacheTests.m */; }; + B5E8A92C146D4F38B5C92E17 /* MSIDThrottlingMetaDataReading.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A3F1B92D04C45E8A9C16384 /* MSIDThrottlingMetaDataReading.h */; }; B86FA7D42383757100E5195A /* MSIDMacACLKeychainAccessorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B86FA7C62383748000E5195A /* MSIDMacACLKeychainAccessorTests.m */; }; B86FA7D52383757600E5195A /* MSIDMacTokenCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B86FA7C72383748000E5195A /* MSIDMacTokenCacheTests.m */; }; B86FA7D62383757A00E5195A /* MSIDMacKeychainTokenCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B86FA7C82383748000E5195A /* MSIDMacKeychainTokenCacheTests.m */; }; @@ -2073,9 +2102,12 @@ B8DBEF652395CA6100A16651 /* MSIDKeychainTokenCache.m in Sources */ = {isa = PBXBuildFile; fileRef = B8DBEF622395CA4700A16651 /* MSIDKeychainTokenCache.m */; }; B8F16E90245B548D0047457F /* MSIDWebViewPlatformParams.h in Headers */ = {isa = PBXBuildFile; fileRef = B8F16E8F245B548D0047457F /* MSIDWebViewPlatformParams.h */; }; B8F16E92245B572C0047457F /* MSIDWebViewPlatformParams.m in Sources */ = {isa = PBXBuildFile; fileRef = B8F16E91245B572C0047457F /* MSIDWebViewPlatformParams.m */; }; + BA000000000000000000C001 /* MSIDBoundTokenProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = BA000000000000000000B001 /* MSIDBoundTokenProvider.h */; }; + BA000000000000000000C002 /* MSIDBoundTokenProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = BA000000000000000000B002 /* MSIDBoundTokenProvider.m */; }; + BA000000000000000000C003 /* MSIDBoundTokenProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = BA000000000000000000B002 /* MSIDBoundTokenProvider.m */; }; + BA000000000000000000D101 /* MSIDBoundTokenProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BA000000000000000000D001 /* MSIDBoundTokenProviderTests.m */; }; + BA000000000000000000D102 /* MSIDBoundTokenProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BA000000000000000000D001 /* MSIDBoundTokenProviderTests.m */; }; C2E599251DB14C46D8DD6261 /* MSIDDIContainer.h in Headers */ = {isa = PBXBuildFile; fileRef = 30CDFCBD388F4440556637F9 /* MSIDDIContainer.h */; settings = {ATTRIBUTES = (Project, ); }; }; - 131989824FC049AFB96F9E3E /* MSIDThrottlingRefreshing.h in Headers */ = {isa = PBXBuildFile; fileRef = CACE5D66E6234506B9936BC5 /* MSIDThrottlingRefreshing.h */; }; - 2D9C4F18A6B941579F0D8C36 /* MSIDThrottlingMetaDataReading.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A3F1B92D04C45E8A9C16384 /* MSIDThrottlingMetaDataReading.h */; }; D11DF760D8901DDC5186BC7A /* MSIDDIContainerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E356C396133919B12FB4F01 /* MSIDDIContainerTests.m */; }; D62600131FBD380500EE4487 /* NSString+MSIDExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = D626000F1FBD380500EE4487 /* NSString+MSIDExtensions.m */; }; D62600141FBD380500EE4487 /* NSString+MSIDExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = D626000F1FBD380500EE4487 /* NSString+MSIDExtensions.m */; }; @@ -2132,6 +2164,9 @@ F7AB2212E4C82BF809D126F6 /* MSIDWPJMetadata.m in Sources */ = {isa = PBXBuildFile; fileRef = F7AB272FEBCF984722F26558 /* MSIDWPJMetadata.m */; }; F7AB25B36873F2E237D26F68 /* MSIDWPJMetadata.m in Sources */ = {isa = PBXBuildFile; fileRef = F7AB272FEBCF984722F26558 /* MSIDWPJMetadata.m */; }; F7AB29D8B906BEA5B6EB8F8C /* MSIDWPJMetadata.h in Headers */ = {isa = PBXBuildFile; fileRef = F7AB2563ADFC286EF6D81445 /* MSIDWPJMetadata.h */; }; + FADE01000000000000000002 /* MSIDDeviceTokenGrantRequestNetworkTests.m in Sources */ = {isa = PBXBuildFile; fileRef = FADE01000000000000000001 /* MSIDDeviceTokenGrantRequestNetworkTests.m */; }; + FADE01000000000000000003 /* MSIDDeviceTokenGrantRequestNetworkTests.m in Sources */ = {isa = PBXBuildFile; fileRef = FADE01000000000000000001 /* MSIDDeviceTokenGrantRequestNetworkTests.m */; }; + VC00000000000000000F2001 /* MSIDOpenIdVcHandling.h in Headers */ = {isa = PBXBuildFile; fileRef = VC00000000000000000F2003 /* MSIDOpenIdVcHandling.h */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -2661,6 +2696,7 @@ 2A24814B2CB06A1A006FCB34 /* MSIDSSORemoteSilentTokenRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDSSORemoteSilentTokenRequest.h; sourceTree = ""; }; 2A24814C2CB06A1A006FCB34 /* MSIDSSORemoteSilentTokenRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDSSORemoteSilentTokenRequest.m; sourceTree = ""; }; 2A2481552CB08050006FCB34 /* MSIDXpcSingleSignOnProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDXpcSingleSignOnProvider.h; sourceTree = ""; }; + CA0FEA110000000000000002 /* MSIDXpcCanPerformFailureReason.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDXpcCanPerformFailureReason.h; sourceTree = ""; }; 2A2481562CB08050006FCB34 /* MSIDXpcSingleSignOnProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDXpcSingleSignOnProvider.m; sourceTree = ""; }; 2A24815C2CB08344006FCB34 /* MSIDXpcSilentTokenRequestController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDXpcSilentTokenRequestController.h; sourceTree = ""; }; 2A24815D2CB08344006FCB34 /* MSIDXpcSilentTokenRequestController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDXpcSilentTokenRequestController.m; sourceTree = ""; }; @@ -2700,8 +2736,6 @@ 2E356C396133919B12FB4F01 /* MSIDDIContainerTests.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = MSIDDIContainerTests.m; sourceTree = ""; }; 305240E0A18733A71978A8A4 /* MSIDOnboardingBlobFieldKeys.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = MSIDOnboardingBlobFieldKeys.h; sourceTree = ""; }; 30CDFCBD388F4440556637F9 /* MSIDDIContainer.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = MSIDDIContainer.h; sourceTree = ""; }; - CACE5D66E6234506B9936BC5 /* MSIDThrottlingRefreshing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDThrottlingRefreshing.h; sourceTree = ""; }; - 7A3F1B92D04C45E8A9C16384 /* MSIDThrottlingMetaDataReading.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDThrottlingMetaDataReading.h; sourceTree = ""; }; 4B6D22252E831AEA00546EC8 /* MSIDFlightManagerQueryKeyDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDFlightManagerQueryKeyDelegate.h; sourceTree = ""; }; 4B6D222A2E8342C200546EC8 /* MSIDFlightManagerQueryKeyType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDFlightManagerQueryKeyType.h; sourceTree = ""; }; 4B6D222B2E8342C200546EC8 /* MSIDFlightManagerQueryKeyType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDFlightManagerQueryKeyType.m; sourceTree = ""; }; @@ -2794,7 +2828,6 @@ 602CD4E123739B3C00A4D7F3 /* MSIDBrokerOperationGetAccountsRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSIDBrokerOperationGetAccountsRequest.m; sourceTree = ""; }; 6035CD8B207EA67300369E69 /* MSIDTelemetryIntegrationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDTelemetryIntegrationTests.m; sourceTree = ""; }; 6057EE8E20B5FCF8007976EB /* MSIDAADOAuthEmbeddedWebviewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDAADOAuthEmbeddedWebviewController.h; sourceTree = ""; }; - VC00000000000000000F2003 /* MSIDOpenIdVcHandling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDOpenIdVcHandling.h; sourceTree = ""; }; 6057EE8F20B5FDF8007976EB /* MSIDAADOAuthEmbeddedWebviewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDAADOAuthEmbeddedWebviewController.m; sourceTree = ""; }; 606830032098ACC100CCA6AB /* MSIDNegotiateHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDNegotiateHandler.h; sourceTree = ""; }; 606830042098ACED00CCA6AB /* MSIDNegotiateHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDNegotiateHandler.m; sourceTree = ""; }; @@ -2805,7 +2838,6 @@ 6068303720A33A7400CCA6AB /* MSIDPKeyAuthHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDPKeyAuthHandler.h; sourceTree = ""; }; 6068303820A33A9000CCA6AB /* MSIDPKeyAuthHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDPKeyAuthHandler.m; sourceTree = ""; }; 606B108A20D084B600B34224 /* MSIDAADV1WebviewFactoryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDAADV1WebviewFactoryTests.m; sourceTree = ""; }; - B427657A2F3EC2A100F79587 /* MSIDWebviewNavigationHandlerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDWebviewNavigationHandlerTests.m; sourceTree = ""; }; 606B108D20D08C9500B34224 /* MSIDOAuth2EmbeddedWebviewControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDOAuth2EmbeddedWebviewControllerTests.m; sourceTree = ""; }; 607123BF210FCA7400B91068 /* MSIDAADAuthorityValidationRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDAADAuthorityValidationRequest.h; sourceTree = ""; }; 607123C0210FCAAD00B91068 /* MSIDAADAuthorityValidationRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDAADAuthorityValidationRequest.m; sourceTree = ""; }; @@ -2881,6 +2913,8 @@ 720B5B522DD57C3700318FE5 /* MSIDEcdhApv.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDEcdhApv.h; sourceTree = ""; }; 720B5B542DD57D6600318FE5 /* MSIDEcdhApv.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDEcdhApv.m; sourceTree = ""; }; 720B5B572DD58A6A00318FE5 /* MSIDJWECryptoTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDJWECryptoTests.m; sourceTree = ""; }; + 7222DA3A2FFEE3F40076ED4F /* MSIDDeviceTokenGrantRequestMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDDeviceTokenGrantRequestMock.h; sourceTree = ""; }; + 7222DA3D2FFEE42B0076ED4F /* MSIDDeviceTokenGrantRequestMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDDeviceTokenGrantRequestMock.m; sourceTree = ""; }; 722AC5162F0EF1AC005BE6A5 /* MSIDTestBoundAppRefreshTokenRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDTestBoundAppRefreshTokenRequest.h; sourceTree = ""; }; 722AC5182F0EF275005BE6A5 /* MSIDTestBoundAppRefreshTokenRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDTestBoundAppRefreshTokenRequest.m; sourceTree = ""; }; 7233F08D2F88967A009C9602 /* MSIDDeviceTokenGrantRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDDeviceTokenGrantRequest.h; sourceTree = ""; }; @@ -2927,6 +2961,9 @@ 72C764F82E09CFA400043AB1 /* MSIDBoundRefreshTokenTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDBoundRefreshTokenTests.m; sourceTree = ""; }; 72D961AD2DE12F19005DED66 /* MSIDCachedNonce.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDCachedNonce.h; sourceTree = ""; }; 72D961AF2DE12F2E005DED66 /* MSIDCachedNonce.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDCachedNonce.m; sourceTree = ""; }; + 72DB19C4300709E5008AE594 /* MSIDDeviceTokenUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDDeviceTokenUtil.h; sourceTree = ""; }; + 72DB19C730070A0F008AE594 /* MSIDDeviceTokenUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDDeviceTokenUtil.m; sourceTree = ""; }; + 72DB19CB30072077008AE594 /* MSIDDeviceTokenUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDDeviceTokenUtilTests.m; sourceTree = ""; }; 740340B72460E5C400DFCF27 /* MSIDCurrentRequestTelemetrySerializedItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDCurrentRequestTelemetrySerializedItem.h; sourceTree = ""; }; 740340B82460E5C400DFCF27 /* MSIDCurrentRequestTelemetrySerializedItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDCurrentRequestTelemetrySerializedItem.m; sourceTree = ""; }; 74043F7C245CC84B00D3E7C1 /* MSIDCurrentRequestTelemetryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDCurrentRequestTelemetryTests.m; sourceTree = ""; }; @@ -2935,10 +2972,10 @@ 74F04D47246C8AC000094017 /* MSIDLastRequestTelemetrySerializedItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDLastRequestTelemetrySerializedItem.h; sourceTree = ""; }; 74F04D48246C8AC000094017 /* MSIDLastRequestTelemetrySerializedItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDLastRequestTelemetrySerializedItem.m; sourceTree = ""; }; 74F04D4C246CB5B100094017 /* MSIDCurrentRequestTelemetrySerializedItem+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSIDCurrentRequestTelemetrySerializedItem+Internal.h"; sourceTree = ""; }; + 7A3F1B92D04C45E8A9C16384 /* MSIDThrottlingMetaDataReading.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDThrottlingMetaDataReading.h; sourceTree = ""; }; 80878AED247A7BBF000BC522 /* MSIDWorkPlaceJoinUtilBase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDWorkPlaceJoinUtilBase.h; sourceTree = ""; }; 80878AEE247A84C1000BC522 /* MSIDWorkPlaceJoinUtilBase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDWorkPlaceJoinUtilBase.m; sourceTree = ""; }; 809B38212480C3C8001DF9D4 /* MSIDWorkPlaceJoinUtilBase+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSIDWorkPlaceJoinUtilBase+Internal.h"; sourceTree = ""; }; - B7A1F4D2C8E94F1B9D6E3A21 /* MSIDWorkPlaceJoinUtilProviding.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDWorkPlaceJoinUtilProviding.h; sourceTree = ""; }; 80B6BF3B2480A3E30031BFE8 /* MSIDWorkPlaceJoinUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDWorkPlaceJoinUtilTests.m; sourceTree = ""; }; 886F516829CCA68A00F09471 /* MSIDCIAMAuthority.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDCIAMAuthority.h; sourceTree = ""; }; 886F516A29CCA6B800F09471 /* MSIDCIAMAuthority.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDCIAMAuthority.m; sourceTree = ""; }; @@ -3012,6 +3049,7 @@ 96A3E9B8208941D700BE5262 /* MSIDSystemWebviewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDSystemWebviewController.m; sourceTree = ""; }; 96A3E9C020895CCE00BE5262 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.3.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; }; 96B39696EBC44368B865FB1E /* MSIDDeviceTokenGrantRequestTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDDeviceTokenGrantRequestTests.m; sourceTree = ""; }; + FE01A1B2C3D4E5F600000001 /* MSIDDeviceTokenUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDDeviceTokenUtilTests.m; sourceTree = ""; }; 96B82B6E208DD88F00CAB843 /* SafariServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SafariServices.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.2.sdk/System/Library/Frameworks/SafariServices.framework; sourceTree = DEVELOPER_DIR; }; 96B8D57920946D2600E3F4A6 /* MSIDPkce.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSIDPkce.m; sourceTree = ""; }; 96B8D57A20946D2600E3F4A6 /* MSIDPkce.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSIDPkce.h; sourceTree = ""; }; @@ -3589,18 +3627,25 @@ B41163B529BAC20000E64619 /* MSIDAADOAuthEmbeddedWebviewControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDAADOAuthEmbeddedWebviewControllerTests.m; sourceTree = ""; }; B41163B829BAC9BF00E64619 /* MSIDWKNavigationActionMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDWKNavigationActionMock.m; sourceTree = ""; }; B41163BB29BAC9DE00E64619 /* MSIDWKNavigationActionMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDWKNavigationActionMock.h; sourceTree = ""; }; + B4134C182FEA3E410037FE68 /* MSIDMobileOnboardingState.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDMobileOnboardingState.h; sourceTree = ""; }; + B4134C192FEA3E410037FE68 /* MSIDMobileOnboardingState.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDMobileOnboardingState.m; sourceTree = ""; }; + B4134C422FEC495C0037FE68 /* MSIDMockUXCallbackProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDMockUXCallbackProvider.h; sourceTree = ""; }; + B4134C432FEC495C0037FE68 /* MSIDMockUXCallbackProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDMockUXCallbackProvider.m; sourceTree = ""; }; B41DD0A12FB4187200F81A9A /* MSIDIntuneDeviceIdCache.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDIntuneDeviceIdCache.h; sourceTree = ""; }; B41DD0A32FB4187900F81A9A /* MSIDIntuneDeviceIdCache.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDIntuneDeviceIdCache.m; sourceTree = ""; }; B41DD0A62FB41A0000F81A9A /* MSIDIntuneDeviceIdCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDIntuneDeviceIdCacheTests.m; sourceTree = ""; }; B41F0CD52F871F230029E631 /* MSIDWebMDMEnrollmentCompletionResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDWebMDMEnrollmentCompletionResponse.h; sourceTree = ""; }; B41F0CD72F871F2E0029E631 /* MSIDWebMDMEnrollmentCompletionResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDWebMDMEnrollmentCompletionResponse.m; sourceTree = ""; }; - B427654E2F3EC27600F79587 /* MSIDWebviewNavigationHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDWebviewNavigationHandler.h; sourceTree = ""; }; - B42765502F3EC29F00F79587 /* MSIDWebviewNavigationHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDWebviewNavigationHandler.m; sourceTree = ""; }; B42558B52F57A65A0024523D /* MSIDOnboardingBlobBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDOnboardingBlobBuilder.h; sourceTree = ""; }; B42558B72F57ADFD0024523D /* MSIDOnboardingBlobBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDOnboardingBlobBuilder.m; sourceTree = ""; }; B42558BA2F57AE340024523D /* MSIDOnboardingBlobBuilderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDOnboardingBlobBuilderTests.m; sourceTree = ""; }; + B427654E2F3EC27600F79587 /* MSIDWebviewNavigationHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDWebviewNavigationHandler.h; sourceTree = ""; }; + B42765502F3EC29F00F79587 /* MSIDWebviewNavigationHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDWebviewNavigationHandler.m; sourceTree = ""; }; + B427657A2F3EC2A100F79587 /* MSIDWebviewNavigationHandlerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDWebviewNavigationHandlerTests.m; sourceTree = ""; }; B42C16002CE7E53800553316 /* MSIDFamilyRefreshToken.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDFamilyRefreshToken.h; sourceTree = ""; }; B42C16022CE7E54C00553316 /* MSIDFamilyRefreshToken.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDFamilyRefreshToken.m; sourceTree = ""; }; + B42DA4823008B0E80098AE41 /* MSIDOnboardingBlobBuilder+MSIDTestUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSIDOnboardingBlobBuilder+MSIDTestUtil.h"; sourceTree = ""; }; + B42DA4833008B0E80098AE41 /* MSIDOnboardingBlobBuilder+MSIDTestUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "MSIDOnboardingBlobBuilder+MSIDTestUtil.m"; sourceTree = ""; }; B431B5222AF040450020CD3D /* MSIDBrokerOperationPasskeyAssertionRequestTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDBrokerOperationPasskeyAssertionRequestTests.m; sourceTree = ""; }; B431B5252AF05B3F0020CD3D /* MSIDBrokerOperationPasskeyCredentialRequestTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDBrokerOperationPasskeyCredentialRequestTests.m; sourceTree = ""; }; B431B5282AF05C890020CD3D /* MSIDBrokerOperationGetPasskeyAssertionResponseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDBrokerOperationGetPasskeyAssertionResponseTests.m; sourceTree = ""; }; @@ -3658,6 +3703,13 @@ B4EC850A2EF65C58005567DA /* MSIDAesGcmDecryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSIDAesGcmDecryptor.swift; sourceTree = ""; }; B4EC850B2EF65C58005567DA /* MSIDJweResponse+EcdhAesGcm.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSIDJweResponse+EcdhAesGcm.h"; sourceTree = ""; }; B4EC850C2EF65C58005567DA /* MSIDJweResponse+EcdhAesGcm.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "MSIDJweResponse+EcdhAesGcm.m"; sourceTree = ""; }; + B4F104BC2FE3A16500EBEB5F /* MSIDUXCallbackProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDUXCallbackProtocol.h; sourceTree = ""; }; + B4F104BD2FE3A16500EBEB5F /* MSIDUXCallbackProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDUXCallbackProvider.h; sourceTree = ""; }; + B4F104BE2FE3A16500EBEB5F /* MSIDUXCallbackProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDUXCallbackProvider.m; sourceTree = ""; }; + B4FBF0312EF33A7600EDE1E9 /* MSIDOnboardingStatus.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDOnboardingStatus.h; sourceTree = ""; }; + B4FBF0322EF33A7600EDE1E9 /* MSIDOnboardingStatus.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDOnboardingStatus.m; sourceTree = ""; }; + B4FBF0362EF33AAA00EDE1E9 /* MSIDOnboardingStatusTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDOnboardingStatusTests.m; sourceTree = ""; }; + B4FBF03F2EF44BBB00EDE1E9 /* MSIDOnboardingStatusCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDOnboardingStatusCacheTests.m; sourceTree = ""; }; B4FDC6F62F3EFFB600091B6C /* MSIDWebviewNavigationDecisionResolver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDWebviewNavigationDecisionResolver.h; sourceTree = ""; }; B4FDC6F82F3EFFD800091B6C /* MSIDWebviewNavigationDecisionResolver.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDWebviewNavigationDecisionResolver.m; sourceTree = ""; }; B500F7302F1144A900E64911 /* MSIDBrokerOperationGetDefaultAccountRequestTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDBrokerOperationGetDefaultAccountRequestTests.m; sourceTree = ""; }; @@ -3671,10 +3723,7 @@ B5AAE1182F03D7AA0026B21B /* MSIDBrokerOperationGetDefaultAccountResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDBrokerOperationGetDefaultAccountResponse.m; sourceTree = ""; }; B5AAE11C2F03DADD0026B21B /* MSIDBrokerOperationGetDefaultAccountRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDBrokerOperationGetDefaultAccountRequest.h; sourceTree = ""; }; B5AAE11D2F03DADD0026B21B /* MSIDBrokerOperationGetDefaultAccountRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDBrokerOperationGetDefaultAccountRequest.m; sourceTree = ""; }; - B4FBF0312EF33A7600EDE1E9 /* MSIDOnboardingStatus.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDOnboardingStatus.h; sourceTree = ""; }; - B4FBF0322EF33A7600EDE1E9 /* MSIDOnboardingStatus.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDOnboardingStatus.m; sourceTree = ""; }; - B4FBF0362EF33AAA00EDE1E9 /* MSIDOnboardingStatusTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDOnboardingStatusTests.m; sourceTree = ""; }; - B4FBF03F2EF44BBB00EDE1E9 /* MSIDOnboardingStatusCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDOnboardingStatusCacheTests.m; sourceTree = ""; }; + B7A1F4D2C8E94F1B9D6E3A21 /* MSIDWorkPlaceJoinUtilProviding.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDWorkPlaceJoinUtilProviding.h; sourceTree = ""; }; B86FA7C62383748000E5195A /* MSIDMacACLKeychainAccessorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSIDMacACLKeychainAccessorTests.m; sourceTree = ""; }; B86FA7C72383748000E5195A /* MSIDMacTokenCacheTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSIDMacTokenCacheTests.m; sourceTree = ""; }; B86FA7C82383748000E5195A /* MSIDMacKeychainTokenCacheTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSIDMacKeychainTokenCacheTests.m; sourceTree = ""; }; @@ -3684,6 +3733,10 @@ B8DBEF632395CA4800A16651 /* MSIDKeychainTokenCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSIDKeychainTokenCache.h; sourceTree = ""; }; B8F16E8F245B548D0047457F /* MSIDWebViewPlatformParams.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDWebViewPlatformParams.h; sourceTree = ""; }; B8F16E91245B572C0047457F /* MSIDWebViewPlatformParams.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDWebViewPlatformParams.m; sourceTree = ""; }; + BA000000000000000000B001 /* MSIDBoundTokenProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDBoundTokenProvider.h; sourceTree = ""; }; + BA000000000000000000B002 /* MSIDBoundTokenProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDBoundTokenProvider.m; sourceTree = ""; }; + BA000000000000000000D001 /* MSIDBoundTokenProviderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDBoundTokenProviderTests.m; sourceTree = ""; }; + CACE5D66E6234506B9936BC5 /* MSIDThrottlingRefreshing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDThrottlingRefreshing.h; sourceTree = ""; }; D626000F1FBD380500EE4487 /* NSString+MSIDExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+MSIDExtensions.m"; sourceTree = ""; }; D62600101FBD380500EE4487 /* NSDictionary+MSIDExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+MSIDExtensions.h"; sourceTree = ""; }; D62600111FBD380500EE4487 /* NSDictionary+MSIDExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+MSIDExtensions.m"; sourceTree = ""; }; @@ -3746,6 +3799,8 @@ E7B67293257ED6E30053773F /* MSIDLRUCache.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDLRUCache.h; sourceTree = ""; }; F7AB2563ADFC286EF6D81445 /* MSIDWPJMetadata.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSIDWPJMetadata.h; sourceTree = ""; }; F7AB272FEBCF984722F26558 /* MSIDWPJMetadata.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSIDWPJMetadata.m; sourceTree = ""; }; + FADE01000000000000000001 /* MSIDDeviceTokenGrantRequestNetworkTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSIDDeviceTokenGrantRequestNetworkTests.m; sourceTree = ""; }; + VC00000000000000000F2003 /* MSIDOpenIdVcHandling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSIDOpenIdVcHandling.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -3907,6 +3962,7 @@ isa = PBXGroup; children = ( 2A2481552CB08050006FCB34 /* MSIDXpcSingleSignOnProvider.h */, + CA0FEA110000000000000002 /* MSIDXpcCanPerformFailureReason.h */, 1E62D0E3228B75E3000E2BBC /* MSIDKeychainUtil.m */, 2A2481562CB08050006FCB34 /* MSIDXpcSingleSignOnProvider.m */, 2A366B752D9EEB4400774DD4 /* MSIDXpcProviderCaching.h */, @@ -4093,6 +4149,10 @@ 23642AB32187D88C00F97009 /* mocks */ = { isa = PBXGroup; children = ( + 7222DA3D2FFEE42B0076ED4F /* MSIDDeviceTokenGrantRequestMock.m */, + 7222DA3A2FFEE3F40076ED4F /* MSIDDeviceTokenGrantRequestMock.h */, + B4134C422FEC495C0037FE68 /* MSIDMockUXCallbackProvider.h */, + B4134C432FEC495C0037FE68 /* MSIDMockUXCallbackProvider.m */, 722AC5182F0EF275005BE6A5 /* MSIDTestBoundAppRefreshTokenRequest.m */, 722AC5162F0EF1AC005BE6A5 /* MSIDTestBoundAppRefreshTokenRequest.h */, 729357EE2DDBCBAB0001D03C /* MSIDNonceTokenRequestMock.m */, @@ -4590,6 +4650,8 @@ 600D19A720964C330004CD43 /* workplacejoin */ = { isa = PBXGroup; children = ( + 72DB19C730070A0F008AE594 /* MSIDDeviceTokenUtil.m */, + 72DB19C4300709E5008AE594 /* MSIDDeviceTokenUtil.h */, 600D19AC20964CAF0004CD43 /* MSIDRegistrationInformation.h */, 600D19AD20964CC00004CD43 /* MSIDRegistrationInformation.m */, 600D19B020964CD40004CD43 /* MSIDPkeyAuthHelper.h */, @@ -5288,6 +5350,8 @@ 72978AEA2E4C240B00DEA46D /* MSIDBoundRefreshToken+Redemption.h */, 72C1EBF62DE91ACC004C40A4 /* MSIDBoundRefreshToken.m */, 72C1EBF42DE91ABE004C40A4 /* MSIDBoundRefreshToken.h */, + BA000000000000000000B002 /* MSIDBoundTokenProvider.m */, + BA000000000000000000B001 /* MSIDBoundTokenProvider.h */, B2675689228CE6FC000F01D7 /* protocols */, B251CC4E204105AD005E0179 /* MSIDCredentialType.h */, B251CC4F204105AD005E0179 /* MSIDCredentialType.m */, @@ -5518,6 +5582,8 @@ B2AF1D3D218BD02F0080C1A0 /* parameters */ = { isa = PBXGroup; children = ( + B4134C182FEA3E410037FE68 /* MSIDMobileOnboardingState.h */, + B4134C192FEA3E410037FE68 /* MSIDMobileOnboardingState.m */, 72978AF12E4C2C3300DEA46D /* MSIDBoundRefreshTokenRedemptionParameters.m */, 72978AEF2E4C2A1E00DEA46D /* MSIDBoundRefreshTokenRedemptionParameters.h */, B2968C8322F3C3E8005AFC33 /* MSIDBrokerInvocationOptions.h */, @@ -5920,6 +5986,8 @@ D626FFE91FBD200A00EE4487 /* util */ = { isa = PBXGroup; children = ( + B42DA4823008B0E80098AE41 /* MSIDOnboardingBlobBuilder+MSIDTestUtil.h */, + B42DA4833008B0E80098AE41 /* MSIDOnboardingBlobBuilder+MSIDTestUtil.m */, 23CA0C5B220A540A00768729 /* NSData+MSIDTestUtil.h */, 23CA0C5C220A540A00768729 /* NSData+MSIDTestUtil.m */, B233F8B0219CDF5B00DC90E3 /* MSIDTestURLResponse+Util.h */, @@ -6056,6 +6124,9 @@ D6DA89721FBA6A4E004C56C7 /* src */ = { isa = PBXGroup; children = ( + B4F104BC2FE3A16500EBEB5F /* MSIDUXCallbackProtocol.h */, + B4F104BD2FE3A16500EBEB5F /* MSIDUXCallbackProvider.h */, + B4F104BE2FE3A16500EBEB5F /* MSIDUXCallbackProvider.m */, 7252BB772F2AE53700B2287F /* MSIDSwiftBridgingHeader.h */, 7209A3D42EB581BB0050CB13 /* MSIDJweResponse.m */, 7209A3D22EB581A90050CB13 /* MSIDJweResponse.h */, @@ -6138,6 +6209,7 @@ D6DA89731FBA6A4E004C56C7 /* tests */ = { isa = PBXGroup; children = ( + 72DB19CB30072077008AE594 /* MSIDDeviceTokenUtilTests.m */, B2C758FE207EE58200C1FE74 /* automation */, 1EE8FF6424F4C0E600CA1445 /* File.swift */, 1EE8FF6224F4C0E600CA1445 /* IdentityCoreTests-Bridging-Header.h */, @@ -6194,6 +6266,7 @@ 2321532C1FDF4FD800C6960D /* MSIDBaseTokenTests.m */, 724C9DD32E6906270039BAA0 /* MSIDBoundRefreshTokenRedemptionTests.m */, 72C764F82E09CFA400043AB1 /* MSIDBoundRefreshTokenTests.m */, + BA000000000000000000D001 /* MSIDBoundTokenProviderTests.m */, B48FC0612D7A90F4007B80DB /* MSIDBrokerFlightProviderTests.m */, B2E97FB22914CC4500AFD558 /* MSIDBrokerNativeAppOperationResponseTests.m */, 2318D7882E12B8E800A5A46E /* MSIDBrokerOperationBrowserNativeMessageMATSReportTests.m */, @@ -6250,6 +6323,8 @@ 6080B9A823887D21009B1322 /* MSIDDeviceInfoTests.m */, A08D09E724A85A1E00C9193D /* MSIDDevicePopManagerTest.m */, 96B39696EBC44368B865FB1E /* MSIDDeviceTokenGrantRequestTests.m */, + FE01A1B2C3D4E5F600000001 /* MSIDDeviceTokenUtilTests.m */, + FADE01000000000000000001 /* MSIDDeviceTokenGrantRequestNetworkTests.m */, DB2B5B443CD84503A7A0A8F5 /* MSIDDeviceTokenResponseHandlerTests.m */, 2E356C396133919B12FB4F01 /* MSIDDIContainerTests.m */, B27CCDD4229EF2C000CAD565 /* MSIDDictionaryExtensionsTests.m */, @@ -6397,7 +6472,11 @@ files = ( 73D7C94FBD27C5CA3480B739 /* MSIDDIContainer.h in Headers */, AE6838E26F5648EFAB74A481 /* MSIDThrottlingRefreshing.h in Headers */, + B4134C1C2FEA3E410037FE68 /* MSIDMobileOnboardingState.h in Headers */, B5E8A92C146D4F38B5C92E17 /* MSIDThrottlingMetaDataReading.h in Headers */, + B4F104C02FE3A16500EBEB5F /* MSIDUXCallbackProtocol.h in Headers */, + B4F104C12FE3A16500EBEB5F /* MSIDUXCallbackProvider.h in Headers */, + 72DB19C630070A08008AE594 /* MSIDDeviceTokenUtil.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6509,6 +6588,7 @@ 2306D2B520AD05A700F875A3 /* MSIDURLSessionDelegate.h in Headers */, 1E2EDFF9219125400054FAD9 /* MSIDTokenResponse+Internal.h in Headers */, 238E19E02086FE28004DF483 /* MSIDAADRefreshTokenGrantRequest.h in Headers */, + 72DB19C530070A08008AE594 /* MSIDDeviceTokenUtil.h in Headers */, 2318D7842E11664700A5A46E /* MSIDBrokerOperationBrowserNativeMessageMATSReport.h in Headers */, B286B9A62389DD1E007833AD /* MSIDSystemWebviewController.h in Headers */, B286B9AF2389DD63007833AD /* MSIDClientTLSHandler.h in Headers */, @@ -6705,6 +6785,7 @@ 5887EBF12BBF6490005F9634 /* MSIDAuthenticationSchemeSshCert.h in Headers */, B2AF1D2E218BCEDE0080C1A0 /* MSIDInteractiveTokenRequest.h in Headers */, 2A2481592CB08050006FCB34 /* MSIDXpcSingleSignOnProvider.h in Headers */, + CA0FEA110000000000000001 /* MSIDXpcCanPerformFailureReason.h in Headers */, 1E707FE22407337300716148 /* MSIDBrokerOperationResponse.h in Headers */, B251CC48204105A7005E0179 /* MSIDBaseToken.h in Headers */, 1E62D0E6228B760A000E2BBC /* MSIDKeychainUtil.h in Headers */, @@ -6808,6 +6889,7 @@ B297E1EB20A1388E00F370EC /* MSIDDefaultAccountCacheQuery.h in Headers */, B2C0748B246B71300008D701 /* MSIDAssymetricKeyPairWithCert.h in Headers */, B286B9562385F01A007833AD /* MSIDOIDCSignoutRequest.h in Headers */, + B4134C1A2FEA3E410037FE68 /* MSIDMobileOnboardingState.h in Headers */, B2C708B0219A614C00D917B8 /* MSIDDefaultBrokerTokenRequest.h in Headers */, B286B9D52389DF2E007833AD /* MSIDRegistrationInformation.h in Headers */, A0C7DD7C25D1E98D00F5B5B6 /* NSError+MSIDThrottlingExtension.h in Headers */, @@ -6827,6 +6909,8 @@ B5AAE11B2F03D7AA0026B21B /* MSIDBrokerOperationGetDefaultAccountResponse.h in Headers */, B26CEAE723653C62009E6E54 /* MSIDASWebAuthenticationSessionHandler.h in Headers */, 2338ECCE208A675D00809B9E /* MSIDAADRequestErrorHandler.h in Headers */, + B4F104C32FE3A16500EBEB5F /* MSIDUXCallbackProtocol.h in Headers */, + B4F104C42FE3A16500EBEB5F /* MSIDUXCallbackProvider.h in Headers */, B26A0B8C2071B763006BD95A /* MSIDAADV1Oauth2Factory.h in Headers */, B2C708B4219A620E00D917B8 /* MSIDBrokerCryptoProvider.h in Headers */, B2CDB5791FE33A46003A4B5C /* MSIDAccount.h in Headers */, @@ -6849,6 +6933,7 @@ B443F0002AD6327700782168 /* MSIDBrokerOperationPasskeyCredentialRequest.h in Headers */, 23AE20982342D3BF00108F76 /* MSIDSilentController+Internal.h in Headers */, 72C1EBF52DE91AC8004C40A4 /* MSIDBoundRefreshToken.h in Headers */, + BA000000000000000000C001 /* MSIDBoundTokenProvider.h in Headers */, 23B39A8620993572000AA905 /* MSIDAADAuthorityMetadataRequest.h in Headers */, B28D90AA218FD1F800E230D6 /* MSIDDefaultTokenResponseValidator.h in Headers */, B2F671E82467A34400649855 /* MSIDAuthorizationCodeResult.h in Headers */, @@ -6968,9 +7053,12 @@ D6D9A4561FBD40BF00EFA430 /* NSURL+MSIDTestUtil.h in Headers */, B217861823A57ED800839CE8 /* MSIDAuthorizationControllerMock.h in Headers */, B2E4A07B24DDE5D7007CE642 /* NSUUID+MSIDTestUtil.h in Headers */, + B42DA4863008B0E80098AE41 /* MSIDOnboardingBlobBuilder+MSIDTestUtil.h in Headers */, B217862923A5839300839CE8 /* MSIDSSOExtensionSignoutRequestMock.h in Headers */, + B4134C442FEC495C0037FE68 /* MSIDMockUXCallbackProvider.h in Headers */, 2A0278A32D6E3787005655B4 /* MSIDLastRequestTelemetry+Tests.h in Headers */, 969CCB5622A9EB0300A55515 /* MSIDTestCacheDataSource.h in Headers */, + 7222DA3B2FFEE4020076ED4F /* MSIDDeviceTokenGrantRequestMock.h in Headers */, B28AC66421A0BB9D00A1FC4A /* MSIDTestBrokerResponseHelper.h in Headers */, B23ECF031FF30BB90015FC1D /* MSIDTestIdentifiers.h in Headers */, D626FFF61FBD200A00EE4487 /* MSIDTestURLResponse.h in Headers */, @@ -7595,6 +7683,7 @@ B286B9F12389F866007833AD /* MSIDWebviewFactoryTests.m in Sources */, B252913B2096698100E78695 /* MSIDAADIdTokenClaimsFactoryTests.m in Sources */, 72C764FA2E09CFB800043AB1 /* MSIDBoundRefreshTokenTests.m in Sources */, + BA000000000000000000D101 /* MSIDBoundTokenProviderTests.m in Sources */, B2BE923121A0EFB100F5AB8C /* MSIDDefaultTokenRequestProviderTests.m in Sources */, 729357F42DDBD3F80001D03C /* MSIDNonceTokenRequestTest.m in Sources */, 23FB5C20225516FB002BF1EB /* MSIDClaimsRequestTests.m in Sources */, @@ -7692,6 +7781,7 @@ 2A294C2E2F31372E0042AEA0 /* MSIDExecutionFlowTagTests.m in Sources */, 963553BF20CA7C52005235E5 /* MSIDSystemWebviewControllerTests.m in Sources */, 963CFAF320AD817600BDA25F /* MSIDWebviewAuthorizationTests.m in Sources */, + 72DB19CC30072079008AE594 /* MSIDDeviceTokenUtilTests.m in Sources */, B2525C752330623E006FBA4B /* MSIDMainThreadUtilTests.m in Sources */, 234A0BE32BCDCCB100AFBBAA /* MSIDBrowserNativeMessageSignOutRequestTests.m in Sources */, 2338ECDA208A7CBD00809B9E /* MSIDAADRequestErrorHandlerTests.m in Sources */, @@ -7724,6 +7814,8 @@ B281B338226BBB1C009619AB /* MSIDOAuthRequestConfiguratorTests.m in Sources */, A08D0A4824A8841400C9193D /* MSIDAuthenticationSchemeTest.m in Sources */, 3AA5A4B540B84C95881A3197 /* MSIDDeviceTokenGrantRequestTests.m in Sources */, + FE01A1B2C3D4E5F600000002 /* MSIDDeviceTokenUtilTests.m in Sources */, + FADE01000000000000000002 /* MSIDDeviceTokenGrantRequestNetworkTests.m in Sources */, 3A7F0C6025AB453BB48081FB /* MSIDDeviceTokenResponseHandlerTests.m in Sources */, B2808001204CB29900944D89 /* MSIDAADTokenResponseTests.m in Sources */, 4BADFA5F2E85D7FC00E8C26F /* MSIDFlightManagerTests.swift in Sources */, @@ -7775,6 +7867,7 @@ 23C10AA02B40D9350063D97C /* MSIDBrowserNativeMessageSignOutResponse.m in Sources */, 7209A3D62EB581BE0050CB13 /* MSIDJweResponse.m in Sources */, B251CC3A2041058D005E0179 /* MSIDLegacySingleResourceToken.m in Sources */, + B4134C1B2FEA3E410037FE68 /* MSIDMobileOnboardingState.m in Sources */, 233E96F822652D3A007FCE2A /* MSIDAggregatedDispatcher.m in Sources */, B2A3C2812145D04E0082525C /* MSIDAuthorityCacheRecord.m in Sources */, B2C708B2219A620700D917B8 /* MSIDBrokerKeyProvider.m in Sources */, @@ -7848,6 +7941,7 @@ B286B97F2389DC08007833AD /* MSIDBrokerOperationRequest.m in Sources */, 23FB5C3122551866002BF1EB /* MSIDClaimsRequest+ClientCapabilities.m in Sources */, 72C1EBF72DE91AD0004C40A4 /* MSIDBoundRefreshToken.m in Sources */, + BA000000000000000000C002 /* MSIDBoundTokenProvider.m in Sources */, 23B39ACD209CF317000AA905 /* MSIDAADNetworkConfiguration.m in Sources */, B5AAE11A2F03D7AA0026B21B /* MSIDBrokerOperationGetDefaultAccountResponse.m in Sources */, 23FB5C462255A135002BF1EB /* MSIDIndividualClaimRequest.m in Sources */, @@ -7953,6 +8047,7 @@ D6D9A4521FBD3FB800EFA430 /* NSURL+MSIDExtensions.m in Sources */, B23ECEF11FF2F6270015FC1D /* MSIDAADV2IdTokenClaims.m in Sources */, 239EED6A242D8FAD00162F0F /* MSIDAADTokenRequestServerTelemetry.m in Sources */, + B4F104C22FE3A16500EBEB5F /* MSIDUXCallbackProvider.m in Sources */, B2C0748D246B71300008D701 /* MSIDAssymetricKeyPairWithCert.m in Sources */, B4B591B42F3AC90600CBA6A9 /* MSIDOnboardingStatusCache.m in Sources */, 9641B5251FCF3EEF00AFA0EC /* MSIDMacTokenCache.m in Sources */, @@ -7984,6 +8079,7 @@ B253154B23DE31F400432133 /* MSIDBrokerOperationGetDeviceInfoRequest.m in Sources */, B2A3C2802145D02E0082525C /* MSIDAadAuthorityCacheRecord.m in Sources */, B28BDA81217E964B003E5670 /* MSIDB2CTokenResponse.m in Sources */, + 72DB19C830070A11008AE594 /* MSIDDeviceTokenUtil.m in Sources */, A057458425A789DC0098E469 /* MSIDThrottlingService.m in Sources */, 9641B52B1FCF3F3A00AFA0EC /* MSIDKeyedArchiverSerializer.m in Sources */, B2FF0831245E4C89001C7F3B /* MSIDWorkplaceJoinChallenge.m in Sources */, @@ -8041,7 +8137,6 @@ B286B98D2389DC34007833AD /* MSIDBrokerOperationTokenResponse.m in Sources */, 2394F1FA2D4890BD00E44F6E /* MSIDWebOAuth2AuthCodeOperation.m in Sources */, B4CC96612F982FEA007F281A /* MSIDSessionCachePersistence.m in Sources */, - 724C9E322E6FAB170039BAA0 /* MSIDConcatKdfProvider.swift in Sources */, B286B9832389DC15007833AD /* MSIDBrokerOperationInteractiveTokenRequest.m in Sources */, 1EE42FF1248825CE00899491 /* MSIDAccessTokenWithAuthScheme.m in Sources */, 606830062098ACED00CCA6AB /* MSIDNegotiateHandler.m in Sources */, @@ -8210,6 +8305,7 @@ buildActionMask = 2147483647; files = ( 6E4F659424D48B6D0070CA36 /* MSIDSymmetricKeyTests.m in Sources */, + 72DB19CD30072079008AE594 /* MSIDDeviceTokenUtilTests.m in Sources */, B29A36B620AFA03200427B63 /* MSIDOauth2FactoryTests.m in Sources */, 23CC944920465CEC00AA0551 /* MSIDTokenCacheDataSourceIntegrationTests.m in Sources */, B86FA7D62383757A00E5195A /* MSIDMacKeychainTokenCacheTests.m in Sources */, @@ -8341,6 +8437,7 @@ B2BE923521A0F80100F5AB8C /* MSIDLegacyTokenRequestProviderTests.m in Sources */, B48FC0632D7A90FA007B80DB /* MSIDBrokerFlightProviderTests.m in Sources */, 72C764F92E09CFB800043AB1 /* MSIDBoundRefreshTokenTests.m in Sources */, + BA000000000000000000D102 /* MSIDBoundTokenProviderTests.m in Sources */, 23FB5C21225516FB002BF1EB /* MSIDClaimsRequestTests.m in Sources */, E75DD02625D5E474007664A6 /* MSIDThrottlingServiceIntegrationTests.m in Sources */, B286BA07238A110A007833AD /* MSIDOIDCSignoutRequestTests.m in Sources */, @@ -8394,6 +8491,8 @@ 236B7C2122B1DE3C00A6EFBB /* MSIDExternalAADCacheSeederIntegrationTests.m in Sources */, A08D0A4924A8841400C9193D /* MSIDAuthenticationSchemeTest.m in Sources */, 43F7552B93054DDE99785519 /* MSIDDeviceTokenGrantRequestTests.m in Sources */, + FE01A1B2C3D4E5F600000003 /* MSIDDeviceTokenUtilTests.m in Sources */, + FADE01000000000000000003 /* MSIDDeviceTokenGrantRequestNetworkTests.m in Sources */, 6F37128C10B64C978F1D19D8 /* MSIDDeviceTokenResponseHandlerTests.m in Sources */, B2DD5B9F204761550084313F /* MSIDAccessTokenTests.m in Sources */, B2DD5BC120479AA80084313F /* MSIDCacheItemJsonSerializerTests.m in Sources */, @@ -8449,12 +8548,14 @@ 964E669720AE97FD00857009 /* MSIDTestWebviewInteractingViewController.m in Sources */, B2E4A06D24DDE559007CE642 /* MSIDTestContext.m in Sources */, B28AC66521A0BB9D00A1FC4A /* MSIDTestBrokerResponseHelper.m in Sources */, + 7222DA3E2FFEE42D0076ED4F /* MSIDDeviceTokenGrantRequestMock.m in Sources */, B2E4A07324DDE575007CE642 /* MSIDTestTelemetryEventsObserver.m in Sources */, B217862A23A5839300839CE8 /* MSIDSSOExtensionSignoutRequestMock.m in Sources */, D6D9A4571FBD40BF00EFA430 /* NSURL+MSIDTestUtil.m in Sources */, 23D4DEE82D92537D005A77E4 /* MSIDFlightManagerMockProvider.m in Sources */, 589842B9252544940075DFED /* MSIDAccountMetadataCacheMockUpdateAuthorityParameters.m in Sources */, D626FFF11FBD200A00EE4487 /* MSIDTestURLResponse.m in Sources */, + B42DA4843008B0E80098AE41 /* MSIDOnboardingBlobBuilder+MSIDTestUtil.m in Sources */, 963E68E721489A9500D7D0CC /* NSString+MSIDTestUtil.m in Sources */, 23185368206D8B1D0024DCA4 /* MSIDTestTokenResponse.m in Sources */, 961ACDFD22A1F60800B9266C /* NSData+MSIDTestUtil.m in Sources */, @@ -8477,6 +8578,7 @@ D626FFF71FBD200A00EE4487 /* MSIDTestURLSessionDataTask.m in Sources */, 6078EB50226DA97100235498 /* MSIDTestCacheUtil.m in Sources */, B2E2A94D239320B100BA2EA3 /* MSIDTestParametersProvider.m in Sources */, + B4134C462FEC495C0037FE68 /* MSIDMockUXCallbackProvider.m in Sources */, 961ACE0522A1FA8200B9266C /* MSIDApplicationTestUtil.m in Sources */, D626FFF41FBD200A00EE4487 /* MSIDTestURLSession.m in Sources */, D6D9A44D1FBD3EEA00EFA430 /* NSDictionary+MSIDTestUtil.m in Sources */, @@ -8508,10 +8610,12 @@ 23185366206D8B080024DCA4 /* MSIDTestConfiguration.m in Sources */, 23F32F261FFDAF1A00B2905E /* MSIDTestBrokerResponse.m in Sources */, B217862B23A5839300839CE8 /* MSIDSSOExtensionSignoutRequestMock.m in Sources */, + 7222DA3F2FFEE42D0076ED4F /* MSIDDeviceTokenGrantRequestMock.m in Sources */, D626FFF51FBD200A00EE4487 /* MSIDTestURLSession.m in Sources */, B216826223AB09C300F4897A /* MSIDSSOExtensionGetAccountsRequestMock.m in Sources */, B2E4A07424DDE576007CE642 /* MSIDTestTelemetryEventsObserver.m in Sources */, 5898429F252544900075DFED /* MSIDAccountMetadataCacheMockGetAuthorityParameters.m in Sources */, + B4134C452FEC495C0037FE68 /* MSIDMockUXCallbackProvider.m in Sources */, B2E2A94E239320B100BA2EA3 /* MSIDTestParametersProvider.m in Sources */, 969CCB5822A9EB7D00A55515 /* MSIDTestCacheDataSource.m in Sources */, 96290E5721489BB800FDD5C8 /* NSString+MSIDTestUtil.m in Sources */, @@ -8531,6 +8635,7 @@ B2BE923D21A0FD2B00F5AB8C /* MSIDTestSwizzle.m in Sources */, B2BE925521A24B8200F5AB8C /* MSIDTestTokenRequestProvider.m in Sources */, B2E4A07924DDE5D4007CE642 /* NSUUID+MSIDTestUtil.m in Sources */, + B42DA4853008B0E80098AE41 /* MSIDOnboardingBlobBuilder+MSIDTestUtil.m in Sources */, B253154723DD763E00432133 /* MSIDSSOExtensionGetDeviceInfoRequestMock.m in Sources */, B217861923A57EDB00839CE8 /* MSIDAuthorizationControllerMock.m in Sources */, B2E4A07624DDE5CD007CE642 /* NSDate+MSIDTestUtil.m in Sources */, @@ -8594,6 +8699,7 @@ 72C1EBFE2DEA81A1004C40A4 /* MSIDBoundRefreshTokenCacheItem.m in Sources */, A0C7DDA425D1EA0D00F5B5B6 /* NSError+MSIDThrottlingExtension.m in Sources */, 235480C720DDF81000246F72 /* MSIDAuthorityFactory.m in Sources */, + B4F104BF2FE3A16500EBEB5F /* MSIDUXCallbackProvider.m in Sources */, 239DF9C920E05847002D428B /* MSIDAADRequestConfigurator.m in Sources */, E733EDFD25C0A4B100ACB79A /* MSIDThumbprintCalculator.m in Sources */, 23B39A8220993302000AA905 /* MSIDAadAuthorityResolver.m in Sources */, @@ -8837,6 +8943,7 @@ 72978AF32E4C2C3500DEA46D /* MSIDBoundRefreshTokenRedemptionParameters.m in Sources */, B2C708182195283500D917B8 /* MSIDBrokerTokenRequest.m in Sources */, 72C1EBF82DE91AD0004C40A4 /* MSIDBoundRefreshToken.m in Sources */, + BA000000000000000000C003 /* MSIDBoundTokenProvider.m in Sources */, 232173E22182A998009852C6 /* NSDictionary+MSIDJsonSerializable.m in Sources */, B2C707F42192524700D917B8 /* MSIDDefaultTokenRequestProvider.m in Sources */, 724C9E332E6FAB170039BAA0 /* MSIDConcatKdfProvider.swift in Sources */, @@ -8929,12 +9036,14 @@ 60F7BE8B21DA4E2900F1BBA1 /* MSIDPrimaryRefreshToken.m in Sources */, B214C39F1FE854FE0070C4F2 /* MSIDLegacyTokenCacheAccessor.m in Sources */, D62600161FBD380500EE4487 /* NSDictionary+MSIDExtensions.m in Sources */, + B4134C1D2FEA3E410037FE68 /* MSIDMobileOnboardingState.m in Sources */, B2B1D579204369D600DD81F0 /* MSIDAccountType.m in Sources */, B2C707EE21924C8300D917B8 /* MSIDTokenResponseValidator.m in Sources */, 609E74C7228DCEA1005E3FED /* MSIDAccountMetadata.m in Sources */, 23D744792097B2DA00210C51 /* MSIDAADV1AuthorizationCodeRequest.m in Sources */, B41F0CD82F871F320029E631 /* MSIDWebMDMEnrollmentCompletionResponse.m in Sources */, B2AF1D34218BCEEB0080C1A0 /* MSIDSilentTokenRequest.m in Sources */, + 72DB19C930070A11008AE594 /* MSIDDeviceTokenUtil.m in Sources */, 230847A72082C5830024CE7C /* MSIDHttpRequest.m in Sources */, B20657C91FC926B200412B7D /* MSIDTelemetryHttpEvent.m in Sources */, B2C7089321991CED00D917B8 /* MSIDAADV1BrokerResponse.m in Sources */, diff --git a/IdentityCore/src/MSIDConstants.h b/IdentityCore/src/MSIDConstants.h index 8000c64bb7..9d7bd75d87 100644 --- a/IdentityCore/src/MSIDConstants.h +++ b/IdentityCore/src/MSIDConstants.h @@ -125,12 +125,15 @@ extern NSString * _Nonnull const MSID_PLATFORM_KEY;//The SDK platform. iOS or OS extern NSString * _Nonnull const MSID_SOURCE_PLATFORM_KEY;//The source SDK platform. iOS or OSX extern NSString * _Nonnull const MSID_PLATFORM_SEQUENCE_KEY; extern NSString * _Nonnull const MSID_VERSION_KEY; +extern NSString * _Nonnull const MSID_BROKER_VER_KEY;//x-client-brkrver (broker only) extern NSString * _Nonnull const MSID_CPU_KEY;//E.g. ARM64 extern NSString * _Nonnull const MSID_OS_VER_KEY;//iOS/OSX version extern NSString * _Nonnull const MSID_DEVICE_MODEL_KEY;//E.g. iPhone 5S extern NSString * _Nonnull const MSID_APP_NAME_KEY; extern NSString * _Nonnull const MSID_APP_VER_KEY; +extern NSString * _Nonnull const MSID_APP_CLIENT_ID_KEY;//x-ms-client-id (source app OAuth client ID, broker only) extern NSString * _Nonnull const MSID_CCS_HINT_KEY; + extern NSString * _Nonnull const MSID_WEBAUTH_IGNORE_SSO_KEY; extern NSString * _Nonnull const MSID_WEBAUTH_REFRESH_TOKEN_KEY; extern NSString * _Nonnull const MSID_USER_FEDERATED_IDENTITY_CREDENTIAL_KEY; @@ -291,4 +294,11 @@ extern NSString * _Nonnull const MSID_FLIGHT_DISABLE_OPEN_NEW_WINDOW_IN_BROWSER; /// Default: OFF extern NSString * _Nonnull const MSID_FLIGHT_DISABLE_MOBILE_ONBOARDING; +/// Flight key for MDM profile install notification delay (seconds). +/// Owner: swagup +extern NSString * _Nonnull const MSID_FLIGHT_MDM_PROFILE_INSTALLED_NOTIFICATION_DELAY; + +/// Default delay (in seconds) before the MDM profile install notification fires. +extern NSTimeInterval const MSIDMDMProfileInstalledNotificationDefaultDelay; + #define METHODANDLINE [NSString stringWithFormat:@"%s [Line %d]", __PRETTY_FUNCTION__, __LINE__] diff --git a/IdentityCore/src/MSIDConstants.m b/IdentityCore/src/MSIDConstants.m index cfb4325dc9..c0874dec07 100644 --- a/IdentityCore/src/MSIDConstants.m +++ b/IdentityCore/src/MSIDConstants.m @@ -27,11 +27,13 @@ NSString *const MSID_SOURCE_PLATFORM_KEY = @"x-client-src-SKU"; NSString *const MSID_PLATFORM_SEQUENCE_KEY = @"x-client-xtra-sku"; NSString *const MSID_VERSION_KEY = @"x-client-Ver"; +NSString *const MSID_BROKER_VER_KEY = @"x-client-brkrver"; NSString *const MSID_CPU_KEY = @"x-client-CPU"; NSString *const MSID_OS_VER_KEY = @"x-client-OS"; NSString *const MSID_DEVICE_MODEL_KEY = @"x-client-DM"; NSString *const MSID_APP_NAME_KEY = @"x-app-name"; NSString *const MSID_APP_VER_KEY = @"x-app-ver"; +NSString *const MSID_APP_CLIENT_ID_KEY = @"x-ms-client-id"; NSString *const MSID_CCS_HINT_KEY = @"X-AnchorMailbox"; NSString *const MSID_WEBAUTH_IGNORE_SSO_KEY = @"x-ms-sso-Ignore-SSO"; NSString *const MSID_WEBAUTH_REFRESH_TOKEN_KEY = @"x-ms-sso-RefreshToken"; @@ -133,4 +135,8 @@ NSString *const MSID_FLIGHT_DISABLE_MOBILE_ONBOARDING = @"disable_mobile_onboarding"; +NSString *const MSID_FLIGHT_MDM_PROFILE_INSTALLED_NOTIFICATION_DELAY = @"mdm_profile_installed_notification_delay"; + +NSTimeInterval const MSIDMDMProfileInstalledNotificationDefaultDelay = 75.0; + #define METHODANDLINE [NSString stringWithFormat:@"%s [Line %d]", __PRETTY_FUNCTION__, __LINE__] diff --git a/IdentityCore/src/MSIDFlightManager.m b/IdentityCore/src/MSIDFlightManager.m index 425a2325dc..e2f7e40a13 100644 --- a/IdentityCore/src/MSIDFlightManager.m +++ b/IdentityCore/src/MSIDFlightManager.m @@ -34,6 +34,8 @@ @interface MSIDFlightManager() @implementation MSIDFlightManager +@synthesize flightProvider = _flightProvider; + + (instancetype)sharedInstance { static MSIDFlightManager *sharedInstance = nil; @@ -113,35 +115,56 @@ - (dispatch_queue_t)initializeDispatchQueue - (void)setFlightProvider:(id)flightProvider { - dispatch_barrier_async(self.synchronizationQueue, ^{ + dispatch_barrier_sync(self.synchronizationQueue, ^{ self->_flightProvider = flightProvider; }); } +- (id)flightProvider +{ + __block id flightProvider = nil; + // Read on the synchronization queue so external callers are serialized against the + // barrier write in setFlightProvider:, matching the internal readers below. Capturing + // into a strong local keeps the provider alive for the duration of the call. + dispatch_sync(self.synchronizationQueue, ^{ + flightProvider = self->_flightProvider; + }); + + return flightProvider; +} + #pragma mark - MSIDFlightManagerInterface -- (BOOL)boolForKey:(nonnull NSString *)flightKey +- (BOOL)boolForKey:(nonnull NSString *)flightKey { __block BOOL result = NO; - if (self.flightProvider) - { - dispatch_sync(self.synchronizationQueue, ^{ - result = [self.flightProvider boolForKey:flightKey]; - }); - } + // Read the provider only from within the synchronization queue. Testing it via the + // unsynchronized property getter first would race the barrier write in setFlightProvider:, + // and capturing it into a strong local keeps it alive for the duration of the call. + dispatch_sync(self.synchronizationQueue, ^{ + id flightProvider = self->_flightProvider; + if (flightProvider) + { + result = [flightProvider boolForKey:flightKey]; + } + }); return result; } - (nullable NSString *)stringForKey:(nonnull NSString *)flightKey { - __block NSString* result = nil; - if (self.flightProvider) - { - dispatch_sync(self.synchronizationQueue, ^{ - result = [self.flightProvider stringForKey:flightKey]; - }); - } + __block NSString *result = nil; + // Read the provider only from within the synchronization queue. Testing it via the + // unsynchronized property getter first would race the barrier write in setFlightProvider:, + // and capturing it into a strong local keeps it alive for the duration of the call. + dispatch_sync(self.synchronizationQueue, ^{ + id flightProvider = self->_flightProvider; + if (flightProvider) + { + result = [flightProvider stringForKey:flightKey]; + } + }); return result; } diff --git a/IdentityCore/src/MSIDOAuth2Constants.h b/IdentityCore/src/MSIDOAuth2Constants.h index 027accb5f4..b350248ad1 100644 --- a/IdentityCore/src/MSIDOAuth2Constants.h +++ b/IdentityCore/src/MSIDOAuth2Constants.h @@ -60,6 +60,7 @@ extern NSString *const MSID_OAUTH2_CORRELATION_ID_REQUEST; extern NSString *const MSID_OAUTH2_CORRELATION_ID_REQUEST_VALUE; extern NSString *const MSID_OAUTH2_SAML11_BEARER_VALUE; extern NSString *const MSID_OAUTH2_SAML2_BEARER_VALUE; +extern NSString *const MSID_OAUTH2_JWT_BEARER_VALUE; extern NSString *const MSID_OAUTH2_SCOPE_OPENID_VALUE; extern NSString *const MSID_OAUTH2_SCOPE_OFFLINE_ACCESS_VALUE; extern NSString *const MSID_OAUTH2_SCOPE_PROFILE_VALUE; diff --git a/IdentityCore/src/MSIDOAuth2Constants.m b/IdentityCore/src/MSIDOAuth2Constants.m index 9d104f2279..f02fc3bac8 100644 --- a/IdentityCore/src/MSIDOAuth2Constants.m +++ b/IdentityCore/src/MSIDOAuth2Constants.m @@ -61,6 +61,7 @@ NSString *const MSID_OAUTH2_ASSERTION = @"assertion"; NSString *const MSID_OAUTH2_SAML11_BEARER_VALUE = @"urn:ietf:params:oauth:grant-type:saml1_1-bearer"; NSString *const MSID_OAUTH2_SAML2_BEARER_VALUE = @"urn:ietf:params:oauth:grant-type:saml2-bearer"; +NSString *const MSID_OAUTH2_JWT_BEARER_VALUE = @"urn:ietf:params:oauth:grant-type:jwt-bearer"; NSString *const MSID_OAUTH2_SCOPE_OPENID_VALUE = @"openid"; NSString *const MSID_OAUTH2_SCOPE_PROFILE_VALUE = @"profile"; NSString *const MSID_OAUTH2_SCOPE_EMAIL_VALUE = @"email"; diff --git a/IdentityCore/src/MSIDUXCallbackProtocol.h b/IdentityCore/src/MSIDUXCallbackProtocol.h new file mode 100644 index 0000000000..ec3745b243 --- /dev/null +++ b/IdentityCore/src/MSIDUXCallbackProtocol.h @@ -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 + +NS_ASSUME_NONNULL_BEGIN + +@protocol MSIDUXCallbackProtocol + +/// Called when the webview loads a profile install URL during MDM onboarding. +/// The host app should schedule a local notification after the given delay. +- (void)scheduleMDMProfileInstalledNotificationWithDelay:(NSTimeInterval)delay; + +/// Called when enrollment completes successfully. The host app should cancel +/// any previously scheduled MDM profile installed notification. +- (void)cancelMDMProfileInstalledNotification; + +@end + +NS_ASSUME_NONNULL_END diff --git a/IdentityCore/src/MSIDUXCallbackProvider.h b/IdentityCore/src/MSIDUXCallbackProvider.h new file mode 100644 index 0000000000..3ab4b34a8e --- /dev/null +++ b/IdentityCore/src/MSIDUXCallbackProvider.h @@ -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 +#import "MSIDUXCallbackProtocol.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MSIDUXCallbackProvider : NSObject + +@property (nonatomic, class, nullable) id uxCallbackProvider; + +@end + +NS_ASSUME_NONNULL_END diff --git a/IdentityCore/src/MSIDUXCallbackProvider.m b/IdentityCore/src/MSIDUXCallbackProvider.m new file mode 100644 index 0000000000..807534053c --- /dev/null +++ b/IdentityCore/src/MSIDUXCallbackProvider.m @@ -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 "MSIDUXCallbackProvider.h" + +static id s_uxCallbackProvider; + +@implementation MSIDUXCallbackProvider + ++ (id)uxCallbackProvider +{ + return s_uxCallbackProvider; +} + ++ (void)setUxCallbackProvider:(id)uxCallbackProvider +{ + s_uxCallbackProvider = uxCallbackProvider; +} + +@end diff --git a/IdentityCore/src/broker_operation/request/interactive_token_request/MSIDBrokerOperationInteractiveTokenRequest.h b/IdentityCore/src/broker_operation/request/interactive_token_request/MSIDBrokerOperationInteractiveTokenRequest.h index 3a8ad7c178..2034e643ee 100644 --- a/IdentityCore/src/broker_operation/request/interactive_token_request/MSIDBrokerOperationInteractiveTokenRequest.h +++ b/IdentityCore/src/broker_operation/request/interactive_token_request/MSIDBrokerOperationInteractiveTokenRequest.h @@ -43,9 +43,7 @@ NS_ASSUME_NONNULL_BEGIN /// blob via `MSIDBrokerOperationTokenResponse.onboardingBlob`. @property (nonatomic, copy, nullable) NSString *onboardingBlob; -/// Mirrors `MSIDInteractiveRequestParameters.isNewMobileOnboardingFlow`. -/// Round-tripped across the SSO extension IPC boundary so the broker can -/// branch on whether the request originated from the new mobile onboarding flow. +// Indicates the new mobile onboarding flow. Serialized across SSO extension IPC. @property (nonatomic) BOOL isNewMobileOnboardingFlow; + (instancetype)tokenRequestWithParameters:(MSIDInteractiveTokenRequestParameters *)parameters diff --git a/IdentityCore/src/controllers/MSIDLocalInteractiveController.m b/IdentityCore/src/controllers/MSIDLocalInteractiveController.m index 6f5bac3c40..51bec02751 100644 --- a/IdentityCore/src/controllers/MSIDLocalInteractiveController.m +++ b/IdentityCore/src/controllers/MSIDLocalInteractiveController.m @@ -24,6 +24,7 @@ #import "MSIDLocalInteractiveController+Internal.h" #import "MSIDInteractiveTokenRequest+Internal.h" #import "MSIDInteractiveTokenRequestParameters.h" +#import "MSIDWebviewConstants.h" #import "MSIDAccountIdentifier.h" #import "MSIDTelemetry+Internal.h" #import "MSIDTelemetryAPIEvent.h" @@ -285,7 +286,7 @@ - (void)handleWebMDMEnrollmentCompletionResponse:(MSIDWebMDMEnrollmentCompletion @"Passed nil completionBlock to handleWebMDMEnrollmentCompletionResponse."); return; } - + NSString *status = mdmEnrollmentCompletionResponse.status ?: @""; // Failure path: MDM enrollment did not complete. @@ -308,7 +309,8 @@ - (void)handleWebMDMEnrollmentCompletionResponse:(MSIDWebMDMEnrollmentCompletion return; } - // MDM enrollment complete. Retry the token request through the appropriate controller. + // MDM enrollment complete. + // Retry the token request through the appropriate controller. // If broker is installed and SSO extension is active, the factory returns the SSO controller. MSID_LOG_WITH_CTX(MSIDLogLevelInfo, self.requestParameters, @"MDM enrollment complete (status=%@); retrying token request.", status); @@ -358,11 +360,11 @@ - (void)handleSpecialRedirectURL:(NSURL *)URL completion:completion]; } -- (BOOL)processResponseHeadersAndCheckForASWebAuthHandoff:(NSDictionary *)headers - responseURL:(NSURL *)responseURL +- (BOOL)processNavigationResponseAndCheckForASWebAuthHandoff:(NSHTTPURLResponse *)response + embeddedWebviewController:(nullable MSIDOAuth2EmbeddedWebviewController *)embeddedWebviewController { - return [self.navigationHandler processResponseHeadersAndCheckForASWebAuthHandoff:headers - responseURL:responseURL]; + return [self.navigationHandler processNavigationResponseAndCheckForASWebAuthHandoff:response + embeddedWebviewController:embeddedWebviewController]; } #if !MSID_EXCLUDE_SYSTEMWV diff --git a/IdentityCore/src/controllers/broker/mac/MSIDXpcInteractiveTokenRequestController.h b/IdentityCore/src/controllers/broker/mac/MSIDXpcInteractiveTokenRequestController.h index b82443743b..76d2aa7f5c 100644 --- a/IdentityCore/src/controllers/broker/mac/MSIDXpcInteractiveTokenRequestController.h +++ b/IdentityCore/src/controllers/broker/mac/MSIDXpcInteractiveTokenRequestController.h @@ -24,6 +24,7 @@ #import "MSIDLocalInteractiveController.h" +#import "MSIDXpcCanPerformFailureReason.h" NS_ASSUME_NONNULL_BEGIN @@ -36,6 +37,11 @@ NS_ASSUME_NONNULL_BEGIN + (BOOL)canPerformRequest; +// Same as canPerformRequest above, but additionally reports the specific reason for a NO result via the +// reason out-param, so external callers can surface it to their own telemetry. The reason is left +// untouched if the caller passes nil. ++ (BOOL)canPerformRequest:(MSIDXpcCanPerformFailureReason * _Nullable)reason; + @end NS_ASSUME_NONNULL_END diff --git a/IdentityCore/src/controllers/broker/mac/MSIDXpcInteractiveTokenRequestController.m b/IdentityCore/src/controllers/broker/mac/MSIDXpcInteractiveTokenRequestController.m index 0869299756..aa34f3ec9e 100644 --- a/IdentityCore/src/controllers/broker/mac/MSIDXpcInteractiveTokenRequestController.m +++ b/IdentityCore/src/controllers/broker/mac/MSIDXpcInteractiveTokenRequestController.m @@ -83,10 +83,30 @@ - (void)acquireToken:(MSIDRequestCompletionBlock)completionBlock } + (BOOL)canPerformRequest +{ + return [self canPerformRequest:nil]; +} + ++ (BOOL)canPerformRequest:(MSIDXpcCanPerformFailureReason *)reason { if (@available(macOS 13, *)) { - return [MSIDXpcSingleSignOnProvider canPerformRequest:MSIDXpcProviderCache.sharedInstance]; + MSIDXpcCanPerformFailureReason failureReason = MSIDXpcCanPerformFailureReasonNone; + BOOL canPerform = [MSIDXpcSingleSignOnProvider canPerformRequest:MSIDXpcProviderCache.sharedInstance + reason:&failureReason]; + if (reason) + { + *reason = failureReason; + } + if (!canPerform) + { + MSID_LOG_WITH_CTX(MSIDLogLevelInfo, nil, @"[MSIDXpcInteractiveTokenRequestController canPerformRequest] returned NO, reason: %@ (%ld)", MSIDXpcCanPerformFailureReasonToString(failureReason), (long)failureReason); + } + return canPerform; } else { + if (reason) + { + *reason = MSIDXpcCanPerformFailureReasonUnsupportedOSVersion; + } return NO; } } diff --git a/IdentityCore/src/controllers/broker/mac/MSIDXpcSilentTokenRequestController.h b/IdentityCore/src/controllers/broker/mac/MSIDXpcSilentTokenRequestController.h index 8bf5ec34fc..5887a6ab2f 100644 --- a/IdentityCore/src/controllers/broker/mac/MSIDXpcSilentTokenRequestController.h +++ b/IdentityCore/src/controllers/broker/mac/MSIDXpcSilentTokenRequestController.h @@ -24,6 +24,7 @@ #import "MSIDSilentController.h" +#import "MSIDXpcCanPerformFailureReason.h" NS_ASSUME_NONNULL_BEGIN @@ -31,6 +32,11 @@ NS_ASSUME_NONNULL_BEGIN + (BOOL)canPerformRequest; +// Same as canPerformRequest above, but additionally reports the specific reason for a NO result via the +// reason out-param, so external callers can surface it to their own telemetry. The reason is left +// untouched if the caller passes nil. ++ (BOOL)canPerformRequest:(MSIDXpcCanPerformFailureReason * _Nullable)reason; + @end NS_ASSUME_NONNULL_END diff --git a/IdentityCore/src/controllers/broker/mac/MSIDXpcSilentTokenRequestController.m b/IdentityCore/src/controllers/broker/mac/MSIDXpcSilentTokenRequestController.m index 97bb9fed71..1845913acc 100644 --- a/IdentityCore/src/controllers/broker/mac/MSIDXpcSilentTokenRequestController.m +++ b/IdentityCore/src/controllers/broker/mac/MSIDXpcSilentTokenRequestController.m @@ -45,13 +45,33 @@ - (void)acquireToken:(MSIDRequestCompletionBlock)completionBlock } + (BOOL)canPerformRequest +{ + return [self canPerformRequest:nil]; +} + ++ (BOOL)canPerformRequest:(MSIDXpcCanPerformFailureReason *)reason { if (@available(macOS 13, *)) { - return [MSIDXpcSingleSignOnProvider canPerformRequest:MSIDXpcProviderCache.sharedInstance]; + MSIDXpcCanPerformFailureReason failureReason = MSIDXpcCanPerformFailureReasonNone; + BOOL canPerform = [MSIDXpcSingleSignOnProvider canPerformRequest:MSIDXpcProviderCache.sharedInstance + reason:&failureReason]; + if (reason) + { + *reason = failureReason; + } + if (!canPerform) + { + MSID_LOG_WITH_CTX(MSIDLogLevelInfo, nil, @"[MSIDXpcSilentTokenRequestController canPerformRequest] returned NO, reason: %@ (%ld)", MSIDXpcCanPerformFailureReasonToString(failureReason), (long)failureReason); + } + return canPerform; } else { + if (reason) + { + *reason = MSIDXpcCanPerformFailureReasonUnsupportedOSVersion; + } return NO; } } diff --git a/IdentityCore/src/oauth2/token/MSIDBoundTokenProvider.h b/IdentityCore/src/oauth2/token/MSIDBoundTokenProvider.h new file mode 100644 index 0000000000..896483ba7d --- /dev/null +++ b/IdentityCore/src/oauth2/token/MSIDBoundTokenProvider.h @@ -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 +#import "MSIDRequestContext.h" + +@class MSIDBrowserNativeMessageGetTokenRequest; + +NS_ASSUME_NONNULL_BEGIN + +/// Completion block for a bound-token acquisition. +/// @param response Serialized browser-native-message response payload (JSON string) on success, otherwise nil. +/// @param error Populated when acquisition fails, otherwise nil. +typedef void (^MSIDBoundTokenProviderCompletionBlock)(NSString *_Nullable response, NSError *_Nullable error); + +/// Common Core orchestrator that services a browser-native-message GetToken request for a host such as +/// OneAuth (embedded in Edge). +/// +/// On unmanaged iOS the platform SSO Extension is unavailable, so the host cannot silently invoke the +/// broker through `ASAuthorizationSingleSignOnProvider`. Instead the host hands the GetToken request to +/// this provider, which owns the orchestration that would otherwise live behind the SSO Extension: +/// - transforms `MSIDBrowserNativeMessageGetTokenRequest` into the parameters used across Common Core, +/// - decides whether to service the request silently or interactively, +/// - silent path: redeems a cached BART SPA against ESTS in-process (no broker flip), +/// - interactive path: flips to the broker (Authenticator) via URL scheme to mint the initial token. +@interface MSIDBoundTokenProvider : NSObject + +/// Acquire a bound token for the supplied browser-native-message GetToken request. +/// @param request The GetToken request constructed by the host (e.g. OneAuth). +/// @param context Optional request context used for correlation and logging. +/// @param completionBlock Invoked with the serialized response payload or an error. +- (void)acquireBoundTokenWithRequest:(MSIDBrowserNativeMessageGetTokenRequest *)request + context:(nullable id)context + completionBlock:(MSIDBoundTokenProviderCompletionBlock)completionBlock; + +@end + +NS_ASSUME_NONNULL_END diff --git a/IdentityCore/src/oauth2/token/MSIDBoundTokenProvider.m b/IdentityCore/src/oauth2/token/MSIDBoundTokenProvider.m new file mode 100644 index 0000000000..8a160c0e58 --- /dev/null +++ b/IdentityCore/src/oauth2/token/MSIDBoundTokenProvider.m @@ -0,0 +1,113 @@ +// +// 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 "MSIDBoundTokenProvider.h" +#import "MSIDBrowserNativeMessageGetTokenRequest.h" +#import "MSIDError.h" +#import "MSIDLogger+Internal.h" +#import "NSString+MSIDExtensions.h" + +NSString *const MSID_BOUND_TOKEN_PROVIDER_LOG_PREFIX = @"[MSIDBoundTokenProvider]"; + +@implementation MSIDBoundTokenProvider + +- (void)acquireBoundTokenWithRequest:(MSIDBrowserNativeMessageGetTokenRequest *)request + context:(nullable id)context + completionBlock:(MSIDBoundTokenProviderCompletionBlock)completionBlock +{ + NSParameterAssert(completionBlock); + if (!completionBlock) return; + + if (![self validateRequest:request context:context completionBlock:completionBlock]) + { + return; + } + + MSID_LOG_WITH_CTX(MSIDLogLevelInfo, context, + @"%@ Servicing GetToken request in-process (no SSO extension). clientId: %@", + MSID_BOUND_TOKEN_PROVIDER_LOG_PREFIX, request.clientId); + + // Stub seam: the real silent-redemption / interactive-broker-flip orchestration is layered on top of + // this provider. Returns the serialized browser-native-message response payload. + NSString *responsePayload = [self stubResponsePayloadForRequest:request]; + + MSID_LOG_WITH_CTX(MSIDLogLevelInfo, context, + @"%@ In-process GetToken request completed.", MSID_BOUND_TOKEN_PROVIDER_LOG_PREFIX); + + completionBlock(responsePayload, nil); +} + +#pragma mark - Private + +- (BOOL)validateRequest:(MSIDBrowserNativeMessageGetTokenRequest *)request + context:(nullable id)context + completionBlock:(MSIDBoundTokenProviderCompletionBlock)completionBlock +{ + if (!request) + { + NSError *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, + @"A GetToken request is required.", nil, nil, nil, + context.correlationId, nil, NO); + completionBlock(nil, error); + return NO; + } + + if ([NSString msidIsStringNilOrBlank:request.clientId] || + [NSString msidIsStringNilOrBlank:request.redirectUri]) + { + NSError *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidDeveloperParameter, + @"clientId and redirectUri are required to acquire a bound token.", + nil, nil, nil, context.correlationId, nil, NO); + completionBlock(nil, error); + return NO; + } + + return YES; +} + +- (NSString *)stubResponsePayloadForRequest:(MSIDBrowserNativeMessageGetTokenRequest *)request +{ + NSMutableDictionary *payload = [NSMutableDictionary new]; + payload[@"clientId"] = request.clientId ?: @""; + payload[@"redirectUri"] = request.redirectUri ?: @""; + payload[@"scope"] = request.scopes ?: @""; + payload[@"servicedBy"] = @"MSIDBoundTokenProvider"; + payload[@"transport"] = @"in_proc_common_core"; + if (request.state) + { + payload[@"state"] = request.state; + } + + NSError *serializationError = nil; + NSData *data = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&serializationError]; + if (serializationError || !data) + { + MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"%@ Failed to serialize bound token payload: %@", MSID_BOUND_TOKEN_PROVIDER_LOG_PREFIX, serializationError); + return @"{}"; + } + + return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; +} + +@end diff --git a/IdentityCore/src/parameters/MSIDInteractiveRequestParameters.h b/IdentityCore/src/parameters/MSIDInteractiveRequestParameters.h index e621c843a1..7bddfd93fe 100644 --- a/IdentityCore/src/parameters/MSIDInteractiveRequestParameters.h +++ b/IdentityCore/src/parameters/MSIDInteractiveRequestParameters.h @@ -27,10 +27,13 @@ #import "MSIDConstants.h" @class WKWebView; +@class MSIDMobileOnboardingState; #if TARGET_OS_IPHONE @class UIViewController; #endif +NS_ASSUME_NONNULL_BEGIN + @interface MSIDInteractiveRequestParameters : MSIDRequestParameters @property (nonatomic) MSIDWebviewType webviewType; @@ -47,10 +50,11 @@ @property (nonatomic) BOOL prefersEphemeralWebBrowserSession; @property (nonatomic) NSString *telemetryWebviewType; -/* Marks the current request as part of the new mobile onboarding flow. - Set to YES when the server issues `msauth://enroll` during the embedded - webview leg, so the bit survives the hop into the broker SSO extension - where the broker can branch on it during device-registration bootstrap. */ +// Shared mutable onboarding state, passed by reference across recreated params. +@property (nonatomic, nullable) MSIDMobileOnboardingState *mobileOnboardingState; + @property (nonatomic) BOOL isNewMobileOnboardingFlow; @end + +NS_ASSUME_NONNULL_END diff --git a/IdentityCore/src/parameters/MSIDInteractiveRequestParameters.m b/IdentityCore/src/parameters/MSIDInteractiveRequestParameters.m index edd171f9f7..a6a48eca34 100644 --- a/IdentityCore/src/parameters/MSIDInteractiveRequestParameters.m +++ b/IdentityCore/src/parameters/MSIDInteractiveRequestParameters.m @@ -24,7 +24,24 @@ #import "MSIDInteractiveRequestParameters.h" #import "NSOrderedSet+MSIDExtensions.h" #import "MSIDClaimsRequest.h" +#import "MSIDMobileOnboardingState.h" @implementation MSIDInteractiveRequestParameters +@dynamic isNewMobileOnboardingFlow; + +- (BOOL)isNewMobileOnboardingFlow +{ + return self.mobileOnboardingState.isNewMobileOnboardingFlow; +} + +- (void)setIsNewMobileOnboardingFlow:(BOOL)isNewMobileOnboardingFlow +{ + if (!self.mobileOnboardingState) + { + self.mobileOnboardingState = [MSIDMobileOnboardingState new]; + } + self.mobileOnboardingState.isNewMobileOnboardingFlow = isNewMobileOnboardingFlow; +} + @end diff --git a/IdentityCore/src/parameters/MSIDMobileOnboardingState.h b/IdentityCore/src/parameters/MSIDMobileOnboardingState.h new file mode 100644 index 0000000000..c59e8e5309 --- /dev/null +++ b/IdentityCore/src/parameters/MSIDMobileOnboardingState.h @@ -0,0 +1,35 @@ +// 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 + +NS_ASSUME_NONNULL_BEGIN + +// Shared mutable state for mobile onboarding flow. +@interface MSIDMobileOnboardingState : NSObject + +@property (nonatomic) BOOL isNewMobileOnboardingFlow; + +@end + +NS_ASSUME_NONNULL_END diff --git a/IdentityCore/src/parameters/MSIDMobileOnboardingState.m b/IdentityCore/src/parameters/MSIDMobileOnboardingState.m new file mode 100644 index 0000000000..724721bf91 --- /dev/null +++ b/IdentityCore/src/parameters/MSIDMobileOnboardingState.m @@ -0,0 +1,28 @@ +// 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 "MSIDMobileOnboardingState.h" + +@implementation MSIDMobileOnboardingState + +@end diff --git a/IdentityCore/src/parameters/MSIDRequestParameters.m b/IdentityCore/src/parameters/MSIDRequestParameters.m index af8051accc..c89d9f28e2 100644 --- a/IdentityCore/src/parameters/MSIDRequestParameters.m +++ b/IdentityCore/src/parameters/MSIDRequestParameters.m @@ -34,6 +34,8 @@ #import "MSIDAccountIdentifier.h" #import "MSIDIntuneApplicationStateManager.h" #import "MSIDAuthenticationScheme.h" +#import "MSIDExecutionFlowLogger.h" +#import "MSIDExecutionFlowConstants.h" @implementation MSIDRequestParameters @@ -172,13 +174,31 @@ - (NSURL *)tokenEndpoint - (void)setCloudAuthorityWithCloudHostName:(NSString *)cloudHostName { if ([NSString msidIsStringNilOrBlank:cloudHostName]) return; + + NSString *lowercaseHostName = cloudHostName.lowercaseString; + + // Only rewrite the cloud authority when cloud_instance_host_name is a recognized + // Microsoft identity host; unrecognized values are ignored so the originally + // configured authority continues to be used. + if (![self.authority isRecognizedMicrosoftIdentityHost:lowercaseHostName]) + { + MSID_LOG_WITH_CTX(MSIDLogLevelWarning, self, @"Ignoring cloud_instance_host_name: host is not a recognized Microsoft identity host."); + + if (self.correlationId) + { + MSIDExecutionFlowInsertTag(MSIDCloudInstanceHostNameTagToString(MSIDCloudInstanceHostNameIgnoredTag), nil, self.correlationId); + } + + return; + } + NSError *cloudHostError = nil; - _cloudAuthority = [self.authority authorityWithUpdatedCloudHostInstanceName:cloudHostName error:&cloudHostError]; + _cloudAuthority = [self.authority authorityWithUpdatedCloudHostInstanceName:lowercaseHostName error:&cloudHostError]; if (!_cloudAuthority && cloudHostError) { - MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"Failed to create authority with cloud host name %@, and error %@, %ld", cloudHostName, cloudHostError.domain, (long)cloudHostError.code); + MSID_LOG_WITH_CTX(MSIDLogLevelError, self, @"Failed to create authority with cloud host name %@, and error %@, %ld", lowercaseHostName, cloudHostError.domain, (long)cloudHostError.code); } [self updateMSIDConfiguration]; } diff --git a/IdentityCore/src/requests/MSIDDeviceTokenGrantRequest.h b/IdentityCore/src/requests/MSIDDeviceTokenGrantRequest.h index 30270edf1c..3390359319 100644 --- a/IdentityCore/src/requests/MSIDDeviceTokenGrantRequest.h +++ b/IdentityCore/src/requests/MSIDDeviceTokenGrantRequest.h @@ -35,6 +35,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) MSIDWPJKeyPairWithCert *wpjInfo; +@property (nonatomic, nullable) NSString *nonce; + - (instancetype _Nullable)initWithEndpoint:(nonnull NSURL *)endpoint requestParameters:(nonnull MSIDRequestParameters *)requestParameters scopes:(nullable NSString *)scope diff --git a/IdentityCore/src/requests/MSIDDeviceTokenGrantRequest.m b/IdentityCore/src/requests/MSIDDeviceTokenGrantRequest.m index ff8dc20a7d..aa5634ee64 100644 --- a/IdentityCore/src/requests/MSIDDeviceTokenGrantRequest.m +++ b/IdentityCore/src/requests/MSIDDeviceTokenGrantRequest.m @@ -27,8 +27,6 @@ #import #import "MSIDDeviceTokenGrantRequest.h" #import "MSIDAADRequestConfigurator.h" -#import "MSIDKeyOperationUtil.h" -#import "MSIDJWTHelper.h" #import "MSIDNonceTokenRequest.h" #import "MSIDTokenResponse.h" #import "MSIDRequestParameters.h" @@ -38,11 +36,11 @@ #import "MSIDAADV2Oauth2Factory.h" #import "MSIDDeviceTokenResponseHandler.h" #import "MSIDTokenResponseValidator.h" +#import "MSIDDeviceTokenUtil.h" @interface MSIDDeviceTokenGrantRequest() @property (nonatomic) NSString *redirectUri; -@property (nonatomic) NSString *nonce; @property (nonatomic) NSString *enrollmentId; @property (nonatomic) MSIDWPJKeyPairWithCert *wpjInfo; @property (nonatomic) NSString *clientId; @@ -69,26 +67,46 @@ - (instancetype _Nullable)initWithEndpoint:(nonnull NSURL *)endpoint { if (!registrationInformation) { - MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"Failed to create device token request parameters: registration information is nil."); + NSString *errorMessage = @"Failed to create device token request parameters: registration information is nil."; + MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"%@", errorMessage); + if (error) + { + *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, errorMessage, nil, nil, nil, requestParameters.correlationId, nil, YES); + } return nil; } if (registrationInformation.certificateData == nil || registrationInformation.privateKeyRef == nil) { - MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"Failed to create device token request parameters: registration information is missing certificate data or private key."); + NSString *errorMessage = @"Failed to create device token request parameters: registration information is missing certificate data or private key."; + MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"%@", errorMessage); + if (error) + { + *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, errorMessage, nil, nil, nil, requestParameters.correlationId, nil, YES); + } return nil; } if ([NSString msidIsStringNilOrBlank:endpoint.absoluteString]) { - MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"Failed to create device token request parameters: authorityEndpoint is nil."); + NSString *errorMessage = @"Failed to create device token request parameters: authorityEndpoint is nil."; + MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"%@", errorMessage); + if (error) + { + *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, errorMessage, nil, nil, nil, requestParameters.correlationId, nil, YES); + } return nil; } NSString *clientId = requestParameters.clientId; if ([NSString msidIsStringNilOrBlank:clientId]) { - MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"Failed to create device token request parameters: clientId is nil or blank."); + NSString *errorMessage = @"Failed to create device token request parameters: clientId is nil or blank."; + MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"%@", errorMessage); + if (error) + { + *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, errorMessage, nil, nil, nil, requestParameters.correlationId, nil, YES); + } return nil; } @@ -108,25 +126,45 @@ - (instancetype _Nullable)initWithEndpoint:(nonnull NSURL *)endpoint if ([NSString msidIsStringNilOrBlank:resource]) { - MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"Failed to create device token request parameters: resource is nil or blank."); + NSString *errorMessage = @"Failed to create device token request parameters: resource is nil or blank."; + MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"%@", errorMessage); + if (error) + { + *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, errorMessage, nil, nil, nil, requestParameters.correlationId, nil, YES); + } return nil; } if ([NSString msidIsStringNilOrBlank:redirectUri]) { - MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"Failed to create device token request parameters: redirectURI is nil or blank."); + NSString *errorMessage = @"Failed to create device token request parameters: redirectURI is nil or blank."; + MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"%@", errorMessage); + if (error) + { + *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, errorMessage, nil, nil, nil, requestParameters.correlationId, nil, YES); + } return nil; } if (!scopesSet || scopesSet.count == 0) { - MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"Failed to create device token request parameters: scope is nil or empty."); + NSString *errorMessage = @"Failed to create device token request parameters: scope is nil or empty."; + MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"%@", errorMessage); + if (error) + { + *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, errorMessage, nil, nil, nil, requestParameters.correlationId, nil, YES); + } return nil; } if ([scopesSet containsObject:@"aza"]) { - MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"Failed to create device token request parameters: scopes contains aza."); + NSString *errorMessage = @"Failed to create device token request parameters: scopes contains aza."; + MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"%@", errorMessage); + if (error) + { + *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, errorMessage, nil, nil, nil, requestParameters.correlationId, nil, YES); + } return nil; } @@ -146,44 +184,29 @@ - (instancetype _Nullable)initWithEndpoint:(nonnull NSURL *)endpoint - (void)executeRequestWithCompletion:(nonnull MSIDRequestCompletionBlock)completionBlock { - MSIDRequestParameters *nonceReqParams = [MSIDRequestParameters new]; - nonceReqParams.correlationId = self.context.correlationId; - nonceReqParams.authority = [[MSIDAADAuthority alloc] initWithURL:self.urlRequest.URL rawTenant:MSIDAADTenantTypeCommonRawValue context:self.context error:nil]; - // Passing blank accountId details as device token is not associated with a specific account. This is required to bypass cache look up in nonce request and directly request new nonce from server. - nonceReqParams.accountIdentifier = [[MSIDAccountIdentifier alloc] initWithDisplayableId:@"" homeAccountId:@""]; - MSIDNonceTokenRequest *nonceRequest = [[MSIDNonceTokenRequest alloc] initWithRequestParameters:nonceReqParams]; - __weak typeof(self) weakSelf = self; - [nonceRequest executeRequestWithCompletion:^(NSString * _Nullable resultNonce, NSError * _Nullable error) + if ([NSString msidIsStringNilOrBlank:self.nonce]) { - __strong typeof(weakSelf) strongSelf = weakSelf; - if (!strongSelf) - { - return; - } - - if (!resultNonce || error) - { - NSError *nonceError = error ?: MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, @"Failed to retrieve nonce for device token request: nonce is nil.", nil, nil, nil, strongSelf.context.correlationId, nil, YES); - MSID_LOG_WITH_CTX(MSIDLogLevelError, strongSelf.context, @"Failed to retrieve nonce for device token request: %@", nonceError); - completionBlock(nil, nonceError); - return; - } - strongSelf.nonce = resultNonce; - [strongSelf tokenRequestWithCompletionBlock:completionBlock]; - }]; + MSID_LOG_WITH_CTX(MSIDLogLevelError, self.context, @"Failed to execute device token request: nonce is nil or blank."); + NSError *nonceError = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, @"Failed to execute device token request: nonce is nil or blank.", nil, nil, nil, self.context.correlationId, nil, YES); + completionBlock(nil, nonceError); + return; + } + [self tokenRequestWithCompletionBlock:completionBlock]; } - (void)tokenRequestWithCompletionBlock:(nonnull MSIDRequestCompletionBlock)completionBlock { NSError *jwtError; - NSString *jwt = [self getTokenRedemptionJwtForResource:self.resource - scopes:self.scopesSet - redirectUri:self.redirectUri - audience:self.urlRequest.URL.absoluteString - clientId:self.clientId - extraPayloadClaims:nil - context:self.context - error:&jwtError]; + NSString *jwt = [MSIDDeviceTokenUtil getDeviceTokenRequestJwtForResource:self.resource + scopes:self.scopesSet + redirectUri:self.redirectUri + audience:self.urlRequest.URL.absoluteString + clientId:self.clientId + nonce:self.nonce + registrationInformation:self.wpjInfo + extraPayloadClaims:nil + context:self.context + error:&jwtError]; if ([NSString msidIsStringNilOrBlank:jwt]) { @@ -198,17 +221,9 @@ - (void)tokenRequestWithCompletionBlock:(nonnull MSIDRequestCompletionBlock)comp __auto_type requestConfigurator = [MSIDAADRequestConfigurator new]; [requestConfigurator configure:self]; - NSMutableDictionary *requestParameters = [NSMutableDictionary new]; - requestParameters[MSID_OAUTH2_CLIENT_INFO] = @NO; // Set client_info = 0 to explicitly set that id token is not expected. - - if (self.enrollmentId) - { - requestParameters[MSID_ENROLLMENT_ID] = self.enrollmentId; - } - requestParameters[MSID_OAUTH2_GRANT_TYPE] = @"urn:ietf:params:oauth:grant-type:jwt-bearer"; - requestParameters[@"request"] = jwt; - - self.parameters = requestParameters; + self.parameters = [MSIDDeviceTokenUtil deviceTokenRequestBodyParametersWithJwt:jwt + enrollmentId:self.enrollmentId + extraParameters:nil]; __weak typeof(self) weakSelf = self; [self sendWithBlock:^(NSDictionary *tokenJsonResponse, NSError *tokenError) { @@ -225,78 +240,13 @@ - (void)tokenRequestWithCompletionBlock:(nonnull MSIDRequestCompletionBlock)comp return; } - MSIDDeviceTokenResponseHandler *tokenResponseHandler = (MSIDDeviceTokenResponseHandler *)strongSelf.tokenResponseHandler; - [tokenResponseHandler handleTokenResponse:tokenJsonResponse - context:strongSelf.requestParameters - error:tokenError - completionBlock:^(MSIDTokenResult * _Nullable result, NSError * _Nullable error) { - completionBlock(result, error); - }]; + [MSIDDeviceTokenUtil handleDeviceTokenResponse:tokenJsonResponse + requestParameters:strongSelf.requestParameters + responseHandler:(MSIDDeviceTokenResponseHandler *)strongSelf.tokenResponseHandler + error:tokenError + completionBlock:completionBlock]; }]; } -#pragma mark standard payload - -- (NSString *)getTokenRedemptionJwtForResource:(nonnull NSString *)resource - scopes:(NSSet *)scopes - redirectUri:(nonnull NSString *)redirectUri - audience:(nonnull NSString *)audience - clientId:(nonnull NSString *)clientId - extraPayloadClaims:(NSDictionary *)extraPayloadClaims - context:(id _Nullable)context - error:(NSError * __autoreleasing *)error -{ - MSIDWPJKeyPairWithCert *workplacejoinData = self.wpjInfo; - NSMutableDictionary *jwtPayload = [NSMutableDictionary new]; - for (NSString *key in extraPayloadClaims) - { - jwtPayload[key] = extraPayloadClaims[key]; - } - jwtPayload[MSID_OAUTH2_GRANT_TYPE] = MSID_OAUTH2_DEVICE_TOKEN; - jwtPayload[@"aud"] = audience; - jwtPayload[@"iss"] = clientId; // Issuer is the client ID - jwtPayload[MSID_OAUTH2_REDIRECT_URI] = redirectUri; - [jwtPayload setObject:clientId forKey:MSID_OAUTH2_CLIENT_ID]; - if (![NSString msidIsStringNilOrBlank:self.nonce]) - { - [jwtPayload setObject:self.nonce forKey:@"request_nonce"]; - } - NSString *scopeString = [scopes.allObjects componentsJoinedByString:@" "]; - if (![NSString msidIsStringNilOrBlank:scopeString]) - { - [jwtPayload setObject:scopeString forKey:MSID_OAUTH2_SCOPE]; - } - [jwtPayload setObject:resource forKey:@"resource"]; - - NSArray *certificateData = @[[NSString stringWithFormat:@"%@", [[workplacejoinData certificateData] base64EncodedStringWithOptions:kNilOptions]]]; - MSIDJwtAlgorithm alg = [[MSIDKeyOperationUtil sharedInstance] getJwtAlgorithmForKey:self.wpjInfo.privateKeyRef context:context error:error]; - if (!alg) - { - MSID_LOG_WITH_CTX(MSIDLogLevelError, context, @"[Device token] Failed to get JWT algorithm for signing key."); - return nil; - } - - NSDictionary *header = @{ - @"alg" : alg, - @"typ" : @"JWT", - @"x5c" : certificateData - }; - - NSString *signedJwt = [MSIDJWTHelper createSignedJWTforHeader:header payload:jwtPayload signingKey:workplacejoinData.privateKeyRef]; - if ([NSString msidIsStringNilOrBlank:signedJwt]) - { - MSID_LOG_WITH_CTX(MSIDLogLevelError, context, @"[Device token] Failed to sign JWT for requesting device token."); - if (error) - { - *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, @"Failed to sign JWT for requesting device token.", nil, nil, nil, context.correlationId, nil, YES); - } - return nil; - } - - return signedJwt; -} - - - @end #endif diff --git a/IdentityCore/src/requests/broker/mac/MSIDSSOXpcInteractiveTokenRequest.m b/IdentityCore/src/requests/broker/mac/MSIDSSOXpcInteractiveTokenRequest.m index e3f57372fa..5c8824d29a 100644 --- a/IdentityCore/src/requests/broker/mac/MSIDSSOXpcInteractiveTokenRequest.m +++ b/IdentityCore/src/requests/broker/mac/MSIDSSOXpcInteractiveTokenRequest.m @@ -84,8 +84,8 @@ - (void)executeRequestImplWithCompletionBlock:(MSIDInteractiveRequestCompletionB } [MSIDMainThreadUtil executeOnMainThreadIfNeeded:^{ - [self performXpcRequest:jsonDictionary]; self.requestCompletionBlock = completionBlock; + [self performXpcRequest:jsonDictionary]; }]; } diff --git a/IdentityCore/src/telemetry/MSIDOnboardingBlobBuilder.h b/IdentityCore/src/telemetry/MSIDOnboardingBlobBuilder.h index 37f4eb1a85..04533aefbd 100644 --- a/IdentityCore/src/telemetry/MSIDOnboardingBlobBuilder.h +++ b/IdentityCore/src/telemetry/MSIDOnboardingBlobBuilder.h @@ -71,6 +71,31 @@ typedef NS_ENUM(NSInteger, MSIDOnboardingSeedClassification) /// itself, sufficient evidence that this session is brokered. - (void)ensureBrokeredOnboardingMode; +/// Processes navigation response data for onboarding telemetry signals. +/// Extracts last-loaded domain from the URL host, reads blocking errors +/// from the x-ms-clitelem header, and records remediation steps for known error codes. +/// This consolidates the logic previously in the base webview controller so it can be +/// called from both the base webview controller and the navigation handler. +- (void)processResponseHeaders:(NSDictionary *)headers + responseURL:(NSURL *)responseURL; + +/// Flag indicating whether the strong-auth (MFA) setup step has been recorded during +/// the session. `finalizeForEndURL:error:` reads this to decide whether to stamp +/// StrongAuthSetupCompleted on the success path; MDM completion is stamped elsewhere. +@property (nonatomic, readonly) BOOL strongAuthSetupStarted; + +/// Records the terminal onboarding steps when the web flow ends. On the success path +/// (non-nil `endURL` and nil `error`) stamps StrongAuthSetupCompleted if the strong-auth +/// setup step was recorded during the session. Independently, if `endURL` points at a +/// well-known MDM-enrollment fwlink, stamps the mapped enrollment step. Safe to call on +/// either the success or failure path. +- (void)finalizeForEndURL:(nullable NSURL *)endURL error:(nullable NSError *)error; + +/// Maps a terminal `endURL` that points at a well-known go.microsoft.com fwlink +/// (browser://go.microsoft.com/fwlink[/]?...LinkId=...) to the onboarding step +/// that should be recorded. Returns nil for any URL that is not a recognized fwlink. ++ (nullable NSString *)onboardingStepForEndURL:(nullable NSURL *)endURL; + /// Returns the accumulated blob serialized as JSON. Always populated when the builder /// was constructed (carries the seed fields plus any recorded steps, blocking errors, /// ux flow, and last loaded domain). Returns @"" only if JSON serialization fails. diff --git a/IdentityCore/src/telemetry/MSIDOnboardingBlobBuilder.m b/IdentityCore/src/telemetry/MSIDOnboardingBlobBuilder.m index 4890fbdeac..a5fc8215d3 100644 --- a/IdentityCore/src/telemetry/MSIDOnboardingBlobBuilder.m +++ b/IdentityCore/src/telemetry/MSIDOnboardingBlobBuilder.m @@ -25,6 +25,8 @@ #import "MSIDOnboardingBlobBuilder.h" #import "MSIDOnboardingBlobFieldKeys.h" #import "MSIDSessionCachePersistence.h" +#import "NSString+MSIDExtensions.h" +#import "MSIDOAuth2Constants.h" // Seed field keys — must match xplat core (Djinni-generated constants). static NSString *const MSID_ONBOARDING_FIELD_SCHEMA_VERSION = @"schema_version"; @@ -82,6 +84,8 @@ @interface MSIDOnboardingBlobBuilder () @property (nonatomic) MSIDSessionCachePersistence *sessionCachePersistence; +@property (nonatomic, readwrite) BOOL strongAuthSetupStarted; + @end @implementation MSIDOnboardingBlobBuilder @@ -248,6 +252,98 @@ - (void)ensureBrokeredOnboardingMode } } +#pragma mark - HTTP Response Telemetry Processing + +// Error codes that are returned during normal sign-in flow and should not be +// treated as blocking onboarding errors. +// 50058 UserInformationNotProvided - User not signed in / no valid SSO session found +// 50097 DeviceAuthenticationRequired - Device auth interrupt triggered by CA policy +// 50126 InvalidUserNameOrPassword - Wrong username or password ++ (NSSet *)nonBlockingOnboardingErrorCodes +{ + static NSSet *codes = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + codes = [NSSet setWithObjects:@"50058", @"50097", @"50126", nil]; + }); + return codes; +} + +- (void)processResponseHeaders:(NSDictionary *)headers responseURL:(NSURL *)responseURL +{ + NSString *host = responseURL.host; + if (host.length > 0) + { + [self setLastLoadedDomain:host]; + } + + NSString *cliTelem = headers[MSID_OAUTH2_CLIENT_TELEMETRY]; + if ([NSString msidIsStringNilOrBlank:cliTelem]) + { + return; + } + + // Format: ,,,, + NSArray *components = [cliTelem componentsSeparatedByString:@","]; + if (components.count < 2) + { + return; + } + + NSString *errorCode = [components[1] msidTrimmedString]; + if (errorCode.length == 0 || [errorCode isEqualToString:@"0"]) + { + return; + } + + if ([[self.class nonBlockingOnboardingErrorCodes] containsObject:errorCode]) + { + return; + } + + [self addBlockingError:errorCode]; + [self recordRemediationStepForErrorCode:errorCode]; +} + +- (void)recordRemediationStepForErrorCode:(NSString *)errorCode +{ + NSDate *now = [NSDate date]; + + // 50079: Strong auth enrollment needed (MFA setup, not MFA fulfillment like 50076/50078) + if ([errorCode isEqualToString:@"50079"] && !self.strongAuthSetupStarted) + { + [self addStep:MSIDOnboardingBlobStepStrongAuthSetupStarted timestamp:now]; + self.strongAuthSetupStarted = YES; + } + // 50129 (DeviceIsNotWorkplaceJoined), 501291 (DeviceIsNotWorkplaceJoinedForMamApp): device registration needed + else if ([errorCode isEqualToString:@"50129"] || [errorCode isEqualToString:@"501291"]) + { + [self addStep:MSIDOnboardingBlobStepDeviceRegistrationRequired timestamp:now]; + } + // 530001, 530002: Device not compliant + else if ([errorCode isEqualToString:@"530001"] || [errorCode isEqualToString:@"530002"]) + { + [self addStep:MSIDOnboardingBlobStepDeviceNotCompliant timestamp:now]; + } + // 53000, 530003: MDM enrollment required + else if ([errorCode isEqualToString:@"53000"] || [errorCode isEqualToString:@"530003"]) + { + [self addStep:MSIDOnboardingBlobStepMdmEnrollmentRequired timestamp:now]; + } + // 50127: MAM app, device not registered + else if ([errorCode isEqualToString:@"50127"]) + { + [self addStep:MSIDOnboardingBlobStepBrokerInstallPromptedForMAM timestamp:now]; + } + // 501271: Broker app needs to be installed + else if ([errorCode isEqualToString:@"501271"]) + { + [self addStep:MSIDOnboardingBlobStepBrokerInstallPrompted timestamp:now]; + } + + // All other error codes (50076, 50078, 53005, 53003, etc.): blocking error only, no step. +} + - (NSString *)finalizeBlob { NSMutableDictionary *blob = [NSMutableDictionary dictionary]; @@ -296,4 +392,90 @@ - (NSString *)finalizeBlob return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] ?: @""; } +- (void)finalizeForEndURL:(NSURL *)endURL error:(NSError *)error +{ + BOOL flowSucceeded = (endURL != nil && error == nil); + if (flowSucceeded && self.strongAuthSetupStarted) + { + // MDMEnrollmentFinished is stamped from the in_app_enrollment_complete redirect. + [self addStep:MSIDOnboardingBlobStepStrongAuthSetupCompleted timestamp:[NSDate date]]; + } + + NSString *endUrlStep = [MSIDOnboardingBlobBuilder onboardingStepForEndURL:endURL]; + if (endUrlStep) + { + [self addStep:endUrlStep timestamp:[NSDate date]]; + } +} + +// Maps a terminal endURL that points at a well-known go.microsoft.com fwlink +// (browser://go.microsoft.com/fwlink[/]?...LinkId=...) to the onboarding +// step that should be recorded against the current blob. The LinkId-to-step +// map is the single extension point: new LinkIds (potentially mapping to a +// different step) just add entries here. ++ (NSString *)onboardingStepForEndURL:(NSURL *)endURL +{ + if (!endURL) + { + return nil; + } + + NSURLComponents *components = [NSURLComponents componentsWithURL:endURL resolvingAgainstBaseURL:NO]; + if (!components) + { + return nil; + } + + if ([components.scheme caseInsensitiveCompare:@"browser"] != NSOrderedSame) + { + return nil; + } + + if ([components.host caseInsensitiveCompare:@"go.microsoft.com"] != NSOrderedSame) + { + return nil; + } + + NSString *path = components.path; + if ([path caseInsensitiveCompare:@"/fwlink"] != NSOrderedSame + && [path caseInsensitiveCompare:@"/fwlink/"] != NSOrderedSame) + { + return nil; + } + + NSString *linkIdValue = nil; + for (NSURLQueryItem *item in components.queryItems) + { + if ([item.name caseInsensitiveCompare:@"LinkId"] == NSOrderedSame) + { + linkIdValue = item.value; + break; + } + } + + if (linkIdValue.length == 0) + { + return nil; + } + + return [[self onboardingStepsByFwlinkLinkId] objectForKey:linkIdValue]; +} + +// LinkId value -> onboarding step constant. Extension point: future LinkIds +// (which may map to a different onboarding step) are added here. ++ (NSDictionary *)onboardingStepsByFwlinkLinkId +{ + static NSDictionary *map = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + map = @{ + @"396941" : MSIDOnboardingBlobStepMdmEnrollmentStarted, // Public + @"2132314" : MSIDOnboardingBlobStepMdmEnrollmentStarted, // China + @"2114747" : MSIDOnboardingBlobStepMdmEnrollmentStarted, // GOV + @"399153" : MSIDOnboardingBlobStepMdmEnrollmentStarted, // PPE + }; + }); + return map; +} + @end diff --git a/IdentityCore/src/telemetry/MSIDOnboardingBlobFieldKeys.h b/IdentityCore/src/telemetry/MSIDOnboardingBlobFieldKeys.h index fb2cd7f56e..a318da467d 100644 --- a/IdentityCore/src/telemetry/MSIDOnboardingBlobFieldKeys.h +++ b/IdentityCore/src/telemetry/MSIDOnboardingBlobFieldKeys.h @@ -36,6 +36,9 @@ extern NSString * const MSIDOnboardingBlobIPCKey; // decide whether a session is brokered before finalizing the blob. extern NSString * const MSIDOnboardingModeBrokered; +// UX flow tags (appended to the `ux_flow_used` array). Records which onboarding UX a session went through. +extern NSString * const MSIDOnboardingUxFlowMobileOnboardingPhase1; + // Field keys for populated blob extern NSString * const MSIDOnboardingBlobFieldBlockingErrors; extern NSString * const MSIDOnboardingBlobFieldLastBlockingError; @@ -63,6 +66,33 @@ extern NSString * const MSIDOnboardingBlobStepJITComplianceBitSetStarted; extern NSString * const MSIDOnboardingBlobStepJITComplianceBitSetCompleted; extern NSString * const MSIDOnboardingBlobStepTokenIssued; +// New mobile-onboarding funnel steps (free-form passthrough; not C++-aggregated). +extern NSString * const MSIDOnboardingBlobStepProfileDownloadCompleted; +extern NSString * const MSIDOnboardingBlobStepComplianceRemediationMSAuthRedirect; +extern NSString * const MSIDOnboardingBlobStepMobileOnboardingClientFlightDisabledLegacyFallback; +extern NSString * const MSIDOnboardingBlobStepProfileInstallNotificationScheduled; +extern NSString * const MSIDOnboardingBlobStepSSOExtensionUnavailable; +extern NSString * const MSIDOnboardingBlobStepMdmEnrollmentCompletionRetryStarted; +extern NSString * const MSIDOnboardingBlobStepMdmEnrollmentRequestMalformed; +extern NSString * const MSIDOnboardingBlobStepMdmEnrollmentUrlMissing; +extern NSString * const MSIDOnboardingBlobStepComplianceRemediationRequestMalformed; +extern NSString * const MSIDOnboardingBlobStepComplianceRemediationUrlMissing; +extern NSString * const MSIDOnboardingBlobStepProfileInstallUrlMissing; +extern NSString * const MSIDOnboardingBlobStepProfileInstallUrlMalformed; +extern NSString * const MSIDOnboardingBlobStepMdmEnrollmentFailed; +extern NSString * const MSIDOnboardingBlobStepProfileDownloadFlowStarted; +extern NSString * const MSIDOnboardingBlobStepProfileDownloadFlowCancelled; +extern NSString * const MSIDOnboardingBlobStepProfileDownloadFlowFailed; + +// Token-request retry after MDM enrollment completes (free-form passthrough; not C++-aggregated). +extern NSString * const MSIDOnboardingBlobStepTokenRequestRetryStarted; + +// Seeded-BRT silent bootstrap funnel (broker-only; not aggregated in C++, fanned +// out to mo_steps_list via EntityStore's dynamic blob iteration). Stamped at the +// consumption site in ADBrokerJoinDeviceAction. +extern NSString * const MSIDOnboardingBlobStepSeededBRTBootstrapStarted; +extern NSString * const MSIDOnboardingBlobStepSeededBRTBootstrapCompleted; + // Step ID values used in C++ aggregation. Values must match // MSAIOnboardingBlobConstants (Djinni-generated) byte-for-byte. extern NSString * const MSIDOnboardingBlobStepStrongAuthSetupStarted; diff --git a/IdentityCore/src/telemetry/MSIDOnboardingBlobFieldKeys.m b/IdentityCore/src/telemetry/MSIDOnboardingBlobFieldKeys.m index 8767416a4e..5e9698d342 100644 --- a/IdentityCore/src/telemetry/MSIDOnboardingBlobFieldKeys.m +++ b/IdentityCore/src/telemetry/MSIDOnboardingBlobFieldKeys.m @@ -30,6 +30,9 @@ // Onboarding mode values (must match xplat core / Djinni-generated constants). NSString * const MSIDOnboardingModeBrokered = @"brokered"; +// UX flow tags (appended to the `ux_flow_used` array). +NSString * const MSIDOnboardingUxFlowMobileOnboardingPhase1 = @"MobileOnboardingPhase1"; + // Field keys for populated blob NSString * const MSIDOnboardingBlobFieldBlockingErrors = @"blocking_errors"; NSString * const MSIDOnboardingBlobFieldLastBlockingError = @"last_blocking_error"; @@ -57,6 +60,31 @@ NSString * const MSIDOnboardingBlobStepJITComplianceBitSetCompleted = @"JITComplianceBitSetCompleted"; NSString * const MSIDOnboardingBlobStepTokenIssued = @"TokenIssued"; +// New mobile-onboarding funnel steps +NSString * const MSIDOnboardingBlobStepProfileDownloadCompleted = @"ProfileDownloadCompleted"; +NSString * const MSIDOnboardingBlobStepComplianceRemediationMSAuthRedirect = @"ComplianceRemediationMSAuthRedirect"; +NSString * const MSIDOnboardingBlobStepMobileOnboardingClientFlightDisabledLegacyFallback = @"MobileOnboardingClientFlightDisabledLegacyFallback"; +NSString * const MSIDOnboardingBlobStepProfileInstallNotificationScheduled = @"ProfileInstallNotificationScheduled"; +NSString * const MSIDOnboardingBlobStepSSOExtensionUnavailable = @"SSOExtensionUnavailable"; +NSString * const MSIDOnboardingBlobStepMdmEnrollmentCompletionRetryStarted = @"MDMEnrollmentCompletionRetryStarted"; +NSString * const MSIDOnboardingBlobStepMdmEnrollmentRequestMalformed = @"MDMEnrollmentRequestMalformed"; +NSString * const MSIDOnboardingBlobStepMdmEnrollmentUrlMissing = @"MDMEnrollmentUrlMissing"; +NSString * const MSIDOnboardingBlobStepComplianceRemediationRequestMalformed = @"ComplianceRemediationRequestMalformed"; +NSString * const MSIDOnboardingBlobStepComplianceRemediationUrlMissing = @"ComplianceRemediationUrlMissing"; +NSString * const MSIDOnboardingBlobStepProfileInstallUrlMissing = @"ProfileInstallUrlMissing"; +NSString * const MSIDOnboardingBlobStepProfileInstallUrlMalformed = @"ProfileInstallUrlMalformed"; +NSString * const MSIDOnboardingBlobStepMdmEnrollmentFailed = @"MDMEnrollmentFailed"; +NSString * const MSIDOnboardingBlobStepProfileDownloadFlowStarted = @"ProfileDownloadFlowStarted"; +NSString * const MSIDOnboardingBlobStepProfileDownloadFlowCancelled = @"ProfileDownloadFlowCancelled"; +NSString * const MSIDOnboardingBlobStepProfileDownloadFlowFailed = @"ProfileDownloadFlowFailed"; + +// Token-request retry after MDM enrollment completes +NSString * const MSIDOnboardingBlobStepTokenRequestRetryStarted = @"TokenRequestRetryStarted"; + +// Seeded-BRT silent bootstrap funnel (broker-only; not aggregated in C++). +NSString * const MSIDOnboardingBlobStepSeededBRTBootstrapStarted = @"SeededBRTBootstrapStarted"; +NSString * const MSIDOnboardingBlobStepSeededBRTBootstrapCompleted = @"SeededBRTBootstrapCompleted"; + // Step ID values used in C++ aggregation (must match MSAIOnboardingBlobConstants) NSString * const MSIDOnboardingBlobStepStrongAuthSetupStarted = @"StrongAuthSetupStarted"; NSString * const MSIDOnboardingBlobStepStrongAuthSetupCompleted = @"StrongAuthSetupCompleted"; diff --git a/IdentityCore/src/telemetry/execution_flow/MSIDExecutionFlowConstants.h b/IdentityCore/src/telemetry/execution_flow/MSIDExecutionFlowConstants.h index 598446b1ce..a001fdede0 100644 --- a/IdentityCore/src/telemetry/execution_flow/MSIDExecutionFlowConstants.h +++ b/IdentityCore/src/telemetry/execution_flow/MSIDExecutionFlowConstants.h @@ -123,3 +123,21 @@ typedef NS_ENUM(NSInteger, MSIDSSORemoteSilentTokenRequestTag) }; /// Returns the string representation for each MSIDSSORemoteSilentTokenRequestTag value. FOUNDATION_EXPORT NSString * _Nonnull MSIDSSORemoteSilentTokenRequestTagToString(MSIDSSORemoteSilentTokenRequestTag state); + +/// An enum of MSIDCloudInstanceHostNameTag. +typedef NS_ENUM(NSInteger, MSIDCloudInstanceHostNameTag) +{ + MSIDCloudInstanceHostNameIgnoredTag = 0 +}; +/// Returns the string representation for each MSIDCloudInstanceHostNameTag value. +FOUNDATION_EXPORT NSString * _Nonnull MSIDCloudInstanceHostNameTagToString(MSIDCloudInstanceHostNameTag state); + +/// An enum of MSIDPkeyAuthTag. +typedef NS_ENUM(NSInteger, MSIDPkeyAuthTag) +{ + MSIDPkeyAuthAddedRefreshTokenCredentialTag = 0, + MSIDPkeyAuthSkippedRefreshTokenCredentialUntrustedHostTag +}; + +/// Returns the string representation for each MSIDPkeyAuthTag value. +FOUNDATION_EXPORT NSString * _Nonnull MSIDPkeyAuthTagToString(MSIDPkeyAuthTag state); diff --git a/IdentityCore/src/telemetry/execution_flow/MSIDExecutionFlowConstants.m b/IdentityCore/src/telemetry/execution_flow/MSIDExecutionFlowConstants.m index 21a4e26cb0..eb2005b276 100644 --- a/IdentityCore/src/telemetry/execution_flow/MSIDExecutionFlowConstants.m +++ b/IdentityCore/src/telemetry/execution_flow/MSIDExecutionFlowConstants.m @@ -150,3 +150,26 @@ return [NSString stringWithFormat:@"MSIDSSORemoteSilentTokenRequestTag(%ld)", (long)state]; } +NSString *MSIDCloudInstanceHostNameTagToString(MSIDCloudInstanceHostNameTag state) +{ + switch (state) + { + case MSIDCloudInstanceHostNameIgnoredTag: + return @"lziv8"; + } + // Fallback for any future enum values + return [NSString stringWithFormat:@"MSIDCloudInstanceHostNameTag(%ld)", (long)state]; +} + +NSString *MSIDPkeyAuthTagToString(MSIDPkeyAuthTag state) +{ + switch (state) + { + case MSIDPkeyAuthAddedRefreshTokenCredentialTag: + return @"p5e7g"; + case MSIDPkeyAuthSkippedRefreshTokenCredentialUntrustedHostTag: + return @"mi1dp"; + } + // Fallback for any future enum values + return [NSString stringWithFormat:@"MSIDPkeyAuthTag(%ld)", (long)state]; +} diff --git a/IdentityCore/src/util/MSIDHelpers.h b/IdentityCore/src/util/MSIDHelpers.h index a27a293085..e1b6a5c9fc 100644 --- a/IdentityCore/src/util/MSIDHelpers.h +++ b/IdentityCore/src/util/MSIDHelpers.h @@ -23,10 +23,21 @@ #import +NS_ASSUME_NONNULL_BEGIN + @interface MSIDHelpers : NSObject /*! Returns integer value if the passed object can be converted to integer, 0 otherwise */ + (NSInteger)msidIntegerValue:(id)value; -+ (NSString *)normalizeUserId:(NSString *)userId; ++ (nullable NSString *)normalizeUserId:(nullable NSString *)userId; + +/*! The Apple Developer team identifiers of Microsoft first-party apps. */ ++ (NSSet *)microsoft1PAppsTeamIDs; + +/*! Returns YES if the given Apple Developer team identifier belongs to a + Microsoft first-party app (see +microsoft1PAppsTeamIDs). */ ++ (BOOL)isMicrosoftFirstPartyAppWithTeamId:(nullable NSString *)teamId; @end + +NS_ASSUME_NONNULL_END diff --git a/IdentityCore/src/util/MSIDHelpers.m b/IdentityCore/src/util/MSIDHelpers.m index 83b3851169..1179285141 100644 --- a/IdentityCore/src/util/MSIDHelpers.m +++ b/IdentityCore/src/util/MSIDHelpers.m @@ -35,7 +35,7 @@ + (NSInteger)msidIntegerValue:(id)value return 0; } -+ (NSString *)normalizeUserId:(NSString *)userId ++ (nullable NSString *)normalizeUserId:(nullable NSString *)userId { if (!userId) { @@ -46,4 +46,23 @@ + (NSString *)normalizeUserId:(NSString *)userId return normalized.length ? normalized : nil; } ++ (NSSet *)microsoft1PAppsTeamIDs +{ + static NSSet *teamIDs = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + teamIDs = [[NSSet alloc] initWithArray:@[@"SGGM6D27TK", + @"9KBH5RKYEW", // 9KB* is the prefix for enterprise-signed 1st party iOS apps + @"UBF8T346G9" + ]]; + }); + + return teamIDs; +} + ++ (BOOL)isMicrosoftFirstPartyAppWithTeamId:(nullable NSString *)teamId +{ + return teamId.length > 0 && [[self microsoft1PAppsTeamIDs] containsObject:teamId]; +} + @end diff --git a/IdentityCore/src/util/NSBundle+MSIDExtensions.h b/IdentityCore/src/util/NSBundle+MSIDExtensions.h index a7d0aed458..810c360230 100644 --- a/IdentityCore/src/util/NSBundle+MSIDExtensions.h +++ b/IdentityCore/src/util/NSBundle+MSIDExtensions.h @@ -28,6 +28,7 @@ NS_ASSUME_NONNULL_BEGIN @interface NSBundle (MSIDExtensions) + (NSString *)msidAppVersion; ++ (NSString *)msidAppName; @end diff --git a/IdentityCore/src/util/NSBundle+MSIDExtensions.m b/IdentityCore/src/util/NSBundle+MSIDExtensions.m index 62f637d21b..065b93558e 100644 --- a/IdentityCore/src/util/NSBundle+MSIDExtensions.m +++ b/IdentityCore/src/util/NSBundle+MSIDExtensions.m @@ -36,4 +36,16 @@ + (NSString *)msidAppVersion return appVersion; } ++ (NSString *)msidAppName +{ + NSDictionary *info = [[NSBundle mainBundle] infoDictionary]; + NSString *appName = info[@"CFBundleDisplayName"]; + if (!appName) + { + appName = info[@"CFBundleName"]; + } + + return appName ?: @""; +} + @end diff --git a/IdentityCore/src/util/NSString+MSIDExtensions.m b/IdentityCore/src/util/NSString+MSIDExtensions.m index 272ebee7e8..62348f69d1 100644 --- a/IdentityCore/src/util/NSString+MSIDExtensions.m +++ b/IdentityCore/src/util/NSString+MSIDExtensions.m @@ -73,7 +73,10 @@ - (NSString *)msidBase64UrlDecode + (BOOL)msidIsStringNilOrBlank:(NSString *)string { - if (!string || [string isKindOfClass:[NSNull class]] || !string.length) + // Treat any non-NSString (nil, NSNull, or a mis-typed value such as an NSNumber/__NSCFBoolean + // decoded from JSON or an MDM configuration plist) as blank, rather than sending -length to it + // and raising an unrecognized-selector exception. + if (!string || ![string isKindOfClass:[NSString class]] || !string.length) { return YES; } diff --git a/IdentityCore/src/util/mac/MSIDXpcCanPerformFailureReason.h b/IdentityCore/src/util/mac/MSIDXpcCanPerformFailureReason.h new file mode 100644 index 0000000000..774e30571a --- /dev/null +++ b/IdentityCore/src/util/mac/MSIDXpcCanPerformFailureReason.h @@ -0,0 +1,56 @@ +// +// 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 + +NS_ASSUME_NONNULL_BEGIN + +// Distinguishes why +[MSIDXpcSingleSignOnProvider canPerformRequest:] returned NO, +// so telemetry/logging can disambiguate which of the client-side static gates rejected the request. +// +// Kept in a lightweight standalone header (no SSOExtension/broker dependencies) so that consumers +// which only need the failure-reason type (e.g. the Xpc token request controllers and their +// downstream callers) do not have to import the full MSIDXpcSingleSignOnProvider interface. +typedef NS_ENUM(NSInteger, MSIDXpcCanPerformFailureReason) +{ + // canPerformRequest succeeded, no failure occurred. + MSIDXpcCanPerformFailureReasonNone = 0, + // Neither the MacBrokerApp nor the CompanyPortal Xpc component is installed on the device. + MSIDXpcCanPerformFailureReasonNoProviderInstalled, + // Failed to construct the SSOExtension getDeviceInfo request object. + MSIDXpcCanPerformFailureReasonDeviceInfoRequestCreationFailed, + // SSOExtension getDeviceInfo handshake completed with a hard error. + MSIDXpcCanPerformFailureReasonDeviceInfoHandshakeError, + // SSOExtension getDeviceInfo handshake did not complete before the 1 second timeout expired. + MSIDXpcCanPerformFailureReasonDeviceInfoHandshakeTimeout, + // No installed Xpc provider matches the cached/available Xpc configuration. + MSIDXpcCanPerformFailureReasonValidateCacheProviderFailed, + // The Xpc broker flow is gated behind macOS 13+ (or otherwise unsupported on this OS version) and was rejected before reaching MSIDXpcSingleSignOnProvider. + MSIDXpcCanPerformFailureReasonUnsupportedOSVersion, +}; + +// Returns a human readable, non-PII name for the given failure reason, suitable for logging. +extern NSString *MSIDXpcCanPerformFailureReasonToString(MSIDXpcCanPerformFailureReason reason); + +NS_ASSUME_NONNULL_END diff --git a/IdentityCore/src/util/mac/MSIDXpcSingleSignOnProvider.h b/IdentityCore/src/util/mac/MSIDXpcSingleSignOnProvider.h index d139a31b22..dc4cf9aa04 100644 --- a/IdentityCore/src/util/mac/MSIDXpcSingleSignOnProvider.h +++ b/IdentityCore/src/util/mac/MSIDXpcSingleSignOnProvider.h @@ -27,6 +27,7 @@ #import "MSIDSSOExtensionRequestDelegate.h" #import "MSIDRequestContext.h" #import "MSIDXpcProviderCaching.h" +#import "MSIDXpcCanPerformFailureReason.h" NS_ASSUME_NONNULL_BEGIN @@ -50,6 +51,11 @@ NS_ASSUME_NONNULL_BEGIN + (BOOL)canPerformRequest:(id)xpcProviderCache; +// Same as canPerformRequest: above, but additionally reports the specific reason for a NO result via the reason out-param. +// The reason is left untouched if the caller passes nil. ++ (BOOL)canPerformRequest:(id)xpcProviderCache + reason:(MSIDXpcCanPerformFailureReason * _Nullable)reason; + NS_ASSUME_NONNULL_END @end diff --git a/IdentityCore/src/util/mac/MSIDXpcSingleSignOnProvider.m b/IdentityCore/src/util/mac/MSIDXpcSingleSignOnProvider.m index 875b6f419d..51e14295b9 100644 --- a/IdentityCore/src/util/mac/MSIDXpcSingleSignOnProvider.m +++ b/IdentityCore/src/util/mac/MSIDXpcSingleSignOnProvider.m @@ -67,6 +67,29 @@ #import "MSIDXpcProviderCaching.h" #import "MSIDFlightManager.h" +static const NSTimeInterval MSIDXpcDispatcherEndpointLookupTimeout = 10.0; +static const NSTimeInterval MSIDXpcBrokerReplyTimeout = 60.0; + +// Interactive broker requests are gated on real user interaction (password, MFA, consent) and +// therefore have no safe upper bound. Applying the fixed broker-reply watchdog to them could abort +// a legitimate in-progress flow and surface a spurious MSIDErrorBrokerXpcUnexpectedError, so the +// watchdog is disabled for interactive requests. A timeout value <= 0 disables the watchdog; +// connection interruption/invalidation still completes the request via the transport-failure path. +static const NSTimeInterval MSIDXpcBrokerReplyTimeoutDisabled = 0.0; + +static NSError *MSIDXpcCreateTransportError(NSString *description, NSError *underlyingError) +{ + return MSIDCreateError(MSIDErrorDomain, + MSIDErrorBrokerXpcUnexpectedError, + description, + nil, + nil, + underlyingError, + nil, + nil, + YES); +} + @protocol MSIDXpcBrokerInstanceProtocol - (void)handleXpcWithRequestParams:(NSDictionary *)passedInParams @@ -84,6 +107,7 @@ - (void)getBrokerInstanceEndpointWithReply:(void (^)(NSXPCListenerEndpoint * _N @end typedef void (^NSXPCListenerEndpointCompletionBlock)(id _Nullable xpcService, NSXPCConnection * _Nullable directConnection, NSError *error); +typedef BOOL (^MSIDXpcRequestCompletedBlock)(void); @interface MSIDXpcSingleSignOnProvider () @@ -92,9 +116,59 @@ @interface MSIDXpcSingleSignOnProvider () // Tests swizzle this to honor the flight regardless of build configuration so that // flight-controlled behavior can be verified deterministically. - (BOOL)isXpcInstanceCacheEnabled; +- (BOOL)isXpcPlatformSupported; + +- (NSXPCConnection *)dispatcherConnectionWithMachServiceName:(NSString *)machServiceName; +- (NSXPCConnection *)directConnectionWithEndpoint:(NSXPCListenerEndpoint *)endpoint; +- (void)scheduleBlock:(dispatch_block_t)block afterTimeout:(NSTimeInterval)timeout; +- (void)getXpcService:(id)xpcProviderCache + requestCompleted:(MSIDXpcRequestCompletedBlock)requestCompleted + withContinueBlock:(NSXPCListenerEndpointCompletionBlock)continueBlock; + +// Shared funnel for both the silent and interactive public entry points. brokerReplyTimeout controls +// the broker-reply watchdog for this request (<= 0 disables it; see MSIDXpcBrokerReplyTimeoutDisabled). +- (void)handleRequestParam:(NSDictionary *)requestParam + parentViewFrame:(NSRect)frame + assertKindOfResponseClass:(Class)aClass + xpcProviderCache:(id)xpcProviderCache + context:(id)context + brokerReplyTimeout:(NSTimeInterval)brokerReplyTimeout + continueBlock:(MSIDSSOExtensionRequestDelegateCompletionBlock)continueBlock; + +- (void)attemptBrokerRequest:(NSDictionary *)requestParam + parentViewFrame:(NSRect)frame + assertKindOfResponseClass:(Class)aClass + xpcProviderCache:(id)xpcProviderCache + useCachedEndpoint:(BOOL)useCachedEndpoint + brokerReplyTimeout:(NSTimeInterval)brokerReplyTimeout + context:(id)context + continueBlock:(MSIDSSOExtensionRequestDelegateCompletionBlock)continueBlock; @end +NSString *MSIDXpcCanPerformFailureReasonToString(MSIDXpcCanPerformFailureReason reason) +{ + switch (reason) + { + case MSIDXpcCanPerformFailureReasonNone: + return @"None"; + case MSIDXpcCanPerformFailureReasonNoProviderInstalled: + return @"NoProviderInstalled"; + case MSIDXpcCanPerformFailureReasonDeviceInfoRequestCreationFailed: + return @"DeviceInfoRequestCreationFailed"; + case MSIDXpcCanPerformFailureReasonDeviceInfoHandshakeError: + return @"DeviceInfoHandshakeError"; + case MSIDXpcCanPerformFailureReasonDeviceInfoHandshakeTimeout: + return @"DeviceInfoHandshakeTimeout"; + case MSIDXpcCanPerformFailureReasonValidateCacheProviderFailed: + return @"ValidateCacheProviderFailed"; + case MSIDXpcCanPerformFailureReasonUnsupportedOSVersion: + return @"UnsupportedOSVersion"; + } + + return @"Unknown"; +} + @implementation MSIDXpcSingleSignOnProvider - (BOOL)isXpcInstanceCacheEnabled @@ -106,17 +180,47 @@ - (BOOL)isXpcInstanceCacheEnabled #endif } +- (BOOL)isXpcPlatformSupported +{ + if (@available(macOS 13.0, *)) + { + return YES; + } + + return NO; +} + - (void)handleRequestParam:(NSDictionary *)requestParam assertKindOfResponseClass:(Class)aClass xpcProviderCache:(id)xpcProviderCache context:(id)context continueBlock:(MSIDSSOExtensionRequestDelegateCompletionBlock)continueBlock { + // Silent requests are not gated on user interaction, so the standard broker-reply watchdog applies. [self handleRequestParam:requestParam parentViewFrame:CGRectZero assertKindOfResponseClass:(Class)aClass xpcProviderCache:xpcProviderCache context:(id)context + brokerReplyTimeout:MSIDXpcBrokerReplyTimeout + continueBlock:continueBlock]; +} + +- (void)handleRequestParam:(NSDictionary *)requestParam + parentViewFrame:(NSRect)frame + assertKindOfResponseClass:(Class)aClass + xpcProviderCache:(id)xpcProviderCache + context:(id)context + continueBlock:(MSIDSSOExtensionRequestDelegateCompletionBlock)continueBlock +{ + // Interactive requests are gated on real user interaction (password, MFA, consent), which has no + // safe upper bound, so the broker-reply watchdog is disabled to avoid aborting a legitimate flow. + [self handleRequestParam:requestParam + parentViewFrame:frame + assertKindOfResponseClass:aClass + xpcProviderCache:xpcProviderCache + context:context + brokerReplyTimeout:MSIDXpcBrokerReplyTimeoutDisabled continueBlock:continueBlock]; } @@ -125,13 +229,15 @@ - (void)handleRequestParam:(NSDictionary *)requestParam assertKindOfResponseClass:(Class)aClass xpcProviderCache:(id)xpcProviderCache context:(id)context + brokerReplyTimeout:(NSTimeInterval)brokerReplyTimeout continueBlock:(MSIDSSOExtensionRequestDelegateCompletionBlock)continueBlock { [self attemptBrokerRequest:requestParam parentViewFrame:frame assertKindOfResponseClass:aClass xpcProviderCache:xpcProviderCache - useCachedEndpoint:[self isXpcInstanceCacheEnabled] + useCachedEndpoint:[self isXpcInstanceCacheEnabled] + brokerReplyTimeout:brokerReplyTimeout context:context continueBlock:continueBlock]; } @@ -157,6 +263,7 @@ - (void)attemptBrokerRequest:(NSDictionary *)requestParam assertKindOfResponseClass:(Class)aClass xpcProviderCache:(id)xpcProviderCache useCachedEndpoint:(BOOL)useCachedEndpoint + brokerReplyTimeout:(NSTimeInterval)brokerReplyTimeout context:(id)context continueBlock:(MSIDSSOExtensionRequestDelegateCompletionBlock)continueBlock { @@ -179,6 +286,13 @@ - (void)attemptBrokerRequest:(NSDictionary *)requestParam } }; + BOOL (^isCompleted)(void) = ^BOOL { + @synchronized (stateLock) + { + return outerCompleted; + } + }; + // Marks the request reply as received. Returns the previous value so callers can // distinguish "this is the reply" from "the reply already arrived". BOOL (^markReplyReceived)(void) = ^BOOL { @@ -199,7 +313,23 @@ - (void)attemptBrokerRequest:(NSDictionary *)requestParam NSXPCListenerEndpointCompletionBlock continueBlockInternal = ^(id xpcService, NSXPCConnection *directConnection, NSError *error) { - if (!xpcService || error) + if (isCompleted()) + { + [directConnection invalidate]; + return; + } + + NSError *transportError = error; + if (!transportError && !xpcService) + { + transportError = MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- XPC service proxy is unavailable", nil); + } + else if (!transportError && !directConnection) + { + transportError = MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- XPC direct connection is unavailable", nil); + } + + if (!xpcService || !directConnection || transportError) { // Connection-establishment / connection-handler failure path. // If the request reply already came in, this is just our own [directConnection invalidate] @@ -209,13 +339,13 @@ - (void)attemptBrokerRequest:(NSDictionary *)requestParam if (fromCache && useCachedEndpoint) { - MSID_LOG_WITH_CTX(MSIDLogLevelError, context, @"[Entra broker] CLIENT - cached XPC endpoint failed (%@), clearing cache and retrying via dispatcher", error); + MSID_LOG_WITH_CTX(MSIDLogLevelError, context, @"[Entra broker] CLIENT - cached XPC endpoint failed (%@), clearing cache and retrying via dispatcher", transportError); [xpcProviderCache clearCachedBrokerInstanceEndpoint]; __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) { // Provider deallocated mid-flight — we still owe the caller a callback. - if (continueBlock) continueBlock(nil, error); + if (continueBlock) continueBlock(nil, transportError); return; } // Note: the recursive call has its own __block completion state, so the outer @@ -227,19 +357,38 @@ - (void)attemptBrokerRequest:(NSDictionary *)requestParam assertKindOfResponseClass:aClass xpcProviderCache:xpcProviderCache useCachedEndpoint:NO + brokerReplyTimeout:brokerReplyTimeout context:context continueBlock:continueBlock]; return; } - if (continueBlock) continueBlock(nil, error); + if (continueBlock) continueBlock(nil, transportError); return; } + // Broker-reply watchdog. Disabled (brokerReplyTimeout <= 0) for interactive requests, whose + // reply is gated on unbounded user interaction; a connection interruption/invalidation still + // completes the request via the transport-failure path above. + if (brokerReplyTimeout > 0) + { + NSError *brokerReplyTimeoutError = MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- broker reply timed out", nil); + [self scheduleBlock:^{ + if (isReplyReceived()) return; + if (!claimCompletion()) return; + + [directConnection suspend]; + [directConnection invalidate]; + if (continueBlock) continueBlock(nil, brokerReplyTimeoutError); + } afterTimeout:brokerReplyTimeout]; + } + [xpcService handleXpcWithRequestParams:requestParam parentViewFrame:frame completionBlock:^(NSDictionary * _Nullable replyParam, NSDate * _Nonnull __unused xpcStartDate, NSString * _Nonnull __unused processId, NSError * _Nullable callbackError) { // Mark synchronously so the connection's invalidation handler (which fires as a side // effect of our own invalidate below) does not treat this as a transport failure. (void)markReplyReceived(); + if (isCompleted()) return; + [directConnection suspend]; [directConnection invalidate]; @@ -283,11 +432,19 @@ - (void)attemptBrokerRequest:(NSDictionary *)requestParam else { fromCache = NO; - [self getXpcService:xpcProviderCache withContinueBlock:continueBlockInternal]; + [self getXpcService:xpcProviderCache + requestCompleted:isCompleted + withContinueBlock:continueBlockInternal]; } } + (BOOL)canPerformRequest:(id)xpcProviderCache +{ + return [self canPerformRequest:xpcProviderCache reason:nil]; +} + ++ (BOOL)canPerformRequest:(id)xpcProviderCache + reason:(MSIDXpcCanPerformFailureReason *)reason { // Step 0: If none of the XPC components (CP or MacBrokerApp) exist on the device, return false. // Step 1: Read from the userDefaults cache to find the correct XPC configuration based on the active SsoExtension. @@ -299,15 +456,29 @@ + (BOOL)canPerformRequest:(id)xpcProviderCache // Step 1.2.2: If the handshake fails because canPerformRequest returns NO, use predefined logic to decide the XPC provider/configuration (use the XPC component from the MacBrokerApp first, then from the CompanyPortal App). // Step 1.2.2.1: If using the XPC provider from the MacBroker App, return true. // Step 1.2.2.2: If using the XPC provider from the CompanyPortal App, return true. - + + if (reason) + { + *reason = MSIDXpcCanPerformFailureReasonNone; + } + /* Step 0 Start*/ if (!xpcProviderCache.isXpcProviderInstalledOnDevice) { MSID_LOG_WITH_CTX(MSIDLogLevelInfo, nil, @"[Entra broker] CLIENT Xpc component is not available on device", nil, nil); + if (reason) + { + *reason = MSIDXpcCanPerformFailureReasonNoProviderInstalled; + } return NO; } /* Step 0 End*/ + // Tracks whether the getDeviceInfo handshake below hit a hard error or timed out, so that if the + // subsequent validateCacheXpcProvider check ultimately fails, we can report the more specific root cause. + __block BOOL handshakeHadError = NO; + BOOL handshakeTimedOut = NO; + /* Step 1 Start: decide Xpc configuration */ if (!xpcProviderCache.xpcConfiguration && [MSIDSSOExtensionGetDeviceInfoRequest canPerformRequest]) { @@ -328,6 +499,10 @@ + (BOOL)canPerformRequest:(id)xpcProviderCache { // This is unlikely to happen, but if it does, return NO MSID_LOG_WITH_CTX_PII(MSIDLogLevelError, nil, @"[Entra broker] CLIENT get error when creating getDeviceInfoRequest with error: %@", ssoExtensionRequestError); + if (reason) + { + *reason = MSIDXpcCanPerformFailureReasonDeviceInfoRequestCreationFailed; + } return NO; } @@ -339,6 +514,7 @@ + (BOOL)canPerformRequest:(id)xpcProviderCache if (error) { MSID_LOG_WITH_CTX_PII(MSIDLogLevelError, nil, @"[Entra broker] CLIENT did not receive deviceInfo with error: %@", error); + handshakeHadError = YES; dispatch_group_leave(group); return; } @@ -349,7 +525,12 @@ + (BOOL)canPerformRequest:(id)xpcProviderCache // waiting expired in 1 sec dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC); - dispatch_group_wait(group, timeout); + long waitResult = dispatch_group_wait(group, timeout); + handshakeTimedOut = (waitResult != 0); + if (handshakeTimedOut) + { + MSID_LOG_WITH_CTX(MSIDLogLevelWarning, nil, @"[Entra broker] CLIENT getDeviceInfo handshake timed out after 1 sec", nil, nil); + } } if (!xpcProviderCache.xpcConfiguration) @@ -362,6 +543,23 @@ + (BOOL)canPerformRequest:(id)xpcProviderCache MSID_LOG_WITH_CTX(MSIDLogLevelInfo, nil, @"[Entra broker] CLIENT no %@ Xpc component found on device. Failed to validate cached Xpc and skip Xpc flow", xpcProviderCache.xpcConfiguration.xpcHostAppName, nil); // Reset the cached Xpc provider/configuration. xpcProviderCache.cachedXpcProviderType = MSIDUnknownSsoProvider; + if (reason) + { + // If the getDeviceInfo handshake above timed out or errored, surface that as the root cause + // instead of the more generic validation failure it ultimately fell through to. + if (handshakeTimedOut) + { + *reason = MSIDXpcCanPerformFailureReasonDeviceInfoHandshakeTimeout; + } + else if (handshakeHadError) + { + *reason = MSIDXpcCanPerformFailureReasonDeviceInfoHandshakeError; + } + else + { + *reason = MSIDXpcCanPerformFailureReasonValidateCacheProviderFailed; + } + } return NO; } @@ -467,12 +665,31 @@ - (void)forceRunOnBackgroundQueue:(BOOL)forceOnBackgroundQueue dispatchBlock:(vo } } -- (void)getXpcService:(id)xpcProviderCache withContinueBlock:(NSXPCListenerEndpointCompletionBlock)continueBlock +- (NSXPCConnection *)dispatcherConnectionWithMachServiceName:(NSString *)machServiceName +{ + return [[NSXPCConnection alloc] initWithMachServiceName:machServiceName options:0]; +} + +- (NSXPCConnection *)directConnectionWithEndpoint:(NSXPCListenerEndpoint *)endpoint +{ + return [[NSXPCConnection alloc] initWithListenerEndpoint:endpoint]; +} + +- (void)scheduleBlock:(dispatch_block_t)block afterTimeout:(NSTimeInterval)timeout +{ + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)), + dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), + block); +} + +- (void)getXpcService:(id)xpcProviderCache + requestCompleted:(MSIDXpcRequestCompletedBlock)requestCompleted + withContinueBlock:(NSXPCListenerEndpointCompletionBlock)continueBlock { if (!xpcProviderCache.xpcConfiguration) { MSID_LOG_WITH_CTX(MSIDLogLevelWarning, nil, @"[Entra broker] CLIENT - Code should not be triggerred at here", nil, nil); - continueBlock(nil, nil, MSIDCreateError(MSIDErrorDomain, MSIDErrorBrokerXpcUnexpectedError, @"[Entra broker] CLIENT - Xpc configuration is not available", nil, nil, nil, nil, nil, YES)); + continueBlock(nil, nil, MSIDXpcCreateTransportError(@"[Entra broker] CLIENT - Xpc configuration is not available", nil)); return; } @@ -484,7 +701,12 @@ - (void)getXpcService:(id)xpcProviderCache withContinueB BOOL cacheEnabled = [self isXpcInstanceCacheEnabled]; MSID_LOG_WITH_CTX(MSIDLogLevelInfo, nil, @"[Entra broker] CLIENT - started establishing connection to %@", xpcProviderCache.xpcConfiguration.xpcMachServiceName); - NSXPCConnection *connection = [[NSXPCConnection alloc] initWithMachServiceName:xpcProviderCache.xpcConfiguration.xpcMachServiceName options:0]; + NSXPCConnection *connection = [self dispatcherConnectionWithMachServiceName:xpcProviderCache.xpcConfiguration.xpcMachServiceName]; + if (!connection) + { + continueBlock(nil, nil, MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- dispatcher connection is unavailable", nil)); + return; + } NSString *codeSigningRequirement = [self codeSignRequirementForBundleId:xpcProviderCache.xpcConfiguration.xpcBrokerDispatchServiceBundleId devIdentity:[self signingIdentity]]; if ([NSString msidIsStringNilOrBlank:codeSigningRequirement]) @@ -494,54 +716,102 @@ - (void)getXpcService:(id)xpcProviderCache withContinueB return; } connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(MSIDXpcBrokerDispatcherProtocol)]; - if (@available(macOS 13.0, *)) { - [connection setCodeSigningRequirement:codeSigningRequirement]; - } else { - // Intentionally left empty because the entire XPC flow will only be available on macOS 13 and above and gaurded through canPerformRequest + if ([self isXpcPlatformSupported]) + { + if (@available(macOS 13.0, *)) + { + [connection setCodeSigningRequirement:codeSigningRequirement]; + } + } + else + { + continueBlock(nil, nil, MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- unsupported platform for dispatcher connection", nil)); return; } - // Ensure that both the interruption handler and invalidation handler do not trigger unexpected dispatch_group_leave - // when the connection is unavailable or rejected by the XPC. This is achieved by adding a manual check. - __block BOOL isConnectionErroredOut = NO; + NSObject *dispatcherStateLock = [NSObject new]; + __block BOOL dispatcherReplyReceived = NO; + void (^markDispatcherReplyReceived)(void) = ^{ + @synchronized (dispatcherStateLock) + { + dispatcherReplyReceived = YES; + } + }; + BOOL (^isDispatcherReplyReceived)(void) = ^BOOL { + @synchronized (dispatcherStateLock) + { + return dispatcherReplyReceived; + } + }; + // Install handlers BEFORE resume so a synchronous failure cannot fire in the gap between // resume and handler installation. [connection setInterruptionHandler:^{ - NSError *xpcError = MSIDCreateError(MSIDErrorDomain, MSIDErrorBrokerXpcUnexpectedError, @"[Entra broker] CLIENT -- dispatcher connection is interrupted", nil, nil, nil, nil, nil, YES); - if (!isConnectionErroredOut && continueBlock) - { - isConnectionErroredOut = YES; - continueBlock(nil, nil, xpcError); - } + if (isDispatcherReplyReceived() || requestCompleted()) return; + + NSError *xpcError = MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- dispatcher connection is interrupted", nil); + if (continueBlock) continueBlock(nil, nil, xpcError); }]; [connection setInvalidationHandler:^{ - NSError *xpcError = MSIDCreateError(MSIDErrorDomain, MSIDErrorBrokerXpcUnexpectedError, @"[Entra broker] CLIENT -- dispatcher connection is invalidated", nil, nil, nil, nil, nil, YES); - if (!isConnectionErroredOut && continueBlock) - { - isConnectionErroredOut = YES; - continueBlock(nil, nil, xpcError); - return; - } + if (isDispatcherReplyReceived() || requestCompleted()) return; + + NSError *xpcError = MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- dispatcher connection is invalidated", nil); + if (continueBlock) continueBlock(nil, nil, xpcError); }]; [connection resume]; id parentXpcService = [connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) { + if (isDispatcherReplyReceived() || requestCompleted()) return; + MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"[Entra broker] CLIENT -- failed to connect to dispatcher, error: %@", error); + NSError *xpcError = MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- failed to connect to dispatcher", error); + if (continueBlock) continueBlock(nil, nil, xpcError); [connection invalidate]; }]; + if (!parentXpcService) + { + NSError *xpcError = MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- dispatcher proxy is unavailable", nil); + if (continueBlock) continueBlock(nil, nil, xpcError); + [connection invalidate]; + return; + } + + NSError *dispatcherTimeoutError = MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- dispatcher endpoint lookup timed out", nil); + [self scheduleBlock:^{ + if (isDispatcherReplyReceived() || requestCompleted()) return; + + if (continueBlock) continueBlock(nil, nil, dispatcherTimeoutError); + [connection invalidate]; + } afterTimeout:MSIDXpcDispatcherEndpointLookupTimeout]; [parentXpcService getBrokerInstanceEndpointWithReply:^(NSXPCListenerEndpoint * _Nullable listenerEndpoint, NSDictionary * _Nullable __unused params, NSError * _Nullable error) { + markDispatcherReplyReceived(); + if (requestCompleted()) + { + return; + } + [connection suspend]; - [connection invalidate]; + if (error) { - NSError *xpcUnexpectedError = MSIDCreateError(MSIDErrorDomain, MSIDErrorBrokerXpcUnexpectedError, [NSString stringWithFormat:@"[Entra broker] CLIENT - get broker instance endpoint failed: %@", error], nil, nil, nil, nil, nil, YES); + NSError *xpcUnexpectedError = MSIDXpcCreateTransportError(@"[Entra broker] CLIENT - get broker instance endpoint failed", error); + if (continueBlock) continueBlock(nil, nil, xpcUnexpectedError); + [connection invalidate]; + return; + } + + if (!listenerEndpoint) + { + NSError *xpcUnexpectedError = MSIDXpcCreateTransportError(@"[Entra broker] CLIENT - broker instance endpoint is unavailable", nil); if (continueBlock) continueBlock(nil, nil, xpcUnexpectedError); + [connection invalidate]; return; } + [connection invalidate]; MSID_LOG_WITH_CTX(MSIDLogLevelInfo, nil, @"[Entra broker] CLIENT - connected to new service endpoint %@", listenerEndpoint); // Populate cache with the freshly-issued endpoint. CAS against the providerType captured @@ -556,7 +826,14 @@ - (void)getXpcService:(id)xpcProviderCache withContinueB } } - NSXPCConnection *directConnection = [[NSXPCConnection alloc] initWithListenerEndpoint:listenerEndpoint]; + NSXPCConnection *directConnection = [self directConnectionWithEndpoint:listenerEndpoint]; + if (!directConnection) + { + NSError *xpcUnexpectedError = MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- direct connection is unavailable", nil); + if (continueBlock) continueBlock(nil, nil, xpcUnexpectedError); + return; + } + directConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(MSIDXpcBrokerInstanceProtocol)]; NSString *clientCodeSigningRequirement = [self codeSignRequirementForBundleId:xpcProviderCache.xpcConfiguration.xpcBrokerInstanceServiceBundleId devIdentity:[self signingIdentity]]; if ([NSString msidIsStringNilOrBlank:clientCodeSigningRequirement]) @@ -566,45 +843,43 @@ - (void)getXpcService:(id)xpcProviderCache withContinueB return; } - if (@available(macOS 13.0, *)) { - [directConnection setCodeSigningRequirement:clientCodeSigningRequirement]; - } else { - // This should not happen since the entry point has been guarded by version - MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"[Entra broker] CLIENT - fall into unsupported platform end XPC disconnect from service!", nil); + if ([self isXpcPlatformSupported]) + { + if (@available(macOS 13.0, *)) + { + [directConnection setCodeSigningRequirement:clientCodeSigningRequirement]; + } + } + else + { + NSError *xpcUnexpectedError = MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- unsupported platform for direct connection", nil); + if (continueBlock) continueBlock(nil, nil, xpcUnexpectedError); + return; } - - // Per-instance one-shot gate: interruption + invalidation can both fire on connection - // teardown; ensure continueBlock is only invoked once for connection failure. - __block BOOL instanceConnectionErroredOut = NO; // Install handlers BEFORE resume. [directConnection setInterruptionHandler:^{ - NSError *xpcError = MSIDCreateError(MSIDErrorDomain, MSIDErrorBrokerXpcUnexpectedError, @"[Entra broker] CLIENT -- instance connection is interrupted", nil, nil, nil, nil, nil, YES); - if (!instanceConnectionErroredOut && continueBlock) - { - instanceConnectionErroredOut = YES; - continueBlock(nil, nil, xpcError); - } + NSError *xpcError = MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- instance connection is interrupted", nil); + if (continueBlock) continueBlock(nil, nil, xpcError); }]; [directConnection setInvalidationHandler:^{ - NSError *xpcError = MSIDCreateError(MSIDErrorDomain, MSIDErrorBrokerXpcUnexpectedError, @"[Entra broker] CLIENT -- instance connection is invalidated", nil, nil, nil, nil, nil, YES); - if (!instanceConnectionErroredOut && continueBlock) - { - instanceConnectionErroredOut = YES; - continueBlock(nil, nil, xpcError); - } + NSError *xpcError = MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- instance connection is invalidated", nil); + if (continueBlock) continueBlock(nil, nil, xpcError); }]; id directService = [directConnection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull callbackError) { MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"[Entra broker] CLIENT -- failed to connect to instance, error: %@", callbackError); - if (!instanceConnectionErroredOut && continueBlock) - { - instanceConnectionErroredOut = YES; - continueBlock(nil, nil, callbackError); - } + if (continueBlock) continueBlock(nil, nil, callbackError); [directConnection invalidate]; }]; + if (!directService) + { + NSError *xpcUnexpectedError = MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- direct proxy is unavailable", nil); + if (continueBlock) continueBlock(nil, directConnection, xpcUnexpectedError); + [directConnection invalidate]; + return; + } [directConnection resume]; @@ -629,7 +904,19 @@ - (void)getXpcServiceFromCachedEndpoint:(NSXPCListenerEndpoint *)endpoint MSID_LOG_WITH_CTX(MSIDLogLevelInfo, context, @"[Entra broker] CLIENT - using cached XPC instance endpoint, skipping dispatcher round-trip"); - NSXPCConnection *directConnection = [[NSXPCConnection alloc] initWithListenerEndpoint:endpoint]; + if (!endpoint) + { + if (continueBlock) continueBlock(nil, nil, MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- cached instance endpoint is unavailable", nil)); + return; + } + + NSXPCConnection *directConnection = [self directConnectionWithEndpoint:endpoint]; + if (!directConnection) + { + if (continueBlock) continueBlock(nil, nil, MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- cached direct connection is unavailable", nil)); + return; + } + directConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(MSIDXpcBrokerInstanceProtocol)]; NSString *clientCodeSigningRequirement = [self codeSignRequirementForBundleId:xpcProviderCache.xpcConfiguration.xpcBrokerInstanceServiceBundleId devIdentity:[self signingIdentity]]; @@ -639,45 +926,44 @@ - (void)getXpcServiceFromCachedEndpoint:(NSXPCListenerEndpoint *)endpoint return; } - if (@available(macOS 13.0, *)) { - [directConnection setCodeSigningRequirement:clientCodeSigningRequirement]; - } else { + if ([self isXpcPlatformSupported]) + { + if (@available(macOS 13.0, *)) + { + [directConnection setCodeSigningRequirement:clientCodeSigningRequirement]; + } + } + else + { // Should not happen — XPC flow is gated to macOS 13+ via canPerformRequest:. if (continueBlock) continueBlock(nil, nil, MSIDCreateError(MSIDErrorDomain, MSIDErrorBrokerXpcUnexpectedError, @"[Entra broker] CLIENT -- unsupported platform for cached endpoint connection", nil, nil, nil, nil, nil, YES)); return; } - __block BOOL instanceConnectionErroredOut = NO; - // Install handlers BEFORE resume so a synchronous failure on a stale endpoint does not slip // through the gap. [directConnection setInterruptionHandler:^{ - NSError *xpcError = MSIDCreateError(MSIDErrorDomain, MSIDErrorBrokerXpcUnexpectedError, @"[Entra broker] CLIENT -- cached instance connection is interrupted", nil, nil, nil, nil, nil, YES); - if (!instanceConnectionErroredOut && continueBlock) - { - instanceConnectionErroredOut = YES; - continueBlock(nil, nil, xpcError); - } + NSError *xpcError = MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- cached instance connection is interrupted", nil); + if (continueBlock) continueBlock(nil, nil, xpcError); }]; [directConnection setInvalidationHandler:^{ - NSError *xpcError = MSIDCreateError(MSIDErrorDomain, MSIDErrorBrokerXpcUnexpectedError, @"[Entra broker] CLIENT -- cached instance connection is invalidated", nil, nil, nil, nil, nil, YES); - if (!instanceConnectionErroredOut && continueBlock) - { - instanceConnectionErroredOut = YES; - continueBlock(nil, nil, xpcError); - } + NSError *xpcError = MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- cached instance connection is invalidated", nil); + if (continueBlock) continueBlock(nil, nil, xpcError); }]; id directService = [directConnection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull callbackError) { MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"[Entra broker] CLIENT -- failed to connect to cached instance endpoint, error: %@", callbackError); - if (!instanceConnectionErroredOut && continueBlock) - { - instanceConnectionErroredOut = YES; - continueBlock(nil, nil, callbackError); - } + if (continueBlock) continueBlock(nil, nil, callbackError); [directConnection invalidate]; }]; + if (!directService) + { + NSError *xpcUnexpectedError = MSIDXpcCreateTransportError(@"[Entra broker] CLIENT -- cached direct proxy is unavailable", nil); + if (continueBlock) continueBlock(nil, directConnection, xpcUnexpectedError); + [directConnection invalidate]; + return; + } [directConnection resume]; diff --git a/IdentityCore/src/validation/MSIDAADAuthority.m b/IdentityCore/src/validation/MSIDAADAuthority.m index 68ed7627a9..6c14f735c3 100644 --- a/IdentityCore/src/validation/MSIDAADAuthority.m +++ b/IdentityCore/src/validation/MSIDAADAuthority.m @@ -35,6 +35,7 @@ #import "MSIDJsonSerializableFactory.h" #import "MSIDJsonSerializableTypes.h" #import "MSIDProviderType.h" +#import "MSIDAADNetworkConfiguration.h" @interface MSIDAADAuthority() @@ -251,6 +252,11 @@ - (BOOL)supportsBrokeredAuthentication return YES; } +- (BOOL)isAADAuthority +{ + return YES; +} + - (BOOL)supportsMAMScenarios { #if TARGET_OS_IPHONE @@ -356,11 +362,42 @@ + (MSIDAADTenant *)tenantFromAuthorityUrl:(NSURL *)url #pragma mark - Sovereign +- (BOOL)isRecognizedMicrosoftIdentityHost:(NSString *)host +{ + if ([NSString msidIsStringNilOrBlank:host]) return NO; + + NSString *lowercaseHost = host.lowercaseString; + + // A known AAD public/sovereign cloud host. + if ([MSIDAADNetworkConfiguration.defaultConfiguration isAADPublicCloud:lowercaseHost]) return YES; + + // Otherwise, a network environment already discovered via instance metadata. + // Host names are case-insensitive, but the cache may store preferred_network + // values without case normalization, so match case-insensitively. + for (NSString *cloudEnvironment in [MSIDAadAuthorityCache sharedInstance].allCloudNetworkEnvironments) + { + if ([cloudEnvironment caseInsensitiveCompare:lowercaseHost] == NSOrderedSame) return YES; + } + + return NO; +} + - (MSIDAuthority *)authorityWithUpdatedCloudHostInstanceName:(NSString *)cloudHostInstanceName error:(NSError *__autoreleasing*)error { if ([NSString msidIsStringNilOrBlank:cloudHostInstanceName]) return nil; - - NSURL *cloudAuthorityURL = [self.url msidAADAuthorityWithCloudInstanceHostname:cloudHostInstanceName]; + + NSString *lowercaseHostName = cloudHostInstanceName.lowercaseString; + + // Only build a cloud authority when the host is a recognized Microsoft identity + // host. This mirrors the check in setCloudAuthorityWithCloudHostName: so callers + // get consistent behavior. + if (![self isRecognizedMicrosoftIdentityHost:lowercaseHostName]) + { + MSID_LOG_WITH_CTX(MSIDLogLevelWarning, nil, @"Ignoring cloud_instance_host_name in authorityWithUpdatedCloudHostInstanceName: host is not a recognized Microsoft identity host."); + return nil; + } + + NSURL *cloudAuthorityURL = [self.url msidAADAuthorityWithCloudInstanceHostname:lowercaseHostName]; return [[MSIDAADAuthority alloc] initWithURL:cloudAuthorityURL context:nil error:error]; } diff --git a/IdentityCore/src/validation/MSIDAuthority+Internal.h b/IdentityCore/src/validation/MSIDAuthority+Internal.h index 65a2fac51a..dfc9a692f1 100644 --- a/IdentityCore/src/validation/MSIDAuthority+Internal.h +++ b/IdentityCore/src/validation/MSIDAuthority+Internal.h @@ -48,5 +48,13 @@ NS_ASSUME_NONNULL_BEGIN - (nullable MSIDAuthority *)authorityWithUpdatedCloudHostInstanceName:(NSString *)cloudHostInstanceName error:(NSError * _Nullable __autoreleasing * _Nullable)error; +/*! + Returns YES when @c host is a recognized identity host for this authority type. + The base implementation returns NO; authority types that support cloud host + rewriting (e.g. AAD) override this to validate against their known host list. + Matching is expected to be case-insensitive. Returns NO for nil or blank input. + */ +- (BOOL)isRecognizedMicrosoftIdentityHost:(nullable NSString *)host; + NS_ASSUME_NONNULL_END @end diff --git a/IdentityCore/src/validation/MSIDAuthority.h b/IdentityCore/src/validation/MSIDAuthority.h index 937213f80e..a1fdb82157 100644 --- a/IdentityCore/src/validation/MSIDAuthority.h +++ b/IdentityCore/src/validation/MSIDAuthority.h @@ -88,6 +88,10 @@ typedef void(^MSIDOpenIdConfigurationInfoBlock)(MSIDOpenIdProviderMetadata * _Nu - (BOOL)supportsBrokeredAuthentication; +// Whether this authority is an AAD authority (MSIDAADAuthority). Lets callers gate +// AAD-only behavior without depending on the concrete MSIDAADAuthority type. +- (BOOL)isAADAuthority; + // Only certain authorities support passing clientID as an allowed scope - (BOOL)supportsClientIDAsScope; diff --git a/IdentityCore/src/validation/MSIDAuthority.m b/IdentityCore/src/validation/MSIDAuthority.m index 83d256f075..78063a670b 100644 --- a/IdentityCore/src/validation/MSIDAuthority.m +++ b/IdentityCore/src/validation/MSIDAuthority.m @@ -182,6 +182,11 @@ - (BOOL)supportsBrokeredAuthentication return NO; } +- (BOOL)isAADAuthority +{ + return NO; +} + - (BOOL)excludeFromAuthorityValidation { return NO; @@ -367,6 +372,11 @@ - (MSIDAuthority *)authorityWithUpdatedCloudHostInstanceName:(__unused NSString return nil; } +- (BOOL)isRecognizedMicrosoftIdentityHost:(__unused NSString *)host +{ + return NO; +} + #pragma mark - MSIDJsonSerializable - (instancetype)initWithJSONDictionary:(NSDictionary *)json error:(NSError *__autoreleasing*)error diff --git a/IdentityCore/src/webview/MSIDWebviewConstants.h b/IdentityCore/src/webview/MSIDWebviewConstants.h index 1ed03202e8..4ba7a9db48 100644 --- a/IdentityCore/src/webview/MSIDWebviewConstants.h +++ b/IdentityCore/src/webview/MSIDWebviewConstants.h @@ -61,11 +61,13 @@ extern NSString * const MSID_ASWEBAUTH_HANDOFF_USE_EPHEMERAL_KEY; // x- extern NSString *const MSID_ASWEBAUTH_HANDOFF_REDIRECT_SCHEME_KEY; // @"x-ms-aswebauth-handoff-redirect-scheme"; extern NSString * const MSID_ASWEBAUTH_HANDOFF_INCLUDE_HEADERS_KEY; // x-ms-aswebauth-handoff-include-headers extern NSString * const MSID_ASWEBAUTH_HANDOFF_ATTACH_HEADERS_KEY; // x-ms-aswebauth-handoff-attach-headers +extern NSString * const MSID_ASWEBAUTH_HANDOFF_PURPOSE_KEY; // x-ms-aswebauth-handoff-purpose extern NSString * const MSID_ASWEBAUTH_HANDOFF_HEADER_PREFIX; // x-ms-aswebauth-handoff- // ASWebAuthentication handoff header values extern NSString * const MSID_ASWEBAUTH_HANDOFF_VALUE_TRUE; // "true" extern NSString * const MSID_ASWEBAUTH_HANDOFF_VALUE_FALSE; // "false" +extern NSString * const MSID_ASWEBAUTH_HANDOFF_PURPOSE_VALUE_DOWNLOAD_PROFILE; // "download-profile" #pragma mark - OpenID4VC bring-back query parameters diff --git a/IdentityCore/src/webview/MSIDWebviewConstants.m b/IdentityCore/src/webview/MSIDWebviewConstants.m index 366320facf..ff8b3385ce 100644 --- a/IdentityCore/src/webview/MSIDWebviewConstants.m +++ b/IdentityCore/src/webview/MSIDWebviewConstants.m @@ -59,11 +59,13 @@ NSString *const MSID_ASWEBAUTH_HANDOFF_REDIRECT_SCHEME_KEY = @"x-ms-aswebauth-handoff-redirect-scheme"; NSString *const MSID_ASWEBAUTH_HANDOFF_INCLUDE_HEADERS_KEY = @"x-ms-aswebauth-handoff-include-headers"; NSString *const MSID_ASWEBAUTH_HANDOFF_ATTACH_HEADERS_KEY = @"x-ms-aswebauth-handoff-attach-headers"; +NSString *const MSID_ASWEBAUTH_HANDOFF_PURPOSE_KEY = @"x-ms-aswebauth-handoff-purpose"; NSString *const MSID_ASWEBAUTH_HANDOFF_HEADER_PREFIX = @"x-ms-aswebauth-handoff-"; // ASWebAuthentication handoff header values NSString *const MSID_ASWEBAUTH_HANDOFF_VALUE_TRUE = @"true"; NSString *const MSID_ASWEBAUTH_HANDOFF_VALUE_FALSE = @"false"; +NSString *const MSID_ASWEBAUTH_HANDOFF_PURPOSE_VALUE_DOWNLOAD_PROFILE = @"download-profile"; #pragma mark - OpenID4VC bring-back query parameters diff --git a/IdentityCore/src/webview/embeddedWebview/MSIDAADOAuthEmbeddedWebviewController.m b/IdentityCore/src/webview/embeddedWebview/MSIDAADOAuthEmbeddedWebviewController.m index 1924ce0442..a9cfcfb85f 100644 --- a/IdentityCore/src/webview/embeddedWebview/MSIDAADOAuthEmbeddedWebviewController.m +++ b/IdentityCore/src/webview/embeddedWebview/MSIDAADOAuthEmbeddedWebviewController.m @@ -39,6 +39,8 @@ #import "NSURL+MSIDExtensions.h" #import "NSString+MSIDExtensions.h" #import "MSIDInteractiveRequestParameters.h" +#import "MSIDOnboardingBlobBuilder.h" +#import "MSIDOnboardingBlobFieldKeys.h" #if !MSID_EXCLUDE_WEBKIT @@ -124,6 +126,9 @@ - (BOOL)decidePolicyAADForNavigationAction:(WKNavigationAction *)navigationActio MSID_LOG_WITH_CTX(MSIDLogLevelInfo, self.context, @"Server issued msauth://enroll - enabling mobile onboarding for this session."); interactiveRequestParameters.isNewMobileOnboardingFlow = YES; + + // Tag the blob with the new mobile onboarding UX (nil-safe). + [self.onboardingBlobBuilder addUxFlowUsed:MSIDOnboardingUxFlowMobileOnboardingPhase1]; } else { @@ -142,6 +147,8 @@ - (BOOL)decidePolicyAADForNavigationAction:(WKNavigationAction *)navigationActio if (legacyBrowserURL) { + [self.onboardingBlobBuilder addStep:MSIDOnboardingBlobStepMobileOnboardingClientFlightDisabledLegacyFallback + timestamp:[NSDate date]]; MSID_LOG_WITH_CTX(MSIDLogLevelInfo, self.context, @"Mobile onboarding disabled on client; falling back to legacy " @"flow by opening intuneRedirectUrl via browser:// scheme."); diff --git a/IdentityCore/src/webview/embeddedWebview/MSIDOAuth2EmbeddedWebviewController.h b/IdentityCore/src/webview/embeddedWebview/MSIDOAuth2EmbeddedWebviewController.h index d471210e1a..e38c323a79 100644 --- a/IdentityCore/src/webview/embeddedWebview/MSIDOAuth2EmbeddedWebviewController.h +++ b/IdentityCore/src/webview/embeddedWebview/MSIDOAuth2EmbeddedWebviewController.h @@ -74,9 +74,6 @@ typedef NSURLRequest *(^MSIDExternalDecidePolicyForBrowserActionBlock)(MSIDOAuth requestURL:(NSURL *)requestURL error:(NSError *)error; -- (void)finalizeOnboardingTelemetry:(NSURL *)endURL - error:(NSError *)error; - @property (atomic, readonly) NSURL *startURL; @property (atomic, readonly) NSURL *endURL; @property (nonatomic, readonly) NSDictionary *customHeaders; @@ -94,8 +91,6 @@ typedef NSURLRequest *(^MSIDExternalDecidePolicyForBrowserActionBlock)(MSIDOAuth // Readonly flags exposing whether each remediation step has been recorded against // the current onboarding blob builder. Subclasses use these to decide whether to // emit matching completion steps when the flow ends successfully. -@property (nonatomic, readonly) BOOL onboardingStrongAuthSetupStarted; -@property (nonatomic, readonly) BOOL onboardingMdmEnrollmentStarted; @property (nonatomic, readonly) BOOL onboardingDeviceRegistrationStarted; @property (nonatomic, readonly) BOOL onboardingRemediationStarted; diff --git a/IdentityCore/src/webview/embeddedWebview/MSIDOAuth2EmbeddedWebviewController.m b/IdentityCore/src/webview/embeddedWebview/MSIDOAuth2EmbeddedWebviewController.m index b351397d06..a1df5df406 100644 --- a/IdentityCore/src/webview/embeddedWebview/MSIDOAuth2EmbeddedWebviewController.m +++ b/IdentityCore/src/webview/embeddedWebview/MSIDOAuth2EmbeddedWebviewController.m @@ -70,8 +70,6 @@ @implementation MSIDOAuth2EmbeddedWebviewController } // Backed by readonly properties declared in the public header. -@synthesize onboardingStrongAuthSetupStarted = _onboardingStrongAuthSetupStarted; -@synthesize onboardingMdmEnrollmentStarted = _onboardingMdmEnrollmentStarted; @synthesize endURL = _endURL; #if AD_BROKER @@ -223,7 +221,7 @@ - (void)endWebAuthWithURL:(NSURL *)endURL // Record the terminal onboarding step on the shared builder if (_onboardingBlobBuilder && [MSIDWebAuthNUtil amIRunningInExtension]) { - [self finalizeOnboardingTelemetry:endURL error:error]; + [_onboardingBlobBuilder finalizeForEndURL:endURL error:error]; _onboardingBlobBuilder = nil; } @@ -373,55 +371,62 @@ - (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAut - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { + WKNavigationResponsePolicy responsePolicy = WKNavigationResponsePolicyAllow; + + NSHTTPURLResponse *response = nil; if (navigationResponse && [navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) { - NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response; - - [self processOnboardingTelemetryForResponse:response]; - - if (self.navigationResponseBlock) - { - self.navigationResponseBlock(response); - } + response = (NSHTTPURLResponse *)navigationResponse.response; } - - WKNavigationResponsePolicy responsePolicy = WKNavigationResponsePolicyAllow; - id contextObject = self.context; - MSIDInteractiveRequestParameters *interactiveRequestParameters = - [contextObject isKindOfClass:[MSIDInteractiveRequestParameters class]] - ? (MSIDInteractiveRequestParameters *)contextObject : nil; - - if (interactiveRequestParameters.isNewMobileOnboardingFlow) + if (response) { - id strongNavigationDelegate = self.navigationDelegate; - if ((strongNavigationDelegate) - && [strongNavigationDelegate respondsToSelector:@selector(processResponseHeadersAndCheckForASWebAuthHandoff:responseURL:)] - && [navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) - { - NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response; + id contextObject = self.context; + MSIDInteractiveRequestParameters *interactiveRequestParameters = + [contextObject isKindOfClass:[MSIDInteractiveRequestParameters class]] + ? (MSIDInteractiveRequestParameters *)contextObject : nil; - // Process the response headers and determine if a hand-off to ASWebAuthenticationSession is signaled. - // The response URL is passed so the delegate can verify the issuing origin is allowed (HTTPS + allowlisted host) - // before honoring an ASWebAuth header. - BOOL didHandoff = [strongNavigationDelegate processResponseHeadersAndCheckForASWebAuthHandoff:response.allHeaderFields - responseURL:response.URL]; + if (interactiveRequestParameters.isNewMobileOnboardingFlow) + { + // In the new onboarding flow, the navigation delegate processes telemetry + // and checks for ASWebAuthenticationSession hand-off in a single call. + id strongNavigationDelegate = self.navigationDelegate; + if ((strongNavigationDelegate) + && [strongNavigationDelegate respondsToSelector:@selector(processNavigationResponseAndCheckForASWebAuthHandoff:embeddedWebviewController:)]) + { + BOOL didHandoff = [strongNavigationDelegate processNavigationResponseAndCheckForASWebAuthHandoff:response + embeddedWebviewController:self]; #if !MSID_EXCLUDE_SYSTEMWV - // If a hand-off is signaled, and the navigation delegate implements the hand-off method, perform the hand-off to ASWebAuthenticationSession and cancel the current navigation. - if (didHandoff - && [strongNavigationDelegate respondsToSelector:@selector(performASWebAuthenticationHandoffWithCompletion:)]) - { - NSURL *responseURL = response.URL; - responsePolicy = WKNavigationResponsePolicyCancel; - [strongNavigationDelegate performASWebAuthenticationHandoffWithCompletion:^(MSIDWebviewNavigationDecision *decision, NSError *error) + // If a hand-off is signaled, and the navigation delegate implements the hand-off method, perform the hand-off to ASWebAuthenticationSession and cancel the current navigation. + if (didHandoff + && [strongNavigationDelegate respondsToSelector:@selector(performASWebAuthenticationHandoffWithCompletion:)]) { - [self performNavigationDecision:decision - requestURL:responseURL - error:error]; - }]; - } + NSURL *responseURL = response.URL; + responsePolicy = WKNavigationResponsePolicyCancel; + [strongNavigationDelegate performASWebAuthenticationHandoffWithCompletion:^(MSIDWebviewNavigationDecision *decision, NSError *error) + { + [self performNavigationDecision:decision + requestURL:responseURL + error:error]; + }]; + } #endif // !MSID_EXCLUDE_SYSTEMWV + } + } + else + { + // Legacy flow: process onboarding telemetry locally. + MSIDOnboardingBlobBuilder *builder = self.onboardingBlobBuilder; + if (builder && response) + { + [builder processResponseHeaders:response.allHeaderFields responseURL:response.URL]; + } + } + + if (self.navigationResponseBlock) + { + self.navigationResponseBlock(response); } } @@ -782,208 +787,6 @@ - (void)performNavigationDecision:(MSIDWebviewNavigationDecision *)navigationDec }]; } -#pragma mark - Onboarding telemetry - -- (void)finalizeOnboardingTelemetry:(NSURL *)endURL - error:(NSError *)error -{ - MSIDOnboardingBlobBuilder *onboardingBlobBuilder = self.onboardingBlobBuilder; - if (onboardingBlobBuilder) - { - BOOL flowSucceeded = (endURL != nil && error == nil); - if (flowSucceeded) - { - NSDate *now = [NSDate date]; - if (_onboardingStrongAuthSetupStarted) - { - [onboardingBlobBuilder addStep:MSIDOnboardingBlobStepStrongAuthSetupCompleted timestamp:now]; - } - if (_onboardingMdmEnrollmentStarted) - { - [onboardingBlobBuilder addStep:MSIDOnboardingBlobStepMdmEnrollmentFinished timestamp:now]; - } - } - - NSString *endUrlStep = [self onboardingStepForEndURL:endURL]; - if (endUrlStep) - { - [onboardingBlobBuilder addStep:endUrlStep timestamp:[NSDate date]]; - } - } -} - -// Maps a terminal endURL that points at a well-known go.microsoft.com fwlink -// (browser://go.microsoft.com/fwlink[/]?...LinkId=...) to the onboarding -// step that should be recorded against the current blob. The LinkId-to-step -// map is the single extension point: new LinkIds (potentially mapping to a -// different step) just add entries here. -- (NSString *)onboardingStepForEndURL:(NSURL *)endURL -{ - if (!endURL) - { - return nil; - } - - NSURLComponents *components = [NSURLComponents componentsWithURL:endURL resolvingAgainstBaseURL:NO]; - if (!components) - { - return nil; - } - - if ([components.scheme caseInsensitiveCompare:@"browser"] != NSOrderedSame) - { - return nil; - } - - if ([components.host caseInsensitiveCompare:@"go.microsoft.com"] != NSOrderedSame) - { - return nil; - } - - NSString *path = components.path; - if ([path caseInsensitiveCompare:@"/fwlink"] != NSOrderedSame - && [path caseInsensitiveCompare:@"/fwlink/"] != NSOrderedSame) - { - return nil; - } - - NSString *linkIdValue = nil; - for (NSURLQueryItem *item in components.queryItems) - { - if ([item.name caseInsensitiveCompare:@"LinkId"] == NSOrderedSame) - { - linkIdValue = item.value; - break; - } - } - - if (linkIdValue.length == 0) - { - return nil; - } - - return [[self.class onboardingStepsByFwlinkLinkId] objectForKey:linkIdValue]; -} - -// LinkId value -> onboarding step constant. Extension point: future LinkIds -// (which may map to a different onboarding step) are added here. -+ (NSDictionary *)onboardingStepsByFwlinkLinkId -{ - static NSDictionary *map = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - map = @{ - @"396941" : MSIDOnboardingBlobStepMdmEnrollmentStarted, // Public - @"2132314" : MSIDOnboardingBlobStepMdmEnrollmentStarted, // China - @"2114747" : MSIDOnboardingBlobStepMdmEnrollmentStarted, // GOV - @"399153" : MSIDOnboardingBlobStepMdmEnrollmentStarted, // PPE - }; - }); - return map; -} - -- (void)processOnboardingTelemetryForResponse:(NSHTTPURLResponse *)response -{ - MSIDOnboardingBlobBuilder *builder = self.onboardingBlobBuilder; - if (!builder || !response) - { - return; - } - - NSString *host = response.URL.host; - if (host.length > 0) - { - [builder setLastLoadedDomain:host]; - } - - NSString *cliTelem = response.allHeaderFields[MSID_OAUTH2_CLIENT_TELEMETRY]; - if ([NSString msidIsStringNilOrBlank:cliTelem]) - { - return; - } - - // Format: ,,,, - NSArray *components = [cliTelem componentsSeparatedByString:@","]; - if (components.count < 2) - { - return; - } - - NSString *errorCode = [components[1] msidTrimmedString]; - if (errorCode.length == 0 || [errorCode isEqualToString:@"0"]) - { - return; - } - - // Check list of expected errors to ignore as part of normal sign-in flow. - if ([[self.class nonBlockingOnboardingErrorCodes] containsObject:errorCode]) - { - return; - } - - [builder addBlockingError:errorCode]; - [self recordOnboardingRemediationStepForErrorCode:errorCode builder:builder]; -} - -// Error codes that are returned during normal sign-in flow and should not be -// treated as blocking onboarding errors (e.g. user not signed in, wrong password, -// device auth interrupt). This list will be extended over time. -// 50058 UserInformationNotProvided - User not signed in / no valid SSO session found -// 50097 DeviceAuthenticationRequired - Device auth interrupt triggered by CA policy -// 50126 InvalidUserNameOrPassword - Wrong username or password -+ (NSSet *)nonBlockingOnboardingErrorCodes -{ - static NSSet *codes = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - codes = [NSSet setWithObjects:@"50058", @"50097", @"50126", nil]; - }); - return codes; -} - -- (void)recordOnboardingRemediationStepForErrorCode:(NSString *)errorCode - builder:(MSIDOnboardingBlobBuilder *)builder -{ - NSDate *now = [NSDate date]; - - // 50079: Strong auth enrollment needed (MFA setup, not MFA fulfillment like 50076/50078) - if ([errorCode isEqualToString:@"50079"] && !_onboardingStrongAuthSetupStarted) - { - [builder addStep:MSIDOnboardingBlobStepStrongAuthSetupStarted timestamp:now]; - _onboardingStrongAuthSetupStarted = YES; - } - // 50129 (DeviceIsNotWorkplaceJoined): Device registration needed, - // 501291 (DeviceIsNotWorkplaceJoinedForMamApp): Device registration needed for MAM app - else if ([errorCode isEqualToString:@"50129"] || [errorCode isEqualToString:@"501291"]) - { - [builder addStep:MSIDOnboardingBlobStepDeviceRegistrationRequired timestamp:now]; - } - // 530001 (DeviceNotCompliantBrowserNotSupported): Browser not supported, - // 530002: (DeviceNotCompliantDeviceCompliantRequired): The device is required to be compliant to access this resource - else if ([errorCode isEqualToString:@"530001"] || [errorCode isEqualToString:@"530002"]) - { - [builder addStep:MSIDOnboardingBlobStepDeviceNotCompliant timestamp:now]; - } - // 53000 (DeviceNotCompliant): The user must enroll their device with an approved MDM provider like Intune, - // 530003 (DeviceNotCompliantDeviceManagementRequired): MDM enrollment required - else if ([errorCode isEqualToString:@"53000"] || [errorCode isEqualToString:@"530003"]) - { - [builder addStep:MSIDOnboardingBlobStepMdmEnrollmentRequired timestamp:now]; - } - // 50127: Client app is a MAM app and device is not registered - else if ([errorCode isEqualToString:@"50127"]) - { - [builder addStep:MSIDOnboardingBlobStepBrokerInstallPromptedForMAM timestamp:now]; - } - // 501271: Broker app needs to be installed for device authentication to succeed. - else if ([errorCode isEqualToString:@"501271"]) - { - [builder addStep:MSIDOnboardingBlobStepBrokerInstallPrompted timestamp:now]; - } - - // All other error codes (50076, 50078, 53005, 53003, etc.): blocking error only, no step -} - @end #endif diff --git a/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationDecisionResolver.h b/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationDecisionResolver.h index 5e66b094f2..18f39bc321 100644 --- a/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationDecisionResolver.h +++ b/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationDecisionResolver.h @@ -62,10 +62,12 @@ NS_ASSUME_NONNULL_BEGIN * * @param URL The special redirect URL to resolve (msauth://, browser://, etc.). If nil or missing a scheme, returns a failWithError decision. * @param embeddedWebviewController The webview controller handling the navigation. May be nil for flows that do not require it. - * @return Navigation decision to apply, or nil if the URL cannot be processed + * @param additionalHeaders Extra headers to merge onto the MDM enrollment request. The broker flow supplies the broker version (x-client-brkrver); the non-broker flow supplies the running process's first-party app-identity headers. Pass nil to add none. + * @return Navigation decision to apply. Always non-nil: unhandled schemes resolve to continueDefault and invalid/unprocessable URLs resolve to a failWithError decision. */ - (MSIDWebviewNavigationDecision * _Nullable)resolveDecisionForURL:(NSURL * _Nullable)URL - embeddedWebviewController:(MSIDOAuth2EmbeddedWebviewController * _Nullable)embeddedWebviewController; + embeddedWebviewController:(MSIDOAuth2EmbeddedWebviewController * _Nullable)embeddedWebviewController + additionalHeaders:(NSDictionary * _Nullable)additionalHeaders; @end NS_ASSUME_NONNULL_END diff --git a/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationDecisionResolver.m b/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationDecisionResolver.m index 71569b085f..37273cb845 100644 --- a/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationDecisionResolver.m +++ b/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationDecisionResolver.m @@ -28,7 +28,11 @@ #import "MSIDSSOExtensionInteractiveTokenRequestController.h" #import "MSIDConstants.h" #import "MSIDIntuneDeviceIdCache.h" +#import "MSIDOnboardingBlobFieldKeys.h" #import "MSIDVersion.h" +#import "MSIDUXCallbackProvider.h" +#import "MSIDOnboardingBlobBuilder.h" +#import "MSIDOAuth2EmbeddedWebviewController.h" #if !MSID_EXCLUDE_WEBKIT @@ -48,8 +52,8 @@ + (instancetype)sharedInstance - (MSIDWebviewNavigationDecision * _Nullable)resolveDecisionForURL:(NSURL * _Nullable)URL embeddedWebviewController:(MSIDOAuth2EmbeddedWebviewController * _Nullable)embeddedWebviewController + additionalHeaders:(NSDictionary * _Nullable)additionalHeaders { - // Validate required parameters if (!URL) { MSID_LOG_WITH_CTX(MSIDLogLevelWarning, nil, @"[NavDecision] Cannot resolve: URL is nil."); @@ -79,7 +83,8 @@ - (MSIDWebviewNavigationDecision * _Nullable)resolveDecisionForURL:(NSURL * _Nul { // Handle msauth:// URLs return [self handleMSAuthURL:URL - embeddedWebviewController:embeddedWebviewController]; + embeddedWebviewController:embeddedWebviewController + callerHeaders:additionalHeaders]; } else if ([scheme isEqualToString:MSID_SCHEME_BROWSER]) { @@ -99,6 +104,7 @@ - (MSIDWebviewNavigationDecision * _Nullable)resolveDecisionForURL:(NSURL * _Nul - (MSIDWebviewNavigationDecision *)handleMSAuthURL:(NSURL *)URL embeddedWebviewController:(MSIDOAuth2EmbeddedWebviewController * _Nullable)embeddedWebviewController + callerHeaders:(NSDictionary * _Nullable)callerHeaders { NSString *host = URL.host.lowercaseString; @@ -115,17 +121,19 @@ - (MSIDWebviewNavigationDecision *)handleMSAuthURL:(NSURL *)URL // Parse query parameters NSDictionary *params = [URL msidQueryParameters]; - MSID_LOG_WITH_CTX(MSIDLogLevelInfo, nil, @"[NavDecision] Resolving decision for msauth host '%@'.", host); // Route based on host if ([host isEqualToString:MSID_MDM_ENROLL_HOST]) { - return [self decisionForEnrollURL:params]; + return [self decisionForEnrollURL:params + embeddedWebviewController:embeddedWebviewController + callerHeaders:callerHeaders]; } else if ([host isEqualToString:MSID_MDM_PROFILE_DOWNLOAD_COMPLETE_HOST]) { - return [self decisionForProfileDownloadComplete:params]; + return [self decisionForProfileDownloadComplete:params + embeddedWebviewController:embeddedWebviewController]; } else if ([host isEqualToString:MSID_COMPLIANCE_HOST]) { @@ -135,7 +143,8 @@ - (MSIDWebviewNavigationDecision *)handleMSAuthURL:(NSURL *)URL else if ([host isEqualToString:MSID_MDM_ENROLLMENT_COMPLETION_HOST]) { return [self decisionForEnrollmentCompletionURL:URL - params:params]; + params:params + embeddedWebviewController:embeddedWebviewController]; } else { @@ -148,7 +157,10 @@ - (MSIDWebviewNavigationDecision *)handleMSAuthURL:(NSURL *)URL #pragma mark - URL Decision Resolvers - (MSIDWebviewNavigationDecision *)decisionForEnrollURL:(NSDictionary *)params + embeddedWebviewController:(MSIDOAuth2EmbeddedWebviewController * _Nullable)embeddedWebviewController + callerHeaders:(NSDictionary * _Nullable)additionalHeaders { + MSIDOnboardingBlobBuilder *onboardingBlobBuilder = embeddedWebviewController.onboardingBlobBuilder; MSID_LOG_WITH_CTX(MSIDLogLevelInfo, nil, @"[Enroll] Building enrollment request from msauth redirect."); NSCharacterSet *whitespace = [NSCharacterSet whitespaceAndNewlineCharacterSet]; @@ -157,6 +169,7 @@ - (MSIDWebviewNavigationDecision *)decisionForEnrollURL:(NSDictionary *)params NSString *intuneURLString = [params[MSID_INTUNE_URL_KEY] stringByTrimmingCharactersInSet:whitespace]; if (intuneURLString.length == 0) { + [onboardingBlobBuilder addStep:MSIDOnboardingBlobStepMdmEnrollmentUrlMissing timestamp:[NSDate date]]; MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"[Enroll] Missing required intuneUrl parameter in msauth enrollment URL."); NSError *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, @@ -215,27 +228,35 @@ - (MSIDWebviewNavigationDecision *)decisionForEnrollURL:(NSDictionary *)params } // Prepare additional headers for enrollment. - NSMutableDictionary *additionalHeaders = [NSMutableDictionary dictionary]; + NSMutableDictionary *headers = [NSMutableDictionary dictionary]; + + // Merge caller-supplied headers first, then stamp the SDK-controlled values so they + // are authoritative and cannot be overridden by a caller. + if (additionalHeaders.count > 0) + { + [headers addEntriesFromDictionary:additionalHeaders]; + } NSString *platformName = [MSIDVersion platformName]; if (platformName.length > 0) { - additionalHeaders[MSID_PLATFORM_KEY] = platformName; + headers[MSID_PLATFORM_KEY] = platformName; } NSString *sdkVersion = [MSIDVersion sdkVersion]; if (sdkVersion.length > 0) { - additionalHeaders[MSID_VERSION_KEY] = sdkVersion; + headers[MSID_VERSION_KEY] = sdkVersion; } // Build the final request with all query params and headers. NSURLRequest *request = [self buildRequestForURL:decodedIntuneURL - extraHeaders:additionalHeaders + extraHeaders:headers extraParams:allQueryParams]; if (!request) { + [onboardingBlobBuilder addStep:MSIDOnboardingBlobStepMdmEnrollmentRequestMalformed timestamp:[NSDate date]]; MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"[Enroll] Failed to build enrollment request from intuneUrl: %@", MSID_PII_LOG_MASKABLE(decodedIntuneURL)); NSError *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, @@ -244,12 +265,15 @@ - (MSIDWebviewNavigationDecision *)decisionForEnrollURL:(NSDictionary *)params return [MSIDWebviewNavigationDecision failWithError:error]; } + [onboardingBlobBuilder addStep:MSIDOnboardingBlobStepMdmEnrollmentStarted timestamp:[NSDate date]]; MSID_LOG_WITH_CTX(MSIDLogLevelInfo, nil, @"[Enroll] Built enrollment request for host '%@'.", request.URL.host); return [MSIDWebviewNavigationDecision loadRequest:request]; } - (MSIDWebviewNavigationDecision *)decisionForProfileDownloadComplete:(NSDictionary *)params + embeddedWebviewController:(MSIDOAuth2EmbeddedWebviewController * _Nullable)embeddedWebviewController { + MSIDOnboardingBlobBuilder *onboardingBlobBuilder = embeddedWebviewController.onboardingBlobBuilder; MSID_LOG_WITH_CTX(MSIDLogLevelInfo, nil, @"[ProfileDownload] Processing MDM profile download completion redirect."); NSCharacterSet *whitespace = [NSCharacterSet whitespaceAndNewlineCharacterSet]; @@ -284,6 +308,7 @@ - (MSIDWebviewNavigationDecision *)decisionForProfileDownloadComplete:(NSDiction NSString *profileInstallURL = [params[MSID_INTUNE_PROFILE_INSTALL_URL_KEY] stringByTrimmingCharactersInSet:whitespace]; if (profileInstallURL.length == 0) { + [onboardingBlobBuilder addStep:MSIDOnboardingBlobStepProfileInstallUrlMissing timestamp:[NSDate date]]; MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"[ProfileDownload] Missing required profile install URL in profile download completion redirect."); NSError *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, @@ -305,6 +330,7 @@ - (MSIDWebviewNavigationDecision *)decisionForProfileDownloadComplete:(NSDiction NSURL *profileURL = [NSURL URLWithString:decodedProfileInstallURL]; if (!profileURL || profileURL.scheme.length == 0 || profileURL.host.length == 0) { + [onboardingBlobBuilder addStep:MSIDOnboardingBlobStepProfileInstallUrlMalformed timestamp:[NSDate date]]; MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"[ProfileDownload] Profile install URL is malformed (missing scheme or host). URL: %@", MSID_PII_LOG_MASKABLE(decodedProfileInstallURL)); @@ -316,15 +342,30 @@ - (MSIDWebviewNavigationDecision *)decisionForProfileDownloadComplete:(NSDiction } MSID_LOG_WITH_CTX(MSIDLogLevelInfo, nil, @"[ProfileDownload] Built profile install request for host '%@'.", profileURL.host); + + // The MDM profile-installed reminder is scheduled earlier, in + // MSIDWebviewNavigationHandler, at the ASWebAuthenticationSession hand-off launch + // (before the user leaves for Settings). Here we only record that the profile + // download itself completed and the install redirect returned. + [onboardingBlobBuilder addStep:MSIDOnboardingBlobStepProfileDownloadCompleted timestamp:[NSDate date]]; return [MSIDWebviewNavigationDecision loadRequest:[NSURLRequest requestWithURL:profileURL]]; } - (MSIDWebviewNavigationDecision *)decisionForEnrollmentCompletionURL:(NSURL *)URL params:(NSDictionary *)params + embeddedWebviewController:(MSIDOAuth2EmbeddedWebviewController * _Nullable)embeddedWebviewController { + MSIDOnboardingBlobBuilder *onboardingBlobBuilder = embeddedWebviewController.onboardingBlobBuilder; MSID_LOG_WITH_CTX(MSIDLogLevelInfo, nil, @"[EnrollmentCompletion] Processing enrollment completion redirect."); + // Cancel any previously scheduled MDM profile installed notification + id provider = MSIDUXCallbackProvider.uxCallbackProvider; + if ([provider respondsToSelector:@selector(cancelMDMProfileInstalledNotification)]) + { + [provider cancelMDMProfileInstalledNotification]; + } + // Check if SSO extension can perform request if ([MSIDSSOExtensionInteractiveTokenRequestController canPerformRequest]) { @@ -333,6 +374,7 @@ - (MSIDWebviewNavigationDecision *)decisionForEnrollmentCompletionURL:(NSURL *)U } // SSO extension not available - load error URL if provided. + [onboardingBlobBuilder addStep:MSIDOnboardingBlobStepSSOExtensionUnavailable timestamp:[NSDate date]]; MSID_LOG_WITH_CTX(MSIDLogLevelWarning, nil, @"[EnrollmentCompletion] SSO extension is not available; attempting fallback error URL."); NSString *errorUrlString = [params[MSID_MDM_ENROLLMENT_COMPLETION_ERROR_URL_KEY] @@ -351,6 +393,7 @@ - (MSIDWebviewNavigationDecision *)decisionForEnrollmentCompletionURL:(NSURL *)U NSURL *errorURL = [NSURL URLWithString:decodedErrorUrlString]; if (errorURL) { + [onboardingBlobBuilder addStep:MSIDOnboardingBlobStepMdmEnrollmentCompletionRetryStarted timestamp:[NSDate date]]; MSID_LOG_WITH_CTX(MSIDLogLevelInfo, nil, @"[EnrollmentCompletion] Loading fallback error URL in webview (host: '%@').", errorURL.host); return [MSIDWebviewNavigationDecision loadRequest:[NSURLRequest requestWithURL:errorURL]]; } @@ -373,6 +416,8 @@ - (MSIDWebviewNavigationDecision *)decisionForEnrollmentCompletionURL:(NSURL *)U - (MSIDWebviewNavigationDecision *)decisionForComplianceURL:(NSDictionary *)params embeddedWebviewController:(MSIDOAuth2EmbeddedWebviewController * _Nullable)embeddedWebviewController { + MSIDOnboardingBlobBuilder *onboardingBlobBuilder = embeddedWebviewController.onboardingBlobBuilder; + [onboardingBlobBuilder addStep:MSIDOnboardingBlobStepComplianceRemediationMSAuthRedirect timestamp:[NSDate date]]; MSID_LOG_WITH_CTX(MSIDLogLevelInfo, nil, @"[Compliance] Building compliance request from msauth redirect."); NSCharacterSet *whitespace = [NSCharacterSet whitespaceAndNewlineCharacterSet]; @@ -381,6 +426,7 @@ - (MSIDWebviewNavigationDecision *)decisionForComplianceURL:(NSDictionary *)para NSString *intuneURLString = [params[MSID_INTUNE_URL_KEY] stringByTrimmingCharactersInSet:whitespace]; if (intuneURLString.length == 0) { + [onboardingBlobBuilder addStep:MSIDOnboardingBlobStepComplianceRemediationUrlMissing timestamp:[NSDate date]]; MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"[Compliance] Missing required intuneUrl parameter in msauth compliance URL."); NSError *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, @@ -414,6 +460,7 @@ - (MSIDWebviewNavigationDecision *)decisionForComplianceURL:(NSDictionary *)para if (!request) { + [onboardingBlobBuilder addStep:MSIDOnboardingBlobStepComplianceRemediationRequestMalformed timestamp:[NSDate date]]; MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"[Compliance] Failed to build compliance request from intuneUrl: %@", MSID_PII_LOG_MASKABLE(decodedIntuneURL)); NSError *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, @@ -518,6 +565,7 @@ - (nullable NSURLRequest *)buildRequestForURL:(NSString *)URLString return request; } + @end #endif // !MSID_EXCLUDE_WEBKIT diff --git a/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationDelegate.h b/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationDelegate.h index 54b544d1f5..5a539c96ee 100644 --- a/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationDelegate.h +++ b/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationDelegate.h @@ -51,23 +51,25 @@ NS_ASSUME_NONNULL_BEGIN completion:(void (^)(MSIDWebviewNavigationDecision * _Nullable navigationDecision, NSError * _Nullable error))completion; /** - * Caches response headers and detects an ASWebAuthenticationSession hand-off signal. + * Caches response headers, processes onboarding telemetry, and detects an + * ASWebAuthenticationSession hand-off signal. * On YES, caller should cancel the WKWebView navigation and invoke the hand-off method below. * - * Security: hand-off is honored only when @c responseURL is HTTPS and its host is on the allowlist. + * Security: hand-off is honored only when the response URL is HTTPS and its host is on the allowlist. * - * @param headers HTTP response headers (raw `allHeaderFields`) - * @param responseURL URL of the response that delivered @c headers (@c NSHTTPURLResponse.URL). - * @return YES if @c headers signal a hand-off AND @c responseURL is from allowed origin; NO otherwise. + * @param response The HTTP navigation response containing headers and URL. + * @param embeddedWebviewController The controller that drove the navigation, passed explicitly + * so the delegate does not have to reach back through shared state. + * @return YES if headers signal a hand-off AND the response URL is from allowed origin; NO otherwise. */ -- (BOOL)processResponseHeadersAndCheckForASWebAuthHandoff:(NSDictionary *)headers - responseURL:(nullable NSURL *)responseURL; +- (BOOL)processNavigationResponseAndCheckForASWebAuthHandoff:(NSHTTPURLResponse *)response + embeddedWebviewController:(nullable MSIDOAuth2EmbeddedWebviewController *)embeddedWebviewController; #if !MSID_EXCLUDE_SYSTEMWV /** * Performs the hand-off using the headers cached by the last - * @c processResponseHeadersAndCheckForASWebAuthHandoff:responseURL: call. + * @c processNavigationResponseAndCheckForASWebAuthHandoff:embeddedWebviewController: call. * * @param completion Completion block - MUST be called exactly once; failures surface as a @c failWithError decision */ diff --git a/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationHandler.h b/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationHandler.h index 8fe0131b2b..b017b95ad4 100644 --- a/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationHandler.h +++ b/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationHandler.h @@ -90,22 +90,41 @@ NS_ASSUME_NONNULL_BEGIN completion:(void (^)(MSIDWebviewNavigationDecision * _Nullable navigationDecision, NSError * _Nullable error))completion; /** - * Caches response headers and detects an ASWebAuthenticationSession hand-off signal. + * Handles special redirect URLs (msauth://, browser://), merging caller-supplied + * additional headers onto the MDM enrollment request. The broker flow supplies the + * broker version (x-client-brkrver); the non-broker flow's base method above supplies + * the running process's first-party app-identity headers. + * + * @param URL The special redirect URL + * @param embeddedWebviewController The embedded webview controller instance + * @param additionalHeaders Extra headers to stamp on the enrollment request. Pass nil to add none. + * @param completion Completion block with the navigation decision or error + */ +- (void)handleSpecialRedirectURL:(NSURL *)URL + embeddedWebviewController:(MSIDOAuth2EmbeddedWebviewController * _Nullable)embeddedWebviewController + additionalHeaders:(NSDictionary * _Nullable)additionalHeaders + completion:(void (^)(MSIDWebviewNavigationDecision * _Nullable navigationDecision, NSError * _Nullable error))completion; + +/** + * Caches response headers, processes onboarding telemetry, and detects an + * ASWebAuthenticationSession hand-off signal. * On YES, caller should cancel the WKWebView navigation and invoke the hand-off method below. * - * Security: hand-off is honored only when @c responseURL is HTTPS and its host is on the allowlist. + * Security: hand-off is honored only when the response URL is HTTPS and its host is on the allowlist. * - * @param headers HTTP response headers (raw `allHeaderFields`) - * @param responseURL URL of the response that delivered @c headers (@c NSHTTPURLResponse.URL). - * @return YES if @c headers signal a hand-off AND @c responseURL is from allowed origin; NO otherwise. + * @param response The HTTP navigation response containing headers and URL. + * @param embeddedWebviewController The controller that drove the navigation, passed explicitly + * so the handler can access onboarding telemetry and other per-session state without + * reaching back through shared state (which can race with session-completion cleanup). + * @return YES if headers signal a hand-off AND the response URL is from allowed origin; NO otherwise. */ -- (BOOL)processResponseHeadersAndCheckForASWebAuthHandoff:(NSDictionary *)headers - responseURL:(nullable NSURL *)responseURL; +- (BOOL)processNavigationResponseAndCheckForASWebAuthHandoff:(NSHTTPURLResponse *)response + embeddedWebviewController:(nullable MSIDOAuth2EmbeddedWebviewController *)embeddedWebviewController; #if !MSID_EXCLUDE_SYSTEMWV /** * Performs the hand-off using the headers cached by the last - * @c processResponseHeadersAndCheckForASWebAuthHandoff:responseURL: call. + * @c processNavigationResponseAndCheckForASWebAuthHandoff:embeddedWebviewController: call. * * @param parentController The view controller that presents the webview * @param completion Completion block - MUST be called exactly once; failures surface as a @c failWithError decision diff --git a/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationHandler.m b/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationHandler.m index 677a80fe10..f6b09c58f4 100644 --- a/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationHandler.m +++ b/IdentityCore/src/webview/embeddedWebview/MSIDWebviewNavigationHandler.m @@ -30,6 +30,15 @@ #import "MSIDRequestContext.h" #import "MSIDWebviewNavigationDelegate.h" #import "MSIDWebviewConstants.h" +#import "MSIDConstants.h" +#import "MSIDUXCallbackProvider.h" +#import "MSIDFlightManager.h" +#import "MSIDOnboardingBlobBuilder.h" +#import "MSIDOnboardingBlobFieldKeys.h" +#import "MSIDConstants.h" +#import "MSIDHelpers.h" +#import "MSIDKeychainUtil.h" +#import "NSBundle+MSIDExtensions.h" #if !MSID_EXCLUDE_WEBKIT @@ -37,6 +46,7 @@ @interface MSIDWebviewNavigationHandler() @property (nonatomic) id context; @property (nonatomic) NSDictionary *lastResponseHeaders; +@property (nonatomic, weak) MSIDOnboardingBlobBuilder *onboardingBlobBuilder; @end @@ -75,23 +85,67 @@ - (void)configureWebviewController:(id)webviewController - (void)handleSpecialRedirectURL:(NSURL *)URL embeddedWebviewController:(MSIDOAuth2EmbeddedWebviewController * _Nullable)embeddedWebviewController completion:(void (^)(MSIDWebviewNavigationDecision * _Nullable navigationDecision, NSError * _Nullable error))completion +{ + // Non-broker (in-app) flow: the running process is the caller, so supply its first-party + // app-identity headers rather than nil. The broker flow calls the variant below with the + // broker version instead. + [self handleSpecialRedirectURL:URL + embeddedWebviewController:embeddedWebviewController + additionalHeaders:[self firstPartyAppHeadersForCurrentProcess] + completion:completion]; +} + +- (void)handleSpecialRedirectURL:(NSURL *)URL + embeddedWebviewController:(MSIDOAuth2EmbeddedWebviewController * _Nullable)embeddedWebviewController + additionalHeaders:(NSDictionary * _Nullable)additionalHeaders + completion:(void (^)(MSIDWebviewNavigationDecision * _Nullable navigationDecision, NSError * _Nullable error))completion { MSID_LOG_WITH_CTX(MSIDLogLevelInfo, self.context, @"Handling special redirect: %@", _PII_NULLIFY(URL)); MSIDWebviewNavigationDecisionResolver *util = [MSIDWebviewNavigationDecisionResolver sharedInstance]; MSIDWebviewNavigationDecision *navigationDecision = [util resolveDecisionForURL:URL - embeddedWebviewController:embeddedWebviewController]; + embeddedWebviewController:embeddedWebviewController + additionalHeaders:additionalHeaders]; completion(navigationDecision, nil); } -- (BOOL)processResponseHeadersAndCheckForASWebAuthHandoff:(NSDictionary *)headers - responseURL:(NSURL *)responseURL +// Builds the running process's first-party app-identity headers for the non-broker +// (in-app) enrollment flow. The current process is the caller, so its keychain team ID +// gates the headers and its main bundle supplies x-app-name / x-app-ver. Returns an empty +// dictionary for non first-party processes so no attribution headers are stamped. +- (NSDictionary *)firstPartyAppHeadersForCurrentProcess +{ + if (![MSIDHelpers isMicrosoftFirstPartyAppWithTeamId:[MSIDKeychainUtil sharedInstance].teamId]) + { + return @{}; + } + + NSMutableDictionary *headers = [NSMutableDictionary new]; + NSString *appName = [NSBundle msidAppName]; + NSString *appVersion = [NSBundle msidAppVersion]; + if (appName.length) headers[MSID_APP_NAME_KEY] = appName; + if (appVersion.length) headers[MSID_APP_VER_KEY] = appVersion; + return headers; +} + +- (BOOL)processNavigationResponseAndCheckForASWebAuthHandoff:(NSHTTPURLResponse *)response + embeddedWebviewController:(nullable MSIDOAuth2EmbeddedWebviewController *)embeddedWebviewController { + NSDictionary *headers = response.allHeaderFields; + NSURL *responseURL = response.URL; + // Normalize and capture headers for later use. This also allows for case-insensitive lookup of header values. self.lastResponseHeaders = [self normalizeHeaders:headers]; - // TODO: Add telemetry for response headers + // Process onboarding telemetry from the response if the builder is available. + // This records blocking errors (x-ms-clitelem) and last-loaded domain. + MSIDOnboardingBlobBuilder *builder = embeddedWebviewController.onboardingBlobBuilder; + self.onboardingBlobBuilder = builder; + if (builder && response) + { + [builder processResponseHeaders:response.allHeaderFields responseURL:response.URL]; + } NSString *handoffURLString = self.lastResponseHeaders[MSID_ASWEBAUTH_HANDOFF_URL_KEY]; BOOL hasHandoffHeader = [handoffURLString isKindOfClass:NSString.class] && ((NSString *)handoffURLString).length > 0; @@ -127,8 +181,29 @@ - (void)performASWebAuthenticationHandoffWithParentController:(MSIDViewControlle return; } + MSIDOnboardingBlobBuilder *onboardingBlobBuilder = self.onboardingBlobBuilder; + [onboardingBlobBuilder addStep:MSIDOnboardingBlobStepProfileDownloadFlowStarted timestamp:[NSDate date]]; + + void (^completionBlock)(MSIDWebviewNavigationDecision * _Nullable, NSError * _Nullable) = completion; + completion = ^(MSIDWebviewNavigationDecision * _Nullable decision, NSError * _Nullable error) + { + // The hand-off outcome is carried on the decision (failWithError embeds the + // error; loadRequest signals success) and mirrored in the trailing error param. + // Classify the outcome from the decision, falling back to the error param. + NSError *outcomeError = decision.error ?: error; + if ([outcomeError.domain isEqualToString:MSIDErrorDomain] && outcomeError.code == MSIDErrorUserCancel) + { + [onboardingBlobBuilder addStep:MSIDOnboardingBlobStepProfileDownloadFlowCancelled timestamp:[NSDate date]]; + } + else if (outcomeError) + { + [onboardingBlobBuilder addStep:MSIDOnboardingBlobStepProfileDownloadFlowFailed timestamp:[NSDate date]]; + } + completionBlock(decision, error); + }; + // Retrieve the hand-off URL captured by the most recent - // processResponseHeadersAndCheckForASWebAuthHandoff:responseURL: call. + // processNavigationResponseAndCheckForASWebAuthHandoff:embeddedWebviewController: call. id rawHandoffURL = self.lastResponseHeaders[MSID_ASWEBAUTH_HANDOFF_URL_KEY]; NSString *handoffURLString = [rawHandoffURL isKindOfClass:NSString.class] ? (NSString *)rawHandoffURL : nil; NSURL *handoffURL = handoffURLString.length > 0 ? [NSURL URLWithString:handoffURLString] : nil; @@ -141,7 +216,7 @@ - (void)performASWebAuthenticationHandoffWithParentController:(MSIDViewControlle MSIDErrorInternal, @"ASWebAuthentication hand-off requested without a valid hand-off URL.", nil, nil, nil, self.context.correlationId, nil, YES); - completion([MSIDWebviewNavigationDecision failWithError:missingURLError], nil); + completion([MSIDWebviewNavigationDecision failWithError:missingURLError], missingURLError); return; } @@ -170,7 +245,7 @@ - (void)handleASWebAuthenticationHandoffWithURL:(NSURL *)handoffURL MSIDErrorSessionCanceledProgrammatically, @"ASWebAuthentication handoff URL is invalid", nil, nil, validationError, self.context.correlationId, nil, YES); - completion([MSIDWebviewNavigationDecision failWithError:error], nil); + completion([MSIDWebviewNavigationDecision failWithError:error], error); return; } @@ -209,11 +284,18 @@ - (void)handleASWebAuthenticationTransition:(NSURL *)URL NSError *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInternal, @"ASWebAuthentication transition called with no URL", nil, nil, nil, self.context.correlationId, nil, YES); - completion([MSIDWebviewNavigationDecision failWithError:error], nil); + completion([MSIDWebviewNavigationDecision failWithError:error], error); return; } NSString *redirectURI = [NSString stringWithFormat:@"%@://", callbackURLScheme]; + + // Schedule the MDM profile-installed reminder *before* handing off to the system + // browser/Settings, so it can fire while the user is away installing the profile. + // The post-return `profile_download_complete` callback arrives only after the user is + // back in Authenticator (foreground), at which point the banner would be suppressed. + [self scheduleMDMProfileInstalledNotificationIfNeeded]; + // Launch ASWebAuthenticationSession with the provided URL and configuration [[MSIDSystemWebviewTransitionManager sharedInstance] transitionToSystemWebviewWithURL:URL redirectURI:redirectURI @@ -241,16 +323,59 @@ - (void)handleASWebAuthenticationTransition:(NSURL *)URL { // Neither URL nor error - unexpected MSID_LOG_WITH_CTX(MSIDLogLevelError, self.context, @"[MSIDWebviewNavigationHandler] Transition completed with neither URL nor error"); - NSError *unexpectedError = MSIDCreateError(MSIDErrorDomain, MSIDErrorInternal, - @"Transition completed with neither URL nor error", - nil, nil, nil, self.context.correlationId, nil, YES); - navigationDecision = [MSIDWebviewNavigationDecision failWithError:unexpectedError]; + error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInternal, + @"Transition completed with neither URL nor error", + nil, nil, nil, self.context.correlationId, nil, YES); + navigationDecision = [MSIDWebviewNavigationDecision failWithError:error]; } - completion(navigationDecision, nil); + completion(navigationDecision, error); }]; } +// Schedules the "MDM profile installed" reminder before presenting the +// profile-download ASWebAuthenticationSession (i.e. before the user leaves +// for Settings). Must happen here, not in the later `profile_download_complete` +// callback, since that only fires after we're foreground again, when +// notifications don't show. +// +// Detected via the `x-ms-aswebauth-handoff-purpose: download-profile` response +// header rather than the hand-off URL, to stay decoupled from Intune's URL shape. +- (void)scheduleMDMProfileInstalledNotificationIfNeeded +{ + // Only arm for the MDM profile-download hand-off, not for other ASWebAuthenticationSession transitions. + id purpose = self.lastResponseHeaders[MSID_ASWEBAUTH_HANDOFF_PURPOSE_KEY]; + if (![purpose isKindOfClass:NSString.class] + || [purpose caseInsensitiveCompare:MSID_ASWEBAUTH_HANDOFF_PURPOSE_VALUE_DOWNLOAD_PROFILE] != NSOrderedSame) + { + return; + } + + MSID_LOG_WITH_CTX(MSIDLogLevelInfo, self.context, + @"[ProfileDownload] Detected MDM profile-download hand-off; scheduling profile-installed notification before system webview transition."); + + NSString *delayString = [[MSIDFlightManager sharedInstance] stringForKey:MSID_FLIGHT_MDM_PROFILE_INSTALLED_NOTIFICATION_DELAY]; + NSTimeInterval delay = delayString.length > 0 ? delayString.doubleValue : MSIDMDMProfileInstalledNotificationDefaultDelay; + if (delay <= 0) + { + delay = MSIDMDMProfileInstalledNotificationDefaultDelay; + } + + id provider = MSIDUXCallbackProvider.uxCallbackProvider; + if (provider) + { + MSID_LOG_WITH_CTX(MSIDLogLevelInfo, self.context, + @"[ProfileDownload] Scheduling MDM profile-installed notification with delay %.2f seconds.", delay); + [provider scheduleMDMProfileInstalledNotificationWithDelay:delay]; + [self.onboardingBlobBuilder addStep:MSIDOnboardingBlobStepProfileInstallNotificationScheduled timestamp:[NSDate date]]; + } + else + { + MSID_LOG_WITH_CTX(MSIDLogLevelWarning, self.context, + @"[ProfileDownload] No UX callback provider registered; cannot schedule MDM profile-installed notification."); + } +} + #endif // !MSID_EXCLUDE_SYSTEMWV #pragma mark - Private: ASWebAuthentication Handoff Helpers diff --git a/IdentityCore/src/webview/embeddedWebview/challangeHandlers/MSIDPKeyAuthHandler.m b/IdentityCore/src/webview/embeddedWebview/challangeHandlers/MSIDPKeyAuthHandler.m index bbf48ea55f..da9a412c85 100644 --- a/IdentityCore/src/webview/embeddedWebview/challangeHandlers/MSIDPKeyAuthHandler.m +++ b/IdentityCore/src/webview/embeddedWebview/challangeHandlers/MSIDPKeyAuthHandler.m @@ -35,6 +35,9 @@ #import "MSIDRequestTelemetryConstants.h" #endif #import "MSIDWorkPlaceJoinUtil.h" +#import "MSIDAADNetworkConfiguration.h" +#import "MSIDExecutionFlowLogger.h" +#import "MSIDExecutionFlowConstants.h" @implementation MSIDPKeyAuthHandler @@ -93,12 +96,29 @@ + (BOOL)handleChallenge:(NSString *)challengeUrl [responseReq setValue:currentRequestTelemetryString forHTTPHeaderField:MSID_CURRENT_TELEMETRY_HEADER_NAME]; #endif - // Adding refreshTokenCredential (PRT) header to the challenge response. Header is available in customheaders dictionary + // Adding refreshTokenCredential (PRT) header to the challenge response. Header is available in customheaders dictionary. + // Only attach the PRT header when the challenge is being submitted to a known AAD host, to avoid leaking it to untrusted hosts. NSString *credentialHeader = [customHeaders objectForKey:MSID_REFRESH_TOKEN_CREDENTIAL]; if (credentialHeader) { - MSID_LOG_WITH_CTX(MSIDLogLevelInfo, context, @"Added refresh token to the PkeyAuth response."); - [responseReq setValue:credentialHeader forHTTPHeaderField:MSID_REFRESH_TOKEN_CREDENTIAL]; + NSString *submitHost = [NSURL URLWithString:submitUrl].host.lowercaseString; + if (submitHost && [[MSIDAADNetworkConfiguration defaultConfiguration] isAADPublicCloud:submitHost]) + { + MSID_LOG_WITH_CTX(MSIDLogLevelInfo, context, @"Added refresh token to the PkeyAuth response."); + [responseReq setValue:credentialHeader forHTTPHeaderField:MSID_REFRESH_TOKEN_CREDENTIAL]; + if (context.correlationId) + { + MSIDExecutionFlowInsertTag(MSIDPkeyAuthTagToString(MSIDPkeyAuthAddedRefreshTokenCredentialTag), nil, context.correlationId); + } + } + else + { + MSID_LOG_WITH_CTX(MSIDLogLevelInfo, context, @"Skipped adding refresh token to the PkeyAuth response because the submit URL host is not a known AAD host."); + if (context.correlationId) + { + MSIDExecutionFlowInsertTag(MSIDPkeyAuthTagToString(MSIDPkeyAuthSkippedRefreshTokenCredentialUntrustedHostTag), nil, context.correlationId); + } + } } else { diff --git a/IdentityCore/src/webview/embeddedWebview/challangeHandlers/ios/MSIDCertAuthHandler.m b/IdentityCore/src/webview/embeddedWebview/challangeHandlers/ios/MSIDCertAuthHandler.m index 5e99489fe5..85afc163fb 100644 --- a/IdentityCore/src/webview/embeddedWebview/challangeHandlers/ios/MSIDCertAuthHandler.m +++ b/IdentityCore/src/webview/embeddedWebview/challangeHandlers/ios/MSIDCertAuthHandler.m @@ -30,21 +30,75 @@ #import "NSDictionary+MSIDQueryItems.h" #import "MSIDCertAuthManager.h" -#if !MSID_EXCLUDE_SYSTEMWV +#if MSID_ENABLE_TEST_HOOKS +#import +#endif + +#if TARGET_OS_IPHONE && !MSID_EXCLUDE_SYSTEMWV && MSID_ENABLE_TEST_HOOKS + +// Test-only API surface. Declared in a file-private class extension so the +// formal property contract lives only inside this .m and never reaches any +// header consumer. The implementations are additionally compiled out of any +// build where MSID_ENABLE_TEST_HOOKS is not defined, so no shipping binary +// contains the test hooks even via Objective-C runtime reflection. +// Consumers opt in by defining MSID_ENABLE_TEST_HOOKS=1 only for +// test-bearing CMake/Xcode configurations. +@interface MSIDCertAuthHandler () + +// When YES, +handleChallenge: refuses the CBA challenge. +@property (class, nonatomic) BOOL disableCertBasedAuth; + +// When non-NULL, the iOS challenge handler answers every subsequent +// WKWebView client-cert challenge in-process with this identity (until +// the slot is cleared) instead of routing to SFSafariViewController. +// The host test installs the identity via SecPKCS12Import and assigns +// it here; tear-down assigns NULL to clear it. This is the mechanism +// that lets MSAL ObjC consumers run end-to-end CBA tests on hosted CI +// without a UI agent (matching what other platforms already do via +// silent client-cert credential responses). +@property (class, nonatomic) SecIdentityRef testIdentityForCertBasedAuth; + +@end -static BOOL s_disableCertBasedAuth = NO; +static BOOL s_disableCertBasedAuth = NO; +static SecIdentityRef s_testIdentityForCertBasedAuth = NULL; #endif @implementation MSIDCertAuthHandler -#if TARGET_OS_IPHONE && !MSID_EXCLUDE_SYSTEMWV +#if TARGET_OS_IPHONE && !MSID_EXCLUDE_SYSTEMWV && MSID_ENABLE_TEST_HOOKS -+ (void)disableCertBasedAuth ++ (BOOL)disableCertBasedAuth { - // This is a private API only to ensure nobody with access to internal headers takes dependency on it - // This should be executed in automation tests only - s_disableCertBasedAuth = YES; + return s_disableCertBasedAuth; +} + ++ (void)setDisableCertBasedAuth:(BOOL)disableCertBasedAuth +{ + s_disableCertBasedAuth = disableCertBasedAuth; +} + ++ (SecIdentityRef)testIdentityForCertBasedAuth +{ + return s_testIdentityForCertBasedAuth; +} + ++ (void)setTestIdentityForCertBasedAuth:(SecIdentityRef)testIdentityForCertBasedAuth +{ + // Retain the new identity BEFORE releasing the old one. If a caller passes + // the same SecIdentityRef that's already installed, releasing first could + // drop the last reference and leave us retaining a dangling pointer. + SecIdentityRef previous = s_testIdentityForCertBasedAuth; + if (testIdentityForCertBasedAuth) + { + CFRetain(testIdentityForCertBasedAuth); + } + s_testIdentityForCertBasedAuth = testIdentityForCertBasedAuth; + if (previous) + { + CFRelease(previous); + } } #endif @@ -66,11 +120,27 @@ + (BOOL)handleChallenge:(NSURLAuthenticationChallenge *)challenge { #if !MSID_EXCLUDE_SYSTEMWV +#if TARGET_OS_IPHONE && MSID_ENABLE_TEST_HOOKS if (s_disableCertBasedAuth) { MSID_LOG_WITH_CTX(MSIDLogLevelError, context, @"Cert based auth is explicitly disabled. Ignoring challenge."); return NO; } + + // Test-only short-circuit: if a test has injected an identity via + // +setTestIdentityForCertBasedAuth:, answer the challenge in-process + // and skip the SFSafariViewController hand-off entirely. This is what + // lets end-to-end CBA tests run on hosted CI with no UI agent. + if (s_testIdentityForCertBasedAuth) + { + MSID_LOG_WITH_CTX(MSIDLogLevelInfo, context, @"Answering CBA challenge with injected test identity."); + NSURLCredential *credential = [NSURLCredential credentialWithIdentity:s_testIdentityForCertBasedAuth + certificates:nil + persistence:NSURLCredentialPersistenceNone]; + completionHandler(NSURLSessionAuthChallengeUseCredential, credential); + return YES; + } +#endif MSIDWebviewSession *currentSession = [MSIDWebviewAuthorization currentSession]; diff --git a/IdentityCore/src/workplacejoin/MSIDDeviceTokenUtil.h b/IdentityCore/src/workplacejoin/MSIDDeviceTokenUtil.h new file mode 100644 index 0000000000..c12f8a74bb --- /dev/null +++ b/IdentityCore/src/workplacejoin/MSIDDeviceTokenUtil.h @@ -0,0 +1,77 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import +#import "MSIDConstants.h" + +@class MSIDRequestParameters; +@class MSIDExternalSSOContext; +@class MSIDHttpRequest; +@class MSIDWPJKeyPairWithCert; +@class MSIDDeviceTokenResponseHandler; +@protocol MSIDRequestContext; + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^MSIDDeviceTokenRequestCompletionBlock)(MSIDHttpRequest * _Nullable deviceTokenRequest, NSError * _Nullable error); + +@interface MSIDDeviceTokenUtil : NSObject + ++ (nullable NSURL *)getDeviceTokenEndpoint:(nonnull MSIDRequestParameters *)requestParameters + tenantId:(nonnull NSString *)tenantId; + ++ (void)getDeviceTokenRequest:(nonnull MSIDRequestParameters *)requestParameters + tenantId:(nonnull NSString *)tenantId + resource:(nonnull NSString *)resource + enrollmentId:(nullable NSString *)enrollmentId + extraParameters:(nullable NSDictionary *)extraParameters + ssoContext:(nullable MSIDExternalSSOContext *)ssoContext + completionBlock:(nonnull MSIDDeviceTokenRequestCompletionBlock)completionBlock; + ++ (nullable NSString *)getDeviceTokenRequestJwtForResource:(nonnull NSString *)resource + scopes:(nullable NSSet *)scopes + redirectUri:(nonnull NSString *)redirectUri + audience:(nonnull NSString *)audience + clientId:(nonnull NSString *)clientId + nonce:(nullable NSString *)nonce + registrationInformation:(nonnull MSIDWPJKeyPairWithCert *)registrationInformation + extraPayloadClaims:(nullable NSDictionary *)extraPayloadClaims + context:(nullable id)context + error:(NSError *__nullable __autoreleasing *__nullable)error; + +/// Builds the device token request body parameters shared by the request builder and the grant request. ++ (nonnull NSMutableDictionary *)deviceTokenRequestBodyParametersWithJwt:(nonnull NSString *)signedJwt + enrollmentId:(nullable NSString *)enrollmentId + extraParameters:(nullable NSDictionary *)extraParameters; + ++ (void)handleDeviceTokenResponse:(nullable NSDictionary *)tokenJsonResponse + requestParameters:(nonnull MSIDRequestParameters *)requestParameters + responseHandler:(nullable MSIDDeviceTokenResponseHandler *)responseHandler + error:(nullable NSError *)error + completionBlock:(nonnull MSIDRequestCompletionBlock)completionBlock; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/IdentityCore/src/workplacejoin/MSIDDeviceTokenUtil.m b/IdentityCore/src/workplacejoin/MSIDDeviceTokenUtil.m new file mode 100644 index 0000000000..1bf0b10c2d --- /dev/null +++ b/IdentityCore/src/workplacejoin/MSIDDeviceTokenUtil.m @@ -0,0 +1,319 @@ +// +// 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 "MSIDDeviceTokenUtil.h" +#import "MSIDRequestParameters.h" +#import "MSIDWorkPlaceJoinUtil.h" +#import "MSIDWorkPlaceJoinUtilBase.h" +#import "MSIDWPJKeyPairWithCert.h" +#import "MSIDNonceTokenRequest.h" +#import "MSIDHttpRequest.h" +#import "MSIDAADRequestConfigurator.h" +#import "MSIDAADAuthority.h" +#import "MSIDAADTenant.h" +#import "MSIDAccountIdentifier.h" +#import "MSIDAuthority.h" +#import "MSIDOAuth2Constants.h" +#import "MSIDKeyOperationUtil.h" +#import "MSIDJWTHelper.h" +#import "MSIDDeviceTokenResponseHandler.h" +#import "MSIDOauth2Factory.h" + +@interface MSIDDeviceTokenUtil () + +// Overridable seam used to look up the workplace-join registration for a tenant. +// Exposed so tests can inject a fake registration via a subclass without swizzling. ++ (nullable MSIDWPJKeyPairWithCert *)deviceRegistrationForTenantId:(nullable NSString *)tenantId + context:(nullable id)context; + +@end + +@implementation MSIDDeviceTokenUtil + ++ (nullable MSIDWPJKeyPairWithCert *)deviceRegistrationForTenantId:(nullable NSString *)tenantId + context:(nullable id)context +{ + return [MSIDWorkPlaceJoinUtil getWPJKeysWithTenantId:tenantId context:context]; +} + ++ (nullable NSURL *)getDeviceTokenEndpoint:(nonnull MSIDRequestParameters *)requestParameters + tenantId:(nonnull NSString *)tenantId +{ + if (!requestParameters) + { + MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"Failed to construct device token endpoint: requestParameters is nil."); + return nil; + } + + NSURL *url = requestParameters.authority.url; + + if (!url) + { + MSID_LOG_WITH_CTX(MSIDLogLevelError, requestParameters, @"Failed to construct device token endpoint: authority url is nil."); + return nil; + } + + if ([url.pathComponents.lastObject isEqualToString:@"common"]) + { + if ([NSString msidIsStringNilOrBlank:tenantId]) + { + MSID_LOG_WITH_CTX(MSIDLogLevelError, requestParameters, @"Failed to construct device token endpoint: tenantId is nil or blank while authority is common."); + return nil; + } + + url = [url URLByDeletingLastPathComponent]; + url = [url URLByAppendingPathComponent:tenantId]; + } + + NSURL *endpoint = [url URLByAppendingPathComponent:@"oauth2/v2.0/token"]; + MSID_LOG_WITH_CTX(MSIDLogLevelVerbose, requestParameters, @"Constructed device token endpoint."); + return endpoint; +} + ++ (void)getDeviceTokenRequest:(nonnull MSIDRequestParameters *)requestParameters + tenantId:(nonnull NSString *)tenantId + resource:(nonnull NSString *)resource + enrollmentId:(nullable NSString *)enrollmentId + extraParameters:(nullable NSDictionary *)extraParameters + ssoContext:(nullable MSIDExternalSSOContext *)ssoContext + completionBlock:(nonnull MSIDDeviceTokenRequestCompletionBlock)completionBlock +{ + if (!requestParameters) + { + NSString *errorMessage = @"Failed to create device token request: requestParameters is nil."; + MSID_LOG_WITH_CTX(MSIDLogLevelError, nil, @"%@", errorMessage); + NSError *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, errorMessage, nil, nil, nil, nil, nil, YES); + completionBlock(nil, error); + return; + } + + if ([NSString msidIsStringNilOrBlank:resource]) + { + NSString *errorMessage = @"Failed to create device token request: resource is nil or blank."; + MSID_LOG_WITH_CTX(MSIDLogLevelError, requestParameters, @"%@", errorMessage); + NSError *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, errorMessage, nil, nil, nil, requestParameters.correlationId, nil, YES); + completionBlock(nil, error); + return; + } + + NSURL *endpoint = [self getDeviceTokenEndpoint:requestParameters tenantId:tenantId]; + if (!endpoint) + { + NSString *errorMessage = @"Failed to create device token request: could not construct endpoint."; + MSID_LOG_WITH_CTX(MSIDLogLevelError, requestParameters, @"%@", errorMessage); + NSError *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, errorMessage, nil, nil, nil, requestParameters.correlationId, nil, YES); + completionBlock(nil, error); + return; + } + + // The workplace-join registration is required to sign the device-token JWT. + MSIDWPJKeyPairWithCert *wpjCerts = [self deviceRegistrationForTenantId:tenantId context:requestParameters]; + if (!wpjCerts) + { + NSString *errorMessage = @"Failed to create device token request: no device registration found for the requested tenant."; + MSID_LOG_WITH_CTX(MSIDLogLevelError, requestParameters, @"%@", errorMessage); + NSError *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorWorkplaceJoinRequired, errorMessage, nil, nil, nil, requestParameters.correlationId, nil, YES); + completionBlock(nil, error); + return; + } + + // Device tokens are not tied to a user, so use the first available enrollment id (if any). + NSString *deviceEnrollmentId = enrollmentId; + if ([NSString msidIsStringNilOrBlank:deviceEnrollmentId]) + { + deviceEnrollmentId = [requestParameters.authority enrollmentIdForHomeAccountId:nil + legacyUserId:nil + context:requestParameters + error:nil]; + } + + NSSet *scopesSet = nil; + NSString *scope = requestParameters.allTokenRequestScopes; + if (![NSString msidIsStringNilOrBlank:scope]) + { + scopesSet = [NSSet setWithArray:[scope componentsSeparatedByString:@" "]]; + } + + // 1. Fetch a fresh nonce from the server. It is embedded into the signed JWT as request_nonce. + MSIDRequestParameters *nonceRequestParameters = [MSIDRequestParameters new]; + nonceRequestParameters.correlationId = requestParameters.correlationId; + nonceRequestParameters.authority = [[MSIDAADAuthority alloc] initWithURL:endpoint + rawTenant:MSIDAADTenantTypeCommonRawValue + context:requestParameters + error:nil]; + // Blank account id bypasses the nonce cache and forces a fresh nonce request. + nonceRequestParameters.accountIdentifier = [[MSIDAccountIdentifier alloc] initWithDisplayableId:@"" + homeAccountId:@""]; + + MSIDNonceTokenRequest *nonceRequest = + [[MSIDNonceTokenRequest alloc] initWithRequestParameters:nonceRequestParameters]; + [nonceRequest executeRequestWithCompletion:^(NSString * _Nullable resultNonce, NSError * _Nullable nonceError) + { + if ([NSString msidIsStringNilOrBlank:resultNonce]) + { + NSString *errorMessage = @"Failed to retrieve nonce for device token request."; + MSID_LOG_WITH_CTX(MSIDLogLevelError, requestParameters, @"%@ %@", errorMessage, MSID_PII_LOG_MASKABLE(nonceError)); + NSError *finalNonceError = nonceError ?: MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, errorMessage, nil, nil, nil, requestParameters.correlationId, nil, YES); + completionBlock(nil, finalNonceError); + return; + } + + // 2. Build the signed device-token JWT using the common core workplace-join helper. + NSError *jwtError; + NSString *signedJwt = [self getDeviceTokenRequestJwtForResource:resource + scopes:scopesSet + redirectUri:requestParameters.redirectUri + audience:endpoint.absoluteString + clientId:requestParameters.clientId + nonce:resultNonce + registrationInformation:wpjCerts + extraPayloadClaims:nil + context:requestParameters + error:&jwtError]; + if ([NSString msidIsStringNilOrBlank:signedJwt]) + { + NSString *errorMessage = @"Failed to create signed JWT for device token request."; + MSID_LOG_WITH_CTX(MSIDLogLevelError, requestParameters, @"%@ %@", errorMessage, MSID_PII_LOG_MASKABLE(jwtError)); + NSError *finalJwtError = jwtError ?: MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, errorMessage, nil, nil, nil, requestParameters.correlationId, nil, YES); + completionBlock(nil, finalJwtError); + return; + } + + // 3. POST the signed JWT to the token endpoint using the common core HTTP request factory. + // The signed JWT is attached to the request body under the 'request' key. + NSMutableURLRequest *urlRequest = [NSMutableURLRequest new]; + urlRequest.URL = endpoint; + urlRequest.HTTPMethod = @"POST"; + + MSIDHttpRequest *deviceTokenHttpRequest = [MSIDHttpRequest new]; + deviceTokenHttpRequest.urlRequest = urlRequest; + deviceTokenHttpRequest.context = requestParameters; + [[MSIDAADRequestConfigurator new] configure:deviceTokenHttpRequest]; + + deviceTokenHttpRequest.parameters = [self deviceTokenRequestBodyParametersWithJwt:signedJwt + enrollmentId:deviceEnrollmentId + extraParameters:extraParameters]; + + completionBlock(deviceTokenHttpRequest, nil); + }]; +} + ++ (nonnull NSMutableDictionary *)deviceTokenRequestBodyParametersWithJwt:(nonnull NSString *)signedJwt + enrollmentId:(nullable NSString *)enrollmentId + extraParameters:(nullable NSDictionary *)extraParameters +{ + NSMutableDictionary *bodyParameters = [NSMutableDictionary new]; + if (extraParameters) + { + [bodyParameters addEntriesFromDictionary:extraParameters]; + } + bodyParameters[MSID_OAUTH2_CLIENT_INFO] = @NO; // id token is not expected for a device token + bodyParameters[MSID_OAUTH2_GRANT_TYPE] = MSID_OAUTH2_JWT_BEARER_VALUE; + bodyParameters[@"request"] = signedJwt; + if (![NSString msidIsStringNilOrBlank:enrollmentId]) + { + bodyParameters[MSID_ENROLLMENT_ID] = enrollmentId; + } + return bodyParameters; +} + ++ (nullable NSString *)getDeviceTokenRequestJwtForResource:(nonnull NSString *)resource + scopes:(NSSet *)scopes + redirectUri:(nonnull NSString *)redirectUri + audience:(nonnull NSString *)audience + clientId:(nonnull NSString *)clientId + nonce:(NSString *)nonce + registrationInformation:(nonnull MSIDWPJKeyPairWithCert *)registrationInformation + extraPayloadClaims:(NSDictionary *)extraPayloadClaims + context:(id _Nullable)context + error:(NSError * __autoreleasing *)error +{ + MSIDWPJKeyPairWithCert *workplacejoinData = registrationInformation; + NSMutableDictionary *jwtPayload = [NSMutableDictionary new]; + for (NSString *key in extraPayloadClaims) + { + jwtPayload[key] = extraPayloadClaims[key]; + } + jwtPayload[MSID_OAUTH2_GRANT_TYPE] = MSID_OAUTH2_DEVICE_TOKEN; + jwtPayload[@"aud"] = audience; + jwtPayload[@"iss"] = clientId; // Issuer is the client ID + jwtPayload[MSID_OAUTH2_REDIRECT_URI] = redirectUri; + [jwtPayload setObject:clientId forKey:MSID_OAUTH2_CLIENT_ID]; + if (![NSString msidIsStringNilOrBlank:nonce]) + { + [jwtPayload setObject:nonce forKey:@"request_nonce"]; + } + NSString *scopeString = [scopes.allObjects componentsJoinedByString:@" "]; + if (![NSString msidIsStringNilOrBlank:scopeString]) + { + [jwtPayload setObject:scopeString forKey:MSID_OAUTH2_SCOPE]; + } + [jwtPayload setObject:resource forKey:@"resource"]; + + NSArray *certificateData = @[[NSString stringWithFormat:@"%@", [[workplacejoinData certificateData] base64EncodedStringWithOptions:kNilOptions]]]; + MSIDJwtAlgorithm alg = [[MSIDKeyOperationUtil sharedInstance] getJwtAlgorithmForKey:registrationInformation.privateKeyRef context:context error:error]; + if (!alg) + { + MSID_LOG_WITH_CTX(MSIDLogLevelError, context, @"[Device token] Failed to get JWT algorithm for signing key."); + return nil; + } + + NSDictionary *header = @{ + @"alg" : alg, + @"typ" : @"JWT", + @"x5c" : certificateData + }; + + NSString *signedJwt = [MSIDJWTHelper createSignedJWTforHeader:header payload:jwtPayload signingKey:workplacejoinData.privateKeyRef]; + if ([NSString msidIsStringNilOrBlank:signedJwt]) + { + MSID_LOG_WITH_CTX(MSIDLogLevelError, context, @"[Device token] Failed to sign JWT for requesting device token."); + if (error) + { + *error = MSIDCreateError(MSIDErrorDomain, MSIDErrorInvalidInternalParameter, @"Failed to sign JWT for requesting device token.", nil, nil, nil, context.correlationId, nil, YES); + } + return nil; + } + + return signedJwt; +} + ++ (void)handleDeviceTokenResponse:(nullable NSDictionary *)tokenJsonResponse + requestParameters:(nonnull MSIDRequestParameters *)requestParameters + responseHandler:(nullable MSIDDeviceTokenResponseHandler *)responseHandler + error:(nullable NSError *)error + completionBlock:(nonnull MSIDRequestCompletionBlock)completionBlock +{ + MSIDDeviceTokenResponseHandler *tokenResponseHandler = responseHandler ?: [[MSIDDeviceTokenResponseHandler alloc] initWithRequestParameters:requestParameters + oauthFactory:[MSIDOauth2Factory new]]; + [tokenResponseHandler handleTokenResponse:tokenJsonResponse + context:requestParameters + error:error + completionBlock:^(MSIDTokenResult * _Nullable result, NSError * _Nullable resultError) { + completionBlock(result, resultError); + }]; +} + +@end + diff --git a/IdentityCore/tests/MSIDAADAuthorityTests.m b/IdentityCore/tests/MSIDAADAuthorityTests.m index 249a46d12e..d06d90fa2c 100644 --- a/IdentityCore/tests/MSIDAADAuthorityTests.m +++ b/IdentityCore/tests/MSIDAADAuthorityTests.m @@ -779,6 +779,144 @@ - (void)testNeedsUpdateToHomeAuthority_consumers_shouldReturnTrue XCTAssertTrue([authority needsUpdateToHomeAuthority:NO]); } +#pragma mark - authorityWithUpdatedCloudHostInstanceName + +- (void)testAuthorityWithUpdatedCloudHostInstanceName_whenKnownPublicCloudHost_shouldReturnUpdatedAuthority +{ + MSIDAADAuthority *authority = (MSIDAADAuthority *)[@"https://login.microsoftonline.com/common" aadAuthority]; + NSError *error = nil; + + MSIDAuthority *updatedAuthority = [authority authorityWithUpdatedCloudHostInstanceName:@"login.microsoftonline.com" error:&error]; + + XCTAssertNotNil(updatedAuthority); + XCTAssertNil(error); + XCTAssertEqualObjects(updatedAuthority.environment, @"login.microsoftonline.com"); +} + +- (void)testAuthorityWithUpdatedCloudHostInstanceName_whenKnownSovereignCloudHost_shouldReturnUpdatedAuthority +{ + MSIDAADAuthority *authority = (MSIDAADAuthority *)[@"https://login.microsoftonline.com/common" aadAuthority]; + NSError *error = nil; + + MSIDAuthority *updatedAuthority = [authority authorityWithUpdatedCloudHostInstanceName:@"login.microsoftonline.de" error:&error]; + + XCTAssertNotNil(updatedAuthority); + XCTAssertNil(error); + XCTAssertEqualObjects(updatedAuthority.environment, @"login.microsoftonline.de"); +} + +- (void)testAuthorityWithUpdatedCloudHostInstanceName_whenUnknownHost_shouldReturnNil +{ + MSIDAADAuthority *authority = (MSIDAADAuthority *)[@"https://login.microsoftonline.com/common" aadAuthority]; + NSError *error = nil; + + MSIDAuthority *updatedAuthority = [authority authorityWithUpdatedCloudHostInstanceName:@"unknown-host.example.com" error:&error]; + + XCTAssertNil(updatedAuthority); + XCTAssertNil(error); +} + +- (void)testAuthorityWithUpdatedCloudHostInstanceName_whenHostInCacheWithDifferentCase_shouldReturnUpdatedAuthority +{ + MSIDAadAuthorityCache *cache = [MSIDAadAuthorityCache sharedInstance]; + NSSet *savedEnvironments = cache.allCloudNetworkEnvironments; + // Simulate a cache that stores the preferred_network host without case normalization. + cache.allCloudNetworkEnvironments = [NSSet setWithObject:@"Login.Contoso-Sovereign.COM"]; + + @try + { + MSIDAADAuthority *authority = (MSIDAADAuthority *)[@"https://login.microsoftonline.com/common" aadAuthority]; + NSError *error = nil; + + MSIDAuthority *updatedAuthority = [authority authorityWithUpdatedCloudHostInstanceName:@"login.contoso-sovereign.com" error:&error]; + + XCTAssertNotNil(updatedAuthority); + XCTAssertNil(error); + XCTAssertEqualObjects(updatedAuthority.environment, @"login.contoso-sovereign.com"); + } + @finally + { + cache.allCloudNetworkEnvironments = savedEnvironments; + } +} + +- (void)testAuthorityWithUpdatedCloudHostInstanceName_whenNilHost_shouldReturnNil +{ + MSIDAADAuthority *authority = (MSIDAADAuthority *)[@"https://login.microsoftonline.com/common" aadAuthority]; + NSError *error = nil; + NSString *nilHost = nil; + + MSIDAuthority *updatedAuthority = [authority authorityWithUpdatedCloudHostInstanceName:nilHost error:&error]; + + XCTAssertNil(updatedAuthority); + XCTAssertNil(error); +} + +#pragma mark - isRecognizedMicrosoftIdentityHost + +- (void)testIsRecognizedMicrosoftIdentityHost_whenKnownPublicCloudHost_shouldReturnYes +{ + MSIDAADAuthority *authority = (MSIDAADAuthority *)[@"https://login.microsoftonline.com/common" aadAuthority]; + XCTAssertTrue([authority isRecognizedMicrosoftIdentityHost:@"login.microsoftonline.com"]); +} + +- (void)testIsRecognizedMicrosoftIdentityHost_whenKnownSovereignCloudHost_shouldReturnYes +{ + MSIDAADAuthority *authority = (MSIDAADAuthority *)[@"https://login.microsoftonline.com/common" aadAuthority]; + XCTAssertTrue([authority isRecognizedMicrosoftIdentityHost:@"login.microsoftonline.de"]); +} + +- (void)testIsRecognizedMicrosoftIdentityHost_whenMixedCaseKnownHost_shouldReturnYes +{ + MSIDAADAuthority *authority = (MSIDAADAuthority *)[@"https://login.microsoftonline.com/common" aadAuthority]; + XCTAssertTrue([authority isRecognizedMicrosoftIdentityHost:@"Login.MicrosoftOnline.COM"]); +} + +- (void)testIsRecognizedMicrosoftIdentityHost_whenUnknownHost_shouldReturnNo +{ + MSIDAADAuthority *authority = (MSIDAADAuthority *)[@"https://login.microsoftonline.com/common" aadAuthority]; + XCTAssertFalse([authority isRecognizedMicrosoftIdentityHost:@"unknown-host.example.com"]); +} + +- (void)testIsRecognizedMicrosoftIdentityHost_whenNilHost_shouldReturnNo +{ + MSIDAADAuthority *authority = (MSIDAADAuthority *)[@"https://login.microsoftonline.com/common" aadAuthority]; + NSString *nilHost = nil; + XCTAssertFalse([authority isRecognizedMicrosoftIdentityHost:nilHost]); +} + +- (void)testIsRecognizedMicrosoftIdentityHost_whenBlankHost_shouldReturnNo +{ + MSIDAADAuthority *authority = (MSIDAADAuthority *)[@"https://login.microsoftonline.com/common" aadAuthority]; + XCTAssertFalse([authority isRecognizedMicrosoftIdentityHost:@" "]); +} + +- (void)testIsRecognizedMicrosoftIdentityHost_whenHostInCacheWithDifferentCase_shouldReturnYes +{ + MSIDAadAuthorityCache *cache = [MSIDAadAuthorityCache sharedInstance]; + NSSet *savedEnvironments = cache.allCloudNetworkEnvironments; + // Simulate a cache that stores the preferred_network host without case normalization. + cache.allCloudNetworkEnvironments = [NSSet setWithObject:@"Login.Contoso-Sovereign.COM"]; + + @try + { + MSIDAADAuthority *authority = (MSIDAADAuthority *)[@"https://login.microsoftonline.com/common" aadAuthority]; + XCTAssertTrue([authority isRecognizedMicrosoftIdentityHost:@"login.contoso-sovereign.com"]); + } + @finally + { + cache.allCloudNetworkEnvironments = savedEnvironments; + } +} + +- (void)testIsRecognizedMicrosoftIdentityHost_whenNonAADAuthority_shouldReturnNo +{ + // Non-AAD authority types inherit the base implementation, which recognizes no + // cloud host, so even a known AAD host is not recognized for e.g. ADFS. + MSIDAuthority *adfsAuthority = [@"https://login.microsoftonline.com/adfs" adfsAuthority]; + XCTAssertFalse([adfsAuthority isRecognizedMicrosoftIdentityHost:@"login.microsoftonline.com"]); +} + #pragma mark - Private diff --git a/IdentityCore/tests/MSIDAADOAuthEmbeddedWebviewControllerTests.m b/IdentityCore/tests/MSIDAADOAuthEmbeddedWebviewControllerTests.m index 004430d84e..d871ec067c 100644 --- a/IdentityCore/tests/MSIDAADOAuthEmbeddedWebviewControllerTests.m +++ b/IdentityCore/tests/MSIDAADOAuthEmbeddedWebviewControllerTests.m @@ -29,6 +29,15 @@ #import "MSIDWKNavigationActionMock.h" #import "MSIDWebAuthNUtil.h" #import "MSIDTestBundle.h" +#import "MSIDWebviewConstants.h" +#import "MSIDConstants.h" +#import "MSIDFlightManager.h" +#import "MSIDFlightManagerMockProvider.h" +#import "MSIDInteractiveTokenRequestParameters.h" +#import "MSIDTestParametersProvider.h" +#import "MSIDOnboardingBlobBuilder.h" +#import "MSIDOnboardingBlobBuilder+MSIDTestUtil.h" +#import "MSIDOnboardingBlobFieldKeys.h" #if !MSID_EXCLUDE_WEBKIT @@ -106,6 +115,8 @@ - (BOOL)application:(__unused UIApplication *)application @interface MSIDAADOAuthEmbeddedWebviewControllerTests : XCTestCase +@property (nonatomic) MSIDFlightManagerMockProvider *flightProvider; + @end @implementation MSIDAADOAuthEmbeddedWebviewControllerTests @@ -113,12 +124,14 @@ @implementation MSIDAADOAuthEmbeddedWebviewControllerTests - (void)setUp { [super setUp]; - // Put setup code here. This method is called before the invocation of each test method in the class. + self.flightProvider = [MSIDFlightManagerMockProvider new]; + MSIDFlightManager.sharedInstance.flightProvider = self.flightProvider; } - (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the class. + MSIDFlightManager.sharedInstance.flightProvider = nil; + self.flightProvider = nil; [super tearDown]; } @@ -523,6 +536,90 @@ - (void)testOpenIdVcURLMutation_whenOriginalURLHasNoQueryString_shouldStillAppen XCTAssertNotNil(queryMap[@"x_ms_caller_bundle_id"]); } +#pragma mark - Mobile onboarding stamping (msauth://enroll) + +- (MSIDAADOAuthEmbeddedWebviewController *)onboardingControllerWithBuilder:(MSIDOnboardingBlobBuilder *)builder + context:(MSIDInteractiveTokenRequestParameters *)parameters +{ + MSIDAADOAuthEmbeddedWebviewController *webVC = [[MSIDAADOAuthEmbeddedWebviewController alloc] + initWithStartURL:[NSURL URLWithString:@"https://contoso.com/oauth/authorize"] + endURL:[NSURL URLWithString:@"endurl://host"] + webview:nil + customHeaders:nil + platfromParams:nil + context:parameters]; + webVC.onboardingBlobBuilder = builder; + return webVC; +} + +- (void)testDecidePolicyForNavigationAction_whenMsauthEnrollAndOnboardingEnabled_shouldStampPhase1UxFlowAndEnableFlow +{ + self.flightProvider.boolForKeyContainer = @{ MSID_FLIGHT_DISABLE_MOBILE_ONBOARDING: @NO }; + + MSIDOnboardingBlobBuilder *builder = [MSIDOnboardingBlobBuilder msidTestBuilder]; + MSIDInteractiveTokenRequestParameters *params = [MSIDTestParametersProvider testInteractiveParameters]; + MSIDAADOAuthEmbeddedWebviewController *webVC = [self onboardingControllerWithBuilder:builder context:params]; + + NSString *encoded = [@"https://manage.microsoft.com/enroll" stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + NSString *urlString = [NSString stringWithFormat:@"msauth://%@?%@=%@", MSID_MDM_ENROLL_HOST, MSID_INTUNE_URL_KEY, encoded]; + NSURLRequest *request = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:urlString]]; + MSIDWKNavigationActionMock *action = [[MSIDWKNavigationActionMock alloc] initWithRequest:request]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"decision handler"]; + [webVC decidePolicyAADForNavigationAction:action decisionHandler:^(WKNavigationActionPolicy __unused decision) { + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + + XCTAssertTrue([[builder msidUxFlowsUsed] containsObject:MSIDOnboardingUxFlowMobileOnboardingPhase1]); + XCTAssertTrue(params.isNewMobileOnboardingFlow); +} + +- (void)testDecidePolicyForNavigationAction_whenMsauthEnrollAndOnboardingDisabledWithValidIntuneUrl_shouldStampClientFlightDisabledLegacyFallback +{ + self.flightProvider.boolForKeyContainer = @{ MSID_FLIGHT_DISABLE_MOBILE_ONBOARDING: @YES }; + + MSIDOnboardingBlobBuilder *builder = [MSIDOnboardingBlobBuilder msidTestBuilder]; + MSIDInteractiveTokenRequestParameters *params = [MSIDTestParametersProvider testInteractiveParameters]; + MSIDAADOAuthEmbeddedWebviewController *webVC = [self onboardingControllerWithBuilder:builder context:params]; + + NSString *encoded = [@"https://manage.microsoft.com/enroll" stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + NSString *urlString = [NSString stringWithFormat:@"msauth://%@?%@=%@", MSID_MDM_ENROLL_HOST, MSID_INTUNE_URL_KEY, encoded]; + NSURLRequest *request = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:urlString]]; + MSIDWKNavigationActionMock *action = [[MSIDWKNavigationActionMock alloc] initWithRequest:request]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"decision handler"]; + [webVC decidePolicyAADForNavigationAction:action decisionHandler:^(WKNavigationActionPolicy __unused decision) { + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + + XCTAssertTrue([[builder msidStampedStepIds] containsObject:MSIDOnboardingBlobStepMobileOnboardingClientFlightDisabledLegacyFallback]); + XCTAssertFalse(params.isNewMobileOnboardingFlow); +} + +- (void)testDecidePolicyForNavigationAction_whenMsauthEnrollAndOnboardingDisabledWithMissingIntuneUrl_shouldNotStampFallback +{ + self.flightProvider.boolForKeyContainer = @{ MSID_FLIGHT_DISABLE_MOBILE_ONBOARDING: @YES }; + + MSIDOnboardingBlobBuilder *builder = [MSIDOnboardingBlobBuilder msidTestBuilder]; + MSIDInteractiveTokenRequestParameters *params = [MSIDTestParametersProvider testInteractiveParameters]; + MSIDAADOAuthEmbeddedWebviewController *webVC = [self onboardingControllerWithBuilder:builder context:params]; + + NSString *urlString = [NSString stringWithFormat:@"msauth://%@", MSID_MDM_ENROLL_HOST]; + NSURLRequest *request = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:urlString]]; + MSIDWKNavigationActionMock *action = [[MSIDWKNavigationActionMock alloc] initWithRequest:request]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"decision handler"]; + [webVC decidePolicyAADForNavigationAction:action decisionHandler:^(WKNavigationActionPolicy __unused decision) { + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + + XCTAssertFalse([[builder msidStampedStepIds] containsObject:MSIDOnboardingBlobStepMobileOnboardingClientFlightDisabledLegacyFallback]); + XCTAssertFalse([[builder msidUxFlowsUsed] containsObject:MSIDOnboardingUxFlowMobileOnboardingPhase1]); +} + #pragma mark - openid-vc handler delegation #if TARGET_OS_IPHONE diff --git a/IdentityCore/tests/MSIDBoundTokenProviderTests.m b/IdentityCore/tests/MSIDBoundTokenProviderTests.m new file mode 100644 index 0000000000..4a366c41fb --- /dev/null +++ b/IdentityCore/tests/MSIDBoundTokenProviderTests.m @@ -0,0 +1,152 @@ +// +// 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 +#import "MSIDBoundTokenProvider.h" +#import "MSIDBrowserNativeMessageGetTokenRequest.h" +#import "MSIDError.h" + +@interface MSIDBoundTokenProviderTests : XCTestCase + +@end + +@implementation MSIDBoundTokenProviderTests + +// A production-shaped GetToken request built from the real MSIDBrowserNativeMessageGetTokenRequest properties. +- (MSIDBrowserNativeMessageGetTokenRequest *)validRequest +{ + MSIDBrowserNativeMessageGetTokenRequest *request = [MSIDBrowserNativeMessageGetTokenRequest new]; + request.clientId = @"00000000-0000-0000-0000-000000000001"; + request.redirectUri = @"brk-com.microsoft.test://auth"; + request.scopes = @"user.read"; + request.state = @"test-state"; + request.prompt = MSIDPromptTypeDefault; + request.canShowUI = YES; + request.isSts = NO; + request.nonce = @"test-nonce"; + request.loginHint = @"user@contoso.com"; + request.instanceAware = NO; + request.platformSequence = @"oneauth|1.2.3,msal|1.0.0"; + request.extraParameters = @{ @"foo": @"bar" }; + return request; +} + +- (NSDictionary *)payloadDictionaryFromResponse:(NSString *)response +{ + NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding]; + XCTAssertNotNil(data); + + NSError *jsonError = nil; + NSDictionary *payload = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + XCTAssertNil(jsonError); + XCTAssertTrue([payload isKindOfClass:NSDictionary.class]); + + return payload; +} + +// A GetToken request handed to the provider is serviced entirely in-process, +// returning a payload, with no SSO extension / ASAuthorization involvement. +- (void)testAcquireBoundToken_inProc_returnsPayload +{ + MSIDBoundTokenProvider *provider = [MSIDBoundTokenProvider new]; + XCTestExpectation *expectation = [self expectationWithDescription:@"in-proc completion"]; + + [provider acquireBoundTokenWithRequest:[self validRequest] + context:nil + completionBlock:^(NSString *response, NSError *error) { + XCTAssertNil(error); + XCTAssertNotNil(response); + + NSDictionary *payload = [self payloadDictionaryFromResponse:response]; + XCTAssertEqualObjects(payload[@"clientId"], @"00000000-0000-0000-0000-000000000001"); + XCTAssertEqualObjects(payload[@"redirectUri"], @"brk-com.microsoft.test://auth"); + XCTAssertEqualObjects(payload[@"scope"], @"user.read"); + XCTAssertEqualObjects(payload[@"state"], @"test-state"); + XCTAssertEqualObjects(payload[@"transport"], @"in_proc_common_core"); + XCTAssertEqualObjects(payload[@"servicedBy"], @"MSIDBoundTokenProvider"); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:5.0]; +} + +- (void)testAcquireBoundToken_nilRequest_returnsInvalidInternalParameterError +{ + MSIDBoundTokenProvider *provider = [MSIDBoundTokenProvider new]; + MSIDBrowserNativeMessageGetTokenRequest *nilRequest = nil; + XCTestExpectation *expectation = [self expectationWithDescription:@"nil request error"]; + + [provider acquireBoundTokenWithRequest:nilRequest + context:nil + completionBlock:^(NSString *response, NSError *error) { + XCTAssertNil(response); + XCTAssertEqualObjects(error.domain, MSIDErrorDomain); + XCTAssertEqual(error.code, MSIDErrorInvalidInternalParameter); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:5.0]; +} + +- (void)testAcquireBoundToken_missingClientId_returnsError +{ + MSIDBoundTokenProvider *provider = [MSIDBoundTokenProvider new]; + MSIDBrowserNativeMessageGetTokenRequest *request = [self validRequest]; + request.clientId = @""; + + XCTestExpectation *expectation = [self expectationWithDescription:@"validation error"]; + + [provider acquireBoundTokenWithRequest:request + context:nil + completionBlock:^(NSString *response, NSError *error) { + XCTAssertNil(response); + XCTAssertEqualObjects(error.domain, MSIDErrorDomain); + XCTAssertEqual(error.code, MSIDErrorInvalidDeveloperParameter); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:5.0]; +} + +- (void)testAcquireBoundToken_missingRedirectUri_returnsInvalidDeveloperParameterError +{ + MSIDBoundTokenProvider *provider = [MSIDBoundTokenProvider new]; + MSIDBrowserNativeMessageGetTokenRequest *request = [self validRequest]; + request.redirectUri = @""; + + XCTestExpectation *expectation = [self expectationWithDescription:@"redirect validation error"]; + + [provider acquireBoundTokenWithRequest:request + context:nil + completionBlock:^(NSString *response, NSError *error) { + XCTAssertNil(response); + XCTAssertEqualObjects(error.domain, MSIDErrorDomain); + XCTAssertEqual(error.code, MSIDErrorInvalidDeveloperParameter); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:5.0]; +} + +@end diff --git a/IdentityCore/tests/MSIDDeviceTokenGrantRequestNetworkTests.m b/IdentityCore/tests/MSIDDeviceTokenGrantRequestNetworkTests.m new file mode 100644 index 0000000000..8047cbc109 --- /dev/null +++ b/IdentityCore/tests/MSIDDeviceTokenGrantRequestNetworkTests.m @@ -0,0 +1,238 @@ +// +// 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 +#import "MSIDDeviceTokenGrantRequest.h" +#import "MSIDDeviceTokenResponseHandler.h" +#import "MSIDRequestParameters.h" +#import "MSIDAuthenticationScheme.h" +#import "MSIDExternalSSOContextMock.h" +#import "MSIDOAuth2Constants.h" +#import "MSIDAADAuthority.h" +#import "MSIDAADV2Oauth2Factory.h" +#import "NSData+MSIDExtensions.h" +#import "NSString+MSIDExtensions.h" +#import "MSIDTestSecureEnclaveKeyPairGenerator.h" +#import "MSIDError.h" +#import "MSIDTokenResult.h" +#import "MSIDAccessToken.h" +#import "MSIDTestURLSession.h" +#import "MSIDTestIdentifiers.h" +#import "MSIDTestIdTokenUtil.h" +#import "NSDictionary+MSIDTestUtil.h" +#import "MSIDHttpRequestProtocol.h" +#import "MSIDRequestContext.h" +#import "MSIDDeviceTokenGrantRequestMock.h" + +#pragma mark - Tests + +@interface MSIDDeviceTokenGrantRequestNetworkTests : XCTestCase + +@property (nonatomic) NSURL *testEndpoint; +@property (nonatomic) MSIDRequestParameters *defaultRequestParameters; +@property (nonatomic) MSIDWPJKeyPairWithCertMock *mockRegistrationInfo; +@property (nonatomic) MSIDDeviceTokenResponseHandler *defaultResponseHandler; +@property (nonatomic) SecKeyRef eccPrivateKey; + +@end + +@implementation MSIDDeviceTokenGrantRequestNetworkTests + +- (void)setUp +{ + [super setUp]; + [MSIDTestURLSession clearResponses]; + + self.testEndpoint = [[NSURL alloc] initWithString:@"https://login.microsoftonline.com/common/oauth2/v2.0/token"]; + + self.defaultRequestParameters = [MSIDRequestParameters new]; + self.defaultRequestParameters.clientId = @"test-client-id"; + self.defaultRequestParameters.redirectUri = @"msauth.com.test://auth"; + self.defaultRequestParameters.authScheme = [MSIDAuthenticationScheme new]; + self.defaultRequestParameters.correlationId = [NSUUID UUID]; + self.defaultRequestParameters.authority = [[MSIDAADAuthority alloc] initWithURL:[NSURL URLWithString:@"https://login.microsoftonline.com/common"] + rawTenant:nil + context:nil + error:nil]; + + NSData *mockCertData = [NSData msidDataFromBase64UrlEncodedString:[self dummyEccCertificate]]; + MSIDTestSecureEnclaveKeyPairGenerator *keyGen = [[MSIDTestSecureEnclaveKeyPairGenerator alloc] initWithSharedAccessGroup:@"test" useSecureEnclave:NO applicationTag:@"test"]; + SecCertificateRef mockCert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)mockCertData); + self.eccPrivateKey = keyGen.eccPrivateKey; + self.mockRegistrationInfo = [[MSIDWPJKeyPairWithCertMock alloc] initWithPrivateKey:self.eccPrivateKey certificate:mockCert certificateIssuer:@"some-issuer"]; + + __auto_type factory = [MSIDAADV2Oauth2Factory new]; + self.defaultResponseHandler = [[MSIDDeviceTokenResponseHandler alloc] initWithRequestParameters:self.defaultRequestParameters + oauthFactory:factory]; +} + +- (void)tearDown +{ + self.testEndpoint = nil; + self.defaultRequestParameters = nil; + self.mockRegistrationInfo = nil; + self.defaultResponseHandler = nil; + [MSIDTestURLSession clearResponses]; + + [super tearDown]; +} + +- (MSIDDeviceTokenGrantRequestMock *)mockRequest +{ + MSIDDeviceTokenGrantRequestMock *request = [[MSIDDeviceTokenGrantRequestMock alloc] initWithEndpoint:self.testEndpoint + requestParameters:self.defaultRequestParameters + scopes:@"scope1 scope2" + registrationInformation:self.mockRegistrationInfo + resource:@"https://graph.microsoft.com" + enrollmentId:@"enrollment-id" + extraParameters:nil + ssoContext:nil + tokenResponseHandler:self.defaultResponseHandler + error:nil]; + request.nonce = @"test-nonce"; + return request; +} + +#pragma mark - sendWithBlock: error path + +- (void)testExecuteRequest_whenSendWithBlockReturnsError_shouldCallCompletionBlockWithSameError +{ + // Arrange + MSIDDeviceTokenGrantRequestMock *request = [self mockRequest]; + XCTAssertNotNil(request); + + NSError *networkError = MSIDCreateError(MSIDErrorDomain, MSIDErrorServerUnhandledResponse, @"Simulated network failure", nil, nil, nil, nil, nil, YES); + request.expectedResponse = nil; + request.expectedError = networkError; + + XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; + __block NSError *capturedError = nil; + __block MSIDTokenResult *capturedResult = nil; + + // Act + [request executeRequestWithCompletion:^(MSIDTokenResult *result, NSError *error) + { + capturedResult = result; + capturedError = error; + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + + // Assert + XCTAssertTrue(request.sendWithBlockCalled); + XCTAssertNil(capturedResult); + XCTAssertNotNil(capturedError); + XCTAssertEqualObjects(capturedError.domain, MSIDErrorDomain); + XCTAssertEqual(capturedError.code, MSIDErrorServerUnhandledResponse); +} + +#pragma mark - sendWithBlock: success path + +- (void)testExecuteRequest_whenSendWithBlockReturnsValidResponse_shouldCallCompletionBlockWithTokenResult +{ + // Arrange + MSIDDeviceTokenGrantRequestMock *request = [self mockRequest]; + XCTAssertNotNil(request); + + NSString *clientInfoString = [@{ @"uid" : DEFAULT_TEST_UID, @"utid" : DEFAULT_TEST_UTID } msidBase64UrlJson]; + NSDictionary *tokenResponse = @{ + MSID_OAUTH2_TOKEN_TYPE : @"Bearer", + MSID_OAUTH2_ACCESS_TOKEN : @"test-device-access-token", + MSID_OAUTH2_EXPIRES_IN : @"3600", + MSID_OAUTH2_SCOPE : @"scope1 scope2", + MSID_OAUTH2_CLIENT_INFO : clientInfoString, + MSID_OAUTH2_ID_TOKEN : [MSIDTestIdTokenUtil defaultV2IdToken] + }; + request.expectedResponse = tokenResponse; + request.expectedError = nil; + + XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; + __block NSError *capturedError = nil; + __block MSIDTokenResult *capturedResult = nil; + + // Act + [request executeRequestWithCompletion:^(MSIDTokenResult *result, NSError *error) + { + capturedResult = result; + capturedError = error; + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + + // Assert + XCTAssertTrue(request.sendWithBlockCalled); + XCTAssertNil(capturedError); + XCTAssertNotNil(capturedResult); + XCTAssertEqualObjects(capturedResult.accessToken.accessToken, @"test-device-access-token"); +} + +#pragma mark - sendWithBlock: malformed response + +- (void)testExecuteRequest_whenSendWithBlockReturnsResponseWithoutClientInfo_shouldCallCompletionBlockWithError +{ + // Arrange + MSIDDeviceTokenGrantRequestMock *request = [self mockRequest]; + XCTAssertNotNil(request); + + // Missing client_info causes AAD v2 response verification to fail. + NSDictionary *malformedResponse = @{ + MSID_OAUTH2_TOKEN_TYPE : @"Bearer", + MSID_OAUTH2_ACCESS_TOKEN : @"test-device-access-token", + MSID_OAUTH2_EXPIRES_IN : @"3600", + MSID_OAUTH2_SCOPE : @"scope1 scope2" + }; + request.expectedResponse = malformedResponse; + request.expectedError = nil; + + XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; + __block NSError *capturedError = nil; + __block MSIDTokenResult *capturedResult = nil; + + // Act + [request executeRequestWithCompletion:^(MSIDTokenResult *result, NSError *error) + { + capturedResult = result; + capturedError = error; + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + + // Assert + XCTAssertTrue(request.sendWithBlockCalled); + XCTAssertNil(capturedResult); + XCTAssertNotNil(capturedError); + XCTAssertEqualObjects(capturedError.domain, MSIDErrorDomain); +} + +#pragma mark - Helpers + +- (NSString *)dummyEccCertificate +{ + return @"MIIDNzCCAh-gAwIBAgIQKBcXojifRIxLIuut33ZknzANBgkqhkiG9w0BAQsFADB4MXYwEQYKCZImiZPyLGQBGRYDbmV0MBUGCgmSJomT8ixkARkWB3dpbmRvd3MwHQYDVQQDExZNUy1Pcmdhbml6YXRpb24tQWNjZXNzMCsGA1UECxMkODJkYmFjYTQtM2U4MS00NmNhLTljNzMtMDk1MGMxZWFjYTk3MB4XDTIzMDMxMzIxMjk0OFoXDTMzMDMxMzIxNTk0OFowLzEtMCsGA1UEAxMkOWVlNWYzM2ItOTc0OS00M2U3LTk1NjctODMxOGVhNDEyNTRiMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEl-xbT_nXgQkkzQOX7NPrvh9vPMt7yrzLqBthSpZXuIjV77izK_GW91qHTzZImhwbvXG6AcVH9Qs7ilN-VIb9xaOB0DCBzTAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB_wQMMAoGCCsGAQUFBwMCMA4GA1UdDwEB_wQEAwIHgDAiBgsqhkiG9xQBBYIcAgQTBIEQo8MK5pvg9k-6UZTxtj7IITAiBgsqhkiG9xQBBYIcAwQTBIEQj-LgHz1F-kSyqt3J40Sn7zAiBgsqhkiG9xQBBYIcBQQTBIEQkq1F9o3jGk21ENGwmnSoyjAUBgsqhkiG9xQBBYIcCAQFBIECTkEwEwYLKoZIhvcUAQWCHAcEBASBATAwDQYJKoZIhvcNAQELBQADggEBAFYbeUHpPcZj6Z8BcPhQ59dOi3-aGSYKX6Ub6GBv1CgiqU9EJ-P6VOipCL5dR458nMXJ4j97_pOXwPT0sS1rSTJ8_x3YpGLIJXpvkqDEHIoUvX1sR1tOlvXhUiP0O6l35-sil1itUZAKqS7RZtd8TWnMIgw3rCHbDHA9OlagunL6o75YC5Y74VdedZbCUjTy-IuU_VKM5gpa3c6uf_QleYgdQFlDjMH9w4TkqaWNONNoYulLZI8AykT9QtYB0iAsFr4KRL58ot1svOhqMil9vKDTkDrixEyThCcHmyyHeNoBjmXtaubOAiE3cMoJs7bV7I1uOS9aAI-Hm0W9NV-CkeE"; +} + +@end diff --git a/IdentityCore/tests/MSIDDeviceTokenGrantRequestTests.m b/IdentityCore/tests/MSIDDeviceTokenGrantRequestTests.m index 2ea8675bb0..339275f706 100644 --- a/IdentityCore/tests/MSIDDeviceTokenGrantRequestTests.m +++ b/IdentityCore/tests/MSIDDeviceTokenGrantRequestTests.m @@ -32,7 +32,16 @@ #import "MSIDAADAuthority.h" #import "MSIDAADV2Oauth2Factory.h" #import "NSData+MSIDExtensions.h" +#import "NSString+MSIDExtensions.h" #import "MSIDTestSecureEnclaveKeyPairGenerator.h" +#import "MSIDError.h" +#import "MSIDTokenResult.h" +#import "MSIDTestURLSession.h" +#import "MSIDTestURLResponse.h" +#import "NSDictionary+MSIDTestUtil.h" +#import "MSIDTestIdentifiers.h" +#import "MSIDDeviceTokenGrantRequestMock.h" +#import "MSIDAccessToken.h" @interface MSIDDeviceTokenGrantRequestTests : XCTestCase @@ -49,6 +58,7 @@ @implementation MSIDDeviceTokenGrantRequestTests - (void)setUp { [super setUp]; + [MSIDTestURLSession clearResponses]; self.testEndpoint = [[NSURL alloc] initWithString:@"https://login.microsoftonline.com/common/oauth2/v2.0/token"]; @@ -74,6 +84,7 @@ - (void)tearDown self.defaultRequestParameters = nil; self.mockRegistrationInfo = nil; self.defaultResponseHandler = nil; + [MSIDTestURLSession clearResponses]; [super tearDown]; } @@ -342,6 +353,152 @@ - (void)testInit_whenSingleScope_shouldReturnNonNilRequest XCTAssertNotNil(request); } +#pragma mark - executeRequestWithCompletion: nonce validation + +- (void)testExecuteRequest_whenNonceIsNil_shouldCallCompletionBlockWithError +{ + // Arrange + MSIDDeviceTokenGrantRequest *request = [[MSIDDeviceTokenGrantRequest alloc] initWithEndpoint:self.testEndpoint + requestParameters:self.defaultRequestParameters + scopes:@"scope1 scope2" + registrationInformation:self.mockRegistrationInfo + resource:@"https://graph.microsoft.com" + enrollmentId:nil + extraParameters:nil + ssoContext:nil + tokenResponseHandler:self.defaultResponseHandler + error:nil]; + XCTAssertNotNil(request); + request.nonce = nil; + + XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; + __block NSError *capturedError = nil; + __block MSIDTokenResult *capturedResult = nil; + + // Act + [request executeRequestWithCompletion:^(MSIDTokenResult *result, NSError *error) + { + capturedResult = result; + capturedError = error; + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + + // Assert + XCTAssertNil(capturedResult); + XCTAssertNotNil(capturedError); + XCTAssertEqualObjects(capturedError.domain, MSIDErrorDomain); + XCTAssertEqual(capturedError.code, MSIDErrorInvalidInternalParameter); +} + +- (void)testExecuteRequest_whenNonceIsBlank_shouldCallCompletionBlockWithError +{ + // Arrange + MSIDDeviceTokenGrantRequest *request = [[MSIDDeviceTokenGrantRequest alloc] initWithEndpoint:self.testEndpoint + requestParameters:self.defaultRequestParameters + scopes:@"scope1 scope2" + registrationInformation:self.mockRegistrationInfo + resource:@"https://graph.microsoft.com" + enrollmentId:nil + extraParameters:nil + ssoContext:nil + tokenResponseHandler:self.defaultResponseHandler + error:nil]; + XCTAssertNotNil(request); + request.nonce = @""; + + XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; + __block NSError *capturedError = nil; + __block MSIDTokenResult *capturedResult = nil; + + // Act + [request executeRequestWithCompletion:^(MSIDTokenResult *result, NSError *error) + { + capturedResult = result; + capturedError = error; + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + + // Assert + XCTAssertNil(capturedResult); + XCTAssertNotNil(capturedError); + XCTAssertEqualObjects(capturedError.domain, MSIDErrorDomain); + XCTAssertEqual(capturedError.code, MSIDErrorInvalidInternalParameter); +} + +- (void)testExecuteRequest_whenNonceIsSet_shouldCallCompletionBlockWithoutError +{ + // Arrange + self.defaultRequestParameters.authority = [[MSIDAADAuthority alloc] initWithURL:[NSURL URLWithString:@"https://login.microsoftonline.com/common"] + rawTenant:nil + context:nil + error:nil]; + MSIDDeviceTokenGrantRequestMock *request = [[MSIDDeviceTokenGrantRequestMock alloc] initWithEndpoint:self.testEndpoint + requestParameters:self .defaultRequestParameters + scopes:@"scope1 scope2" + registrationInformation:self.mockRegistrationInfo + resource:@"https://graph.microsoft.com" + enrollmentId:nil + extraParameters:nil + ssoContext:nil + tokenResponseHandler:self.defaultResponseHandler + error:nil]; + XCTAssertNotNil(request); + request.nonce = @"test-nonce-value"; + NSString *clientInfoString = [@{ @"uid" : DEFAULT_TEST_UID, @"utid" : DEFAULT_TEST_UTID } msidBase64UrlJson]; + NSDictionary *tokenResponse = @{ + MSID_OAUTH2_TOKEN_TYPE : @"Bearer", + MSID_OAUTH2_ACCESS_TOKEN : @"test-device-access-token", + MSID_OAUTH2_EXPIRES_IN : @"3600", + MSID_OAUTH2_SCOPE : @"scope1 scope2", + MSID_OAUTH2_CLIENT_INFO : clientInfoString, + MSID_OAUTH2_ID_TOKEN : @"test-id-token" + }; + request.expectedResponse = tokenResponse; + request.expectedError = nil; + + XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; + __block NSError *capturedError = nil; + __block MSIDTokenResult *capturedResult = nil; + + // Act + [request executeRequestWithCompletion:^(MSIDTokenResult *result, NSError *error) + { + capturedResult = result; + capturedError = error; + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + + // Assert + XCTAssertNotNil(capturedResult); + XCTAssertEqualObjects([capturedResult.accessToken accessToken], @"test-device-access-token"); + XCTAssertNil(capturedError); +} + +- (NSMutableDictionary *)mockedRequestHeadersForDeviceTokenRequest +{ + return [@{ + @"Accept" : [MSIDTestIgnoreSentinel sentinel], + @"Content-Length" : [MSIDTestIgnoreSentinel sentinel], + @"Content-Type" : [MSIDTestIgnoreSentinel sentinel], + @"User-Agent" : [MSIDTestIgnoreSentinel sentinel], + @"x-client-SKU": [MSIDTestIgnoreSentinel sentinel], + @"x-client-OS": [MSIDTestIgnoreSentinel sentinel], + @"x-app-name": [MSIDTestIgnoreSentinel sentinel], + @"x-ms-PkeyAuth+": [MSIDTestIgnoreSentinel sentinel], + @"x-client-Ver": [MSIDTestIgnoreSentinel sentinel], + @"x-client-CPU": [MSIDTestIgnoreSentinel sentinel], + @"x-app-ver": [MSIDTestIgnoreSentinel sentinel], + @"x-client-DM": [MSIDTestIgnoreSentinel sentinel], + @"Connection": [MSIDTestIgnoreSentinel sentinel], + } mutableCopy]; +} + - (NSString *)dummyEccCertificate { return @"MIIDNzCCAh-gAwIBAgIQKBcXojifRIxLIuut33ZknzANBgkqhkiG9w0BAQsFADB4MXYwEQYKCZImiZPyLGQBGRYDbmV0MBUGCgmSJomT8ixkARkWB3dpbmRvd3MwHQYDVQQDExZNUy1Pcmdhbml6YXRpb24tQWNjZXNzMCsGA1UECxMkODJkYmFjYTQtM2U4MS00NmNhLTljNzMtMDk1MGMxZWFjYTk3MB4XDTIzMDMxMzIxMjk0OFoXDTMzMDMxMzIxNTk0OFowLzEtMCsGA1UEAxMkOWVlNWYzM2ItOTc0OS00M2U3LTk1NjctODMxOGVhNDEyNTRiMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEl-xbT_nXgQkkzQOX7NPrvh9vPMt7yrzLqBthSpZXuIjV77izK_GW91qHTzZImhwbvXG6AcVH9Qs7ilN-VIb9xaOB0DCBzTAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB_wQMMAoGCCsGAQUFBwMCMA4GA1UdDwEB_wQEAwIHgDAiBgsqhkiG9xQBBYIcAgQTBIEQo8MK5pvg9k-6UZTxtj7IITAiBgsqhkiG9xQBBYIcAwQTBIEQj-LgHz1F-kSyqt3J40Sn7zAiBgsqhkiG9xQBBYIcBQQTBIEQkq1F9o3jGk21ENGwmnSoyjAUBgsqhkiG9xQBBYIcCAQFBIECTkEwEwYLKoZIhvcUAQWCHAcEBASBATAwDQYJKoZIhvcNAQELBQADggEBAFYbeUHpPcZj6Z8BcPhQ59dOi3-aGSYKX6Ub6GBv1CgiqU9EJ-P6VOipCL5dR458nMXJ4j97_pOXwPT0sS1rSTJ8_x3YpGLIJXpvkqDEHIoUvX1sR1tOlvXhUiP0O6l35-sil1itUZAKqS7RZtd8TWnMIgw3rCHbDHA9OlagunL6o75YC5Y74VdedZbCUjTy-IuU_VKM5gpa3c6uf_QleYgdQFlDjMH9w4TkqaWNONNoYulLZI8AykT9QtYB0iAsFr4KRL58ot1svOhqMil9vKDTkDrixEyThCcHmyyHeNoBjmXtaubOAiE3cMoJs7bV7I1uOS9aAI-Hm0W9NV-CkeE"; diff --git a/IdentityCore/tests/MSIDDeviceTokenUtilTests.m b/IdentityCore/tests/MSIDDeviceTokenUtilTests.m new file mode 100644 index 0000000000..4a1a8210e6 --- /dev/null +++ b/IdentityCore/tests/MSIDDeviceTokenUtilTests.m @@ -0,0 +1,495 @@ +// +// 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 +#import "MSIDDeviceTokenUtil.h" +#import "MSIDWPJKeyPairWithCert.h" +#import "MSIDRequestParameters.h" +#import "MSIDAADAuthority.h" +#import "MSIDAuthenticationScheme.h" +#import "MSIDHttpRequest.h" +#import "MSIDOAuth2Constants.h" +#import "MSIDConstants.h" +#import "MSIDError.h" +#import "MSIDTokenResult.h" +#import "MSIDAccessToken.h" +#import "MSIDDeviceTokenResponseHandler.h" +#import "MSIDAADV2Oauth2Factory.h" +#import "MSIDCachedNonce.h" +#import "MSIDNonceTokenRequestMock.h" +#import "NSData+MSIDExtensions.h" +#import "NSURL+MSIDExtensions.h" +#import "NSDictionary+MSIDTestUtil.h" +#import "MSIDTestIdentifiers.h" +#import "MSIDTestIdTokenUtil.h" +#import "MSIDTestURLSession.h" +#import "MSIDTestURLResponse.h" +#import "MSIDTestURLResponse+Util.h" +#import "MSIDAADNetworkConfiguration.h" +#import "MSIDAuthority.h" +#import "MSIDExternalSSOContextMock.h" + +#pragma mark - Test subclass + +// Subclass that overrides the workplace-join lookup seam so tests can inject a fake +// registration without swizzling or touching the keychain. +@interface MSIDDeviceTokenUtilTestMock : MSIDDeviceTokenUtil + +@property (class, nonatomic, strong, nullable) MSIDWPJKeyPairWithCert *stubbedRegistration; + +// Re-declared private seam from MSIDDeviceTokenUtil so we can override it. ++ (nullable MSIDWPJKeyPairWithCert *)deviceRegistrationForTenantId:(nullable NSString *)tenantId + context:(nullable id)context; + +@end + +@implementation MSIDDeviceTokenUtilTestMock + +static MSIDWPJKeyPairWithCert *gStubbedRegistration = nil; + ++ (MSIDWPJKeyPairWithCert *)stubbedRegistration { return gStubbedRegistration; } ++ (void)setStubbedRegistration:(MSIDWPJKeyPairWithCert *)stubbedRegistration { gStubbedRegistration = stubbedRegistration; } + ++ (MSIDWPJKeyPairWithCert *)deviceRegistrationForTenantId:(__unused NSString *)tenantId + context:(__unused id)context +{ + return gStubbedRegistration; +} + +@end + +// Re-declare the private workplace-join lookup seam on the base class so tests can invoke +// its real implementation directly (without going through the overriding mock subclass). +@interface MSIDDeviceTokenUtil (Testing) ++ (nullable MSIDWPJKeyPairWithCert *)deviceRegistrationForTenantId:(nullable NSString *)tenantId + context:(nullable id)context; +@end + +#pragma mark - Tests + +@interface MSIDDeviceTokenUtilTests : XCTestCase +@end + +@implementation MSIDDeviceTokenUtilTests + +- (void)setUp +{ + [super setUp]; + [MSIDDeviceTokenUtilTestMock setStubbedRegistration:nil]; + [MSIDTestURLSession clearResponses]; +} + +- (void)tearDown +{ + MSIDDeviceTokenUtilTestMock.stubbedRegistration = nil; + [MSIDTestURLSession clearResponses]; + [super tearDown]; +} + +#pragma mark - Helpers + +- (MSIDRequestParameters *)defaultRequestParametersWithCommonAuthority +{ + MSIDRequestParameters *requestParams = [MSIDRequestParameters new]; + requestParams.clientId = @"my_client_id"; + requestParams.redirectUri = @"my_redirect_uri"; + requestParams.target = @"scope1 scope2"; + requestParams.authScheme = [MSIDAuthenticationScheme new]; + requestParams.correlationId = [NSUUID UUID]; + requestParams.authority = [[MSIDAADAuthority alloc] initWithURL:[NSURL URLWithString:@"https://login.microsoftonline.com/common"] + rawTenant:nil + context:nil + error:nil]; + return requestParams; +} + +- (NSString *)dummyEccCertificate +{ + return @"MIIDNzCCAh-gAwIBAgIQKBcXojifRIxLIuut33ZknzANBgkqhkiG9w0BAQsFADB4MXYwEQYKCZImiZPyLGQBGRYDbmV0MBUGCgmSJomT8ixkARkWB3dpbmRvd3MwHQYDVQQDExZNUy1Pcmdhbml6YXRpb24tQWNjZXNzMCsGA1UECxMkODJkYmFjYTQtM2U4MS00NmNhLTljNzMtMDk1MGMxZWFjYTk3MB4XDTIzMDMxMzIxMjk0OFoXDTMzMDMxMzIxNTk0OFowLzEtMCsGA1UEAxMkOWVlNWYzM2ItOTc0OS00M2U3LTk1NjctODMxOGVhNDEyNTRiMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEl-xbT_nXgQkkzQOX7NPrvh9vPMt7yrzLqBthSpZXuIjV77izK_GW91qHTzZImhwbvXG6AcVH9Qs7ilN-VIb9xaOB0DCBzTAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB_wQMMAoGCCsGAQUFBwMCMA4GA1UdDwEB_wQEAwIHgDAiBgsqhkiG9xQBBYIcAgQTBIEQo8MK5pvg9k-6UZTxtj7IITAiBgsqhkiG9xQBBYIcAwQTBIEQj-LgHz1F-kSyqt3J40Sn7zAiBgsqhkiG9xQBBYIcBQQTBIEQkq1F9o3jGk21ENGwmnSoyjAUBgsqhkiG9xQBBYIcCAQFBIECTkEwEwYLKoZIhvcUAQWCHAcEBASBATAwDQYJKoZIhvcNAQELBQADggEBAFYbeUHpPcZj6Z8BcPhQ59dOi3-aGSYKX6Ub6GBv1CgiqU9EJ-P6VOipCL5dR458nMXJ4j97_pOXwPT0sS1rSTJ8_x3YpGLIJXpvkqDEHIoUvX1sR1tOlvXhUiP0O6l35-sil1itUZAKqS7RZtd8TWnMIgw3rCHbDHA9OlagunL6o75YC5Y74VdedZbCUjTy-IuU_VKM5gpa3c6uf_QleYgdQFlDjMH9w4TkqaWNONNoYulLZI8AykT9QtYB0iAsFr4KRL58ot1svOhqMil9vKDTkDrixEyThCcHmyyHeNoBjmXtaubOAiE3cMoJs7bV7I1uOS9aAI-Hm0W9NV-CkeE"; +} + +- (NSString *)dummyPrivateKey +{ + return @"MIIEowIBAAKCAQEA1H1ZmEe+OrXboN63oF8i+H649IHZaPySEnjQYF61TXS6vg0j2EC5e43xql3AG43NgDVW7ZrwtFvm5xIvXKCnN3BoQCi6JtUN6K7eZCnFdQIdrAV2Pyq5zkl9RItziKKFg+Gf92Bz5TQVgP3i/mb2xZe5fabNa0Jdj9tMSlq1QppDTyV01NOqk+AfPNwJsFlMZegGFdjLC3thGIgJEywmCaJacg+SBx2Vp3DawnuFMhWp1WRHJweZWZScCTCApiE5HJY4zMI44NJPOLUkUnN6zc7Yzw0AXKIZBid99OWlhJ6jQ92ayQEzmfNZM0IRRtl1VeU5TOQ1NcvKSyQFQ5uyvQIDAQABAoIBAEmRRI3GeQQWpn2h3m11wsPKC/sLYdxJZcFjdrGG2LqCaY0XO4vJjO5MDJlxb+uaQsXascf91sx67QyfbSpirMIy9sUP1LNRHEmtEW4YUDbcjq1aDsB76GyVYPt0VIG/0v4ABcQ97qIyUCeivw5ZU6LBjwUD1ScHiSEfSeCMWyk9YgRUozM3yZOpvugwjOF7efEjVlWvGvIfh9U/Xyeuj+NJ3r8zW87K+ySzGwrPwEmfBBfyd5LOqZzPJAKGJ3og8oaMDf4IWV7iSicCcPCbq6psj+B/i4HZc9u3MqE7YjKVbNG2S6qDsLUxpWfct72ZeKtfZcb3Kqa1nh0RximqUoECgYEA6UfzvsHg283KOTxQqX3v2IDbtwv73wKd/+V8sq8mhtjJhv5SpyJe99L/cuoxTu2ELUPmKIP++b7oyMvdRNyJaLIMRYDGGAKeLXXtWht//tCmHXyOZDhs9oHC3EMNHmqXBDSObL/CbN5puAQbCjofUX5zTNMdmq0qTOPYUezxjHECgYEA6S8JXstQ5RL+pmonoFlPWfuwXL8chpGJH1iOab1WYwjAbszSy1LJBQu5dWhKcqLl3EFywJgWRUKGFlQ/099XRVTHl4YhjEN054GEBxsTkZUhwXh0l0v6lPnsG6daWcTZ44gh4FXtqfPD/5/RcWUfhYQW0NoeIzviWt8MqZ6NIQ0CgYEA1TDpg/qBOb9vQTFq8grix7Szlyx/iYZFyNf8RvwktHWobxM7i/ywV8HfrDB00ZHlCs0TqRFAUxNygBc3Zzg455JX/qi54LV7w0YTnRamucQLG8V6CAM9KWbbIxqwAY0d6DzzsFTrJT151i8CWy1U89AhJSOG2ZXJo61SQ0TMVzECgYA6w8PUw+BLGpJaVf5OhrNctfUoKnGB6ENqRuL8+t4+bwIv6iZlXyORxfajA/lfEnZjH4tPxgQ2yCEKl4jOWEaiDk+OfBsQQh/AB//B2qz/z1mGbFjVmCw6RxGdlntKjDVtBe2jn4QZhHksfpZFwXpEJ5moYI+fyYOt6vBB/tcKMQKBgD7q4f036ad5TeX14vsFSSkGeOJrbUw0UqYeUit9B8DICwrV42/z60kTXxGg+2Wo8gL5Fo2tKCUe34BvvpMP92EKB/qbjoIirbZVnEDP9K1rCdGdEaYzDlRXsQ/p/bM6Tz3X++wpnqcDQhJp6lTDVLaX4faSQjWuVVIHVn1zpvIr"; +} + +- (MSIDWPJKeyPairWithCert *)dummyRegistration +{ + NSData *certData = [NSData msidDataFromBase64UrlEncodedString:[self dummyEccCertificate]]; + SecCertificateRef certRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData); + + NSDictionary *keyAttr = @{(__bridge NSString *)kSecAttrKeyType : (__bridge NSString *)kSecAttrKeyTypeRSA, + (__bridge NSString *)kSecAttrKeyClass : (__bridge NSString *)kSecAttrKeyClassPrivate}; + NSData *keyData = [NSData msidDataFromBase64UrlEncodedString:[self dummyPrivateKey]]; + SecKeyRef keyRef = SecKeyCreateWithData((__bridge CFDataRef)keyData, (__bridge CFDictionaryRef)keyAttr, NULL); + + MSIDWPJKeyPairWithCert *registration = [[MSIDWPJKeyPairWithCert alloc] initWithPrivateKey:keyRef + certificate:certRef + certificateIssuer:@"issuer"]; + if (certRef) CFRelease(certRef); + if (keyRef) CFRelease(keyRef); + return registration; +} + +- (NSDictionary *)decodeJwtPayloadSegment:(NSString *)segment +{ + NSData *payloadData = [NSData msidDataFromBase64UrlEncodedString:segment]; + return [NSJSONSerialization JSONObjectWithData:payloadData options:0 error:nil]; +} + +- (NSDictionary *)validDeviceTokenJson +{ + NSString *clientInfoString = [@{ @"uid" : DEFAULT_TEST_UID, @"utid" : DEFAULT_TEST_UTID } msidBase64UrlJson]; + return @{ + @"token_type" : @"Bearer", + @"access_token" : @"device-access-token", + @"expires_in" : @"3600", + @"scope" : @"scope1 scope2", + @"client_info" : clientInfoString, + @"id_token" : [MSIDTestIdTokenUtil defaultV2IdToken] + }; +} + +// A permissive request-header matcher: every header the request may send is mapped to an +// ignore sentinel so matching succeeds on URL/body regardless of header values. +- (NSMutableDictionary *)permissiveIgnoreRequestHeaders +{ + NSArray *keys = @[ @"Accept", @"Content-Length", @"Content-Type", @"User-Agent", + @"x-client-SKU", @"x-client-OS", @"x-app-name", @"x-app-ver", + @"x-ms-PkeyAuth+", @"x-client-Ver", @"x-client-CPU", @"x-client-DM", + @"Connection", @"client-request-id", @"return-client-request-id", + @"x-client-current-telemetry", @"x-client-last-telemetry", + @"x-client-last-endpoint", @"x-client-last-error", + @"x-client-last-request", @"x-client-last-response-time", + @"X-AnchorMailbox" ]; + NSMutableDictionary *headers = [NSMutableDictionary new]; + for (NSString *key in keys) + { + headers[key] = [[MSIDTestIgnoreSentinel alloc] init]; + } + return headers; +} + +#pragma mark - getDeviceTokenEndpoint:tenantId: + +- (void)testGetDeviceTokenEndpoint_whenCommonAuthority_shouldReplaceCommonWithTenantIdAndAppendPath +{ + MSIDRequestParameters *requestParams = [self defaultRequestParametersWithCommonAuthority]; + + NSURL *endpoint = [MSIDDeviceTokenUtil getDeviceTokenEndpoint:requestParams tenantId:@"my-tenant-id"]; + + XCTAssertEqualObjects(endpoint.absoluteString, @"https://login.microsoftonline.com/my-tenant-id/oauth2/v2.0/token"); +} + +- (void)testGetDeviceTokenEndpoint_whenTenantedAuthority_shouldAppendPathWithoutReplacingTenant +{ + MSIDRequestParameters *requestParams = [MSIDRequestParameters new]; + requestParams.authority = [[MSIDAADAuthority alloc] initWithURL:[NSURL URLWithString:@"https://login.microsoftonline.com/contoso.com"] + rawTenant:nil + context:nil + error:nil]; + + NSURL *endpoint = [MSIDDeviceTokenUtil getDeviceTokenEndpoint:requestParams tenantId:@"unused-tenant-id"]; + + XCTAssertEqualObjects(endpoint.absoluteString, @"https://login.microsoftonline.com/contoso.com/oauth2/v2.0/token"); +} + +- (void)testGetDeviceTokenEndpoint_whenRequestParametersNil_shouldReturnNil +{ + MSIDRequestParameters *requestParams = nil; + NSURL *endpoint = [MSIDDeviceTokenUtil getDeviceTokenEndpoint:requestParams tenantId:@"my-tenant-id"]; + XCTAssertNil(endpoint); +} + +- (void)testGetDeviceTokenEndpoint_whenAuthorityUrlNil_shouldReturnNil +{ + MSIDRequestParameters *requestParams = [MSIDRequestParameters new]; + requestParams.authority = nil; + + NSURL *endpoint = [MSIDDeviceTokenUtil getDeviceTokenEndpoint:requestParams tenantId:@"my-tenant-id"]; + XCTAssertNil(endpoint); +} + +- (void)testGetDeviceTokenEndpoint_whenCommonAuthorityAndBlankTenantId_shouldReturnNil +{ + MSIDRequestParameters *requestParams = [self defaultRequestParametersWithCommonAuthority]; + + NSURL *endpoint = [MSIDDeviceTokenUtil getDeviceTokenEndpoint:requestParams tenantId:@""]; + XCTAssertNil(endpoint); +} + +#pragma mark - deviceTokenRequestBodyParametersWithJwt:enrollmentId:extraParameters: + +- (void)testDeviceTokenRequestBodyParameters_whenBasic_shouldContainClientInfoGrantTypeAndRequest +{ + NSDictionary *body = [MSIDDeviceTokenUtil deviceTokenRequestBodyParametersWithJwt:@"signed.jwt.value" + enrollmentId:nil + extraParameters:nil]; + + XCTAssertEqualObjects(body[MSID_OAUTH2_CLIENT_INFO], @NO); + XCTAssertEqualObjects(body[MSID_OAUTH2_GRANT_TYPE], MSID_OAUTH2_JWT_BEARER_VALUE); + XCTAssertEqualObjects(body[@"request"], @"signed.jwt.value"); + XCTAssertNil(body[MSID_ENROLLMENT_ID]); +} + +- (void)testDeviceTokenRequestBodyParameters_whenEnrollmentIdProvided_shouldIncludeEnrollmentId +{ + NSDictionary *body = [MSIDDeviceTokenUtil deviceTokenRequestBodyParametersWithJwt:@"signed.jwt.value" + enrollmentId:@"enrollment-123" + extraParameters:nil]; + + XCTAssertEqualObjects(body[MSID_ENROLLMENT_ID], @"enrollment-123"); +} + +- (void)testDeviceTokenRequestBodyParameters_whenEnrollmentIdBlank_shouldOmitEnrollmentId +{ + NSDictionary *body = [MSIDDeviceTokenUtil deviceTokenRequestBodyParametersWithJwt:@"signed.jwt.value" + enrollmentId:@"" + extraParameters:nil]; + + XCTAssertNil(body[MSID_ENROLLMENT_ID]); +} + +- (void)testDeviceTokenRequestBodyParameters_whenExtraParametersProvided_shouldMergeThem +{ + NSDictionary *body = [MSIDDeviceTokenUtil deviceTokenRequestBodyParametersWithJwt:@"signed.jwt.value" + enrollmentId:nil + extraParameters:@{@"extra_key" : @"extra_value"}]; + + XCTAssertEqualObjects(body[@"extra_key"], @"extra_value"); + XCTAssertEqualObjects(body[MSID_OAUTH2_GRANT_TYPE], MSID_OAUTH2_JWT_BEARER_VALUE); +} + +#pragma mark - getDeviceTokenRequestJwtForResource:... + +- (void)testGetDeviceTokenRequestJwt_whenValidInputs_shouldReturnSignedJwtWithThreeSegments +{ + NSError *error; + NSString *jwt = [MSIDDeviceTokenUtil getDeviceTokenRequestJwtForResource:@"https://graph.microsoft.com" + scopes:[NSSet setWithArray:@[@"scope1", @"scope2"]] + redirectUri:@"my_redirect_uri" + audience:@"https://login.microsoftonline.com/tenantId/oauth2/v2.0/token" + clientId:@"my_client_id" + nonce:@"test-nonce" + registrationInformation:[self dummyRegistration] + extraPayloadClaims:nil + context:nil + error:&error]; + + XCTAssertNil(error); + XCTAssertNotNil(jwt); + XCTAssertEqual([jwt componentsSeparatedByString:@"."].count, 3); +} + +- (void)testGetDeviceTokenRequestJwt_whenValidInputs_shouldContainExpectedPayloadClaims +{ + NSString *jwt = [MSIDDeviceTokenUtil getDeviceTokenRequestJwtForResource:@"https://graph.microsoft.com" + scopes:[NSSet setWithArray:@[@"scope1"]] + redirectUri:@"my_redirect_uri" + audience:@"https://login.microsoftonline.com/tenantId/oauth2/v2.0/token" + clientId:@"my_client_id" + nonce:@"test-nonce" + registrationInformation:[self dummyRegistration] + extraPayloadClaims:@{@"custom_claim" : @"custom_value"} + context:nil + error:nil]; + + NSArray *segments = [jwt componentsSeparatedByString:@"."]; + NSDictionary *payload = [self decodeJwtPayloadSegment:segments[1]]; + + XCTAssertEqualObjects(payload[MSID_OAUTH2_GRANT_TYPE], MSID_OAUTH2_DEVICE_TOKEN); + XCTAssertEqualObjects(payload[@"aud"], @"https://login.microsoftonline.com/tenantId/oauth2/v2.0/token"); + XCTAssertEqualObjects(payload[@"iss"], @"my_client_id"); + XCTAssertEqualObjects(payload[MSID_OAUTH2_CLIENT_ID], @"my_client_id"); + XCTAssertEqualObjects(payload[MSID_OAUTH2_REDIRECT_URI], @"my_redirect_uri"); + XCTAssertEqualObjects(payload[@"resource"], @"https://graph.microsoft.com"); + XCTAssertEqualObjects(payload[@"request_nonce"], @"test-nonce"); + XCTAssertEqualObjects(payload[MSID_OAUTH2_SCOPE], @"scope1"); + XCTAssertEqualObjects(payload[@"custom_claim"], @"custom_value"); +} + +- (void)testGetDeviceTokenRequestJwt_whenNonceBlank_shouldOmitRequestNonceClaim +{ + NSString *jwt = [MSIDDeviceTokenUtil getDeviceTokenRequestJwtForResource:@"https://graph.microsoft.com" + scopes:nil + redirectUri:@"my_redirect_uri" + audience:@"https://login.microsoftonline.com/tenantId/oauth2/v2.0/token" + clientId:@"my_client_id" + nonce:@"" + registrationInformation:[self dummyRegistration] + extraPayloadClaims:nil + context:nil + error:nil]; + + NSArray *segments = [jwt componentsSeparatedByString:@"."]; + NSDictionary *payload = [self decodeJwtPayloadSegment:segments[1]]; + + XCTAssertNil(payload[@"request_nonce"]); + XCTAssertNil(payload[MSID_OAUTH2_SCOPE]); +} + +- (void)testGetDeviceTokenRequestJwt_whenSigningKeyUnusable_shouldReturnNil +{ + // A registration with no signable private key cannot produce a signing algorithm, + // so JWT creation fails and returns nil. + NSString *jwt = [MSIDDeviceTokenUtil getDeviceTokenRequestJwtForResource:@"https://graph.microsoft.com" + scopes:nil + redirectUri:@"my_redirect_uri" + audience:@"https://login.microsoftonline.com/tenantId/oauth2/v2.0/token" + clientId:@"my_client_id" + nonce:@"test-nonce" + registrationInformation:[MSIDWPJKeyPairWithCertMock new] + extraPayloadClaims:nil + context:nil + error:nil]; + + XCTAssertNil(jwt); +} + +#pragma mark - handleDeviceTokenResponse:... + +- (void)testHandleDeviceTokenResponse_whenErrorProvided_shouldForwardError +{ + MSIDRequestParameters *requestParams = [self defaultRequestParametersWithCommonAuthority]; + MSIDDeviceTokenResponseHandler *handler = [[MSIDDeviceTokenResponseHandler alloc] initWithRequestParameters:requestParams + oauthFactory:[MSIDAADV2Oauth2Factory new]]; + NSError *inputError = MSIDCreateError(MSIDErrorDomain, MSIDErrorServerInvalidGrant, @"bad grant", nil, nil, nil, nil, nil, NO); + + XCTestExpectation *expectation = [self expectationWithDescription:@"handleDeviceTokenResponse completion called."]; + [MSIDDeviceTokenUtil handleDeviceTokenResponse:nil + requestParameters:requestParams + responseHandler:handler + error:inputError + completionBlock:^(MSIDTokenResult * _Nullable result, NSError * _Nullable error) + { + XCTAssertNil(result); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, MSIDErrorServerInvalidGrant); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testHandleDeviceTokenResponse_whenNilResponseHandlerAndErrorProvided_shouldForwardError +{ + MSIDRequestParameters *requestParams = [self defaultRequestParametersWithCommonAuthority]; + NSError *inputError = MSIDCreateError(MSIDErrorDomain, MSIDErrorServerInvalidGrant, @"bad grant", nil, nil, nil, nil, nil, NO); + + XCTestExpectation *expectation = [self expectationWithDescription:@"handleDeviceTokenResponse completion called."]; + [MSIDDeviceTokenUtil handleDeviceTokenResponse:nil + requestParameters:requestParams + responseHandler:nil + error:inputError + completionBlock:^(MSIDTokenResult * _Nullable result, NSError * _Nullable error) + { + XCTAssertNil(result); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, MSIDErrorServerInvalidGrant); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testHandleDeviceTokenResponse_whenValidResponse_shouldReturnTokenResult +{ + MSIDRequestParameters *requestParams = [self defaultRequestParametersWithCommonAuthority]; + MSIDDeviceTokenResponseHandler *handler = [[MSIDDeviceTokenResponseHandler alloc] initWithRequestParameters:requestParams + oauthFactory:[MSIDAADV2Oauth2Factory new]]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"handleDeviceTokenResponse completion called."]; + [MSIDDeviceTokenUtil handleDeviceTokenResponse:[self validDeviceTokenJson] + requestParameters:requestParams + responseHandler:handler + error:nil + completionBlock:^(MSIDTokenResult * _Nullable result, NSError * _Nullable error) + { + XCTAssertNil(error); + XCTAssertNotNil(result); + XCTAssertEqualObjects(result.accessToken.accessToken, @"device-access-token"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +#pragma mark - getDeviceTokenRequest:... + +- (void)testGetDeviceTokenRequest_whenRequestParametersNil_shouldReturnError +{ + MSIDRequestParameters *requestParams = nil; + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion called."]; + [MSIDDeviceTokenUtilTestMock getDeviceTokenRequest:requestParams + tenantId:@"tenantId" + resource:@"https://graph.microsoft.com" + enrollmentId:nil + extraParameters:nil + ssoContext:nil + completionBlock:^(MSIDHttpRequest * _Nullable deviceTokenRequest, NSError * _Nullable error) + { + XCTAssertNil(deviceTokenRequest); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, MSIDErrorInvalidInternalParameter); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testGetDeviceTokenRequest_whenResourceBlank_shouldReturnError +{ + MSIDRequestParameters *requestParams = [self defaultRequestParametersWithCommonAuthority]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion called."]; + [MSIDDeviceTokenUtilTestMock getDeviceTokenRequest:requestParams + tenantId:@"tenantId" + resource:@"" + enrollmentId:nil + extraParameters:nil + ssoContext:nil + completionBlock:^(MSIDHttpRequest * _Nullable deviceTokenRequest, NSError * _Nullable error) + { + XCTAssertNil(deviceTokenRequest); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, MSIDErrorInvalidInternalParameter); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +#pragma mark - deviceRegistrationForTenantId:context: (base seam) + +- (void)testDeviceRegistrationForTenantId_whenNoRegistrationInKeychain_shouldReturnNil +{ + // Exercises the real seam implementation (workplace-join keychain lookup), which returns + // nil in the test environment where no device registration is present. + MSIDWPJKeyPairWithCert *registration = [MSIDDeviceTokenUtil deviceRegistrationForTenantId:@"some-tenant" + context:nil]; + XCTAssertNil(registration); +} + +@end + diff --git a/IdentityCore/tests/MSIDFlightManagerTests.swift b/IdentityCore/tests/MSIDFlightManagerTests.swift index 9a25463ee9..6bf822006c 100644 --- a/IdentityCore/tests/MSIDFlightManagerTests.swift +++ b/IdentityCore/tests/MSIDFlightManagerTests.swift @@ -208,6 +208,46 @@ class MSIDFlightManagerTests: XCTestCase { XCTAssertTrue(mockFlightProvider.stringForKeyCalled) } + func testConcurrentReads_whileProviderSwappedAndCleared_doNotCrash() { + let flightManager = MSIDFlightManager.sharedInstance() + + let iterations = 100 + let operationsPerIteration = 3 + let expectation = XCTestExpectation(description: "All operations complete") + expectation.expectedFulfillmentCount = iterations * operationsPerIteration + + // Interleave provider swaps (including nil) with concurrent reads. Before the reader + // fix, boolForKey:/stringForKey: tested the provider through the unsynchronized getter + // before dispatching onto the synchronization queue, so a swap-and-release racing a + // read could leave the read messaging a freed provider. Reading the provider once from + // inside the queue into a strong local removes that window. + for i in 0.. Bool { - boolForKeyCalled = true - lastBoolKey = flightKey + lock.lock() + _boolForKeyCalled = true + _lastBoolKey = flightKey + lock.unlock() return boolValues[flightKey] ?? false } func string(forKey key: String) -> String? { - stringForKeyCalled = true - lastStringKey = key + lock.lock() + _stringForKeyCalled = true + _lastStringKey = key + lock.unlock() return stringValues[key] } } diff --git a/IdentityCore/tests/MSIDOAuth2EmbeddedWebviewControllerTests.m b/IdentityCore/tests/MSIDOAuth2EmbeddedWebviewControllerTests.m index 7fbac7ce76..60af353dee 100644 --- a/IdentityCore/tests/MSIDOAuth2EmbeddedWebviewControllerTests.m +++ b/IdentityCore/tests/MSIDOAuth2EmbeddedWebviewControllerTests.m @@ -31,13 +31,13 @@ #import "MSIDFlightManagerMockProvider.h" #import "MSIDConstants.h" #import "MSIDOnboardingBlobFieldKeys.h" +#import "MSIDOnboardingBlobBuilder.h" #if !MSID_EXCLUDE_WEBKIT // Expose private methods for testing @interface MSIDOAuth2EmbeddedWebviewController (Testing) - (BOOL)shouldOpenURLInSystemBrowser:(NSURL *)url targetFrame:(WKFrameInfo *)targetFrame; -- (NSString *)onboardingStepForEndURL:(NSURL *)endURL; @end @interface MSIDOAuth2EmbeddedWebviewControllerTests : XCTestCase @@ -162,113 +162,164 @@ - (void)testShouldOpenURL_whenNilURL_shouldReturnNo - (void)testOnboardingStepForFwlinkEndURL_whenLinkId396941_shouldReturnMdmEnrollmentStarted { - MSIDOAuth2EmbeddedWebviewController *webVC = [self createTestWebviewController]; NSURL *url = [NSURL URLWithString:@"browser://go.microsoft.com/fwlink/?LinkId=396941"]; - XCTAssertEqualObjects([webVC onboardingStepForEndURL:url], MSIDOnboardingBlobStepMdmEnrollmentStarted); + XCTAssertEqualObjects([MSIDOnboardingBlobBuilder onboardingStepForEndURL:url], MSIDOnboardingBlobStepMdmEnrollmentStarted); } - (void)testOnboardingStepForFwlinkEndURL_whenLinkId2132314Lowercase_shouldReturnMdmEnrollmentStarted { - MSIDOAuth2EmbeddedWebviewController *webVC = [self createTestWebviewController]; NSURL *url = [NSURL URLWithString:@"browser://go.microsoft.com/fwlink/?linkid=2132314"]; - XCTAssertEqualObjects([webVC onboardingStepForEndURL:url], MSIDOnboardingBlobStepMdmEnrollmentStarted); + XCTAssertEqualObjects([MSIDOnboardingBlobBuilder onboardingStepForEndURL:url], MSIDOnboardingBlobStepMdmEnrollmentStarted); } - (void)testOnboardingStepForFwlinkEndURL_whenLinkId2114747Lowercase_shouldReturnMdmEnrollmentStarted { - MSIDOAuth2EmbeddedWebviewController *webVC = [self createTestWebviewController]; NSURL *url = [NSURL URLWithString:@"browser://go.microsoft.com/fwlink/?linkid=2114747"]; - XCTAssertEqualObjects([webVC onboardingStepForEndURL:url], MSIDOnboardingBlobStepMdmEnrollmentStarted); + XCTAssertEqualObjects([MSIDOnboardingBlobBuilder onboardingStepForEndURL:url], MSIDOnboardingBlobStepMdmEnrollmentStarted); } - (void)testOnboardingStepForFwlinkEndURL_whenLinkId399153_shouldReturnMdmEnrollmentStarted { - MSIDOAuth2EmbeddedWebviewController *webVC = [self createTestWebviewController]; NSURL *url = [NSURL URLWithString:@"browser://go.microsoft.com/fwlink/?LinkId=399153"]; - XCTAssertEqualObjects([webVC onboardingStepForEndURL:url], MSIDOnboardingBlobStepMdmEnrollmentStarted); + XCTAssertEqualObjects([MSIDOnboardingBlobBuilder onboardingStepForEndURL:url], MSIDOnboardingBlobStepMdmEnrollmentStarted); } - (void)testOnboardingStepForFwlinkEndURL_whenNoTrailingSlash_shouldReturnMdmEnrollmentStarted { - MSIDOAuth2EmbeddedWebviewController *webVC = [self createTestWebviewController]; NSURL *url = [NSURL URLWithString:@"browser://go.microsoft.com/fwlink?LinkId=396941"]; - XCTAssertEqualObjects([webVC onboardingStepForEndURL:url], MSIDOnboardingBlobStepMdmEnrollmentStarted); + XCTAssertEqualObjects([MSIDOnboardingBlobBuilder onboardingStepForEndURL:url], MSIDOnboardingBlobStepMdmEnrollmentStarted); } - (void)testOnboardingStepForFwlinkEndURL_whenLinkIdKeyUpperCase_shouldReturnMdmEnrollmentStarted { - MSIDOAuth2EmbeddedWebviewController *webVC = [self createTestWebviewController]; NSURL *url = [NSURL URLWithString:@"browser://go.microsoft.com/fwlink/?LINKID=396941"]; - XCTAssertEqualObjects([webVC onboardingStepForEndURL:url], MSIDOnboardingBlobStepMdmEnrollmentStarted); + XCTAssertEqualObjects([MSIDOnboardingBlobBuilder onboardingStepForEndURL:url], MSIDOnboardingBlobStepMdmEnrollmentStarted); } - (void)testOnboardingStepForFwlinkEndURL_whenExtraQueryParamsAndReorder_shouldReturnMdmEnrollmentStarted { - MSIDOAuth2EmbeddedWebviewController *webVC = [self createTestWebviewController]; NSURL *url = [NSURL URLWithString:@"browser://go.microsoft.com/fwlink/?clcid=0x409&LinkId=396941&foo=bar"]; - XCTAssertEqualObjects([webVC onboardingStepForEndURL:url], MSIDOnboardingBlobStepMdmEnrollmentStarted); + XCTAssertEqualObjects([MSIDOnboardingBlobBuilder onboardingStepForEndURL:url], MSIDOnboardingBlobStepMdmEnrollmentStarted); } - (void)testOnboardingStepForFwlinkEndURL_whenSchemeAndHostMixedCase_shouldReturnMdmEnrollmentStarted { - MSIDOAuth2EmbeddedWebviewController *webVC = [self createTestWebviewController]; NSURL *url = [NSURL URLWithString:@"BROWSER://Go.Microsoft.com/FwLink/?LinkId=396941"]; - XCTAssertEqualObjects([webVC onboardingStepForEndURL:url], MSIDOnboardingBlobStepMdmEnrollmentStarted); + XCTAssertEqualObjects([MSIDOnboardingBlobBuilder onboardingStepForEndURL:url], MSIDOnboardingBlobStepMdmEnrollmentStarted); } - (void)testOnboardingStepForFwlinkEndURL_whenNilURL_shouldReturnNil { - MSIDOAuth2EmbeddedWebviewController *webVC = [self createTestWebviewController]; - XCTAssertNil([webVC onboardingStepForEndURL:nil]); + XCTAssertNil([MSIDOnboardingBlobBuilder onboardingStepForEndURL:nil]); } - (void)testOnboardingStepForFwlinkEndURL_whenHttpsScheme_shouldReturnNil { - MSIDOAuth2EmbeddedWebviewController *webVC = [self createTestWebviewController]; NSURL *url = [NSURL URLWithString:@"https://go.microsoft.com/fwlink/?LinkId=396941"]; - XCTAssertNil([webVC onboardingStepForEndURL:url]); + XCTAssertNil([MSIDOnboardingBlobBuilder onboardingStepForEndURL:url]); } - (void)testOnboardingStepForFwlinkEndURL_whenWrongHost_shouldReturnNil { - MSIDOAuth2EmbeddedWebviewController *webVC = [self createTestWebviewController]; NSURL *url = [NSURL URLWithString:@"browser://go.example.com/fwlink/?LinkId=396941"]; - XCTAssertNil([webVC onboardingStepForEndURL:url]); + XCTAssertNil([MSIDOnboardingBlobBuilder onboardingStepForEndURL:url]); } - (void)testOnboardingStepForFwlinkEndURL_whenPathHasSuffix_shouldReturnNil { - MSIDOAuth2EmbeddedWebviewController *webVC = [self createTestWebviewController]; NSURL *url = [NSURL URLWithString:@"browser://go.microsoft.com/fwlink2/?LinkId=396941"]; - XCTAssertNil([webVC onboardingStepForEndURL:url]); + XCTAssertNil([MSIDOnboardingBlobBuilder onboardingStepForEndURL:url]); } - (void)testOnboardingStepForFwlinkEndURL_whenPathHasPrefix_shouldReturnNil { - MSIDOAuth2EmbeddedWebviewController *webVC = [self createTestWebviewController]; NSURL *url = [NSURL URLWithString:@"browser://go.microsoft.com/foo/fwlink?LinkId=396941"]; - XCTAssertNil([webVC onboardingStepForEndURL:url]); + XCTAssertNil([MSIDOnboardingBlobBuilder onboardingStepForEndURL:url]); } - (void)testOnboardingStepForFwlinkEndURL_whenUnknownLinkIdValue_shouldReturnNil { - MSIDOAuth2EmbeddedWebviewController *webVC = [self createTestWebviewController]; NSURL *url = [NSURL URLWithString:@"browser://go.microsoft.com/fwlink/?LinkId=12345"]; - XCTAssertNil([webVC onboardingStepForEndURL:url]); + XCTAssertNil([MSIDOnboardingBlobBuilder onboardingStepForEndURL:url]); } - (void)testOnboardingStepForFwlinkEndURL_whenLinkIdMissing_shouldReturnNil { - MSIDOAuth2EmbeddedWebviewController *webVC = [self createTestWebviewController]; NSURL *url = [NSURL URLWithString:@"browser://go.microsoft.com/fwlink/?foo=bar"]; - XCTAssertNil([webVC onboardingStepForEndURL:url]); + XCTAssertNil([MSIDOnboardingBlobBuilder onboardingStepForEndURL:url]); } - (void)testOnboardingStepForFwlinkEndURL_whenLinkIdValueEmpty_shouldReturnNil { - MSIDOAuth2EmbeddedWebviewController *webVC = [self createTestWebviewController]; NSURL *url = [NSURL URLWithString:@"browser://go.microsoft.com/fwlink/?LinkId="]; - XCTAssertNil([webVC onboardingStepForEndURL:url]); + XCTAssertNil([MSIDOnboardingBlobBuilder onboardingStepForEndURL:url]); +} + +#pragma mark - finalizeOnboardingTelemetry:error: + +- (MSIDOnboardingBlobBuilder *)builderForFinalizeTest +{ + NSDictionary *seed = @{@"schema_version": @"1.0.0", @"session_correlation_id": @"abc-123", @"onboarding_mode": @"non-brokered"}; + NSString *seedJson = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:seed options:0 error:nil] + encoding:NSUTF8StringEncoding]; + return [[MSIDOnboardingBlobBuilder alloc] initWithSeedJson:seedJson clientId:@"clientA" target:@"resource1"]; +} + +- (NSArray *)stampedStepIdsFromBuilder:(MSIDOnboardingBlobBuilder *)builder +{ + NSData *data = [[builder finalizeBlob] dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *parsed = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + NSMutableArray *stepIds = [NSMutableArray new]; + for (NSDictionary *step in parsed[@"steps_list"]) + { + [stepIds addObject:step[@"step_id"]]; + } + return stepIds; +} + +- (void)testFinalizeOnboardingTelemetry_whenBuilderStrongAuthFlagSetAndSuccess_shouldStampStrongAuthSetupCompleted +{ + MSIDOAuth2EmbeddedWebviewController *webVC = [self createTestWebviewController]; + MSIDOnboardingBlobBuilder *builder = [self builderForFinalizeTest]; + webVC.onboardingBlobBuilder = builder; + + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"https://login.microsoftonline.com"] + statusCode:200 + HTTPVersion:@"HTTP/1.1" + headerFields:@{@"x-ms-clitelem": @"2,50079,0,,"}]; + [webVC.onboardingBlobBuilder processResponseHeaders:response.allHeaderFields responseURL:response.URL]; + XCTAssertTrue(builder.strongAuthSetupStarted); + + [webVC.onboardingBlobBuilder finalizeForEndURL:[NSURL URLWithString:@"https://contoso.com/done"] error:nil]; + + XCTAssertTrue([[self stampedStepIdsFromBuilder:builder] containsObject:MSIDOnboardingBlobStepStrongAuthSetupCompleted]); +} + +- (void)testFinalizeOnboardingTelemetry_whenFlagSetButError_shouldNotStampCompleted +{ + MSIDOAuth2EmbeddedWebviewController *webVC = [self createTestWebviewController]; + MSIDOnboardingBlobBuilder *builder = [self builderForFinalizeTest]; + [builder processResponseHeaders:@{@"x-ms-clitelem": @"2,50079,0,,"} responseURL:[NSURL URLWithString:@"https://login.microsoftonline.com"]]; + webVC.onboardingBlobBuilder = builder; + + NSError *error = [NSError errorWithDomain:@"TestDomain" code:-1 userInfo:nil]; + [webVC.onboardingBlobBuilder finalizeForEndURL:[NSURL URLWithString:@"https://contoso.com/done"] error:error]; + + XCTAssertFalse([[self stampedStepIdsFromBuilder:builder] containsObject:MSIDOnboardingBlobStepStrongAuthSetupCompleted]); +} + +- (void)testFinalizeOnboardingTelemetry_whenNoStartedFlagAndSuccess_shouldNotStampCompleted +{ + MSIDOAuth2EmbeddedWebviewController *webVC = [self createTestWebviewController]; + MSIDOnboardingBlobBuilder *builder = [self builderForFinalizeTest]; + XCTAssertFalse(builder.strongAuthSetupStarted); + webVC.onboardingBlobBuilder = builder; + + [webVC.onboardingBlobBuilder finalizeForEndURL:[NSURL URLWithString:@"https://contoso.com/done"] error:nil]; + + NSArray *steps = [self stampedStepIdsFromBuilder:builder]; + XCTAssertFalse([steps containsObject:MSIDOnboardingBlobStepStrongAuthSetupCompleted]); + XCTAssertFalse([steps containsObject:MSIDOnboardingBlobStepMdmEnrollmentFinished]); } @end diff --git a/IdentityCore/tests/MSIDOnboardingBlobBuilderTests.m b/IdentityCore/tests/MSIDOnboardingBlobBuilderTests.m index ecc8513c50..0582c82511 100644 --- a/IdentityCore/tests/MSIDOnboardingBlobBuilderTests.m +++ b/IdentityCore/tests/MSIDOnboardingBlobBuilderTests.m @@ -26,6 +26,7 @@ #import "MSIDOnboardingBlobBuilder.h" #import "MSIDOnboardingBlobFieldKeys.h" #import "MSIDSessionCachePersistence.h" +#import "MSIDOAuth2Constants.h" static NSString * const kTestSuiteName = @"test.MSIDOnboardingBlobBuilderTests"; static NSString * const kCacheKey = @"com.microsoft.oneauth.session_correlation_cache"; @@ -726,4 +727,173 @@ - (void)testEnsureBrokeredOnboardingMode_whenAlreadyBrokered_shouldBeNoOp XCTAssertEqualObjects(parsed[@"onboarding_mode"], @"brokered"); } +#pragma mark - processResponseHeaders:responseURL: + +- (NSUInteger)countOfStep:(NSString *)stepId inBlob:(NSDictionary *)parsed +{ + NSUInteger count = 0; + for (NSDictionary *step in parsed[@"steps_list"]) + { + if ([step[@"step_id"] isEqualToString:stepId]) + { + count++; + } + } + return count; +} + +- (void)testProcessResponseHeaders_whenResponseURLHasHost_shouldSetLastLoadedDomain +{ + MSIDOnboardingBlobBuilder *builder = [self builderWithTestDefaults]; + + [builder processResponseHeaders:@{} responseURL:[NSURL URLWithString:@"https://login.microsoftonline.com/common/oauth2/authorize"]]; + + NSDictionary *parsed = [self parsedJsonFromBlob:[builder finalizeBlob]]; + XCTAssertEqualObjects(parsed[@"last_loaded_domain"], @"login.microsoftonline.com"); +} + +- (void)testProcessResponseHeaders_whenNoClitelemHeader_shouldNotRecordBlockingError +{ + MSIDOnboardingBlobBuilder *builder = [self builderWithTestDefaults]; + + [builder processResponseHeaders:@{} responseURL:[NSURL URLWithString:@"https://login.microsoftonline.com"]]; + + NSDictionary *parsed = [self parsedJsonFromBlob:[builder finalizeBlob]]; + XCTAssertEqual([parsed[@"blocking_errors"] count], 0); + XCTAssertEqual([parsed[@"steps_list"] count], 0); + XCTAssertFalse(builder.strongAuthSetupStarted); +} + +- (void)testProcessResponseHeaders_whenClitelemErrorCodeIsZero_shouldNotRecordBlockingError +{ + MSIDOnboardingBlobBuilder *builder = [self builderWithTestDefaults]; + + [builder processResponseHeaders:@{MSID_OAUTH2_CLIENT_TELEMETRY: @"2,0,0,,"} + responseURL:[NSURL URLWithString:@"https://login.microsoftonline.com"]]; + + NSDictionary *parsed = [self parsedJsonFromBlob:[builder finalizeBlob]]; + XCTAssertEqual([parsed[@"blocking_errors"] count], 0); + XCTAssertEqual([parsed[@"steps_list"] count], 0); +} + +- (void)testProcessResponseHeaders_whenClitelemMalformed_shouldNotRecordBlockingError +{ + MSIDOnboardingBlobBuilder *builder = [self builderWithTestDefaults]; + + [builder processResponseHeaders:@{MSID_OAUTH2_CLIENT_TELEMETRY: @"2"} + responseURL:[NSURL URLWithString:@"https://login.microsoftonline.com"]]; + + NSDictionary *parsed = [self parsedJsonFromBlob:[builder finalizeBlob]]; + XCTAssertEqual([parsed[@"blocking_errors"] count], 0); + XCTAssertEqual([parsed[@"steps_list"] count], 0); +} + +- (void)testProcessResponseHeaders_whenNonBlockingErrorCode_shouldNotRecordBlockingErrorOrStep +{ + MSIDOnboardingBlobBuilder *builder = [self builderWithTestDefaults]; + + [builder processResponseHeaders:@{MSID_OAUTH2_CLIENT_TELEMETRY: @"2,50126,0,,"} + responseURL:[NSURL URLWithString:@"https://login.microsoftonline.com"]]; + + NSDictionary *parsed = [self parsedJsonFromBlob:[builder finalizeBlob]]; + XCTAssertEqual([parsed[@"blocking_errors"] count], 0); + XCTAssertEqual([parsed[@"steps_list"] count], 0); +} + +- (void)testProcessResponseHeaders_whenStrongAuthSetupErrorCode_shouldRecordStepAndSetFlag +{ + MSIDOnboardingBlobBuilder *builder = [self builderWithTestDefaults]; + + [builder processResponseHeaders:@{MSID_OAUTH2_CLIENT_TELEMETRY: @"2,50079,0,,"} + responseURL:[NSURL URLWithString:@"https://login.microsoftonline.com"]]; + + XCTAssertTrue(builder.strongAuthSetupStarted); + + NSDictionary *parsed = [self parsedJsonFromBlob:[builder finalizeBlob]]; + XCTAssertEqualObjects(parsed[@"last_blocking_error"], @"50079"); + XCTAssertEqual([self countOfStep:MSIDOnboardingBlobStepStrongAuthSetupStarted inBlob:parsed], 1); +} + +- (void)testProcessResponseHeaders_whenStrongAuthSetupErrorCodeSeenTwice_shouldRecordStepOnce +{ + MSIDOnboardingBlobBuilder *builder = [self builderWithTestDefaults]; + + NSDictionary *headers = @{MSID_OAUTH2_CLIENT_TELEMETRY: @"2,50079,0,,"}; + NSURL *url = [NSURL URLWithString:@"https://login.microsoftonline.com"]; + [builder processResponseHeaders:headers responseURL:url]; + [builder processResponseHeaders:headers responseURL:url]; + + NSDictionary *parsed = [self parsedJsonFromBlob:[builder finalizeBlob]]; + XCTAssertEqual([self countOfStep:MSIDOnboardingBlobStepStrongAuthSetupStarted inBlob:parsed], 1); +} + +- (void)testProcessResponseHeaders_whenMdmEnrollmentRequiredErrorCode_shouldRecordStep +{ + MSIDOnboardingBlobBuilder *builder = [self builderWithTestDefaults]; + + [builder processResponseHeaders:@{MSID_OAUTH2_CLIENT_TELEMETRY: @"2,53000,0,,"} + responseURL:[NSURL URLWithString:@"https://login.microsoftonline.com"]]; + + NSDictionary *parsed = [self parsedJsonFromBlob:[builder finalizeBlob]]; + XCTAssertEqual([self countOfStep:MSIDOnboardingBlobStepMdmEnrollmentRequired inBlob:parsed], 1); +} + +- (void)testProcessResponseHeaders_whenDeviceRegistrationErrorCode_shouldRecordStep +{ + MSIDOnboardingBlobBuilder *builder = [self builderWithTestDefaults]; + + [builder processResponseHeaders:@{MSID_OAUTH2_CLIENT_TELEMETRY: @"2,50129,0,,"} + responseURL:[NSURL URLWithString:@"https://login.microsoftonline.com"]]; + + NSDictionary *parsed = [self parsedJsonFromBlob:[builder finalizeBlob]]; + XCTAssertEqual([self countOfStep:MSIDOnboardingBlobStepDeviceRegistrationRequired inBlob:parsed], 1); + XCTAssertFalse(builder.strongAuthSetupStarted); +} + +- (void)testProcessResponseHeaders_whenDeviceNotCompliantErrorCode_shouldRecordStep +{ + MSIDOnboardingBlobBuilder *builder = [self builderWithTestDefaults]; + + [builder processResponseHeaders:@{MSID_OAUTH2_CLIENT_TELEMETRY: @"2,530001,0,,"} + responseURL:[NSURL URLWithString:@"https://login.microsoftonline.com"]]; + + NSDictionary *parsed = [self parsedJsonFromBlob:[builder finalizeBlob]]; + XCTAssertEqual([self countOfStep:MSIDOnboardingBlobStepDeviceNotCompliant inBlob:parsed], 1); +} + +- (void)testProcessResponseHeaders_whenBrokerInstallForMamErrorCode_shouldRecordStep +{ + MSIDOnboardingBlobBuilder *builder = [self builderWithTestDefaults]; + + [builder processResponseHeaders:@{MSID_OAUTH2_CLIENT_TELEMETRY: @"2,50127,0,,"} + responseURL:[NSURL URLWithString:@"https://login.microsoftonline.com"]]; + + NSDictionary *parsed = [self parsedJsonFromBlob:[builder finalizeBlob]]; + XCTAssertEqual([self countOfStep:MSIDOnboardingBlobStepBrokerInstallPromptedForMAM inBlob:parsed], 1); +} + +- (void)testProcessResponseHeaders_whenBrokerInstallErrorCode_shouldRecordStep +{ + MSIDOnboardingBlobBuilder *builder = [self builderWithTestDefaults]; + + [builder processResponseHeaders:@{MSID_OAUTH2_CLIENT_TELEMETRY: @"2,501271,0,,"} + responseURL:[NSURL URLWithString:@"https://login.microsoftonline.com"]]; + + NSDictionary *parsed = [self parsedJsonFromBlob:[builder finalizeBlob]]; + XCTAssertEqual([self countOfStep:MSIDOnboardingBlobStepBrokerInstallPrompted inBlob:parsed], 1); +} + +- (void)testProcessResponseHeaders_whenBlockingErrorNotMappedToStep_shouldRecordBlockingErrorOnly +{ + MSIDOnboardingBlobBuilder *builder = [self builderWithTestDefaults]; + + [builder processResponseHeaders:@{MSID_OAUTH2_CLIENT_TELEMETRY: @"2,50076,0,,"} + responseURL:[NSURL URLWithString:@"https://login.microsoftonline.com"]]; + + NSDictionary *parsed = [self parsedJsonFromBlob:[builder finalizeBlob]]; + XCTAssertEqualObjects(parsed[@"last_blocking_error"], @"50076"); + XCTAssertEqual([parsed[@"steps_list"] count], 0); + XCTAssertFalse(builder.strongAuthSetupStarted); +} + @end diff --git a/IdentityCore/tests/MSIDPKeyAuthHandlerTests.m b/IdentityCore/tests/MSIDPKeyAuthHandlerTests.m index 3351d57fe3..3be562ec1a 100644 --- a/IdentityCore/tests/MSIDPKeyAuthHandlerTests.m +++ b/IdentityCore/tests/MSIDPKeyAuthHandlerTests.m @@ -31,6 +31,8 @@ #import "MSIDBasicContext.h" #import "MSIDTestSwizzle.h" #import "MSIDInteractiveTokenRequestParameters.h" +#import "MSIDExecutionFlowLogger.h" +#import "MSIDExecutionFlowConstants.h" @interface MSIDPKeyAuthHandlerTests : XCTestCase @@ -143,6 +145,32 @@ - (void)testHandleChallengeWithRefreshToken_happyPath_shouldReturnSuccess XCTAssertTrue(handleResult); } +- (void)testHandleChallengeWithRefreshToken_whenSubmitUrlIsNotKnownAADHost_shouldNotAttachPRTHeader +{ + [self makeAppV2GroupEntitled:YES]; + + __auto_type pkeyUrl = @"urn:http-auth:PKeyAuth?CertAuthorities=OU%3d82dbaca4-3e81-46ca-9c73-0950c1eaca97%2cCN%3dMS-Organization-Access%2cDC%3dwindows%2cDC%3dnet&Version=1.0&Context=SOMECONTEXT&nonce=_bQWemEag2Zze-FR1kw2r-XyrDYxmQB2PftHsshTEJc&SubmitUrl=https%3a%2f%2fcontoso.untrusted.com%2fcommon%2fDeviceAuthPKeyAuth&TenantId=f645ad92-e38d-4d1a-b510-d1b09a74a8ca"; + NSString *value = @"FakeRefreshToken"; + NSDictionary *customHeaders = @{ MSID_REFRESH_TOKEN_CREDENTIAL : value}; + + __auto_type *context = [MSIDInteractiveTokenRequestParameters new]; + context.appRequestMetadata = nil; + context.extraURLQueryParameters = @{@"eqp1": @"val1", @"eqp2": @"val2"}; + __block BOOL callback = NO; + BOOL handleResult = [MSIDPKeyAuthHandler handleChallenge:pkeyUrl + context:context + customHeaders:customHeaders + externalSSOContext:nil + completionHandler:^(NSURLRequest *challengeResponse, NSError *error) { + XCTAssertNotNil(challengeResponse); + XCTAssertNil([[challengeResponse allHTTPHeaderFields] objectForKey:MSID_REFRESH_TOKEN_CREDENTIAL], @"RefreshToken should not be attached for non-AAD hosts"); + XCTAssertNil(error); + callback = YES; + }]; + XCTAssertTrue(callback); + XCTAssertTrue(handleResult); +} + - (void)testHandleChallengeNilRefreshToken_shouldProceedWithSuccess { [self makeAppV2GroupEntitled:YES]; @@ -198,6 +226,80 @@ - (void)testHandleChallengeWithEmptyCustomHeaders_shouldProceedWithSuccess XCTAssertTrue(handleResult); } +- (void)testHandleChallengeWithRefreshToken_whenKnownAADHost_shouldRecordAddedExecutionFlowTag +{ + [self makeAppV2GroupEntitled:YES]; + + __auto_type pkeyUrl = @"urn:http-auth:PKeyAuth?CertAuthorities=OU%3d82dbaca4-3e81-46ca-9c73-0950c1eaca97%2cCN%3dMS-Organization-Access%2cDC%3dwindows%2cDC%3dnet&Version=1.0&Context=SOMECONTEXT&nonce=_bQWemEag2Zze-FR1kw2r-XyrDYxmQB2PftHsshTEJc&SubmitUrl=https%3a%2f%2flogin.microsoftonline.com%2fcommon%2fDeviceAuthPKeyAuth&TenantId=f645ad92-e38d-4d1a-b510-d1b09a74a8ca"; + NSString *value = @"FakeRefreshToken"; + NSDictionary *customHeaders = @{ MSID_REFRESH_TOKEN_CREDENTIAL : value}; + + __auto_type *context = [MSIDInteractiveTokenRequestParameters new]; + context.appRequestMetadata = nil; + context.correlationId = [NSUUID UUID]; + MSIDExecutionFlowRegister(context.correlationId); + + __block BOOL callback = NO; + BOOL handleResult = [MSIDPKeyAuthHandler handleChallenge:pkeyUrl + context:context + customHeaders:customHeaders + externalSSOContext:nil + completionHandler:^(NSURLRequest *challengeResponse, NSError *error) { + XCTAssertNotNil(challengeResponse); + XCTAssertTrue([[[challengeResponse allHTTPHeaderFields] objectForKey:MSID_REFRESH_TOKEN_CREDENTIAL] isEqual:value]); + XCTAssertNil(error); + callback = YES; + }]; + XCTAssertTrue(callback); + XCTAssertTrue(handleResult); + + XCTestExpectation *flowExpectation = [self expectationWithDescription:@"execution flow should contain the added PRT tag"]; + MSIDExecutionFlowRetrieve(context.correlationId, nil, YES, ^(NSString * _Nullable executionFlow) { + XCTAssertNotNil(executionFlow); + XCTAssertTrue([executionFlow containsString:MSIDPkeyAuthTagToString(MSIDPkeyAuthAddedRefreshTokenCredentialTag)], @"Flow should record the added PRT tag"); + XCTAssertFalse([executionFlow containsString:MSIDPkeyAuthTagToString(MSIDPkeyAuthSkippedRefreshTokenCredentialUntrustedHostTag)], @"Flow should not record the skipped tag"); + [flowExpectation fulfill]; + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testHandleChallengeWithRefreshToken_whenUntrustedHost_shouldRecordSkippedExecutionFlowTag +{ + [self makeAppV2GroupEntitled:YES]; + + __auto_type pkeyUrl = @"urn:http-auth:PKeyAuth?CertAuthorities=OU%3d82dbaca4-3e81-46ca-9c73-0950c1eaca97%2cCN%3dMS-Organization-Access%2cDC%3dwindows%2cDC%3dnet&Version=1.0&Context=SOMECONTEXT&nonce=_bQWemEag2Zze-FR1kw2r-XyrDYxmQB2PftHsshTEJc&SubmitUrl=https%3a%2f%2fcontoso.untrusted.com%2fcommon%2fDeviceAuthPKeyAuth&TenantId=f645ad92-e38d-4d1a-b510-d1b09a74a8ca"; + NSString *value = @"FakeRefreshToken"; + NSDictionary *customHeaders = @{ MSID_REFRESH_TOKEN_CREDENTIAL : value}; + + __auto_type *context = [MSIDInteractiveTokenRequestParameters new]; + context.appRequestMetadata = nil; + context.correlationId = [NSUUID UUID]; + MSIDExecutionFlowRegister(context.correlationId); + + __block BOOL callback = NO; + BOOL handleResult = [MSIDPKeyAuthHandler handleChallenge:pkeyUrl + context:context + customHeaders:customHeaders + externalSSOContext:nil + completionHandler:^(NSURLRequest *challengeResponse, NSError *error) { + XCTAssertNotNil(challengeResponse); + XCTAssertNil([[challengeResponse allHTTPHeaderFields] objectForKey:MSID_REFRESH_TOKEN_CREDENTIAL]); + XCTAssertNil(error); + callback = YES; + }]; + XCTAssertTrue(callback); + XCTAssertTrue(handleResult); + + XCTestExpectation *flowExpectation = [self expectationWithDescription:@"execution flow should contain the skipped PRT tag"]; + MSIDExecutionFlowRetrieve(context.correlationId, nil, YES, ^(NSString * _Nullable executionFlow) { + XCTAssertNotNil(executionFlow); + XCTAssertTrue([executionFlow containsString:MSIDPkeyAuthTagToString(MSIDPkeyAuthSkippedRefreshTokenCredentialUntrustedHostTag)], @"Flow should record the skipped PRT tag"); + XCTAssertFalse([executionFlow containsString:MSIDPkeyAuthTagToString(MSIDPkeyAuthAddedRefreshTokenCredentialTag)], @"Flow should not record the added tag"); + [flowExpectation fulfill]; + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + - (void)makeAppV2GroupEntitled:(BOOL)entitled { [MSIDTestSwizzle classMethod:@selector(v2AccessGroupAllowedWithContext:) diff --git a/IdentityCore/tests/MSIDRequestParametersTests.m b/IdentityCore/tests/MSIDRequestParametersTests.m index 47451a3d29..b8ec38b2f7 100644 --- a/IdentityCore/tests/MSIDRequestParametersTests.m +++ b/IdentityCore/tests/MSIDRequestParametersTests.m @@ -30,6 +30,8 @@ #import "MSIDAuthenticationSchemePop.h" #import "MSIDAuthenticationSchemeSshCert.h" #import "MSIDAccountIdentifier.h" +#import "MSIDExecutionFlowLogger.h" +#import "MSIDExecutionFlowConstants.h" @interface MSIDRequestParametersTests : XCTestCase @@ -498,4 +500,169 @@ - (void)testReverseNestedAuth_whenNestedAuthParametersPresentCalledTwice_shouldR XCTAssertEqualObjects(parameters.nestedAuthBrokerRedirectUri, @"myredirect"); } +#pragma mark - setCloudAuthorityWithCloudHostName + +- (void)testSetCloudAuthorityWithCloudHostName_whenKnownPublicCloudHost_shouldSetCloudAuthority +{ + MSIDAuthority *authority = [@"https://login.microsoftonline.com/common" aadAuthority]; + NSOrderedSet *scopes = [NSOrderedSet orderedSetWithObjects:@"myscope1", nil]; + NSOrderedSet *oidcScopes = [NSOrderedSet orderedSetWithObjects:@"openid", nil]; + NSError *error = nil; + MSIDRequestParameters *parameters = [[MSIDRequestParameters alloc] initWithAuthority:authority + authScheme:[MSIDAuthenticationScheme new] + redirectUri:@"myredirect" + clientId:@"myclient_id" + scopes:scopes + oidcScopes:oidcScopes + correlationId:nil + telemetryApiId:nil + intuneAppIdentifier:@"com.microsoft.mytest" + requestType:MSIDRequestLocalType + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(parameters); + + [parameters setCloudAuthorityWithCloudHostName:@"login.microsoftonline.com"]; + + XCTAssertNotNil(parameters.cloudAuthority); + XCTAssertEqualObjects(parameters.cloudAuthority.environment, @"login.microsoftonline.com"); +} + +- (void)testSetCloudAuthorityWithCloudHostName_whenKnownSovereignCloudHost_shouldSetCloudAuthority +{ + MSIDAuthority *authority = [@"https://login.microsoftonline.com/common" aadAuthority]; + NSOrderedSet *scopes = [NSOrderedSet orderedSetWithObjects:@"myscope1", nil]; + NSOrderedSet *oidcScopes = [NSOrderedSet orderedSetWithObjects:@"openid", nil]; + NSError *error = nil; + MSIDRequestParameters *parameters = [[MSIDRequestParameters alloc] initWithAuthority:authority + authScheme:[MSIDAuthenticationScheme new] + redirectUri:@"myredirect" + clientId:@"myclient_id" + scopes:scopes + oidcScopes:oidcScopes + correlationId:nil + telemetryApiId:nil + intuneAppIdentifier:@"com.microsoft.mytest" + requestType:MSIDRequestLocalType + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(parameters); + + [parameters setCloudAuthorityWithCloudHostName:@"login.microsoftonline.de"]; + + XCTAssertNotNil(parameters.cloudAuthority); + XCTAssertEqualObjects(parameters.cloudAuthority.environment, @"login.microsoftonline.de"); +} + +- (void)testSetCloudAuthorityWithCloudHostName_whenUnknownHost_shouldNotSetCloudAuthority +{ + MSIDAuthority *authority = [@"https://login.microsoftonline.com/common" aadAuthority]; + NSOrderedSet *scopes = [NSOrderedSet orderedSetWithObjects:@"myscope1", nil]; + NSOrderedSet *oidcScopes = [NSOrderedSet orderedSetWithObjects:@"openid", nil]; + NSError *error = nil; + MSIDRequestParameters *parameters = [[MSIDRequestParameters alloc] initWithAuthority:authority + authScheme:[MSIDAuthenticationScheme new] + redirectUri:@"myredirect" + clientId:@"myclient_id" + scopes:scopes + oidcScopes:oidcScopes + correlationId:nil + telemetryApiId:nil + intuneAppIdentifier:@"com.microsoft.mytest" + requestType:MSIDRequestLocalType + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(parameters); + + [parameters setCloudAuthorityWithCloudHostName:@"unknown-host.example.com"]; + + XCTAssertNil(parameters.cloudAuthority); +} + +- (void)testSetCloudAuthorityWithCloudHostName_whenNilHost_shouldNotSetCloudAuthority +{ + MSIDAuthority *authority = [@"https://login.microsoftonline.com/common" aadAuthority]; + NSOrderedSet *scopes = [NSOrderedSet orderedSetWithObjects:@"myscope1", nil]; + NSOrderedSet *oidcScopes = [NSOrderedSet orderedSetWithObjects:@"openid", nil]; + NSError *error = nil; + MSIDRequestParameters *parameters = [[MSIDRequestParameters alloc] initWithAuthority:authority + authScheme:[MSIDAuthenticationScheme new] + redirectUri:@"myredirect" + clientId:@"myclient_id" + scopes:scopes + oidcScopes:oidcScopes + correlationId:nil + telemetryApiId:nil + intuneAppIdentifier:@"com.microsoft.mytest" + requestType:MSIDRequestLocalType + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(parameters); + + [parameters setCloudAuthorityWithCloudHostName:nil]; + + XCTAssertNil(parameters.cloudAuthority); +} + +- (void)testSetCloudAuthorityWithCloudHostName_whenBlankHost_shouldNotSetCloudAuthority +{ + MSIDAuthority *authority = [@"https://login.microsoftonline.com/common" aadAuthority]; + NSOrderedSet *scopes = [NSOrderedSet orderedSetWithObjects:@"myscope1", nil]; + NSOrderedSet *oidcScopes = [NSOrderedSet orderedSetWithObjects:@"openid", nil]; + NSError *error = nil; + MSIDRequestParameters *parameters = [[MSIDRequestParameters alloc] initWithAuthority:authority + authScheme:[MSIDAuthenticationScheme new] + redirectUri:@"myredirect" + clientId:@"myclient_id" + scopes:scopes + oidcScopes:oidcScopes + correlationId:nil + telemetryApiId:nil + intuneAppIdentifier:@"com.microsoft.mytest" + requestType:MSIDRequestLocalType + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(parameters); + + [parameters setCloudAuthorityWithCloudHostName:@" "]; + + XCTAssertNil(parameters.cloudAuthority); +} + +- (void)testSetCloudAuthorityWithCloudHostName_whenUnknownHostAndCorrelationIdAvailable_shouldRecordExecutionFlowTag +{ + MSIDAuthority *authority = [@"https://login.microsoftonline.com/common" aadAuthority]; + NSOrderedSet *scopes = [NSOrderedSet orderedSetWithObjects:@"myscope1", nil]; + NSOrderedSet *oidcScopes = [NSOrderedSet orderedSetWithObjects:@"openid", nil]; + NSUUID *correlationId = [NSUUID new]; + NSError *error = nil; + MSIDRequestParameters *parameters = [[MSIDRequestParameters alloc] initWithAuthority:authority + authScheme:[MSIDAuthenticationScheme new] + redirectUri:@"myredirect" + clientId:@"myclient_id" + scopes:scopes + oidcScopes:oidcScopes + correlationId:correlationId + telemetryApiId:nil + intuneAppIdentifier:@"com.microsoft.mytest" + requestType:MSIDRequestLocalType + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(parameters); + + MSIDExecutionFlowRegister(parameters.correlationId); + + [parameters setCloudAuthorityWithCloudHostName:@"unknown-host.example.com"]; + + XCTAssertNil(parameters.cloudAuthority); + + XCTestExpectation *flowExpectation = [self expectationWithDescription:@"execution flow should record the ignored cloud host tag"]; + MSIDExecutionFlowRetrieve(parameters.correlationId, nil, YES, ^(NSString * _Nullable executionFlow) { + XCTAssertNotNil(executionFlow); + XCTAssertTrue([executionFlow containsString:MSIDCloudInstanceHostNameTagToString(MSIDCloudInstanceHostNameIgnoredTag)], @"Flow should record the ignored cloud host tag"); + [flowExpectation fulfill]; + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + @end diff --git a/IdentityCore/tests/MSIDStringExtensionsTests.m b/IdentityCore/tests/MSIDStringExtensionsTests.m index 04c8bb9d79..6bc0231fc4 100644 --- a/IdentityCore/tests/MSIDStringExtensionsTests.m +++ b/IdentityCore/tests/MSIDStringExtensionsTests.m @@ -96,6 +96,18 @@ - (void)testMsidIsStringNilOrBlank_whenNonEmpty_shouldReturnFalse XCTAssertFalse([NSString msidIsStringNilOrBlank:str], "Not an empty string %@", str); } +- (void)testMsidIsStringNilOrBlank_whenNonStringObject_shouldReturnTrue +{ + // Regression: a mis-typed value (e.g. a JSON/plist boolean decoded as __NSCFBoolean, or any + // other non-NSString) must be treated as blank rather than crashing with an unrecognized + // selector when -length is sent to it. + XCTAssertTrue([NSString msidIsStringNilOrBlank:(NSString *)@YES]); + XCTAssertTrue([NSString msidIsStringNilOrBlank:(NSString *)@NO]); + XCTAssertTrue([NSString msidIsStringNilOrBlank:(NSString *)@42]); + XCTAssertTrue([NSString msidIsStringNilOrBlank:(NSString *)@[@"a"]]); + XCTAssertTrue([NSString msidIsStringNilOrBlank:(NSString *)@{@"a": @"b"}]); +} + - (void)testMsidTrimmedString { XCTAssertEqualObjects([@" \t\r\n test" msidTrimmedString], @"test"); diff --git a/IdentityCore/tests/MSIDWebviewNavigationDecisionResolverTests.m b/IdentityCore/tests/MSIDWebviewNavigationDecisionResolverTests.m index c40ee1957f..7959df3b96 100644 --- a/IdentityCore/tests/MSIDWebviewNavigationDecisionResolverTests.m +++ b/IdentityCore/tests/MSIDWebviewNavigationDecisionResolverTests.m @@ -34,6 +34,13 @@ #import "MSIDTestCacheDataSource.h" #import "MSIDTestSwizzle.h" #import "MSIDVersion.h" +#import "MSIDUXCallbackProvider.h" +#import "MSIDUXCallbackProtocol.h" +#import "MSIDConstants.h" +#import "MSIDMockUXCallbackProvider.h" +#import "MSIDOnboardingBlobBuilder.h" +#import "MSIDOnboardingBlobBuilder+MSIDTestUtil.h" +#import "MSIDOnboardingBlobFieldKeys.h" @interface MSIDWebviewNavigationDecisionResolverTests : XCTestCase @@ -55,12 +62,14 @@ - (void)setUp self.dataSource = [MSIDTestCacheDataSource new]; self.deviceIdCache = [[MSIDIntuneDeviceIdCache alloc] initWithDataSource:self.dataSource]; [MSIDIntuneDeviceIdCache setSharedCache:self.deviceIdCache]; + } - (void)tearDown { [MSIDTestSwizzle reset]; [self.dataSource reset]; + MSIDUXCallbackProvider.uxCallbackProvider = nil; [super tearDown]; } @@ -85,7 +94,8 @@ - (MSIDOAuth2EmbeddedWebviewController *)createWebviewControllerWithExternalBloc - (void)testResolveDecision_nilURL_returnsFailWithError { MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:nil - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionFailWithError); XCTAssertNotNil(decision.error); @@ -97,7 +107,8 @@ - (void)testResolveDecision_URLMissingScheme_returnsFailWithError // rather than nil so the caller always receives an actionable navigation outcome. NSURL *url = [NSURL URLWithString:@"//host/path"]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionFailWithError); XCTAssertNotNil(decision.error); @@ -109,7 +120,8 @@ - (void)testResolveDecision_browserScheme_returnsContinueDefault { NSURL *url = [NSURL URLWithString:@"browser://some.host/path"]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionContinueDefault); } @@ -118,7 +130,8 @@ - (void)testResolveDecision_unknownScheme_returnsContinueDefault { NSURL *url = [NSURL URLWithString:@"foobar://some.host"]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionContinueDefault); } @@ -130,7 +143,8 @@ - (void)testResolveDecision_msauthEmptyHost_returnsFailWithError // msauth:/// has no host NSURL *url = [NSURL URLWithString:@"msauth:///path"]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionFailWithError); XCTAssertNotNil(decision.error); @@ -140,7 +154,8 @@ - (void)testResolveDecision_msauthUnknownHost_returnsContinueDefault { NSURL *url = [NSURL URLWithString:@"msauth://unknownhost"]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionContinueDefault); } @@ -151,7 +166,8 @@ - (void)testEnrollURL_missingIntuneURL_returnsFailWithError { NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"msauth://%@", MSID_MDM_ENROLL_HOST]]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionFailWithError); XCTAssertNotNil(decision.error); @@ -166,7 +182,8 @@ - (void)testEnrollURL_validIntuneURL_returnsLoadRequest NSURL *url = [NSURL URLWithString:urlString]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); XCTAssertNotNil(decision.request); @@ -186,7 +203,8 @@ - (void)testEnrollURL_attachesCachedDeviceId_whenPresent NSURL *url = [NSURL URLWithString:urlString]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); @@ -205,9 +223,9 @@ - (void)testEnrollURL_attachesCachedDeviceId_whenPresent - (void)testEnrollURL_attachesPlatformAndVersionHeadersFromMSIDVersion { - // The resolver no longer accepts caller-supplied app name/version; it derives - // x-client-SKU / x-client-Ver from MSIDVersion. The test target's MSIDVersion - // returns "TEST.iOS" / "1.0.0" (see tests/MSIDVersion.m). + // This test asserts the SDK-controlled x-client-SKU / x-client-Ver headers, which the + // resolver always derives from MSIDVersion (independent of any caller-supplied headers). + // The test target's MSIDVersion returns "TEST.iOS" / "1.0.0" (see tests/MSIDVersion.m). NSString *targetURL = @"https://manage.microsoft.com/enroll"; NSString *encoded = [targetURL stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; NSString *urlString = [NSString stringWithFormat:@"msauth://%@?%@=%@", @@ -215,13 +233,70 @@ - (void)testEnrollURL_attachesPlatformAndVersionHeadersFromMSIDVersion NSURL *url = [NSURL URLWithString:urlString]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); XCTAssertEqualObjects([decision.request valueForHTTPHeaderField:MSID_PLATFORM_KEY], [MSIDVersion platformName]); XCTAssertEqualObjects([decision.request valueForHTTPHeaderField:MSID_VERSION_KEY], [MSIDVersion sdkVersion]); } +- (void)testEnrollURL_whenAdditionalHeadersProvided_attachesBrokerVersionHeader +{ + NSString *targetURL = @"https://manage.microsoft.com/enroll"; + NSString *encoded = [targetURL stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + NSString *urlString = [NSString stringWithFormat:@"msauth://%@?%@=%@", + MSID_MDM_ENROLL_HOST, MSID_INTUNE_URL_KEY, encoded]; + NSURL *url = [NSURL URLWithString:urlString]; + + MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url + embeddedWebviewController:nil + additionalHeaders:@{MSID_BROKER_VER_KEY: @"6.1.2"}]; + XCTAssertNotNil(decision); + XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); + XCTAssertEqualObjects([decision.request valueForHTTPHeaderField:MSID_BROKER_VER_KEY], @"6.1.2"); +} + +- (void)testEnrollURL_whenAdditionalHeadersNil_doesNotAttachBrokerVersionHeader +{ + NSString *targetURL = @"https://manage.microsoft.com/enroll"; + NSString *encoded = [targetURL stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + NSString *urlString = [NSString stringWithFormat:@"msauth://%@?%@=%@", + MSID_MDM_ENROLL_HOST, MSID_INTUNE_URL_KEY, encoded]; + NSURL *url = [NSURL URLWithString:urlString]; + + // Passing nil additional headers omits the broker version header. + MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url + embeddedWebviewController:nil + additionalHeaders:nil]; + XCTAssertNotNil(decision); + XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); + XCTAssertNil([decision.request valueForHTTPHeaderField:MSID_BROKER_VER_KEY]); +} + +- (void)testEnrollURL_whenAdditionalHeadersContainAppHeaders_attachesThemVerbatim +{ + // The resolver is a dumb merger: it stamps whatever the caller supplies without + // doing any of its own gating. + NSString *targetURL = @"https://manage.microsoft.com/enroll"; + NSString *encoded = [targetURL stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + NSString *urlString = [NSString stringWithFormat:@"msauth://%@?%@=%@", + MSID_MDM_ENROLL_HOST, MSID_INTUNE_URL_KEY, encoded]; + NSURL *url = [NSURL URLWithString:urlString]; + + NSDictionary *headers = @{MSID_BROKER_VER_KEY: @"6.1.2", + MSID_APP_NAME_KEY: @"Contoso", + MSID_APP_VER_KEY: @"1.2.3"}; + MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url + embeddedWebviewController:nil + additionalHeaders:headers]; + XCTAssertNotNil(decision); + XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); + XCTAssertEqualObjects([decision.request valueForHTTPHeaderField:MSID_BROKER_VER_KEY], @"6.1.2"); + XCTAssertEqualObjects([decision.request valueForHTTPHeaderField:MSID_APP_NAME_KEY], @"Contoso"); + XCTAssertEqualObjects([decision.request valueForHTTPHeaderField:MSID_APP_VER_KEY], @"1.2.3"); +} + #pragma mark - Profile download complete host - (void)testProfileDownloadComplete_missingDeviceId_returnsLoadRequest @@ -231,7 +306,8 @@ - (void)testProfileDownloadComplete_missingDeviceId_returnsLoadRequest NSURL *url = [NSURL URLWithString:urlString]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); XCTAssertNotNil(decision.request); @@ -244,7 +320,8 @@ - (void)testProfileDownloadComplete_missingProfileInstallURL_returnsFailWithErro NSURL *url = [NSURL URLWithString:urlString]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionFailWithError); XCTAssertNotNil(decision.error); @@ -262,7 +339,8 @@ - (void)testProfileDownloadComplete_validParams_returnsLoadRequest NSURL *url = [NSURL URLWithString:urlString]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); XCTAssertNotNil(decision.request); @@ -281,7 +359,8 @@ - (void)testProfileDownloadComplete_cachesDeviceId NSURL *url = [NSURL URLWithString:urlString]; [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; NSError *readError = nil; NSString *cached = [self.deviceIdCache intuneDeviceIdWithContext:nil error:&readError]; XCTAssertNil(readError); @@ -303,7 +382,8 @@ - (void)testProfileDownloadComplete_malformedProfileURL_returnsFailWithError XCTAssertNotNil(url, @"Test input msauth URL should itself be valid"); MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionFailWithError); XCTAssertNotNil(decision.error); @@ -316,7 +396,8 @@ - (void)testComplianceURL_missingIntuneURL_returnsFailWithError NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"msauth://%@", MSID_COMPLIANCE_HOST]]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionFailWithError); XCTAssertNotNil(decision.error); @@ -331,7 +412,8 @@ - (void)testComplianceURL_validParams_noExternalBlock_returnsLoadRequest NSURL *url = [NSURL URLWithString:urlString]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); XCTAssertNotNil(decision.request); @@ -357,7 +439,8 @@ - (void)testComplianceURL_withExternalBlock_blockReturnsNil_returnsLoadRequest MSIDOAuth2EmbeddedWebviewController *webviewController = [self createWebviewControllerWithExternalBlock:block]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:webviewController]; + embeddedWebviewController:webviewController + additionalHeaders:nil]; XCTAssertTrue(invoked, @"External block must be invoked when a webview controller is provided."); XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); @@ -386,7 +469,8 @@ - (void)testComplianceURL_withExternalBlock_blockReturnsRequest_usesUpdatedReque // externalDecidePolicyForBrowserAction is non-nil. MSIDOAuth2EmbeddedWebviewController *webviewController = [self createWebviewControllerWithExternalBlock:block]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:webviewController]; + embeddedWebviewController:webviewController + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); XCTAssertEqualObjects(decision.request.URL.absoluteString, @"https://override.example.com/path"); @@ -410,7 +494,8 @@ - (void)testEnrollmentCompletion_ssoExtensionAvailable_returnsCompleteWithURL NSURL *url = [NSURL URLWithString:urlString]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionCompleteWithURL); XCTAssertEqualObjects(decision.URL, url); @@ -429,7 +514,8 @@ - (void)testEnrollmentCompletion_ssoExtensionUnavailable_noErrorURL_returnsFailW NSURL *url = [NSURL URLWithString:urlString]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionFailWithError); XCTAssertNotNil(decision.error); @@ -453,7 +539,8 @@ - (void)testEnrollmentCompletion_ssoExtensionUnavailable_withErrorURL_returnsLoa NSURL *url = [NSURL URLWithString:urlString]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); XCTAssertEqualObjects(decision.request.URL.absoluteString, errorURL); @@ -469,7 +556,8 @@ - (void)testEnrollURL_whitespaceOnlyIntuneURL_returnsFailWithError NSURL *url = [NSURL URLWithString:urlString]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionFailWithError); XCTAssertNotNil(decision.error); @@ -482,7 +570,8 @@ - (void)testComplianceURL_whitespaceOnlyIntuneURL_returnsFailWithError NSURL *url = [NSURL URLWithString:urlString]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionFailWithError); XCTAssertNotNil(decision.error); @@ -499,7 +588,8 @@ - (void)testProfileDownloadComplete_whitespaceOnlyDeviceId_doesNotCache NSURL *url = [NSURL URLWithString:urlString]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); @@ -517,7 +607,8 @@ - (void)testProfileDownloadComplete_whitespaceOnlyProfileInstallURL_returnsFailW NSURL *url = [NSURL URLWithString:urlString]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionFailWithError); XCTAssertNotNil(decision.error); @@ -538,7 +629,8 @@ - (void)testEnrollmentCompletion_ssoExtensionUnavailable_whitespaceErrorURL_retu NSURL *url = [NSURL URLWithString:urlString]; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionFailWithError); XCTAssertNotNil(decision.error); @@ -567,7 +659,8 @@ - (void)testComplianceURL_legacySchemeRewrite_preservesHostPathAndQuery MSIDOAuth2EmbeddedWebviewController *webviewController = [self createWebviewControllerWithExternalBlock:block]; [self.resolver resolveDecisionForURL:url - embeddedWebviewController:webviewController]; + embeddedWebviewController:webviewController + additionalHeaders:nil]; XCTAssertNotNil(receivedURL); XCTAssertEqualObjects(receivedURL.scheme, @"browser"); XCTAssertEqualObjects(receivedURL.host, @"compliance.microsoft.com"); @@ -594,12 +687,233 @@ - (void)testComplianceURL_externalBlockNotInvoked_whenWebviewControllerIsNil }; MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url - embeddedWebviewController:nil]; + embeddedWebviewController:nil + additionalHeaders:nil]; XCTAssertNotNil(decision); XCTAssertFalse(invoked, @"External block must not be invoked when no webview controller is provided."); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); } +- (void)testProfileDownloadComplete_whenProviderIsNil_shouldNotCrash +{ + MSIDUXCallbackProvider.uxCallbackProvider = nil; + + NSString *profileURL = @"https://manage.microsoft.com/profile.mobileconfig"; + NSString *encodedURL = [profileURL stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + NSString *urlString = [NSString stringWithFormat:@"msauth://%@?%@=device123&%@=%@", + MSID_MDM_PROFILE_DOWNLOAD_COMPLETE_HOST, + MSID_INTUNE_DEVICE_ID_KEY, + MSID_INTUNE_PROFILE_INSTALL_URL_KEY, + encodedURL]; + NSURL *url = [NSURL URLWithString:urlString]; + + MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url embeddedWebviewController:nil + additionalHeaders:nil]; + XCTAssertNotNil(decision); + XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); +} + +#pragma mark - Cancel Notification on Enrollment Completion + +- (void)testEnrollmentCompletion_whenProviderSet_shouldCancelNotification +{ + MSIDMockUXCallbackProvider *mockProvider = [MSIDMockUXCallbackProvider new]; + MSIDUXCallbackProvider.uxCallbackProvider = mockProvider; + + [MSIDTestSwizzle classMethod:@selector(canPerformRequest) + class:[MSIDSSOExtensionInteractiveTokenRequestController class] + block:(id)^(void) + { + return YES; + }]; + + NSString *urlString = [NSString stringWithFormat:@"msauth://%@", MSID_MDM_ENROLLMENT_COMPLETION_HOST]; + NSURL *url = [NSURL URLWithString:urlString]; + + [self.resolver resolveDecisionForURL:url embeddedWebviewController:nil + additionalHeaders:nil]; + + XCTAssertTrue(mockProvider.cancelCalled, @"Cancel should be invoked on enrollment completion."); +} + +- (void)testEnrollmentCompletion_whenProviderIsNil_shouldNotCrash +{ + MSIDUXCallbackProvider.uxCallbackProvider = nil; + + [MSIDTestSwizzle classMethod:@selector(canPerformRequest) + class:[MSIDSSOExtensionInteractiveTokenRequestController class] + block:(id)^(void) + { + return YES; + }]; + + NSString *urlString = [NSString stringWithFormat:@"msauth://%@", MSID_MDM_ENROLLMENT_COMPLETION_HOST]; + NSURL *url = [NSURL URLWithString:urlString]; + + MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url embeddedWebviewController:nil + additionalHeaders:nil]; + XCTAssertNotNil(decision); + XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionCompleteWithURL); +} + +#pragma mark - Onboarding telemetry step stamping + +- (MSIDOAuth2EmbeddedWebviewController *)controllerWithOnboardingBuilder:(MSIDOnboardingBlobBuilder *)builder +{ + MSIDOAuth2EmbeddedWebviewController *controller = [self createWebviewControllerWithExternalBlock:nil]; + controller.onboardingBlobBuilder = builder; + return controller; +} + +- (NSString *)enrollishURLForHost:(NSString *)host targetURL:(NSString *)targetURL +{ + NSString *encoded = [targetURL stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + return [NSString stringWithFormat:@"msauth://%@?%@=%@", host, MSID_INTUNE_URL_KEY, encoded]; +} + +- (void)testResolveEnroll_whenIntuneUrlMissing_shouldStampMdmEnrollmentUrlMissing +{ + MSIDOnboardingBlobBuilder *builder = [MSIDOnboardingBlobBuilder msidTestBuilder]; + MSIDOAuth2EmbeddedWebviewController *controller = [self controllerWithOnboardingBuilder:builder]; + NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"msauth://%@", MSID_MDM_ENROLL_HOST]]; + + MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url embeddedWebviewController:controller + additionalHeaders:nil]; + + XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionFailWithError); + XCTAssertTrue([[builder msidStampedStepIds] containsObject:MSIDOnboardingBlobStepMdmEnrollmentUrlMissing]); +} + +- (void)testResolveEnroll_whenValidIntuneUrl_shouldStampMdmEnrollmentStarted +{ + MSIDOnboardingBlobBuilder *builder = [MSIDOnboardingBlobBuilder msidTestBuilder]; + MSIDOAuth2EmbeddedWebviewController *controller = [self controllerWithOnboardingBuilder:builder]; + NSURL *url = [NSURL URLWithString:[self enrollishURLForHost:MSID_MDM_ENROLL_HOST targetURL:@"https://manage.microsoft.com/enroll"]]; + + MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url embeddedWebviewController:controller + additionalHeaders:nil]; + + XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); + NSArray *steps = [builder msidStampedStepIds]; + XCTAssertTrue([steps containsObject:MSIDOnboardingBlobStepMdmEnrollmentStarted]); + XCTAssertFalse([steps containsObject:MSIDOnboardingBlobStepMdmEnrollmentRequestMalformed]); +} + +- (void)testResolveProfileDownload_whenContinueUrlMissing_shouldStampProfileInstallUrlMissing +{ + MSIDOnboardingBlobBuilder *builder = [MSIDOnboardingBlobBuilder msidTestBuilder]; + MSIDOAuth2EmbeddedWebviewController *controller = [self controllerWithOnboardingBuilder:builder]; + NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"msauth://%@?%@=device123", + MSID_MDM_PROFILE_DOWNLOAD_COMPLETE_HOST, MSID_INTUNE_DEVICE_ID_KEY]]; + + MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url embeddedWebviewController:controller + additionalHeaders:nil]; + + XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionFailWithError); + XCTAssertTrue([[builder msidStampedStepIds] containsObject:MSIDOnboardingBlobStepProfileInstallUrlMissing]); +} + +- (void)testResolveProfileDownload_whenContinueUrlMalformed_shouldStampProfileInstallUrlMalformed +{ + MSIDOnboardingBlobBuilder *builder = [MSIDOnboardingBlobBuilder msidTestBuilder]; + MSIDOAuth2EmbeddedWebviewController *controller = [self controllerWithOnboardingBuilder:builder]; + NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"msauth://%@?%@=notaurl", + MSID_MDM_PROFILE_DOWNLOAD_COMPLETE_HOST, MSID_INTUNE_PROFILE_INSTALL_URL_KEY]]; + + MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url embeddedWebviewController:controller + additionalHeaders:nil]; + + XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionFailWithError); + XCTAssertTrue([[builder msidStampedStepIds] containsObject:MSIDOnboardingBlobStepProfileInstallUrlMalformed]); +} + +- (void)testResolveProfileDownload_whenValidUrlAndProviderNil_shouldStampCompletedOnly +{ + MSIDUXCallbackProvider.uxCallbackProvider = nil; + + MSIDOnboardingBlobBuilder *builder = [MSIDOnboardingBlobBuilder msidTestBuilder]; + MSIDOAuth2EmbeddedWebviewController *controller = [self controllerWithOnboardingBuilder:builder]; + NSString *encoded = [@"https://manage.microsoft.com/profile.mobileconfig" stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"msauth://%@?%@=%@", + MSID_MDM_PROFILE_DOWNLOAD_COMPLETE_HOST, MSID_INTUNE_PROFILE_INSTALL_URL_KEY, encoded]]; + + MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url embeddedWebviewController:controller + additionalHeaders:nil]; + + XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); + NSArray *steps = [builder msidStampedStepIds]; + XCTAssertTrue([steps containsObject:MSIDOnboardingBlobStepProfileDownloadCompleted]); + XCTAssertFalse([steps containsObject:MSIDOnboardingBlobStepProfileInstallNotificationScheduled]); +} + +- (void)testResolveCompliance_whenValidIntuneUrl_shouldStampComplianceRemediationMSAuthRedirect +{ + MSIDOnboardingBlobBuilder *builder = [MSIDOnboardingBlobBuilder msidTestBuilder]; + MSIDOAuth2EmbeddedWebviewController *controller = [self controllerWithOnboardingBuilder:builder]; + NSURL *url = [NSURL URLWithString:[self enrollishURLForHost:MSID_COMPLIANCE_HOST targetURL:@"https://manage.microsoft.com/compliance"]]; + + MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url embeddedWebviewController:controller + additionalHeaders:nil]; + + XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); + NSArray *steps = [builder msidStampedStepIds]; + XCTAssertTrue([steps containsObject:MSIDOnboardingBlobStepComplianceRemediationMSAuthRedirect]); + XCTAssertFalse([steps containsObject:MSIDOnboardingBlobStepComplianceRemediationRequestMalformed]); +} + +- (void)testResolveCompliance_whenIntuneUrlMissing_shouldStampComplianceRemediationUrlMissing +{ + MSIDOnboardingBlobBuilder *builder = [MSIDOnboardingBlobBuilder msidTestBuilder]; + MSIDOAuth2EmbeddedWebviewController *controller = [self controllerWithOnboardingBuilder:builder]; + NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"msauth://%@", MSID_COMPLIANCE_HOST]]; + + MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url embeddedWebviewController:controller + additionalHeaders:nil]; + + XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionFailWithError); + NSArray *steps = [builder msidStampedStepIds]; + XCTAssertTrue([steps containsObject:MSIDOnboardingBlobStepComplianceRemediationMSAuthRedirect]); + XCTAssertTrue([steps containsObject:MSIDOnboardingBlobStepComplianceRemediationUrlMissing]); +} + +- (void)testResolveEnrollmentCompletion_whenSSOUnavailableNoErrorURL_shouldStampSSOExtensionUnavailable +{ + [MSIDTestSwizzle classMethod:@selector(canPerformRequest) + class:[MSIDSSOExtensionInteractiveTokenRequestController class] + block:(id)^(void) { return NO; }]; + + MSIDOnboardingBlobBuilder *builder = [MSIDOnboardingBlobBuilder msidTestBuilder]; + MSIDOAuth2EmbeddedWebviewController *controller = [self controllerWithOnboardingBuilder:builder]; + NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"msauth://%@", MSID_MDM_ENROLLMENT_COMPLETION_HOST]]; + + MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url embeddedWebviewController:controller + additionalHeaders:nil]; + + XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionFailWithError); + XCTAssertTrue([[builder msidStampedStepIds] containsObject:MSIDOnboardingBlobStepSSOExtensionUnavailable]); +} + +- (void)testResolveEnrollmentCompletion_whenSSOUnavailableWithErrorURL_shouldStampMdmEnrollmentCompletionRetryStarted +{ + [MSIDTestSwizzle classMethod:@selector(canPerformRequest) + class:[MSIDSSOExtensionInteractiveTokenRequestController class] + block:(id)^(void) { return NO; }]; + + MSIDOnboardingBlobBuilder *builder = [MSIDOnboardingBlobBuilder msidTestBuilder]; + MSIDOAuth2EmbeddedWebviewController *controller = [self controllerWithOnboardingBuilder:builder]; + NSString *encoded = [@"https://enroll.microsoft.com/error" stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"msauth://%@?%@=%@", + MSID_MDM_ENROLLMENT_COMPLETION_HOST, MSID_MDM_ENROLLMENT_COMPLETION_ERROR_URL_KEY, encoded]]; + + MSIDWebviewNavigationDecision *decision = [self.resolver resolveDecisionForURL:url embeddedWebviewController:controller + additionalHeaders:nil]; + + XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); + NSArray *steps = [builder msidStampedStepIds]; + XCTAssertTrue([steps containsObject:MSIDOnboardingBlobStepSSOExtensionUnavailable]); + XCTAssertTrue([steps containsObject:MSIDOnboardingBlobStepMdmEnrollmentCompletionRetryStarted]); +} + @end #endif // !MSID_EXCLUDE_WEBKIT diff --git a/IdentityCore/tests/MSIDWebviewNavigationHandlerTests.m b/IdentityCore/tests/MSIDWebviewNavigationHandlerTests.m index 0f7c4bfcdc..ca53623606 100644 --- a/IdentityCore/tests/MSIDWebviewNavigationHandlerTests.m +++ b/IdentityCore/tests/MSIDWebviewNavigationHandlerTests.m @@ -33,6 +33,20 @@ #import "MSIDOAuth2EmbeddedWebviewController.h" #import "MSIDTestWebviewInteractingViewController.h" #import "MSIDWebviewNavigationDelegate.h" +#import "MSIDUXCallbackProvider.h" +#import "MSIDUXCallbackProtocol.h" +#import "MSIDFlightManager.h" +#import "MSIDFlightManagerMockProvider.h" +#import "MSIDConstants.h" +#import "MSIDMockUXCallbackProvider.h" +#import "MSIDOnboardingBlobBuilder.h" +#import "MSIDOnboardingBlobFieldKeys.h" +#import "MSIDTestSwizzle.h" +#import "MSIDKeychainUtil.h" +#import "NSBundle+MSIDExtensions.h" +#if !MSID_EXCLUDE_SYSTEMWV +#import "MSIDSystemWebviewTransitionManager.h" +#endif // Stub conforming to MSIDWebviewNavigationDelegate for delegate-wiring assertions. @interface MSIDTestNavigationDelegateStub : NSObject @@ -46,6 +60,7 @@ @interface MSIDWebviewNavigationHandler (Testing) // Expose private methods and properties for testing. @property (nonatomic) NSDictionary *lastResponseHeaders; +@property (nonatomic, weak) MSIDOnboardingBlobBuilder *onboardingBlobBuilder; - (BOOL)isValidHandoffURL:(NSURL *)url error:(NSError *__autoreleasing *)error; - (BOOL)isURLInAllowedDomains:(NSURL *)url; @@ -54,6 +69,7 @@ - (NSString *)callbackURLScheme; - (BOOL)shouldUseEphemeralSession; - (nullable NSDictionary *)extractAdditionalHeadersToForward; - (NSDictionary *)buildAdditionalHeadersFromList:(NSString *)attachHeadersList; +- (void)scheduleMDMProfileInstalledNotificationIfNeeded; @end @@ -61,6 +77,7 @@ @interface MSIDWebviewNavigationHandlerTests : XCTestCase @property (nonatomic) MSIDWebviewNavigationHandler *handler; @property (nonatomic) MSIDTestContext *context; +@property (nonatomic) MSIDFlightManagerMockProvider *flightProvider; @end @@ -71,12 +88,19 @@ - (void)setUp [super setUp]; self.context = [MSIDTestContext new]; self.handler = [[MSIDWebviewNavigationHandler alloc] initWithContext:self.context]; + + self.flightProvider = [MSIDFlightManagerMockProvider new]; + MSIDFlightManager.sharedInstance.flightProvider = self.flightProvider; } - (void)tearDown { + [MSIDTestSwizzle reset]; self.handler = nil; self.context = nil; + MSIDUXCallbackProvider.uxCallbackProvider = nil; + MSIDFlightManager.sharedInstance.flightProvider = nil; + self.flightProvider = nil; [super tearDown]; } @@ -373,7 +397,7 @@ - (void)testBuildAdditionalHeadersFromList_whenHeaderKeyIsMixedCase_shouldNormal NSString *upperCaseHeader = [[NSString stringWithFormat:@"%@token", MSID_ASWEBAUTH_HANDOFF_HEADER_PREFIX] uppercaseString]; NSString *lowerCaseHeader = [upperCaseHeader lowercaseString]; - // Simulate what processResponseHeadersAndCheckForASWebAuthHandoff:responseURL: does: normalize the raw server headers first + // Simulate what processNavigationResponseAndCheckForASWebAuthHandoff:embeddedWebviewController: does: normalize the raw server headers first NSDictionary *rawHeaders = @{upperCaseHeader: @"tok123"}; NSDictionary *normalised = [self.handler normalizeHeaders:rawHeaders]; self.handler.lastResponseHeaders = normalised; @@ -514,7 +538,121 @@ - (void)testHandleSpecialRedirectURL_shouldInvokeCompletionExactlyOnce [self waitForExpectations:@[expectation] timeout:1.0]; } -#pragma mark - processResponseHeadersAndCheckForASWebAuthHandoff:responseURL: (synchronous) +- (void)testHandleSpecialRedirectURL_whenAdditionalHeadersProvided_shouldAttachBrokerVersionHeaderOnEnrollRequest +{ + // The four-argument overload must forward the caller-supplied additional headers + // (here the broker version) through to the resolver so the MDM enrollment request + // advertises the x-client-brkrver header. + NSString *targetURL = @"https://manage.microsoft.com/enroll"; + NSString *encoded = [targetURL stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + NSString *urlString = [NSString stringWithFormat:@"msauth://%@?%@=%@", + MSID_MDM_ENROLL_HOST, MSID_INTUNE_URL_KEY, encoded]; + NSURL *URL = [NSURL URLWithString:urlString]; + XCTestExpectation *expectation = [self expectationWithDescription:@"completion invoked"]; + + [self.handler handleSpecialRedirectURL:URL + embeddedWebviewController:nil + additionalHeaders:@{MSID_BROKER_VER_KEY: @"6.1.2"} + completion:^(MSIDWebviewNavigationDecision * _Nullable decision, NSError * _Nullable error) + { + XCTAssertNotNil(decision); + XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); + XCTAssertEqualObjects([decision.request valueForHTTPHeaderField:MSID_BROKER_VER_KEY], @"6.1.2"); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:1.0]; +} + +- (void)testHandleSpecialRedirectURL_whenBrokerVersionOmitted_shouldNotAttachBrokerVersionHeaderOnEnrollRequest +{ + // The two-argument variant builds only the current process's first-party app headers + // (never a broker version). The unit-test host is not first-party, so no headers at all + // are stamped here. + NSString *targetURL = @"https://manage.microsoft.com/enroll"; + NSString *encoded = [targetURL stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + NSString *urlString = [NSString stringWithFormat:@"msauth://%@?%@=%@", + MSID_MDM_ENROLL_HOST, MSID_INTUNE_URL_KEY, encoded]; + NSURL *URL = [NSURL URLWithString:urlString]; + XCTestExpectation *expectation = [self expectationWithDescription:@"completion invoked"]; + + [self.handler handleSpecialRedirectURL:URL + embeddedWebviewController:nil + completion:^(MSIDWebviewNavigationDecision * _Nullable decision, NSError * _Nullable error) + { + XCTAssertNotNil(decision); + XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); + XCTAssertNil([decision.request valueForHTTPHeaderField:MSID_BROKER_VER_KEY]); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:1.0]; +} + +- (void)testHandleSpecialRedirectURL_whenFirstPartyProcess_shouldAttachAppNameAndVersionHeadersOnEnrollRequest +{ + // The two-argument (overall/non-broker) overload builds the app-identity headers from the + // running process. Force a first-party keychain team ID so the gate opens; assert the + // headers equal the same bundle accessors the handler uses. + // (Keychain has no DI seam in this repo, so a swizzle is used as a one-off.) + [MSIDTestSwizzle instanceMethod:@selector(teamId) + class:[MSIDKeychainUtil class] + block:(id)^NSString *(__unused id obj) { return @"UBF8T346G9"; }]; + + NSString *targetURL = @"https://manage.microsoft.com/enroll"; + NSString *encoded = [targetURL stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + NSString *urlString = [NSString stringWithFormat:@"msauth://%@?%@=%@", + MSID_MDM_ENROLL_HOST, MSID_INTUNE_URL_KEY, encoded]; + NSURL *URL = [NSURL URLWithString:urlString]; + XCTestExpectation *expectation = [self expectationWithDescription:@"completion invoked"]; + + [self.handler handleSpecialRedirectURL:URL + embeddedWebviewController:nil + completion:^(MSIDWebviewNavigationDecision * _Nullable decision, NSError * _Nullable error) + { + XCTAssertNotNil(decision); + XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); + XCTAssertEqualObjects([decision.request valueForHTTPHeaderField:MSID_APP_NAME_KEY], [NSBundle msidAppName]); + XCTAssertEqualObjects([decision.request valueForHTTPHeaderField:MSID_APP_VER_KEY], [NSBundle msidAppVersion]); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:1.0]; +} + +- (void)testHandleSpecialRedirectURL_whenNonFirstPartyProcess_shouldNotAttachAppNameAndVersionHeadersOnEnrollRequest +{ + // A non-first-party running process must never have the app name/version headers stamped. + [MSIDTestSwizzle instanceMethod:@selector(teamId) + class:[MSIDKeychainUtil class] + block:(id)^NSString *(__unused id obj) { return @"43AQ936H96"; }]; + + NSString *targetURL = @"https://manage.microsoft.com/enroll"; + NSString *encoded = [targetURL stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + NSString *urlString = [NSString stringWithFormat:@"msauth://%@?%@=%@", + MSID_MDM_ENROLL_HOST, MSID_INTUNE_URL_KEY, encoded]; + NSURL *URL = [NSURL URLWithString:urlString]; + XCTestExpectation *expectation = [self expectationWithDescription:@"completion invoked"]; + + [self.handler handleSpecialRedirectURL:URL + embeddedWebviewController:nil + completion:^(MSIDWebviewNavigationDecision * _Nullable decision, NSError * _Nullable error) + { + XCTAssertNotNil(decision); + XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionLoadRequest); + XCTAssertNil([decision.request valueForHTTPHeaderField:MSID_APP_NAME_KEY]); + XCTAssertNil([decision.request valueForHTTPHeaderField:MSID_APP_VER_KEY]); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:1.0]; +} + +#pragma mark - processNavigationResponseAndCheckForASWebAuthHandoff:embeddedWebviewController: (synchronous) // An allowed response URL used by happy-path tests below. Matches an entry in // MSIDASWebAuthenticationConstants.asWebAuthAllowedDomains so the origin check passes. @@ -523,12 +661,23 @@ - (void)testHandleSpecialRedirectURL_shouldInvokeCompletionExactlyOnce return [NSURL URLWithString:@"https://portal.manage.microsoft.com/some/path"]; } +// Helper to create an NSHTTPURLResponse with given headers and URL. +static NSHTTPURLResponse *MSIDTestHTTPResponse(NSDictionary *headers, NSURL *url) +{ + return [[NSHTTPURLResponse alloc] initWithURL:url + statusCode:200 + HTTPVersion:@"HTTP/1.1" + headerFields:headers]; +} + - (void)testProcessResponseHeaders_whenNoHandoffHeader_shouldReturnNO { NSDictionary *headers = @{@"Content-Type": @"application/json"}; - BOOL hasHandoff = [self.handler processResponseHeadersAndCheckForASWebAuthHandoff:headers - responseURL:MSIDTestAllowedResponseURL()]; + NSHTTPURLResponse *response = MSIDTestHTTPResponse(headers, MSIDTestAllowedResponseURL()); + + BOOL hasHandoff = [self.handler processNavigationResponseAndCheckForASWebAuthHandoff:response + embeddedWebviewController:nil]; XCTAssertFalse(hasHandoff); // Side effect: headers are still normalized into lastResponseHeaders for later use. @@ -539,18 +688,10 @@ - (void)testProcessResponseHeaders_whenHandoffHeaderIsEmptyString_shouldReturnNO { NSDictionary *headers = @{MSID_ASWEBAUTH_HANDOFF_URL_KEY: @""}; - BOOL hasHandoff = [self.handler processResponseHeadersAndCheckForASWebAuthHandoff:headers - responseURL:MSIDTestAllowedResponseURL()]; - - XCTAssertFalse(hasHandoff); -} - -- (void)testProcessResponseHeaders_whenHandoffHeaderIsNonString_shouldReturnNO -{ - NSDictionary *headers = @{MSID_ASWEBAUTH_HANDOFF_URL_KEY: @42}; + NSHTTPURLResponse *response = MSIDTestHTTPResponse(headers, MSIDTestAllowedResponseURL()); - BOOL hasHandoff = [self.handler processResponseHeadersAndCheckForASWebAuthHandoff:headers - responseURL:MSIDTestAllowedResponseURL()]; + BOOL hasHandoff = [self.handler processNavigationResponseAndCheckForASWebAuthHandoff:response + embeddedWebviewController:nil]; XCTAssertFalse(hasHandoff); } @@ -561,8 +702,10 @@ - (void)testProcessResponseHeaders_whenHandoffHeaderIsMixedCase_shouldStillBeDet NSString *uppercaseKey = MSID_ASWEBAUTH_HANDOFF_URL_KEY.uppercaseString; NSDictionary *headers = @{uppercaseKey: @"https://www.example.com/handoff"}; - BOOL hasHandoff = [self.handler processResponseHeadersAndCheckForASWebAuthHandoff:headers - responseURL:MSIDTestAllowedResponseURL()]; + NSHTTPURLResponse *response = MSIDTestHTTPResponse(headers, MSIDTestAllowedResponseURL()); + + BOOL hasHandoff = [self.handler processNavigationResponseAndCheckForASWebAuthHandoff:response + embeddedWebviewController:nil]; XCTAssertTrue(hasHandoff); // The normalized headers should expose the lowercased key for later use. @@ -575,24 +718,29 @@ - (void)testProcessResponseHeaders_alwaysUpdatesLastResponseHeadersToNormalizedF self.handler.lastResponseHeaders = @{@"stale": @"value"}; NSDictionary *headers = @{@"X-Custom": @"v1", @"Other-Header": @"v2"}; - (void)[self.handler processResponseHeadersAndCheckForASWebAuthHandoff:headers - responseURL:MSIDTestAllowedResponseURL()]; + NSHTTPURLResponse *response = MSIDTestHTTPResponse(headers, MSIDTestAllowedResponseURL()); + + (void)[self.handler processNavigationResponseAndCheckForASWebAuthHandoff:response + embeddedWebviewController:nil]; XCTAssertEqualObjects(self.handler.lastResponseHeaders[@"x-custom"], @"v1"); XCTAssertEqualObjects(self.handler.lastResponseHeaders[@"other-header"], @"v2"); XCTAssertNil(self.handler.lastResponseHeaders[@"stale"], @"Previous headers must be replaced, not merged."); } -#pragma mark - processResponseHeadersAndCheckForASWebAuthHandoff:responseURL: (response-URL origin gate) +#pragma mark - processNavigationResponseAndCheckForASWebAuthHandoff:embeddedWebviewController: (response-URL origin gate) -- (void)testProcessResponseHeaders_whenHandoffHeaderPresentButResponseURLIsNil_shouldReturnNOAndStillCacheHeaders +- (void)testProcessResponseHeaders_whenHandoffHeaderPresentButResponseURLIsInvalid_shouldReturnNOAndStillCacheHeaders { // Security gate: an attacker-controlled page (or a non-HTTP response somehow reaching here) - // must not be able to force a hand-off by injecting only the header. + // must not be able to force a hand-off by injecting only the header. Use a syntactically valid + // but non-HTTPS origin (about:blank) so the origin gate — not URL construction — is exercised. NSDictionary *headers = @{MSID_ASWEBAUTH_HANDOFF_URL_KEY: @"https://portal.manage.microsoft.com/handoff"}; - BOOL hasHandoff = [self.handler processResponseHeadersAndCheckForASWebAuthHandoff:headers - responseURL:nil]; + NSHTTPURLResponse *response = MSIDTestHTTPResponse(headers, [NSURL URLWithString:@"about:blank"]); + + BOOL hasHandoff = [self.handler processNavigationResponseAndCheckForASWebAuthHandoff:response + embeddedWebviewController:nil]; XCTAssertFalse(hasHandoff); // Headers are still cached so downstream consumers see consistent state. @@ -606,8 +754,10 @@ - (void)testProcessResponseHeaders_whenHandoffHeaderPresentButResponseURLIsHTTP_ NSDictionary *headers = @{MSID_ASWEBAUTH_HANDOFF_URL_KEY: @"https://portal.manage.microsoft.com/handoff"}; NSURL *httpResponseURL = [NSURL URLWithString:@"http://portal.manage.microsoft.com/some/path"]; - BOOL hasHandoff = [self.handler processResponseHeadersAndCheckForASWebAuthHandoff:headers - responseURL:httpResponseURL]; + NSHTTPURLResponse *response = MSIDTestHTTPResponse(headers, httpResponseURL); + + BOOL hasHandoff = [self.handler processNavigationResponseAndCheckForASWebAuthHandoff:response + embeddedWebviewController:nil]; XCTAssertFalse(hasHandoff); } @@ -619,8 +769,10 @@ - (void)testProcessResponseHeaders_whenHandoffHeaderPresentButResponseURLHostNot NSDictionary *headers = @{MSID_ASWEBAUTH_HANDOFF_URL_KEY: @"https://portal.manage.microsoft.com/handoff"}; NSURL *attackerOrigin = [NSURL URLWithString:@"https://evil.example.com/landing"]; - BOOL hasHandoff = [self.handler processResponseHeadersAndCheckForASWebAuthHandoff:headers - responseURL:attackerOrigin]; + NSHTTPURLResponse *response = MSIDTestHTTPResponse(headers, attackerOrigin); + + BOOL hasHandoff = [self.handler processNavigationResponseAndCheckForASWebAuthHandoff:response + embeddedWebviewController:nil]; XCTAssertFalse(hasHandoff); } @@ -631,8 +783,10 @@ - (void)testProcessResponseHeaders_whenHandoffHeaderPresentButResponseURLLooksLi NSDictionary *headers = @{MSID_ASWEBAUTH_HANDOFF_URL_KEY: @"https://portal.manage.microsoft.com/handoff"}; NSURL *spoofedOrigin = [NSURL URLWithString:@"https://portal.manage.microsoft.com.attacker.com/path"]; - BOOL hasHandoff = [self.handler processResponseHeadersAndCheckForASWebAuthHandoff:headers - responseURL:spoofedOrigin]; + NSHTTPURLResponse *response = MSIDTestHTTPResponse(headers, spoofedOrigin); + + BOOL hasHandoff = [self.handler processNavigationResponseAndCheckForASWebAuthHandoff:response + embeddedWebviewController:nil]; XCTAssertFalse(hasHandoff); } @@ -641,8 +795,10 @@ - (void)testProcessResponseHeaders_whenHandoffHeaderPresentAndResponseURLIsAllow { NSDictionary *headers = @{MSID_ASWEBAUTH_HANDOFF_URL_KEY: @"https://portal.manage.microsoft.com/handoff"}; - BOOL hasHandoff = [self.handler processResponseHeadersAndCheckForASWebAuthHandoff:headers - responseURL:MSIDTestAllowedResponseURL()]; + NSHTTPURLResponse *response = MSIDTestHTTPResponse(headers, MSIDTestAllowedResponseURL()); + + BOOL hasHandoff = [self.handler processNavigationResponseAndCheckForASWebAuthHandoff:response + embeddedWebviewController:nil]; XCTAssertTrue(hasHandoff); } @@ -653,8 +809,10 @@ - (void)testProcessResponseHeaders_whenHandoffHeaderPresentAndResponseURLHostIsU NSDictionary *headers = @{MSID_ASWEBAUTH_HANDOFF_URL_KEY: @"https://portal.manage.microsoft.com/handoff"}; NSURL *mixedCaseOrigin = [NSURL URLWithString:@"https://PORTAL.MANAGE.microsoft.com/path"]; - BOOL hasHandoff = [self.handler processResponseHeadersAndCheckForASWebAuthHandoff:headers - responseURL:mixedCaseOrigin]; + NSHTTPURLResponse *response = MSIDTestHTTPResponse(headers, mixedCaseOrigin); + + BOOL hasHandoff = [self.handler processNavigationResponseAndCheckForASWebAuthHandoff:response + embeddedWebviewController:nil]; XCTAssertTrue(hasHandoff); } @@ -675,7 +833,7 @@ - (void)testPerformASWebAuthHandoff_whenCompletionIsNil_shouldNotCrash - (void)testPerformASWebAuthHandoff_whenNoHandoffURLCaptured_shouldCompleteWithFailWithError { - // No prior processResponseHeadersAndCheckForASWebAuthHandoff:responseURL: call captured a hand-off URL. + // No prior processNavigationResponseAndCheckForASWebAuthHandoff:embeddedWebviewController: call captured a hand-off URL. XCTestExpectation *expectation = [self expectationWithDescription:@"completion invoked"]; MSIDViewController *parent = [MSIDViewController new]; @@ -686,7 +844,8 @@ - (void)testPerformASWebAuthHandoff_whenNoHandoffURLCaptured_shouldCompleteWithF XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionFailWithError); XCTAssertNotNil(decision.error); - XCTAssertNil(error); + XCTAssertNotNil(error); + XCTAssertEqualObjects(error, decision.error); [expectation fulfill]; }]; @@ -697,7 +856,7 @@ - (void)testPerformASWebAuthHandoff_whenHandoffURLFailsValidation_shouldComplete { // Capture a hand-off URL whose domain is not in the allowlist so validation // short-circuits before reaching the system webview transition manager. - // Bypass the processResponseHeadersAndCheckForASWebAuthHandoff:responseURL: origin gate by + // Bypass the processNavigationResponseAndCheckForASWebAuthHandoff:embeddedWebviewController: origin gate by // populating lastResponseHeaders directly — this test isolates the perform-side validation. self.handler.lastResponseHeaders = @{MSID_ASWEBAUTH_HANDOFF_URL_KEY: @"https://www.example.com/handoff"}; @@ -711,11 +870,458 @@ - (void)testPerformASWebAuthHandoff_whenHandoffURLFailsValidation_shouldComplete XCTAssertNotNil(decision); XCTAssertEqual(decision.type, MSIDWebviewNavigationDecisionFailWithError); XCTAssertNotNil(decision.error); - XCTAssertNil(error); + XCTAssertNotNil(error); + XCTAssertEqualObjects(error, decision.error); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:1.0]; +} + +#pragma mark - scheduleMDMProfileInstalledNotificationIfNeeded + +- (void)testScheduleMDMProfileInstalledNotification_whenPurposeIsDownloadProfileAndProviderSet_shouldScheduleWithDefaultDelay +{ + MSIDMockUXCallbackProvider *mockProvider = [MSIDMockUXCallbackProvider new]; + MSIDUXCallbackProvider.uxCallbackProvider = mockProvider; + + self.handler.lastResponseHeaders = @{ MSID_ASWEBAUTH_HANDOFF_PURPOSE_KEY: MSID_ASWEBAUTH_HANDOFF_PURPOSE_VALUE_DOWNLOAD_PROFILE }; + + [self.handler scheduleMDMProfileInstalledNotificationIfNeeded]; + + XCTAssertTrue(mockProvider.scheduleCalled, @"Notification should be scheduled for the profile-download hand-off."); + XCTAssertEqualWithAccuracy(mockProvider.receivedDelay, MSIDMDMProfileInstalledNotificationDefaultDelay, 0.01); +} + +- (void)testScheduleMDMProfileInstalledNotification_whenFlightConfiguresDelay_shouldPassFlightDelay +{ + MSIDMockUXCallbackProvider *mockProvider = [MSIDMockUXCallbackProvider new]; + MSIDUXCallbackProvider.uxCallbackProvider = mockProvider; + + self.flightProvider.stringForKeyContainer = @{ MSID_FLIGHT_MDM_PROFILE_INSTALLED_NOTIFICATION_DELAY: @"5" }; + + self.handler.lastResponseHeaders = @{ MSID_ASWEBAUTH_HANDOFF_PURPOSE_KEY: MSID_ASWEBAUTH_HANDOFF_PURPOSE_VALUE_DOWNLOAD_PROFILE }; + + [self.handler scheduleMDMProfileInstalledNotificationIfNeeded]; + + XCTAssertTrue(mockProvider.scheduleCalled); + XCTAssertEqualWithAccuracy(mockProvider.receivedDelay, 5.0, 0.01); +} + +- (void)testScheduleMDMProfileInstalledNotification_whenFlightDelayIsNegative_shouldFallbackToDefault +{ + MSIDMockUXCallbackProvider *mockProvider = [MSIDMockUXCallbackProvider new]; + MSIDUXCallbackProvider.uxCallbackProvider = mockProvider; + + self.flightProvider.stringForKeyContainer = @{ MSID_FLIGHT_MDM_PROFILE_INSTALLED_NOTIFICATION_DELAY: @"-5" }; + + self.handler.lastResponseHeaders = @{ MSID_ASWEBAUTH_HANDOFF_PURPOSE_KEY: MSID_ASWEBAUTH_HANDOFF_PURPOSE_VALUE_DOWNLOAD_PROFILE }; + + [self.handler scheduleMDMProfileInstalledNotificationIfNeeded]; + + XCTAssertTrue(mockProvider.scheduleCalled); + XCTAssertEqualWithAccuracy(mockProvider.receivedDelay, MSIDMDMProfileInstalledNotificationDefaultDelay, 0.01); +} + +- (void)testScheduleMDMProfileInstalledNotification_whenPurposeHasDifferentCase_shouldSchedule +{ + MSIDMockUXCallbackProvider *mockProvider = [MSIDMockUXCallbackProvider new]; + MSIDUXCallbackProvider.uxCallbackProvider = mockProvider; + + self.handler.lastResponseHeaders = @{ MSID_ASWEBAUTH_HANDOFF_PURPOSE_KEY: @"Download-Profile" }; + + [self.handler scheduleMDMProfileInstalledNotificationIfNeeded]; + + XCTAssertTrue(mockProvider.scheduleCalled, @"Match on the purpose value should be case-insensitive."); +} + +- (void)testScheduleMDMProfileInstalledNotification_whenPurposeIsDifferentValue_shouldNotSchedule +{ + MSIDMockUXCallbackProvider *mockProvider = [MSIDMockUXCallbackProvider new]; + MSIDUXCallbackProvider.uxCallbackProvider = mockProvider; + + self.handler.lastResponseHeaders = @{ MSID_ASWEBAUTH_HANDOFF_PURPOSE_KEY: @"sign-in" }; + + [self.handler scheduleMDMProfileInstalledNotificationIfNeeded]; + + XCTAssertFalse(mockProvider.scheduleCalled, @"Notification must not be scheduled for a non profile-download purpose."); +} + +- (void)testScheduleMDMProfileInstalledNotification_whenPurposeHeaderAbsent_shouldNotSchedule +{ + MSIDMockUXCallbackProvider *mockProvider = [MSIDMockUXCallbackProvider new]; + MSIDUXCallbackProvider.uxCallbackProvider = mockProvider; + + self.handler.lastResponseHeaders = @{}; + + [self.handler scheduleMDMProfileInstalledNotificationIfNeeded]; + + XCTAssertFalse(mockProvider.scheduleCalled, @"With no purpose header (no fallback), the notification must not be scheduled."); +} + +- (void)testScheduleMDMProfileInstalledNotification_whenProviderIsNil_shouldNotCrash +{ + MSIDUXCallbackProvider.uxCallbackProvider = nil; + + self.handler.lastResponseHeaders = @{ MSID_ASWEBAUTH_HANDOFF_PURPOSE_KEY: MSID_ASWEBAUTH_HANDOFF_PURPOSE_VALUE_DOWNLOAD_PROFILE }; + + XCTAssertNoThrow([self.handler scheduleMDMProfileInstalledNotificationIfNeeded]); +} + +- (void)testScheduleMDMProfileInstalledNotification_whenScheduled_shouldStampNotificationScheduled +{ + MSIDMockUXCallbackProvider *mockProvider = [MSIDMockUXCallbackProvider new]; + MSIDUXCallbackProvider.uxCallbackProvider = mockProvider; + + MSIDOnboardingBlobBuilder *builder = [self onboardingBuilderForHandoffTest]; + self.handler.onboardingBlobBuilder = builder; + self.handler.lastResponseHeaders = @{ MSID_ASWEBAUTH_HANDOFF_PURPOSE_KEY: MSID_ASWEBAUTH_HANDOFF_PURPOSE_VALUE_DOWNLOAD_PROFILE }; + + [self.handler scheduleMDMProfileInstalledNotificationIfNeeded]; + + XCTAssertTrue(mockProvider.scheduleCalled); + XCTAssertTrue([[self stampedStepIdsFromBuilder:builder] containsObject:MSIDOnboardingBlobStepProfileInstallNotificationScheduled], + @"A successful schedule must stamp ProfileInstallNotificationScheduled."); +} + +- (void)testScheduleMDMProfileInstalledNotification_whenPurposeIsDifferentValue_shouldNotStampNotificationScheduled +{ + MSIDMockUXCallbackProvider *mockProvider = [MSIDMockUXCallbackProvider new]; + MSIDUXCallbackProvider.uxCallbackProvider = mockProvider; + + MSIDOnboardingBlobBuilder *builder = [self onboardingBuilderForHandoffTest]; + self.handler.onboardingBlobBuilder = builder; + self.handler.lastResponseHeaders = @{ MSID_ASWEBAUTH_HANDOFF_PURPOSE_KEY: @"sign-in" }; + + [self.handler scheduleMDMProfileInstalledNotificationIfNeeded]; + + XCTAssertFalse(mockProvider.scheduleCalled); + XCTAssertFalse([[self stampedStepIdsFromBuilder:builder] containsObject:MSIDOnboardingBlobStepProfileInstallNotificationScheduled], + @"No scheduling for a non profile-download purpose means no stamp."); +} + +- (void)testScheduleMDMProfileInstalledNotification_whenProviderIsNil_shouldNotStampNotificationScheduled +{ + MSIDUXCallbackProvider.uxCallbackProvider = nil; + + MSIDOnboardingBlobBuilder *builder = [self onboardingBuilderForHandoffTest]; + self.handler.onboardingBlobBuilder = builder; + self.handler.lastResponseHeaders = @{ MSID_ASWEBAUTH_HANDOFF_PURPOSE_KEY: MSID_ASWEBAUTH_HANDOFF_PURPOSE_VALUE_DOWNLOAD_PROFILE }; + + [self.handler scheduleMDMProfileInstalledNotificationIfNeeded]; + + XCTAssertFalse([[self stampedStepIdsFromBuilder:builder] containsObject:MSIDOnboardingBlobStepProfileInstallNotificationScheduled], + @"The stamp is recorded only alongside an actual schedule, so a nil provider records nothing."); +} + +#pragma mark - performASWebAuthenticationHandoff onboarding telemetry + +// Finalizes the given builder and returns the ordered list of stamped step_id values. +- (NSArray *)stampedStepIdsFromBuilder:(MSIDOnboardingBlobBuilder *)builder +{ + NSData *data = [[builder finalizeBlob] dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *parsed = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + NSMutableArray *stepIds = [NSMutableArray new]; + for (NSDictionary *step in parsed[@"steps_list"]) + { + [stepIds addObject:step[@"step_id"]]; + } + return stepIds; +} + +- (MSIDOnboardingBlobBuilder *)onboardingBuilderForHandoffTest +{ + NSDictionary *seed = @{@"schema_version": @"1.0.0", @"session_correlation_id": @"abc-123", @"onboarding_mode": @"non-brokered"}; + NSString *seedJson = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:seed options:0 error:nil] + encoding:NSUTF8StringEncoding]; + return [[MSIDOnboardingBlobBuilder alloc] initWithSeedJson:seedJson clientId:@"clientA" target:@"resource1"]; +} + +- (void)testPerformASWebAuthHandoff_whenBuilderPresent_shouldStampSessionStarted +{ + MSIDOnboardingBlobBuilder *builder = [self onboardingBuilderForHandoffTest]; + self.handler.onboardingBlobBuilder = builder; + + XCTestExpectation *expectation = [self expectationWithDescription:@"completion invoked"]; + MSIDViewController *parent = [MSIDViewController new]; + [self.handler performASWebAuthenticationHandoffWithParentController:parent + completion:^(MSIDWebviewNavigationDecision * _Nullable decision, + NSError * _Nullable error) + { + (void)decision; (void)error; + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:1.0]; + + XCTAssertTrue([[self stampedStepIdsFromBuilder:builder] containsObject:MSIDOnboardingBlobStepProfileDownloadFlowStarted]); +} + +- (void)testPerformASWebAuthHandoff_whenNoHandoffURLCaptured_shouldStampSessionStartFailedNotCompleted +{ + MSIDOnboardingBlobBuilder *builder = [self onboardingBuilderForHandoffTest]; + self.handler.onboardingBlobBuilder = builder; + + XCTestExpectation *expectation = [self expectationWithDescription:@"completion invoked"]; + MSIDViewController *parent = [MSIDViewController new]; + [self.handler performASWebAuthenticationHandoffWithParentController:parent + completion:^(MSIDWebviewNavigationDecision * _Nullable decision, + NSError * _Nullable error) + { + (void)decision; (void)error; + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:1.0]; + + NSArray *steps = [self stampedStepIdsFromBuilder:builder]; + XCTAssertTrue([steps containsObject:MSIDOnboardingBlobStepProfileDownloadFlowStarted]); + XCTAssertTrue([steps containsObject:MSIDOnboardingBlobStepProfileDownloadFlowFailed]); +} + +- (void)testPerformASWebAuthHandoff_whenHandoffURLFailsValidation_shouldStampSessionStartFailedNotCompleted +{ + MSIDOnboardingBlobBuilder *builder = [self onboardingBuilderForHandoffTest]; + self.handler.onboardingBlobBuilder = builder; + self.handler.lastResponseHeaders = @{MSID_ASWEBAUTH_HANDOFF_URL_KEY: @"https://www.example.com/handoff"}; + + XCTestExpectation *expectation = [self expectationWithDescription:@"completion invoked"]; + MSIDViewController *parent = [MSIDViewController new]; + [self.handler performASWebAuthenticationHandoffWithParentController:parent + completion:^(MSIDWebviewNavigationDecision * _Nullable decision, + NSError * _Nullable error) + { + (void)decision; (void)error; + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:1.0]; + + NSArray *steps = [self stampedStepIdsFromBuilder:builder]; + XCTAssertTrue([steps containsObject:MSIDOnboardingBlobStepProfileDownloadFlowFailed]); +} + +// Swizzles the ASWeb transition so it completes synchronously with the injected +// (callbackURL, error), exercising the hand-off outcome classification without +// launching a real ASWebAuthenticationSession. +- (void)swizzleTransitionWithCallbackURL:(NSURL *)callbackURL error:(NSError *)error +{ + [self swizzleTransitionWithInvocationBlock:nil callbackURL:callbackURL error:error]; +} + +- (void)swizzleTransitionWithInvocationBlock:(void (^ _Nullable)(void))invocationBlock + callbackURL:(NSURL *)callbackURL + error:(NSError *)error +{ + [MSIDTestSwizzle instanceMethod:@selector(transitionToSystemWebviewWithURL:redirectURI:parentController:useAuthenticationSession:allowSafariViewController:useEphemeralSession:additionalHeaders:context:completionBlock:) + class:[MSIDSystemWebviewTransitionManager class] + block:(id)^(__unused id obj, + __unused NSURL *URL, + __unused NSString *redirectURI, + __unused MSIDViewController *parentController, + __unused BOOL useAuthenticationSession, + __unused BOOL allowSafariViewController, + __unused BOOL useEphemeralSession, + __unused NSDictionary *additionalHeaders, + __unused id context, + MSIDWebUICompletionHandler completionBlock) + { + if (invocationBlock) + { + invocationBlock(); + } + + if (completionBlock) + { + completionBlock(callbackURL, error); + } + }]; +} + +- (void)testPerformASWebAuthHandoff_whenPurposeIsDownloadProfile_shouldScheduleBeforeTransition +{ + MSIDMockUXCallbackProvider *mockProvider = [MSIDMockUXCallbackProvider new]; + MSIDUXCallbackProvider.uxCallbackProvider = mockProvider; + + MSIDOnboardingBlobBuilder *builder = [self onboardingBuilderForHandoffTest]; + self.handler.onboardingBlobBuilder = builder; + self.handler.lastResponseHeaders = @{ + MSID_ASWEBAUTH_HANDOFF_URL_KEY: @"https://portal.manage.microsoft.com/handoff", + MSID_ASWEBAUTH_HANDOFF_PURPOSE_KEY: MSID_ASWEBAUTH_HANDOFF_PURPOSE_VALUE_DOWNLOAD_PROFILE + }; + + XCTestExpectation *transitionExpectation = [self expectationWithDescription:@"system transition invoked"]; + [self swizzleTransitionWithInvocationBlock:^ + { + XCTAssertTrue(mockProvider.scheduleCalled, @"The reminder must be scheduled before the system transition starts."); + XCTAssertEqualWithAccuracy(mockProvider.receivedDelay, MSIDMDMProfileInstalledNotificationDefaultDelay, 0.01); + [transitionExpectation fulfill]; + } + callbackURL:[NSURL URLWithString:@"msauth://profile_download_complete"] + error:nil]; + + XCTestExpectation *completionExpectation = [self expectationWithDescription:@"completion invoked"]; + [self.handler performASWebAuthenticationHandoffWithParentController:[MSIDViewController new] + completion:^(__unused MSIDWebviewNavigationDecision * _Nullable decision, + __unused NSError * _Nullable error) + { + [completionExpectation fulfill]; + }]; + + [self waitForExpectations:@[transitionExpectation, completionExpectation] timeout:1.0]; + XCTAssertTrue([[self stampedStepIdsFromBuilder:builder] containsObject:MSIDOnboardingBlobStepProfileInstallNotificationScheduled]); +} + +- (void)testPerformASWebAuthHandoff_whenPurposeIsDownloadProfileAndFlightConfiguresDelay_shouldUseFlightDelayBeforeTransition +{ + MSIDMockUXCallbackProvider *mockProvider = [MSIDMockUXCallbackProvider new]; + MSIDUXCallbackProvider.uxCallbackProvider = mockProvider; + self.flightProvider.stringForKeyContainer = @{ MSID_FLIGHT_MDM_PROFILE_INSTALLED_NOTIFICATION_DELAY: @"300" }; + self.handler.lastResponseHeaders = @{ + MSID_ASWEBAUTH_HANDOFF_URL_KEY: @"https://portal.manage.microsoft.com/handoff", + MSID_ASWEBAUTH_HANDOFF_PURPOSE_KEY: MSID_ASWEBAUTH_HANDOFF_PURPOSE_VALUE_DOWNLOAD_PROFILE + }; + + XCTestExpectation *transitionExpectation = [self expectationWithDescription:@"system transition invoked"]; + [self swizzleTransitionWithInvocationBlock:^ + { + XCTAssertTrue(mockProvider.scheduleCalled, @"The reminder must be scheduled before the system transition starts."); + XCTAssertEqualWithAccuracy(mockProvider.receivedDelay, 300.0, 0.01); + [transitionExpectation fulfill]; + } + callbackURL:[NSURL URLWithString:@"msauth://profile_download_complete"] + error:nil]; + + XCTestExpectation *completionExpectation = [self expectationWithDescription:@"completion invoked"]; + [self.handler performASWebAuthenticationHandoffWithParentController:[MSIDViewController new] + completion:^(__unused MSIDWebviewNavigationDecision * _Nullable decision, + __unused NSError * _Nullable error) + { + [completionExpectation fulfill]; + }]; + + [self waitForExpectations:@[transitionExpectation, completionExpectation] timeout:1.0]; +} + +- (void)testPerformASWebAuthHandoff_whenTransitionCancelledByUser_shouldStampCancelledNotFailed +{ + MSIDOnboardingBlobBuilder *builder = [self onboardingBuilderForHandoffTest]; + self.handler.onboardingBlobBuilder = builder; + self.handler.lastResponseHeaders = @{MSID_ASWEBAUTH_HANDOFF_URL_KEY: @"https://portal.manage.microsoft.com/handoff"}; + + NSError *cancelError = MSIDCreateError(MSIDErrorDomain, MSIDErrorUserCancel, @"User cancelled", nil, nil, nil, nil, nil, NO); + [self swizzleTransitionWithCallbackURL:nil error:cancelError]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"completion invoked"]; + MSIDViewController *parent = [MSIDViewController new]; + [self.handler performASWebAuthenticationHandoffWithParentController:parent + completion:^(MSIDWebviewNavigationDecision * _Nullable decision, + NSError * _Nullable error) + { + (void)decision; (void)error; + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:1.0]; + + NSArray *steps = [self stampedStepIdsFromBuilder:builder]; + XCTAssertTrue([steps containsObject:MSIDOnboardingBlobStepProfileDownloadFlowCancelled]); + XCTAssertFalse([steps containsObject:MSIDOnboardingBlobStepProfileDownloadFlowFailed]); +} + +- (void)testPerformASWebAuthHandoff_whenTransitionFailsWithNonCancelError_shouldStampFailedNotCancelled +{ + MSIDOnboardingBlobBuilder *builder = [self onboardingBuilderForHandoffTest]; + self.handler.onboardingBlobBuilder = builder; + self.handler.lastResponseHeaders = @{MSID_ASWEBAUTH_HANDOFF_URL_KEY: @"https://portal.manage.microsoft.com/handoff"}; + + NSError *serverError = MSIDCreateError(MSIDErrorDomain, MSIDErrorServerInvalidResponse, @"Server error", nil, nil, nil, nil, nil, NO); + [self swizzleTransitionWithCallbackURL:nil error:serverError]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"completion invoked"]; + MSIDViewController *parent = [MSIDViewController new]; + [self.handler performASWebAuthenticationHandoffWithParentController:parent + completion:^(MSIDWebviewNavigationDecision * _Nullable decision, + NSError * _Nullable error) + { + (void)decision; (void)error; + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:1.0]; + + NSArray *steps = [self stampedStepIdsFromBuilder:builder]; + XCTAssertTrue([steps containsObject:MSIDOnboardingBlobStepProfileDownloadFlowFailed]); + XCTAssertFalse([steps containsObject:MSIDOnboardingBlobStepProfileDownloadFlowCancelled]); +} + +- (void)testPerformASWebAuthHandoff_whenCancelCodeFromForeignDomain_shouldStampFailedNotCancelled +{ + MSIDOnboardingBlobBuilder *builder = [self onboardingBuilderForHandoffTest]; + self.handler.onboardingBlobBuilder = builder; + self.handler.lastResponseHeaders = @{MSID_ASWEBAUTH_HANDOFF_URL_KEY: @"https://portal.manage.microsoft.com/handoff"}; + + // Same numeric code as MSIDErrorUserCancel but from a foreign domain: the domain + // guard must classify this as Failed, not Cancelled. + NSError *foreignError = [NSError errorWithDomain:@"SomeOtherDomain" code:MSIDErrorUserCancel userInfo:nil]; + [self swizzleTransitionWithCallbackURL:nil error:foreignError]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"completion invoked"]; + MSIDViewController *parent = [MSIDViewController new]; + [self.handler performASWebAuthenticationHandoffWithParentController:parent + completion:^(MSIDWebviewNavigationDecision * _Nullable decision, + NSError * _Nullable error) + { + (void)decision; (void)error; [expectation fulfill]; }]; + [self waitForExpectations:@[expectation] timeout:1.0]; + + NSArray *steps = [self stampedStepIdsFromBuilder:builder]; + XCTAssertTrue([steps containsObject:MSIDOnboardingBlobStepProfileDownloadFlowFailed]); + XCTAssertFalse([steps containsObject:MSIDOnboardingBlobStepProfileDownloadFlowCancelled]); +} + +- (void)testPerformASWebAuthHandoff_whenNoBuilder_shouldNotCrash +{ + self.handler.onboardingBlobBuilder = nil; + + XCTestExpectation *expectation = [self expectationWithDescription:@"completion invoked"]; + MSIDViewController *parent = [MSIDViewController new]; + XCTAssertNoThrow([self.handler performASWebAuthenticationHandoffWithParentController:parent + completion:^(MSIDWebviewNavigationDecision * _Nullable decision, + NSError * _Nullable error) + { + (void)decision; (void)error; + [expectation fulfill]; + }]); + [self waitForExpectations:@[expectation] timeout:1.0]; +} + +- (void)testPerformASWebAuthHandoff_whenHandoffHeaderIsNonString_shouldFailWithoutHandoff +{ + MSIDOnboardingBlobBuilder *builder = [self onboardingBuilderForHandoffTest]; + self.handler.onboardingBlobBuilder = builder; + // Non-string handoff header value: the isKindOfClass:NSString guard must reject it + // so the flow fails cleanly instead of misinterpreting it as a valid hand-off URL. + self.handler.lastResponseHeaders = @{MSID_ASWEBAUTH_HANDOFF_URL_KEY: @42}; + XCTestExpectation *expectation = [self expectationWithDescription:@"completion invoked"]; + MSIDViewController *parent = [MSIDViewController new]; + __block MSIDWebviewNavigationDecision *capturedDecision = nil; + __block NSError *capturedError = nil; + [self.handler performASWebAuthenticationHandoffWithParentController:parent + completion:^(MSIDWebviewNavigationDecision * _Nullable decision, + NSError * _Nullable error) + { + capturedDecision = decision; + capturedError = error; + [expectation fulfill]; + }]; [self waitForExpectations:@[expectation] timeout:1.0]; + + XCTAssertNotNil(capturedError); + XCTAssertEqualObjects(capturedError.domain, MSIDErrorDomain); + XCTAssertEqual(capturedError.code, MSIDErrorInternal); + XCTAssertNotNil(capturedDecision); + + NSArray *steps = [self stampedStepIdsFromBuilder:builder]; + XCTAssertFalse([steps containsObject:MSIDOnboardingBlobStepProfileDownloadFlowCancelled]); } #endif // !MSID_EXCLUDE_SYSTEMWV diff --git a/IdentityCore/tests/automation/ui_tests_lib/MSIDBaseUITest.m b/IdentityCore/tests/automation/ui_tests_lib/MSIDBaseUITest.m index 571e5c656a..d1887f7663 100644 --- a/IdentityCore/tests/automation/ui_tests_lib/MSIDBaseUITest.m +++ b/IdentityCore/tests/automation/ui_tests_lib/MSIDBaseUITest.m @@ -246,9 +246,9 @@ - (void)performAction:(NSString *)action } sleep(1); - [application.buttons[action] msidTap]; + [self tapActionButtonWhenHittable:application.buttons[action] application:application]; #else - [application.buttons[action] msidTap]; + [self tapActionButtonWhenHittable:application.buttons[action] application:application]; if (jsonString) { @@ -260,6 +260,58 @@ - (void)performAction:(NSString *)action #endif } +// The automation host app's action buttons are arranged subviews of a +// UIStackView with no enclosing scroll container (see MainAutomation.storyboard). +// Diagnostics ruled out a degenerate-frame issue: the button reports +// exists=1, isHittable=1, and a perfectly valid, fully on-screen frame right +// before tapping. +// +// The activity log revealed the real race: between us requesting the tap +// and XCTest synthesizing it, XCTest runs its own "make frontmost" dance — +// "Check for interrupting elements affecting