├── .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 | [](https://www.nuget.org/packages/Microsoft.Identity.Client.Extensions.Msal/)
10 |
11 | [](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