├── .deployment ├── .editorconfig ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── Directory.Build.props ├── LICENSE ├── MsalNetExt.sln ├── README.md ├── azure-pipelines.yml ├── build ├── MSAL.snk ├── SolutionWideAnalyzerConfig.ruleset ├── credscan-exclusion.json └── tsaConfig.json ├── docs └── keyring_fallback_proposal.md ├── release_build.yaml ├── sample └── ManualTestApp │ ├── Config.cs │ ├── ExampleUsage.cs │ ├── ManualTestApp.csproj │ └── Program.cs ├── src ├── Microsoft.Identity.Client.Extensions.Adal │ ├── AdalCache.cs │ ├── AdalCacheStorage.cs │ ├── InternalsVisibleTo.cs │ └── Microsoft.Identity.Client.Extensions.Adal.csproj ├── Microsoft.Identity.Client.Extensions.Msal │ ├── Accessors │ │ ├── DpApiEncryptedFileAccessor.cs │ │ ├── FileAccessor.cs │ │ ├── FileWithPermissions.cs │ │ ├── ICacheAccessor.cs │ │ ├── LinuxKeyRingAccessor.cs │ │ └── MacKeyChainAccessor.cs │ ├── Cache Architecture.png │ ├── CacheChangedEventArgs.cs │ ├── FileIOWithRetries.cs │ ├── InternalsVisibleTo.cs │ ├── Microsoft.Identity.Client.Extensions.Msal.csproj │ ├── MsalCacheHelper.cs │ ├── MsalCachePersistenceException.cs │ ├── Properties │ │ └── InternalsVisibleTo.cs │ └── Storage.cs └── Shared │ ├── Constants.cs │ ├── CrossPlatLock.cs │ ├── EnvUtils.cs │ ├── InteropException.cs │ ├── Linux │ ├── GError.cs │ ├── Libsecret.cs │ └── LinuxNativeMethods.cs │ ├── Mac │ ├── CoreFoundation.cs │ ├── LibSystem.cs │ ├── MacKeyChain.cs │ ├── MacOSKeychainCredential.cs │ └── SecurityFramework.cs │ ├── Shared.projitems │ ├── Shared.shproj │ ├── SharedUtilities.cs │ ├── StorageCreationProperties.cs │ ├── StorageCreationPropertiesBuilder.cs │ └── TraceSourceLogger.cs └── tests ├── Automation.TestApp ├── Automation.TestApp.csproj └── Program.cs ├── Directory.Build.props ├── FileLockApp ├── FileLockApp.csproj ├── GlobalSuppressions.cs └── Program.cs ├── KeyChainTestApp ├── Program.cs ├── StorageTestApp.csproj └── readme.md ├── Microsoft.Identity.Client.Extensions.Adal.UnitTests ├── AdalCacheStorageTests.cs ├── AdalCacheTests.cs ├── Microsoft.Identity.Client.Extensions.Adal.UnitTests.csproj └── RunOnPlatformAttribute.cs └── Microsoft.Identity.Client.Extensions.Msal.UnitTests ├── AssertException.cs ├── CrossPlatLockTests.cs ├── FileIOWithRetriesTests.cs ├── IntegrationTests.cs ├── MacKeyChainTests.cs ├── Microsoft.Identity.Client.Extensions.Msal.UnitTests.csproj ├── MockTokenCache.cs ├── MsalCacheHelperTests.cs ├── MsalCacheStorageIntegrationTests.cs ├── MsalCacheStorageTests.cs ├── ResourceHelper.cs ├── Resources ├── token_cache_adfs.json └── token_cache_one_acc_seed.json ├── RunOnPlatformAttribute.cs ├── TestCategories.cs ├── TestHelper.cs └── TraceStringListener.cs /.deployment: -------------------------------------------------------------------------------- 1 | [config] 2 | project = tests/WebAppTestWithAzureSDK/WebAppTestWithAzureSDK.csproj -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/microsoft-authentication-extensions-for-dotnet/f8f43abe5462786198517d416dc910dc82b3ed03/.editorconfig -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | 9 | { 10 | "name": ".NET Core Launch (console)", 11 | "type": "coreclr", 12 | "request": "launch", 13 | "preLaunchTask": "build", 14 | // If you have changed target frameworks, make sure to update the program path. 15 | //"program": "${workspaceFolder}/sample/ManualTestApp/bin/Debug/netcoreapp3.0/ManualTestApp.dll", 16 | "program": "${workspaceFolder}/tests/KeyChainTestApp/bin/Debug/netcoreapp3.1/StorageTestApp.dll", 17 | "args": [], 18 | "cwd": "${workspaceFolder}/tests/KeyChainTestApp", 19 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 20 | "console": "integratedTerminal", 21 | "stopAtEntry": false 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "dotnet", 9 | "type": "shell", 10 | "args": [ 11 | "build", 12 | // Ask dotnet build to generate full paths for file names. 13 | "/property:GenerateFullPaths=true", 14 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel 15 | "/consoleloggerparameters:NoSummary" 16 | ], 17 | "group": "build", 18 | "presentation": { 19 | "reveal": "silent" 20 | }, 21 | "problemMatcher": "$msCompile" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | True 4 | 5 | true 6 | $(MSBuildThisFileDirectory)/build/MSAL.snk 7 | true 8 | true 9 | true 10 | 11 | net45 12 | netstandard2.0 13 | netcoreapp3.1 14 | 15 | $(TargetFrameworkNetDesktop);$(TargetFrameworkNetStandard);$(TargetFrameworkNetCore) 16 | $(TargetFrameworkNetStandard);$(TargetFrameworkNetCore) 17 | $(TargetFrameworkNetStandard);$(TargetFrameworkNetCore) 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | >**Note** 2 | >This package continues to be maintained alongside [Microsoft.Identity.Client](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet) repository. [Microsoft.Identity.Client.Extensions.Msal](https://www.nuget.org/packages/Microsoft.Identity.Client.Extensions.Msal) will be versioned in sync with [Microsoft.Identity.Client](https://www.nuget.org/packages/Microsoft.Identity.Client). No breaking changes exist between version 2.x and version 4.x. 3 | > 4 | 5 | # MSAL token cache extension for public client applications 6 | 7 | A cross-platform token cache serialization mechanism - [see details on the Wiki](https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet/wiki/Cross-platform-Token-Cache). 8 | 9 | [![NuGet](https://img.shields.io/nuget/vpre/Microsoft.Identity.Client.Extensions.Msal.svg?style=flat-square&label=nuget&colorB=00b200)](https://www.nuget.org/packages/Microsoft.Identity.Client.Extensions.Msal/) 10 | 11 | [![Build Status](https://identitydivision.visualstudio.com/IDDP/_apis/build/status/CI/DotNet/MSAL%20YAML/Cache%20Ext/Extension%20CI-PR?branchName=master)](https://identitydivision.visualstudio.com/IDDP/_build/latest?definitionId=1071&branchName=master) 12 | 13 | > 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). 14 | 15 | ## Samples 16 | 17 | We aim to have all [MSAL public client samples](https://docs.microsoft.com/en-gb/azure/active-directory/develop/sample-v2-code#desktop-and-mobile-public-client-apps) use the extensions. 18 | 19 | # Contributing 20 | 21 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 22 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 23 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 24 | 25 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 26 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 27 | provided by the bot. You will only need to do this once across all repos using our CLA. 28 | 29 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 30 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 31 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 32 | 33 | 34 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - main 3 | 4 | pr: 5 | autoCancel: true 6 | branches: 7 | include: 8 | - main 9 | 10 | strategy: 11 | matrix: 12 | linux: 13 | imageName: 'ubuntu-latest' 14 | mac: 15 | imageName: 'macOS-latest' 16 | windows: 17 | imageName: 'windows-latest' 18 | 19 | pool: 20 | vmImage: $(imageName) 21 | 22 | steps: 23 | 24 | - task: UseDotNet@2 25 | displayName: 'Use .NET Core SDK 3.1.x' 26 | inputs: 27 | version: 3.1.x 28 | 29 | - task: UseDotNet@2 30 | displayName: 'Use .NET SDK 6.x' 31 | inputs: 32 | version: 6.x 33 | 34 | - task: DotNetCoreCLI@2 35 | displayName: Restore 36 | inputs: 37 | command: 'restore' 38 | 39 | - task: DotNetCoreCLI@2 40 | displayName: Build 41 | inputs: 42 | command: 'build' 43 | 44 | - task: DotNetCoreCLI@2 45 | displayName: 'Run unit tests' 46 | inputs: 47 | command: test 48 | projects: 'tests/**/*UnitTests*.csproj' 49 | arguments: '--no-build --no-restore --collect "Code coverage"' 50 | -------------------------------------------------------------------------------- /build/MSAL.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/microsoft-authentication-extensions-for-dotnet/f8f43abe5462786198517d416dc910dc82b3ed03/build/MSAL.snk -------------------------------------------------------------------------------- /build/SolutionWideAnalyzerConfig.ruleset: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /build/credscan-exclusion.json: -------------------------------------------------------------------------------- 1 | { 2 | "tool": "Credential Scanner", 3 | "suppressions": [ 4 | { 5 | "file": "token_cache_adfs.json", 6 | "_justification": "A few dummy test constants, not actual secrets." 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /build/tsaConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "codebaseName": "Unified .NET Core Extensions", 3 | "tools": [ 4 | "binskim", 5 | "credscan", 6 | "policheck" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /docs/keyring_fallback_proposal.md: -------------------------------------------------------------------------------- 1 | # Proposal for Linux plain-text file fallback 2 | 3 | **Context:** Today, the MSAL extension libraries (.net, java, python, javascript) store secrets in KeyRings via LibSecret. These components not available on all Linux distros and they cannot be started when connected via SSH. 4 | 5 | **Proposal:** Extensions are to provide a mechanism for products to detect if this secret storage is usable. If it is not, extensions are to write the token cache to an unecrypted file. It then becomes the reponsability of the Linux users to protect their files, using, for example, encrypted disks. 6 | l 7 | #### Current API 8 | 9 | ```csharp 10 | var storageProperties = 11 | // CacheFileName is used for storgae only on Win. On Mac and Linux, it's used 12 | // to produce an event, but contents are empty. Actual secrets are stored in KeyRing 13 | // KeyChain. 14 | new StorageCreationPropertiesBuilder(Config.CacheFileName, Config.CacheDir, Config.ClientId) 15 | .WithLinuxKeyring( 16 | Config.LinuxKeyRingSchema, 17 | Config.LinuxKeyRingCollection, 18 | Config.LinuxKeyRingLabel, 19 | Config.LinuxKeyRingAttr1, 20 | Config.LinuxKeyRingAttr2) 21 | .WithMacKeyChain( 22 | Config.KeyChainServiceName, 23 | Config.KeyChainAccountName) 24 | .Build(); 25 | 26 | MsalCacheHelper cacheHelper = MsalCacheHelper.Create(storageProperties); 27 | cacheHelper.RegisterCache(app.UserTokenCache); 28 | 29 | ``` 30 | ## Goals 31 | 32 | 1. API for fallback to file for Linux. (P0) 33 | 34 | 2. Developers must opt-in to fallback, it should not be a default, since fallback is insecure. (P0) 35 | 36 | 3. API for detecting if persistence is not working. This will allow products to show users a warning message about the fallback. (P1) 37 | 38 | 4. If a user connects both via SSH and via UI, her SSH token cache (i.e. the plaintext file) should not be deleted. (P2) 39 | 40 | ### Non-goals 41 | 1. We do not plan to support multiple token cache sources. Token cache is read either from file or from KeyRing. No merging mechanisms exist. 42 | 2. Mechanism is not supposed to work on Windows and Mac. Encryption on Windows and Mac via current mechanisms (DPAPI / KeyChain) is guaranteed by the OS. 43 | 44 | ## Proposal 45 | 46 | #### Add a method to check persistence on Linux 47 | 48 | ```csharp 49 | void cacheHelper.VerifyPersistence(); 50 | ``` 51 | 52 | This method MUST not affect the token cache. It will attempt to write and read a dummy secret. Different storage attributes will be used so as to not interfere with the real token cache (e.g. Windows - different file path, Mac - different account, Linux - differnt keyring attribute) 53 | 54 | If this method fails it throws an exception with more details. Typically the failure points are: 55 | 56 | - LibSecret is not installed 57 | - Incorrect version of LibSecret is installed 58 | - D-BUS is not running (typical in SSH scenario) 59 | - No wallet is listening on the other end 60 | 61 | #### Add a method to persist data in a plaintext file 62 | 63 | 64 | ```csharp 65 | new StorageCreationPropertiesBuilder(Config.CacheFileName, Config.CacheDir, Config.ClientId) 66 | .WithLinuxUnprotectedFile() //new method 67 | .WithMacKeyChain(...); // no change 68 | 69 | ``` 70 | `Config.CacheFileName` will contain the unprotected cache. 71 | 72 | Note: `WithLinuxUnprotectedFile` cannot be used in conjuction with `WithLinuxKeyring` - an exception will be thrown 73 | 74 | #### Suggested pattern for extension consumers 75 | 76 | Libraries consuming the extension will: 77 | 78 | 1. create a cache helper with a the normal `KeyRing` setup 79 | 2. call `cacheHelper.VerifyPersistence()` 80 | 3. If this throws an exception, show the user a meaningful message / URL to help page to inform them to secure their secrets storage 81 | 4. Create a cache helper using `.WithLinuxUnprotectedFile` using a file path that comes from either: 82 | - a well known env variable, e.g. LINUX_DEV_TOOLS_TOKEN_CACHE 83 | - if LINUX_DEV_TOOLS_TOKEN_CACHE is not set, default to a well known location 84 | 85 | 86 | #### Important note about signaling API 87 | 88 | Some consumenrs of the library are using the event `CacheChanged`. While not all extensions expose this event, all extension need to ensure the event is triggered. 89 | This is done via a `FileWatcher` mechanism as follows: 90 | 91 | - on Windows, encrypted data is persisted to `Config.CacheFileName` 92 | - on Mac, data is stored in KeyChain. A dummy 1 byte is written to `Config.CacheFileName` 93 | - on Linux, data is persisted EITHER in KeyRin or in `Config.CacheFileName`. To maintain the signaling semantics, a dummy 1 byte will be written to a file named 94 | `Config.CacheFileName` + '.signal' -------------------------------------------------------------------------------- /release_build.yaml: -------------------------------------------------------------------------------- 1 | pool: 2 | name: Hosted Windows 2019 with VS2019 3 | #demands: npm 4 | 5 | variables: 6 | BuildConfiguration: 'release' 7 | Codeql.Enabled: true 8 | Codeql.Language: csharp 9 | 10 | steps: 11 | 12 | # required for the signing task, but should not be used for the entire build 13 | - task: UseDotNet@2 14 | displayName: 'Use .NET Core SDK 2.1.x' 15 | inputs: 16 | version: 2.1.x 17 | 18 | # Use latest SDK for build 19 | - task: UseDotNet@2 20 | displayName: 'Use .NET SDK 6.x' 21 | inputs: 22 | version: 6.x 23 | 24 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-policheck.PoliCheck@2 25 | displayName: 'Run PoliCheck' 26 | inputs: 27 | targetType: F 28 | 29 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@3 30 | displayName: 'Run CredScan' 31 | inputs: 32 | suppressionsFile: 'build/credscan-exclusion.json' 33 | outputFormat: pre 34 | debugMode: false 35 | 36 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-postanalysis.PostAnalysis@2 37 | displayName: 'Post Analysis' 38 | inputs: 39 | GdnBreakGdnToolCredScan: true 40 | GdnBreakGdnToolPoliCheck: true 41 | 42 | - task: DotNetCoreCLI@2 43 | displayName: 'Build solution' 44 | inputs: 45 | arguments: '-c $(BuildConfiguration) /p:ClientSemVer=$(ClientSemVer) /p:SourceLinkCreate=true /p:ContinousIntegrationBuild=true' 46 | 47 | - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 48 | displayName: 'Sign Binaries' 49 | inputs: 50 | ConnectedServiceName: 'IDDP Code Signing' 51 | FolderPath: '$(Build.SourcesDirectory)\src' 52 | Pattern: '**\bin\**\*.dll' 53 | UseMinimatch: true 54 | signConfigType: inlineSignParams 55 | inlineOperation: | 56 | [ 57 | { 58 | "keyCode": "CP-230012", 59 | "operationSetCode": "SigntoolSign", 60 | "parameters": [ 61 | { 62 | "parameterName": "OpusName", 63 | "parameterValue": "Microsoft.Identity.Client.Extensions.Msal" 64 | }, 65 | { 66 | "parameterName": "OpusInfo", 67 | "parameterValue": "https://www.nuget.org/packages/Microsoft.Identity.Client.Extensions.Msal/" 68 | }, 69 | { 70 | "parameterName": "PageHash", 71 | "parameterValue": "/NPH" 72 | }, 73 | { 74 | "parameterName": "FileDigest", 75 | "parameterValue": "/fd sha256" 76 | }, 77 | { 78 | "parameterName": "TimeStamp", 79 | "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" 80 | } 81 | ], 82 | "toolName": "signtool.exe", 83 | "toolVersion": "6.2.9304.0" 84 | }, 85 | { 86 | "keyCode": "CP-230012", 87 | "operationSetCode": "SigntoolVerify", 88 | "parameters": [ ], 89 | "toolName": "signtool.exe", 90 | "toolVersion": "6.2.9304.0" 91 | } 92 | ] 93 | SessionTimeout: 20 94 | 95 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-binskim.BinSkim@4 96 | displayName: 'Run BinSkim ' 97 | inputs: 98 | InputType: Basic 99 | AnalyzeTargetGlob: '$(Build.SourcesDirectory)\src\**\bin\**\*.dll' 100 | AnalyzeVerbose: true 101 | AnalyzeHashes: true 102 | 103 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-postanalysis.PostAnalysis@2 104 | displayName: 'Check BinSkim Results' 105 | inputs: 106 | GdnBreakGdnToolBinSkim: true 107 | 108 | - task: DotNetCoreCLI@2 109 | displayName: Pack projects 110 | inputs: 111 | command: pack 112 | packagesToPack: 'src/**/*.csproj' 113 | nobuild: true 114 | versioningScheme: byEnvVar 115 | versionEnvVar: ClientSemVer 116 | 117 | - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 118 | displayName: 'Sign Packages' 119 | inputs: 120 | ConnectedServiceName: 'IDDP Code Signing' 121 | FolderPath: '$(Build.ArtifactStagingDirectory)' 122 | Pattern: '*nupkg' 123 | signConfigType: inlineSignParams 124 | inlineOperation: | 125 | [ 126 | { 127 | "keyCode": "CP-401405", 128 | "operationSetCode": "NuGetSign", 129 | "parameters": [ ], 130 | "toolName": "sign", 131 | "toolVersion": "1.0" 132 | }, 133 | { 134 | "keyCode": "CP-401405", 135 | "operationSetCode": "NuGetVerify", 136 | "parameters": [ ], 137 | "toolName": "sign", 138 | "toolVersion": "1.0" 139 | } 140 | ] 141 | SessionTimeout: 20 142 | 143 | - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 144 | displayName: 'Get Software Bill Of Materials (SBOM)' 145 | inputs: 146 | BuildDropPath: '$(Build.ArtifactStagingDirectory)' 147 | 148 | - task: PublishBuildArtifacts@1 149 | displayName: 'Publish Artifacts' 150 | inputs: 151 | ArtifactName: packages 152 | 153 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-uploadtotsa.TSAUpload@2 154 | displayName: 'TSA Upload' 155 | inputs: 156 | GdnPublishTsaOnboard: false 157 | GdnPublishTsaConfigFile: '$(Build.SourcesDirectory)/build/tsaConfig.json' 158 | continueOnError: true 159 | -------------------------------------------------------------------------------- /sample/ManualTestApp/Config.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using Microsoft.Identity.Client.Extensions.Msal; 7 | 8 | namespace ManualTestApp 9 | { 10 | internal static class Config 11 | { 12 | // App settings 13 | public static readonly string[] Scopes = new[] { "user.read" }; 14 | 15 | // Use "common" if you want to allow any "enterprise" (work or school) account AND any user account (live.com, outlook, hotmail) to log in. 16 | // Use an actual tenant ID to allow only your enterprise to log in. 17 | // Use "organizations" to allow only enterprise log-in, this is required for the Username / Password flow 18 | public const string Authority = "https://login.microsoftonline.com/organizations"; 19 | 20 | // DO NOT USE THIS CLIENT ID IN YOUR APP. WE REGULARLY DELETE THEM. CREATE YOUR OWN! 21 | public const string ClientId = "1d18b3b0-251b-4714-a02a-9956cec86c2d"; 22 | 23 | // Cache settings 24 | public const string CacheFileName = "myapp_msal_cache.txt"; 25 | public readonly static string CacheDir = MsalCacheHelper.UserRootDirectory; 26 | 27 | public const string KeyChainServiceName = "myapp_msal_service"; 28 | public const string KeyChainAccountName = "myapp_msal_account"; 29 | 30 | public const string LinuxKeyRingSchema = "com.contoso.devtools.tokencache"; 31 | public const string LinuxKeyRingCollection = MsalCacheHelper.LinuxKeyRingDefaultCollection; 32 | public const string LinuxKeyRingLabel = "MSAL token cache for all Contoso dev tool apps."; 33 | public static readonly KeyValuePair LinuxKeyRingAttr1 = new KeyValuePair("Version", "1"); 34 | public static readonly KeyValuePair LinuxKeyRingAttr2 = new KeyValuePair("ProductGroup", "MyApps"); 35 | 36 | // For Username / Password flow - to be used only for testing! 37 | public const string Username = "liu.kang@bogavrilltd.onmicrosoft.com"; 38 | public const string Password = ""; 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sample/ManualTestApp/ExampleUsage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Microsoft.Identity.Client; 7 | using Microsoft.Identity.Client.Extensions.Msal; 8 | 9 | namespace ManualTestApp 10 | { 11 | /// 12 | /// This class shows how to applications are supposed to use the extension API 13 | /// 14 | public class ExampleUsage 15 | { 16 | private const string TraceSourceName = "MSAL.Contoso.CacheExtension"; 17 | 18 | /// 19 | /// Start reading here... 20 | /// 21 | public static async Task Example_Async() 22 | { 23 | // 1. Use MSAL to create an instance of the Public Client Application 24 | var app = PublicClientApplicationBuilder.Create(Config.ClientId).Build(); 25 | 26 | // 2. Configure the storage 27 | var cacheHelper = await CreateCacheHelperAsync().ConfigureAwait(false); 28 | 29 | // 3. Let the cache helper handle MSAL's cache 30 | cacheHelper.RegisterCache(app.UserTokenCache); 31 | 32 | // 4. Optionally, store some other secret 33 | StoreOtherSecret(); 34 | } 35 | 36 | 37 | 38 | private static async Task CreateCacheHelperAsync() 39 | { 40 | StorageCreationProperties storageProperties; 41 | MsalCacheHelper cacheHelper; 42 | try 43 | { 44 | storageProperties = ConfigureSecureStorage(usePlaintextFileOnLinux: false); 45 | cacheHelper = await MsalCacheHelper.CreateAsync( 46 | storageProperties, 47 | new TraceSource(TraceSourceName)) 48 | .ConfigureAwait(false); 49 | 50 | // the underlying persistence mechanism might not be usable 51 | // this typically happens on Linux over SSH 52 | cacheHelper.VerifyPersistence(); 53 | 54 | return cacheHelper; 55 | } 56 | catch (MsalCachePersistenceException ex) 57 | { 58 | Console.WriteLine("Cannot persist data securely. "); 59 | Console.WriteLine("Details: " + ex); 60 | 61 | 62 | if (SharedUtilities.IsLinuxPlatform()) 63 | { 64 | storageProperties = ConfigureSecureStorage(usePlaintextFileOnLinux: true); 65 | 66 | Console.WriteLine($"Falling back on using a plaintext " + 67 | $"file located at {storageProperties.CacheFilePath} Users are responsible for securing this file!"); 68 | 69 | cacheHelper = await MsalCacheHelper.CreateAsync( 70 | storageProperties, 71 | new TraceSource(TraceSourceName)) 72 | .ConfigureAwait(false); 73 | 74 | return cacheHelper; 75 | } 76 | throw; 77 | } 78 | } 79 | 80 | private static StorageCreationProperties ConfigureSecureStorage(bool usePlaintextFileOnLinux) 81 | { 82 | if (!usePlaintextFileOnLinux) 83 | { 84 | return new StorageCreationPropertiesBuilder( 85 | Config.CacheFileName, 86 | Config.CacheDir) 87 | .WithLinuxKeyring( 88 | Config.LinuxKeyRingSchema, 89 | Config.LinuxKeyRingCollection, 90 | Config.LinuxKeyRingLabel, 91 | Config.LinuxKeyRingAttr1, 92 | Config.LinuxKeyRingAttr2) 93 | .WithMacKeyChain( 94 | Config.KeyChainServiceName, 95 | Config.KeyChainAccountName) 96 | .Build(); 97 | } 98 | 99 | return new StorageCreationPropertiesBuilder( 100 | Config.CacheFileName + "plaintext", // do not use the same file name so as not to overwrite the encypted version 101 | Config.CacheDir) 102 | .WithLinuxUnprotectedFile() 103 | .WithMacKeyChain( 104 | Config.KeyChainServiceName, 105 | Config.KeyChainAccountName) 106 | .Build(); 107 | 108 | } 109 | 110 | private static void StoreOtherSecret() 111 | { 112 | var storageProperties = new StorageCreationPropertiesBuilder( 113 | Config.CacheFileName + ".other_secrets", 114 | Config.CacheDir) 115 | .WithMacKeyChain( 116 | Config.KeyChainServiceName + ".other_secrets", 117 | Config.KeyChainAccountName) 118 | .WithLinuxKeyring( 119 | Config.LinuxKeyRingSchema, 120 | Config.LinuxKeyRingCollection, 121 | Config.LinuxKeyRingLabel, 122 | Config.LinuxKeyRingAttr1, 123 | new KeyValuePair("other_secrets", "secret_description")); 124 | 125 | Storage storage = Storage.Create(storageProperties.Build()); 126 | 127 | byte[] secretBytes = Encoding.UTF8.GetBytes("secret"); 128 | 129 | using (new CrossPlatLock(Config.CacheFileName + ".other_secrets.lock")) 130 | { 131 | Console.WriteLine("Writing..."); 132 | storage.WriteData(secretBytes); 133 | 134 | Console.WriteLine("Writing again..."); 135 | storage.WriteData(secretBytes); 136 | 137 | 138 | Console.WriteLine("Reading..."); 139 | var data = storage.ReadData(); 140 | Console.WriteLine("Read: " + Encoding.UTF8.GetString(data)); 141 | 142 | Console.WriteLine("Deleting..."); 143 | storage.Clear(); 144 | } 145 | } 146 | } 147 | } 148 | 149 | -------------------------------------------------------------------------------- /sample/ManualTestApp/ManualTestApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | false 6 | 7 | netcoreapp3.1;net472 8 | netcoreapp3.1 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Microsoft.Identity.Client.Extensions.Adal/AdalCache.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using Microsoft.IdentityModel.Clients.ActiveDirectory; 8 | 9 | namespace Microsoft.Identity.Client.Extensions.Adal 10 | { 11 | /// 12 | /// Override Adal token cache 13 | /// 14 | public sealed class AdalCache : TokenCache 15 | { 16 | /// 17 | /// A default logger for use if the user doesn't want to provide their own. 18 | /// 19 | private static readonly Lazy s_staticLogger = new Lazy(() => 20 | { 21 | return new TraceSourceLogger((TraceSource)EnvUtils.GetNewTraceSource(nameof(AdalCache) + "Singleton")); 22 | }); 23 | 24 | /// 25 | /// Storage that handles the storing of the adal cache file on disk. 26 | /// 27 | private readonly AdalCacheStorage _store; 28 | 29 | /// 30 | /// Logger to log events to. 31 | /// 32 | private readonly TraceSourceLogger _logger; 33 | private CrossPlatLock _cacheLock; 34 | private readonly int _lockFileRetryDelay; 35 | private readonly int _lockFileRetryCount; 36 | 37 | /// 38 | /// Initializes a new instance of the class. 39 | /// 40 | /// Adal cache storage 41 | /// Logger 42 | public AdalCache(AdalCacheStorage storage, TraceSource logger) : this(storage, logger, CrossPlatLock.LockfileRetryDelayDefault, CrossPlatLock.LockfileRetryCountDefault) 43 | { 44 | } 45 | 46 | /// 47 | /// Initializes a new instance of the class. 48 | /// 49 | /// Adal cache storage 50 | /// Logger 51 | /// Delay in ms between retries if cache lock is contended 52 | /// Number of retries if cache lock is contended 53 | public AdalCache(AdalCacheStorage storage, TraceSource logger, int lockRetryDelay, int lockRetryCount) 54 | { 55 | _logger = logger == null ? s_staticLogger.Value : new TraceSourceLogger(logger); 56 | _store = storage ?? throw new ArgumentNullException(nameof(storage)); 57 | _lockFileRetryCount = lockRetryCount; 58 | _lockFileRetryDelay = lockRetryDelay; 59 | 60 | AfterAccess = AfterAccessNotification; 61 | BeforeAccess = BeforeAccessNotification; 62 | 63 | _logger.LogInformation($"Initializing adal cache"); 64 | 65 | byte[] data = _store.ReadData(); 66 | 67 | _logger.LogInformation($"Read '{data?.Length}' bytes from storage"); 68 | 69 | if (data != null && data.Length > 0) 70 | { 71 | try 72 | { 73 | _logger.LogInformation($"Deserializing data into memory"); 74 | DeserializeAdalV3(data); 75 | } 76 | catch (Exception e) 77 | { 78 | _logger.LogInformation($"An exception was encountered while deserializing the data during initialization of {nameof(AdalCache)} : {e}"); 79 | DeserializeAdalV3(null); 80 | _store.Clear(); 81 | } 82 | } 83 | 84 | _logger.LogInformation($"Done initializing"); 85 | } 86 | 87 | // Triggered right before ADAL needs to access the cache. 88 | // Reload the cache from the persistent store in case it changed since the last access. 89 | // Internal for testing. 90 | internal void BeforeAccessNotification(TokenCacheNotificationArgs args) 91 | { 92 | _logger.LogInformation($"Before access"); 93 | 94 | _logger.LogInformation($"Acquiring lock for token cache"); 95 | _cacheLock = new CrossPlatLock(Path.Combine(_store.CreationProperties.CacheDirectory, _store.CreationProperties.CacheFileName) + ".lockfile", this._lockFileRetryDelay, this._lockFileRetryCount); 96 | 97 | _logger.LogInformation($"Before access, the store has changed"); 98 | byte[] fileData = _store.ReadData(); 99 | _logger.LogInformation($"Read '{fileData?.Length}' bytes from storage"); 100 | 101 | if (fileData != null && fileData.Length > 0) 102 | { 103 | try 104 | { 105 | _logger.LogInformation($"Deserializing the store"); 106 | DeserializeAdalV3(fileData); 107 | } 108 | catch (Exception e) 109 | { 110 | _logger.LogError($"An exception was encountered while deserializing the {nameof(AdalCache)} : {e}"); 111 | _logger.LogError($"No data found in the store, clearing the cache in memory."); 112 | 113 | // Clear the memory cache 114 | DeserializeAdalV3(null); 115 | _store.Clear(); 116 | throw; 117 | } 118 | } 119 | else 120 | { 121 | _logger.LogInformation($"No data found in the store, clearing the cache in memory."); 122 | 123 | // Clear the memory cache 124 | DeserializeAdalV3(null); 125 | } 126 | } 127 | 128 | // Triggered right after ADAL accessed the cache. 129 | // Internal for testing. 130 | internal void AfterAccessNotification(TokenCacheNotificationArgs args) 131 | { 132 | _logger.LogInformation($"After access"); 133 | 134 | try 135 | { 136 | // if the access operation resulted in a cache update 137 | if (HasStateChanged) 138 | { 139 | _logger.LogInformation($"After access, cache in memory HasChanged"); 140 | try 141 | { 142 | _logger.LogInformation($"Before Write Store"); 143 | byte[] data = SerializeAdalV3(); 144 | _logger.LogInformation($"Serializing '{data.Length}' bytes"); 145 | _store.WriteData(data); 146 | 147 | _logger.LogInformation($"After write store"); 148 | HasStateChanged = false; 149 | } 150 | catch (Exception e) 151 | { 152 | _logger.LogError($"An exception was encountered while serializing the {nameof(AdalCache)} : {e}"); 153 | _logger.LogError($"No data found in the store, clearing the cache in memory."); 154 | 155 | // The cache is corrupt clear it out 156 | DeserializeAdalV3(null); 157 | _store.Clear(); 158 | throw; 159 | } 160 | } 161 | } 162 | finally 163 | { 164 | _logger.LogInformation($"Releasing lock"); 165 | // Get a local copy and call null before disposing because when the lock is disposed the next thread will replace CacheLock with its instance, 166 | // therefore we do not want to null out CacheLock after dispose since this may orphan a CacheLock. 167 | var localLockCopy = _cacheLock; 168 | _cacheLock = null; 169 | localLockCopy?.Dispose(); 170 | 171 | } 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/Microsoft.Identity.Client.Extensions.Adal/InternalsVisibleTo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Runtime.CompilerServices; 5 | 6 | [assembly: InternalsVisibleTo("Microsoft.Identity.Client.Extensions.Adal.UnitTests, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")] 7 | -------------------------------------------------------------------------------- /src/Microsoft.Identity.Client.Extensions.Adal/Microsoft.Identity.Client.Extensions.Adal.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 1.0.0-localbuild 7 | 8 | 9 | $(ClientSemVer) 10 | 11 | $(DesktopTargetFrameworks) 12 | $(DefineConstants);ADAL 13 | Microsoft 14 | Microsoft 15 | © Microsoft Corporation. All rights reserved. 16 | MIT 17 | https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet 18 | Microsoft Authentication Library ADAL Azure Active Directory AAD Identity .NET 19 | This package contains extensions to Azure Active Directory Library for .NET (ADAL.NET) 20 | true 21 | https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet 22 | MIT 23 | 24 | 25 | 26 | true 27 | true 28 | 29 | true 30 | snupkg 31 | 32 | 33 | 34 | true 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/Microsoft.Identity.Client.Extensions.Msal/Accessors/DpApiEncryptedFileAccessor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Security.Cryptography; 6 | 7 | namespace Microsoft.Identity.Client.Extensions.Msal 8 | { 9 | internal class DpApiEncryptedFileAccessor : ICacheAccessor 10 | { 11 | private readonly string _cacheFilePath; 12 | private readonly TraceSourceLogger _logger; 13 | private readonly ICacheAccessor _unencryptedFileAccessor; 14 | 15 | public DpApiEncryptedFileAccessor(string cacheFilePath, TraceSourceLogger logger) 16 | { 17 | if (string.IsNullOrEmpty(cacheFilePath)) 18 | { 19 | throw new ArgumentNullException(nameof(cacheFilePath)); 20 | } 21 | 22 | _cacheFilePath = cacheFilePath; 23 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 24 | _unencryptedFileAccessor = new FileAccessor(_cacheFilePath, false, _logger); 25 | } 26 | 27 | public void Clear() 28 | { 29 | _logger.LogInformation("Clearing cache"); 30 | _unencryptedFileAccessor.Clear(); 31 | } 32 | 33 | public ICacheAccessor CreateForPersistenceValidation() 34 | { 35 | return new DpApiEncryptedFileAccessor(_cacheFilePath + ".test", _logger); 36 | } 37 | 38 | public byte[] Read() 39 | { 40 | 41 | byte[] fileData = _unencryptedFileAccessor.Read(); 42 | 43 | if (fileData != null && fileData.Length > 0) 44 | { 45 | _logger.LogInformation($"Unprotecting the data"); 46 | fileData = ProtectedData.Unprotect(fileData, optionalEntropy: null, scope: DataProtectionScope.CurrentUser); 47 | } 48 | 49 | return fileData; 50 | } 51 | 52 | public void Write(byte[] data) 53 | { 54 | if (data.Length != 0) 55 | { 56 | _logger.LogInformation($"Protecting the data"); 57 | data = ProtectedData.Protect(data, optionalEntropy: null, scope: DataProtectionScope.CurrentUser); 58 | } 59 | 60 | _unencryptedFileAccessor.Write(data); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Microsoft.Identity.Client.Extensions.Msal/Accessors/FileAccessor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Security.AccessControl; 7 | using System.Text; 8 | 9 | namespace Microsoft.Identity.Client.Extensions.Msal 10 | { 11 | internal class FileAccessor : ICacheAccessor 12 | { 13 | public static readonly byte[] DummyData = Encoding.UTF8.GetBytes("{}"); 14 | 15 | private readonly string _cacheFilePath; 16 | private readonly TraceSourceLogger _logger; 17 | private readonly bool _setOwnerOnlyPermission; 18 | 19 | internal FileAccessor(string cacheFilePath, bool setOwnerOnlyPermissions, TraceSourceLogger logger) 20 | { 21 | _cacheFilePath = cacheFilePath; 22 | _setOwnerOnlyPermission = setOwnerOnlyPermissions; 23 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 24 | } 25 | 26 | public void Clear() 27 | { 28 | _logger.LogInformation("Deleting cache file"); 29 | FileIOWithRetries.DeleteCacheFile(_cacheFilePath, _logger); 30 | } 31 | 32 | public ICacheAccessor CreateForPersistenceValidation() 33 | { 34 | return new FileAccessor(_cacheFilePath + ".test", _setOwnerOnlyPermission, _logger); 35 | } 36 | 37 | public byte[] Read() 38 | { 39 | _logger.LogInformation("Reading from file"); 40 | 41 | byte[] fileData = null; 42 | bool cacheFileExists = File.Exists(_cacheFilePath); 43 | _logger.LogInformation($"Cache file exists? '{cacheFileExists}'"); 44 | 45 | if (cacheFileExists) 46 | { 47 | FileIOWithRetries.TryProcessFile(() => 48 | { 49 | fileData = File.ReadAllBytes(_cacheFilePath); 50 | _logger.LogInformation($"Read '{fileData.Length}' bytes from the file"); 51 | }, _logger); 52 | } 53 | 54 | return fileData; 55 | } 56 | 57 | public void Write(byte[] data) 58 | { 59 | FileIOWithRetries.CreateAndWriteToFile(_cacheFilePath, data, _setOwnerOnlyPermission, _logger); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Microsoft.Identity.Client.Extensions.Msal/Accessors/FileWithPermissions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Runtime.InteropServices; 7 | using System.Security.AccessControl; 8 | using System.Security.Principal; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using Microsoft.Win32.SafeHandles; 12 | 13 | namespace Microsoft.Identity.Client.Extensions.Msal.Accessors 14 | { 15 | internal static class FileWithPermissions 16 | { 17 | #region Unix specific 18 | 19 | 20 | /// 21 | /// Equivalent to calling open() with flags O_CREAT|O_WRONLY|O_TRUNC. O_TRUNC will truncate the file. 22 | /// See https://man7.org/linux/man-pages/man2/open.2.html 23 | /// 24 | [DllImport("libc", EntryPoint = "creat", SetLastError = true)] 25 | private static extern int PosixCreate([MarshalAs(UnmanagedType.LPStr)] string pathname, int mode); 26 | 27 | [DllImport("libc", EntryPoint = "chmod", SetLastError = true)] 28 | private static extern int PosixChmod([MarshalAs(UnmanagedType.LPStr)] string pathname, int mode); 29 | 30 | #endregion 31 | 32 | 33 | /// 34 | /// Creates a new file with "600" permissions (i.e. read / write only by the owner) and writes some data to it. 35 | /// On Windows, file security is more complex, but an equivalent is achieved. 36 | /// 37 | /// 38 | /// This logic will not work on Mono, see https://github.com/NuGet/NuGet.Client/commit/d62db666c710bf95121fe8f5c6a6cbe01985456f 39 | /// 40 | /// 41 | public static void WriteToNewFileWithOwnerRWPermissions(string path, byte[] data) 42 | { 43 | 44 | if (SharedUtilities.IsWindowsPlatform()) 45 | { 46 | WriteToNewFileWithOwnerRWPermissionsWindows(path, data); 47 | } 48 | else if (SharedUtilities.IsMacPlatform() || SharedUtilities.IsLinuxPlatform()) 49 | { 50 | WriteToNewFileWithOwnerRWPermissionsUnix(path, data); 51 | } 52 | else 53 | { 54 | throw new PlatformNotSupportedException(); 55 | } 56 | } 57 | 58 | /// 59 | /// Based on https://stackoverflow.com/questions/45132081/file-permissions-on-linux-unix-with-net-core and on 60 | /// https://github.com/NuGet/NuGet.Client/commit/d62db666c710bf95121fe8f5c6a6cbe01985456f 61 | /// 62 | private static void WriteToNewFileWithOwnerRWPermissionsUnix(string path, byte[] data) 63 | { 64 | int _0600 = Convert.ToInt32("600", 8); 65 | 66 | int fileDescriptor = PosixCreate(path, _0600); 67 | 68 | // if creat() fails, then try to use File.Create because it will throw a meaningful exception. 69 | if (fileDescriptor == -1) 70 | { 71 | int posixCreateError = Marshal.GetLastWin32Error(); 72 | using (File.Create(path)) 73 | { 74 | // File.Create() should have thrown an exception with an appropriate error message 75 | } 76 | File.Delete(path); 77 | throw new InvalidOperationException($"libc creat() failed with last error code {posixCreateError}, but File.Create did not"); 78 | } 79 | 80 | var safeFileHandle = new SafeFileHandle((IntPtr)fileDescriptor, ownsHandle: true); 81 | using (var fileStream = new FileStream(safeFileHandle, FileAccess.ReadWrite)) 82 | { 83 | fileStream.Write(data, 0, data.Length); 84 | } 85 | } 86 | 87 | /// 88 | /// Windows has a more complex file security system. "600" mode, i.e. read/write for owner translates to this in Windows. 89 | /// 90 | /// 91 | /// 92 | private static void WriteToNewFileWithOwnerRWPermissionsWindows(string filePath, byte[] data) 93 | { 94 | FileSecurity security = new FileSecurity(); 95 | var rights = FileSystemRights.Read | FileSystemRights.Write; 96 | 97 | // https://stackoverflow.com/questions/39480255/c-sharp-how-to-grant-access-only-to-current-user-and-restrict-access-to-others 98 | security.AddAccessRule( 99 | new FileSystemAccessRule( 100 | WindowsIdentity.GetCurrent().Name, 101 | rights, 102 | InheritanceFlags.None, 103 | PropagationFlags.NoPropagateInherit, 104 | AccessControlType.Allow)); 105 | 106 | security.SetAccessRuleProtection(isProtected: true, preserveInheritance: false); 107 | 108 | FileStream fs = null; 109 | 110 | try 111 | { 112 | #if NET45_OR_GREATER 113 | if (File.Exists(filePath)) 114 | { 115 | File.Delete(filePath); 116 | } 117 | 118 | fs = File.Create(filePath, data.Length, FileOptions.None, security); 119 | #else 120 | FileInfo info = new FileInfo(filePath); 121 | fs = info.Create(FileMode.Create, rights, FileShare.Read, data.Length, FileOptions.None, security); 122 | #endif 123 | 124 | 125 | fs.Write(data, 0, data.Length); 126 | } 127 | finally 128 | { 129 | fs?.Dispose(); 130 | } 131 | } 132 | 133 | 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Microsoft.Identity.Client.Extensions.Msal/Accessors/ICacheAccessor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace Microsoft.Identity.Client.Extensions.Msal 11 | { 12 | internal interface ICacheAccessor 13 | { 14 | /// 15 | /// Deletes the cache 16 | /// 17 | void Clear(); 18 | 19 | /// 20 | /// Reads the cache 21 | /// 22 | /// Unprotected cache 23 | byte[] Read(); 24 | 25 | /// 26 | /// Writes the cache 27 | /// 28 | /// Unprotected cache 29 | void Write(byte[] data); 30 | 31 | /// 32 | /// Create an ICacheAccessor that can be used for validating persistence. This must 33 | /// be similar but not identical to the current accessor, so that to avoid overwriting an actual token cache 34 | /// 35 | ICacheAccessor CreateForPersistenceValidation(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Microsoft.Identity.Client.Extensions.Msal/Accessors/MacKeyChainAccessor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using System.Text; 7 | 8 | namespace Microsoft.Identity.Client.Extensions.Msal 9 | { 10 | /// 11 | /// 12 | /// 13 | internal class MacKeychainAccessor : ICacheAccessor 14 | { 15 | private readonly string _cacheFilePath; 16 | private readonly string _service; 17 | private readonly string _account; 18 | private readonly TraceSourceLogger _logger; 19 | 20 | private readonly MacOSKeychain _keyChain; 21 | 22 | public MacKeychainAccessor(string cacheFilePath, string keyChainServiceName, string keyChainAccountName, TraceSourceLogger logger) 23 | { 24 | if (string.IsNullOrWhiteSpace(cacheFilePath)) 25 | { 26 | throw new ArgumentNullException(nameof(cacheFilePath)); 27 | } 28 | 29 | if (string.IsNullOrWhiteSpace(keyChainServiceName)) 30 | { 31 | throw new ArgumentNullException(nameof(keyChainServiceName)); 32 | } 33 | 34 | if (string.IsNullOrWhiteSpace(keyChainAccountName)) 35 | { 36 | throw new ArgumentNullException(nameof(keyChainAccountName)); 37 | } 38 | 39 | _cacheFilePath = cacheFilePath; 40 | _service = keyChainServiceName; 41 | _account = keyChainAccountName; 42 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 43 | 44 | _keyChain = new MacOSKeychain(); 45 | } 46 | 47 | public void Clear() 48 | { 49 | _logger.LogInformation("Clearing cache"); 50 | FileIOWithRetries.DeleteCacheFile(_cacheFilePath, _logger); 51 | 52 | _logger.LogInformation($"Before delete mac keychain service: {_service} account {_account}"); 53 | _keyChain.Remove(_service, _account); 54 | _logger.LogInformation($"After delete mac keychain service: {_service} account {_account}"); 55 | } 56 | 57 | 58 | public byte[] Read() 59 | { 60 | _logger.LogInformation($"ReadDataCore, Before reading from mac keychain service: {_service} account {_account}"); 61 | var entry = _keyChain.Get(_service, _account); 62 | _logger.LogInformation($"ReadDataCore, After reading mac keychain {entry?.Password?.Length ?? 0} chars service: {_service} account {_account}"); 63 | 64 | return entry?.Password; 65 | } 66 | 67 | public void Write(byte[] data) 68 | { 69 | _logger.LogInformation($"Before write to mac keychain service: {_service} account {_account}"); 70 | 71 | _keyChain.AddOrUpdate(_service, _account, data); 72 | _logger.LogInformation($"After write to mac keychain service: {_service} account {_account}"); 73 | 74 | // Change the "last modified" attribute and trigger file changed events 75 | FileIOWithRetries.TouchFile(_cacheFilePath, _logger); 76 | } 77 | 78 | public ICacheAccessor CreateForPersistenceValidation() 79 | { 80 | return new MacKeychainAccessor( 81 | _cacheFilePath + ".test", 82 | _service + Guid.NewGuid().ToString(), 83 | _account, 84 | _logger); 85 | } 86 | 87 | public override string ToString() 88 | { 89 | return $"MacKeyChain accessor pointing to: service {_service}, account {_account}, file {_cacheFilePath}"; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Microsoft.Identity.Client.Extensions.Msal/Cache Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/microsoft-authentication-extensions-for-dotnet/f8f43abe5462786198517d416dc910dc82b3ed03/src/Microsoft.Identity.Client.Extensions.Msal/Cache Architecture.png -------------------------------------------------------------------------------- /src/Microsoft.Identity.Client.Extensions.Msal/CacheChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace Microsoft.Identity.Client.Extensions.Msal 8 | { 9 | /// 10 | /// Event args describing which accounts have been added or removed on a cache change 11 | /// 12 | public class CacheChangedEventArgs : EventArgs 13 | { 14 | /// 15 | /// Gets an enumerable of for each account added to the cache. 16 | /// 17 | public readonly IEnumerable AccountsAdded; 18 | 19 | /// 20 | /// Gets an enumerable of for each account removed from the cache. 21 | /// 22 | public readonly IEnumerable AccountsRemoved; 23 | 24 | /// 25 | /// Constructs a new instance of this class. 26 | /// 27 | /// An enumerable of for each account added to the cache. 28 | /// An enumerable of for each account removed from the cache. 29 | public CacheChangedEventArgs(IEnumerable added, IEnumerable removed) 30 | { 31 | AccountsAdded = added; 32 | AccountsRemoved = removed; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Microsoft.Identity.Client.Extensions.Msal/FileIOWithRetries.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Threading; 7 | using Microsoft.Identity.Client.Extensions.Msal.Accessors; 8 | 9 | namespace Microsoft.Identity.Client.Extensions.Msal 10 | { 11 | internal static class FileIOWithRetries 12 | { 13 | private const int FileLockRetryCount = 20; 14 | private const int FileLockRetryWaitInMs = 200; 15 | 16 | internal static void DeleteCacheFile(string filePath, TraceSourceLogger logger) 17 | { 18 | bool cacheFileExists = File.Exists(filePath); 19 | logger.LogInformation($"DeleteCacheFile Cache file exists '{cacheFileExists}'"); 20 | 21 | TryProcessFile(() => 22 | { 23 | logger.LogInformation("Before deleting the cache file"); 24 | try 25 | { 26 | File.Delete(filePath); 27 | } 28 | catch (Exception e) 29 | { 30 | logger.LogError($"Problem deleting the cache file '{e}'"); 31 | } 32 | 33 | logger.LogInformation($"After deleting the cache file."); 34 | }, logger); 35 | } 36 | 37 | internal static void CreateAndWriteToFile(string filePath, byte[] data, bool setChmod600, TraceSourceLogger logger) 38 | { 39 | EnsureParentDirectoryExists(filePath, logger); 40 | 41 | logger.LogInformation($"Writing cache file"); 42 | 43 | TryProcessFile(() => 44 | { 45 | if (setChmod600) 46 | { 47 | logger.LogInformation($"Writing file with chmod 600"); 48 | FileWithPermissions.WriteToNewFileWithOwnerRWPermissions(filePath, data); 49 | } 50 | else 51 | { 52 | logger.LogInformation($"Writing file without special permissions"); 53 | File.WriteAllBytes(filePath, data); 54 | } 55 | }, logger); 56 | } 57 | 58 | private static void EnsureParentDirectoryExists(string filePath, TraceSourceLogger logger) 59 | { 60 | string directoryForCacheFile = Path.GetDirectoryName(filePath); 61 | if (!Directory.Exists(directoryForCacheFile)) 62 | { 63 | string directory = Path.GetDirectoryName(filePath); 64 | logger.LogInformation($"Creating directory '{directory}'"); 65 | Directory.CreateDirectory(directory); 66 | } 67 | } 68 | 69 | 70 | /// 71 | /// Changes the LastWriteTime of the file, without actually writing anything to it. 72 | /// 73 | /// 74 | /// Creates the file if it does not exist. 75 | /// This operation will enable a to fire. 76 | /// 77 | internal static void TouchFile(string filePath, TraceSourceLogger logger) 78 | { 79 | EnsureParentDirectoryExists(filePath, logger); 80 | logger.LogInformation($"Touching file..."); 81 | 82 | TryProcessFile(() => 83 | { 84 | if (!File.Exists(filePath)) 85 | { 86 | logger.LogInformation($"File {filePath} does not exist. Creating it.."); 87 | 88 | var fs = File.Create(filePath); 89 | fs.Dispose(); 90 | } 91 | 92 | File.SetLastWriteTimeUtc(filePath, DateTime.UtcNow); 93 | 94 | }, logger); 95 | } 96 | 97 | internal static void TryProcessFile(Action action, TraceSourceLogger logger) 98 | { 99 | for (int tryCount = 0; tryCount <= FileLockRetryCount; tryCount++) 100 | { 101 | try 102 | { 103 | action.Invoke(); 104 | return; 105 | } 106 | catch (Exception e) 107 | { 108 | Thread.Sleep(TimeSpan.FromMilliseconds(FileLockRetryWaitInMs)); 109 | 110 | 111 | 112 | if (tryCount == FileLockRetryCount) 113 | { 114 | logger.LogError($"An exception was encountered while processing the cache file ex:'{e}'"); 115 | } 116 | else 117 | { 118 | logger.LogWarning($"An exception was encountered while processing the cache file. Operation will be retried. Ex:'{e}'"); 119 | } 120 | } 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Microsoft.Identity.Client.Extensions.Msal/InternalsVisibleTo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Runtime.CompilerServices; 5 | 6 | [assembly: InternalsVisibleTo("Microsoft.Identity.Client.Extensions.Msal.UnitTests, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")] 7 | -------------------------------------------------------------------------------- /src/Microsoft.Identity.Client.Extensions.Msal/Microsoft.Identity.Client.Extensions.Msal.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 2.0.0-localbuild 7 | 8 | $(ClientSemVer) 9 | 10 | $(DesktopTargetFrameworks) 11 | $(DefineConstants);MSAL 12 | Microsoft 13 | Microsoft 14 | This package contains extensions to Microsoft Authentication Library for .NET (MSAL.NET) 15 | © Microsoft Corporation. All rights reserved. 16 | MIT 17 | https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet 18 | https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet 19 | Microsoft Authentication Library MSAL Azure Active Directory AAD Identity .NET 20 | true 21 | MIT 22 | 23 | 24 | 25 | true 26 | true 27 | 28 | true 29 | snupkg 30 | 31 | 32 | 33 | true 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/Microsoft.Identity.Client.Extensions.Msal/MsalCachePersistenceException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Runtime.Serialization; 6 | 7 | namespace Microsoft.Identity.Client.Extensions.Msal 8 | { 9 | /// 10 | /// Exception that results when trying to persist data to the underlying OS mechanism (KeyRing, KeyChain, DPAPI) 11 | /// Inspect inner exception for details. 12 | /// 13 | public class MsalCachePersistenceException : Exception 14 | { 15 | /// 16 | /// 17 | /// 18 | public MsalCachePersistenceException() 19 | { 20 | } 21 | 22 | /// 23 | /// 24 | /// 25 | /// 26 | public MsalCachePersistenceException(string message) : base(message) 27 | { 28 | } 29 | 30 | /// 31 | /// 32 | /// 33 | /// 34 | /// 35 | public MsalCachePersistenceException(string message, Exception innerException) : base(message, innerException) 36 | { 37 | } 38 | 39 | /// 40 | /// 41 | /// 42 | /// 43 | /// 44 | protected MsalCachePersistenceException(SerializationInfo info, StreamingContext context) : base(info, context) 45 | { 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Microsoft.Identity.Client.Extensions.Msal/Properties/InternalsVisibleTo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Runtime.CompilerServices; 5 | 6 | // For NSubstitute to work 7 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] 8 | [assembly: InternalsVisibleTo("Automation.TestApp, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d96616729b54f6d013d71559a017f50aa4861487226c523959d1579b93f3fdf71c08b980fd3130062b03d3de115c4b84e7ac46aef5e192a40e7457d5f3a08f66ceab71143807f2c3cb0da5e23b38f0559769978406f6e5d30ceadd7985fc73a5a609a8b74a1df0a29399074a003a226c943d480fec96dbec7106a87896539ad")] 9 | [assembly: InternalsVisibleTo("FileLockApp, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d96616729b54f6d013d71559a017f50aa4861487226c523959d1579b93f3fdf71c08b980fd3130062b03d3de115c4b84e7ac46aef5e192a40e7457d5f3a08f66ceab71143807f2c3cb0da5e23b38f0559769978406f6e5d30ceadd7985fc73a5a609a8b74a1df0a29399074a003a226c943d480fec96dbec7106a87896539ad")] 10 | [assembly: InternalsVisibleTo("StorageTestApp, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d96616729b54f6d013d71559a017f50aa4861487226c523959d1579b93f3fdf71c08b980fd3130062b03d3de115c4b84e7ac46aef5e192a40e7457d5f3a08f66ceab71143807f2c3cb0da5e23b38f0559769978406f6e5d30ceadd7985fc73a5a609a8b74a1df0a29399074a003a226c943d480fec96dbec7106a87896539ad")] 11 | -------------------------------------------------------------------------------- /src/Shared/Constants.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | #if ADAL 9 | namespace Microsoft.Identity.Client.Extensions.Adal 10 | #elif MSAL 11 | namespace Microsoft.Identity.Client.Extensions.Msal 12 | #else // WEB 13 | namespace Microsoft.Identity.Client.Extensions.Web 14 | #endif 15 | { 16 | internal class Constants 17 | { 18 | public const string MacKeyChainDeleteFailed = "SecKeychainItemDelete failed with error code: {0}"; 19 | public const string MacKeyChainFindFailed = "SecKeychainFindGenericPassword failed with error code: {0}"; 20 | public const string MacKeyChainInsertFailed = "SecKeychainAddGenericPassword failed with error code: {0}"; 21 | public const string MacKeyChainUpdateFailed = "SecKeychainItemModifyAttributesAndData failed with error code: {0}"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Shared/CrossPlatLock.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.Text; 8 | using System.Threading; 9 | 10 | #if ADAL 11 | namespace Microsoft.Identity.Client.Extensions.Adal 12 | #elif MSAL 13 | namespace Microsoft.Identity.Client.Extensions.Msal 14 | #else // WEB 15 | namespace Microsoft.Identity.Client.Extensions.Web 16 | #endif 17 | { 18 | /// 19 | /// A cross-process lock that works on all platforms, implemented using files. 20 | /// Does not ensure thread safety, i.e. 2 threads from the same process will pass through this lock. 21 | /// 22 | /// 23 | /// Thread locking should be done using or another such primitive. 24 | /// 25 | public sealed class CrossPlatLock : IDisposable 26 | { 27 | internal const int LockfileRetryDelayDefault = 100; 28 | internal const int LockfileRetryCountDefault = 60000 / LockfileRetryDelayDefault; 29 | private FileStream _lockFileStream; 30 | 31 | /// 32 | /// Creates a file lock and maintains it until the lock is disposed. Any other process trying to get the lock will wait (spin waiting) until the lock is released. 33 | /// Works on Windows, Mac and Linux. 34 | /// 35 | /// The path of the lock file, e.g. {MsalCacheHelper.UserRootDirectory}/MyAppsSecrets.lockfile 36 | /// Delay between each attempt to get the lock. Defaults to 100ms 37 | /// How many times to try to get the lock before bailing. Defaults to 600 times. 38 | /// This class is experimental and may be removed from the public API. 39 | public CrossPlatLock(string lockfilePath, int lockFileRetryDelay = LockfileRetryDelayDefault, int lockFileRetryCount = LockfileRetryCountDefault) 40 | { 41 | Exception exception = null; 42 | FileStream fileStream = null; 43 | 44 | // Create lock file dir if it doesn't already exist 45 | 46 | Directory.CreateDirectory(Path.GetDirectoryName(lockfilePath)); 47 | string lockerProcessInfo = $"{SharedUtilities.GetCurrentProcessId()} {SharedUtilities.GetCurrentProcessName()}"; 48 | 49 | for (int tryCount = 0; tryCount < lockFileRetryCount; tryCount++) 50 | { 51 | try 52 | { 53 | // We are using the file locking to synchronize the store, do not allow multiple writers or readers for the file. 54 | const int defaultBufferSize = 4096; 55 | var fileShare = FileShare.None; 56 | if (SharedUtilities.IsWindowsPlatform()) 57 | { 58 | // This is so that Windows can offer read due to the granularity of the locking. Unix will not 59 | // lock with FileShare.Read. Read access on Windows is only for debugging purposes and will not 60 | // affect the functionality. 61 | // 62 | // See: https://github.com/dotnet/coreclr/blob/98472784f82cee7326a58e0c4acf77714cdafe03/src/System.Private.CoreLib/shared/System/IO/FileStream.Unix.cs#L74-L89 63 | fileShare = FileShare.Read; 64 | } 65 | 66 | var fileOptions = FileOptions.DeleteOnClose; 67 | if (SharedUtilities.IsMonoPlatform()) 68 | { 69 | // Deleting on close/dispose would cause a file locked by another process to be deleted when 70 | // running on Mono since locking is a two step process - it requires creating a FileStream and then 71 | // calling FileStream.Lock, which then may fail. 72 | fileOptions = FileOptions.None; 73 | } 74 | 75 | fileStream = new FileStream(lockfilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, fileShare, defaultBufferSize, fileOptions); 76 | 77 | if (SharedUtilities.IsMonoPlatform()) 78 | { 79 | // Mono requires FileStream.Lock to be called to lock the file. Using FileShare.None when creating the 80 | // FileStream is not enough to lock the file on Mono. 81 | fileStream.Lock(0, 0); 82 | } 83 | 84 | using (var writer = new StreamWriter(fileStream, Encoding.UTF8, defaultBufferSize, leaveOpen: true)) 85 | { 86 | writer.WriteLine(lockerProcessInfo); 87 | } 88 | break; 89 | } 90 | catch (IOException ex) 91 | { 92 | fileStream?.Dispose(); 93 | fileStream = null; 94 | exception = ex; 95 | Thread.Sleep(lockFileRetryDelay); 96 | } 97 | catch (UnauthorizedAccessException ex) 98 | { 99 | fileStream?.Dispose(); 100 | fileStream = null; 101 | exception = ex; 102 | Thread.Sleep(lockFileRetryDelay); 103 | } 104 | } 105 | 106 | _lockFileStream = fileStream ?? throw new InvalidOperationException("Could not get access to the shared lock file.", exception); 107 | } 108 | 109 | /// 110 | /// Releases the lock 111 | /// 112 | public void Dispose() 113 | { 114 | _lockFileStream?.Dispose(); 115 | _lockFileStream = null; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Shared/EnvUtils.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | 7 | #if ADAL 8 | namespace Microsoft.Identity.Client.Extensions.Adal 9 | #else //MSAL 10 | namespace Microsoft.Identity.Client.Extensions.Msal 11 | #endif 12 | 13 | { 14 | internal static class EnvUtils 15 | { 16 | internal const string TraceLevelEnvVarName = "IDENTITYEXTENSIONTRACELEVEL"; 17 | private const string DefaultTraceSource = "Microsoft.Identity.Client.Extensions.TraceSource"; 18 | 19 | internal static TraceSource GetNewTraceSource(string sourceName) 20 | { 21 | sourceName = sourceName ?? DefaultTraceSource; 22 | #if DEBUG 23 | var level = SourceLevels.Verbose; 24 | #else 25 | var level = SourceLevels.Warning; 26 | #endif 27 | string traceSourceLevelEnvVar = Environment.GetEnvironmentVariable(EnvUtils.TraceLevelEnvVarName); 28 | if (!string.IsNullOrEmpty(traceSourceLevelEnvVar) && 29 | Enum.TryParse(traceSourceLevelEnvVar, ignoreCase: true, result: out SourceLevels result)) 30 | { 31 | level = result; 32 | } 33 | 34 | return new TraceSource(sourceName, level); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Shared/InteropException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.ComponentModel; 7 | using System.Diagnostics; 8 | using System.Text; 9 | 10 | namespace Microsoft.Identity.Extensions 11 | { 12 | /// 13 | /// An unexpected error occurred in interop-code. 14 | /// 15 | [DebuggerDisplay("{DebuggerDisplay}")] 16 | internal class InteropException : Exception 17 | { 18 | public InteropException() 19 | : base() { } 20 | 21 | public InteropException(string message, int errorCode) 22 | : base(message + " .Error code: " + errorCode) 23 | { 24 | ErrorCode = errorCode; 25 | } 26 | 27 | public InteropException(string message, int errorCode, Exception innerException) 28 | : base(message + ". Error code: " + errorCode, innerException) 29 | { 30 | ErrorCode = errorCode; 31 | } 32 | 33 | /// 34 | /// Native error code. 35 | /// 36 | public int ErrorCode { get; } 37 | 38 | private string DebuggerDisplay => $"{Message} [0x{ErrorCode:x}]"; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Shared/Linux/GError.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | #if ADAL 5 | namespace Microsoft.Identity.Client.Extensions.Adal 6 | #elif MSAL 7 | namespace Microsoft.Identity.Client.Extensions.Msal 8 | #else // WEB 9 | namespace Microsoft.Identity.Client.Extensions.Web 10 | #endif 11 | { 12 | /// 13 | /// Error returned by libsecret library if saving or retrieving fails 14 | /// https://developer.gnome.org/glib/stable/glib-Error-Reporting.html 15 | /// 16 | internal struct GError 17 | { 18 | #pragma warning disable IDE1006 // Naming Styles 19 | #pragma warning disable CS0649 // Never assigned to (is marshalled) 20 | /// 21 | /// error domain 22 | /// 23 | public uint Domain; 24 | 25 | /// 26 | /// error code 27 | /// 28 | public int Code; 29 | 30 | /// 31 | /// detailed error message 32 | /// 33 | public string Message; 34 | #pragma warning restore IDE1006 // Naming Styles 35 | #pragma warning restore CS0649 // Never assigned to 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Shared/Linux/Libsecret.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Runtime.InteropServices; 6 | 7 | #if ADAL 8 | namespace Microsoft.Identity.Client.Extensions.Adal 9 | #elif MSAL 10 | namespace Microsoft.Identity.Client.Extensions.Msal 11 | #else // WEB 12 | namespace Microsoft.Identity.Client.Extensions.Web 13 | #endif 14 | { 15 | /// 16 | /// Data structures and methods required for saving and retrieving secret using keyring in linux 17 | /// https://developer.gnome.org/libsecret/0.18/ 18 | /// 19 | internal static class Libsecret 20 | { 21 | /// 22 | /// type of the attribute of the schema for the secret store 23 | /// 24 | public enum SecretSchemaAttributeType 25 | { 26 | /// 27 | /// string attribute 28 | /// 29 | SECRET_SCHEMA_ATTRIBUTE_STRING = 0, 30 | 31 | /// 32 | /// integer attribute 33 | /// 34 | SECRET_SCHEMA_ATTRIBUTE_INTEGER = 1, 35 | 36 | /// 37 | /// boolean attribute 38 | /// 39 | SECRET_SCHEMA_ATTRIBUTE_BOOLEAN = 2, 40 | } 41 | 42 | /// 43 | /// flags for the schema creation 44 | /// 45 | public enum SecretSchemaFlags 46 | { 47 | /// 48 | /// no specific flag 49 | /// 50 | SECRET_SCHEMA_NONE = 0, 51 | 52 | /// 53 | /// during matching of the schema, set this flag to skip matching the name 54 | /// 55 | SECRET_SCHEMA_DONT_MATCH_NAME = 1 << 1, 56 | } 57 | 58 | #pragma warning disable SA1300 // suppressing warning for lowercase function name 59 | 60 | /// 61 | /// creates a schema for saving secret 62 | /// 63 | /// Name of the schema 64 | /// flags to skip matching name for comparison 65 | /// first attribute of the schema 66 | /// type of the first attribute 67 | /// second attribute of the schema 68 | /// type of the second attribute 69 | /// null parameter to indicate end of attributes 70 | /// a schema for saving and retrieving secret 71 | [DllImport("libsecret-1.so.0", CallingConvention = CallingConvention.StdCall)] 72 | public static extern IntPtr secret_schema_new(string name, int flags, string attribute1, int attribute1Type, string attribute2, int attribute2Type, IntPtr end); 73 | 74 | /// 75 | /// saves a secret in the secret stroe using the keyring 76 | /// 77 | /// schema for saving secret 78 | /// collection where to save the secret 79 | /// label of the secret 80 | /// the secret to save 81 | /// optional GCancellable object or null 82 | /// error encountered during saving 83 | /// type of the first attribute 84 | /// value of the first attribute 85 | /// type of the second attribute 86 | /// value of the second attribute 87 | /// null parameter to indicate end of attributes 88 | /// whether the save is successful or not 89 | [DllImport("libsecret-1.so.0", CallingConvention = CallingConvention.StdCall)] 90 | public static extern int secret_password_store_sync(IntPtr schema, string collection, string label, string password, IntPtr cancellable, out IntPtr error, string attribute1Type, string attribute1Value, string attribute2Type, string attribute2Value, IntPtr end); 91 | 92 | /// 93 | /// retrieve a secret from the secret store using the keyring 94 | /// 95 | /// schema for retrieving secret 96 | /// optional GCancellable object or null 97 | /// >error encountered during retrieval 98 | /// type of the first attribute 99 | /// value of the first attribute 100 | /// type of the second attribute 101 | /// value of the second attribute 102 | /// null parameter to indicate end of attributes 103 | /// the retrieved secret 104 | [DllImport("libsecret-1.so.0", CallingConvention = CallingConvention.StdCall)] 105 | public static extern string secret_password_lookup_sync(IntPtr schema, IntPtr cancellable, out IntPtr error, string attribute1Type, string attribute1Value, string attribute2Type, string attribute2Value, IntPtr end); 106 | 107 | /// 108 | /// clears a secret from the secret store using the keyring 109 | /// 110 | /// schema for the secret 111 | /// optional GCancellable object or null 112 | /// >error encountered during clearing 113 | /// type of the first attribute 114 | /// value of the first attribute 115 | /// type of the second attribute 116 | /// value of the second attribute 117 | /// null parameter to indicate end of attributes 118 | /// the retrieved secret 119 | [DllImport("libsecret-1.so.0", CallingConvention = CallingConvention.StdCall)] 120 | public static extern int secret_password_clear_sync(IntPtr schema, IntPtr cancellable, out IntPtr error, string attribute1Type, string attribute1Value, string attribute2Type, string attribute2Value, IntPtr end); 121 | 122 | #pragma warning restore SA1300 // suppressing warning for lowercase function name 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Shared/Linux/LinuxNativeMethods.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Runtime.InteropServices; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | #if ADAL 12 | namespace Microsoft.Identity.Client.Extensions.Adal 13 | #elif MSAL 14 | namespace Microsoft.Identity.Client.Extensions.Msal 15 | #else // WEB 16 | namespace Microsoft.Identity.Client.Extensions.Web 17 | #endif 18 | { 19 | internal static class LinuxNativeMethods 20 | { 21 | public const int RootUserId = 0; 22 | 23 | /// 24 | /// Get the real user ID of the calling process. 25 | /// 26 | /// the real user ID of the calling process 27 | [DllImport("libc")] 28 | public static extern int getuid(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Shared/Mac/CoreFoundation.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Runtime.InteropServices; 6 | using System.Text; 7 | using static Microsoft.Identity.Extensions.Mac.LibSystem; 8 | 9 | namespace Microsoft.Identity.Extensions.Mac 10 | { 11 | internal static class CoreFoundation 12 | { 13 | private const string CoreFoundationFrameworkLib = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"; 14 | 15 | public static readonly IntPtr Handle; 16 | public static readonly IntPtr kCFBooleanTrue; 17 | public static readonly IntPtr kCFBooleanFalse; 18 | 19 | static CoreFoundation() 20 | { 21 | Handle = dlopen(CoreFoundationFrameworkLib, 0); 22 | 23 | kCFBooleanTrue = GetGlobal(Handle, "kCFBooleanTrue"); 24 | kCFBooleanFalse = GetGlobal(Handle, "kCFBooleanFalse"); 25 | } 26 | 27 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 28 | public static extern IntPtr CFArrayCreateMutable(IntPtr allocator, long capacity, IntPtr callbacks); 29 | 30 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 31 | public static extern void CFArrayInsertValueAtIndex(IntPtr theArray, long idx, IntPtr value); 32 | 33 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 34 | public static extern long CFArrayGetCount(IntPtr theArray); 35 | 36 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 37 | public static extern IntPtr CFArrayGetValueAtIndex(IntPtr theArray, long idx); 38 | 39 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 40 | public static extern IntPtr CFDictionaryCreateMutable( 41 | IntPtr allocator, 42 | long capacity, 43 | IntPtr keyCallBacks, 44 | IntPtr valueCallBacks); 45 | 46 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 47 | public static extern void CFDictionaryAddValue( 48 | IntPtr theDict, 49 | IntPtr key, 50 | IntPtr value); 51 | 52 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 53 | public static extern IntPtr CFDictionaryGetValue(IntPtr theDict, IntPtr key); 54 | 55 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 56 | public static extern bool CFDictionaryGetValueIfPresent(IntPtr theDict, IntPtr key, out IntPtr value); 57 | 58 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 59 | public static extern IntPtr CFStringCreateWithBytes(IntPtr alloc, byte[] bytes, long numBytes, 60 | CFStringEncoding encoding, bool isExternalRepresentation); 61 | 62 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 63 | public static extern long CFStringGetLength(IntPtr theString); 64 | 65 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 66 | public static extern bool CFStringGetCString(IntPtr theString, IntPtr buffer, long bufferSize, CFStringEncoding encoding); 67 | 68 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 69 | public static extern void CFRetain(IntPtr cf); 70 | 71 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 72 | public static extern void CFRelease(IntPtr cf); 73 | 74 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 75 | public static extern int CFGetTypeID(IntPtr cf); 76 | 77 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 78 | public static extern int CFStringGetTypeID(); 79 | 80 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 81 | public static extern int CFDataGetTypeID(); 82 | 83 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 84 | public static extern int CFDictionaryGetTypeID(); 85 | 86 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 87 | public static extern int CFArrayGetTypeID(); 88 | 89 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 90 | public static extern IntPtr CFDataGetBytePtr(IntPtr theData); 91 | 92 | [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 93 | public static extern int CFDataGetLength(IntPtr theData); 94 | } 95 | 96 | internal enum CFStringEncoding 97 | { 98 | kCFStringEncodingUTF8 = 0x08000100, 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Shared/Mac/LibSystem.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Runtime.InteropServices; 7 | using System.Text; 8 | 9 | namespace Microsoft.Identity.Extensions.Mac 10 | { 11 | internal static class LibSystem 12 | { 13 | private const string LibSystemLib = "/usr/lib/libSystem.dylib"; 14 | 15 | [DllImport(LibSystemLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 16 | public static extern IntPtr dlopen(string name, int flags); 17 | 18 | [DllImport(LibSystemLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 19 | public static extern IntPtr dlsym(IntPtr handle, string symbol); 20 | 21 | public static IntPtr GetGlobal(IntPtr handle, string symbol) 22 | { 23 | IntPtr ptr = dlsym(handle, symbol); 24 | var structure = Marshal.PtrToStructure(ptr, typeof(IntPtr)); 25 | 26 | return (IntPtr)structure; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Shared/Mac/MacOSKeychainCredential.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | using System.Diagnostics; 4 | 5 | namespace Microsoft.Identity.Extensions.Mac 6 | { 7 | [DebuggerDisplay("{DebuggerDisplay}")] 8 | internal class MacOSKeychainCredential 9 | { 10 | internal MacOSKeychainCredential(string service, string account, byte[] password, string label) 11 | { 12 | Service = service; 13 | Account = account; 14 | Password = password; 15 | Label = label; 16 | } 17 | 18 | public string Service { get; } 19 | 20 | public string Account { get; } 21 | 22 | public string Label { get; } 23 | 24 | public byte[] Password { get; } 25 | 26 | private string DebuggerDisplay => $"{Label} [Service: {Service}, Account: {Account}]"; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Shared/Shared.projitems: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | f575599f-41ad-4cdd-b0ad-3b659a43ee84 7 | 8 | 9 | Microsoft.Identity.Extensions 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Shared/Shared.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | f575599f-41ad-4cdd-b0ad-3b659a43ee84 5 | 14.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Shared/SharedUtilities.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using System.Globalization; 7 | using System.IO; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | #if ADAL 12 | namespace Microsoft.Identity.Client.Extensions.Adal 13 | #elif MSAL 14 | namespace Microsoft.Identity.Client.Extensions.Msal 15 | #else // WEB 16 | namespace Microsoft.Identity.Client.Extensions.Web 17 | #endif 18 | { 19 | /// 20 | /// A set of utilities shared between service and client 21 | /// 22 | public static class SharedUtilities 23 | { 24 | /// 25 | /// default base cache path 26 | /// 27 | private static readonly string s_homeEnvVar = Environment.GetEnvironmentVariable("HOME"); 28 | private static readonly string s_lognameEnvVar = Environment.GetEnvironmentVariable("LOGNAME"); 29 | private static readonly string s_userEnvVar = Environment.GetEnvironmentVariable("USER"); 30 | private static readonly string s_lNameEnvVar = Environment.GetEnvironmentVariable("LNAME"); 31 | private static readonly string s_usernameEnvVar = Environment.GetEnvironmentVariable("USERNAME"); 32 | 33 | private static readonly Lazy s_isMono = new Lazy(() => Type.GetType("Mono.Runtime") != null); 34 | 35 | private static string s_processName = null; 36 | private static int s_processId = default(int); 37 | 38 | /// 39 | /// Is this a windows platform 40 | /// 41 | /// A value indicating if we are running on windows or not 42 | public static bool IsWindowsPlatform() 43 | { 44 | return Environment.OSVersion.Platform == PlatformID.Win32NT; 45 | } 46 | 47 | /// 48 | /// Is this a MAC platform 49 | /// 50 | /// A value indicating if we are running on mac or not 51 | public static bool IsMacPlatform() 52 | { 53 | #if NET45_OR_GREATER 54 | // we have to also check for PlatformID.Unix because Mono can sometimes return Unix as the platform on a Mac machine. 55 | // see http://www.mono-project.com/docs/faq/technical/ 56 | return Environment.OSVersion.Platform == PlatformID.MacOSX || Environment.OSVersion.Platform == PlatformID.Unix; 57 | #else 58 | return System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.OSX); 59 | #endif 60 | } 61 | 62 | /// 63 | /// Is this a linux platform 64 | /// 65 | /// A value indicating if we are running on linux or not 66 | public static bool IsLinuxPlatform() 67 | { 68 | #if NET45_OR_GREATER 69 | return Environment.OSVersion.Platform == PlatformID.Unix; 70 | #else 71 | return System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux); 72 | #endif 73 | } 74 | 75 | /// 76 | /// Is this running on mono 77 | /// 78 | /// A value indicating if we are running on mono or not 79 | internal static bool IsMonoPlatform() 80 | { 81 | return s_isMono.Value; 82 | } 83 | 84 | /// 85 | /// Instantiates the process if not done already and retrieves the id of the process. 86 | /// Caches it for the next call. 87 | /// 88 | /// process id 89 | internal static int GetCurrentProcessId() 90 | { 91 | if (s_processId == default(int)) 92 | { 93 | using (var process = Process.GetCurrentProcess()) 94 | { 95 | s_processId = process.Id; 96 | s_processName = process.ProcessName; 97 | } 98 | } 99 | 100 | return s_processId; 101 | } 102 | 103 | /// 104 | /// Instantiates the process if not done already and retrieves the name of the process. 105 | /// Caches it for the next call 106 | /// 107 | /// process name 108 | internal static string GetCurrentProcessName() 109 | { 110 | if (string.IsNullOrEmpty(s_processName)) 111 | { 112 | using (var process = Process.GetCurrentProcess()) 113 | { 114 | s_processName = process.ProcessName; 115 | s_processId = process.Id; 116 | } 117 | } 118 | 119 | return s_processName; 120 | } 121 | 122 | /// 123 | /// Generate the default file location 124 | /// 125 | /// Root directory 126 | public static string GetUserRootDirectory() 127 | { 128 | return !IsWindowsPlatform() 129 | ? SharedUtilities.GetUserHomeDirOnUnix() 130 | : Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); 131 | } 132 | 133 | private static string GetUserHomeDirOnUnix() 134 | { 135 | if (SharedUtilities.IsWindowsPlatform()) 136 | { 137 | throw new NotSupportedException(); 138 | } 139 | 140 | if (!string.IsNullOrEmpty(SharedUtilities.s_homeEnvVar)) 141 | { 142 | return SharedUtilities.s_homeEnvVar; 143 | } 144 | 145 | string username = null; 146 | if (!string.IsNullOrEmpty(SharedUtilities.s_lognameEnvVar)) 147 | { 148 | username = s_lognameEnvVar; 149 | } 150 | else if (!string.IsNullOrEmpty(SharedUtilities.s_userEnvVar)) 151 | { 152 | username = s_userEnvVar; 153 | } 154 | else if (!string.IsNullOrEmpty(SharedUtilities.s_lNameEnvVar)) 155 | { 156 | username = s_lNameEnvVar; 157 | } 158 | else if (!string.IsNullOrEmpty(SharedUtilities.s_usernameEnvVar)) 159 | { 160 | username = s_usernameEnvVar; 161 | } 162 | 163 | if (SharedUtilities.IsMacPlatform()) 164 | { 165 | return !string.IsNullOrEmpty(username) ? Path.Combine("/Users", username) : null; 166 | } 167 | else if (SharedUtilities.IsLinuxPlatform()) 168 | { 169 | if (LinuxNativeMethods.getuid() == LinuxNativeMethods.RootUserId) 170 | { 171 | return "/root"; 172 | } 173 | else 174 | { 175 | return !string.IsNullOrEmpty(username) ? Path.Combine("/home", username) : null; 176 | } 177 | } 178 | else 179 | { 180 | throw new NotSupportedException(); 181 | } 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/Shared/StorageCreationProperties.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Text; 8 | 9 | #if ADAL 10 | namespace Microsoft.Identity.Client.Extensions.Adal 11 | #elif MSAL 12 | namespace Microsoft.Identity.Client.Extensions.Msal 13 | #else // WEB 14 | namespace Microsoft.Identity.Client.Extensions.Web 15 | #endif 16 | { 17 | /// 18 | /// An immutable class containing information required to instantiate storage objects for MSAL caches in various platforms. 19 | /// 20 | public class StorageCreationProperties 21 | { 22 | /// 23 | /// This constructor is intentionally internal. To get one of these objects use . 24 | /// 25 | internal StorageCreationProperties( 26 | string cacheFileName, 27 | string cacheDirectory, 28 | string macKeyChainServiceName, 29 | string macKeyChainAccountName, 30 | bool useLinuxPlaintextFallback, 31 | bool usePlaintextFallback, 32 | string keyringSchemaName, 33 | string keyringCollection, 34 | string keyringSecretLabel, 35 | KeyValuePair keyringAttribute1, 36 | KeyValuePair keyringAttribute2, 37 | int lockRetryDelay, 38 | int lockRetryCount, 39 | string clientId, 40 | string authority) 41 | { 42 | CacheFileName = cacheFileName; 43 | CacheDirectory = cacheDirectory; 44 | CacheFilePath = Path.Combine(CacheDirectory, CacheFileName); 45 | 46 | UseLinuxUnencryptedFallback = useLinuxPlaintextFallback; 47 | UseUnencryptedFallback = usePlaintextFallback; 48 | 49 | MacKeyChainServiceName = macKeyChainServiceName; 50 | MacKeyChainAccountName = macKeyChainAccountName; 51 | 52 | KeyringSchemaName = keyringSchemaName; 53 | KeyringCollection = keyringCollection; 54 | KeyringSecretLabel = keyringSecretLabel; 55 | KeyringAttribute1 = keyringAttribute1; 56 | KeyringAttribute2 = keyringAttribute2; 57 | 58 | ClientId = clientId; 59 | Authority = authority; 60 | LockRetryDelay = lockRetryDelay; 61 | LockRetryCount = lockRetryCount; 62 | 63 | Validate(); 64 | } 65 | 66 | private void Validate() 67 | { 68 | if (UseLinuxUnencryptedFallback && UseUnencryptedFallback) 69 | { 70 | throw new ArgumentException("UseLinuxUnencryptedFallback and UseUnencryptedFallback are mutually exclusive. UseLinuxUnencryptedFallback is the safer option. "); 71 | 72 | } 73 | if ((UseLinuxUnencryptedFallback || UseUnencryptedFallback) && 74 | ( 75 | !string.IsNullOrEmpty(KeyringSecretLabel) || 76 | !string.IsNullOrEmpty(KeyringSchemaName) || 77 | !string.IsNullOrEmpty(KeyringCollection))) 78 | { 79 | throw new ArgumentException("Using plaintext storage is mutually exclusive with other Linux storage options. "); 80 | } 81 | 82 | if ((UseUnencryptedFallback ) && 83 | ( !string.IsNullOrEmpty(MacKeyChainServiceName) || 84 | !string.IsNullOrEmpty(MacKeyChainAccountName))) 85 | { 86 | throw new ArgumentException("Using plaintext storage is mutually exclusive with other Mac storage options. "); 87 | 88 | } 89 | } 90 | 91 | /// 92 | /// Gets the full path to the cache file, combining the directory and filename. 93 | /// 94 | public string CacheFilePath { get; } 95 | 96 | /// 97 | /// The name of the cache file. 98 | /// 99 | public readonly string CacheFileName; 100 | 101 | /// 102 | /// The name of the directory containing the cache file. 103 | /// 104 | public readonly string CacheDirectory; 105 | 106 | /// 107 | /// The mac keychain service name. 108 | /// 109 | public readonly string MacKeyChainServiceName; 110 | 111 | /// 112 | /// The mac keychain account name. 113 | /// 114 | public readonly string MacKeyChainAccountName; 115 | 116 | /// 117 | /// The linux keyring schema name. 118 | /// 119 | public readonly string KeyringSchemaName; 120 | 121 | /// 122 | /// The linux keyring collection. 123 | /// 124 | public readonly string KeyringCollection; 125 | 126 | /// 127 | /// The linux keyring secret label. 128 | /// 129 | public readonly string KeyringSecretLabel; 130 | 131 | /// 132 | /// Additional linux keyring attribute. 133 | /// 134 | public readonly KeyValuePair KeyringAttribute1; 135 | 136 | /// 137 | /// Additional linux keyring attribute. 138 | /// 139 | public readonly KeyValuePair KeyringAttribute2; 140 | 141 | /// 142 | /// The delay between retries if a lock is contended and a retry is requested. (in ms) 143 | /// 144 | public readonly int LockRetryDelay; 145 | 146 | /// 147 | /// Flag which indicates that a plaintext file will be used on Linux for secret storage 148 | /// 149 | public readonly bool UseLinuxUnencryptedFallback; 150 | 151 | /// 152 | /// Flag which indicates that a plaintext file will be used on all OSes for secret storage 153 | /// 154 | public readonly bool UseUnencryptedFallback; 155 | 156 | /// 157 | /// The number of time to retry the lock if it is contended and retrying is possible 158 | /// 159 | public readonly int LockRetryCount; 160 | 161 | /// 162 | /// The client id. 163 | /// 164 | /// Only required for the MsalCacheHelper.CacheChanged event 165 | public string ClientId { get; } 166 | 167 | /// 168 | /// The authority 169 | /// 170 | /// Only required for the MsalCacheHelper.CacheChanged event 171 | public string Authority { get; } 172 | 173 | internal bool IsCacheEventConfigured 174 | { 175 | get 176 | { 177 | return !string.IsNullOrEmpty(ClientId) && 178 | !string.IsNullOrEmpty(Authority); 179 | } 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/Shared/TraceSourceLogger.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using System.Globalization; 7 | 8 | #if ADAL 9 | namespace Microsoft.Identity.Client.Extensions.Adal 10 | #elif MSAL 11 | namespace Microsoft.Identity.Client.Extensions.Msal 12 | #else // WEB 13 | namespace Microsoft.Identity.Client.Extensions.Web 14 | #endif 15 | { 16 | /// 17 | /// 18 | /// 19 | public class TraceSourceLogger 20 | { 21 | /// 22 | /// 23 | /// 24 | /// 25 | public TraceSourceLogger(TraceSource traceSource) 26 | { 27 | Source = traceSource; 28 | } 29 | 30 | /// 31 | /// 32 | /// 33 | public TraceSource Source { get; } 34 | 35 | /// 36 | /// 37 | /// 38 | /// 39 | public void LogInformation(string message) 40 | { 41 | Source.TraceEvent(TraceEventType.Information, /*id*/ 0, FormatLogMessage(message)); 42 | } 43 | 44 | /// 45 | /// 46 | /// 47 | public void LogError(string message) 48 | { 49 | Source.TraceEvent(TraceEventType.Error, /*id*/ 0, FormatLogMessage(message)); 50 | } 51 | 52 | /// 53 | /// 54 | /// 55 | /// 56 | public void LogWarning(string message) 57 | { 58 | Source.TraceEvent(TraceEventType.Warning, /*id*/ 0, FormatLogMessage(message)); 59 | } 60 | 61 | private static string FormatLogMessage(string message) 62 | { 63 | return $"[MSAL.Extension][{DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)}] {message}"; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Automation.TestApp/Automation.TestApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | $(TargetFrameworkNetCore) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/Automation.TestApp/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using System.Globalization; 7 | using System.IO; 8 | using System.Threading.Tasks; 9 | using Microsoft.Identity.Client.Extensions.Msal; 10 | 11 | namespace Automation.TestApp 12 | { 13 | 14 | 15 | 16 | /// 17 | /// 18 | /// 19 | public static class Program 20 | { 21 | private static readonly TimeSpan s_artificialContention = TimeSpan.FromMilliseconds(500); 22 | 23 | #pragma warning disable UseAsyncSuffix // Use Async suffix 24 | internal static async Task Main(string[] args) 25 | #pragma warning restore UseAsyncSuffix // Use Async suffix 26 | { 27 | 28 | string protectedFile; 29 | if (args == null || args.Length == 0 || string.IsNullOrEmpty(args[0])) 30 | { 31 | protectedFile = Path.Combine(Directory.GetCurrentDirectory(), "fileX.txt"); 32 | } 33 | else 34 | { 35 | protectedFile = args[0]; 36 | } 37 | 38 | string lockFile = protectedFile + ".lock"; 39 | 40 | await WritePayloadToSyncFileAsync(lockFile, protectedFile) 41 | .ConfigureAwait(false); 42 | 43 | return 0; 44 | } 45 | 46 | 47 | private async static Task WritePayloadToSyncFileAsync(string lockFile, string protectedFile) 48 | { 49 | string pid = Process.GetCurrentProcess().Id.ToString(CultureInfo.InvariantCulture); 50 | string errorFile = protectedFile + $"{pid}.e.txt"; 51 | string pidFIle = Path.Combine(Path.GetDirectoryName(protectedFile), pid + ".txt"); 52 | 53 | Console.WriteLine("Starting process: " + pid); 54 | CrossPlatLock crossPlatLock = null; 55 | try 56 | { 57 | crossPlatLock = new CrossPlatLock(lockFile); 58 | using (StreamWriter sw = new StreamWriter(protectedFile, true)) 59 | { 60 | await sw.WriteLineAsync($"< {pid} {DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)}").ConfigureAwait(false); 61 | 62 | // increase contention by simulating a slow writer 63 | await Task.Delay(s_artificialContention).ConfigureAwait(false); 64 | 65 | await sw.WriteLineAsync($"> {pid} {DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)}").ConfigureAwait(false); 66 | ; 67 | Console.WriteLine("Process finished: " + pid); 68 | 69 | } 70 | } 71 | catch (Exception e) 72 | { 73 | File.WriteAllText(errorFile, e.ToString()); 74 | throw; 75 | } 76 | finally 77 | { 78 | File.WriteAllText(pidFIle, "done"); 79 | crossPlatLock.Dispose(); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | 6 | 7 | net472 8 | netcoreapp3.1 9 | 10 | $(TargetFrameworkNetCore);$(TargetFrameworkNetDesktop); 11 | $(TargetFrameworkNetCore) 12 | $(TargetFrameworkNetCore) 13 | 14 | false 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/FileLockApp/FileLockApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/FileLockApp/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("AsyncUsage.CSharp.Naming", "UseAsyncSuffix:Use Async suffix", Justification = "", Scope = "member", Target = "~M:FileLockApp.Program.Main(System.String[])~System.Threading.Tasks.Task")] 9 | -------------------------------------------------------------------------------- /tests/FileLockApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Globalization; 4 | using System.IO; 5 | using System.Threading.Tasks; 6 | using Microsoft.Identity.Client.Extensions.Msal; 7 | 8 | namespace FileLockApp 9 | { 10 | class Program 11 | { 12 | public static async Task Main(string[] args) 13 | { 14 | if (args.Length < 2) 15 | { 16 | PrintUsage(); 17 | Console.Read(); 18 | return; 19 | } 20 | 21 | string filePath = args[0]; 22 | if (!File.Exists(filePath)) 23 | { 24 | File.Create(filePath); 25 | } 26 | int delay = int.Parse(args[1], CultureInfo.InvariantCulture); 27 | 28 | // this object tries to acquire the file lock every 100ms and gives up after 600 attempts (about 1 min) 29 | using (var crossPlatLock = new CrossPlatLock(filePath + ".lockfile")) 30 | { 31 | Console.WriteLine("Acquired the lock..."); 32 | 33 | Console.WriteLine("Writing..."); 34 | 35 | File.WriteAllText(filePath, "< " + Process.GetCurrentProcess().Id); 36 | Console.WriteLine($"Waiting for {delay}s"); 37 | 38 | await Task.Delay(delay * 1000).ConfigureAwait(false); 39 | 40 | Console.WriteLine("Writing..."); 41 | File.WriteAllText(filePath, "> " + Process.GetCurrentProcess().Id); 42 | } 43 | } 44 | 45 | private static void PrintUsage() 46 | { 47 | Console.WriteLine("Usage: FileLockApp.exe "); 48 | Console.WriteLine(""); 49 | Console.WriteLine("This app will acquire a file lock on the file_path "); 50 | Console.WriteLine("Will write '< process_id' "); 51 | Console.WriteLine("Sleep for delay_in_seconds"); 52 | Console.WriteLine("Will write '> process_id' "); 53 | 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/KeyChainTestApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Microsoft.Identity.Client.Extensions.Msal; 4 | 5 | namespace KeyChainTestApp 6 | { 7 | class Program 8 | { 9 | private static TraceSourceLogger s_logger = 10 | new TraceSourceLogger(new System.Diagnostics.TraceSource("CacheExt.TestApp")); 11 | 12 | private static byte[] s_payload = Encoding.UTF8.GetBytes("Hello world from the MSAL cache test app"); 13 | static void Main(string[] args) 14 | { 15 | if (!SharedUtilities.IsMacPlatform()) 16 | { 17 | Console.WriteLine("This app should run on a Mac"); 18 | Console.ReadLine(); 19 | return; 20 | } 21 | 22 | while (true) 23 | { 24 | // Display menu 25 | Console.WriteLine($@" 26 | 1. Test KeyChain entry similar to PowerShell 27 | 2. Test KeyChain entry different location (read - write - read - delete) 28 | 29 | Enter your Selection: "); 30 | char.TryParse(Console.ReadLine(), out var selection); 31 | try 32 | { 33 | switch (selection)  34 | { 35 | case '1': 36 | MacKeychainAccessor macKeychainAccessor1 = 37 | new MacKeychainAccessor( 38 | cacheFilePath: "~/.local/.IdentityService", 39 | keyChainServiceName: "Microsoft.Developer.IdentityService", 40 | keyChainAccountName: "msal.cache", 41 | s_logger); 42 | 43 | TestAccessors(macKeychainAccessor1); 44 | 45 | 46 | break; 47 | case '2': 48 | 49 | Console.WriteLine("Type a keychain service or Enter to use `Microsoft.Developer.IdentityService` "); 50 | string service = Console.ReadLine(); 51 | if (string.IsNullOrEmpty(service)) 52 | { 53 | service = "Microsoft.Developer.IdentityService"; 54 | 55 | } 56 | 57 | Console.WriteLine("Type a keychain account or Enter to use `msal.cache.2` "); 58 | string account = Console.ReadLine(); 59 | if (string.IsNullOrEmpty(account)) 60 | { 61 | account = $"msal.cache.2"; 62 | } 63 | 64 | Console.WriteLine($"Using Account {account} and Service: {service}"); 65 | 66 | MacKeychainAccessor macKeychainAccessor2 = 67 | new MacKeychainAccessor( 68 | cacheFilePath: "~/.local/microsoft.test.txt", 69 | keyChainServiceName: service, 70 | keyChainAccountName: account, 71 | s_logger); 72 | 73 | ReadOrReadWriteClear(macKeychainAccessor2); 74 | 75 | break; 76 | 77 | 78 | 79 | 80 | 81 | } 82 | } 83 | catch (Exception ex) 84 | { 85 | PrintException(ex); 86 | } 87 | } 88 | } 89 | 90 | private static void PrintException(Exception ex) 91 | { 92 | Console.ForegroundColor = ConsoleColor.Red; 93 | Console.WriteLine("Exception : " + ex); 94 | Console.ResetColor(); 95 | Console.WriteLine("Hit Enter to continue"); 96 | 97 | Console.Read(); 98 | } 99 | 100 | private static void TestAccessors(ICacheAccessor macKeychainAccessor1) 101 | { 102 | var persistenceValidator = macKeychainAccessor1.CreateForPersistenceValidation(); 103 | try 104 | { 105 | Console.WriteLine("Trying the location used for validation first .. "); 106 | ReadOrReadWriteClear(persistenceValidator); 107 | 108 | } 109 | catch (Exception e) 110 | { 111 | PrintException(e); 112 | } 113 | 114 | try 115 | { 116 | Console.WriteLine("Trying the real location"); 117 | ReadOrReadWriteClear(macKeychainAccessor1); 118 | } 119 | catch (Exception e) 120 | { 121 | PrintException(e); 122 | } 123 | } 124 | 125 | 126 | 127 | private static void ReadOrReadWriteClear(ICacheAccessor accessor) 128 | { 129 | 130 | Console.WriteLine(accessor.ToString()); 131 | var bytes = accessor.Read(); 132 | if (bytes == null || bytes.Length == 0) 133 | { 134 | Console.WriteLine("No data found, writing some"); 135 | accessor.Write(s_payload); 136 | var bytes2 = accessor.Read(); 137 | accessor.Clear(); 138 | Console.WriteLine("All good"); 139 | 140 | } 141 | else 142 | { 143 | string s = Encoding.UTF8.GetString(bytes) ; 144 | Console.WriteLine($"Found some data ... {s.Substring(0,20)}..."); 145 | 146 | Console.WriteLine("Stopping"); 147 | 148 | } 149 | 150 | 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /tests/KeyChainTestApp/StorageTestApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/KeyChainTestApp/readme.md: -------------------------------------------------------------------------------- 1 | This simple app tries to read - write - delete from various locations of the `login` keychain. Its purpose is to troubleshoot keychain sharing issues. 2 | 3 | 1. Clone https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet 4 | 2. Make sure you have .NET Core SDK installed, at least version 3.1 - https://dotnet.microsoft.com/download. Check by running `dotnet --list-sdks` 5 | 3. Navigate to _repo_\tests\KeyChainTestApp 6 | 4. Build using `dotnet build` 7 | 5. Run using `dotnet run` 8 | 9 | 6. In the app, try option 1 and then option 2. Errors will be printed in red in the console. 10 | 11 | Note: please hit "always allow" or "allow" if prompted 12 | 13 | 7. Try to delete KeyChain entry Microsoft.Developer.IdentityService and try step 6 again 14 | 8. Try to use the app that fails (PowerShell etc.) again -------------------------------------------------------------------------------- /tests/Microsoft.Identity.Client.Extensions.Adal.UnitTests/AdalCacheStorageTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.Linq; 9 | using Microsoft.VisualStudio.TestTools.UnitTesting; 10 | 11 | namespace Microsoft.Identity.Client.Extensions.Adal.UnitTests 12 | { 13 | [TestClass] 14 | public class AdalCacheStorageTests 15 | { 16 | public static readonly string CacheFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); 17 | private readonly TraceSource _logger = new TraceSource("TestSource"); 18 | private static StorageCreationProperties s_storageCreationProperties; 19 | 20 | [ClassInitialize] 21 | public static void ClassInitialize(TestContext _) 22 | { 23 | var builder = new StorageCreationPropertiesBuilder(Path.GetFileName(CacheFilePath), Path.GetDirectoryName(CacheFilePath)); 24 | builder = builder.WithMacKeyChain(serviceName: "Microsoft.Developer.IdentityService", accountName: "ADALCache"); 25 | builder = builder.WithLinuxKeyring( 26 | schemaName: "IdentityServiceAdalCache.cache", 27 | collection: "default", 28 | secretLabel: "ADALCache", 29 | attribute1: new KeyValuePair("AdalClientID", "Microsoft.Developer.IdentityService"), 30 | attribute2: new KeyValuePair("AdalClientVersion", "1.0.0.0")); 31 | s_storageCreationProperties = builder.Build(); 32 | } 33 | 34 | [TestInitialize] 35 | public void TestInitialize() 36 | { 37 | CleanTestData(); 38 | } 39 | 40 | [TestCleanup] 41 | public void TestCleanup() 42 | { 43 | CleanTestData(); 44 | } 45 | 46 | [TestMethod] 47 | public void AdalTestUserDirectory() 48 | { 49 | Assert.AreEqual(AdalCacheStorage.UserRootDirectory, 50 | Environment.OSVersion.Platform == PlatformID.Win32NT 51 | ? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) 52 | : Environment.GetEnvironmentVariable("HOME")); 53 | } 54 | 55 | [TestMethod] 56 | public void AdalNewStoreNoFile() 57 | { 58 | var store = new AdalCacheStorage(s_storageCreationProperties, logger: _logger); 59 | Assert.IsFalse(store.ReadData().Any()); 60 | } 61 | 62 | [TestMethod] 63 | public void AdalWriteEmptyData() 64 | { 65 | var store = new AdalCacheStorage(s_storageCreationProperties, logger: _logger); 66 | Assert.ThrowsException(() => store.WriteData(null)); 67 | 68 | store.WriteData(new byte[0]); 69 | 70 | var pass = store.ReadData(); 71 | Assert.IsFalse(pass.Any()); 72 | } 73 | 74 | [DoNotRunOnLinux] 75 | public void AdalWriteGoodData() 76 | { 77 | var store = new AdalCacheStorage(s_storageCreationProperties, logger: _logger); 78 | Assert.ThrowsException(() => store.WriteData(null)); 79 | 80 | byte[] data = { 2, 2, 3 }; 81 | byte[] data2 = { 2, 2, 3, 4, 4 }; 82 | store.WriteData(data); 83 | Assert.IsTrue(Enumerable.SequenceEqual(store.ReadData(), data)); 84 | 85 | store.WriteData(data); 86 | store.WriteData(data2); 87 | store.WriteData(data); 88 | store.WriteData(data2); 89 | Assert.IsTrue(Enumerable.SequenceEqual(store.ReadData(), data2)); 90 | } 91 | 92 | [DoNotRunOnLinux] 93 | public void AdalTestClear() 94 | { 95 | var store = new AdalCacheStorage(s_storageCreationProperties, logger: _logger); 96 | var store2 = new AdalCacheStorage(s_storageCreationProperties, logger: _logger); 97 | Assert.IsNotNull(Exception(() => store.WriteData(null))); 98 | 99 | byte[] data = { 2, 2, 3 }; 100 | store.WriteData(data); 101 | store2.ReadData(); 102 | 103 | Assert.IsTrue(Enumerable.SequenceEqual(store.ReadData(), data)); 104 | Assert.IsTrue(File.Exists(CacheFilePath)); 105 | 106 | store.Clear(); 107 | 108 | Assert.IsFalse(store.ReadData().Any()); 109 | Assert.IsFalse(store2.ReadData().Any()); 110 | Assert.IsFalse(File.Exists(CacheFilePath)); 111 | } 112 | 113 | /// 114 | /// Records an exception thrown when executing the provided action 115 | /// 116 | /// The type of exception to record 117 | /// The action to execute 118 | /// The exception if thrown; otherwise, null 119 | private static TException Exception(Action action) 120 | where TException : Exception 121 | { 122 | try 123 | { 124 | action(); 125 | return null; 126 | } 127 | catch (TException ex) 128 | { 129 | return ex; 130 | } 131 | } 132 | 133 | private void CleanTestData() 134 | { 135 | if (File.Exists(CacheFilePath)) 136 | { 137 | File.Delete(CacheFilePath); 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /tests/Microsoft.Identity.Client.Extensions.Adal.UnitTests/AdalCacheTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.Security.Cryptography; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using Microsoft.IdentityModel.Clients.ActiveDirectory; 11 | using Microsoft.VisualStudio.TestTools.UnitTesting; 12 | 13 | namespace Microsoft.Identity.Client.Extensions.Adal.UnitTests 14 | { 15 | [TestClass] 16 | public class AdalCacheTests 17 | { 18 | public static readonly string CacheFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); 19 | private readonly TraceSource _logger = new TraceSource("TestSource"); 20 | private static StorageCreationProperties s_storageCreationProperties; 21 | 22 | [ClassInitialize] 23 | public static void ClassInitialize(TestContext _) 24 | { 25 | var builder = new StorageCreationPropertiesBuilder(Path.GetFileName(CacheFilePath), Path.GetDirectoryName(CacheFilePath)); 26 | builder = builder.WithMacKeyChain(serviceName: "Microsoft.Developer.IdentityService", accountName: "MSALCache"); 27 | builder = builder.WithLinuxKeyring( 28 | schemaName: "adal.cache", 29 | collection: "default", 30 | secretLabel: "ADALCache", 31 | attribute1: new KeyValuePair("ADALClientID", "Microsoft.Developer.IdentityService"), 32 | attribute2: new KeyValuePair("AdalClientVersion", "1.0.0.0")); 33 | s_storageCreationProperties = builder.Build(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Microsoft.Identity.Client.Extensions.Adal.UnitTests/Microsoft.Identity.Client.Extensions.Adal.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(TestTargetFrameworks) 5 | $(DefineConstants);ADAL 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/Microsoft.Identity.Client.Extensions.Adal.UnitTests/RunOnPlatformAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Runtime.InteropServices; 6 | using Microsoft.VisualStudio.TestTools.UnitTesting; 7 | using Xunit; 8 | 9 | namespace Microsoft.Identity.Client.Extensions.Adal.UnitTests 10 | { 11 | public class RunOnOSXAttribute : RunOnPlatformAttribute 12 | { 13 | public RunOnOSXAttribute() : base(OSPlatform.OSX) 14 | { 15 | } 16 | } 17 | 18 | public class RunOnWindowsAttribute : RunOnPlatformAttribute 19 | { 20 | public RunOnWindowsAttribute() : base(OSPlatform.Windows) 21 | { 22 | } 23 | } 24 | 25 | public class RunOnLinuxAttribute : RunOnPlatformAttribute 26 | { 27 | public RunOnLinuxAttribute() : base(OSPlatform.Linux) 28 | { 29 | } 30 | } 31 | 32 | public class DoNotRunOnLinuxAttribute : DoNotRunOnPlatformAttribute 33 | { 34 | public DoNotRunOnLinuxAttribute() : base(OSPlatform.Linux) 35 | { 36 | } 37 | } 38 | 39 | public class RunOnPlatformAttribute : TestMethodAttribute 40 | { 41 | private readonly OSPlatform _platform; 42 | 43 | protected RunOnPlatformAttribute(OSPlatform platform) 44 | { 45 | _platform = platform; 46 | } 47 | 48 | public override TestResult[] Execute(ITestMethod testMethod) 49 | { 50 | if ((SharedUtilities.IsLinuxPlatform() && _platform != OSPlatform.Linux) || 51 | (SharedUtilities.IsMacPlatform() && _platform != OSPlatform.OSX) || 52 | (SharedUtilities.IsWindowsPlatform() && _platform != OSPlatform.Windows)) 53 | { 54 | return new[] 55 | { 56 | new TestResult 57 | { 58 | Outcome = UnitTestOutcome.Inconclusive, 59 | TestFailureException = new AssertInconclusiveException("Skipped on platform") 60 | } 61 | }; 62 | } 63 | 64 | return base.Execute(testMethod); 65 | } 66 | } 67 | 68 | public class DoNotRunOnPlatformAttribute : TestMethodAttribute 69 | { 70 | private readonly OSPlatform _platform; 71 | 72 | protected DoNotRunOnPlatformAttribute(OSPlatform platform) 73 | { 74 | _platform = platform; 75 | } 76 | 77 | public override TestResult[] Execute(ITestMethod testMethod) 78 | { 79 | if ((SharedUtilities.IsLinuxPlatform() && _platform == OSPlatform.Linux) || 80 | (SharedUtilities.IsMacPlatform() && _platform == OSPlatform.OSX) || 81 | (SharedUtilities.IsWindowsPlatform() && _platform == OSPlatform.Windows)) 82 | { 83 | return new[] 84 | { 85 | new TestResult 86 | { 87 | Outcome = UnitTestOutcome.Inconclusive, 88 | TestFailureException = new AssertInconclusiveException("Skipped on platform") 89 | } 90 | }; 91 | } 92 | 93 | return base.Execute(testMethod); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/Microsoft.Identity.Client.Extensions.Msal.UnitTests/AssertException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using System.Globalization; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using Microsoft.VisualStudio.TestTools.UnitTesting; 10 | 11 | namespace Microsoft.Identity.Client.Extensions.Msal.UnitTests 12 | { 13 | public static class AssertException 14 | { 15 | public static void DoesNotThrow(Action testCode) 16 | { 17 | var ex = Recorder.Exception(testCode); 18 | if (ex != null) 19 | { 20 | throw new AssertFailedException("DoesNotThrow failed.", ex); 21 | } 22 | } 23 | 24 | public static void DoesNotThrow(Func testCode) 25 | { 26 | var ex = Recorder.Exception(testCode); 27 | if (ex != null) 28 | { 29 | throw new AssertFailedException("DoesNotThrow failed.", ex); 30 | } 31 | } 32 | 33 | public static TException Throws(Action testCode) 34 | where TException : Exception 35 | { 36 | return Throws(testCode, false); 37 | } 38 | 39 | public static TException Throws(Action testCode, bool allowDerived) 40 | where TException : Exception 41 | { 42 | var exception = Recorder.Exception(testCode); 43 | 44 | if (exception == null) 45 | { 46 | throw new AssertFailedException("AssertExtensions.Throws failed. No exception occurred."); 47 | } 48 | 49 | CheckExceptionType(exception, allowDerived); 50 | 51 | return exception; 52 | } 53 | 54 | public static TException Throws(Func testCode) 55 | where TException : Exception 56 | { 57 | return Throws(testCode, false); 58 | } 59 | 60 | public static TException Throws(Func testCode, bool allowDerived) 61 | where TException : Exception 62 | { 63 | var exception = Recorder.Exception(testCode); 64 | 65 | if (exception == null) 66 | { 67 | throw new AssertFailedException("AssertExtensions.Throws failed. No exception occurred."); 68 | } 69 | 70 | CheckExceptionType(exception, allowDerived); 71 | 72 | return exception; 73 | } 74 | 75 | public static async Task TaskThrowsAsync(Func testCode, bool allowDerived = false) 76 | where T : Exception 77 | { 78 | Exception exception = null; 79 | try 80 | { 81 | await testCode().ConfigureAwait(false); 82 | } 83 | catch (Exception ex) 84 | { 85 | exception = ex; 86 | } 87 | 88 | if (exception == null) 89 | { 90 | throw new AssertFailedException("AssertExtensions.TaskThrowsAsync failed. No exception occurred."); 91 | } 92 | 93 | if (exception is AggregateException aggEx) 94 | { 95 | if (aggEx.InnerException.GetType() == typeof(AssertFailedException)) 96 | { 97 | throw aggEx.InnerException; 98 | } 99 | 100 | var exceptionsMatching = aggEx.InnerExceptions.OfType().ToList(); 101 | 102 | if (!exceptionsMatching.Any()) 103 | { 104 | ThrowAssertFailedForExceptionMismatch(typeof(T), exception); 105 | } 106 | 107 | return exceptionsMatching.First(); 108 | } 109 | 110 | CheckExceptionType(exception, allowDerived); 111 | 112 | return (exception as T); 113 | } 114 | 115 | public static void TaskDoesNotThrow(Func testCode) 116 | { 117 | var exception = Recorder.Exception(() => testCode().Wait()); 118 | 119 | if (exception == null) 120 | { 121 | return; 122 | } 123 | 124 | throw new AssertFailedException( 125 | string.Format( 126 | CultureInfo.CurrentCulture, 127 | "AssertExtensions.TaskDoesNotThrow failed. Incorrect exception {0} occurred.", 128 | exception.GetType().Name), 129 | exception); 130 | } 131 | 132 | public static void TaskDoesNotThrow(Func testCode) where T : Exception 133 | { 134 | var exception = Recorder.Exception(() => testCode().Wait()); 135 | 136 | if (exception == null) 137 | { 138 | return; 139 | } 140 | 141 | var exceptionsMatching = exception.InnerExceptions.OfType().ToList(); 142 | 143 | if (!exceptionsMatching.Any()) 144 | { 145 | return; 146 | } 147 | 148 | ThrowAssertFailedForExceptionMismatch(typeof(T), exception); 149 | } 150 | 151 | private static void CheckExceptionType(Exception actualException, bool allowDerived) 152 | { 153 | Type expectedType = typeof(TException); 154 | 155 | string message = string.Format( 156 | CultureInfo.CurrentCulture, 157 | "Checking exception:{0}\tType:{1}{0}\tToString: {2}{0}", 158 | Environment.NewLine, 159 | actualException.GetType().FullName, 160 | actualException.ToString()); 161 | 162 | Debug.WriteLine(message); 163 | 164 | if (allowDerived) 165 | { 166 | if (!(actualException is TException)) 167 | { 168 | ThrowAssertFailedForExceptionMismatch(expectedType, actualException); 169 | } 170 | } 171 | else 172 | { 173 | if (!expectedType.Equals(actualException.GetType())) 174 | { 175 | ThrowAssertFailedForExceptionMismatch(expectedType, actualException); 176 | } 177 | } 178 | } 179 | 180 | private static void ThrowAssertFailedForExceptionMismatch(Type expectedExceptionType, Exception actualException) 181 | { 182 | throw new AssertFailedException( 183 | string.Format( 184 | CultureInfo.CurrentCulture, 185 | "Exception types do not match. Expected: {0} Actual: {1}", 186 | expectedExceptionType.Name, 187 | actualException.GetType().Name), 188 | actualException); 189 | } 190 | 191 | private static class Recorder 192 | { 193 | public static Exception Exception(Action code) 194 | { 195 | try 196 | { 197 | code(); 198 | return null; 199 | } 200 | catch (Exception e) 201 | { 202 | return e; 203 | } 204 | } 205 | 206 | public static TException Exception(Action code) 207 | where TException : Exception 208 | { 209 | try 210 | { 211 | code(); 212 | return null; 213 | } 214 | catch (TException ex) 215 | { 216 | return ex; 217 | } 218 | catch (Exception e) 219 | { 220 | throw new AssertFailedException($"Expected to capture a {typeof(TException)} exception but got {e.GetType()}"); 221 | } 222 | } 223 | 224 | public static TException Exception(Func code) 225 | where TException : Exception 226 | { 227 | try 228 | { 229 | code(); 230 | return null; 231 | } 232 | catch (TException ex) 233 | { 234 | return ex; 235 | } 236 | catch (Exception e) 237 | { 238 | throw new AssertFailedException($"Expected to capture a {typeof(TException)} exception but got {e.GetType()}"); 239 | } 240 | } 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /tests/Microsoft.Identity.Client.Extensions.Msal.UnitTests/CrossPlatLockTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | using System; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using Microsoft.VisualStudio.TestTools.UnitTesting; 9 | 10 | namespace Microsoft.Identity.Client.Extensions.Msal.UnitTests 11 | { 12 | [TestClass] 13 | public class CrossPlatLockTests 14 | { 15 | const int NumTasks = 100; 16 | 17 | public TestContext TestContext { get; set; } 18 | 19 | [RunOnWindows] 20 | [TestCategory(TestCategories.Regression)] 21 | [WorkItem(187)] // https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet/issues/187 22 | public void DirNotExists() 23 | { 24 | // Arrange 25 | 26 | string cacheFileDir; 27 | 28 | // ensure the cache directory does not exist 29 | do 30 | { 31 | string tempDirName = System.IO.Path.GetRandomFileName(); 32 | cacheFileDir = Path.Combine(Path.GetTempPath(), tempDirName); 33 | 34 | } while (Directory.Exists(cacheFileDir)); 35 | 36 | using (var crossPlatLock = new CrossPlatLock( 37 | Path.Combine(cacheFileDir, "file.lockfile"), // the directory is guaranteed to not exist 38 | 100, 39 | 1)) 40 | { 41 | // no-op 42 | } 43 | 44 | // before fixing the bug, an exception would occur here: 45 | // System.InvalidOperationException: Could not get access to the shared lock file. 46 | // ---> System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\Users\.... 47 | } 48 | 49 | 50 | 51 | 52 | #if NETCOREAPP 53 | [TestMethod] 54 | [Ignore] // Could not get this to run on CI build due to small differences in where the App file gets dropped 55 | public async Task MultipleProcessesUseAccessorAsync() 56 | { 57 | Trace.WriteLine("Starting test on " + TestHelper.GetOs()); 58 | string dir = Path.Combine(Directory.GetCurrentDirectory(), "AutomationApp"); 59 | 60 | if (!Directory.Exists(dir)) 61 | { 62 | Assert.Fail("Directory does not exist!" + dir); 63 | } 64 | 65 | string protectedFile = Path.Combine(dir, "protected_file.txt"); 66 | 67 | File.Delete(protectedFile); 68 | 69 | 70 | Trace.WriteLine($"Working dir: {dir}"); 71 | 72 | ProcessStartInfo psi = new ProcessStartInfo(); 73 | 74 | 75 | psi.FileName = "dotnet"; 76 | string dll = Path.Combine(dir, "Automation.TestApp.dll"); 77 | psi.Arguments = dll + " " + protectedFile; 78 | 79 | psi.WorkingDirectory = dir; 80 | psi.UseShellExecute = false; 81 | 82 | 83 | var procs = Enumerable.Range(1, NumTasks) 84 | .Select((n) => 85 | { 86 | Process proc = Process.Start(psi); 87 | Trace.WriteLine($"Process start {proc.Id}"); 88 | return proc; 89 | }) 90 | .ToList(); 91 | 92 | var tasks = procs .Select(pr => pr.WaitForExitAsync()); 93 | 94 | //await Task.Delay(30 * 1000).ConfigureAwait(false); 95 | await Task.WhenAll(tasks).ConfigureAwait(false); 96 | 97 | 98 | ValidateResult(protectedFile, NumTasks); 99 | } 100 | 101 | private void ValidateResult(string protectedFile, int expectedNumberOfOperations) 102 | { 103 | Trace.WriteLine("Protected File Content:"); 104 | Trace.WriteLine(File.ReadAllText(protectedFile)); 105 | 106 | var lines = File.ReadAllLines(protectedFile); 107 | 108 | string previousThread = null; 109 | 110 | foreach (var line in lines) 111 | { 112 | var tokens = line.Split(' '); 113 | string inOutToken = tokens[0]; 114 | string payload = tokens[1]; 115 | 116 | Assert.IsTrue(!string.IsNullOrWhiteSpace(payload)); 117 | if (previousThread != null) 118 | { 119 | Assert.AreEqual(payload, previousThread); 120 | Assert.AreEqual(">", inOutToken); 121 | previousThread = null; 122 | } 123 | else 124 | { 125 | Assert.AreEqual("<", inOutToken); 126 | previousThread = payload; 127 | } 128 | } 129 | 130 | Assert.AreEqual(expectedNumberOfOperations * 2, lines.Count()); 131 | 132 | } 133 | #endif 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /tests/Microsoft.Identity.Client.Extensions.Msal.UnitTests/FileIOWithRetriesTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Microsoft.VisualStudio.TestTools.UnitTesting; 10 | 11 | namespace Microsoft.Identity.Client.Extensions.Msal.UnitTests 12 | { 13 | [TestClass] 14 | public class FileIOWithRetriesTests 15 | { 16 | private TraceSource _logger; 17 | private TraceStringListener _testListener; 18 | 19 | [TestInitialize] 20 | public void TestInitialize() 21 | { 22 | _logger = new TraceSource("TestSource", SourceLevels.All); 23 | _testListener = new TraceStringListener(); 24 | 25 | _logger.Listeners.Add(_testListener); 26 | } 27 | 28 | [TestMethod] 29 | public async Task Touch_FiresEvents_Async() 30 | { 31 | _logger.TraceInformation($"Starting test on " + TestHelper.GetOs()); 32 | 33 | // a directory and a path that do not exist 34 | string dir = Path.Combine(Directory.GetCurrentDirectory(), Guid.NewGuid().ToString()); 35 | Directory.CreateDirectory(dir); 36 | string fileName = "testFile"; 37 | string path = Path.Combine(dir, fileName); 38 | 39 | FileSystemWatcher watcher = new FileSystemWatcher(dir, fileName); 40 | watcher.EnableRaisingEvents = true; 41 | var semaphore = new SemaphoreSlim(0); 42 | int cacheChangedEventFired = 0; 43 | 44 | // expect this event to be fired twice 45 | watcher.Changed += (sender, args) => 46 | { 47 | _logger.TraceInformation("Event fired!"); 48 | cacheChangedEventFired++; 49 | semaphore.Release(); 50 | }; 51 | 52 | Assert.IsFalse(File.Exists(path)); 53 | try 54 | { 55 | _logger.TraceInformation($"Touch once"); 56 | 57 | FileIOWithRetries.TouchFile(path, new TraceSourceLogger(_logger)); 58 | DateTime initialLastWriteTime = File.GetLastWriteTimeUtc(path); 59 | Assert.IsTrue(File.Exists(path)); 60 | 61 | // LastWriteTime is not granular enough 62 | await Task.Delay(50).ConfigureAwait(false); 63 | 64 | _logger.TraceInformation($"Touch twice"); 65 | FileIOWithRetries.TouchFile(path, new TraceSourceLogger(_logger)); 66 | Assert.IsTrue(File.Exists(path)); 67 | 68 | DateTime subsequentLastWriteTime = File.GetLastWriteTimeUtc(path); 69 | Assert.IsTrue(subsequentLastWriteTime > initialLastWriteTime); 70 | 71 | _logger.TraceInformation($"Semaphore at {semaphore.CurrentCount}"); 72 | await semaphore.WaitAsync(5000).ConfigureAwait(false); 73 | _logger.TraceInformation($"Semaphore at {semaphore.CurrentCount}"); 74 | await semaphore.WaitAsync(10000).ConfigureAwait(false); 75 | Assert.IsTrue( 76 | cacheChangedEventFired==2 || 77 | cacheChangedEventFired == 3, 78 | "Expecting the event to be fired 2 times as the file is touched 2 times. On Linux, " + 79 | "the file watcher sometimes fires an additional time for the file creation"); 80 | } 81 | finally 82 | { 83 | _logger.TraceInformation("Cleaning up"); 84 | Trace.WriteLine(_testListener.CurrentLog); 85 | File.Delete(path); 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/Microsoft.Identity.Client.Extensions.Msal.UnitTests/IntegrationTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.Security.Cryptography; 9 | using System.Text; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Microsoft.Identity.Client; 13 | using Microsoft.Identity.Client.Utils; 14 | using Microsoft.VisualStudio.TestTools.UnitTesting; 15 | using NSubstitute; 16 | using NSubstitute.ExceptionExtensions; 17 | 18 | namespace Microsoft.Identity.Client.Extensions.Msal.UnitTests 19 | { 20 | [TestClass] 21 | public class IntegrationTests 22 | { 23 | public static readonly string CacheFilePath = Path.Combine( 24 | Path.GetTempPath(), 25 | Path.GetTempFileName()); 26 | private readonly TraceSource _logger = new TraceSource("TestSource"); 27 | public const string ClientId = "1d18b3b0-251b-4714-a02a-9956cec86c2d"; 28 | 29 | private MsalCacheHelper _cacheHelper; 30 | 31 | [TestInitialize] 32 | public void TestInitialize() 33 | { 34 | var storageBuilder = new StorageCreationPropertiesBuilder( 35 | Path.GetFileName(CacheFilePath), 36 | Path.GetDirectoryName(CacheFilePath)); 37 | storageBuilder = storageBuilder.WithMacKeyChain( 38 | serviceName: "Microsoft.Developer.IdentityService.Test", 39 | accountName: "MSALCacheTest"); 40 | 41 | // unit tests run on Linux boxes without LibSecret 42 | storageBuilder.WithLinuxUnprotectedFile(); 43 | 44 | // 1. Use MSAL to create an instance of the Public Client Application 45 | var app = PublicClientApplicationBuilder.Create(ClientId) 46 | .Build(); 47 | 48 | // 3. Create the high level MsalCacheHelper based on properties and a logger 49 | _cacheHelper = MsalCacheHelper.CreateAsync( 50 | storageBuilder.Build(), 51 | new TraceSource("MSAL.CacheExtension.Test")) 52 | .GetAwaiter().GetResult(); 53 | 54 | // 4. Let the cache helper handle MSAL's cache 55 | _cacheHelper.RegisterCache(app.UserTokenCache); 56 | } 57 | 58 | [TestMethod] 59 | public void ImportExport() 60 | { 61 | var storageBuilder = new StorageCreationPropertiesBuilder( 62 | Path.GetFileName(CacheFilePath), 63 | Path.GetDirectoryName(CacheFilePath)); 64 | 65 | storageBuilder = storageBuilder.WithMacKeyChain( 66 | serviceName: "Microsoft.Developer.IdentityService.Test", 67 | accountName: "MSALCacheTest"); 68 | 69 | // unit tests run on Linux boxes without LibSecret 70 | storageBuilder.WithLinuxUnprotectedFile(); 71 | 72 | // 1. Use MSAL to create an instance of the Public Client Application 73 | var app = PublicClientApplicationBuilder.Create(ClientId) 74 | .Build(); 75 | 76 | // 3. Create the high level MsalCacheHelper based on properties and a logger 77 | _cacheHelper = MsalCacheHelper.CreateAsync( 78 | storageBuilder.Build(), 79 | new TraceSource("MSAL.CacheExtension.Test")) 80 | .GetAwaiter().GetResult(); 81 | 82 | // 4. Let the cache helper handle MSAL's cache 83 | _cacheHelper.RegisterCache(app.UserTokenCache); 84 | 85 | // Act 86 | string dataString = "Hello World"; 87 | byte[] dataBytes = Encoding.UTF8.GetBytes(dataString); 88 | var result = _cacheHelper.LoadUnencryptedTokenCache(); 89 | Assert.AreEqual(0, result.Length); 90 | 91 | _cacheHelper.SaveUnencryptedTokenCache(dataBytes); 92 | byte[] actualData = _cacheHelper.LoadUnencryptedTokenCache(); 93 | 94 | Assert.AreEqual(dataString, Encoding.UTF8.GetString(actualData)); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/Microsoft.Identity.Client.Extensions.Msal.UnitTests/MacKeyChainTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using System.Globalization; 7 | using System.Runtime.InteropServices; 8 | using System.Text; 9 | using Microsoft.Identity.Client.Extensions.Msal.UnitTests; 10 | using Microsoft.VisualStudio.TestTools.UnitTesting; 11 | using Xunit.Sdk; 12 | 13 | namespace Microsoft.Identity.Client.Extensions.Msal.UnitTests 14 | { 15 | [TestClass] 16 | public class MacKeyChainTests 17 | { 18 | private const string ServiceName = "foo"; 19 | private const string AccountName = "bar"; 20 | private const string TestNamespace = "msal-test"; 21 | 22 | MacOSKeychain _macOSKeychain; 23 | [TestInitialize] 24 | public void TestInitialize() 25 | { 26 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 27 | { 28 | _macOSKeychain = new MacOSKeychain(); 29 | } 30 | } 31 | 32 | [TestCleanup] 33 | public void TestCleanup() 34 | { 35 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 36 | { 37 | _macOSKeychain.Remove(ServiceName, AccountName); 38 | } 39 | } 40 | 41 | [RunOnOSX] 42 | public void TestWriteKey() 43 | { 44 | string data = "applesauce"; 45 | 46 | _macOSKeychain.AddOrUpdate(ServiceName, AccountName, Encoding.UTF8.GetBytes(data)); 47 | VerifyKey(ServiceName, AccountName, expectedData: data); 48 | } 49 | 50 | [RunOnOSX] 51 | public void TestWriteSameKeyTwice() 52 | { 53 | string data = "applesauce"; 54 | 55 | _macOSKeychain.AddOrUpdate(ServiceName, AccountName, Encoding.UTF8.GetBytes(data)); 56 | VerifyKey(ServiceName, AccountName, expectedData: data); 57 | 58 | _macOSKeychain.AddOrUpdate(ServiceName, AccountName, Encoding.UTF8.GetBytes(data)); 59 | VerifyKey(ServiceName, AccountName, expectedData: data); 60 | } 61 | 62 | [RunOnOSX] 63 | public void TestWriteSameKeyTwiceWithDifferentData() 64 | { 65 | string data = "applesauce"; 66 | _macOSKeychain.AddOrUpdate(ServiceName, AccountName, Encoding.UTF8.GetBytes(data)); 67 | VerifyKey(ServiceName, AccountName, expectedData: data); 68 | 69 | data = "tomatosauce"; 70 | _macOSKeychain.AddOrUpdate(ServiceName, AccountName, Encoding.UTF8.GetBytes(data)); 71 | VerifyKey(ServiceName, AccountName, expectedData: data); 72 | } 73 | 74 | [RunOnOSX] 75 | public void TestRetrieveKey() 76 | { 77 | string data = "applesauce"; 78 | 79 | _macOSKeychain.AddOrUpdate(ServiceName, AccountName, Encoding.UTF8.GetBytes(data)); 80 | VerifyKey(ServiceName, AccountName, expectedData: data); 81 | } 82 | 83 | [RunOnOSX] 84 | public void TestRetrieveNonExistingKey() 85 | { 86 | VerifyKeyIsNull(ServiceName, AccountName); 87 | } 88 | 89 | [RunOnOSX] 90 | public void TestDeleteKey() 91 | { 92 | string data = "applesauce"; 93 | 94 | _macOSKeychain.AddOrUpdate(ServiceName, AccountName, Encoding.UTF8.GetBytes(data)); 95 | VerifyKey(ServiceName, AccountName, expectedData: data); 96 | 97 | _macOSKeychain.Remove(ServiceName, AccountName); 98 | VerifyKeyIsNull(ServiceName, AccountName); 99 | } 100 | 101 | [RunOnOSX] 102 | public void TestDeleteNonExistingKey() 103 | { 104 | _macOSKeychain.Remove(ServiceName, AccountName); 105 | } 106 | 107 | [RunOnOSX] 108 | public void MacOSKeychain_Get_NotFound_ReturnsNull() 109 | { 110 | var keychain = new MacOSKeychain(TestNamespace); 111 | 112 | // Unique service; guaranteed not to exist! 113 | string service = $"https://example.com/{Guid.NewGuid():N}"; 114 | 115 | var credential = keychain.Get(service, account: null); 116 | Assert.IsNull(credential); 117 | } 118 | 119 | [RunOnOSX] 120 | public void MacOSKeychain_ReadWriteDelete() 121 | { 122 | var keychain = new MacOSKeychain(TestNamespace); 123 | 124 | // Create a service that is guaranteed to be unique 125 | string service = $"https://example.com/{Guid.NewGuid():N}"; 126 | const string account = "john.doe"; 127 | const string password = "letmein123"; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Fake credential")] 128 | 129 | try 130 | { 131 | // Write 132 | keychain.AddOrUpdate(service, account, Encoding.UTF8.GetBytes(password)); 133 | 134 | // Read 135 | var outCredential = keychain.Get(service, account); 136 | var stringPassword = Encoding.UTF8.GetString(outCredential.Password); 137 | 138 | Assert.IsNotNull(outCredential); 139 | Assert.AreEqual(account, outCredential.Account); 140 | Assert.AreEqual(password, stringPassword); 141 | } 142 | finally 143 | { 144 | // Ensure we clean up after ourselves even in case of 'get' failures 145 | keychain.Remove(service, account); 146 | } 147 | } 148 | 149 | [RunOnOSX] 150 | public void MacOSKeychain_Remove_NotFound_ReturnsFalse() 151 | { 152 | var keychain = new MacOSKeychain(TestNamespace); 153 | 154 | // Unique service; guaranteed not to exist! 155 | string service = $"https://example.com/{Guid.NewGuid():N}"; 156 | 157 | bool result = keychain.Remove(service, account: null); 158 | Assert.IsFalse(result); 159 | } 160 | 161 | private void VerifyKey(string serviceName, string accountName, string expectedData) 162 | { 163 | var entry = _macOSKeychain.Get(serviceName, accountName); 164 | Assert.AreEqual(expectedData, Encoding.UTF8.GetString(entry.Password)); 165 | } 166 | 167 | private void VerifyKeyIsNull(string serviceName, string accountName) 168 | { 169 | if (_macOSKeychain.Get(serviceName, accountName) != null) 170 | { 171 | Assert.Fail( 172 | "key exists when it shouldn't be. keychainData=\"{0}\"", 173 | _macOSKeychain.Get(serviceName, accountName).Password); 174 | } 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /tests/Microsoft.Identity.Client.Extensions.Msal.UnitTests/Microsoft.Identity.Client.Extensions.Msal.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(TestTargetFrameworks) 5 | $(DefineConstants);MSAL 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Always 42 | 43 | 44 | Always 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | false 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /tests/Microsoft.Identity.Client.Extensions.Msal.UnitTests/MockTokenCache.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Microsoft.Identity.Client; 9 | using Microsoft.Identity.Client.Cache; 10 | 11 | #pragma warning disable CS0618 // Type or member is obsolete (CacheData) 12 | namespace Microsoft.Identity.Client.Extensions.Msal.UnitTests 13 | { 14 | internal class MockTokenCache : ITokenCache, ITokenCacheSerializer 15 | { 16 | private TokenCacheCallback _beforeAccess; 17 | private TokenCacheCallback _afterAccess; 18 | 19 | internal int DeserializeMsalV3_ClearCache { get; set; } 20 | internal int DeserializeMsalV3_MergeCache { get; set; } 21 | 22 | internal string LastDeserializedString { get; set; } 23 | 24 | public void Deserialize(byte[] msalV2State) 25 | { 26 | throw new NotImplementedException(); 27 | } 28 | 29 | public void DeserializeAdalV3(byte[] adalV3State) 30 | { 31 | throw new NotImplementedException(); 32 | } 33 | 34 | public void DeserializeMsalV2(byte[] msalV2State) 35 | { 36 | throw new NotImplementedException(); 37 | } 38 | 39 | public void DeserializeMsalV3(byte[] msalV3State, bool shouldClearExistingCache = false) 40 | { 41 | LastDeserializedString = Encoding.UTF8.GetString(msalV3State); 42 | 43 | if (shouldClearExistingCache) 44 | { 45 | DeserializeMsalV3_ClearCache++; 46 | } 47 | else 48 | { 49 | DeserializeMsalV3_MergeCache++; 50 | } 51 | } 52 | 53 | public void DeserializeUnifiedAndAdalCache(CacheData cacheData) 54 | { 55 | throw new NotImplementedException(); 56 | } 57 | 58 | public byte[] Serialize() 59 | { 60 | throw new NotImplementedException(); 61 | } 62 | 63 | public byte[] SerializeAdalV3() 64 | { 65 | throw new NotImplementedException(); 66 | } 67 | 68 | public byte[] SerializeMsalV2() 69 | { 70 | throw new NotImplementedException(); 71 | } 72 | 73 | public byte[] SerializeMsalV3() 74 | { 75 | return Encoding.UTF8.GetBytes(LastDeserializedString); 76 | } 77 | 78 | public CacheData SerializeUnifiedAndAdalCache() 79 | { 80 | throw new NotImplementedException(); 81 | } 82 | 83 | public void SetAfterAccess(TokenCacheCallback afterAccess) 84 | { 85 | _afterAccess = afterAccess; 86 | } 87 | 88 | public void SetAfterAccessAsync(Func afterAccess) => throw new NotImplementedException(); 89 | 90 | public void SetBeforeAccess(TokenCacheCallback beforeAccess) 91 | { 92 | _beforeAccess = beforeAccess; 93 | } 94 | 95 | public void SetBeforeAccessAsync(Func beforeAccess) => throw new NotImplementedException(); 96 | 97 | public void SetBeforeWrite(TokenCacheCallback beforeWrite) 98 | { 99 | throw new NotImplementedException(); 100 | } 101 | 102 | public void SetBeforeWriteAsync(Func beforeWrite) => throw new NotImplementedException(); 103 | } 104 | } 105 | #pragma warning restore CS0618 // Type or member is obsolete 106 | -------------------------------------------------------------------------------- /tests/Microsoft.Identity.Client.Extensions.Msal.UnitTests/MsalCacheStorageIntegrationTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using Microsoft.VisualStudio.TestTools.UnitTesting; 11 | 12 | namespace Microsoft.Identity.Client.Extensions.Msal.UnitTests 13 | { 14 | /// 15 | /// These tests write data to disk / key chain / key ring etc. 16 | /// 17 | [TestClass] 18 | public class MsalCacheStorageIntegrationTests 19 | { 20 | public static readonly string CacheFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); 21 | private readonly TraceSource _logger = new TraceSource("TestSource", SourceLevels.All); 22 | private static StorageCreationProperties s_storageCreationProperties; 23 | 24 | public TestContext TestContext { get; set; } 25 | 26 | [ClassInitialize] 27 | public static void ClassInitialize(TestContext _) 28 | { 29 | var builder = new StorageCreationPropertiesBuilder( 30 | Path.GetFileName(CacheFilePath), 31 | Path.GetDirectoryName(CacheFilePath)); 32 | builder = builder.WithMacKeyChain(serviceName: "Microsoft.Developer.IdentityService", accountName: "MSALCache"); 33 | 34 | // Tests run on machines without Libsecret 35 | builder = builder.WithLinuxUnprotectedFile(); 36 | s_storageCreationProperties = builder.Build(); 37 | } 38 | 39 | [TestInitialize] 40 | public void TestiInitialize() 41 | { 42 | CleanTestData(); 43 | } 44 | 45 | [TestCleanup] 46 | public void TestCleanup() 47 | { 48 | CleanTestData(); 49 | } 50 | 51 | [TestMethod] 52 | public void MsalTestUserDirectory() 53 | { 54 | Assert.AreEqual(MsalCacheHelper.UserRootDirectory, 55 | Environment.OSVersion.Platform == PlatformID.Win32NT 56 | ? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) 57 | : Environment.GetEnvironmentVariable("HOME")); 58 | } 59 | 60 | [RunOnOSX] 61 | public void CacheStorageFactoryMac() 62 | { 63 | Storage store = Storage.Create(s_storageCreationProperties, logger: _logger); 64 | Assert.IsTrue(store.CacheAccessor is MacKeychainAccessor); 65 | store.VerifyPersistence(); 66 | 67 | store = Storage.Create(s_storageCreationProperties, logger: _logger); 68 | Assert.IsTrue(store.CacheAccessor is MacKeychainAccessor); 69 | } 70 | 71 | [RunOnWindows] 72 | public void CacheStorageFactoryWindows() 73 | { 74 | Storage store = Storage.Create(s_storageCreationProperties, logger: _logger); 75 | Assert.IsTrue(store.CacheAccessor is DpApiEncryptedFileAccessor); 76 | store.VerifyPersistence(); 77 | 78 | store = Storage.Create(s_storageCreationProperties, logger: _logger); 79 | Assert.IsTrue(store.CacheAccessor is DpApiEncryptedFileAccessor); 80 | } 81 | 82 | [TestMethod] 83 | public void CacheFallback() 84 | { 85 | const string data = "data"; 86 | string cacheFilePathFallback = CacheFilePath + "fallback"; 87 | var plaintextStorage = new StorageCreationPropertiesBuilder( 88 | Path.GetFileName(cacheFilePathFallback), 89 | Path.GetDirectoryName(CacheFilePath)) 90 | .WithUnprotectedFile() 91 | .Build(); 92 | 93 | Storage unprotectedStore = Storage.Create(plaintextStorage, _logger); 94 | Assert.IsTrue(unprotectedStore.CacheAccessor is FileAccessor); 95 | 96 | unprotectedStore.VerifyPersistence(); 97 | unprotectedStore.WriteData(Encoding.UTF8.GetBytes(data)); 98 | 99 | // Unprotected cache file should exist 100 | Assert.IsTrue(File.Exists(plaintextStorage.CacheFilePath)); 101 | 102 | string dataReadFromPlaintext = File.ReadAllText(plaintextStorage.CacheFilePath); 103 | 104 | Assert.AreEqual(data, dataReadFromPlaintext); 105 | 106 | // Verify that file permissions are set to 600 107 | FileHelper.AssertChmod600(plaintextStorage.CacheFilePath); 108 | } 109 | 110 | 111 | 112 | [RunOnLinux] 113 | public void CacheStorageFactory_WithFallback_Linux() 114 | { 115 | var storageWithKeyRing = new StorageCreationPropertiesBuilder( 116 | Path.GetFileName(CacheFilePath), 117 | Path.GetDirectoryName(CacheFilePath)) 118 | .WithMacKeyChain(serviceName: "Microsoft.Developer.IdentityService", accountName: "MSALCache") 119 | .WithLinuxKeyring( 120 | schemaName: "msal.cache", 121 | collection: "default", 122 | secretLabel: "MSALCache", 123 | attribute1: new KeyValuePair("MsalClientID", "Microsoft.Developer.IdentityService"), 124 | attribute2: new KeyValuePair("MsalClientVersion", "1.0.0.0")) 125 | .Build(); 126 | 127 | // Tests run on machines without Libsecret 128 | Storage store = Storage.Create(storageWithKeyRing, logger: _logger); 129 | Assert.IsTrue(store.CacheAccessor is LinuxKeyringAccessor); 130 | 131 | // ADO Linux test agents do not have libsecret installed by default 132 | // If you run this test on a Linux box with UI / LibSecret, then this test will fail 133 | // because the statement below will not throw. 134 | AssertException.Throws( 135 | () => store.VerifyPersistence()); 136 | 137 | Storage unprotectedStore = Storage.Create(s_storageCreationProperties, _logger); 138 | Assert.IsTrue(unprotectedStore.CacheAccessor is FileAccessor); 139 | 140 | unprotectedStore.VerifyPersistence(); 141 | 142 | unprotectedStore.WriteData(new byte[] { 2, 3 }); 143 | 144 | // Unproteced cache file should exist 145 | Assert.IsTrue(File.Exists(s_storageCreationProperties.CacheFilePath)); 146 | 147 | // Mimic another sdk client to check libsecret availability by calling 148 | // MsalCacheStorage.VerifyPeristence() -> LinuxKeyringAccessor.CreateForPersistenceValidation() 149 | AssertException.Throws( 150 | () => store.VerifyPersistence()); 151 | 152 | // Verify above call doesn't delete existing cache file 153 | Assert.IsTrue(File.Exists(s_storageCreationProperties.CacheFilePath)); 154 | 155 | // Verify that file permissions are set to 600 156 | FileHelper.AssertChmod600(s_storageCreationProperties.CacheFilePath); 157 | } 158 | 159 | [TestMethod] 160 | public void MsalNewStoreNoFile() 161 | { 162 | var store = Storage.Create(s_storageCreationProperties, logger: _logger); 163 | Assert.IsFalse(store.ReadData().Any()); 164 | } 165 | 166 | [TestMethod] 167 | public void MsalWriteEmptyData() 168 | { 169 | var store = Storage.Create(s_storageCreationProperties, logger: _logger); 170 | Assert.ThrowsException(() => store.WriteData(null)); 171 | 172 | store.WriteData(new byte[0]); 173 | 174 | Assert.IsFalse(store.ReadData().Any()); 175 | } 176 | 177 | [TestMethod] 178 | public void MsalWriteGoodData() 179 | { 180 | var store = Storage.Create(s_storageCreationProperties, logger: _logger); 181 | Assert.ThrowsException(() => store.WriteData(null)); 182 | 183 | byte[] data = { 2, 2, 3 }; 184 | byte[] data2 = { 2, 2, 3, 4, 4 }; 185 | store.WriteData(data); 186 | Assert.IsTrue(Enumerable.SequenceEqual(store.ReadData(), data)); 187 | 188 | store.WriteData(data); 189 | store.WriteData(data2); 190 | store.WriteData(data); 191 | store.WriteData(data2); 192 | Assert.IsTrue(Enumerable.SequenceEqual(store.ReadData(), data2)); 193 | } 194 | 195 | [TestMethod] 196 | public void MsalTestClear() 197 | { 198 | var store = Storage.Create(s_storageCreationProperties, logger: _logger); 199 | store.ReadData(); 200 | 201 | var store2 = Storage.Create(s_storageCreationProperties, logger: _logger); 202 | AssertException.Throws(() => store.WriteData(null)); 203 | 204 | byte[] data = { 2, 2, 3 }; 205 | store.WriteData(data); 206 | store2.ReadData(); 207 | 208 | Assert.IsTrue(Enumerable.SequenceEqual(store.ReadData(), data)); 209 | Assert.IsTrue(File.Exists(CacheFilePath)); 210 | 211 | store.Clear(); 212 | 213 | Assert.IsFalse(store.ReadData().Any()); 214 | Assert.IsFalse(store2.ReadData().Any()); 215 | Assert.IsFalse(File.Exists(CacheFilePath)); 216 | } 217 | 218 | private void CleanTestData() 219 | { 220 | var store = Storage.Create(s_storageCreationProperties, logger: _logger); 221 | store.Clear(); 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /tests/Microsoft.Identity.Client.Extensions.Msal.UnitTests/ResourceHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.IO; 5 | 6 | namespace Microsoft.Identity.Client.Extensions.Msal.UnitTests 7 | { 8 | public class ResourceHelper 9 | { 10 | /// 11 | /// Gets the relative path to a test resource. Resource should be using DeploymentItem (desktop) or 12 | /// by setting Copy to Output Directory to Always (other platforms) 13 | /// 14 | /// 15 | /// This is just a simple workaround for DeploymentItem not being implemented in mstest on netcore 16 | /// Tests seems to run from the bin directory and not from a TestRun dir on netcore 17 | /// Assumes resources are in a Resources dir. 18 | /// 19 | public static string GetTestResourceRelativePath(string resourceName) 20 | { 21 | 22 | #if NET472 23 | return resourceName; 24 | #else 25 | return Path.Combine("Resources", resourceName); 26 | #endif 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Microsoft.Identity.Client.Extensions.Msal.UnitTests/Resources/token_cache_adfs.json: -------------------------------------------------------------------------------- 1 | { 2 | "AccessToken": { 3 | "unzyjm1nugflhp1l338bxhryyhbtcyowx1p+f2fcsiq=-fs.msidlab8.com-accesstoken-publicclientid--openid": { 4 | "home_account_id": "unZYjm1NugFLhP1l338bxhRYyHbtCYOWX1P+F2fCsiQ=", 5 | "environment": "fs.msidlab8.com", 6 | "client_id": "PublicClientId", 7 | "secret": "secret", 8 | "credential_type": "AccessToken", 9 | "target": "openid", 10 | "cached_at": "1613060010", 11 | "expires_on": "1613063610", 12 | "extended_expires_on": "-62135596800", 13 | "ext_expires_on": "-62135596800" 14 | } 15 | }, 16 | "RefreshToken": { 17 | "unzyjm1nugflhp1l338bxhryyhbtcyowx1p+f2fcsiq=-fs.msidlab8.com-refreshtoken-publicclientid--": { 18 | "home_account_id": "unZYjm1NugFLhP1l338bxhRYyHbtCYOWX1P+F2fCsiQ=", 19 | "environment": "fs.msidlab8.com", 20 | "client_id": "PublicClientId", 21 | "secret": "secret", 22 | "credential_type": "RefreshToken" 23 | } 24 | }, 25 | "IdToken": { 26 | "unzyjm1nugflhp1l338bxhryyhbtcyowx1p+f2fcsiq=-fs.msidlab8.com-idtoken-publicclientid--": { 27 | "home_account_id": "unZYjm1NugFLhP1l338bxhRYyHbtCYOWX1P+F2fCsiQ=", 28 | "environment": "fs.msidlab8.com", 29 | "client_id": "PublicClientId", 30 | "secret": "s", 31 | "credential_type": "IdToken" 32 | } 33 | }, 34 | "Account": { 35 | "unZYjm1NugFLhP1l338bxhRYyHbtCYOWX1P+F2fCsiQ=-fs.msidlab8.com-": { 36 | "home_account_id": "unZYjm1NugFLhP1l338bxhRYyHbtCYOWX1P+F2fCsiQ=", 37 | "environment": "fs.msidlab8.com", 38 | "username": "fidlab@msidlab8.com", 39 | "authority_type": "MSSTS" 40 | } 41 | }, 42 | "AppMetadata": { 43 | "appmetadata-fs.msidlab8.com-publicclientid": { 44 | "environment": "fs.msidlab8.com", 45 | "client_id": "PublicClientId" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Microsoft.Identity.Client.Extensions.Msal.UnitTests/Resources/token_cache_one_acc_seed.json: -------------------------------------------------------------------------------- 1 | { 2 | "AccessToken": { 3 | "ae821e4d-f408-451a-af82-882691148603.49f548d0-12b7-4169-a390-bb5304d24462-login.windows.net-accesstoken-1d18b3b0-251b-4714-a02a-9956cec86c2d-49f548d0-12b7-4169-a390-bb5304d24462-files.readwrite files.readwrite.all openid profile user.read user.readbasic.all user.readwrite email": { 4 | "home_account_id": "ae821e4d-f408-451a-af82-882691148603.49f548d0-12b7-4169-a390-bb5304d24462", 5 | "environment": "login.windows.net", 6 | "client_info": "eyJ1aWQiOiJhZTgyMWU0ZC1mNDA4LTQ1MWEtYWY4Mi04ODI2OTExNDg2MDMiLCJ1dGlkIjoiNDlmNTQ4ZDAtMTJiNy00MTY5LWEzOTAtYmI1MzA0ZDI0NDYyIn0", 7 | "client_id": "1d18b3b0-251b-4714-a02a-9956cec86c2d", 8 | "secret": "secret", 9 | "credential_type": "AccessToken", 10 | "realm": "49f548d0-12b7-4169-a390-bb5304d24462", 11 | "target": "Files.ReadWrite Files.ReadWrite.All openid profile User.Read User.ReadBasic.All User.ReadWrite email", 12 | "cached_at": "1583427476", 13 | "expires_on": "1583431075", 14 | "extended_expires_on": "1583431075", 15 | "ext_expires_on": "1583431075" 16 | } 17 | }, 18 | "RefreshToken": { 19 | "ae821e4d-f408-451a-af82-882691148603.49f548d0-12b7-4169-a390-bb5304d24462-login.windows.net-refreshtoken-1d18b3b0-251b-4714-a02a-9956cec86c2d--": { 20 | "home_account_id": "ae821e4d-f408-451a-af82-882691148603.49f548d0-12b7-4169-a390-bb5304d24462", 21 | "environment": "login.windows.net", 22 | "client_info": "eyJ1aWQiOiJhZTgyMWU0ZC1mNDA4LTQ1MWEtYWY4Mi04ODI2OTExNDg2MDMiLCJ1dGlkIjoiNDlmNTQ4ZDAtMTJiNy00MTY5LWEzOTAtYmI1MzA0ZDI0NDYyIn0", 23 | "client_id": "1d18b3b0-251b-4714-a02a-9956cec86c2d", 24 | "secret": "secret", 25 | "credential_type": "RefreshToken" 26 | } 27 | }, 28 | "IdToken": { 29 | "ae821e4d-f408-451a-af82-882691148603.49f548d0-12b7-4169-a390-bb5304d24462-login.windows.net-idtoken-1d18b3b0-251b-4714-a02a-9956cec86c2d-49f548d0-12b7-4169-a390-bb5304d24462-": { 30 | "home_account_id": "ae821e4d-f408-451a-af82-882691148603.49f548d0-12b7-4169-a390-bb5304d24462", 31 | "environment": "login.windows.net", 32 | "client_info": "eyJ1aWQiOiJhZTgyMWU0ZC1mNDA4LTQ1MWEtYWY4Mi04ODI2OTExNDg2MDMiLCJ1dGlkIjoiNDlmNTQ4ZDAtMTJiNy00MTY5LWEzOTAtYmI1MzA0ZDI0NDYyIn0", 33 | "client_id": "1d18b3b0-251b-4714-a02a-9956cec86c2d", 34 | "secret": "secret", 35 | "credential_type": "IdToken", 36 | "realm": "49f548d0-12b7-4169-a390-bb5304d24462" 37 | } 38 | }, 39 | "Account": { 40 | "ae821e4d-f408-451a-af82-882691148603.49f548d0-12b7-4169-a390-bb5304d24462-login.windows.net-49f548d0-12b7-4169-a390-bb5304d24462": { 41 | "home_account_id": "ae821e4d-f408-451a-af82-882691148603.49f548d0-12b7-4169-a390-bb5304d24462", 42 | "environment": "login.windows.net", 43 | "client_info": "eyJ1aWQiOiJhZTgyMWU0ZC1mNDA4LTQ1MWEtYWY4Mi04ODI2OTExNDg2MDMiLCJ1dGlkIjoiNDlmNTQ4ZDAtMTJiNy00MTY5LWEzOTAtYmI1MzA0ZDI0NDYyIn0", 44 | "username": "liu.kang@bogavrilLTD.onmicrosoft.com", 45 | "name": "Liu Kang", 46 | "local_account_id": "ae821e4d-f408-451a-af82-882691148603", 47 | "authority_type": "MSSTS", 48 | "realm": "49f548d0-12b7-4169-a390-bb5304d24462" 49 | } 50 | }, 51 | "AppMetadata": { 52 | "appmetadata-login.windows.net-1d18b3b0-251b-4714-a02a-9956cec86c2d": { 53 | "environment": "login.windows.net", 54 | "client_id": "1d18b3b0-251b-4714-a02a-9956cec86c2d" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Microsoft.Identity.Client.Extensions.Msal.UnitTests/RunOnPlatformAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Runtime.InteropServices; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | 7 | namespace Microsoft.Identity.Client.Extensions.Msal.UnitTests 8 | { 9 | public class RunOnOSXAttribute : RunOnPlatformAttribute 10 | { 11 | public RunOnOSXAttribute() : base(OSPlatform.OSX) 12 | { 13 | } 14 | } 15 | 16 | public class RunOnWindowsAttribute : RunOnPlatformAttribute 17 | { 18 | public RunOnWindowsAttribute() : base(OSPlatform.Windows) 19 | { 20 | } 21 | } 22 | 23 | public class RunOnLinuxAttribute : RunOnPlatformAttribute 24 | { 25 | public RunOnLinuxAttribute() : base(OSPlatform.Linux) 26 | { 27 | } 28 | } 29 | 30 | public class DoNotRunOnWindowsAttribute : DoNotRunOnPlatformAttribute 31 | { 32 | public DoNotRunOnWindowsAttribute(): base(OSPlatform.Windows) 33 | { 34 | 35 | } 36 | } 37 | 38 | public class DoNotRunOnLinuxAttribute : DoNotRunOnPlatformAttribute 39 | { 40 | public DoNotRunOnLinuxAttribute() : base(OSPlatform.Linux) 41 | { 42 | } 43 | } 44 | 45 | public class RunOnPlatformAttribute : TestMethodAttribute 46 | { 47 | private readonly OSPlatform _platform; 48 | 49 | protected RunOnPlatformAttribute(OSPlatform platform) 50 | { 51 | _platform = platform; 52 | } 53 | 54 | public override TestResult[] Execute(ITestMethod testMethod) 55 | { 56 | if ((SharedUtilities.IsLinuxPlatform() && _platform != OSPlatform.Linux) || 57 | (SharedUtilities.IsMacPlatform() && _platform != OSPlatform.OSX) || 58 | (SharedUtilities.IsWindowsPlatform() && _platform != OSPlatform.Windows)) 59 | { 60 | return new[] 61 | { 62 | new TestResult 63 | { 64 | Outcome = UnitTestOutcome.Inconclusive, 65 | TestFailureException = new AssertInconclusiveException("Skipped on platform") 66 | } 67 | }; 68 | } 69 | 70 | return base.Execute(testMethod); 71 | } 72 | } 73 | 74 | 75 | public class DoNotRunOnPlatformAttribute : TestMethodAttribute 76 | { 77 | private readonly OSPlatform _platform; 78 | 79 | protected DoNotRunOnPlatformAttribute(OSPlatform platform) 80 | { 81 | _platform = platform; 82 | } 83 | 84 | public override TestResult[] Execute(ITestMethod testMethod) 85 | { 86 | if ((SharedUtilities.IsLinuxPlatform() && _platform == OSPlatform.Linux) || 87 | (SharedUtilities.IsMacPlatform() && _platform == OSPlatform.OSX) || 88 | (SharedUtilities.IsWindowsPlatform() && _platform == OSPlatform.Windows)) 89 | { 90 | return new[] 91 | { 92 | new TestResult 93 | { 94 | Outcome = UnitTestOutcome.Inconclusive, 95 | TestFailureException = new AssertInconclusiveException("Skipped on platform") 96 | } 97 | }; 98 | } 99 | 100 | return base.Execute(testMethod); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/Microsoft.Identity.Client.Extensions.Msal.UnitTests/TestCategories.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Microsoft.Identity.Client.Extensions.Msal.UnitTests 5 | { 6 | public static class TestCategories 7 | { 8 | public const string Regression = "Regression"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/Microsoft.Identity.Client.Extensions.Msal.UnitTests/TestHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Security.AccessControl; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Microsoft.VisualStudio.TestTools.UnitTesting; 12 | 13 | namespace Microsoft.Identity.Client.Extensions.Msal.UnitTests 14 | { 15 | public static class TestHelper 16 | { 17 | /// 18 | /// Waits asynchronously for the process to exit. 19 | /// 20 | /// The process to wait for cancellation. 21 | /// A cancellation token. If invoked, the task will return 22 | /// immediately as canceled. 23 | /// A Task representing waiting for the process to end. 24 | public static Task WaitForExitAsync(this Process process, 25 | CancellationToken cancellationToken = default(CancellationToken)) 26 | { 27 | var tcs = new TaskCompletionSource(); 28 | process.EnableRaisingEvents = true; 29 | process.Exited += (sender, args) => 30 | { 31 | Trace.WriteLine($"Process finished {process.Id}"); 32 | tcs.TrySetResult(null); 33 | }; 34 | 35 | if (cancellationToken != default(CancellationToken)) 36 | { 37 | cancellationToken.Register(tcs.SetCanceled); 38 | } 39 | 40 | return tcs.Task; 41 | } 42 | public static string GetOs() 43 | { 44 | if (SharedUtilities.IsLinuxPlatform()) 45 | { 46 | return "Linux"; 47 | } 48 | 49 | if (SharedUtilities.IsMacPlatform()) 50 | { 51 | return "Mac"; 52 | } 53 | 54 | if (SharedUtilities.IsWindowsPlatform()) 55 | { 56 | return "Windows"; 57 | } 58 | 59 | throw new InvalidOperationException("Unknown"); 60 | } 61 | } 62 | 63 | public static class FileHelper 64 | { 65 | /// 66 | /// Checks that file permissions are set to 600. 67 | /// 68 | /// 69 | public static void AssertChmod600(string filePath) 70 | { 71 | if (SharedUtilities.IsWindowsPlatform()) 72 | { 73 | FileInfo fi = new FileInfo(filePath); 74 | var acl = fi.GetAccessControl(); 75 | var accessRules = acl.GetAccessRules(true, true, typeof(System.Security.Principal.SecurityIdentifier)); 76 | 77 | Assert.AreEqual(1, accessRules.Count); 78 | 79 | var rule = accessRules.Cast().Single(); 80 | 81 | Assert.AreEqual(FileSystemRights.Read | FileSystemRights.Write | FileSystemRights.Synchronize, rule.FileSystemRights); 82 | Assert.AreEqual(AccessControlType.Allow, rule.AccessControlType); 83 | Assert.AreEqual(System.Security.Principal.WindowsIdentity.GetCurrent().User, rule.IdentityReference); 84 | Assert.IsFalse(rule.IsInherited); 85 | Assert.AreEqual(InheritanceFlags.None, rule.InheritanceFlags); 86 | } 87 | else 88 | { 89 | // e.g. -rw------ 1 user1 user1 1280 Mar 23 08:39 /home/user1/g/Program.cs 90 | var output = ExecuteAndCaptureOutput($"ls -l {filePath}"); 91 | Assert.IsTrue(output.StartsWith("-rw------")); // 600 92 | } 93 | } 94 | 95 | private static string ExecuteAndCaptureOutput(string cmd) 96 | { 97 | var escapedArgs = cmd.Replace("\"", "\\\""); 98 | 99 | var process = new Process 100 | { 101 | StartInfo = new ProcessStartInfo 102 | { 103 | RedirectStandardOutput = true, 104 | UseShellExecute = false, 105 | CreateNoWindow = true, 106 | WindowStyle = ProcessWindowStyle.Hidden, 107 | FileName = "/bin/bash", 108 | Arguments = $"-c \"{escapedArgs}\"" 109 | } 110 | }; 111 | 112 | string output = string.Empty; 113 | 114 | process.OutputDataReceived += (sender, args) => 115 | { 116 | output += args.Data; 117 | }; 118 | 119 | process.Start(); 120 | process.BeginOutputReadLine(); 121 | process.WaitForExit(); 122 | 123 | return output; 124 | 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /tests/Microsoft.Identity.Client.Extensions.Msal.UnitTests/TraceStringListener.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using System.Globalization; 7 | using System.Text; 8 | using Microsoft.VisualStudio.TestTools.UnitTesting; 9 | 10 | namespace Microsoft.Identity.Client.Extensions.Msal.UnitTests 11 | { 12 | public class TraceStringListener : TraceListener 13 | { 14 | private const string TraceSourceName = "TestSource"; 15 | 16 | public static (TraceSource, TraceStringListener) Create() 17 | { 18 | var logger = new TraceSource(TraceSourceName, SourceLevels.All); 19 | var listner = new TraceStringListener(); 20 | 21 | logger.Listeners.Add(listner); 22 | 23 | return (logger, listner); 24 | } 25 | 26 | private readonly StringBuilder _log = new StringBuilder(); 27 | 28 | public string CurrentLog => _log.ToString(); 29 | 30 | public override void Write(string message) 31 | { 32 | _log.Append(FormatLogMessage(message)); 33 | } 34 | 35 | public override void WriteLine(string message) 36 | { 37 | _log.AppendLine(FormatLogMessage(message)); 38 | } 39 | 40 | public void AssertContains(string needle) 41 | { 42 | Assert.IsTrue(CurrentLog.Contains(needle)); 43 | } 44 | 45 | private static string FormatLogMessage(string message) 46 | { 47 | return $"[TEST][{DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)}] {message}"; 48 | } 49 | 50 | public void AssertContainsError(string needle) 51 | { 52 | AssertContains(TraceSourceName + " error"); 53 | AssertContains(needle); 54 | } 55 | } 56 | } 57 | --------------------------------------------------------------------------------