├── .github
├── ISSUE_TEMPLATE.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── AppCreationScripts
├── AppCreationScripts.md
├── Cleanup.ps1
├── Configure.ps1
└── sample.json
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── ReadmeFiles
├── Topology.png
└── deviceCodeFlow.png
├── device-code-flow-console.sln
└── device-code-flow-console
├── MyInformation.cs
├── Program.cs
├── ProtectedApiCallHelper.cs
├── PublicAppUsingDeviceCodeFlow.cs
├── SampleConfiguration.cs
├── appsettings.json
└── device-code-flow-console.csproj
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
4 | > Please provide us with the following information:
5 | > ---------------------------------------------------------------
6 |
7 | ### This issue is for a: (mark with an `x`)
8 | ```
9 | - [ ] bug report -> please search issues before submitting
10 | - [ ] feature request
11 | - [ ] documentation issue or request
12 | - [ ] regression (a behavior that used to work and stopped in a new release)
13 | ```
14 |
15 | ### Minimal steps to reproduce
16 | >
17 |
18 | ### Any log messages given by the failure
19 | >
20 |
21 | ### Expected/desired behavior
22 | >
23 |
24 | ### OS and Version?
25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?)
26 |
27 | ### Versions
28 | >
29 |
30 | ### Mention any other details that might be useful
31 |
32 | > ---------------------------------------------------------------
33 | > Thanks! We'll be in touch soon.
34 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Purpose
2 |
3 | * ...
4 |
5 | ## Does this introduce a breaking change?
6 |
7 | ```
8 | [ ] Yes
9 | [ ] No
10 | ```
11 |
12 | ## Pull Request Type
13 | What kind of change does this Pull Request introduce?
14 |
15 |
16 | ```
17 | [ ] Bugfix
18 | [ ] Feature
19 | [ ] Code style update (formatting, local variables)
20 | [ ] Refactoring (no functional changes, no api changes)
21 | [ ] Documentation content changes
22 | [ ] Other... Please describe:
23 | ```
24 |
25 | ## How to Test
26 | * Get the code
27 |
28 | ```
29 | git clone [repo-address]
30 | cd [repo-name]
31 | git checkout [branch-name]
32 | npm install
33 | ```
34 |
35 | * Test the code
36 |
37 | ```
38 | ```
39 |
40 | ## What to Check
41 | Verify that the following are valid
42 | * ...
43 |
44 | ## Other Information
45 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/AppCreationScripts/AppCreationScripts.md:
--------------------------------------------------------------------------------
1 | # Registering the Azure Active Directory applications and updating the configuration files for this sample using PowerShell scripts
2 |
3 | ## Overview
4 |
5 | ### Quick summary
6 |
7 | 1. On Windows run PowerShell and navigate to the root of the cloned directory
8 | 1. In PowerShell run:
9 | ```PowerShell
10 | Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force
11 | ```
12 | 1. Run the script to create your Azure AD application and configure the code of the sample application accordinly. (Other ways of running the scripts are described below)
13 | ```PowerShell
14 | .\AppCreationScripts\Configure.ps1
15 | ```
16 | 1. Open the Visual Studio solution and click start
17 |
18 | ### More details
19 |
20 | The following paragraphs:
21 |
22 | - [Present the scripts](#presentation-of-the-scripts) and explain their [usage patterns](#usage-pattern-for-tests-and-devops-scenarios) for test and DevOps scenarios.
23 | - Explain the [pre-requisites](#pre-requisites)
24 | - Explain [four ways of running the scripts](#four-ways-to-run-the-script):
25 | - [Interactively](#option-1-interactive) to create the app in your home tenant
26 | - [Passing credentials](#option-2-non-interactive) to create the app in your home tenant
27 | - [Interactively in a specific tenant](#option-3-interactive-but-create-apps-in-a-specified-tenant)
28 | - [Passing credentials in a specific tenant](#option-4-non-interactive-and-create-apps-in-a-specified-tenant)
29 |
30 | ## Goal of the scripts
31 |
32 | ### Presentation of the scripts
33 |
34 | This sample comes with two PowerShell scripts, which automate the creation of the Azure Active Directory applications, and the configuration of the code for this sample. Once you run them, you will only need to build the solution and you are good to test.
35 |
36 | These scripts are:
37 |
38 | - `Configure.ps1` which:
39 | - creates Azure AD applications and their related objects (permissions, dependencies, secrets),
40 | - changes the configuration files in the C# and JavaScript projects.
41 | - creates a summary file named `createdApps.html` in the folder from which you ran the script, and containing, for each Azure AD application it created:
42 | - the identifier of the application
43 | - the AppId of the application
44 | - the url of its registration in the [Azure portal](https://portal.azure.com).
45 |
46 | - `Cleanup.ps1` which cleans-up the Azure AD objects created by `Configure.ps1`. Note that this script does not revert the changes done in the configuration files, though. You will need to undo the change from source control (from Visual Studio, or from the command line using, for instance, git reset).
47 |
48 | ### Usage pattern for tests and DevOps scenarios
49 |
50 | The `Configure.ps1` will stop if it tries to create an Azure AD application which already exists in the tenant. For this, if you are using the script to try/test the sample, or in DevOps scenarios, you might want to run `Cleanup.ps1` just before `Configure.ps1`. This is what is shown in the steps below.
51 |
52 | ## How to use the app creation scripts ?
53 |
54 | ### Pre-requisites
55 |
56 | 1. Open PowerShell (On Windows, press `Windows-R` and type `PowerShell` in the search window)
57 | 2. Navigate to the root directory of the project.
58 | 3. Until you change it, the default [Execution Policy](https:/go.microsoft.com/fwlink/?LinkID=135170) for scripts is usually `Restricted`. In order to run the PowerShell script you need to set the Execution Policy to `RemoteSigned`. You can set this just for the current PowerShell process by running the command:
59 | ```PowerShell
60 | Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
61 | ```
62 | ### (Optionally) install AzureAD PowerShell modules
63 | The scripts install the required PowerShell module (AzureAD) for the current user if needed. However, if you want to install if for all users on the machine, you can follow the following steps:
64 |
65 | 4. If you have never done it already, in the PowerShell window, install the AzureAD PowerShell modules. For this:
66 |
67 | 1. Open PowerShell as admin (On Windows, Search Powershell in the search bar, right click on it and select Run as administrator).
68 | 2. Type:
69 | ```PowerShell
70 | Install-Module AzureAD
71 | ```
72 |
73 | or if you cannot be administrator on your machine, run:
74 | ```PowerShell
75 | Install-Module AzureAD -Scope CurrentUser
76 | ```
77 |
78 | ### Run the script and start running
79 |
80 | 5. Go to the `AppCreationScripts` sub-folder. From the folder where you cloned the repo,
81 | ```PowerShell
82 | cd AppCreationScripts
83 | ```
84 | 6. Run the scripts. See below for the [four options](#four-ways-to-run-the-script) to do that.
85 | 7. Open the Visual Studio solution, and in the solution's context menu, choose **Set Startup Projects**.
86 | 8. select **Start** for the projects
87 |
88 | You're done. this just works!
89 |
90 | ### Four ways to run the script
91 |
92 | We advise four ways of running the script:
93 |
94 | - Interactive: you will be prompted for credentials, and the scripts decide in which tenant to create the objects,
95 | - non-interactive: you will provide credentials, and the scripts decide in which tenant to create the objects,
96 | - Interactive in specific tenant: you will provide the tenant in which you want to create the objects and then you will be prompted for credentials, and the scripts will create the objects,
97 | - non-interactive in specific tenant: you will provide tenant in which you want to create the objects and credentials, and the scripts will create the objects.
98 |
99 | Here are the details on how to do this.
100 |
101 | #### Option 1 (interactive)
102 |
103 | - Just run ``. .\Configure.ps1``, and you will be prompted to sign-in (email address, password, and if needed MFA).
104 | - The script will be run as the signed-in user and will use the tenant in which the user is defined.
105 |
106 | Note that the script will choose the tenant in which to create the applications, based on the user. Also to run the `Cleanup.ps1` script, you will need to re-sign-in.
107 |
108 | #### Option 2 (non-interactive)
109 |
110 | When you know the indentity and credentials of the user in the name of whom you want to create the applications, you can use the non-interactive approach. It's more adapted to DevOps. Here is an example of script you'd want to run in a PowerShell Window
111 |
112 | ```PowerShell
113 | $secpasswd = ConvertTo-SecureString "[Password here]" -AsPlainText -Force
114 | $mycreds = New-Object System.Management.Automation.PSCredential ("[login@tenantName here]", $secpasswd)
115 | . .\Cleanup.ps1 -Credential $mycreds
116 | . .\Configure.ps1 -Credential $mycreds
117 | ```
118 |
119 | Of course, in real life, you might already get the password as a `SecureString`. You might also want to get the password from KeyVault.
120 |
121 | #### Option 3 (Interactive, but create apps in a specified tenant)
122 |
123 | if you want to create the apps in a particular tenant, you can use the following option:
124 | - open the [Azure portal](https://portal.azure.com)
125 | - Select the Azure Active directory you are interested in (in the combo-box below your name on the top right of the browser window)
126 | - Find the "Active Directory" object in this tenant
127 | - Go to **Properties** and copy the content of the **Directory Id** property
128 | - Then use the full syntax to run the scripts:
129 |
130 | ```PowerShell
131 | $tenantId = "yourTenantIdGuid"
132 | . .\Cleanup.ps1 -TenantId $tenantId
133 | . .\Configure.ps1 -TenantId $tenantId
134 | ```
135 |
136 | #### Option 4 (non-interactive, and create apps in a specified tenant)
137 |
138 | This option combines option 2 and option 3: it creates the application in a specific tenant. See option 3 for the way to get the tenant Id. Then run:
139 |
140 | ```PowerShell
141 | $secpasswd = ConvertTo-SecureString "[Password here]" -AsPlainText -Force
142 | $mycreds = New-Object System.Management.Automation.PSCredential ("[login@tenantName here]", $secpasswd)
143 | $tenantId = "yourTenantIdGuid"
144 | . .\Cleanup.ps1 -Credential $mycreds -TenantId $tenantId
145 | . .\Configure.ps1 -Credential $mycreds -TenantId $tenantId
146 | ```
147 |
--------------------------------------------------------------------------------
/AppCreationScripts/Cleanup.ps1:
--------------------------------------------------------------------------------
1 | [CmdletBinding()]
2 | param(
3 | [PSCredential] $Credential,
4 | [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')]
5 | [string] $tenantId
6 | )
7 |
8 | if ((Get-Module -ListAvailable -Name "AzureAD") -eq $null) {
9 | Install-Module "AzureAD" -Scope CurrentUser
10 | }
11 | Import-Module AzureAD
12 | $ErrorActionPreference = 'Stop'
13 |
14 | Function Cleanup
15 | {
16 | <#
17 | .Description
18 | This function removes the Azure AD applications for the sample. These applications were created by the Configure.ps1 script
19 | #>
20 |
21 | # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant
22 | # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of the Azure AD.
23 |
24 | # Login to Azure PowerShell (interactive if credentials are not already provided:
25 | # you'll need to sign-in with creds enabling your to create apps in the tenant)
26 | if (!$Credential -and $TenantId)
27 | {
28 | $creds = Connect-AzureAD -TenantId $tenantId
29 | }
30 | else
31 | {
32 | if (!$TenantId)
33 | {
34 | $creds = Connect-AzureAD -Credential $Credential
35 | }
36 | else
37 | {
38 | $creds = Connect-AzureAD -TenantId $tenantId -Credential $Credential
39 | }
40 | }
41 |
42 | if (!$tenantId)
43 | {
44 | $tenantId = $creds.Tenant.Id
45 | }
46 | $tenant = Get-AzureADTenantDetail
47 | $tenantName = ($tenant.VerifiedDomains | Where { $_._Default -eq $True }).Name
48 |
49 | # Removes the applications
50 | Write-Host "Cleaning-up applications from tenant '$tenantName'"
51 |
52 | Write-Host "Removing 'client' (active-directory-dotnet-deviceprofile) if needed"
53 | $app=Get-AzureADApplication -Filter "DisplayName eq 'active-directory-dotnet-deviceprofile'"
54 |
55 | if ($app)
56 | {
57 | Remove-AzureADApplication -ObjectId $app.ObjectId
58 | Write-Host "Removed."
59 | }
60 |
61 | }
62 |
63 | Cleanup -Credential $Credential -tenantId $TenantId
64 |
--------------------------------------------------------------------------------
/AppCreationScripts/Configure.ps1:
--------------------------------------------------------------------------------
1 | [CmdletBinding()]
2 | param(
3 | [PSCredential] $Credential,
4 | [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')]
5 | [string] $tenantId
6 | )
7 |
8 | <#
9 | This script creates the Azure AD applications needed for this sample and updates the configuration files
10 | for the visual Studio projects from the data in the Azure AD applications.
11 |
12 | Before running this script you need to install the AzureAD cmdlets as an administrator.
13 | For this:
14 | 1) Run Powershell as an administrator
15 | 2) in the PowerShell window, type: Install-Module AzureAD
16 |
17 | There are four ways to run this script. For more information, read the AppCreationScripts.md file in the same folder as this script.
18 | #>
19 |
20 | # Adds the requiredAccesses (expressed as a pipe separated string) to the requiredAccess structure
21 | # The exposed permissions are in the $exposedPermissions collection, and the type of permission (Scope | Role) is
22 | # described in $permissionType
23 | Function AddResourcePermission($requiredAccess, `
24 | $exposedPermissions, [string]$requiredAccesses, [string]$permissionType)
25 | {
26 | foreach($permission in $requiredAccesses.Trim().Split("|"))
27 | {
28 | foreach($exposedPermission in $exposedPermissions)
29 | {
30 | if ($exposedPermission.Value -eq $permission)
31 | {
32 | $resourceAccess = New-Object Microsoft.Open.AzureAD.Model.ResourceAccess
33 | $resourceAccess.Type = $permissionType # Scope = Delegated permissions | Role = Application permissions
34 | $resourceAccess.Id = $exposedPermission.Id # Read directory data
35 | $requiredAccess.ResourceAccess.Add($resourceAccess)
36 | }
37 | }
38 | }
39 | }
40 |
41 | #
42 | # Exemple: GetRequiredPermissions "Microsoft Graph" "Graph.Read|User.Read"
43 | # See also: http://stackoverflow.com/questions/42164581/how-to-configure-a-new-azure-ad-application-through-powershell
44 | Function GetRequiredPermissions([string] $applicationDisplayName, [string] $requiredDelegatedPermissions, [string]$requiredApplicationPermissions, $servicePrincipal)
45 | {
46 | # If we are passed the service principal we use it directly, otherwise we find it from the display name (which might not be unique)
47 | if ($servicePrincipal)
48 | {
49 | $sp = $servicePrincipal
50 | }
51 | else
52 | {
53 | $sp = Get-AzureADServicePrincipal -Filter "DisplayName eq '$applicationDisplayName'"
54 | }
55 | $appid = $sp.AppId
56 | $requiredAccess = New-Object Microsoft.Open.AzureAD.Model.RequiredResourceAccess
57 | $requiredAccess.ResourceAppId = $appid
58 | $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.ResourceAccess]
59 |
60 | # $sp.Oauth2Permissions | Select Id,AdminConsentDisplayName,Value: To see the list of all the Delegated permissions for the application:
61 | if ($requiredDelegatedPermissions)
62 | {
63 | AddResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2Permissions -requiredAccesses $requiredDelegatedPermissions -permissionType "Scope"
64 | }
65 |
66 | # $sp.AppRoles | Select Id,AdminConsentDisplayName,Value: To see the list of all the Application permissions for the application
67 | if ($requiredApplicationPermissions)
68 | {
69 | AddResourcePermission $requiredAccess -exposedPermissions $sp.AppRoles -requiredAccesses $requiredApplicationPermissions -permissionType "Role"
70 | }
71 | return $requiredAccess
72 | }
73 |
74 |
75 | Function UpdateLine([string] $line, [string] $value)
76 | {
77 | $index = $line.IndexOf('=')
78 | $delimiter = ';'
79 | if ($index -eq -1)
80 | {
81 | $index = $line.IndexOf(':')
82 | $delimiter = ','
83 | }
84 | if ($index -ige 0)
85 | {
86 | $line = $line.Substring(0, $index+1) + " "+'"'+$value+'"'+$delimiter
87 | }
88 | return $line
89 | }
90 |
91 | Function UpdateTextFile([string] $configFilePath, [System.Collections.HashTable] $dictionary)
92 | {
93 | $lines = Get-Content $configFilePath
94 | $index = 0
95 | while($index -lt $lines.Length)
96 | {
97 | $line = $lines[$index]
98 | foreach($key in $dictionary.Keys)
99 | {
100 | if ($line.Contains($key))
101 | {
102 | $lines[$index] = UpdateLine $line $dictionary[$key]
103 | }
104 | }
105 | $index++
106 | }
107 |
108 | Set-Content -Path $configFilePath -Value $lines -Force
109 | }
110 |
111 | Set-Content -Value "
" -Path createdApps.html
112 | Add-Content -Value "Application | AppId | Url in the Azure portal |
" -Path createdApps.html
113 |
114 | Function ConfigureApplications
115 | {
116 | <#.Description
117 | This function creates the Azure AD applications for the sample in the provided Azure AD tenant and updates the
118 | configuration files in the client and service project of the visual studio solution (App.Config and Web.Config)
119 | so that they are consistent with the Applications parameters
120 | #>
121 |
122 | # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant
123 | # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of the Azure AD.
124 |
125 | # Login to Azure PowerShell (interactive if credentials are not already provided:
126 | # you'll need to sign-in with creds enabling your to create apps in the tenant)
127 | if (!$Credential -and $TenantId)
128 | {
129 | $creds = Connect-AzureAD -TenantId $tenantId
130 | }
131 | else
132 | {
133 | if (!$TenantId)
134 | {
135 | $creds = Connect-AzureAD -Credential $Credential
136 | }
137 | else
138 | {
139 | $creds = Connect-AzureAD -TenantId $tenantId -Credential $Credential
140 | }
141 | }
142 |
143 | if (!$tenantId)
144 | {
145 | $tenantId = $creds.Tenant.Id
146 | }
147 |
148 | $tenant = Get-AzureADTenantDetail
149 | $tenantName = ($tenant.VerifiedDomains | Where { $_._Default -eq $True }).Name
150 |
151 | # Get the user running the script
152 | $user = Get-AzureADUser -ObjectId $creds.Account.Id
153 |
154 | # Create the client AAD application
155 | Write-Host "Creating the AAD application (active-directory-dotnet-deviceprofile)"
156 | $clientAadApplication = New-AzureADApplication -DisplayName "active-directory-dotnet-deviceprofile" `
157 | -ReplyUrls "urn:ietf:wg:oauth:2.0:oob" `
158 | -AvailableToOtherTenants $True `
159 | -PublicClient $True
160 |
161 | $currentAppId = $clientAadApplication.AppId
162 | $clientServicePrincipal = New-AzureADServicePrincipal -AppId $currentAppId -Tags {WindowsAzureActiveDirectoryIntegratedApp}
163 |
164 | # add the user running the script as an app owner if needed
165 | $owner = Get-AzureADApplicationOwner -ObjectId $clientAadApplication.ObjectId
166 | if ($owner -eq $null)
167 | {
168 | Add-AzureADApplicationOwner -ObjectId $clientAadApplication.ObjectId -RefObjectId $user.ObjectId
169 | Write-Host "'$($user.UserPrincipalName)' added as an application owner to app '$($clientServicePrincipal.DisplayName)'"
170 | }
171 |
172 | Write-Host "Done creating the client application (active-directory-dotnet-deviceprofile)"
173 |
174 | # URL of the AAD application in the Azure portal
175 | # Future? $clientPortalUrl = "https://portal.azure.com/#@"+$tenantName+"/blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Overview/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.ObjectId+"/isMSAApp/"
176 | $clientPortalUrl = "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/CallAnAPI/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.ObjectId+"/isMSAApp/"
177 | Add-Content -Value "client | $currentAppId | active-directory-dotnet-deviceprofile |
" -Path createdApps.html
178 |
179 | $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess]
180 |
181 | # Add Required Resources Access (from 'client' to 'Microsoft Graph')
182 | Write-Host "Getting access from 'client' to 'Microsoft Graph'"
183 | $requiredPermissions = GetRequiredPermissions -applicationDisplayName "Microsoft Graph" `
184 | -requiredDelegatedPermissions "User.Read|User.ReadBasic.All";
185 |
186 | $requiredResourcesAccess.Add($requiredPermissions)
187 |
188 |
189 | Set-AzureADApplication -ObjectId $clientAadApplication.ObjectId -RequiredResourceAccess $requiredResourcesAccess
190 | Write-Host "Granted permissions."
191 |
192 | # Update config file for 'client'
193 | $configFile = $pwd.Path + "\..\device-code-flow-console\appsettings.json"
194 | Write-Host "Updating the sample code ($configFile)"
195 | $dictionary = @{ "ClientId" = $clientAadApplication.AppId };
196 | UpdateTextFile -configFilePath $configFile -dictionary $dictionary
197 |
198 | Add-Content -Value "
" -Path createdApps.html
199 | }
200 |
201 | # Pre-requisites
202 | if ((Get-Module -ListAvailable -Name "AzureAD") -eq $null) {
203 | Install-Module "AzureAD" -Scope CurrentUser
204 | }
205 | Import-Module AzureAD
206 |
207 | # Run interactively (will ask you for the tenant ID)
208 | ConfigureApplications -Credential $Credential -tenantId $TenantId
--------------------------------------------------------------------------------
/AppCreationScripts/sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "Sample": {
3 | "Title": "Invoking an API protected by Azure AD from a text-only device",
4 | "Level": 200,
5 | "Client": ".NET Core 2.1 console app",
6 | "Service": "Microsoft Graph",
7 | "RepositoryUrl": "active-directory-dotnetcore-devicecodeflow-v2",
8 | "Endpoint": "AAD v2.0"
9 | },
10 |
11 | /*
12 | This section describes the Azure AD Applications to configure, and their dependencies
13 | */
14 | "AADApps": [
15 | {
16 | "Id": "client",
17 | "Name": "active-directory-dotnet-deviceprofile",
18 | "Kind": "Desktop",
19 | "UsesROPCOrIWA": true,
20 | "Audience": "AzureADMultipleOrgs",
21 | "RequiredResourcesAccess": [
22 | {
23 | "Resource": "Microsoft Graph",
24 | "DelegatedPermissions": [ "User.Read", "User.ReadBasic.All" ]
25 | }
26 | ]
27 | }
28 | ],
29 |
30 | /*
31 | This section describes how to update the code in configuration files from the apps coordinates, once the apps
32 | are created in Azure AD.
33 | Each section describes a configuration file, for one of the apps, it's type (XML, JSon, plain text), its location
34 | with respect to the root of the sample, and the mappping (which string in the config file is mapped to which value
35 | */
36 | "CodeConfiguration": [
37 | {
38 | "App": "client",
39 | "SettingKind": "JSon",
40 | "SettingFile": "\\..\\device-code-flow-console\\appsettings.json",
41 | "Mappings": [
42 | {
43 | "key": "ClientId",
44 | "value": ".AppId"
45 | }
46 | ]
47 | }
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [project-title] Changelog
2 |
3 |
4 | # x.y.z (yyyy-mm-dd)
5 |
6 | *Features*
7 | * ...
8 |
9 | *Bug Fixes*
10 | * ...
11 |
12 | *Breaking Changes*
13 | * ...
14 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to [project-title]
2 |
3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a
4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
5 | the rights to use your contribution. For details, visit https://cla.microsoft.com.
6 |
7 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
8 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
9 | provided by the bot. You will only need to do this once across all repos using our CLA.
10 |
11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
14 |
15 | - [Code of Conduct](#coc)
16 | - [Issues and Bugs](#issue)
17 | - [Feature Requests](#feature)
18 | - [Submission Guidelines](#submit)
19 |
20 | ## Code of Conduct
21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
22 |
23 | ## Found an Issue?
24 | If you find a bug in the source code or a mistake in the documentation, you can help us by
25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can
26 | [submit a Pull Request](#submit-pr) with a fix.
27 |
28 | ## Want a Feature?
29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub
30 | Repository. If you would like to *implement* a new feature, please submit an issue with
31 | a proposal for your work first, to be sure that we can use it.
32 |
33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr).
34 |
35 | ## Submission Guidelines
36 |
37 | ### Submitting an Issue
38 | Before you submit an issue, search the archive, maybe your question was already answered.
39 |
40 | If your issue appears to be a bug, and hasn't been reported, open a new issue.
41 | Help us to maximize the effort we can spend fixing issues and adding new
42 | features, by not reporting duplicate issues. Providing the following information will increase the
43 | chances of your issue being dealt with quickly:
44 |
45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps
46 | * **Version** - what version is affected (e.g. 0.1.2)
47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you
48 | * **Browsers and Operating System** - is this a problem with all browsers?
49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps
50 | * **Related Issues** - has a similar issue been reported before?
51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be
52 | causing the problem (line of code or commit)
53 |
54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new].
55 |
56 | ### Submitting a Pull Request (PR)
57 | Before you submit your Pull Request (PR) consider the following guidelines:
58 |
59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR
60 | that relates to your submission. You don't want to duplicate effort.
61 |
62 | * Make your changes in a new git fork:
63 |
64 | * Commit your changes using a descriptive commit message
65 | * Push your fork to GitHub:
66 | * In GitHub, create a pull request
67 | * If we suggest changes then:
68 | * Make the required updates.
69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request):
70 |
71 | ```shell
72 | git rebase main -i
73 | git push -f
74 | ```
75 |
76 | That's it! Thank you for your contribution!
77 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
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
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | page_type: sample
3 | languages:
4 | - csharp
5 | - powershell
6 | products:
7 | - azure-active-directory
8 | description: "This sample demonstrates how to leverage MSAL.NET from apps that do not have the capability of offering an interactive authentication experience."
9 | urlFragment: invoke-protected-api-text
10 | ---
11 |
12 | # Invoking an API protected by Microsoft identity platform from a text-only device
13 |
14 | [](https://identitydivision.visualstudio.com/IDDP/_build/latest?definitionId=684)
15 |
16 | > We have renamed the default branch to main. To rename your local repo follow the directions [here](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-branches-in-your-repository/renaming-a-branch#updating-a-local-clone-after-a-branch-name-changes).
17 |
18 | ## About this sample
19 |
20 | ### Overview
21 |
22 | This sample demonstrates how to leverage MSAL.NET from apps that **do not have the capability of offering an interactive authentication experience**. It enables these apps to:
23 |
24 | - authenticate a user
25 | - and call to a web API (in this case, the [Microsoft Graph](https://graph.microsoft.com))
26 |
27 | The sample uses the OAuth2 **device code flow**. The app is built entirely on .NET Core, hence it can be ran as-is on Windows (including Nano Server), OSX, and Linux machines.
28 |
29 | To emulate a device not capable of showing UX, the sample is packaged as a .NET Core console application.
30 | The application signs users in with Azure Active Directory (Azure AD), using the Microsoft Authentication Library for .NET (MSAL.NET) to obtain a JWT access token through the OAuth 2.0 protocol. The access token is then used to call the Microsoft Graph API to obtain information about the user who signed-in. The sample is structured so that you can call your own API
31 |
32 | 
33 |
34 | If you would like to get started immediately, skip this section and jump to *How To Run The Sample*.
35 |
36 | ### Scenario
37 |
38 | The application obtains tokens through a two steps process especially designed for devices and operating systems that cannot display any UX. Examples of such applications are applications running on iOT, or Command-Line tools (CLI). The idea is that:
39 |
40 | 1. whenever a user authentication is required, the command-line app provides a code and asks the user to use another device (such as an internet-connected smartphone) to navigate to [https://microsoft.com/devicelogin](https://microsoft.com/devicelogin), where the user will be prompted to enter the code. That done, the web page will lead the user through a normal authentication experience, including consent prompts and multi factor authentication if necessary.
41 |
42 | 
43 |
44 | 2. Upon successful authentication, the command-line app will receive the required tokens through a back channel and will use it to perform the web API calls it needs. In this case, the sample displays information about the user who signed-in and their manager.
45 |
46 | - Developers who wish to gain good familiarity of programming for Microsoft Graph are advised to go through the [An introduction to Microsoft Graph for developers](https://www.youtube.com/watch?v=EBbnpFdB92A) recorded session.
47 |
48 | ## About the code
49 |
50 | The code for handling the token acquisition process is simple, as it boils down to calling the `AcquireTokenWithDeviceCodeAsync` method of `PublicClientApplication` to which you pass a callback that will display information to the user about where they should navigate to, and which code to enter to initiate a sign-in. See the `GetTokenForWebApiUsingDeviceCodeFlowAsync` method in `device-code-flow-console\PublicAppUsingDeviceCodeFlow.cs`.
51 |
52 | ```CSharp
53 | async Task GetTokenForWebApiUsingDeviceCodeFlowAsync()
54 |
55 | AuthenticationResult result;
56 | try
57 | {
58 | result = await app.AcquireTokenWithDeviceCodeAsync(Scopes,
59 | deviceCodeCallback =>
60 | {
61 | Console.WriteLine(deviceCodeCallback.Message);
62 | return Task.FromResult(0);
63 | });
64 | }
65 |
66 | ...
67 | // error handling omitted here (see sample for details)
68 | return result;
69 | }
70 | ```
71 |
72 | ## How to run this sample
73 |
74 | To run this sample, you'll need:
75 |
76 | - [Visual Studio 2019](https://aka.ms/vsdownload) or just the [.NET Core SDK](https://www.microsoft.com/net/learn/get-started). You will need the .NET Core 3.1 SDK. If you don't have it already, you can download from [Visual Studio SDKs](https://dotnet.microsoft.com/download/visual-studio-sdks)
77 | - An Internet connection
78 | - A Windows machine (necessary if you want to run the app on Windows)
79 | - An OS X machine (necessary if you want to run the app on Mac)
80 | - A Linux machine (necessary if you want to run the app on Linux)
81 | - An Azure Active Directory (Azure AD) tenant. For more information on how to get an Azure AD tenant, see [How to get an Azure AD tenant](https://azure.microsoft.com/en-us/documentation/articles/active-directory-howto-tenant/)
82 | - A user account in your Azure AD tenant. This sample will not work with a Microsoft account (formerly Windows Live account). Therefore, if you signed in to the [Azure portal](https://portal.azure.com) with a Microsoft account and have never created a user account in your directory before, you need to do that now.
83 |
84 | ### Step 1: Clone or download this repository
85 |
86 | From your shell or command line:
87 |
88 | ```Shell
89 | git clone https://github.com/Azure-Samples/active-directory-dotnetcore-devicecodeflow-v2.git
90 | ```
91 |
92 | or download and extract the repository .zip file.
93 |
94 | > Given that the name of the sample is pretty long, and so are the name of the referenced NuGet packages, you might want to clone it in a folder close to the root of your hard drive, to avoid file size limitations on Windows.
95 |
96 | ### Step 2: Setup .NET Core
97 |
98 | The .NET Core [documentation pages](https://www.microsoft.com/net/learn/get-started) provide step by step instructions for installing .NET Core (the .NET Execution Environment) for your platform of choice.
99 |
100 | ### Step 3: Run the sample
101 |
102 | #### If you prefer to use Visual Studio
103 |
104 | Open the solution in Visual Studio, restore the NuGet packages, select the project, and start it in the debugger.
105 |
106 | #### (otherwise) on any platform
107 |
108 | Open a terminal and navigate to the project folder (`device-code-flow-console`).
109 | Restore the packages with the following command:
110 |
111 | ```PowerShell
112 | dotnet restore
113 | ```
114 |
115 | Launch the app by entering the following command:
116 |
117 | ```PowerShell
118 | dotnet run
119 | ```
120 |
121 | #### Operating the sample
122 |
123 | When you run the sample, you will be presented with a prompt telling you
124 |
125 | > To sign in, use a web browser to open the page https://microsoft.com/devicelogin. Enter the code B7D3SVXHV to authenticate.
126 |
127 | Then:
128 |
129 | 1. Open a browser on any device. For instance, the browser can be on the computer on which you are running the sample, or even your smartphone. Then navigate, as instructed, to [https://microsoft.com/devicelogin](https://microsoft.com/devicelogin)
130 |
131 | 2. Once there, type in the code provided by the app (in this sample, I am typing `B7D3SVXHV`) and hit enter. The web page will proceed to prompt you for authentication: please authenticate as a user (native or guest) in the tenant that you specified in the application. Note that, thanks to the fact that you are using an external browser or a different, browser capable device, you can authenticate without restrictions: for example, if your tenant requires you to authenticate using MFA, you are able to do so. That experience would not have been possible if you had to drive the authentication operations exclusively in the console.
132 | 3. Once you successfully authenticate, go back to the console app. You'll see that the app has now access to the token it needs to query the Microsoft Graph API and display information about the signed-in user.
133 |
134 | ### Optional: configure the sample as an app in your directory tenant
135 |
136 | The instructions so far leveraged the Azure AD entry for the app in a Microsoft test tenant: given that the app is multi-tenant, anybody can run the sample against that app entry.
137 | To register your project in your own Azure AD tenant, you can find instructions to manually provision the sample in your own tenant, so that you can exercise complete control on the app settings and behavior.
138 |
139 | - either follow the manual steps
140 | - or use PowerShell scripts that:
141 | - **automatically** creates the Azure AD applications and related objects (passwords, permissions, dependencies) for you
142 | - modify the Visual Studio projects' configuration files.
143 |
144 | If you want to use this automation:
145 | 1. On Windows run PowerShell and navigate to the root of the cloned directory
146 | 1. In PowerShell run:
147 | ```PowerShell
148 | Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force
149 | ```
150 | 1. Run the script to create your Azure AD application and configure the code of the sample application accordingly.
151 | ```PowerShell
152 | .\AppCreationScripts\Configure.ps1
153 | ```
154 | > Other ways of running the scripts are described in [App Creation Scripts](./AppCreationScripts/AppCreationScripts.md)
155 |
156 | 1. Open the Visual Studio solution and click start
157 |
158 | If you don't want to use this automation, follow the steps below
159 |
160 | #### First step: choose the Azure AD tenant where you want to create your applications
161 |
162 | As a first step you'll need to:
163 |
164 | 1. Sign in to the [Azure portal](https://portal.azure.com) using either a work or school account or a personal Microsoft account.
165 | 1. If your account is present in more than one Azure AD tenant, select `Directory + Subscription` at the top right corner in the menu on top of the page, and switch your portal session to the desired Azure AD tenant.
166 | 1. In the left-hand navigation pane, select the **Azure Active Directory** service, and then select **App registrations**.
167 |
168 | > In the next steps, you might need the tenant name (or directory name) or the tenant ID (or directory ID). These are presented in the **Properties**
169 | of the Azure Active Directory window respectively as *Name* and *Directory ID*
170 |
171 | #### Register the client app (active-directory-dotnet-deviceprofile)
172 |
173 | 1. In **App registrations** page, select **New registration**.
174 | 1. When the **Register an application page** appears, enter your application's registration information:
175 | - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `active-directory-dotnet-deviceprofile`.
176 | - In the **Supported account types** section, select **Accounts in any organizational directory**.
177 | 1. Select **Register** to create the application.
178 | 1. On the app **Overview** page, find the **Application (client) ID** value and record it for later. You'll need it to configure the Visual Studio configuration file for this project.
179 | 1. In the list of pages for the app, select **Manifest**, and:
180 | - In the manifest editor, set the ``allowPublicClient`` property to **true**
181 | - Select **Save** in the bar above the manifest editor.
182 | 1. In the list of pages for the app, select **API permissions**
183 | - Click the **Add a permission** button and then,
184 | - Ensure that the **Microsoft APIs** tab is selected
185 | - In the *Commonly used Microsoft APIs* section, click on **Microsoft Graph**
186 | - In the **Delegated permissions** section, ensure that the right permissions are checked: **User.Read**, **User.ReadBasic.All**. Use the search box if necessary.
187 | - Select the **Add permissions** button
188 |
189 | #### Configure the sample to use your Azure AD tenant
190 |
191 | In the steps below, ClientID is the same as Application ID or AppId.
192 |
193 | Open the solution in Visual Studio to configure the projects
194 |
195 | #### Configure the client project
196 |
197 | > Note: if you used the setup scripts, the changes below will have been applied for you
198 |
199 | 1. Open the `device-code-flow-console\appsettings.json` file
200 | 1. Find the app key `ClientId` and replace the existing value with the application ID (clientId) of the `active-directory-dotnet-deviceprofile` application copied from the Azure portal.
201 | 1. (Optional) If you created a single tenant application, find the line where `TenantId` is set and replace the existing value with your tenant ID.
202 |
203 | ## Community Help and Support
204 |
205 | Use [Stack Overflow](http://stackoverflow.com/questions/tagged/msal) to get support from the community.
206 | Ask your questions on Stack Overflow first and browse existing issues to see if someone has asked your question before.
207 | Make sure that your questions or comments are tagged with [`msal` `dotnet`].
208 |
209 | If you find a bug in the sample, please raise the issue on [GitHub Issues](../../issues).
210 |
211 | To provide a recommendation, visit the following [User Voice page](https://feedback.azure.com/forums/169401-azure-active-directory).
212 |
213 | ## Contributing
214 |
215 | If you'd like to contribute to this sample, see [CONTRIBUTING.MD](/CONTRIBUTING.md).
216 |
217 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
218 |
219 | ## More information
220 |
221 | For more information, see MSAL.NET's conceptual documentation:
222 |
223 | - [Device Code Flow for devices without a Web browser](https://aka.ms/msal-net-device-code-flow)
224 | - [Customizing Token cache serialization](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/token-cache-serialization) (was not done in this sample, but you might want to add a serialized cache)
225 | - [Quickstart: Register an application with the Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app)
226 | - [Quickstart: Configure a client application to access web APIs](https://docs.microsoft.com/azure/active-directory/develop/quickstart-configure-app-access-web-apis)
227 |
228 | - [Understanding Azure AD application consent experiences](https://docs.microsoft.com/en-us/azure/active-directory/develop/application-consent-experience)
229 | - [Understand user and admin consent](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-convert-app-to-be-multi-tenant#understand-user-and-admin-consent)
230 | - [Application and service principal objects in Azure Active Directory](https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals)
231 |
232 | For more information about the Microsoft identity platform, see:
233 | - [https://aka.ms/aadv2](https://aka.ms/aadv2)
234 |
--------------------------------------------------------------------------------
/ReadmeFiles/Topology.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/active-directory-dotnetcore-devicecodeflow-v2/90aa403705c574e2fd30ab0e7ec3973f291fce3b/ReadmeFiles/Topology.png
--------------------------------------------------------------------------------
/ReadmeFiles/deviceCodeFlow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/active-directory-dotnetcore-devicecodeflow-v2/90aa403705c574e2fd30ab0e7ec3973f291fce3b/ReadmeFiles/deviceCodeFlow.png
--------------------------------------------------------------------------------
/device-code-flow-console.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.28010.2041
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "device-code-flow-console", "device-code-flow-console\device-code-flow-console.csproj", "{2EA73CD2-D137-4C7F-94FA-314D4998E9B1}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {2EA73CD2-D137-4C7F-94FA-314D4998E9B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {2EA73CD2-D137-4C7F-94FA-314D4998E9B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {2EA73CD2-D137-4C7F-94FA-314D4998E9B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {2EA73CD2-D137-4C7F-94FA-314D4998E9B1}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {3EF5DD5E-765A-475C-A5AB-7B841BD66C54}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/device-code-flow-console/MyInformation.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using Microsoft.Identity.Client;
5 | using Newtonsoft.Json.Linq;
6 | using System;
7 | using System.Linq;
8 | using System.Net.Http;
9 | using System.Threading.Tasks;
10 |
11 | namespace device_code_flow_console
12 | {
13 | ///
14 | /// MyInformation
15 | ///
16 | public class MyInformation
17 | {
18 | ///
19 | /// MyInformation ctor
20 | ///
21 | ///
22 | ///
23 | ///
24 | public MyInformation(IPublicClientApplication app, HttpClient client, string microsoftGraphBaseEndpoint)
25 | {
26 | tokenAcquisitionHelper = new PublicAppUsingDeviceCodeFlow(app);
27 | protectedApiCallHelper = new ProtectedApiCallHelper(client);
28 | this.MicrosoftGraphBaseEndpoint = microsoftGraphBaseEndpoint;
29 | }
30 |
31 | ///
32 | /// tokenAcquisitionHelper
33 | ///
34 | protected PublicAppUsingDeviceCodeFlow tokenAcquisitionHelper;
35 |
36 | ///
37 | /// protectedApiCallHelper
38 | ///
39 | protected ProtectedApiCallHelper protectedApiCallHelper;
40 |
41 | ///
42 | /// Scopes to request access to the protected web API (here Microsoft Graph)
43 | ///
44 | private static string[] Scopes { get; set; } = new string[] { "User.Read", "User.ReadBasic.All"};
45 |
46 | ///
47 | /// Base endpoint for Microsoft Graph
48 | ///
49 | private string MicrosoftGraphBaseEndpoint { get; set; }
50 |
51 | ///
52 | /// URLs of the protected web APIs to call (here Microsoft Graph endpoints)
53 | ///
54 | private string WebApiUrlMe { get { return $"{MicrosoftGraphBaseEndpoint}/v1.0/me"; } }
55 | private string WebApiUrlMyManager { get { return $"{MicrosoftGraphBaseEndpoint}/v1.0/me/manager"; } }
56 |
57 | ///
58 | /// Calls the web API and displays its information
59 | ///
60 | ///
61 | public async Task DisplayMeAndMyManagerAsync()
62 | {
63 | AuthenticationResult authenticationResult = await tokenAcquisitionHelper.AcquireATokenFromCacheOrDeviceCodeFlowAsync(Scopes).ConfigureAwait(false);
64 | if (authenticationResult != null)
65 | {
66 | DisplaySignedInAccount(authenticationResult.Account);
67 |
68 | string accessToken = authenticationResult.AccessToken;
69 | await CallWebApiAndDisplayResultAsync(WebApiUrlMe, accessToken, "Me").ConfigureAwait(false);
70 | await CallWebApiAndDisplayResultAsync(WebApiUrlMyManager, accessToken, "My manager").ConfigureAwait(false);
71 | }
72 | }
73 |
74 | private static void DisplaySignedInAccount(IAccount account)
75 | {
76 | Console.ForegroundColor = ConsoleColor.Green;
77 | Console.WriteLine($"{account.Username} successfully signed-in");
78 | }
79 |
80 | private async Task CallWebApiAndDisplayResultAsync(string url, string accessToken, string title)
81 | {
82 | Console.ForegroundColor = ConsoleColor.White;
83 | Console.WriteLine(title);
84 | Console.ResetColor();
85 | await protectedApiCallHelper.CallWebApiAndProcessResultAsync(url, accessToken, Display).ConfigureAwait(false);
86 | Console.WriteLine();
87 | }
88 |
89 | ///
90 | /// Display the result of the web API call
91 | ///
92 | /// Object to display
93 | private static void Display(JObject result)
94 | {
95 | foreach (JProperty child in result.Properties().Where(p => !p.Name.StartsWith('@')))
96 | {
97 | Console.WriteLine($"{child.Name} = {child.Value}");
98 | }
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/device-code-flow-console/Program.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using Microsoft.Identity.Client;
5 | using System;
6 | using System.Net.Http;
7 | using System.Threading.Tasks;
8 |
9 | namespace device_code_flow_console
10 | {
11 | ///
12 | /// This sample signs-in a user in a two steps process:
13 | /// - it displays a URL and a code, and asks the user to navigate to the URL in a Web browser, and enter the code
14 | /// - then the user signs-in (and goes through multiple factor authentication if needed)
15 | /// and the sample displays information about the user by calling the Microsoft Graph in the name of the signed-in user
16 | ///
17 | /// It uses the Device code flow, which is normally used for devices which don't have a Web browser (which is the case for a
18 | /// .NET Core app, iOT, etc ...)
19 | ///
20 | /// For more information see https://aka.ms/msal-net-device-code-flow
21 | ///
22 | class Program
23 | {
24 | static void Main(string[] args)
25 | {
26 | try
27 | {
28 | RunAsync().GetAwaiter().GetResult();
29 | }
30 | catch(Exception ex)
31 | {
32 | Console.ForegroundColor = ConsoleColor.Red;
33 | Console.WriteLine(ex.Message);
34 | Console.ResetColor();
35 | }
36 |
37 | Console.WriteLine("Press any key to exit");
38 | Console.ReadKey();
39 | }
40 |
41 | private static async Task RunAsync()
42 | {
43 | SampleConfiguration config = SampleConfiguration.ReadFromJsonFile("appsettings.json");
44 | var appConfig = config.PublicClientApplicationOptions;
45 | var app = PublicClientApplicationBuilder.CreateWithApplicationOptions(appConfig)
46 | .Build();
47 | var httpClient = new HttpClient();
48 |
49 | MyInformation myInformation = new MyInformation(app, httpClient, config.MicrosoftGraphBaseEndpoint);
50 | await myInformation.DisplayMeAndMyManagerAsync().ConfigureAwait(false);
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/device-code-flow-console/ProtectedApiCallHelper.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using Newtonsoft.Json;
5 | using Newtonsoft.Json.Linq;
6 | using System;
7 | using System.Linq;
8 | using System.Net.Http;
9 | using System.Net.Http.Headers;
10 | using System.Threading.Tasks;
11 |
12 | namespace device_code_flow_console
13 | {
14 | ///
15 | /// Helper class to call a protected API and process its result
16 | ///
17 | public class ProtectedApiCallHelper
18 | {
19 | ///
20 | /// Constructor
21 | ///
22 | /// HttpClient used to call the protected API
23 | public ProtectedApiCallHelper(HttpClient httpClient)
24 | {
25 | HttpClient = httpClient;
26 | }
27 |
28 | ///
29 | /// HttpClient
30 | ///
31 | protected HttpClient HttpClient { get; private set; }
32 |
33 | ///
34 | /// Calls the protected web API and processes the result
35 | ///
36 | /// URL of the web API to call (supposed to return Json)
37 | /// Access token used as a bearer security token to call the web API
38 | /// Callback used to process the result of the call to the web API
39 | public async Task CallWebApiAndProcessResultAsync(string webApiUrl, string accessToken, Action processResult)
40 | {
41 | if (!string.IsNullOrEmpty(accessToken))
42 | {
43 | var defaultRequetHeaders = HttpClient.DefaultRequestHeaders;
44 | if (defaultRequetHeaders.Accept == null || !defaultRequetHeaders.Accept.Any(m => m.MediaType == "application/json"))
45 | {
46 | HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
47 | }
48 | defaultRequetHeaders.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
49 |
50 | HttpResponseMessage response = await HttpClient.GetAsync(webApiUrl).ConfigureAwait(false);
51 | if (response.IsSuccessStatusCode)
52 | {
53 | string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
54 | JObject result = JsonConvert.DeserializeObject(json) as JObject;
55 | Console.ForegroundColor = ConsoleColor.Gray;
56 | processResult(result);
57 | }
58 | else
59 | {
60 | Console.ForegroundColor = ConsoleColor.Red;
61 | string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
62 |
63 | if (!content.Contains("Resource 'manager' does not exist"))
64 | {
65 | Console.WriteLine($"Failed to call the Web Api: {response.StatusCode}");
66 | Console.WriteLine($"Content: {content}");
67 | }
68 | else
69 | {
70 | Console.ForegroundColor = ConsoleColor.Gray;
71 | Console.WriteLine("No manager");
72 | }
73 | }
74 | Console.ResetColor();
75 | }
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/device-code-flow-console/PublicAppUsingDeviceCodeFlow.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using Microsoft.Identity.Client;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using System.Threading.Tasks;
9 |
10 | namespace device_code_flow_console
11 | {
12 | ///
13 | /// Security token provider using the Device Code flow
14 | ///
15 | public class PublicAppUsingDeviceCodeFlow
16 | {
17 | ///
18 | /// Constructor of a public application leveraging Device Code Flow to sign-in a user
19 | ///
20 | /// MSAL.NET Public client application
21 | ///
22 | /// For more information see https://aka.ms/msal-net-device-code-flow
23 | ///
24 | public PublicAppUsingDeviceCodeFlow(IPublicClientApplication app)
25 | {
26 | App = app;
27 | }
28 |
29 | ///
30 | /// IPublicClientApplication
31 | ///
32 | protected IPublicClientApplication App { get; private set; }
33 |
34 | ///
35 | /// Acquires a token from the token cache, or device code flow
36 | ///
37 | /// An AuthenticationResult if the user successfully signed-in, or otherwise null
38 | public async Task AcquireATokenFromCacheOrDeviceCodeFlowAsync(IEnumerable scopes)
39 | {
40 | AuthenticationResult result = null;
41 | var accounts = await App.GetAccountsAsync().ConfigureAwait(false);
42 |
43 | if (accounts.Any())
44 | {
45 | try
46 | {
47 | // Attempt to get a token from the cache (or refresh it silently if needed)
48 | result = await App.AcquireTokenSilent(scopes, accounts.FirstOrDefault())
49 | .ExecuteAsync().ConfigureAwait(false);
50 | }
51 | catch (MsalUiRequiredException)
52 | {
53 | }
54 | }
55 |
56 | // Cache empty or no token for account in the cache, attempt by device code flow
57 | if (result == null)
58 | {
59 | result = await GetTokenForWebApiUsingDeviceCodeFlowAsync(scopes).ConfigureAwait(false);
60 | }
61 |
62 | return result;
63 | }
64 |
65 | ///
66 | /// Gets an access token so that the application accesses the web api in the name of the user
67 | /// who signs-in on a separate device
68 | ///
69 | /// An authentication result, or null if the user canceled sign-in, or did not sign-in on a separate device
70 | /// after a timeout (15 mins)
71 | private async Task GetTokenForWebApiUsingDeviceCodeFlowAsync(IEnumerable scopes)
72 | {
73 | AuthenticationResult result;
74 | try
75 | {
76 | result = await App.AcquireTokenWithDeviceCode(scopes,
77 | deviceCodeCallback =>
78 | {
79 | // This will print the message on the console which tells the user where to go sign-in using
80 | // a separate browser and the code to enter once they sign in.
81 | // The AcquireTokenWithDeviceCodeAsync() method will poll the server after firing this
82 | // device code callback to look for the successful login of the user via that browser.
83 | // This background polling (whose interval and timeout data is also provided as fields in the
84 | // deviceCodeCallback class) will occur until:
85 | // * The user has successfully logged in via browser and entered the proper code
86 | // * The timeout specified by the server for the lifetime of this code (typically ~15 minutes) has been reached
87 | // * The developing application calls the Cancel() method on a CancellationToken sent into the method.
88 | // If this occurs, an OperationCanceledException will be thrown (see catch below for more details).
89 | Console.WriteLine(deviceCodeCallback.Message);
90 | return Task.FromResult(0);
91 | }).ExecuteAsync().ConfigureAwait(false);
92 | }
93 | catch (MsalServiceException ex)
94 | {
95 | // Kind of errors you could have (in errorCode and ex.Message)
96 | string errorCode = ex.ErrorCode;
97 |
98 | // AADSTS50059: No tenant-identifying information found in either the request or implied by any provided credentials.
99 | // Mitigation: as explained in the message from Azure AD, the authority needs to be tenanted. you have probably created
100 | // your public client application with the following authorities:
101 | // https://login.microsoftonline.com/common or https://login.microsoftonline.com/organizations
102 |
103 | // AADSTS90133: Device Code flow is not supported under /common or /consumers endpoint.
104 | // Mitigation: as explained in the message from Azure AD, the authority needs to be tenanted
105 |
106 | // AADSTS90002: Tenant not found. This may happen if there are
107 | // no active subscriptions for the tenant. Check with your subscription administrator.
108 | // Mitigation: if you have an active subscription for the tenant this might be that you have a typo in the
109 | // tenantId (GUID) or tenant domain name, update the
110 |
111 | // The issues above are typically programming / app configuration errors, they need to be fixed
112 | throw;
113 | }
114 | catch (OperationCanceledException)
115 | {
116 | // If you use an override with a CancellationToken, and call the Cancel() method on it, then this may be triggered
117 | // to indicate that the operation was cancelled.
118 | // See https://docs.microsoft.com/en-us/dotnet/standard/threading/cancellation-in-managed-threads
119 | // for more detailed information on how C# supports cancellation in managed threads.
120 | result = null;
121 | }
122 | catch (MsalClientException ex)
123 | {
124 | string errorCode = ex.ErrorCode;
125 |
126 | // Verification code expired before contacting the server
127 | // This exception will occur if the user does not manage to sign-in before a time out (15 mins) and the
128 | // call to `AcquireTokenWithDeviceCodeAsync` is not cancelled in between
129 | result = null;
130 | }
131 | return result;
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/device-code-flow-console/SampleConfiguration.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using Microsoft.Extensions.Configuration;
5 | using Microsoft.Identity.Client;
6 | using System.IO;
7 | using System.Reflection;
8 |
9 | namespace device_code_flow_console
10 | {
11 | ///
12 | /// Description of the configuration of an AzureAD public client application (desktop/mobile application). This should
13 | /// match the application registration done in the Azure portal
14 | ///
15 | public class SampleConfiguration
16 | {
17 | ///
18 | /// Authentication options
19 | ///
20 | public PublicClientApplicationOptions PublicClientApplicationOptions { get; set; }
21 |
22 | ///
23 | /// Base URL for Microsoft Graph (it varies depending on whether the application is ran
24 | /// in Microsoft Azure public clouds or national / sovereign clouds
25 | ///
26 | public string MicrosoftGraphBaseEndpoint { get; set; }
27 |
28 | ///
29 | /// Reads the configuration from a json file
30 | ///
31 | /// Path to the configuration json file
32 | /// SampleConfiguration as read from the json file
33 | public static SampleConfiguration ReadFromJsonFile(string path)
34 | {
35 | // .NET configuration
36 | IConfigurationRoot Configuration;
37 |
38 | var builder = new ConfigurationBuilder()
39 | .SetBasePath(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location))
40 | .AddJsonFile(path);
41 |
42 | Configuration = builder.Build();
43 | // Read the auth and graph endpoint config
44 | SampleConfiguration config = new SampleConfiguration()
45 | {
46 | PublicClientApplicationOptions = new PublicClientApplicationOptions()
47 | };
48 | Configuration.Bind("Authentication", config.PublicClientApplicationOptions);
49 | config.MicrosoftGraphBaseEndpoint = Configuration.GetValue("WebAPI:MicrosoftGraphBaseEndpoint");
50 | return config;
51 | }
52 | }
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/device-code-flow-console/appsettings.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/active-directory-dotnetcore-devicecodeflow-v2/90aa403705c574e2fd30ab0e7ec3973f291fce3b/device-code-flow-console/appsettings.json
--------------------------------------------------------------------------------
/device-code-flow-console/device-code-flow-console.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | netcoreapp3.1
6 | device_code_flow_console
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | PreserveNewest
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------