├── .gitignore ├── AADB2CGuides ├── Enabling Partners, Suppliers, and Customers to Access Applications with Azure Active Directory.pdf ├── GDPR Considerations for Customer Facing Applications with Azure AD B2C.pdf └── Migrating Application Authentication to Azure AD B2C in a Hybrid Environment.pdf ├── ADFS to AzureAD App Migration ├── ADFS to AAD App Migration Report Template.xlsm ├── ADFS to AAD Migration Tool Help File.docx ├── ADFSAADMigrationUtils.psm1 ├── Media │ ├── image1.png │ ├── image10.png │ ├── image11.png │ ├── image2.png │ ├── image3.png │ ├── image4.png │ ├── image5.png │ ├── image6.png │ ├── image7.png │ ├── image8.png │ └── image9.png ├── Readme.md └── samples │ ├── ADFS2AADUtils.psm1 │ └── New-AzureADAppFromADFSRP.ps1 ├── Access Panel └── Access Panel Deployment Plan.docx ├── Application Proxy └── Application Proxy Deployment Plan.docx ├── Authentication ├── Migrating from Federated Authentication to Pass-through Authentication.docx ├── Migrating from Federated Authentication to Password Hash Synchronization.docx └── Seamless Single Sign-On Deployment Plan.docx ├── Conditional Access ├── CA Deployment Plan.docx └── Readme ├── LICENSE ├── Log Analytics Views ├── Azure AD Account Provisioning Events.omsview ├── AzureADSignins.omsview ├── configure-signal-logic.png ├── create-rule.png ├── details.png └── readme.md ├── Migrationwhitepapers └── Migrating Your Applications to Azure Active Directory.pdf ├── Multi Factor Authentication └── MFADeploymentPlan.docx ├── README.md ├── Self Service Password Reset -SSPR └── SSPR Deployment Plan.docx ├── Single Sign On - SSO └── SaaS SSO Deployment Plan.docx └── User Provisioning ├── Outbound User Provisioning Deployment Plan.docx └── Workday-driven Inbound User Provisioning Deployment Plan.docx /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /AADB2CGuides/Enabling Partners, Suppliers, and Customers to Access Applications with Azure Active Directory.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/AADB2CGuides/Enabling Partners, Suppliers, and Customers to Access Applications with Azure Active Directory.pdf -------------------------------------------------------------------------------- /AADB2CGuides/GDPR Considerations for Customer Facing Applications with Azure AD B2C.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/AADB2CGuides/GDPR Considerations for Customer Facing Applications with Azure AD B2C.pdf -------------------------------------------------------------------------------- /AADB2CGuides/Migrating Application Authentication to Azure AD B2C in a Hybrid Environment.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/AADB2CGuides/Migrating Application Authentication to Azure AD B2C in a Hybrid Environment.pdf -------------------------------------------------------------------------------- /ADFS to AzureAD App Migration/ADFS to AAD App Migration Report Template.xlsm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/ADFS to AzureAD App Migration/ADFS to AAD App Migration Report Template.xlsm -------------------------------------------------------------------------------- /ADFS to AzureAD App Migration/ADFS to AAD Migration Tool Help File.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/ADFS to AzureAD App Migration/ADFS to AAD Migration Tool Help File.docx -------------------------------------------------------------------------------- /ADFS to AzureAD App Migration/ADFSAADMigrationUtils.psm1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | .SYNOPSIS 4 | ADFSAADMigrationUtils.psm1 is a Windows PowerShell module that contains functions to analyze ADFS configuration and tests for compatibility to Migrate to Azure Active Directory 5 | 6 | .DESCRIPTION 7 | 8 | Version: 1.0.0 9 | 10 | ADFSAADMigrationUtils.psm1 is a Windows PowerShell module that contains functions to analyze ADFS configuration and tests for compatibility to Migrate to Azure Active Directory 11 | 12 | 13 | .DISCLAIMER 14 | THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF 15 | ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO 16 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A 17 | PARTICULAR PURPOSE. 18 | 19 | Copyright (c) Microsoft Corporation. All rights reserved. 20 | #> 21 | 22 | Function Remove-InvalidFileNameChars 23 | { 24 | param( 25 | [Parameter(Mandatory=$true, 26 | Position=0, 27 | ValueFromPipeline=$true, 28 | ValueFromPipelineByPropertyName=$true)] 29 | [String]$Name 30 | ) 31 | 32 | $invalidChars = [IO.Path]::GetInvalidFileNameChars() -join '' 33 | $re = "[{0}]" -f [RegEx]::Escape($invalidChars) 34 | return ($Name -replace $re) 35 | } 36 | 37 | 38 | ########################################### 39 | # RP Trust Claim Rule checks 40 | ########################################### 41 | 42 | Add-Type -Language CSharp @" 43 | public class MigrationTestResult 44 | { 45 | public string TestName; 46 | public string ADFSObjectType; 47 | public string ADFSObjectIdentifier; 48 | 49 | public ResultType Result; 50 | public string Message; 51 | public string ExceptionMessage; 52 | public System.Collections.Hashtable Details; 53 | 54 | public MigrationTestResult() 55 | { 56 | Result = ResultType.Pass; 57 | Details = new System.Collections.Hashtable(); 58 | } 59 | } 60 | 61 | public enum ResultType 62 | { 63 | Pass = 0, 64 | Warning = 1, 65 | Fail = 2 66 | } 67 | "@; 68 | 69 | 70 | ########## 71 | #templatized claim rules 72 | ########## 73 | 74 | $MFAMigratableRules = 75 | @{ 76 | "MFA for a User" = 77 | @" 78 | c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid", Value == "__ANYVALUE__"] 79 | => issue(Type = "http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod", Value = "http://schemas.microsoft.com/claims/multipleauthn"); 80 | "@; 81 | "MFA for a Group" = 82 | @" 83 | c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid", Value == "__ANYVALUE__"] 84 | => issue(Type = "http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod", Value = "http://schemas.microsoft.com/claims/multipleauthn"); 85 | "@; 86 | "MFA for unregistered devices" = 87 | @" 88 | c:[Type == "http://schemas.microsoft.com/2012/01/devicecontext/claims/isregistereduser", Value == "false"] 89 | => issue(Type = "http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod", Value = "http://schemas.microsoft.com/claims/multipleauthn"); 90 | "@ 91 | "MFA for extranet" = 92 | @" 93 | c:[Type == "http://schemas.microsoft.com/ws/2012/01/insidecorporatenetwork", Value == "false"] 94 | => issue(Type = "http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod", Value = "http://schemas.microsoft.com/claims/multipleauthn"); 95 | "@ 96 | } 97 | 98 | $DelegationMigratableRules = 99 | @{ 100 | } 101 | 102 | $ImpersonationMigratableRules = 103 | @{ 104 | "ADFS V2 - ProxySid by user" = 105 | @" 106 | c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid", Issuer =~ "^(AD AUTHORITY|SELF AUTHORITY|LOCAL AUTHORITY)$"] 107 | => issue(store = "_ProxyCredentialStore", types = ("http://schemas.microsoft.com/authorization/claims/permit"), query = "isProxySid({0})", param = c.Value); 108 | "@ 109 | "ADFS V2 - ProxySid by group" = 110 | @" 111 | c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid", Issuer =~ "^(AD AUTHORITY|SELF AUTHORITY|LOCAL AUTHORITY)$"] 112 | => issue(store = "_ProxyCredentialStore", types = ("http://schemas.microsoft.com/authorization/claims/permit"), query = "isProxySid({0})", param = c.Value); 113 | "@ 114 | "ADFS V2 - Proxy Trust check" = 115 | @" 116 | c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/proxytrustid", Issuer =~ "^SELF AUTHORITY$"] 117 | => issue(store = "_ProxyCredentialStore", types = ("http://schemas.microsoft.com/authorization/claims/permit"), query = "isProxyTrustProvisioned({0})", param = c.Value); 118 | "@ 119 | } 120 | 121 | $IssuanceAuthorizationMigratableRules = 122 | @{ 123 | "Permit All" = 124 | @" 125 | @RuleTemplate = "AllowAllAuthzRule" 126 | => issue(Type = "http://schemas.microsoft.com/authorization/claims/permit", Value = "true"); 127 | "@ 128 | "Permit a group" = 129 | @" 130 | Assign to groups 131 | @RuleTemplate = "Authorization" 132 | @RuleName = "__ANYVALUE__" 133 | c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid", Value =~ "__ANYVALUE__"] 134 | => issue(Type = "http://schemas.microsoft.com/authorization/claims/permit", Value = "PermitUsersWithClaim"); 135 | "@ 136 | } 137 | 138 | $IssuanceTransformMigratableRules = 139 | @{ 140 | "Extract Attributes from AD" = 141 | @" 142 | @RuleTemplate = "LdapClaims" 143 | @RuleName = "__ANYVALUE__" 144 | c:[Type == "__ANYVALUE__", Issuer == "AD AUTHORITY"] 145 | => issue(store = "Active Directory", types = (__ANYVALUE__), query = ";__ANYVALUE__;{0}", param = c.Value); 146 | "@ 147 | } 148 | 149 | Function Invoke-ADFSClaimRuleAnalysis 150 | { 151 | [CmdletBinding()] 152 | param 153 | ( 154 | [String] 155 | $RuleSetName, 156 | [String] 157 | $ADFSRuleSet, 158 | [Parameter(Mandatory=$true)] 159 | [System.Collections.Hashtable] 160 | $KnownRules 161 | ) 162 | 163 | #Task 1: Compare rule against known patterns of migratable rules 164 | 165 | $ADFSRuleArray = @() 166 | 167 | if (-not [String]::IsNullOrEmpty($ADFSRuleSet)) 168 | { 169 | $ADFSRuleArray = New-AdfsClaimRuleSet -ClaimRule $ADFSRuleSet 170 | } 171 | 172 | 173 | $Details = "" 174 | $ruleIndex = 0 175 | $AnalysisPassed = $true 176 | 177 | foreach($Rule in $ADFSRuleArray.ClaimRules) 178 | { 179 | #Create result object 180 | $Result = new-object PSObject 181 | $Result | Add-Member -NotePropertyName "RuleSet" -NotePropertyValue $RuleSetName 182 | $Result | Add-Member -NotePropertyName "Rule" -NotePropertyValue $Rule 183 | $ruleIndex++ 184 | 185 | 186 | #Task 1: Find Match to known pattern 187 | $matchFound = $false 188 | $migratablePatternName = "N/A" 189 | 190 | foreach($knownRuleKey in $KnownRules.Keys) 191 | { 192 | $knownRuleRegex = $KnownRules[$knownRuleKey] 193 | $knownRuleRegex = [Regex]::Escape($knownRuleRegex).Replace("__ANYVALUE__", ".*").TrimEnd() 194 | 195 | 196 | #JSON files have \r\n instead or \n ... adding flexibility to match these 197 | $knownRuleRegex = $knownRuleRegex.Replace("\n", "\r?\n*") 198 | 199 | if ($rule -match $knownRuleRegex) 200 | { 201 | $migratablePatternName = $knownRuleKey 202 | $matchFound = $true 203 | } 204 | } 205 | 206 | $Result | Add-Member -NotePropertyName "IsKnownRuleMigratablePattern" -NotePropertyValue $matchFound 207 | $Result | Add-Member -NotePropertyName "KnownRulePatternName" -NotePropertyValue $migratablePatternName 208 | 209 | 210 | #Task 2: Break down condition and issuance statement 211 | #Assumption: There is only one "=>" unambigous match in the rule 212 | $separatorIndex = $Rule.IndexOf("=>") 213 | $conditionStatement = $Rule.Substring(0,$separatorIndex).Trim(); 214 | $issuanceStatement = $Rule.Substring($separatorIndex+2).Trim(); 215 | 216 | $Result | Add-Member -NotePropertyName "ConditionStatement" -NotePropertyValue $conditionStatement 217 | $Result | Add-Member -NotePropertyName "IssuanceStatement" -NotePropertyValue $issuanceStatement 218 | 219 | #Task 3: Find claim types in the condition statement 220 | $TypeRegex = '(?i)type\s+={1,2}\s+"(.*?)"' 221 | $ConditionTypeMatch = [Regex]::Match($conditionStatement, $TypeRegex) 222 | if ($ConditionTypeMatch.Success) 223 | { 224 | #TODO: How does this work with claims with multiple condition in the types (eg. c:[Type=="foo"] && c1:[Type=="bar"] 225 | $ConditionClaimType = $ConditionTypeMatch.Groups[1].ToString() 226 | $Result | Add-Member -NotePropertyName "ConditionClaimType" -NotePropertyValue $ConditionClaimType 227 | 228 | $GroupFilter = "N/A" 229 | 230 | if ($ConditionClaimType -eq "http://schemas.xmlsoap.org/claims/Group") 231 | { 232 | $GroupFilterRegex = '(?i)Value\s+(=(~|=)\s+"(.*?)")' 233 | $GroupFilterMatch = [Regex]::Match($conditionStatement, $GroupFilterRegex) 234 | if ($GroupFilterMatch.Success) 235 | { 236 | $GroupFilter = $GroupFilterMatch.Groups[1].ToString() 237 | } 238 | } 239 | 240 | $Result | Add-Member -NotePropertyName "GroupFilter" -NotePropertyValue $GroupFilter 241 | } 242 | 243 | #Task 4: Find claim types in the issuance statement -- explicit Type = .* 244 | 245 | $IssuanceClaimTypes = @() 246 | 247 | $IssuanceTypeMatch = [Regex]::Match($issuanceStatement, $TypeRegex) 248 | if ($IssuanceTypeMatch.Success) 249 | { 250 | $IssuanceClaimTypes += $IssuanceTypeMatch.Groups[1].ToString() 251 | } 252 | 253 | #Task 4a : Find claim types in the issuance statement from the Attribute Store 254 | $AttributeStoreRuleRegex = '(?i).*store\s*=\s*"(.*?)"' 255 | $AttributeStoreName = "N/A" 256 | $AttributeStoreQuery = "N/A" 257 | $ActiveDirectoryAttributesSplit = @() 258 | 259 | 260 | $AttributeStoreRuleMatch = [Regex]::Match($issuanceStatement, $AttributeStoreRuleRegex) 261 | if ($AttributeStoreRuleMatch.Success) 262 | { 263 | $AttributeStoreName = $AttributeStoreRuleMatch.Groups[1].ToString() 264 | $AttributeStoreRuleTypesRegex = '(?i)types\s*=\s*\("(.*?)"\)' 265 | $AttributeStoreRuleTypesMatch = [Regex]::Match($issuanceStatement, $AttributeStoreRuleTypesRegex) 266 | 267 | if ($AttributeStoreRuleTypesMatch.Success) 268 | { 269 | $IssuanceClaimTypes += $AttributeStoreRuleTypesMatch.Groups[1].ToString().Split(',').Trim().Trim('"'); 270 | } 271 | 272 | #Task 4b: Extract the attributes retrieved from the store 273 | $AttributeStoreQueryRegex = '(?i)query\s*=\s*"(.*?)"' 274 | $AttributeStoreQueryMatch = [Regex]::Match($issuanceStatement, $AttributeStoreQueryRegex) 275 | 276 | if ($AttributeStoreQueryMatch.Success) 277 | { 278 | $AttributeStoreQuery = $AttributeStoreQueryMatch.Groups[1].ToString(); 279 | if ($AttributeStoreName -ieq "Active Directory") 280 | { 281 | $AttributeStoreQuerySplit = $AttributeStoreQuery.Split(';'); 282 | $ActiveDirectoryAttributes = $AttributeStoreQuerySplit[1]; 283 | $ActiveDirectoryAttributesSplit = $ActiveDirectoryAttributes.Split(',') 284 | } 285 | } 286 | } 287 | 288 | $Result | Add-Member -NotePropertyName "IssuanceClaimTypes" -NotePropertyValue $IssuanceClaimTypes 289 | $Result | Add-Member -NotePropertyName "AttributeStoreName" -NotePropertyValue $AttributeStoreName 290 | $Result | Add-Member -NotePropertyName "AttributeStoreQuery" -NotePropertyValue $AttributeStoreQuery 291 | $Result | Add-Member -NotePropertyName "ADAttributes" -NotePropertyValue $ActiveDirectoryAttributesSplit 292 | 293 | Write-Output $Result 294 | } 295 | 296 | } 297 | 298 | Function Test-ADFSRPRuleset 299 | { 300 | [CmdletBinding()] 301 | param 302 | ( 303 | [Parameter(Mandatory=$true)] 304 | [String] 305 | $RulesetName, 306 | [String] 307 | $ADFSRuleSet, 308 | [Parameter(Mandatory=$true)] 309 | [System.Collections.Hashtable] 310 | $KnownRules, 311 | [Parameter(Mandatory=$true)] 312 | [ResultType] 313 | $ResultTypeIfUnknownPattern 314 | ) 315 | 316 | $TestResult = New-Object MigrationTestResult 317 | 318 | $RuleAnalysisResult = Invoke-ADFSClaimRuleAnalysis -ADFSRuleSet $ADFSRuleSet -KnownRules $KnownRules -RuleSetName $RulesetName 319 | 320 | #Capture the expanded details of each rule as a result 321 | $TestResult.Details.Add("ClaimRuleProperties", $RuleAnalysisResult) 322 | 323 | #Insight 1: Did we find claim rules that don't match any template 324 | 325 | $UnknownClaimRulePatternFound = @($RuleAnalysisResult | where {$_.IsKnownRuleMigratablePattern -eq $false}).Count -gt 0 326 | $TestResult.Details.Add("UnkwnownPatternFound", $UnknownClaimRulePatternFound) 327 | 328 | if ($UnknownClaimRulePatternFound) 329 | { 330 | $TestResult.Result = $ResultTypeIfUnknownPattern 331 | $TestResult.Message = "At least one non-migratable rule was detected" 332 | } 333 | 334 | Return $TestResult 335 | } 336 | 337 | Function Test-ADFSRPAdditionalAuthenticationRules 338 | { 339 | [CmdletBinding()] 340 | param 341 | ( 342 | [Parameter(Mandatory=$true)] 343 | $ADFSRelyingPartyTrust 344 | ) 345 | 346 | Test-ADFSRPRuleset ` 347 | -RulesetName "AdditionalAuthentication" ` 348 | -ADFSRuleSet $ADFSRelyingPartyTrust.AdditionalAuthenticationRules ` 349 | -KnownRules $MFAMigratableRules ` 350 | -ResultTypeIfUnknownPattern Fail 351 | 352 | } 353 | 354 | Function Test-ADFSRPDelegationAuthorizationRules 355 | { 356 | [CmdletBinding()] 357 | param 358 | ( 359 | [Parameter(Mandatory=$true)] 360 | $ADFSRelyingPartyTrust 361 | ) 362 | 363 | Test-ADFSRPRuleset ` 364 | -RulesetName "DelegationAuthorization" ` 365 | -ADFSRuleSet $ADFSRelyingPartyTrust.DelegationAuthorizationRules ` 366 | -KnownRules $DelegationMigratableRules ` 367 | -ResultTypeIfUnknownPattern Warning 368 | 369 | } 370 | 371 | Function Test-ADFSRPImpersonationAuthorizationRules 372 | { 373 | [CmdletBinding()] 374 | param 375 | ( 376 | [Parameter(Mandatory=$true)] 377 | $ADFSRelyingPartyTrust 378 | ) 379 | 380 | Test-ADFSRPRuleset ` 381 | -RulesetName "ImpersonationAuthorization" ` 382 | -ADFSRuleSet $ADFSRelyingPartyTrust.ImpersonationAuthorizationRules ` 383 | -KnownRules $ImpersonationMigratableRules ` 384 | -ResultTypeIfUnknownPattern Warning 385 | 386 | } 387 | 388 | Function Test-ADFSRPIssuanceAuthorizationRules 389 | { 390 | [CmdletBinding()] 391 | param 392 | ( 393 | [Parameter(Mandatory=$true)] 394 | $ADFSRelyingPartyTrust 395 | ) 396 | 397 | Test-ADFSRPRuleset ` 398 | -RulesetName "IssuanceAuthorization" ` 399 | -ADFSRuleSet $ADFSRelyingPartyTrust.IssuanceAuthorizationRules ` 400 | -KnownRules $IssuanceAuthorizationMigratableRules ` 401 | -ResultTypeIfUnknownPattern Warning 402 | } 403 | 404 | Function Test-ADFSRPIssuanceTransformRules 405 | { 406 | [CmdletBinding()] 407 | param 408 | ( 409 | [Parameter(Mandatory=$true)] 410 | $ADFSRelyingPartyTrust 411 | ) 412 | 413 | Test-ADFSRPRuleset ` 414 | -RulesetName "IssuanceTransform" ` 415 | -ADFSRuleSet $ADFSRelyingPartyTrust.IssuanceTransformRules ` 416 | -KnownRules $IssuanceTransformMigratableRules ` 417 | -ResultTypeIfUnknownPattern Warning 418 | 419 | } 420 | 421 | 422 | ########################################### 423 | # RP Trust properties migration checks 424 | ########################################### 425 | 426 | Function Test-ADFSRPAdditionalWSFedEndpoint 427 | { 428 | [CmdletBinding()] 429 | param 430 | ( 431 | [Parameter(Mandatory=$true)] 432 | $ADFSRelyingPartyTrust 433 | ) 434 | 435 | $TestResult = New-Object MigrationTestResult 436 | 437 | if ($ADFSRelyingPartyTrust.AdditionalWSFedEndpoint.Count -gt 0) 438 | { 439 | $TestResult.Result = [ResultType]::Fail 440 | $TestResult.Message = "Relying Party has additional WS-Federation Endpoints." 441 | 442 | } 443 | else 444 | { 445 | $TestResult.Message = "No additional WS-Federation endpoints were found" 446 | } 447 | 448 | $TestResult.Details.Add("AdditionalWSFedEndpoint.Count", $ADFSRelyingPartyTrust.AdditionalWSFedEndpoint.Count) 449 | 450 | Return $TestResult 451 | } 452 | 453 | Function Test-ADFSRPAllowedAuthenticationClassReferences 454 | { 455 | [CmdletBinding()] 456 | param 457 | ( 458 | [Parameter(Mandatory=$true)] 459 | $ADFSRelyingPartyTrust 460 | ) 461 | 462 | $TestResult = New-Object MigrationTestResult 463 | 464 | if ($ADFSRelyingPartyTrust.AllowedAuthenticationClassReferences.Count -gt 0) 465 | { 466 | $TestResult.Result = [ResultType]::Fail 467 | $TestResult.Message = "Relying Party has set AllowedAuthenticationClassReferences." 468 | 469 | } 470 | else 471 | { 472 | $TestResult.Message = "AllowedAuthenticationClassReferences is not set up." 473 | } 474 | 475 | $TestResult.Details.Add("AllowedAuthenticationClassReferences.Count", $ADFSRelyingPartyTrust.AllowedAuthenticationClassReferences.Count) 476 | 477 | Return $TestResult 478 | } 479 | 480 | Function Test-ADFSRPAlwaysRequireAuthentication 481 | { 482 | [CmdletBinding()] 483 | param 484 | ( 485 | [Parameter(Mandatory=$true)] 486 | $ADFSRelyingPartyTrust 487 | ) 488 | 489 | $TestResult = New-Object MigrationTestResult 490 | 491 | if ($ADFSRelyingPartyTrust.AlwaysRequireAuthentication) 492 | { 493 | $TestResult.Result = [ResultType]::Fail 494 | $TestResult.Message = "Relying Party has AlwaysRequireAuthentication enabled" 495 | } 496 | else 497 | { 498 | $TestResult.Message = "AlwaysRequireAuthentication is not set up." 499 | } 500 | 501 | $TestResult.Details.Add("AlwaysRequireAuthentication", $ADFSRelyingPartyTrust.AlwaysRequireAuthentication) 502 | 503 | Return $TestResult 504 | } 505 | 506 | Function Test-ADFSRPAutoUpdateEnabled 507 | { 508 | [CmdletBinding()] 509 | param 510 | ( 511 | [Parameter(Mandatory=$true)] 512 | $ADFSRelyingPartyTrust 513 | ) 514 | 515 | $TestResult = New-Object MigrationTestResult 516 | 517 | if ($ADFSRelyingPartyTrust.AutoUpdateEnabled) #CSV: False is string "0" 518 | { 519 | $TestResult.Result = [ResultType]::Warning 520 | $TestResult.Message = "Relying Party has AutoUpdateEnabled set to true" 521 | 522 | } 523 | else 524 | { 525 | $TestResult.Message = "AutoUpdateEnabled is not set up." 526 | } 527 | 528 | $TestResult.Details.Add("AutoUpdateEnabled", $ADFSRelyingPartyTrust.AutoUpdateEnabled) 529 | 530 | Return $TestResult 531 | } 532 | 533 | Function Test-ADFSRPClaimsProviderName 534 | { 535 | [CmdletBinding()] 536 | param 537 | ( 538 | [Parameter(Mandatory=$true)] 539 | $ADFSRelyingPartyTrust 540 | ) 541 | 542 | 543 | $TestResult = New-Object MigrationTestResult 544 | $TestResult.Details.Add("ClaimsProviderName.Count", $ADFSRelyingPartyTrust.ClaimsProviderName.Count) 545 | 546 | if ($ADFSRelyingPartyTrust.ClaimsProviderName.Count -gt 1) 547 | { 548 | $TestResult.Result = [ResultType]::Fail 549 | $TestResult.Message = "Relying Party has multiple ClaimsProviders enabled" 550 | } 551 | elseif ($ADFSRelyingPartyTrust.ClaimsProviderName.Count -eq 1 -and $ADFSRelyingPartyTrust.ClaimsProviderName[0] -ne 'Active Directory') 552 | { 553 | $TestResult.Result = [ResultType]::Fail 554 | $TestResult.Message = "Relying Party has a non-Active Directory store: $($ADFSRelyingPartyTrust.ClaimsProviderName[0])" 555 | } 556 | else 557 | { 558 | $TestResult.Message = "No Additional Claim Providers were configured." 559 | } 560 | 561 | 562 | 563 | Return $TestResult 564 | } 565 | 566 | Function Test-ADFSRPEncryptClaims 567 | { 568 | [CmdletBinding()] 569 | param 570 | ( 571 | [Parameter(Mandatory=$true)] 572 | $ADFSRelyingPartyTrust 573 | ) 574 | 575 | $TestResult = New-Object MigrationTestResult 576 | 577 | #CSV: "0" string is false 578 | 579 | if ($ADFSRelyingPartyTrust.EncryptClaims -and $ADFSRelyingPartyTrust.EncryptionCertificate -ne $null) 580 | { 581 | $TestResult.Result = [ResultType]::Pass 582 | $TestResult.Message = "Relying Party is set to encrypt claims. This is supported by Azure AD" 583 | 584 | } 585 | else 586 | { 587 | $TestResult.Message = "Relying Party is not set to encrypt claims." 588 | } 589 | 590 | $TestResult.Details.Add("EncryptClaims", $ADFSRelyingPartyTrust.EncryptClaims) 591 | 592 | Return $TestResult 593 | } 594 | 595 | Function Test-ADFSRPEncryptedNameIdRequired 596 | { 597 | [CmdletBinding()] 598 | param 599 | ( 600 | [Parameter(Mandatory=$true)] 601 | $ADFSRelyingPartyTrust 602 | ) 603 | 604 | $TestResult = New-Object MigrationTestResult 605 | 606 | #CSV: "0" string is false 607 | 608 | if ($ADFSRelyingPartyTrust.EncryptedNameIdRequired -and $ADFSRelyingPartyTrust.EncryptionCertificate -ne $null) 609 | { 610 | $TestResult.Result = [ResultType]::Fail 611 | $TestResult.Message = "Relying Party is set to encrypt Name ID." 612 | 613 | } 614 | else 615 | { 616 | $TestResult.Message = "Relying Party is not set to encrypt name ID." 617 | } 618 | 619 | $TestResult.Details.Add("EncryptedNameIdRequired", $ADFSRelyingPartyTrust.EncryptedNameIdRequired) 620 | 621 | Return $TestResult 622 | } 623 | 624 | Function Test-ADFSRPMonitoringEnabled 625 | { 626 | [CmdletBinding()] 627 | param 628 | ( 629 | [Parameter(Mandatory=$true)] 630 | $ADFSRelyingPartyTrust 631 | ) 632 | 633 | $TestResult = New-Object MigrationTestResult 634 | 635 | if ($ADFSRelyingPartyTrust.MonitoringEnabled) #CSV: boolean syntax 636 | { 637 | $TestResult.Result = [ResultType]::Warning 638 | $TestResult.Message = "Relying Party has MonitoringEnabled set to true" 639 | 640 | } 641 | else 642 | { 643 | $TestResult.Message = "MonitoringEnabled is not set up." 644 | } 645 | 646 | $TestResult.Details.Add("MonitoringEnabled", $ADFSRelyingPartyTrust.MonitoringEnabled) 647 | 648 | Return $TestResult 649 | } 650 | 651 | Function Test-ADFSRPNotBeforeSkew 652 | { 653 | [CmdletBinding()] 654 | param 655 | ( 656 | [Parameter(Mandatory=$true)] 657 | $ADFSRelyingPartyTrust 658 | ) 659 | 660 | $TestResult = New-Object MigrationTestResult 661 | 662 | if ($ADFSRelyingPartyTrust.NotBeforeSkew -gt 0) #CSV: Int Syntax 663 | { 664 | $TestResult.Result = [ResultType]::Warning 665 | $TestResult.Message = "Relying Party has NotBeforeSkew configured" 666 | 667 | } 668 | else 669 | { 670 | $TestResult.Message = "NotBeforeSkew is not set up." 671 | } 672 | 673 | $TestResult.Details.Add("NotBeforeSkew", $ADFSRelyingPartyTrust.NotBeforeSkew) 674 | 675 | Return $TestResult 676 | } 677 | 678 | Function Test-ADFSRPRequestMFAFromClaimsProviders 679 | { 680 | [CmdletBinding()] 681 | param 682 | ( 683 | [Parameter(Mandatory=$true)] 684 | $ADFSRelyingPartyTrust 685 | ) 686 | 687 | $TestResult = New-Object MigrationTestResult 688 | 689 | if ($ADFSRelyingPartyTrust.RequestMFAFromClaimsProviders) #CSV: Boolean syntax 690 | { 691 | $TestResult.Result = [ResultType]::Warning 692 | $TestResult.Message = "Relying Party has RequestMFAFromClaimsProviders set to true" 693 | 694 | } 695 | else 696 | { 697 | $TestResult.Message = "RequestMFAFromClaimsProviders is not set up." 698 | } 699 | 700 | $TestResult.Details.Add("RequestMFAFromClaimsProviders", $ADFSRelyingPartyTrust.RequestMFAFromClaimsProviders) 701 | 702 | Return $TestResult 703 | } 704 | 705 | Function Test-ADFSRPSignedSamlRequestsRequired 706 | { 707 | [CmdletBinding()] 708 | param 709 | ( 710 | [Parameter(Mandatory=$true)] 711 | $ADFSRelyingPartyTrust 712 | ) 713 | 714 | $TestResult = New-Object MigrationTestResult 715 | 716 | if ($ADFSRelyingPartyTrust.SignedSamlRequestsRequired) #CSV: Boolean syntax 717 | { 718 | $TestResult.Result = [ResultType]::Warning 719 | $TestResult.Message = "Relying Party has SignedSamlRequestsRequired set to true" 720 | 721 | } 722 | else 723 | { 724 | $TestResult.Message = "SignedSamlRequestsRequired is not set up." 725 | } 726 | 727 | $TestResult.Details.Add("SignedSamlRequestsRequired", $ADFSRelyingPartyTrust.SignedSamlRequestsRequired) 728 | 729 | Return $TestResult 730 | } 731 | 732 | Function Test-ADFSRPTokenLifetime 733 | { 734 | [CmdletBinding()] 735 | param 736 | ( 737 | [Parameter(Mandatory=$true)] 738 | $ADFSRelyingPartyTrust 739 | ) 740 | 741 | $TestResult = New-Object MigrationTestResult 742 | 743 | if ($ADFSRelyingPartyTrust.TokenLifetime -gt 0 -and $ADFSRelyingPartyTrust.TokenLifetime -lt 10) #CSV: Int Syntax 744 | { 745 | $TestResult.Result = [ResultType]::Fail 746 | $TestResult.Message = "TokenLifetime is set to less than 10 minutes" 747 | 748 | } 749 | else 750 | { 751 | $TestResult.Message = "TokenLifetime is set to a supported value." 752 | } 753 | 754 | $TestResult.Details.Add("TokenLifetime", $ADFSRelyingPartyTrust.TokenLifetime) 755 | 756 | Return $TestResult 757 | } 758 | 759 | ########################################### 760 | # Orchestrating functions 761 | ########################################### 762 | 763 | Function Invoke-TestFunctions([array]$functionsToRun, $ADFSRelyingPartyTrust) 764 | { 765 | $RPStopWatch = [System.Diagnostics.Stopwatch]::StartNew() 766 | $results = @() 767 | $totalFunctions = $functionsToRun.Count 768 | $functionCount = 0 769 | foreach($function in $functionsToRun) 770 | { 771 | $FunctionStopWatch = [System.Diagnostics.Stopwatch]::StartNew() 772 | $StartTime = (Get-Date).Millisecond 773 | $functionCount++ 774 | $percent = 100 * $functionCount / $totalFunctions 775 | #Write-Progress -Activity "Executing Tests" -Status $function -PercentComplete $percent -Id 10 -ParentId 1 776 | $ScriptString = "param(`$ADFSRP) $function -ADFSRelyingPartyTrust `$ADFSRP" 777 | $functionScriptBlock = [ScriptBlock]::Create($ScriptString) 778 | $result = Invoke-Command -NoNewScope -ScriptBlock $functionScriptBlock -ArgumentList ($ADFSRelyingPartyTrust) 779 | $result.TestName = $function 780 | $result.ADFSObjectType = "Relying Party" 781 | $result.ADFSObjectIdentifier = $ADFSRelyingPartyTrust.Name 782 | $results = $results + $result 783 | $FunctionStopWatch.Stop() 784 | #Write-Debug "$function`: $($FunctionStopWatch.Elapsed.TotalMilliseconds) milliseconds to run" 785 | } 786 | $RPStopWatch.Stop() 787 | Write-Debug "-------------$($ADFSRelyingPartyTrust.Name)`: $($RPStopWatch.Elapsed.TotalMilliseconds) milliseconds to run" 788 | 789 | return $results 790 | } 791 | 792 | 793 | <# 794 | .Synopsis 795 | Analyzes an individual Relying Party trust object 796 | 797 | .Description 798 | The cmdlet expects an RP Trust object and returns an object with four complex properties: 799 | * AggregateReportRow: Object with individual properties per each compatibility test performed 800 | (e.g. Test-ADFSRPAdditionalWSFedEndpoint) 801 | * AttributeReportRows: List of Active Directory Attributes found in the RP Trust rule sets. 802 | There is one element in the list for every attribute found 803 | * AttributeStoreReportRows: List of Attribute Stores found in the RP Trust rule sets. There is one 804 | element for every RP Trust and Attribute store found 805 | * ClaimTypeReportRows: List of Claim Types found in the RP Trust rule sets. There is one row for every 806 | RP Trust and Claim Type found 807 | 808 | .Parameter ADFSRPTrust 809 | AD FS Relying party trust Object (either deserialized from a file or straight from AD FS Powershell) 810 | 811 | .Example 812 | Run the test from the ADFS Federation Server: 813 | Get-AdfsRelyingPartyTrust -Identifier urn:myCRMApp | Test-ADFS2AADOnPremRPTrust 814 | #> 815 | 816 | 817 | Function Test-ADFS2AADOnPremRPTrust 818 | { 819 | [CmdletBinding()] 820 | param 821 | ( 822 | [Parameter(Mandatory=$true, ValueFromPipeline=$true)] 823 | $ADFSRPTrust 824 | ) 825 | 826 | $functionsToRun = @( ` 827 | "Test-ADFSRPAdditionalAuthenticationRules", 828 | "Test-ADFSRPAdditionalWSFedEndpoint", 829 | "Test-ADFSRPAllowedAuthenticationClassReferences", 830 | "Test-ADFSRPAlwaysRequireAuthentication", 831 | "Test-ADFSRPAutoUpdateEnabled", 832 | "Test-ADFSRPClaimsProviderName", 833 | "Test-ADFSRPDelegationAuthorizationRules", #out 834 | "Test-ADFSRPEncryptClaims", 835 | "Test-ADFSRPImpersonationAuthorizationRules", #out 836 | "Test-ADFSRPIssuanceAuthorizationRules", #out 837 | "Test-ADFSRPIssuanceTransformRules", 838 | "Test-ADFSRPMonitoringEnabled", 839 | "Test-ADFSRPNotBeforeSkew", 840 | "Test-ADFSRPRequestMFAFromClaimsProviders", 841 | "Test-ADFSRPSignedSamlRequestsRequired", 842 | "Test-ADFSRPTokenLifetime", 843 | "Test-ADFSRPEncryptedNameIdRequired" 844 | ); 845 | 846 | $rpTestResults = Invoke-TestFunctions -FunctionsToRun $functionsToRun -ADFSRelyingPartyTrust $ADFSRPTrust 847 | 848 | $attributeReportRows = @() 849 | $attributeStoreReportRows = @() 850 | $claimTypesReportRows = @() 851 | $RuleDetailReportRows = @() 852 | 853 | #now, assemble the result object 854 | $aggregateReportRow= New-Object -TypeName PSObject 855 | $aggregateReportRow| Add-Member -MemberType NoteProperty -Name "RP Name" -Value $ADFSRPTrust.Name 856 | $aggregateReportRow| Add-Member -MemberType NoteProperty -Name "Result" -Value Pass 857 | 858 | $aggregateMessage = "" 859 | $aggregateDetail = "" 860 | $aggregateNotPassTests = "" 861 | 862 | 863 | foreach($rpTestResult in $rpTestResults) 864 | { 865 | 866 | $aggregateReportRow | Add-Member -MemberType NoteProperty -Name $rpTestResult.TestName -Value $rpTestResult.Result 867 | 868 | if ($rpTestResult.Result -eq [ResultType]::Fail) 869 | { 870 | $aggregateReportRow.Result = [ResultType]::Fail 871 | $aggregateNotPassTests += $rpTestResult.TestName + "(Fail);" 872 | } 873 | 874 | if ($rpTestResult.Result -eq [ResultType]::Warning -and $aggregateReportRow.Result -ne [ResultType]::Fail) 875 | { 876 | $aggregateReportRow.Result = [ResultType]::Warning 877 | $aggregateNotPassTests += $rpTestResult.TestName + "(Warning);" 878 | } 879 | 880 | if (-Not [String]::IsNullOrWhiteSpace( $rpTestResult.Message)) 881 | { 882 | $aggregateMessage += $rpTestResult.TestName + "::" + $rpTestResult.Message.replace("`r``n",",") + "||" 883 | } 884 | 885 | foreach($detailKey in $rpTestResult.Details.Keys) 886 | { 887 | if (-Not [String]::IsNullOrWhiteSpace($rpTestResult.Details[$detailKey])) 888 | { 889 | $aggregateDetail += $rpTestResult.TestName + "::" + $detailKey + "->" + $rpTestResult.Details[ $detailKey].ToString().replace("`r`n",",") + "||" 890 | } 891 | 892 | #additional parsing for claim rule checks 893 | if ($detailKey -eq "ClaimRuleProperties") 894 | { 895 | $ClaimRuleProperties = $rpTestResult.Details[$detailKey] 896 | 897 | foreach($claimRuleProperty in $ClaimRuleProperties) 898 | { 899 | $RuleDetailReportRow = New-Object -TypeName PSObject -Property @{ 900 | "RP Name" = $ADFSRPTrust.Name 901 | "Rule" = $claimRuleProperty.Rule 902 | RuleSet = $claimRuleProperty.RuleSet 903 | IsKnownRuleMigratablePattern = $claimRuleProperty.IsKnownRuleMigratablePattern 904 | KnownRulePatternName = $claimRuleProperty.KnownRulePatternName 905 | } 906 | 907 | $RuleDetailReportRows += $RuleDetailReportRow 908 | 909 | 910 | #ImportFromCsv Application.ActiveWorkbook.Path & "\Attributes.csv", "AD Attributes", 1, 1 911 | #RP Name, RuleSet, ADAttribute 912 | foreach ($ADAttribute in $claimRuleProperty.ADAttributes) 913 | { 914 | $AttributeReportRow = New-Object -TypeName PSObject -Property @{ 915 | "RP Name" = $ADFSRPTrust.Name 916 | "Rule" = $claimRuleProperty.Rule 917 | RuleSet = $claimRuleProperty.RuleSet 918 | ADAttribute = $ADAttribute 919 | } 920 | $attributeReportRows += $AttributeReportRow 921 | } 922 | 923 | if ($claimRuleProperty.AttributeStoreName -ne "N/A") 924 | { 925 | $AttributeStoreReportRow = New-Object -TypeName PSObject -Property @{ 926 | "RP Name" = $ADFSRPTrust.Name 927 | "Rule" = $claimRuleProperty.Rule 928 | AttributeStoreName = $claimRuleProperty.AttributeStoreName 929 | } 930 | $attributeStoreReportRows += $AttributeStoreReportRow 931 | } 932 | 933 | if ($claimRuleProperty.RuleSet -eq "IssuanceTransform") 934 | { 935 | foreach ($ClaimType in $claimRuleProperty.IssuanceClaimTypes) 936 | { 937 | $claimTypesReportRow = New-Object -TypeName PSObject -Property @{ 938 | "RP Name" = $ADFSRPTrust.Name 939 | "Rule" = $claimRuleProperty.Rule 940 | "Claim Type" = $ClaimType 941 | } 942 | $claimTypesReportRows += $claimTypesReportRow 943 | } 944 | } 945 | } 946 | } 947 | } 948 | } 949 | 950 | $aggregateReportRow | Add-Member -MemberType NoteProperty -Name "Message" -Value $aggregateMessage 951 | $aggregateReportRow | Add-Member -MemberType NoteProperty -Name "Details" -Value $aggregateDetail 952 | $aggregateReportRow | Add-Member -MemberType NoteProperty -Name "NotPassedTests" -Value $aggregateNotPassTests 953 | 954 | 955 | New-Object -TypeName PSObject -Property @{ 956 | AggregateReportRow = $aggregateReportRow 957 | AttributeReportRows = $attributeReportRows 958 | AttributeStoreReportRows = $attributeStoreReportRows 959 | ClaimTypeReportRows = $claimTypesReportRows 960 | RuleDetailReportRows = $RuleDetailReportRows 961 | } 962 | } 963 | 964 | <# 965 | .Synopsis 966 | Analyzes a set of Relying Party trusts and produces CSV files with the results 967 | 968 | .Description 969 | The cmdlet expects either a root folder where the RP Trusts are serialized in XML format, 970 | or a CSV file that has all the RP Trust information. After executing, the following files 971 | are created in the directory from which the cmdlet ran: 972 | 973 | 1. ADFSRPConfiguration.csv: This file has one row per RP Trust. There are individual columns 974 | per each compatibility test (e.g. Test-ADFSRPAdditionalWSFedEndpoint) 975 | 2. Attributes.csv: This file contains the list of Active Directory Attributes found in the RP 976 | Trust rule sets. There is one row for each RP Trust/attribute found. 977 | 3. AttributeStores.csv: This file contains the list of Attribute Stores found in the RP Trust 978 | rule sets. There is one row for each RP Trust and Attribute store found 979 | 4. ClaimTypes.csv: This file contains the list of Claim Types found in the RP Trust rule sets. 980 | There is one row for each RP Trust and Claim Type found 981 | 982 | 983 | .Parameter RPXMLFileDirectory 984 | Path to a directory that contains XML files with RP Trust information. 985 | To export the CSVFiles in XML format, run the cmdlet Export-ADFS2AADOnPremConfiguration in the ADFS 986 | server; then, unzip the generated ZIP file and provide "apps" subfolder to the Test-ADFS2AADOnPremRPTrustSet 987 | cmdlet. 988 | 989 | .Parameter RPCSVFilePath 990 | 991 | .Example 992 | Run from a root folder that has XML serialized files 993 | Test-ADFS2AADOnPremRPTrustSet -RPXMLFileDirectory "C:\ADFSConfig\Apps" 994 | 995 | .Example 996 | Run from a CSV file from the ADFS Server 997 | Get-ADFSRelyingPartyTrust | ConvertTo-Csv -NoTypeInformation | Out-File "C:\ADFSConfig\OnPremRPs.csv" 998 | Test-ADFS2AADOnPremRPTrustSet -RPCSVFilePath "C:\ADFSConfig\OnPremRPs.csv" 999 | #> 1000 | 1001 | Function Test-ADFS2AADOnPremRPTrustSet 1002 | { 1003 | [CmdletBinding()] 1004 | param 1005 | ( 1006 | [Parameter(Mandatory=$true, ParameterSetName="RPXMLFileDirectory")] 1007 | [String] 1008 | $RPXMLFileDirectory, 1009 | 1010 | [Parameter(Mandatory=$true, ParameterSetName="RPJSONFilePath")] 1011 | [String] 1012 | $RPJSONFilePath 1013 | ) 1014 | 1015 | $trustSetTestOutput = @() 1016 | 1017 | if ( $PSCmdlet.ParameterSetName -eq "RPXMLFileDirectory" ) 1018 | { 1019 | $fileEntries = [IO.Directory]::GetFiles($RPXMLFileDirectory); 1020 | $totalRPs = $fileEntries.Count 1021 | $rpCount = 0 1022 | 1023 | 1024 | foreach($fileName in $fileEntries) 1025 | { 1026 | $rpCount++ 1027 | $percent = 100 * $rpCount / $totalRPs 1028 | 1029 | 1030 | $ADFSRPTrust = Import-clixml -LiteralPath $fileName 1031 | $RPTrustName = $ADFSRPTrust.Name 1032 | 1033 | Write-Progress -Activity "Analyzing Relying Parties" -Status "Processing $RPTrustName" -PercentComplete $percent -Id 1 1034 | $rpTestResults = Test-ADFS2AADOnPremRPTrust -ADFSRPTrust $ADFSRPTrust 1035 | 1036 | $trustSetTestOutput += $rpTestResults 1037 | } 1038 | } elseif ( $PSCmdlet.ParameterSetName -eq "RPJSONFilePath" ) 1039 | { 1040 | $JsonRPs = Get-Content -Path $RPJSONFilePath -Raw | ConvertFrom-Json 1041 | 1042 | $first = $true #REMOVE this 1043 | 1044 | $JSONRPRows = $JsonRPs.Rows #| Select-Object -First 100 1045 | 1046 | $totalRPs = $JSONRPRows.Count 1047 | $rpCount = 0 1048 | 1049 | $ComplexProperties = @( 1050 | "AccessControlPolicyParameters", #System.Object AccessControlPolicyParameters {get;} 1051 | "AdditionalWSFedEndpoint", #System.Collections.ObjectModel.ReadOnlyCollection[string] AdditionalWSFedEndpoint {get;} 1052 | "AllowedAuthenticationClassReferences",#string[] AllowedAuthenticationClassReferences {get;} 1053 | "ClaimsAccepted", #Microsoft.IdentityServer.Management.Resources.ClaimDescription[] ClaimsAccepted {get;} 1054 | "ClaimsProviderName", #string[] ClaimsProviderName {get;} 1055 | "EncryptionCertificate", #System.Security.Cryptography.X509Certificates.X509Certificate2 EncryptionCertificate {get;} 1056 | #"Identifier" #System.Collections.ObjectModel.ReadOnlyCollection[string] Identifier {get;} 1057 | "ProxyEndpointMappings" #System.Collections.Generic.Dictionary[string,string] ProxyEndpointMappings {get;set;} 1058 | "ProxyTrustedEndpoints" #System.Collections.ObjectModel.ReadOnlyCollection[string] ProxyTrustedEndpoints {get;} 1059 | "RequestSigningCertificate" #System.Collections.ObjectModel.ReadOnlyCollection[System.Security.Cryptography.X509Certificates.X509Certificate2] RequestSigningCertificate {get;} 1060 | "ResultantPolicy" #Microsoft.IdentityServer.PolicyModel.Configuration.PolicyTemplate.PolicyMetadata ResultantPolicy {get;} 1061 | "SamlEndpoints" #Microsoft.IdentityServer.Management.Resources.SamlEndpoint[] SamlEndpoints {get;} 1062 | 1063 | ); 1064 | 1065 | 1066 | $BooleanProperties = @( 1067 | "AlwaysRequireAuthentication", #bool AlwaysRequireAuthentication {get;set;} 1068 | "AutoUpdateEnabled", #bool AutoUpdateEnabled {get;set;} 1069 | "ConflictWithPublishedPolicy", #bool ConflictWithPublishedPolicy {get;} 1070 | "Enabled", #bool Enabled {get;} 1071 | "EnableJWT", #bool EnableJWT {get;} 1072 | "EncryptClaims", #bool EncryptClaims {get;} 1073 | "EncryptedNameIdRequired", #bool EncryptedNameIdRequired {get;} 1074 | #"LastPublishedPolicyCheckSuccessful", #System.Nullable[bool] LastPublishedPolicyCheckSuccessful {get;} ## 1075 | "MonitoringEnabled", #bool MonitoringEnabled {get;} 1076 | "PublishedThroughProxy", #bool PublishedThroughProxy {get;} 1077 | "RefreshTokenProtectionEnabled", #bool RefreshTokenProtectionEnabled {get;} : ##BUGBUG: AADCH seems to return this as an empty string. 1078 | "RequestMFAFromClaimsProviders", #bool RequestMFAFromClaimsProviders {get;} ##BUGBUG: AADCH seems to return this as an empty string. 1079 | "SignedSamlRequestsRequired" #bool SignedSamlRequestsRequired {get;} 1080 | 1081 | ); 1082 | 1083 | $IntProperties = @( 1084 | "NotBeforeSkew", #int NotBeforeSkew {get;} 1085 | "TokenLifetime" #int TokenLifetime {get;} 1086 | ); 1087 | 1088 | 1089 | foreach($JsonRPRow in $JSONRPRows) 1090 | { 1091 | $ADFSRPTrust = New-Object -TypeName PSObject 1092 | 1093 | $propertyCount = 0 1094 | foreach ($JsonRPColumn in $JsonRPs.Columns) 1095 | { 1096 | $PropertyName = $JsonRPColumn.ColumnName 1097 | $PropertyValue = $JsonRPRow[$propertyCount] 1098 | 1099 | if ($PropertyName -in $ComplexProperties) 1100 | { 1101 | try { 1102 | $PropertyValue = ConvertFrom-Json -InputObject $PropertyValue 1103 | } 1104 | catch 1105 | { 1106 | Write-Debug "Error trying to convert from JSON property $PropertyName with value $PropertyValue" 1107 | } 1108 | } 1109 | 1110 | if ($PropertyName -in $BooleanProperties) 1111 | { 1112 | try { 1113 | $PropertyValue = [System.Convert]::ToBoolean($PropertyValue) 1114 | } 1115 | catch 1116 | { 1117 | Write-Debug "Error trying to convert from boolean property $PropertyName with value $PropertyValue" 1118 | $PropertyValue = $false #If missing, we will default as false 1119 | } 1120 | } 1121 | 1122 | if ($PropertyName -in $IntProperties) 1123 | { 1124 | try { 1125 | $PropertyValue = [System.Convert]::ToInt32($PropertyValue) 1126 | } 1127 | catch 1128 | { 1129 | Write-Host "Error trying to convert from int property $PropertyName with value $PropertyValue" 1130 | } 1131 | } 1132 | 1133 | $ADFSRPTrust | Add-Member -MemberType NoteProperty -Name $PropertyName -Value $PropertyValue 1134 | $propertyCount++ 1135 | } 1136 | <# 1137 | if ($first) 1138 | { 1139 | Write-Output $ADFSRPTrust | Get-Member 1140 | Write-Output $ADFSRPTrust 1141 | Write-Output $ADFSRPTrust.AlwaysRequireAuthentication 1142 | if ($ADFSRPTrust.AlwaysRequireAuthentication) 1143 | { 1144 | "AlwaysRequireAuthentication: True" 1145 | } 1146 | else 1147 | { 1148 | "AlwaysRequireAuthentication: False" 1149 | } 1150 | Return 1151 | 1152 | $first = $false 1153 | }#> 1154 | 1155 | $rpCount++ 1156 | $percent = 100 * $rpCount / $totalRPs 1157 | 1158 | 1159 | 1160 | $RPTrustName = $ADFSRPTrust.Name 1161 | Write-Progress -Activity "Analyzing Relying Parties" -Status "Processing app $RPTrustName" -PercentComplete $percent -Id 1 1162 | $rpTestResults = Test-ADFS2AADOnPremRPTrust -ADFSRPTrust $ADFSRPTrust 1163 | $trustSetTestOutput += $rpTestResults 1164 | } 1165 | } 1166 | else 1167 | { 1168 | throw "Invalid input" 1169 | } 1170 | 1171 | #Serialize the reports in different files 1172 | #TODO: Dedup?? 1173 | $trustSetTestOutput | Select-Object -ExpandProperty "AggregateReportRow" | ConvertTo-Csv -NoTypeInformation | Out-File ".\ADFSRPConfiguration.csv" 1174 | $trustSetTestOutput | Select-Object -ExpandProperty "AttributeReportRows" | Select-Object -Property "RP Name","RuleSet","Rule", "ADAttribute" -Unique | ConvertTo-Csv -NoTypeInformation | Out-File ".\Attributes.csv" 1175 | $trustSetTestOutput | Select-Object -ExpandProperty "AttributeStoreReportRows" | Select-Object -Property "RP Name","Rule", "AttributeStoreName" -Unique | ConvertTo-Csv -NoTypeInformation | Out-File ".\AttributeStores.csv" 1176 | $trustSetTestOutput | Select-Object -ExpandProperty "ClaimTypeReportRows" | Select-Object -Property "RP Name","Rule", "Claim Type" -Unique | ConvertTo-Csv -NoTypeInformation | Out-File ".\ClaimTypes.csv" 1177 | $trustSetTestOutput | Select-Object -ExpandProperty "RuleDetailReportRows" | Select-Object -Property "RP Name","RuleSet","Rule", "IsKnownRuleMigratablePattern", "KnownRulePatternName" -Unique | ConvertTo-Csv -NoTypeInformation | Out-File ".\RuleDetails.csv" 1178 | 1179 | } 1180 | 1181 | <# 1182 | .Synopsis 1183 | Exports the configuration of Relying Party Trusts and Claims Provider Trusts 1184 | 1185 | .Description 1186 | Creates and zips a set of files that hold the configuration of AD FS claim providers and relying parties. 1187 | The output files are created under a directory called "ADFS" in the system drive. 1188 | 1189 | 1190 | .Example 1191 | Export-ADFS2AADOnPremConfiguration 1192 | #> 1193 | Function Export-ADFS2AADOnPremConfiguration 1194 | { 1195 | $filePathBase = "$env:systemdrive\ADFS\apps\" 1196 | $zipfileBase = "$env:systemdrive\ADFS\zip\" 1197 | $zipfileName = $zipfileBase + "ADFSApps.zip" 1198 | mkdir $filePathBase -ErrorAction SilentlyContinue 1199 | mkdir $zipfileBase -ErrorAction SilentlyContinue 1200 | 1201 | $AdfsRelyingPartyTrusts = Get-AdfsRelyingPartyTrust 1202 | foreach ($AdfsRelyingPartyTrust in $AdfsRelyingPartyTrusts) 1203 | { 1204 | $RPfileName = $AdfsRelyingPartyTrust.Name.ToString() 1205 | $CleanedRPFileName = Remove-InvalidFileNameChars -Name $RPfileName 1206 | $RPName = "RPT - " + $CleanedRPFileName 1207 | $filePath = $filePathBase + $RPName + '.xml' 1208 | $AdfsRelyingPartyTrust | Export-Clixml -LiteralPath $filePath -ErrorAction SilentlyContinue 1209 | } 1210 | 1211 | $AdfsClaimsProviderTrusts = Get-AdfsClaimsProviderTrust 1212 | foreach ($AdfsClaimsProviderTrust in $AdfsClaimsProviderTrusts) 1213 | { 1214 | 1215 | $CPfileName = $AdfsClaimsProviderTrust.Name.ToString() 1216 | $CleanedCPFileName = Remove-InvalidFileNameChars -Name $CPfileName 1217 | $CPTName = "CPT - " + $CleanedCPFileName 1218 | $filePath = $filePathBase + $CPTName + '.xml' 1219 | $AdfsClaimsProviderTrust | Export-Clixml -LiteralPath $filePath -ErrorAction SilentlyContinue 1220 | 1221 | } 1222 | 1223 | If (Test-Path $zipfileName) 1224 | { 1225 | Remove-Item $zipfileName 1226 | } 1227 | 1228 | Add-Type -assembly "system.io.compression.filesystem" 1229 | [io.compression.zipfile]::CreateFromDirectory($filePathBase, $zipfileName) 1230 | 1231 | invoke-item $zipfileBase 1232 | } 1233 | 1234 | Export-ModuleMember Export-ADFS2AADOnPremConfiguration 1235 | Export-ModuleMember Test-ADFS2AADOnPremRPTrust 1236 | Export-ModuleMember Test-ADFS2AADOnPremRPTrustSet 1237 | 1238 | -------------------------------------------------------------------------------- /ADFS to AzureAD App Migration/Media/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/ADFS to AzureAD App Migration/Media/image1.png -------------------------------------------------------------------------------- /ADFS to AzureAD App Migration/Media/image10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/ADFS to AzureAD App Migration/Media/image10.png -------------------------------------------------------------------------------- /ADFS to AzureAD App Migration/Media/image11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/ADFS to AzureAD App Migration/Media/image11.png -------------------------------------------------------------------------------- /ADFS to AzureAD App Migration/Media/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/ADFS to AzureAD App Migration/Media/image2.png -------------------------------------------------------------------------------- /ADFS to AzureAD App Migration/Media/image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/ADFS to AzureAD App Migration/Media/image3.png -------------------------------------------------------------------------------- /ADFS to AzureAD App Migration/Media/image4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/ADFS to AzureAD App Migration/Media/image4.png -------------------------------------------------------------------------------- /ADFS to AzureAD App Migration/Media/image5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/ADFS to AzureAD App Migration/Media/image5.png -------------------------------------------------------------------------------- /ADFS to AzureAD App Migration/Media/image6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/ADFS to AzureAD App Migration/Media/image6.png -------------------------------------------------------------------------------- /ADFS to AzureAD App Migration/Media/image7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/ADFS to AzureAD App Migration/Media/image7.png -------------------------------------------------------------------------------- /ADFS to AzureAD App Migration/Media/image8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/ADFS to AzureAD App Migration/Media/image8.png -------------------------------------------------------------------------------- /ADFS to AzureAD App Migration/Media/image9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/ADFS to AzureAD App Migration/Media/image9.png -------------------------------------------------------------------------------- /ADFS to AzureAD App Migration/Readme.md: -------------------------------------------------------------------------------- 1 | ADFS to Azure AD App Migration Tool Instructions 2 | ==================================== 3 | 4 | **If you want this same guidance below in Word Document form click to the [ADFS to AAD Migration Tool Help File](https://github.com/Identity-Deployment-Guides/Identity-Deployment-Guides/blob/master/ADFS%20to%20AzureAD%20App%20Migration/ADFS%20to%20AAD%20Migration%20Tool%20Help%20File.docx) in the repo above and click the "download" button.** 5 | 6 | The ADFS to AAD App Migration tool consists of three steps: 7 | 8 | ### **Collect** 9 | First, we collect the relying party applications from your ADFS server. 10 | This is done via a PowerShell module that must run on one of 11 | your ADFS server and it writes the configuration of each application to 12 | the file system as individual .XML files 13 | 14 | ### **Analyze** 15 | Next, our PowerShell module will enumerate through the individual .XML 16 | files and check the configuration of various settings. This analysis can 17 | be done directly on your primary ADFS server or on a different ADFS 18 | server. However, it is necessary for ADFS to be installed to process the configuration. 19 | 20 | ### **Report** 21 | Finally, we produce an Excel report of your relying party applications that indicates which ones are eligible for migration to Azure AD and which ones are not, along with an explanation of why they cannot be migrated. To generate this report, Excel must be installed on the workstation or server being used. 22 | 23 | # **Collect & Analyze** 24 | 25 | ### **Instructions if you want to collect and analyze directly from your ADFS server:** 26 | 27 | 1. Download PowerShell module from [http://aka.ms/migrateapps/adfsscript](http://aka.ms/migrateapps/adfsscript) 28 | 2. Copy PowerShell module to one of your ADFS servers that you want to run analysis. If you need to save file, be sure to save as .psm1 29 | 3. From this same ADFS server, open PowerShell as "Administrator" 30 | 4. Change the directory to where you placed this PowerShell module 31 | 5. From that PowerShell window, run the following: 32 | - `ipmo .\\ADFSAADMigrationUtils.psm1` 33 | - `Export-ADFS2AADOnPremConfiguration` 34 | - `Test-ADFS2AADOnPremRPTrustSet -RPXMLFileDirectory "C:\adfs\apps"` 35 | 6. Collect the following files from the ADFS server. These files are located in the same folder you used in Step 4. 36 | - ADFSRPConfiguration.csv 37 | - Attributes.csv 38 | - AttributeStores.csv 39 | - ClaimTypes.csv 40 | - RuleDetails.csv 41 | 7. On a workstation that has Excel installed, create a folder at c:\adfs and place the .csv files collected from the previous step in this folder 42 | 8. From this same workstation, open "ADFS to AAD App Migration Report Template.xlsm" in Excel and navigate to the Dashboard tab and hit the Refresh Data button on the right. 43 | 44 | **Note:** If you want to re-export and re-analyze the data, just repeat Steps 5-7 and overwrite files in Step 6 with new files 45 | 46 | ### **Instructions If You Want to run the Analysis from Another Server** 47 | 48 | #### **ADFS Server** 49 | 50 | 1. On your ADFS server, download PowerShell module from [http://aka.ms/migrateapps/adfsscript](http://aka.ms/migrateapps/adfsscript). If you need to save file, be sure to save as .psm1. 51 | 2. From this same ADFS server, open PowerShell as "Administrator". 52 | 4. Change the directory to where you placed this PowerShell module. 53 | 5. From that PowerShell window, run the following: 54 | - `ipmo .\\ADFSAADMigrationUtils.psm1` 55 | - `Export-ADFS2AADOnPremConfiguration` 56 | 57 | #### **Run Analysis From Another Server ** 58 | 59 | 1. Copy c:\ADFS\ADFSApps.zip from your ADFS server to another ADFSserver where you want to run analysis 60 | 2. On this other ADFS server, unzip the .XML files to a folder of your choosing 61 | 3. On this other ADFS server, download PowerShell module from [http://aka.ms/migrateapps/adfsscript](http://aka.ms/migrateapps/adfsscript). If you need to save file, be sure to save as .psm1 . 62 | 4. From this other server, open PowerShell as "Administrator". 63 | 5. Change the directory to where you placed this PowerShell module. 64 | 6. From that PowerShell window, run the following: 65 | - `ipmo .\\ADFSAADMigrationUtils.psm1` 66 | - `Test-ADFS2AADOnPremRPTrustSet -RPXMLFileDirectory ""` 67 | 7. Collect the following files from this ADFS server. They will be in the same folder that you changed directories to in Step 5. 68 | - ADFSRPConfiguration.csv 69 | - Attributes.csv 70 | - AttributeStores.csv 71 | - ClaimTypes.csv 72 | - RuleDetails.csv 73 | 8. On a workstation that has Excel installed, create a folder at c:\adfs and place the .csv files collected from the previous step in this folder. 74 | 9. From this same workstation, open "ADFS to AAD App Migration Report Template.xlsm" in Excel and navigate to the Dashboard tab and hit the Refresh Data button on the right. 75 | 76 | **Note:** If you want to re-export and re-analyze the data, just repeat Steps 7-10 and overwrite files in Step 6 with new files. 77 | 78 | # **Report - Instructions for Using the Excel Spreadsheet** 79 | 80 | ### **Refreshing Your Data** 81 | 82 | Anytime you want to refresh your data, just make sure that the latest .csv files are located within c:\\ADFS and from the Dashboard tab, just hit the ‘Refresh Data’ button: 83 | 84 | ![](Media/image1.png) 85 | 86 | ### **Viewing Your All-Up Results** 87 | 88 | The first tab you’ll want to review is the ‘Dashboard’ tab to see an all-up view of how many applications you have and whether they can migrate to Azure AD or not: 89 | 90 | ![](Media/image2.png) 91 | 92 | ### **Viewing Individual Application Results** 93 | 94 | Next, you’ll want to look at the individual status on each application on the ‘AAD App Migration Report’ tab. This will tell you whether the application will readily migrate to Azure AD or whether there are settingd on the application that are currently incompatible with Azure AD or need to be reviewed further. 95 | 96 | ![](Media/image3.png) 97 | 98 | As you can see here, both 7FAM applications passed and can be readily migrate to Azure AD but the remaining three applications have some items on them that could prevent them from being moved to Azure AD. 99 | 100 | **Note**: See bottom of document for description of each result 101 | 102 | Additionally, from this same tab, we include the following items per application so you can gain some further insight about what configuration changes may be required to move your application to Azure AD: 103 | 104 | - **Claim Rules to Review:** We flag any “custom” claim rules that may 105 | not be compatible with Azure AD and provide the total number of 106 | rules per application, so you can prioritize accordingly. 107 | 108 | - **Attributes Not Synced to AAD by Default:** If any of the claim 109 | rules contains AD attributes that aren’t synced to Azure AD by 110 | default, we include this total count here. 111 | 112 | - **Authorization Rules Present:** If your application has any 113 | authorization rules present, all this means is you’ll need to move 114 | these over to Azure AD Conditional Access. 115 | 116 | - **Restricted Claim Types:** Azure AD doesn’t allow certain claim 117 | type URI’s to be modified so we include whether any of the claim 118 | rules contains any of these restricted claim type URI’s. This is 119 | more informational than anything. 120 | 121 | - **Custom Attribute Stores:** Azure AD now supports custom authentication 122 | extensions that allows customers to use other attribute stores beyond Azure 123 | AD attributes, for more information on how to configure these attributes and the current requirements, see [custom extension overview](https://learn.microsoft.com/azure/active-directory/develop/custom-extension-overview). 124 | 125 | ![](Media/image4.png) 126 | 127 | ### **Viewing All your Claim Rules** 128 | 129 | If you want more detail on all your claim rules across all your applications, navigate to the ‘Claim Rules Details’ tab. This provides you with the following information: 130 | 131 | - **RP Name:** The name of the application 132 | 133 | - **Ruleset:** Whether claim rule is for issuance, authorization, 134 | delegation, or impersonation 135 | 136 | - **Rule:** The entirety of the individual claim rule 137 | 138 | - **IsRuleKnownMigrateablePattern:** Whether the rule can be readily 139 | migrated to Azure AD or not. Many rules may migrate with minor 140 | modifications. 141 | 142 | - **KnownRulePatternName:** We run each rule through a series of 143 | checks to see whether it’s compatible with Azure AD. This is just 144 | the name of the rule that the claim rule matched. Only present if 145 | the rule passed. 146 | 147 | ![](Media/image5.png) 148 | 149 | ### **Viewing All your AD Attributes within your Claim Rules** 150 | 151 | If you want more detail on all the AD attributes in use across all your applications, navigate to the ‘AD Attributes’ tab. This provides you with the following information: 152 | 153 | - **RP Name:** The name of the application 154 | 155 | - **Ruleset:** Whether claim rule is for issuance, authorization, 156 | delegation, or impersonation 157 | 158 | - **Rule:** The entirety of the individual claim rule 159 | 160 | - **ADAttribute:** Actual AD attribute in use within the rule 161 | 162 | - **Synced to Azure AD by Default:** By default, Azure AD Connect only 163 | syncs a finite list of attributes although it can be customized to 164 | sync more. You’ll want to ensure that any application you migrate to 165 | Azure AD has all the necessary AD attributes also being synced to 166 | Azure AD via Azure AD Connect. 167 | 168 | - **Note:** We highlight (in red) where the AD attribute is in use 169 | within the claim rule. 170 | 171 | ![](Media/image6.png) 172 | 173 | ### **Viewing All your Claim Type URI’s within your Claim Rules** 174 | 175 | If you want more detail on all the individual claim types URI’s in use across all your applications, navigate to the ‘Claim Types’ tab. When moving an application to Azure AD, it easier to just register your individual claim types within Azure AD rather than asking your software vendor to change their configuration. This provides you with the following information: 176 | 177 | - **RP Name:** The name of the application 178 | 179 | - **Rule:** The entirety of the individual claim rule 180 | 181 | - **Claim Type:** The claim type URI in use within that claim rule 182 | 183 | - **Restricted Claim Type:** Azure AD doesn’t allow certain claim type 184 | URI’s to be modified so we include whether this specific claim type 185 | is restricted within Azure AD. This is more informational than 186 | anything. 187 | 188 | - **Note:** We highlight (in red) where the Claim Type URI is in use 189 | within the claim rule. 190 | 191 | ![](Media/image7.png) 192 | 193 | ### **Viewing All your Attribute Stores within your Claim Rules** 194 | 195 | If you want more detail on all the Attribute Stores in use across all your applications, navigate to the ‘Attribute Stores’ tab. Azure AD doesn’t currently support any custom attribute stores. This provides you with the following information: 196 | 197 | - **RP Name:** The name of the application 198 | 199 | - **Rule:** The entirety of the individual claim rule 200 | 201 | - **AttributeStoreName:** The name of the Attribute Store in use. We 202 | highlight any that aren’t ‘Active Directory’ 203 | 204 | - **Note:** We highlight (in red) where the Attribute Store is in use 205 | within the claim rule. Also, the \_ProxyCredentialStore is an ADFS 206 | 2.0 concept that isn’t truly a blocker for moving an application to 207 | Azure AD. 208 | 209 | ![](Media/image8.png) 210 | 211 | ### **Modeling Change to your applications** 212 | 213 | We wanted to provide a way for customers to see the migration impact of changes they are willing to make to their applications without them actually having to make any changes. So back on the ‘Dashboard’ tab, you can type **Yes** next to the issues you’re willing to resolve and see how that impacts your migration %. Additionally, the issues impacting the most applications will highlight themselves in a light green like so: 214 | 215 | ![](Media/image9.png) 216 | 217 | Next, mark the items you are committing to resolve or features that may be coming as part of the Azure AD roadmap: 218 | 219 | ![](Media/image10.png) 220 | 221 | Upon doing so, the top of the ‘Dashboard’ tab will update to reflect your new migration % like so: 222 | 223 | ![](Media/image11.png) 224 | 225 | # **Viewing All Tests we Ran Your Applications Through** 226 | 227 | If you’re interested to see all the tests we ran your applications through with a status of Pass, Warning, or Fail, navigate to the ‘All Apps Details’ tab. Here is more information on each of the columns: 228 | 229 | | Property | Status | Description | 230 | |------------------------------------------------- |------------------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 231 | | RP Name | N/A | Friendly name of application | 232 | | Result | Pass/Warning/Fail | All-up result for specific application. If application has any fails, result will default to fail. If application only has warning, result will default to warning. | 233 | | Test-ADFSRPAdditionalAuthenticationRules | Pass/Warning | Testing for any on-premises MFA providers. Will need to be moved to Azure MFA or Custom Controls integration with 3rd party MFA provider. | 234 | | Test-ADFSRPAdditionalWSFedEndpoint | Pass/Fail | Testing for multiple WS-Fed assertion endpoints since Azure AD only supports (1) one of these today. | 235 | | Test-ADFSRPAllowedAuthenticationClassReferences | Pass/Fail | Whether the application is configured to only allow certain authentication types | 236 | | Test-ADFSRPAlwaysRequireAuthentication | Pass/Fail | Whether the application is configured to ignore SSO cookies and ‘Always Prompt for Authentication’. Not supported by Azure AD today. | 237 | | Test-ADFSRPAutoUpdateEnabled | Pass/Warning | Whether ADFS is configured to auto update the application based on changes within the federation metadata | 238 | | Test-ADFSRPClaimsProviderName | Pass/Fail | Whether the application is hardcoded to another claim provider. Not supported by Azure AD today. | 239 | | Test-ADFSRPDelegationAuthorizationRules | Pass/Fail | Whether the application has any custom delegation authorization rules defined. Azure AD does not support this today. | 240 | | Test-ADFSRPEncryptClaims | Pass/Fail | Whether the application is configured for SAML Token Encryption. Azure AD does not support this today. | 241 | | Test-ADFSRPImpersonationAuthorizationRules | Pass/Warning | Whether the application has any custom impersonation authorization rules defined. Azure AD does not support this today. | 242 | | Test-ADFSRPIssuanceAuthorizationRules | Pass/Warning | Whether the application has any custom issuance authorization rules defined. Move to AAD CA | 243 | | Test-ADFSRPIssuanceTransformRules | Pass/Warning | Whether the application has any custom issuance transform rules defined | 244 | | Test-ADFSRPMonitoringEnabled | Pass/Warning | Whether ADFS is configured to monitor a federation metadata for this application. | 245 | | Test-ADFSRPNotBeforeSkew | Pass/Warning | Whether ADFS allows a time skew based on the NotBefore and NotOnOrAfter times within SAML token. | 246 | | Test-ADFSRPRequestMFAFromClaimsProviders | Pass/Warning | Whether the application is hardcoded to another claim provider and requires MFA. Not supported by Azure AD today. | 247 | | Test-ADFSRPSignedSamlRequestsRequired | Pass/Fail | Whether the application is configured for SAML Request Signing. Azure AD does not support this today. | 248 | | Test-ADFSRPTokenLifetime | Pass/Fail | Whether the application is configured for a custom token lifetime. ADFS default is 1 hour. | 249 | 250 | Getting support from Microsoft 251 | ============================== 252 | 253 | There are several different avenues from which you can get support during your AD FS – Azure AD migration: 254 | 255 | **Azure Support:** Depending on your Enterprise Agreement with Microsoft, you can call Microsoft Support and open a ticket for any 256 | issue related to your Azure Identity deployment. For more information on how to get in touch with Microsoft Support, please visit our Azure support portal: 257 | 258 | **FastTrack**: If you have purchased Enterprise Mobility and Security (EMS) licenses or Azure AD Premium licenses, you may be eligible to receive deployment assistance from the FastTrack program. For more information on how to engage with FastTrack, please refer to our documentation on the [FastTrack Center Eligibility Benefit for 259 | Enterprise Mobility and 260 | Security](https://docs.microsoft.com/en-us/enterprise-mobility-security/solutions/enterprise-mobility-fasttrack-program) 261 | 262 | **Engage the Product Engineering Team:** If you are working on a major customer deployment with millions of users, you can work with your Microsoft account team or your Cloud Solutions Architect to decide if the project’s deployment complexity warrants working directly with the Azure Identity Product Engineering team. 263 | 264 | **Azure Active Directory Public Forums:** Azure AD also has several closely monitored channels available to the public. Here are some useful links: 265 | 266 | - StackOverflow using the tags 267 | [‘adfs’](https://stackoverflow.com/questions/tagged/adfs) 268 | 269 | - [UserVoice](https://feedback.azure.com/forums/169401-azure-active-directory) 270 | to submit or vote on new feature requests in Azure AD 271 | 272 | - Microsoft Azure on Reddit: https://www.reddit.com/r/AZURE/ 273 | 274 | - [MSDN Forum for Azure 275 | AD](https://social.msdn.microsoft.com/Forums/en-US/home?forum=WindowsAzureAD) 276 | 277 | -------------------------------------------------------------------------------- /ADFS to AzureAD App Migration/samples/ADFS2AADUtils.psm1: -------------------------------------------------------------------------------- 1 | #Requires -Version 4 2 | #Requires -Module @{ ModuleName = 'MSAL.PS'; ModuleVersion = '4.7.1.2' } 3 | 4 | 5 | <# 6 | 7 | .SYNOPSIS 8 | ADFS2AADUtils.psm1 is a Windows PowerShell module to help migrating AD FS configuration to Azure AD 9 | 10 | .DESCRIPTION 11 | 12 | Version: 0.0.1 13 | 14 | ADFS2AADUtils.psm1 is a Windows PowerShell module to help migrating AD FS configuration to Azure AD. 15 | 16 | This module uses MSAL.PS. Check https://www.powershellgallery.com/packages/MSAL.PS/ for instructions 17 | 18 | 19 | .DISCLAIMER 20 | THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF 21 | ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO 22 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A 23 | PARTICULAR PURPOSE. 24 | 25 | Copyright (c) Microsoft Corporation. All rights reserved. 26 | #> 27 | 28 | 29 | $global:authHeader = $null 30 | $global:msgraphToken = $null 31 | $global:tokenRequestedTime = [DateTime]::MinValue 32 | 33 | function Get-MSCloudIdAccessToken { 34 | [CmdletBinding()] 35 | param ( 36 | [string] 37 | $TenantId, 38 | [string] 39 | $ClientID, 40 | [string] 41 | $RedirectUri, 42 | [string] 43 | $Scopes, 44 | [switch] 45 | $Interactive 46 | ) 47 | 48 | $msalToken = $null 49 | if ($Interactive) 50 | { 51 | $msalToken = get-msaltoken -ClientId $ClientID -TenantId $TenantId -RedirectUri $RedirectUri -Scopes $Scopes -Resource 52 | } 53 | else 54 | { 55 | try { 56 | $msalToken = get-msaltoken -ClientId $ClientID -TenantId $TenantId -RedirectUri $RedirectUri -Scopes $Scopes -Silent 57 | } 58 | catch [Microsoft.Identity.Client.MsalUiRequiredException] 59 | { 60 | $MsalToken = get-msaltoken -ClientId $ClientID -TenantId $TenantId -RedirectUri $RedirectUri -Scopes $Scopes 61 | } 62 | } 63 | 64 | Write-Output $MsalToken 65 | } 66 | 67 | <# 68 | .Synopsis 69 | Creates a new Azure AD Application from and AD FS Relying party trust and the Application Gallery 70 | as documented in aka.ms/aadgallery-sso-api 71 | 72 | 73 | .Description 74 | This function queries the Azure AD Gallery App using MS Graph 75 | .Parameter TenantId 76 | Tenant ID we want to connect 77 | .Parameter ClientID 78 | Client ID of the Client used to connect 79 | .Parameter RedirectUri 80 | Redirect URI of the Client used to connect 81 | .Parameter Scopes 82 | Scopes requested in the connection 83 | 84 | .Example 85 | Connect to MS Graph with defaults 86 | Connect-MSGraphAPI 87 | #> 88 | function Connect-MSGraphAPI { 89 | [CmdletBinding()] 90 | param ( 91 | [string] 92 | $TenantId, 93 | [string] 94 | $ClientID = "1b730954-1685-4b74-9bfd-dac224a7b894", 95 | [string] 96 | $RedirectUri = "urn:ietf:wg:oauth:2.0:oob", 97 | [string] 98 | $Scopes = "https://graph.microsoft.com/.default", 99 | [switch] 100 | $Interactive 101 | ) 102 | 103 | $token = Get-MSCloudIdAccessToken -TenantId $TenantId -ClientID $ClientID -RedirectUri $RedirectUri -Scopes $Scopes -Interactive:$Interactive 104 | $Header = @{ } 105 | $Header.Authorization = "Bearer {0}" -f $token.AccessToken 106 | $Header.'Content-type' = "application/json" 107 | 108 | $global:msgraphToken = $token 109 | $global:authHeader = $Header 110 | } 111 | 112 | function Invoke-MSGraphQuery { 113 | [CmdletBinding()] 114 | param ( 115 | # Base URI 116 | [string] 117 | $BaseURI = "https://graph.microsoft.com/", 118 | # endpoint 119 | [string] 120 | $endpoint, 121 | [ValidateSet("v1.0", "beta")] 122 | [string] 123 | $APIVersion = "v1.0", 124 | [string] 125 | $QueryParameters, 126 | # HTTP Method 127 | [Parameter(Mandatory = $true)] 128 | [ValidateSet("GET", "POST", "PUT", "DELETE", "PATCH")] 129 | [string] 130 | $Method, 131 | [string] 132 | $Body 133 | 134 | ) 135 | 136 | begin { 137 | # Header 138 | $CurrentDate = [DateTime](Get-Date) 139 | $Delta= ($CurrentDate - $global:tokenRequestedTime).TotalMinutes 140 | 141 | if ($Delta -gt 55) 142 | { 143 | Connect-MSGraphAPI 144 | $global:tokenRequestedTime = $CurrentDate 145 | } 146 | $Headers = $global:authHeader 147 | } 148 | 149 | process { 150 | 151 | if ($null -notlike $QueryParameters) { 152 | $URI = ("{0}{1}/{2}?{3}" -f $BaseURI, $APIVersion, $endpoint, $QueryParameters) 153 | 154 | } 155 | else { 156 | $URI = ("{0}{1}/{2}" -f $BaseURI, $APIVersion, $endpoint) 157 | } 158 | 159 | switch ($Method) { 160 | "GET" { 161 | 162 | $queryUrl = $URI 163 | Write-Verbose ("Invoking $Method request on $queryUrl...") 164 | while (-not [String]::IsNullOrEmpty($queryUrl)) { 165 | 166 | try { 167 | $pagedResults = Invoke-RestMethod -Method $Method -Uri $queryUrl -Headers $Headers -ErrorAction Stop 168 | 169 | } 170 | catch { 171 | 172 | $StatusCode = [int]$_.Exception.Response.StatusCode 173 | $message = $_.Exception.Message 174 | throw "ERROR During Request - $StatusCode $message" 175 | 176 | } 177 | 178 | 179 | if ($pagedResults.value -ne $null) { 180 | $queryResults += $pagedResults.value 181 | } 182 | else { 183 | $queryResults += $pagedResults 184 | } 185 | $queryCount = $queryResults.Count 186 | Write-Progress -Id 1 -Activity "Querying directory" -CurrentOperation "Retrieving results ($queryCount found so far)" 187 | $queryUrl = "" 188 | 189 | $odataNextLink = $pagedResults | Select-Object -ExpandProperty "@odata.nextLink" -ErrorAction SilentlyContinue 190 | 191 | if ($null -ne $odataNextLink) { 192 | $queryUrl = $odataNextLink 193 | } 194 | else { 195 | $odataNextLink = $pagedResults | Select-Object -ExpandProperty "odata.nextLink" -ErrorAction SilentlyContinue 196 | if ($null -ne $odataNextLink) { 197 | $absoluteUri = [Uri]"https://bogus/$odataNextLink" 198 | $skipToken = $absoluteUri.Query.TrimStart("?") 199 | 200 | } 201 | } 202 | } 203 | 204 | Write-Verbose ("Returning {0} total results" -f $queryResults.count) 205 | Write-Output $queryResults 206 | 207 | } 208 | 209 | "POST" { 210 | $queryUrl = $URI 211 | Write-Verbose ("Invoking $Method request on $queryUrl using $Headers with Body $body...") 212 | 213 | $qErr = $Null 214 | try { 215 | $queryResults = Invoke-RestMethod -Method $Method -Uri $queryUrl -Headers $Headers -Body $Body -UseBasicParsing -ErrorVariable qErr -ErrorAction Stop 216 | Write-Output $queryResults 217 | } 218 | catch { 219 | $StatusCode = [int]$_.Exception.Response.StatusCode 220 | $message = $_.Exception.Message 221 | throw "ERROR During Request - $StatusCode $message" 222 | 223 | 224 | } 225 | } 226 | 227 | "PATCH" { 228 | $queryUrl = $URI 229 | Write-Verbose ("Invoking $Method request on $queryUrl using $Headers with Body $body...") 230 | 231 | $qErr = $Null 232 | try { 233 | $queryResults = Invoke-RestMethod -Method $Method -Uri $queryUrl -Headers $Headers -Body $Body -UseBasicParsing -ErrorVariable qErr -ErrorAction Stop 234 | Write-Output $queryResults 235 | } 236 | catch { 237 | $StatusCode = [int]$_.Exception.Response.StatusCode 238 | $message = $_.Exception.Message 239 | throw "ERROR During Request - $StatusCode $message" 240 | 241 | 242 | } 243 | } 244 | 245 | 246 | "PUT" { 247 | $queryUrl = $URI 248 | Write-Verbose ("Invoking $Method request on $queryUrl...") 249 | $pagedResults = Invoke-RestMethod -Method $Method -Uri $queryUrl -Headers $Headers -Body $Body 250 | } 251 | "DELETE" { 252 | $queryUrl = $URI 253 | Write-Verbose ("Invoking $Method request on $queryUrl...") 254 | $pagedResults = Invoke-RestMethod -Method $Method -Uri $queryUrl -Headers $Headers 255 | } 256 | } 257 | } 258 | 259 | end { 260 | 261 | } 262 | } 263 | 264 | <# 265 | .Synopsis 266 | Start a session to AzureAD and MS Graph Client Library 267 | 268 | .Description 269 | This function prompts for authentication using MSAL.PS and reuses the same token to connect 270 | to Azure AD Powershell 271 | 272 | #> 273 | function Start-ADFS2AADSession 274 | { 275 | #Connect to MS Graph using MSAL.PS 276 | Connect-MSGraphAPI 277 | $msGraphToken = $global:msgraphToken 278 | 279 | #Get an Azure AD Graph silently 280 | $aadTokenPsh = Get-MSCloudIdAccessToken -ClientID 1b730954-1685-4b74-9bfd-dac224a7b894 -Scopes "https://graph.windows.net/.default" -RedirectUri "urn:ietf:wg:oauth:2.0:oob" 281 | 282 | #Connect to AzureAD Powershell Module with MS Graph and Azure AD Graph tokens 283 | Connect-AzureAD -AadAccessToken $aadTokenPsh.AccessToken -MsAccessToken $msGraphToken.AccessToken -AccountId $msGraphToken.Account.UserName -TenantId $msGraphToken.TenantID | Out-Null 284 | 285 | $global:tokenRequestedTime = [DateTime](Get-Date) 286 | 287 | Write-Output "Session Started!" 288 | } 289 | 290 | ################################################################################# 291 | # Wrappers for app management as documented in https://aka.ms/aadgallery-sso-api 292 | ################################################################################# 293 | 294 | <# 295 | .Synopsis 296 | Creates a new Azure AD Application from and AD FS Relying party trust and the Application Gallery 297 | as documented in aka.ms/aadgallery-sso-api 298 | 299 | 300 | .Description 301 | This function queries the Azure AD Gallery App using MS Graph 302 | .Parameter DisplayNameFilter 303 | Filter for the search in the gallery . This is case sensitive and will be used as a "startsWith" filter 304 | 305 | .Example 306 | Get-AzureADApplicationTemplate -DisplayNameFilter ContosoERP 307 | #> 308 | function Get-AzureADApplicationTemplate { 309 | [CmdletBinding()] 310 | param ( 311 | $DisplayNameFilter 312 | ) 313 | 314 | $endpoint = "applicationTemplates" 315 | if (-not [String]::IsNullOrWhiteSpace($DisplayNameFilter)) 316 | { 317 | $endpoint += "/?`$filter=startswith(displayName,'$DisplayNameFilter')" 318 | } 319 | Invoke-MSGraphQuery -endpoint $endpoint -Method GET -APIVersion "beta" 320 | } 321 | 322 | function New-AzureADApplicationTemplateInstance 323 | { 324 | [CmdletBinding()] 325 | param ( 326 | [Parameter(Mandatory = $true)] 327 | $AppTemplateId, 328 | [Parameter(Mandatory = $true)] 329 | $DisplayName 330 | ) 331 | 332 | $endpoint = "applicationTemplates/$AppTemplateId/instantiate" 333 | $body = "" 334 | if (-not [String]::IsNullOrWhiteSpace($DisplayName)) 335 | { 336 | $body += @{ 337 | "displayName" = $DisplayName 338 | } | ConvertTo-Json 339 | } 340 | Invoke-MSGraphQuery -endpoint $endpoint -Method "POST" -Body $body -APIVersion "beta" 341 | } 342 | 343 | 344 | <# 345 | ------------------------------------------------------------- 346 | AD FS Specific functionality 347 | ------------------------------------------------------------- 348 | #> 349 | 350 | 351 | ########################################### 352 | # RP Trust Claim Rule checks 353 | ########################################### 354 | 355 | Add-Type -Language CSharp @" 356 | public class MigrationTestResult 357 | { 358 | public string TestName; 359 | public string ADFSObjectType; 360 | public string ADFSObjectIdentifier; 361 | 362 | public ResultType Result; 363 | public string Message; 364 | public string ExceptionMessage; 365 | public System.Collections.Hashtable Details; 366 | 367 | public MigrationTestResult() 368 | { 369 | Result = ResultType.Pass; 370 | Details = new System.Collections.Hashtable(); 371 | } 372 | } 373 | 374 | public enum ResultType 375 | { 376 | Pass = 0, 377 | Warning = 1, 378 | Fail = 2 379 | } 380 | "@; 381 | 382 | 383 | ########## 384 | #templatized claim rules 385 | ########## 386 | 387 | 388 | $IssuanceTransformMigratableRules = 389 | @{ 390 | "Extract Attributes from AD" = 391 | @" 392 | @RuleTemplate = "LdapClaims" 393 | @RuleName = "__ANYVALUE__" 394 | c:[Type == "__ANYVALUE__", Issuer == "AD AUTHORITY"] 395 | => issue(store = "Active Directory", types = (__ANYVALUE__), query = ";__ANYVALUE__;{0}", param = c.Value); 396 | "@ 397 | } 398 | 399 | Function Invoke-ADFSClaimRuleAnalysis 400 | { 401 | [CmdletBinding()] 402 | param 403 | ( 404 | [String] 405 | $RuleSetName, 406 | [String] 407 | $ADFSRuleSet, 408 | [Parameter(Mandatory=$true)] 409 | [System.Collections.Hashtable] 410 | $KnownRules 411 | ) 412 | 413 | #Task 1: Compare rule against known patterns of migratable rules 414 | 415 | $ADFSRuleArray = @() 416 | 417 | if (-not [String]::IsNullOrEmpty($ADFSRuleSet)) 418 | { 419 | $ADFSRuleArray = New-AdfsClaimRuleSet -ClaimRule $ADFSRuleSet 420 | } 421 | 422 | 423 | $Details = "" 424 | $ruleIndex = 0 425 | $AnalysisPassed = $true 426 | 427 | foreach($Rule in $ADFSRuleArray.ClaimRules) 428 | { 429 | #Create result object 430 | $Result = new-object PSObject 431 | $Result | Add-Member -NotePropertyName "RuleSet" -NotePropertyValue $RuleSetName 432 | $Result | Add-Member -NotePropertyName "Rule" -NotePropertyValue $Rule 433 | $ruleIndex++ 434 | 435 | 436 | #Task 1: Find Match to known pattern 437 | $matchFound = $false 438 | $migratablePatternName = "N/A" 439 | 440 | foreach($knownRuleKey in $KnownRules.Keys) 441 | { 442 | $knownRuleRegex = $KnownRules[$knownRuleKey] 443 | $knownRuleRegex = [Regex]::Escape($knownRuleRegex).Replace("__ANYVALUE__", ".*").TrimEnd() 444 | 445 | 446 | #JSON files have \r\n instead or \n ... adding flexibility to match these 447 | $knownRuleRegex = $knownRuleRegex.Replace("\n", "\r?\n*") 448 | 449 | if ($rule -match $knownRuleRegex) 450 | { 451 | $migratablePatternName = $knownRuleKey 452 | $matchFound = $true 453 | } 454 | } 455 | 456 | $Result | Add-Member -NotePropertyName "IsKnownRuleMigratablePattern" -NotePropertyValue $matchFound 457 | $Result | Add-Member -NotePropertyName "KnownRulePatternName" -NotePropertyValue $migratablePatternName 458 | 459 | 460 | #Task 2: Break down condition and issuance statement 461 | #Assumption: There is only one "=>" unambigous match in the rule 462 | $separatorIndex = $Rule.IndexOf("=>") 463 | $conditionStatement = $Rule.Substring(0,$separatorIndex).Trim(); 464 | $issuanceStatement = $Rule.Substring($separatorIndex+2).Trim(); 465 | 466 | $Result | Add-Member -NotePropertyName "ConditionStatement" -NotePropertyValue $conditionStatement 467 | $Result | Add-Member -NotePropertyName "IssuanceStatement" -NotePropertyValue $issuanceStatement 468 | 469 | #Task 3: Find claim types in the condition statement 470 | $TypeRegex = '(?i)type\s+={1,2}\s+"(.*?)"' 471 | $ConditionTypeMatch = [Regex]::Match($conditionStatement, $TypeRegex) 472 | if ($ConditionTypeMatch.Success) 473 | { 474 | #TODO: How does this work with claims with multiple condition in the types (eg. c:[Type=="foo"] && c1:[Type=="bar"] 475 | $ConditionClaimType = $ConditionTypeMatch.Groups[1].ToString() 476 | $Result | Add-Member -NotePropertyName "ConditionClaimType" -NotePropertyValue $ConditionClaimType 477 | 478 | $GroupFilter = "N/A" 479 | 480 | if ($ConditionClaimType -eq "http://schemas.xmlsoap.org/claims/Group") 481 | { 482 | $GroupFilterRegex = '(?i)Value\s+(=(~|=)\s+"(.*?)")' 483 | $GroupFilterMatch = [Regex]::Match($conditionStatement, $GroupFilterRegex) 484 | if ($GroupFilterMatch.Success) 485 | { 486 | $GroupFilter = $GroupFilterMatch.Groups[1].ToString() 487 | } 488 | } 489 | 490 | $Result | Add-Member -NotePropertyName "GroupFilter" -NotePropertyValue $GroupFilter 491 | } 492 | 493 | #Task 4: Find claim types in the issuance statement -- explicit Type = .* 494 | 495 | $IssuanceClaimTypes = @() 496 | 497 | $IssuanceTypeMatch = [Regex]::Match($issuanceStatement, $TypeRegex) 498 | if ($IssuanceTypeMatch.Success) 499 | { 500 | $IssuanceClaimTypes += $IssuanceTypeMatch.Groups[1].ToString() 501 | } 502 | 503 | #Task 4a : Find claim types in the issuance statement from the Attribute Store 504 | $AttributeStoreRuleRegex = '(?i).*store\s*=\s*"(.*?)"' 505 | $AttributeStoreName = "N/A" 506 | $AttributeStoreQuery = "N/A" 507 | $ActiveDirectoryAttributesSplit = @() 508 | 509 | 510 | $AttributeStoreRuleMatch = [Regex]::Match($issuanceStatement, $AttributeStoreRuleRegex) 511 | if ($AttributeStoreRuleMatch.Success) 512 | { 513 | $AttributeStoreName = $AttributeStoreRuleMatch.Groups[1].ToString() 514 | $AttributeStoreRuleTypesRegex = '(?i)types\s*=\s*\("(.*?)"\)' 515 | $AttributeStoreRuleTypesMatch = [Regex]::Match($issuanceStatement, $AttributeStoreRuleTypesRegex) 516 | 517 | if ($AttributeStoreRuleTypesMatch.Success) 518 | { 519 | $IssuanceClaimTypes += $AttributeStoreRuleTypesMatch.Groups[1].ToString().Split(',').Trim().Trim('"'); 520 | } 521 | 522 | #Task 4b: Extract the attributes retrieved from the store 523 | $AttributeStoreQueryRegex = '(?i)query\s*=\s*"(.*?)"' 524 | $AttributeStoreQueryMatch = [Regex]::Match($issuanceStatement, $AttributeStoreQueryRegex) 525 | 526 | if ($AttributeStoreQueryMatch.Success) 527 | { 528 | $AttributeStoreQuery = $AttributeStoreQueryMatch.Groups[1].ToString(); 529 | if ($AttributeStoreName -ieq "Active Directory") 530 | { 531 | $AttributeStoreQuerySplit = $AttributeStoreQuery.Split(';'); 532 | $ActiveDirectoryAttributes = $AttributeStoreQuerySplit[1]; 533 | $ActiveDirectoryAttributesSplit = $ActiveDirectoryAttributes.Split(',') 534 | } 535 | } 536 | } 537 | 538 | $Result | Add-Member -NotePropertyName "IssuanceClaimTypes" -NotePropertyValue $IssuanceClaimTypes 539 | $Result | Add-Member -NotePropertyName "AttributeStoreName" -NotePropertyValue $AttributeStoreName 540 | $Result | Add-Member -NotePropertyName "AttributeStoreQuery" -NotePropertyValue $AttributeStoreQuery 541 | $Result | Add-Member -NotePropertyName "ADAttributes" -NotePropertyValue $ActiveDirectoryAttributesSplit 542 | 543 | Write-Output $Result 544 | } 545 | 546 | } 547 | 548 | Function Test-ADFSRPRuleset 549 | { 550 | [CmdletBinding()] 551 | param 552 | ( 553 | [Parameter(Mandatory=$true)] 554 | [String] 555 | $RulesetName, 556 | [String] 557 | $ADFSRuleSet, 558 | [Parameter(Mandatory=$true)] 559 | [System.Collections.Hashtable] 560 | $KnownRules, 561 | [Parameter(Mandatory=$true)] 562 | [ResultType] 563 | $ResultTypeIfUnknownPattern 564 | ) 565 | 566 | $TestResult = New-Object MigrationTestResult 567 | 568 | $RuleAnalysisResult = Invoke-ADFSClaimRuleAnalysis -ADFSRuleSet $ADFSRuleSet -KnownRules $KnownRules -RuleSetName $RulesetName 569 | 570 | #Capture the expanded details of each rule as a result 571 | $TestResult.Details.Add("ClaimRuleProperties", $RuleAnalysisResult) 572 | 573 | #Insight 1: Did we find claim rules that don't match any template 574 | 575 | $UnknownClaimRulePatternFound = @($RuleAnalysisResult | where {$_.IsKnownRuleMigratablePattern -eq $false}).Count -gt 0 576 | $TestResult.Details.Add("UnkwnownPatternFound", $UnknownClaimRulePatternFound) 577 | 578 | if ($UnknownClaimRulePatternFound) 579 | { 580 | $TestResult.Result = $ResultTypeIfUnknownPattern 581 | $TestResult.Message = "At least one non-migratable rule was detected" 582 | } 583 | 584 | Return $TestResult 585 | } 586 | 587 | Function Test-ADFSRPIssuanceTransformRules 588 | { 589 | [CmdletBinding()] 590 | param 591 | ( 592 | [Parameter(Mandatory=$true)] 593 | $ADFSRelyingPartyTrust 594 | ) 595 | 596 | Test-ADFSRPRuleset ` 597 | -RulesetName "IssuanceTransform" ` 598 | -ADFSRuleSet $ADFSRelyingPartyTrust.IssuanceTransformRules ` 599 | -KnownRules $IssuanceTransformMigratableRules ` 600 | -ResultTypeIfUnknownPattern Warning 601 | 602 | } 603 | 604 | 605 | ########################################### 606 | # AD FS and Azure AD cross-over functions 607 | ########################################### 608 | 609 | function Get-AzureADClaimsMappingFromADFSRPTrust { 610 | [CmdletBinding()] 611 | param ( 612 | [Parameter(Mandatory=$true, ValueFromPipeline=$true)] 613 | $ADFSRelyingPartyTrust 614 | ) 615 | 616 | $testResult = Test-ADFSRPIssuanceTransformRules -ADFSRelyingPartyTrust $ADFSRelyingPartyTrust 617 | 618 | if ($testResult.Result -ne "Pass") 619 | { 620 | throw "Issuance transform rules for RP Trust are not migratable" 621 | } 622 | 623 | #redundant with the one above ?? 624 | if ($testResult.Details.UnkwnownPatternFound) 625 | { 626 | throw "Issuance transform rules for RP Trust have at least one non-migratable pattern" 627 | } 628 | 629 | $ClaimsSchema = @() 630 | 631 | foreach ($r in $testResult.Details.ClaimRuleProperties) 632 | { 633 | if ($r.KnownRulePatternName -ne "Extract Attributes from AD") 634 | { 635 | throw "Pattern is migratable, but creating the claims policy is not supported" 636 | } 637 | 638 | $ADAttributes = $r.ADAttributes 639 | $ClaimTypes = $r.IssuanceClaimTypes 640 | 641 | for ($i=0;$i -lt $ADAttributes.Count;$i++) 642 | { 643 | $ADAttribute = $ADAttributes[$i] 644 | $ClaimType = $ClaimTypes[$i] 645 | 646 | #TODO: Logic to find out if the attribute is a schema extension in Azure AD 647 | #that requires looking up the Azure AD Connect config 648 | 649 | $ClaimsSchema += new-Object PSObject -Property @{ 650 | Source="user"; 651 | ID=$ADAttribute; 652 | SamlClaimType=$ClaimType 653 | } 654 | } 655 | } 656 | 657 | $ClaimsMappingPolicy = New-Object PSObject -Property @{ 658 | Version=1; 659 | IncludeBasicClaimSet="true"; 660 | ClaimsSchema=$ClaimsSchema 661 | } 662 | 663 | $rootPolicy = New-Object PSObject -Property @{ClaimsMappingPolicy=$ClaimsMappingPolicy} 664 | 665 | Write-Output $rootPolicy 666 | } 667 | 668 | function New-TempSelfSignedCertificate 669 | { 670 | [CmdletBinding()] 671 | param ( 672 | [Parameter(Mandatory = $true)] 673 | $CertificateSubject 674 | ) 675 | 676 | $certStore = "Cert:\CurrentUser\My" 677 | $cert = New-SelfSignedCertificate -Subject $CertificateSubject -CertStoreLocation $certStore 678 | $certThumbprint = $cert.Thumbprint 679 | 680 | #generate a random string as a pfx file password 681 | $pfxPassword = -join ((0x30..0x39) + ( 0x41..0x5A) + ( 0x61..0x7A) | Get-Random -Count 16 | % {[char]$_}) 682 | $pfxPasswordSecureString = ConvertTo-SecureString -String $pfxPassword -Force -AsPlainText 683 | 684 | $certStorePath = [System.IO.Path]::Combine($certStore,$certThumbprint) 685 | $certFilePath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(),$certThumbprint + ".pfx") 686 | 687 | Get-ChildItem -Path $certStorePath | Export-PfxCertificate -FilePath $certFilePath -Password $pfxPasswordSecureString | Out-Null 688 | 689 | $result = new-object PSObject -Property @{ 690 | Thumbprint = $certThumbprint; 691 | Certificate = $cert 692 | PfxFilePath = $certFilePath; 693 | PfxPassword = $pfxPassword; 694 | CertStorePath = $certStorePath 695 | } 696 | 697 | Write-Output $result 698 | } 699 | 700 | function Remove-TempSelfSignedCertificate 701 | { 702 | [CmdletBinding()] 703 | param ( 704 | [Parameter(Mandatory = $true)] 705 | $CertificateInfo 706 | ) 707 | 708 | Remove-Item $CertificateInfo.PfxFilePath 709 | Remove-Item $CertificateInfo.CertStorePath 710 | } 711 | 712 | function New-AzureADCustomSigningKeyFromPfx 713 | { 714 | [CmdletBinding()] 715 | param ( 716 | [Parameter(Mandatory = $true)] 717 | $CertificateInfo 718 | ) 719 | 720 | 721 | #calculate key identifier 722 | $thumbprintBytes = [System.Text.Encoding]::ASCII.GetBytes($CertificateInfo.Thumbprint) 723 | $sha256 = [System.Security.Cryptography.HashAlgorithm]::Create("sha256") 724 | $thumbprintHash = $sha256.ComputeHash($thumbprintBytes) 725 | $customKeyIdentifier = [Convert]::ToBase64String($thumbprintHash) 726 | 727 | $signingKeyId = [Guid]::NewGuid().ToString() 728 | $signingEncodedKey = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes($CertificateInfo.PfxFilePath)) 729 | $signingKey = New-Object PSObject -Property @{ 730 | customKeyIdentifier = $customKeyIdentifier; 731 | endDateTime = $CertificateInfo.Certificate.NotAfter.ToUniversalTime().ToString("o"); 732 | keyId=$signingKeyId; 733 | startDateTime = $CertificateInfo.Certificate.NotBefore.ToUniversalTime().ToString("o"); 734 | type="AsymmetricX509Cert"; 735 | usage="Sign"; 736 | key=$signingEncodedKey; 737 | displayName=$CertificateInfo.Certificate.Subject 738 | } 739 | 740 | $verifyKeyId = [Guid]::NewGuid().ToString() 741 | $verifyEncodedKey = [Convert]::ToBase64String($CertificateInfo.Certificate.Export("Cert")) 742 | $verifyKey = New-Object PSObject -Property @{ 743 | customKeyIdentifier = $customKeyIdentifier; 744 | endDateTime = $CertificateInfo.Certificate.NotAfter.ToUniversalTime().ToString("o"); 745 | keyId=$verifyKeyId; 746 | startDateTime = $CertificateInfo.Certificate.NotBefore.ToUniversalTime().ToString("o"); 747 | type="AsymmetricX509Cert"; 748 | usage="Verify"; 749 | key=$verifyEncodedKey; 750 | displayName=$CertificateInfo.Certificate.Subject 751 | } 752 | 753 | $passwordCredential = New-Object PSObject -Property @{ 754 | customKeyIdentifier = $customKeyIdentifier; 755 | endDateTime = $CertificateInfo.Certificate.NotAfter.ToUniversalTime().ToString("o"); 756 | keyId=$signingKeyId; 757 | startDateTime = $CertificateInfo.Certificate.NotBefore.ToUniversalTime().ToString("o"); 758 | secretText = $CertificateInfo.PfxPassword 759 | } 760 | 761 | $result = new-Object PSObject -Property @{ 762 | keyCredentials = @($signingKey,$verifyKey); 763 | passwordCredentials = @($passwordCredential) 764 | } 765 | 766 | Write-Output $result 767 | } 768 | 769 | <# 770 | .Synopsis 771 | Creates a new Azure AD Application from and AD FS Relying party trust and the Application Gallery 772 | as documented in aka.ms/aadgallery-sso-api 773 | 774 | 775 | .Description 776 | This function creates an Azure AD Application from an AD FS Relying Party Trust as follows: 777 | * It instantiates the app gallery template, which creates an Application and a Service Principal Object using 778 | Microsoft Graph 779 | * It reads the AD FS RP trust issuance transformation rules and creates the claims mapping policy; it 780 | supports basic attribute to claim mapping. 781 | * It copies identifiers and endpoints from the AD FS Relying party trust 782 | * It creates a self-signed certificate for token signing 783 | * Takes a group and assigns it to the created app with the specified role 784 | 785 | To call this function, you should have started a session with 786 | 787 | .Parameter AzureADAppTemplateId 788 | Object Id of the app gallery. This can be retrieved using the Get-AzureADApplicationTemplate function in this module 789 | 790 | .Parameter ADFSRelyingPartyTrust 791 | RP Trust object, returned by the AD FS Get-ADFSRelyingPartyTrust Powershell cmdlet 792 | 793 | .Parameter TestGroupAssignmentObjectId 794 | Object ID of the Azure AD Group that will be assigned to the application. This can be retrieved by the Get-AzureADGroup 795 | cmdlet from the AzureAD Poweshell module. 796 | 797 | .Parameter TestGroupAssignmentRoleName 798 | Name of the role that will be assigned. 799 | 800 | .Example 801 | Start-ADFS2AADSession 802 | 803 | $targetGalleryApp = "GalleryAppName" 804 | $targetGroup = Get-AzureADGroup -SearchString "TestGroupName" 805 | $targetAzureADRole = "TestRoleName" 806 | $targetADFSRPId = "ADFSRPIdentifier" 807 | 808 | $galleryApp = Get-AzureADApplicationTemplate -DisplayNameFilter $targetGalleryApp 809 | 810 | $RP=Get-AdfsRelyingPartyTrust -Identifier $targetADFSRPId 811 | 812 | New-AzureADAppFromADFSRPTrust ` 813 | -AzureADAppTemplateId $galleryApp.id ` 814 | -ADFSRelyingPartyTrust $RP ` 815 | -TestGroupAssignmentObjectId $targetGroup.ObjectId ` 816 | -TestGroupAssignmentRoleName $targetAzureADRole 817 | #> 818 | 819 | function New-AzureADAppFromADFSRPTrust { 820 | [CmdletBinding()] 821 | param ( 822 | [Parameter(Mandatory = $true)] 823 | $AzureADAppTemplateId, 824 | [Parameter(Mandatory=$true, ValueFromPipeline=$true)] 825 | $ADFSRelyingPartyTrust, 826 | [Parameter(Mandatory=$true, ValueFromPipeline=$true)] 827 | $TestGroupAssignmentObjectId, 828 | [Parameter(Mandatory=$true, ValueFromPipeline=$true)] 829 | $TestGroupAssignmentRoleName 830 | ) 831 | $DisplayName = $ADFSRelyingPartyTrust.Name 832 | # This script follows the documentation for creating Azure AD gallery applications using MS Graph APIs: aka.ms/aadgallery-sso-api 833 | #STEP 1: Instantiate App Gallery Template 834 | Write-Progress -Activity "Exporting AD FS RP $DisplayName to Azure AD" -Status "Instantiating app gallery template" 835 | $templateInstance = New-AzureADApplicationTemplateInstance -AppTemplateId $AzureADAppTemplateId -DisplayName $DisplayName 836 | $SPObjectId = $templateInstance.servicePrincipal.objectId 837 | $AppObjectId = $templateInstance.application.objectId 838 | 839 | #STEP 2: Create Claims Mapping Policy 840 | Write-Progress -Activity "Exporting AD FS RP $DisplayName to Azure AD" -Status "Create Claims Mapping Policy" 841 | $ClaimsMP = Get-AzureADClaimsMappingFromADFSRPTrust -ADFSRelyingPartyTrust $ADFSRelyingPartyTrust 842 | $ClaimsMPJSON = $ClaimsMP | ConvertTo-Json -Depth 99 843 | $ClaimsMPJSONArray = @($ClaimsMPJSON) 844 | $ClaimsMPRequestBody = New-Object PSObject -Property @{ 845 | definition=$ClaimsMPJSONArray; 846 | displayName = "Autogenerated - Claims Policy - $DisplayName"; 847 | isOrganizationDefault = "false" 848 | } | ConvertTo-Json -Depth 99 849 | $ClaimsMPObject = Invoke-MSGraphQuery -endpoint "policies/claimsMappingPolicies" -Body $ClaimsMPRequestBody -Method "POST" 850 | $ClaimsMPObjectId = $ClaimsMPObject.id 851 | 852 | #Step 3: Create Self-Signed Certificate 853 | Write-Progress -Activity "Exporting AD FS RP $DisplayName to Azure AD" -Status "Create Self-Signed Token Signing Certificate" 854 | $tokenSigningCert = New-TempSelfSignedCertificate -CertificateSubject "CN=Autogenerated token signing cert for - $DisplayName" 855 | $customKeys = New-AzureADCustomSigningKeyFromPfx -CertificateInfo $tokenSigningCert 856 | Remove-TempSelfSignedCertificate -CertificateInfo $tokenSigningCert 857 | 858 | #Step 4: Adding keys, set SSO mode and endpoints to service principal 859 | #We have to wrap the PATCH operation in a retry loop because it might have 860 | #Read after write inconsistencies 861 | Write-Progress -Activity "Exporting AD FS RP $DisplayName to Azure AD" -Status "Updating Service Principal" 862 | 863 | #Create the body of the patch request for service principal 864 | #Custom keys 865 | $servicePrincipalPatchRequest = $customKeys 866 | 867 | #Preferred SSO mode 868 | $servicePrincipalPatchRequest | Add-Member -NotePropertyName "preferredSingleSignOnMode" -NotePropertyValue "saml" 869 | 870 | 871 | #Login URL and reply URLs 872 | $SamlACSEndpoints =$ADFSRelyingPartyTrust.SamlEndpoints | where {$_.Protocol -eq "SAMLAssertionConsumer"} 873 | $firstSamlACSEndpoint = $SamlACSEndpoints | Sort-Object -Property Index | Select-Object -First 1 -ExpandProperty Location 874 | if ($firstSamlACSEndpoint) 875 | { 876 | $servicePrincipalPatchRequest | Add-Member -NotePropertyName "loginUrl" -NotePropertyValue $firstSamlACSEndpoint.AbsoluteUri 877 | } 878 | else 879 | { 880 | $WSFedEndpoint = $ADFSRelyingPartyTrust.WSFedEndpoint 881 | if ($WSFedEndpoint) 882 | { 883 | $servicePrincipalPatchRequest | Add-Member -NotePropertyName "loginUrl" -NotePropertyValue $WSFedEndpoint.AbsoluteUri 884 | } 885 | else { 886 | #This should note happen if the script validates first 887 | throw "Could not find compatible login URL" 888 | } 889 | } 890 | 891 | #Wrap ADFS property in an array, in case there is only one value, PSH does not set the note property as a single value 892 | $SAMLACSEndpointArray = @($SamlACSEndpoints.Location.AbsoluteUri) 893 | $servicePrincipalPatchRequest | Add-Member -NotePropertyName "replyUrls" -NotePropertyValue $SAMLACSEndpointArray 894 | 895 | #Active SSO Cert 896 | $servicePrincipalPatchRequest | Add-Member -NotePropertyName "preferredTokenSigningKeyThumbprint" -NotePropertyValue $tokenSigningCert.Thumbprint 897 | 898 | 899 | #Serialize and send to MS Graph 900 | $servicePrincipalPatchBody = $servicePrincipalPatchRequest| ConvertTo-Json -Depth 99 901 | 902 | 903 | $millisecondsWait = 500 904 | $patchSuceeded = $false 905 | 906 | do 907 | { 908 | try 909 | { 910 | Write-Debug "Attempting to update service principal properties." 911 | Invoke-MSGraphQuery -endpoint "servicePrincipals/$SPObjectId" -Method "PATCH" -Body $servicePrincipalPatchBody | Out-Null 912 | Write-Debug "Service Principal updated successfully" 913 | $patchSuceeded = $true 914 | } 915 | catch 916 | { 917 | Write-Debug "Update to Service Principal failed ... sleeping $millisecondsWait milliseconds" 918 | Start-Sleep -Milliseconds $millisecondsWait 919 | $millisecondsWait *= 2 920 | } 921 | } 922 | while (-not $patchSuceeded) 923 | 924 | #Step 5: Patch the Application object with the Entity ID 925 | #Wrap ADFS property in an array, in case there is only one value, PSH does not set the note property as a single value 926 | $RPIdentifierArray = @($ADFSRelyingPartyTrust.identifier) 927 | $applicationPatchRequest = new-Object PSObject -Property @{ 928 | identifierUris = $RPIdentifierArray 929 | } 930 | 931 | #Serialize and send to MS Graph 932 | $applicationPatchBody = $applicationPatchRequest| ConvertTo-Json -Depth 99 933 | 934 | $millisecondsWait = 500 935 | $patchSuceeded = $false 936 | 937 | do 938 | { 939 | try 940 | { 941 | Write-Debug "Attempting to update application properties." 942 | Invoke-MSGraphQuery -endpoint "applications/$AppObjectId" -Method "PATCH" -Body $applicationPatchBody | Out-Null 943 | Write-Debug "Application updated successfully" 944 | $patchSuceeded = $true 945 | } 946 | catch 947 | { 948 | Write-Debug "Update to Application failed ... sleeping $millisecondsWait milliseconds" 949 | Start-Sleep -Milliseconds $millisecondsWait 950 | $millisecondsWait *= 2 951 | } 952 | } 953 | while (-not $patchSuceeded) 954 | 955 | #Step 6: Associating Claims Mapping Policy to serviceprincipals 956 | Write-Progress -Activity "Exporting AD FS RP $DisplayName to Azure AD" -Status "Assign Claims Mapping Policy to Service Principal" 957 | #HACK: wrap the Claims Mapping Policy association to service principal in a try loop 958 | #Read after write inconsistencies 959 | $millisecondsWait = 500 960 | $AssociationSucceed = $false 961 | do 962 | { 963 | try { 964 | Write-Debug "Trying to associate claims mapping policy to service principal" 965 | $AssignClaimsMPRequestBody = New-Object PSObject -Property @{ 966 | "@odata.id"="https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies/$ClaimsMPObjectId" 967 | } | ConvertTo-Json -Depth 99 968 | 969 | Invoke-MSGraphQuery -endpoint "servicePrincipals/$SPObjectId/claimsMappingPolicies/`$ref" -Method "POST" -Body $AssignClaimsMPRequestBody 970 | Write-Debug "Claims mapping policy read succesfully" 971 | $AssociationSucceed = $true 972 | } 973 | catch 974 | { 975 | Write-Debug "Did not read back claims mapping policy ... sleeping $millisecondsWait milliseconds" 976 | Start-Sleep -Milliseconds $millisecondsWait 977 | $millisecondsWait *= 2 978 | } 979 | } 980 | while (-not $AssociationSucceed) 981 | 982 | #Step 7: Associate the group to the app 983 | #first, we have to get the appRoles from the application object 984 | Write-Progress -Activity "Exporting AD FS RP $DisplayName to Azure AD" -Status "Creating test group assignment" 985 | 986 | $appObject = Invoke-MSGraphQuery -endpoint "/applications/$AppObjectId" -method GET 987 | $appRoleIdToAssign = $appObject.AppRoles | where {$_.displayName -eq $TestGroupAssignmentRoleName} | Select-Object -ExpandProperty Id 988 | 989 | $appAssignmentRequestBody = new-Object PSObject -Property @{ 990 | principalId = $TestGroupAssignmentObjectId; 991 | principalType = "Group"; 992 | appRoleId = $appRoleIdToAssign; 993 | resourceId = $SPObjectId 994 | } | ConvertTo-JSON -Depth 99 995 | Invoke-MSGraphQuery -endpoint "servicePrincipals/$SPObjectId/appRoleAssignments" -Method "POST" -Body $appAssignmentRequestBody | Out-Null 996 | Write-Debug "Test group assignment completed successfully" 997 | 998 | } 999 | 1000 | Export-ModuleMember -Function Connect-MSGraphAPI 1001 | Export-ModuleMember -Function Start-ADFS2AADSession 1002 | Export-ModuleMember -Function Get-AzureADApplicationTemplate 1003 | Export-ModuleMember -Function New-AzureADAppFromADFSRPTrust -------------------------------------------------------------------------------- /ADFS to AzureAD App Migration/samples/New-AzureADAppFromADFSRP.ps1: -------------------------------------------------------------------------------- 1 | Import-Module AzureAD 2 | Import-Module .\ADFS2AADUtils.psm1 3 | 4 | #Connect to both Azure AD Powershell and MS Graph API Client library 5 | Start-ADFS2AADSession 6 | 7 | ##Replace this values 8 | $targetGalleryApp = "GalleryAppName" 9 | $targetGroup = Get-AzureADGroup -SearchString "TestGroupName" 10 | $targetAzureADRole = "TestRoleName" 11 | $targetADFSRPId = "ADFSRPIdentifier" 12 | 13 | #Run the code below if you need to cleanup 14 | #$RP=Get-AdfsRelyingPartyTrust -Identifier $targetADFSRPId 15 | #Get-AzureADServicePrincipal -SearchString $RP.Name | Remove-AzureADServicePrincipal; 16 | #Get-AzureADApplication -SearchString $RP.Name | Remove-AzureADApplication 17 | 18 | 19 | #Query the app gallery 20 | $galleryApp = Get-AzureADApplicationTemplate -DisplayNameFilter $targetGalleryApp 21 | 22 | #Get the RP from ADFS 23 | $RP=Get-AdfsRelyingPartyTrust -Identifier $targetADFSRPId 24 | 25 | #Migrate! 26 | New-AzureADAppFromADFSRPTrust ` 27 | -AzureADAppTemplateId $galleryApp.id ` 28 | -ADFSRelyingPartyTrust $RP ` 29 | -TestGroupAssignmentObjectId $targetGroup.ObjectId ` 30 | -TestGroupAssignmentRoleName $targetAzureADRole -------------------------------------------------------------------------------- /Access Panel/Access Panel Deployment Plan.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/Access Panel/Access Panel Deployment Plan.docx -------------------------------------------------------------------------------- /Application Proxy/Application Proxy Deployment Plan.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/Application Proxy/Application Proxy Deployment Plan.docx -------------------------------------------------------------------------------- /Authentication/Migrating from Federated Authentication to Pass-through Authentication.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/Authentication/Migrating from Federated Authentication to Pass-through Authentication.docx -------------------------------------------------------------------------------- /Authentication/Migrating from Federated Authentication to Password Hash Synchronization.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/Authentication/Migrating from Federated Authentication to Password Hash Synchronization.docx -------------------------------------------------------------------------------- /Authentication/Seamless Single Sign-On Deployment Plan.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/Authentication/Seamless Single Sign-On Deployment Plan.docx -------------------------------------------------------------------------------- /Conditional Access/CA Deployment Plan.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/Conditional Access/CA Deployment Plan.docx -------------------------------------------------------------------------------- /Conditional Access/Readme: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /Log Analytics Views/Azure AD Account Provisioning Events.omsview: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "location": { 6 | "type": "string", 7 | "defaultValue": "" 8 | }, 9 | "resourcegroup": { 10 | "type": "string", 11 | "defaultValue": "" 12 | }, 13 | "subscriptionId": { 14 | "type": "string", 15 | "defaultValue": "" 16 | }, 17 | "workspace": { 18 | "type": "string", 19 | "defaultValue": "" 20 | }, 21 | "workspaceapiversion": { 22 | "type": "string", 23 | "defaultValue": "" 24 | } 25 | }, 26 | "resources": [ 27 | { 28 | "apiVersion": "[parameters('workspaceapiversion')]", 29 | "name": "[parameters('workspace')]", 30 | "type": "Microsoft.OperationalInsights/workspaces", 31 | "location": "[parameters('location')]", 32 | "id": "[Concat('/subscriptions/', parameters('subscriptionId'), '/resourceGroups/', parameters('resourcegroup'), '/providers/Microsoft.OperationalInsights/workspaces/', parameters('workspace'))]", 33 | "resources": [ 34 | { 35 | "apiVersion": "2015-11-01-preview", 36 | "name": "Azure AD Account Provisioning Events", 37 | "type": "views", 38 | "location": "[parameters('location')]", 39 | "id": "[Concat('/subscriptions/', parameters('subscriptionId'), '/resourceGroups/', parameters('resourcegroup'), '/providers/Microsoft.OperationalInsights/workspaces/', parameters('workspace'),'/views/Azure AD Account Provisioning Events')]", 40 | "dependson": [ 41 | "[Concat('/subscriptions/', parameters('subscriptionId'), '/resourceGroups/', parameters('resourcegroup'), '/providers/Microsoft.OperationalInsights/workspaces/', parameters('workspace'))]" 42 | ], 43 | "properties": { 44 | "Id": "Azure AD Account Provisioning Events", 45 | "Name": "Azure AD Account Provisioning Events", 46 | "Author": null, 47 | "Source": "Local", 48 | "Version": 2, 49 | "Dashboard": [ 50 | { 51 | "Id": "LineChartCalloutBuilderBlade", 52 | "Type": "Blade", 53 | "Version": 0, 54 | "Configuration": { 55 | "General": { 56 | "title": "New users provisioned", 57 | "newGroup": true, 58 | "icon": "", 59 | "useIcon": false 60 | }, 61 | "Header": { 62 | "Title": "Successful add operations", 63 | "Subtitle": "Account provisioning" 64 | }, 65 | "LineChart": { 66 | "Query": "AuditLogs | where OperationName == \"Export\" | where Result contains \"success\" | mvexpand AdditionalDetails | where AdditionalDetails.value == \"EntryExportAdd\" | summarize toint(count()) by bin(TimeGenerated, 1h)", 67 | "Callout": { 68 | "Title": "Total successful adds", 69 | "Series": "", 70 | "Operation": "Sum" 71 | }, 72 | "yAxis": { 73 | "isLogarithmic": false, 74 | "units": { 75 | "baseUnitType": "Count", 76 | "baseUnit": "Ones", 77 | "displayUnit": "AUTO" 78 | }, 79 | "customLabel": "" 80 | }, 81 | "NavigationSelect": {} 82 | }, 83 | "List": { 84 | "Query": "AuditLogs | where OperationName == \"Export\" | where Result contains \"success\" | mvexpand AdditionalDetails | where AdditionalDetails.value == \"EntryExportAdd\" | mvexpand TargetResources | extend appName = tostring(TargetResources.displayName) | where appName !contains \"@\" | summarize toint(count()) by appName", 85 | "HideGraph": false, 86 | "enableSparklines": true, 87 | "ColumnsTitle": { 88 | "Name": "App", 89 | "Value": "Add Operations" 90 | }, 91 | "Color": "#55d455", 92 | "operation": "Sum", 93 | "thresholds": { 94 | "isEnabled": false, 95 | "values": [ 96 | { 97 | "name": "Normal", 98 | "threshold": "Default", 99 | "color": "#009e49", 100 | "isDefault": true 101 | }, 102 | { 103 | "name": "Warning", 104 | "threshold": "60", 105 | "color": "#fcd116", 106 | "isDefault": false 107 | }, 108 | { 109 | "name": "Error", 110 | "threshold": "90", 111 | "color": "#ba141a", 112 | "isDefault": false 113 | } 114 | ] 115 | }, 116 | "NameDSVSeparator": "", 117 | "NavigationQuery": "let baseData = materialize( AuditLogs | where OperationName == \"Export\" | where Result contains \"success\" | order by TimeGenerated desc | extend rn = row_number() ); let AuditLogsExpanded = materialize( baseData | mvexpand AdditionalDetails ); let entities = (){ AuditLogsExpanded | where tostring(AdditionalDetails.key) == \"JoiningProperty\" | project rn, entity = AdditionalDetails.value }; let userIds = (){ AuditLogsExpanded | where tostring(AdditionalDetails.value) == \"EntryExportAdd\" | extend dn1 = tostring(TargetResources[0].displayName), dn2 = tostring(TargetResources[1].displayName) | extend appName = iff(dn1 contains \"@\", iff(dn2 contains \"@\", \"N/A\", dn2), dn1) | project rn, appName }; baseData | join kind=leftouter ( entities ) on rn | join kind=leftouter ( userIds ) on rn | where {selected item} | project-away rn, rn1, rn2", 118 | "NavigationSelect": { 119 | "NavigationQuery": "let baseData = materialize( AuditLogs | where OperationName == \"Export\" | where Result contains \"success\" | order by TimeGenerated desc | extend rn = row_number() ); let AuditLogsExpanded = materialize( baseData | mvexpand AdditionalDetails ); let entities = (){ AuditLogsExpanded | where tostring(AdditionalDetails.key) == \"JoiningProperty\" | project rn, entity = AdditionalDetails.value }; let userIds = (){ AuditLogsExpanded | where tostring(AdditionalDetails.value) == \"EntryExportAdd\" | extend dn1 = tostring(TargetResources[0].displayName), dn2 = tostring(TargetResources[1].displayName) | extend appName = iff(dn1 contains \"@\", iff(dn2 contains \"@\", \"N/A\", dn2), dn1) | project rn, appName }; baseData | join kind=leftouter ( entities ) on rn | join kind=leftouter ( userIds ) on rn | where {selected item} | project-away rn, rn1, rn2" 120 | } 121 | } 122 | } 123 | }, 124 | { 125 | "Id": "LineChartCalloutBuilderBlade", 126 | "Type": "Blade", 127 | "Version": 0, 128 | "Configuration": { 129 | "General": { 130 | "title": "New users provisioned", 131 | "newGroup": false, 132 | "icon": "", 133 | "useIcon": false 134 | }, 135 | "Header": { 136 | "Title": "Failed add operations", 137 | "Subtitle": "Account Provisioning" 138 | }, 139 | "LineChart": { 140 | "Query": "AuditLogs | where OperationName == \"Export\" | extend appName = tostring(TargetResources.displayName) | mvexpand AdditionalDetails | where appName !contains \"@\" and appName !contains \"Self-Service\" and appName !contains \"whatever\" and AdditionalDetails.value == \"EntryExportAdd\" and Result contains \"failure\" | summarize count() by bin(TimeGenerated, 1h)", 141 | "Callout": { 142 | "Title": "Total failed adds", 143 | "Series": "", 144 | "Operation": "Sum" 145 | }, 146 | "yAxis": { 147 | "isLogarithmic": false, 148 | "units": { 149 | "baseUnitType": "", 150 | "baseUnit": "", 151 | "displayUnit": "" 152 | }, 153 | "customLabel": "" 154 | }, 155 | "NavigationSelect": {} 156 | }, 157 | "List": { 158 | "Query": "AuditLogs | where OperationName == \"Export\" | where Result contains \"failure\" | mvexpand AdditionalDetails | where AdditionalDetails.value == \"EntryExportAdd\" | mvexpand TargetResources | extend appName = tostring(TargetResources.displayName) | where appName !contains \"@\" and appName !contains \"Self-Service\" and appName !contains \"whatever\" | summarize count() by appName", 159 | "HideGraph": false, 160 | "enableSparklines": true, 161 | "ColumnsTitle": { 162 | "Name": "App", 163 | "Value": "Count" 164 | }, 165 | "Color": "#e81123", 166 | "thresholds": { 167 | "isEnabled": false, 168 | "values": [ 169 | { 170 | "name": "Normal", 171 | "threshold": "Default", 172 | "color": "#009e49", 173 | "isDefault": true 174 | }, 175 | { 176 | "name": "Warning", 177 | "threshold": "60", 178 | "color": "#fcd116", 179 | "isDefault": false 180 | }, 181 | { 182 | "name": "Error", 183 | "threshold": "90", 184 | "color": "#ba141a", 185 | "isDefault": false 186 | } 187 | ] 188 | }, 189 | "NameDSVSeparator": "", 190 | "NavigationQuery": "let baseData = materialize( AuditLogs | where OperationName == \"Export\" | where Result contains \"failure\" | order by TimeGenerated desc | extend rn = row_number() ); let AuditLogsExpanded = materialize( baseData | mvexpand AdditionalDetails ); let entities = (){ AuditLogsExpanded | where tostring(AdditionalDetails.key) == \"JoiningProperty\" | project rn, entity = AdditionalDetails.value }; let userIds = (){ AuditLogsExpanded | where tostring(AdditionalDetails.value) == \"EntryExportAdd\" | extend dn1 = tostring(TargetResources[0].displayName), dn2 = tostring(TargetResources[1].displayName) | extend appName = iff(dn1 contains \"@\", iff(dn2 contains \"@\", \"N/A\", dn2), dn1) | project rn, appName }; baseData | join kind=leftouter ( entities ) on rn | join kind=leftouter ( userIds ) on rn | where {selected item} | project-away rn, rn1, rn2", 191 | "NavigationSelect": { 192 | "NavigationQuery": "let baseData = materialize( AuditLogs | where OperationName == \"Export\" | where Result contains \"failure\" | order by TimeGenerated desc | extend rn = row_number() ); let AuditLogsExpanded = materialize( baseData | mvexpand AdditionalDetails ); let entities = (){ AuditLogsExpanded | where tostring(AdditionalDetails.key) == \"JoiningProperty\" | project rn, entity = AdditionalDetails.value }; let userIds = (){ AuditLogsExpanded | where tostring(AdditionalDetails.value) == \"EntryExportAdd\" | extend dn1 = tostring(TargetResources[0].displayName), dn2 = tostring(TargetResources[1].displayName) | extend appName = iff(dn1 contains \"@\", iff(dn2 contains \"@\", \"N/A\", dn2), dn1) | project rn, appName }; baseData | join kind=leftouter ( entities ) on rn | join kind=leftouter ( userIds ) on rn | where {selected item} | project-away rn, rn1, rn2" 193 | } 194 | } 195 | } 196 | }, 197 | { 198 | "Id": "LineChartCalloutBuilderBlade", 199 | "Type": "Blade", 200 | "Version": 0, 201 | "Configuration": { 202 | "General": { 203 | "title": "Users Updated", 204 | "newGroup": true, 205 | "icon": "", 206 | "useIcon": false 207 | }, 208 | "Header": { 209 | "Title": "Successful update operations", 210 | "Subtitle": "Account Provisioning" 211 | }, 212 | "LineChart": { 213 | "Query": "AuditLogs | where OperationName == \"Export\" | where Result contains \"success\" | mvexpand AdditionalDetails | where AdditionalDetails.value == \"EntryExportUpdate\" | summarize count() by bin(TimeGenerated, 1h)", 214 | "Callout": { 215 | "Title": "Total successful updates", 216 | "Series": "", 217 | "Operation": "Sum" 218 | }, 219 | "yAxis": { 220 | "isLogarithmic": false, 221 | "units": { 222 | "baseUnitType": "", 223 | "baseUnit": "", 224 | "displayUnit": "" 225 | }, 226 | "customLabel": "" 227 | }, 228 | "NavigationSelect": {} 229 | }, 230 | "List": { 231 | "Query": "AuditLogs | where OperationName == \"Export\" | where Result contains \"success\" | mvexpand AdditionalDetails | where AdditionalDetails.value == \"EntryExportUpdate\" | mvexpand TargetResources | extend appName = tostring(TargetResources.displayName) | where appName !contains \"@\" | summarize count() by appName", 232 | "HideGraph": false, 233 | "enableSparklines": true, 234 | "ColumnsTitle": { 235 | "Name": "App", 236 | "Value": "Count" 237 | }, 238 | "Color": "#55d455", 239 | "thresholds": { 240 | "isEnabled": false, 241 | "values": [ 242 | { 243 | "name": "Normal", 244 | "threshold": "Default", 245 | "color": "#009e49", 246 | "isDefault": true 247 | }, 248 | { 249 | "name": "Warning", 250 | "threshold": "60", 251 | "color": "#fcd116", 252 | "isDefault": false 253 | }, 254 | { 255 | "name": "Error", 256 | "threshold": "90", 257 | "color": "#ba141a", 258 | "isDefault": false 259 | } 260 | ] 261 | }, 262 | "NameDSVSeparator": "", 263 | "NavigationQuery": "let baseData = materialize( AuditLogs | where OperationName == \"Export\" | where Result contains \"success\"| order by TimeGenerated desc | extend rn = row_number() ); let AuditLogsExpanded = materialize( baseData | mvexpand AdditionalDetails ); let entities = (){ AuditLogsExpanded | where tostring(AdditionalDetails.key) == \"JoiningProperty\" | project rn, entity = AdditionalDetails.value }; let userIds = (){ AuditLogsExpanded | where tostring(AdditionalDetails.value) == \"EntryExportUpdate\" | extend dn1 = tostring(TargetResources[0].displayName), dn2 = tostring(TargetResources[1].displayName) | extend appName = iff(dn1 contains \"@\", iff(dn2 contains \"@\", \"N/A\", dn2), dn1) | project rn, appName }; baseData | join kind=leftouter ( entities ) on rn | join kind=leftouter ( userIds ) on rn | where {selected item} | project-away rn, rn1, rn2", 264 | "NavigationSelect": { 265 | "NavigationQuery": "let baseData = materialize( AuditLogs | where OperationName == \"Export\" | where Result contains \"success\"| order by TimeGenerated desc | extend rn = row_number() ); let AuditLogsExpanded = materialize( baseData | mvexpand AdditionalDetails ); let entities = (){ AuditLogsExpanded | where tostring(AdditionalDetails.key) == \"JoiningProperty\" | project rn, entity = AdditionalDetails.value }; let userIds = (){ AuditLogsExpanded | where tostring(AdditionalDetails.value) == \"EntryExportUpdate\" | extend dn1 = tostring(TargetResources[0].displayName), dn2 = tostring(TargetResources[1].displayName) | extend appName = iff(dn1 contains \"@\", iff(dn2 contains \"@\", \"N/A\", dn2), dn1) | project rn, appName }; baseData | join kind=leftouter ( entities ) on rn | join kind=leftouter ( userIds ) on rn | where {selected item} | project-away rn, rn1, rn2" 266 | } 267 | } 268 | } 269 | }, 270 | { 271 | "Id": "LineChartCalloutBuilderBlade", 272 | "Type": "Blade", 273 | "Version": 0, 274 | "Configuration": { 275 | "General": { 276 | "title": "Users updated", 277 | "newGroup": false, 278 | "icon": "", 279 | "useIcon": false 280 | }, 281 | "Header": { 282 | "Title": "Failed update operations", 283 | "Subtitle": "Account provisioning" 284 | }, 285 | "LineChart": { 286 | "Query": "AuditLogs | where OperationName == \"Export\" | where Result contains \"failure\"| mvexpand AdditionalDetails | where AdditionalDetails.value == \"EntryExportUpdate\" | summarize count() by bin(TimeGenerated, 1h)", 287 | "Callout": { 288 | "Title": "Total failed updates", 289 | "Series": "", 290 | "Operation": "Sum" 291 | }, 292 | "yAxis": { 293 | "isLogarithmic": false, 294 | "units": { 295 | "baseUnitType": "", 296 | "baseUnit": "", 297 | "displayUnit": "" 298 | }, 299 | "customLabel": "" 300 | }, 301 | "NavigationSelect": {} 302 | }, 303 | "List": { 304 | "Query": "AuditLogs | where OperationName == \"Export\" | where Result contains \"failure\" | mvexpand AdditionalDetails | where AdditionalDetails.value == \"EntryExportUpdate\" | mvexpand TargetResources | extend appName = tostring(TargetResources.displayName) | where appName !contains \"@\" | summarize count() by appName", 305 | "HideGraph": false, 306 | "enableSparklines": true, 307 | "ColumnsTitle": { 308 | "Name": "App", 309 | "Value": "Count" 310 | }, 311 | "Color": "#e81123", 312 | "thresholds": { 313 | "isEnabled": false, 314 | "values": [ 315 | { 316 | "name": "Normal", 317 | "threshold": "Default", 318 | "color": "#009e49", 319 | "isDefault": true 320 | }, 321 | { 322 | "name": "Warning", 323 | "threshold": "60", 324 | "color": "#fcd116", 325 | "isDefault": false 326 | }, 327 | { 328 | "name": "Error", 329 | "threshold": "90", 330 | "color": "#ba141a", 331 | "isDefault": false 332 | } 333 | ] 334 | }, 335 | "NameDSVSeparator": "", 336 | "NavigationQuery": "let baseData = materialize( AuditLogs | where OperationName == \"Export\" | where Result contains \"failure\" | order by TimeGenerated desc | extend rn = row_number() ); let AuditLogsExpanded = materialize( baseData | mvexpand AdditionalDetails ); let entities = (){ AuditLogsExpanded | where tostring(AdditionalDetails.key) == \"JoiningProperty\" | project rn, entity = AdditionalDetails.value }; let userIds = (){ AuditLogsExpanded | where tostring(AdditionalDetails.value) == \"EntryExportUpdate\" | extend dn1 = tostring(TargetResources[0].displayName), dn2 = tostring(TargetResources[1].displayName) | extend appName = iff(dn1 contains \"@\", iff(dn2 contains \"@\", \"N/A\", dn2), dn1) | project rn, appName }; baseData | join kind=leftouter ( entities ) on rn | join kind=leftouter ( userIds ) on rn | where {selected item} | project-away rn, rn1, rn2", 337 | "NavigationSelect": { 338 | "NavigationQuery": "let baseData = materialize( AuditLogs | where OperationName == \"Export\" | where Result contains \"failure\" | order by TimeGenerated desc | extend rn = row_number() ); let AuditLogsExpanded = materialize( baseData | mvexpand AdditionalDetails ); let entities = (){ AuditLogsExpanded | where tostring(AdditionalDetails.key) == \"JoiningProperty\" | project rn, entity = AdditionalDetails.value }; let userIds = (){ AuditLogsExpanded | where tostring(AdditionalDetails.value) == \"EntryExportUpdate\" | extend dn1 = tostring(TargetResources[0].displayName), dn2 = tostring(TargetResources[1].displayName) | extend appName = iff(dn1 contains \"@\", iff(dn2 contains \"@\", \"N/A\", dn2), dn1) | project rn, appName }; baseData | join kind=leftouter ( entities ) on rn | join kind=leftouter ( userIds ) on rn | where {selected item} | project-away rn, rn1, rn2" 339 | } 340 | } 341 | } 342 | }, 343 | { 344 | "Id": "LineChartCalloutBuilderBlade", 345 | "Type": "Blade", 346 | "Version": 0, 347 | "Configuration": { 348 | "General": { 349 | "title": "Users De-provisioned", 350 | "newGroup": true, 351 | "icon": "", 352 | "useIcon": false 353 | }, 354 | "Header": { 355 | "Title": "Successful delete operations", 356 | "Subtitle": "Account Provisioning" 357 | }, 358 | "LineChart": { 359 | "Query": "AuditLogs | where OperationName == \"Export\" | where Result contains \"success\" | mvexpand AdditionalDetails | where AdditionalDetails.value in (\"EntryExportDelete\", \"EntryExportUpdateSoftDelete\") | summarize count() by bin(TimeGenerated, 1h)", 360 | "Callout": { 361 | "Title": "Total successful deletes", 362 | "Series": "", 363 | "Operation": "Sum" 364 | }, 365 | "yAxis": { 366 | "isLogarithmic": false, 367 | "units": { 368 | "baseUnitType": "", 369 | "baseUnit": "", 370 | "displayUnit": "" 371 | }, 372 | "customLabel": "" 373 | }, 374 | "NavigationSelect": {} 375 | }, 376 | "List": { 377 | "Query": "AuditLogs | where OperationName == \"Export\" | where Result contains \"success\" | mvexpand AdditionalDetails | where AdditionalDetails.value in (\"EntryExportDelete\", \"EntryExportUpdateSoftDelete\") | mvexpand TargetResources | extend appName = tostring(TargetResources.displayName) | where appName !contains \"@\" | summarize count() by appName", 378 | "HideGraph": false, 379 | "enableSparklines": true, 380 | "ColumnsTitle": { 381 | "Name": "App", 382 | "Value": "Count" 383 | }, 384 | "Color": "#55d455", 385 | "thresholds": { 386 | "isEnabled": false, 387 | "values": [ 388 | { 389 | "name": "Normal", 390 | "threshold": "Default", 391 | "color": "#009e49", 392 | "isDefault": true 393 | }, 394 | { 395 | "name": "Warning", 396 | "threshold": "60", 397 | "color": "#fcd116", 398 | "isDefault": false 399 | }, 400 | { 401 | "name": "Error", 402 | "threshold": "90", 403 | "color": "#ba141a", 404 | "isDefault": false 405 | } 406 | ] 407 | }, 408 | "NameDSVSeparator": "", 409 | "NavigationQuery": "let baseData = materialize( AuditLogs | where OperationName == \"Export\" | where Result contains \"success\" | order by TimeGenerated desc | extend rn = row_number() ); let AuditLogsExpanded = materialize( baseData | mvexpand AdditionalDetails ); let entities = (){ AuditLogsExpanded | where tostring(AdditionalDetails.key) == \"JoiningProperty\" | project rn, entity = AdditionalDetails.value }; let userIds = (){ AuditLogsExpanded | where tostring(AdditionalDetails.value) in( \"EntryExportDelete\", \"EntryExportUpdateSoftDelete\") | extend dn1 = tostring(TargetResources[0].displayName), dn2 = tostring(TargetResources[1].displayName) | extend appName = iff(dn1 contains \"@\", iff(dn2 contains \"@\", \"N/A\", dn2), dn1) | project rn, appName }; baseData | join kind=leftouter ( entities ) on rn | join kind=leftouter ( userIds ) on rn | where {selected item} | project-away rn, rn1, rn2", 410 | "NavigationSelect": { 411 | "NavigationQuery": "let baseData = materialize( AuditLogs | where OperationName == \"Export\" | where Result contains \"success\" | order by TimeGenerated desc | extend rn = row_number() ); let AuditLogsExpanded = materialize( baseData | mvexpand AdditionalDetails ); let entities = (){ AuditLogsExpanded | where tostring(AdditionalDetails.key) == \"JoiningProperty\" | project rn, entity = AdditionalDetails.value }; let userIds = (){ AuditLogsExpanded | where tostring(AdditionalDetails.value) in( \"EntryExportDelete\", \"EntryExportUpdateSoftDelete\") | extend dn1 = tostring(TargetResources[0].displayName), dn2 = tostring(TargetResources[1].displayName) | extend appName = iff(dn1 contains \"@\", iff(dn2 contains \"@\", \"N/A\", dn2), dn1) | project rn, appName }; baseData | join kind=leftouter ( entities ) on rn | join kind=leftouter ( userIds ) on rn | where {selected item} | project-away rn, rn1, rn2" 412 | } 413 | } 414 | } 415 | }, 416 | { 417 | "Id": "LineChartCalloutBuilderBlade", 418 | "Type": "Blade", 419 | "Version": 0, 420 | "Configuration": { 421 | "General": { 422 | "title": "Users de-provisioned", 423 | "newGroup": false, 424 | "icon": "", 425 | "useIcon": false 426 | }, 427 | "Header": { 428 | "Title": "Failed delete operations", 429 | "Subtitle": "Account provisioning" 430 | }, 431 | "LineChart": { 432 | "Query": "AuditLogs | where OperationName == \"Export\" | where Result contains \"failure\" | mvexpand AdditionalDetails | where AdditionalDetails.value in (\"EntryExportDelete\", \"EntryExportUpdateSoftDelete\") | summarize count() by bin(TimeGenerated, 1h)", 433 | "Callout": { 434 | "Title": "Total failed deletes", 435 | "Series": "", 436 | "Operation": "Sum" 437 | }, 438 | "yAxis": { 439 | "isLogarithmic": false, 440 | "units": { 441 | "baseUnitType": "", 442 | "baseUnit": "", 443 | "displayUnit": "" 444 | }, 445 | "customLabel": "" 446 | }, 447 | "NavigationSelect": {} 448 | }, 449 | "List": { 450 | "Query": "AuditLogs | where OperationName == \"Export\" | where Result contains \"failure\" | mvexpand AdditionalDetails | where AdditionalDetails.value in (\"EntryExportDelete\", \"EntryExportUpdateSoftDelete\") | mvexpand TargetResources | extend appName = tostring(TargetResources.displayName) | where appName !contains \"@\" | summarize count() by appName", 451 | "HideGraph": false, 452 | "enableSparklines": true, 453 | "ColumnsTitle": { 454 | "Name": "App", 455 | "Value": "Count" 456 | }, 457 | "Color": "#e81123", 458 | "thresholds": { 459 | "isEnabled": false, 460 | "values": [ 461 | { 462 | "name": "Normal", 463 | "threshold": "Default", 464 | "color": "#009e49", 465 | "isDefault": true 466 | }, 467 | { 468 | "name": "Warning", 469 | "threshold": "60", 470 | "color": "#fcd116", 471 | "isDefault": false 472 | }, 473 | { 474 | "name": "Error", 475 | "threshold": "90", 476 | "color": "#ba141a", 477 | "isDefault": false 478 | } 479 | ] 480 | }, 481 | "NameDSVSeparator": "", 482 | "NavigationQuery": "let baseData = materialize( AuditLogs | where OperationName == \"Export\" | where Result contains \"failure\" | order by TimeGenerated desc | extend rn = row_number() ); let AuditLogsExpanded = materialize( baseData | mvexpand AdditionalDetails ); let entities = (){ AuditLogsExpanded | where tostring(AdditionalDetails.key) == \"JoiningProperty\" | project rn, entity = AdditionalDetails.value }; let userIds = (){ AuditLogsExpanded | where tostring(AdditionalDetails.value) in( \"EntryExportDelete\", \"EntryExportUpdateSoftDelete\") | extend dn1 = tostring(TargetResources[0].displayName), dn2 = tostring(TargetResources[1].displayName) | extend appName = iff(dn1 contains \"@\", iff(dn2 contains \"@\", \"N/A\", dn2), dn1) | project rn, appName }; baseData | join kind=leftouter ( entities ) on rn | join kind=leftouter ( userIds ) on rn | where {selected item} | project-away rn, rn1, rn2", 483 | "NavigationSelect": { 484 | "NavigationQuery": "let baseData = materialize( AuditLogs | where OperationName == \"Export\" | where Result contains \"failure\" | order by TimeGenerated desc | extend rn = row_number() ); let AuditLogsExpanded = materialize( baseData | mvexpand AdditionalDetails ); let entities = (){ AuditLogsExpanded | where tostring(AdditionalDetails.key) == \"JoiningProperty\" | project rn, entity = AdditionalDetails.value }; let userIds = (){ AuditLogsExpanded | where tostring(AdditionalDetails.value) in( \"EntryExportDelete\", \"EntryExportUpdateSoftDelete\") | extend dn1 = tostring(TargetResources[0].displayName), dn2 = tostring(TargetResources[1].displayName) | extend appName = iff(dn1 contains \"@\", iff(dn2 contains \"@\", \"N/A\", dn2), dn1) | project rn, appName }; baseData | join kind=leftouter ( entities ) on rn | join kind=leftouter ( userIds ) on rn | where {selected item} | project-away rn, rn1, rn2" 485 | } 486 | } 487 | } 488 | } 489 | ], 490 | "Filters": [], 491 | "OverviewTile": { 492 | "Id": "SingleQueryDonutBuilderTileV1", 493 | "Type": "OverviewTile", 494 | "Version": 2, 495 | "Configuration": { 496 | "Donut": { 497 | "Query": "AuditLogs | where OperationName == \"Export\" | extend resultVerbose = iff(Result==1, \"Failure\", \"Success\") | summarize count() by resultVerbose", 498 | "CenterLegend": { 499 | "Text": "Total", 500 | "Operation": "Sum", 501 | "ArcsToSelect": [] 502 | }, 503 | "Options": { 504 | "colors": [ 505 | "#00188f", 506 | "#0072c6", 507 | "#00bcf2" 508 | ], 509 | "valueColorMapping": [ 510 | { 511 | "value": "Success", 512 | "color": "#007233" 513 | }, 514 | { 515 | "value": "Failure", 516 | "color": "#ba141a" 517 | } 518 | ] 519 | } 520 | }, 521 | "Advanced": { 522 | "DataFlowVerification": { 523 | "Enabled": false, 524 | "Query": "search * | limit 1 | project TimeGenerated", 525 | "Message": "" 526 | } 527 | } 528 | } 529 | } 530 | } 531 | } 532 | ] 533 | } 534 | ] 535 | } -------------------------------------------------------------------------------- /Log Analytics Views/configure-signal-logic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/Log Analytics Views/configure-signal-logic.png -------------------------------------------------------------------------------- /Log Analytics Views/create-rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/Log Analytics Views/create-rule.png -------------------------------------------------------------------------------- /Log Analytics Views/details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/Log Analytics Views/details.png -------------------------------------------------------------------------------- /Log Analytics Views/readme.md: -------------------------------------------------------------------------------- 1 | # Install and use the Log Analytics views for Azure Active Directory 2 | 3 | The Azure Active Directory Log Analytics views helps you analyze and search the Azure AD activity logs in your Azure AD tenant. Azure AD activity logs include: 4 | 5 | * Audit logs: The [audit logs activity report](https://docs.microsoft.com/azure/active-directory/reports-monitoring/concept-audit-logs) gives you access to the history of every task that's performed in your tenant. 6 | * Sign-in logs: With the [sign-in activity report](https://docs.microsoft.com/azure/active-directory/reports-monitoring/concept-sign-ins), you can determine who performed the tasks that are reported in the audit logs. 7 | 8 | ## Prerequisites 9 | 10 | To use the views, you need: 11 | 12 | * A Log Analytics workspace in your Azure subscription. Learn how to [create a Log Analytics workspace](https://docs.microsoft.com/azure/log-analytics/log-analytics-quick-create-workspace). 13 | * First, complete the steps to [route the Azure AD activity logs to your Log Analytics workspace](https://docs.microsoft.com/azure/active-directory/reports-monitoring/howto-integrate-activity-logs-with-log-analytics). 14 | * Download the views from the [GitHub repository](https://aka.ms/AADLogAnalyticsviews) to your local computer. 15 | 16 | ## Install the Log Analytics views 17 | 18 | 1. Navigate to your Log Analytics workspace. To do this, first navigate to the [Azure portal](https://portal.azure.com) and select **All services**. Type **Log Analytics** in the text box, and select **Log Analytics**. Select the workspace you routed the activity logs to, as part of the prerequisites. 19 | 2. Select **View Designer**, select **Import** and then select **Choose File** to import the views from your local computer. 20 | 3. Select the views you downloaded from the prerequisites and select **Save** to save the import. Do this for the **Azure AD Account Provisioning Events** view and the **Sign-ins Events** view. 21 | 22 | ## Use the views 23 | 24 | 1. Navigate to your Log Analytics workspace. To do this, first navigate to the [Azure portal](https://portal.azure.com) and select **All services**. Type **Log Analytics** in the text box, and select **Log Analytics**. Select the workspace you routed the activity logs to, as part of the prerequisites. 25 | 26 | 2. Once you're in the workspace, select **Workspace Summary**. You should see the following three views: 27 | 28 | * **Azure AD Account Provisioning Events**: This view shows reports related to auditing provisioning activity, such as the number of new users provisioned and provisioning failures, number of users updated and update failures and the number of users de-provisioned and corresponding failures. 29 | * **Sign-ins Events**: This view shows the most relevant reports related to monitoring sign-in activity, such as sign-ins by application, user, device, as well as a summary view tracking the number of sign-ins over time. 30 | 31 | 3. Select either of these views to jump in to the individual reports. You can also set alerts on any of the report parameters. For example, let's set an alert for every time there's a sign-in error. To do this, first select the **Sign-ins Events** view, select **Sign-in errors over time** report and then select **Analytics** to open the details page, with the actual query behind the report. 32 | 33 | ![Details](./details.png) 34 | 35 | 36 | 4. Select **Set Alert**, and then select **Whenever the Custom log search is <logic undefined>** under the **Alert criteria** section. Since we want to alert whenever there's a sign-in error, set the **Threshold** of the default alert logic to **1** and then select **Done**. 37 | 38 | ![Configure signal logic](./configure-signal-logic.png) 39 | 40 | 5. Enter a name and description for the alert and set the severity to **Warning**. 41 | 42 | ![Create rule](./create-rule.png) 43 | 44 | 6. Select the action group to alert. In general, this can be either a team you want to notify via email or text message, or it can be an automated task using webhooks, runbooks, functions, logic apps or external ITSM solutions. Learn how to [create and manage action groups in the Azure portal](https://docs.microsoft.com/azure/monitoring-and-diagnostics/monitoring-action-groups). 45 | 46 | 7. Select **Create alert rule** to create the alert. Now you will be alerted every time there's a sign-in error. 47 | 48 | ## Next steps 49 | 50 | * [How to analyze activity logs in Log Analytics](https://docs.microsoft.com/azure/active-directory/reports-monitoring/howto-analyze-activity-logs-log-analytics.md) 51 | * [Get started with Log Analytics in the Azure portal](https://docs.microsoft.com/azure/log-analytics/query-language/get-started-analytics-portal) 52 | -------------------------------------------------------------------------------- /Migrationwhitepapers/Migrating Your Applications to Azure Active Directory.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/Migrationwhitepapers/Migrating Your Applications to Azure Active Directory.pdf -------------------------------------------------------------------------------- /Multi Factor Authentication/MFADeploymentPlan.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/Multi Factor Authentication/MFADeploymentPlan.docx -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing 3 | 4 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 5 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 6 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 7 | 8 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 9 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 10 | provided by the bot. You will only need to do this once across all repos using our CLA. 11 | 12 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 13 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 14 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 15 | -------------------------------------------------------------------------------- /Self Service Password Reset -SSPR/SSPR Deployment Plan.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/Self Service Password Reset -SSPR/SSPR Deployment Plan.docx -------------------------------------------------------------------------------- /Single Sign On - SSO/SaaS SSO Deployment Plan.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/Single Sign On - SSO/SaaS SSO Deployment Plan.docx -------------------------------------------------------------------------------- /User Provisioning/Outbound User Provisioning Deployment Plan.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/User Provisioning/Outbound User Provisioning Deployment Plan.docx -------------------------------------------------------------------------------- /User Provisioning/Workday-driven Inbound User Provisioning Deployment Plan.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/Deployment-Plans/dd510971f5bd1e204c017ace9094416533d371a1/User Provisioning/Workday-driven Inbound User Provisioning Deployment Plan.docx --------------------------------------------------------------------------------