├── .devcontainer └── devcontainer.json ├── .github └── workflows │ └── stale.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── .vsts-ci.yml ├── .vsts-pr.yml ├── Build.props ├── CONTRIBUTING.md ├── CredentialProvider.Microsoft.Tests ├── CredentialProvider.Microsoft.Tests.csproj ├── CredentialProviders │ ├── Vsts │ │ ├── AuthUtilTests.cs │ │ ├── EnvironmentLock.cs │ │ └── VstsCredentialProviderTests.cs │ ├── VstsBuildTask │ │ └── VstsBuildTaskCredentialProviderTests.cs │ └── VstsBuildTaskServiceEndpoint │ │ └── VstsBuildTaskServiceEndpointCredentialProviderTests.cs ├── Logging │ └── LoggingTests.cs └── Util │ ├── FeedEndpointCredentialParserTests.cs │ └── HttpClientFactoryTests.cs ├── CredentialProvider.Microsoft.VSIX ├── Microsoft.CredentialProvider.swixproj ├── Microsoft.CredentialProvider.swr ├── Microsoft.CredentialProvider.vsmanproj ├── nuget.config └── packages.config ├── CredentialProvider.Microsoft ├── CredentialProvider.Microsoft.csproj ├── CredentialProvider.Microsoft.nuspec ├── CredentialProviderArgs.cs ├── CredentialProviders │ ├── CredentialProviderBase.cs │ ├── ICredentialProvider.cs │ ├── Vsts │ │ ├── IAuthUtil.cs │ │ ├── IVstsSessionTokenClient.cs │ │ ├── IVstsSessionTokenFromBearerTokenProvider.cs │ │ ├── MsalTokenProvidersFactory.cs │ │ ├── VstsCredentialProvider.cs │ │ ├── VstsSessionToken.cs │ │ ├── VstsSessionTokenClient.cs │ │ └── VstsSessionTokenFromBearerTokenProvider.cs │ ├── VstsBuildTask │ │ └── VstsBuildTaskCredentialProvider.cs │ └── VstsBuildTaskServiceEndpoint │ │ ├── VstsBuildTaskMsalTokenProvidersFactory.cs │ │ └── VstsBuildTaskServiceEndpointCredentialProvider.cs ├── EULA_Microsoft Visual Studio Team Services Credential Provider.docx ├── Logging │ ├── ConsoleLoggers.cs │ ├── CredentialResult.cs │ ├── ILogger.cs │ ├── LogEveryMessageFileLogger.cs │ ├── LoggerBase.cs │ ├── MultiLogger.cs │ ├── NuGetLoggerAdapter.cs │ └── PluginConnectionLogger.cs ├── Program.cs ├── RequestHandlers │ ├── GetAuthenticationCredentialsRequestHandler.cs │ ├── GetOperationClaimsRequestHandler.cs │ ├── InitializeRequestHandler.cs │ ├── RequestHandlerBase.cs │ ├── RequestHandlerCollection.cs │ ├── SetCredentialsRequestHandler.cs │ └── SetLogLevelRequestHandler.cs ├── Resources.resx ├── ThirdPartyNotices.txt ├── Util │ ├── CertificateUtil.cs │ ├── EncryptedFileWithPermissions.cs │ ├── EnvUtil.cs │ ├── ExtensionMethods.cs │ ├── FeedEndpointCredentialsParser.cs │ ├── HttpClientFactory.cs │ ├── ICache.cs │ └── SessionTokenCache.cs ├── helpericons.ico └── runtimeconfig.template.json ├── Directory.Build.props ├── Directory.Build.targets ├── Directory.Packages.props ├── LICENSE ├── MicrosoftCredentialProvider.sln ├── README.md ├── SECURITY.md ├── build ├── build.yml ├── validate-install-script-bash.yml ├── validate-install-script-powershell.yml └── validate-install-script.yml ├── helpers ├── installcredprovider.ps1 └── installcredprovider.sh ├── nuget.config ├── samples └── dockerfile.sample.txt ├── src ├── Authentication.Tests │ ├── AccountPriorityTests.cs │ ├── Microsoft.Artifacts.Authentication.Tests.csproj │ ├── MsalAuthenticationTests.cs │ ├── MsalHttpClientFactoryTests.cs │ ├── TokenProviderTests.cs │ └── Usings.cs ├── Authentication │ ├── AzureArtifacts.cs │ ├── ITokenProvider.cs │ ├── ITokenProvidersFactory.cs │ ├── Microsoft.Artifacts.Authentication.csproj │ ├── MsalCache.cs │ ├── MsalConstants.cs │ ├── MsalDeviceCodeTokenProvider.cs │ ├── MsalExtensions.cs │ ├── MsalHttpClientFactory.cs │ ├── MsalIntegratedWindowsAuthTokenProvider.cs │ ├── MsalInteractiveTokenProvider.cs │ ├── MsalManagedIdentityTokenProvider.cs │ ├── MsalServicePrincipalTokenProvider.cs │ ├── MsalSilentTokenProvider.cs │ ├── MsalTokenProviders.cs │ ├── PlatformInformation.cs │ ├── README.md │ ├── Resources.resx │ ├── TokenRequest.cs │ ├── WindowsIntegratedAuth.cs │ └── artifacts-icon.png └── artifacts-credprovider-conda │ ├── LICENSE │ ├── README.md │ ├── recipe │ ├── bld.bat │ ├── build.sh │ └── meta.yaml │ └── src │ ├── activate │ ├── artifacts-cred-activate.bat │ ├── artifacts-cred-activate.ps1 │ ├── artifacts-cred-activate.sh │ └── artifacts-cred.py │ └── deactivate │ ├── artifacts-cred-deactivate.bat │ ├── artifacts-cred-deactivate.ps1 │ └── artifacts-cred-deactivate.sh └── test.bat /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/dotnet", 3 | "features": { 4 | "ghcr.io/devcontainers/features/dotnet:1": { 5 | "version": "6.0" 6 | }, 7 | "ghcr.io/microsoft/codespace-features/artifacts-helper:1": { 8 | "dotnet6": true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues 2 | 3 | on: 4 | schedule: 5 | - cron: "0 */12 * * *" 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | steps: 13 | - uses: actions/stale@v3 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'This issue has had no activity in 90 days. Please comment if it is not actually stale.' 17 | stale-issue-label: 'stale' 18 | exempt-issue-labels: 'keep' 19 | days-before-stale: 90 20 | days-before-close: 7 21 | days-before-pr-close: -1 22 | enable-statistics: true 23 | -------------------------------------------------------------------------------- /.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 | 332 | .vscode 333 | !.vscode/launch.json 334 | !.vscode/tasks.json 335 | 336 | # MicroBuild build output 337 | MicroBuild 338 | keys 339 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": ".NET Core Launch (console)", 6 | "type": "coreclr", 7 | "request": "launch", 8 | "preLaunchTask": "build", 9 | "program": "${workspaceFolder}/CredentialProvider.Microsoft/bin/Debug/net6.0/CredentialProvider.Microsoft.dll", 10 | "args": [ 11 | "-Uri", "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/artifacts-credprovider/nuget/v3/index.json", 12 | "-Verbosity", "Debug" 13 | ], 14 | "cwd": "${workspaceFolder}/CredentialProvider.Microsoft", 15 | "console": "integratedTerminal", 16 | "stopAtEntry": false 17 | }, 18 | { 19 | "name": ".NET Core Attach", 20 | "type": "coreclr", 21 | "request": "attach" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/CredentialProvider.Microsoft/CredentialProvider.Microsoft.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "build sln", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "build", 22 | "${workspaceFolder}/CredentialProvider.Microsoft.sln", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.vsts-ci.yml: -------------------------------------------------------------------------------- 1 | resources: 2 | - repo: self 3 | 4 | pool: 5 | vmImage: windows-latest 6 | 7 | trigger: 8 | - master 9 | 10 | variables: 11 | BuildConfiguration: 'Release' 12 | TeamName: 'Package Experience' 13 | 14 | stages: 15 | - stage: Build 16 | dependsOn: [] 17 | jobs: 18 | - job: build 19 | steps: 20 | - template: build/build.yml 21 | parameters: 22 | nuspecProperties: 'VersionSuffix=CI-$(Build.BuildNumber)' 23 | 24 | - stage: Validate 25 | dependsOn: [] 26 | jobs: 27 | - template: build/validate-install-script.yml 28 | -------------------------------------------------------------------------------- /.vsts-pr.yml: -------------------------------------------------------------------------------- 1 | resources: 2 | - repo: self 3 | 4 | pool: 5 | vmImage: windows-latest 6 | 7 | trigger: 8 | - master 9 | 10 | variables: 11 | BuildConfiguration: 'Debug' 12 | TeamName: 'Package Experience' 13 | 14 | stages: 15 | - stage: Build 16 | dependsOn: [] 17 | jobs: 18 | - job: build 19 | steps: 20 | - template: build/build.yml 21 | parameters: 22 | nuspecProperties: 'VersionSuffix=CI-$(Build.BuildNumber)' 23 | 24 | - stage: Validate 25 | dependsOn: [] 26 | jobs: 27 | - template: build/validate-install-script.yml 28 | -------------------------------------------------------------------------------- /Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1.4.1 5 | 6 | netcoreapp3.1;net461;net481;net6.0;net8.0 7 | 8 | 9 | 10 | net8.0 11 | 12 | 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Azure Artifacts Credential Provider 2 | 3 | ### Building 4 | 5 | ```shell 6 | dotnet build CredentialProvider.Microsoft --configuration Release 7 | ``` 8 | 9 | In this and subsequent examples, configuration can be either `debug` or `release`. 10 | 11 | ### Publishing 12 | 13 | ```shell 14 | dotnet publish CredentialProvider.Microsoft --configuration Release --framework net8.0 15 | ``` 16 | 17 | ### Packing 18 | 19 | ```shell 20 | dotnet pack CredentialProvider.Microsoft --configuration Release 21 | ``` 22 | 23 | For CI builds, you can append a pre-release version: 24 | 25 | ```shell 26 | dotnet pack CredentialProvider.Microsoft --configuration Release /p:NuspecProperties=VersionSuffix=MyCustomVersion-2 27 | ``` 28 | 29 | ### Versioning 30 | 31 | When releasing a new version, update the CredentialProviderVersion property in Build.props 32 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft.Tests/CredentialProvider.Microsoft.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | latest 6 | false 7 | true 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft.Tests/CredentialProviders/Vsts/EnvironmentLock.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace CredentialProvider.Microsoft.Tests.CredentialProviders.Vsts 11 | { 12 | internal static class EnvironmentLock 13 | { 14 | private static readonly Semaphore _lock = new Semaphore(1, 1); 15 | private static readonly Dictionary savedEnvVars = new Dictionary(); 16 | 17 | public static async Task WaitAsync() 18 | { 19 | var tcs = new TaskCompletionSource(); 20 | var waiter = new Thread(() => { 21 | try 22 | { 23 | tcs.SetResult(_lock.WaitOne(TimeSpan.FromSeconds(60))); 24 | } 25 | catch(Exception e) 26 | { 27 | tcs.SetException(e); 28 | } 29 | }); 30 | waiter.Start(); 31 | await tcs.Task; 32 | 33 | foreach(object nameObj in Environment.GetEnvironmentVariables().Keys) 34 | { 35 | string name = (string)nameObj; 36 | if (name.Contains("NUGET")) 37 | { 38 | savedEnvVars[name] = Environment.GetEnvironmentVariable(name); 39 | Environment.SetEnvironmentVariable(name, null); 40 | } 41 | } 42 | 43 | return new Releaser(); 44 | } 45 | 46 | private sealed class Releaser : IDisposable 47 | { 48 | public void Dispose() 49 | { 50 | foreach(var envVar in savedEnvVars) 51 | { 52 | Environment.SetEnvironmentVariable(envVar.Key, envVar.Value); 53 | } 54 | savedEnvVars.Clear(); 55 | _lock.Release(); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft.Tests/CredentialProviders/VstsBuildTask/VstsBuildTaskCredentialProviderTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using CredentialProvider.Microsoft.Tests.CredentialProviders.Vsts; 9 | using Microsoft.VisualStudio.TestTools.UnitTesting; 10 | using Moq; 11 | using NuGet.Protocol.Plugins; 12 | using NuGetCredentialProvider.CredentialProviders.VstsBuildTask; 13 | using NuGetCredentialProvider.Logging; 14 | using NuGetCredentialProvider.Util; 15 | 16 | namespace CredentialProvider.Microsoft.Tests.CredentialProviders.VstsBuildTask 17 | { 18 | [TestClass] 19 | public class VstsBuildTaskCredentialProviderTests 20 | { 21 | private readonly Uri testAuthority = new Uri("https://example.aad.authority.com"); 22 | 23 | private Mock mockLogger; 24 | 25 | private VstsBuildTaskCredentialProvider vstsCredentialProvider; 26 | private IDisposable environmentLock; 27 | 28 | [TestInitialize] 29 | public void TestInitialize() 30 | { 31 | mockLogger = new Mock(); 32 | 33 | vstsCredentialProvider = new VstsBuildTaskCredentialProvider(mockLogger.Object); 34 | environmentLock = EnvironmentLock.WaitAsync().Result; 35 | ResetEnvVars(); 36 | } 37 | 38 | [TestCleanup] 39 | public virtual void TestCleanup() 40 | { 41 | ResetEnvVars(); 42 | environmentLock?.Dispose(); 43 | } 44 | 45 | private void ResetEnvVars() 46 | { 47 | Environment.SetEnvironmentVariable(EnvUtil.BuildTaskUriPrefixes, null); 48 | Environment.SetEnvironmentVariable(EnvUtil.BuildTaskAccessToken, null); 49 | } 50 | 51 | [TestMethod] 52 | public async Task CanProvideCredentials_ReturnsFalseWhenEnvironmentVariablesAreNotSet() 53 | { 54 | Uri sourceUri = new Uri(@"http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"); 55 | string uriPrefixesEnvVar = EnvUtil.BuildTaskUriPrefixes; 56 | string accessTokenEnvVar = EnvUtil.BuildTaskAccessToken; 57 | 58 | // Setting environment variables to null 59 | Environment.SetEnvironmentVariable(uriPrefixesEnvVar, null); 60 | Environment.SetEnvironmentVariable(accessTokenEnvVar, null); 61 | 62 | var result = await vstsCredentialProvider.CanProvideCredentialsAsync(sourceUri); 63 | Assert.AreEqual(false, result); 64 | } 65 | 66 | [TestMethod] 67 | public async Task CanProvideCredentials_ReturnsTrueForCorrectEnvironmentVariableAndMatchingSourceUri() 68 | { 69 | Uri sourceUri = new Uri(@"http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"); 70 | string uriPrefixesEnvVar = EnvUtil.BuildTaskUriPrefixes; 71 | string accessTokenEnvVar = EnvUtil.BuildTaskAccessToken; 72 | 73 | Environment.SetEnvironmentVariable(uriPrefixesEnvVar, "http://example.pkgs.vsts.me/"); 74 | Environment.SetEnvironmentVariable(accessTokenEnvVar, "accessToken"); 75 | 76 | var result = await vstsCredentialProvider.CanProvideCredentialsAsync(sourceUri); 77 | Assert.AreEqual(true, result); 78 | } 79 | 80 | [TestMethod] 81 | public async Task HandleRequestAsync_ReturnsSuccess() 82 | { 83 | Uri sourceUri = new Uri(@"http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"); 84 | string uriPrefixesEnvVar = EnvUtil.BuildTaskUriPrefixes; 85 | string accessTokenEnvVar = EnvUtil.BuildTaskAccessToken; 86 | 87 | Environment.SetEnvironmentVariable(uriPrefixesEnvVar, "http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"); 88 | Environment.SetEnvironmentVariable(accessTokenEnvVar, "accessToken"); 89 | 90 | var result = await vstsCredentialProvider.HandleRequestAsync(new GetAuthenticationCredentialsRequest(sourceUri, false, false, false), CancellationToken.None); 91 | Assert.AreEqual(result.ResponseCode, MessageResponseCode.Success); 92 | } 93 | 94 | [TestMethod] 95 | public async Task HandleRequestAsync_ReturnsErrorWhenPrefixDoesNotMatch() 96 | { 97 | Uri sourceUri = new Uri(@"http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"); 98 | string uriPrefixesEnvVar = EnvUtil.BuildTaskUriPrefixes; 99 | string accessTokenEnvVar = EnvUtil.BuildTaskAccessToken; 100 | 101 | Environment.SetEnvironmentVariable(uriPrefixesEnvVar, "http://urlThatDoesNotMatch"); 102 | Environment.SetEnvironmentVariable(accessTokenEnvVar, "accessToken"); 103 | 104 | var result = await vstsCredentialProvider.HandleRequestAsync(new GetAuthenticationCredentialsRequest(sourceUri, false, false, false), CancellationToken.None); 105 | Assert.AreEqual(result.ResponseCode, MessageResponseCode.Error); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft.Tests/Logging/LoggingTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using Moq; 7 | using NuGet.Common; 8 | using NuGetCredentialProvider.Logging; 9 | using System.IO; 10 | 11 | namespace CredentialProvider.Microsoft.Tests.Logging 12 | { 13 | [TestClass] 14 | public class LoggingTests 15 | { 16 | Mock mockWriter = new Mock(); 17 | 18 | [TestMethod] 19 | public void HumanFriendlyTextWriterLogger_EmitsLogLevelAndMessage() 20 | { 21 | mockWriter.Setup(x => x.WriteLine(It.IsAny())); 22 | HumanFriendlyTextWriterLogger logger = new HumanFriendlyTextWriterLogger(mockWriter.Object, writesToConsole: false); 23 | logger.SetLogLevel(LogLevel.Error); 24 | logger.Log(LogLevel.Error, allowOnConsole: true, message: "Something bad happened"); 25 | mockWriter.Verify(x => x.WriteLine("[Error] [CredentialProvider]Something bad happened")); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft.Tests/Util/HttpClientFactoryTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System.Linq; 6 | using Microsoft.Artifacts.Authentication; 7 | using Microsoft.VisualStudio.TestTools.UnitTesting; 8 | using NuGetCredentialProvider.Util; 9 | 10 | namespace CredentialProvider.Microsoft.Tests.Util; 11 | 12 | [TestClass] 13 | public class HttpClientFactoryTests 14 | { 15 | [TestMethod] 16 | public void HttpClientFactory_UserAgent() 17 | { 18 | var httpClientFactory = HttpClientFactory.Default; 19 | var httpClient = httpClientFactory.GetHttpClient(); 20 | var userAgent = httpClient.DefaultRequestHeaders.UserAgent; 21 | 22 | Assert.AreEqual(4, userAgent.Count); 23 | Assert.AreEqual(MsalHttpClientFactory.ProgramProduct, userAgent.ElementAt(0)); 24 | Assert.AreEqual(MsalHttpClientFactory.ProgramComment, userAgent.ElementAt(1)); 25 | Assert.AreEqual(MsalHttpClientFactory.ClrProduct, userAgent.ElementAt(2)); 26 | Assert.AreEqual(MsalHttpClientFactory.ClrComment, userAgent.ElementAt(3)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft.VSIX/Microsoft.CredentialProvider.swixproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $(CredentialProviderVersion) 8 | 4966bd57-3289-44a3-9698-f750a35b726b 9 | 2.0 10 | neutral 11 | false 12 | Microsoft.CredentialProvider 13 | vsix 14 | 15 | 16 | true 17 | 18 | 19 | 20 | .\bin\$(Configuration)\ 21 | $(OutputPath) 22 | ..\CredentialProvider.Microsoft\bin\$(Configuration)\net461\ 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | $(PackagePreprocessorDefinitions);PluginBinPath=$(PluginBinPath);SourceDir=$(SourceDir);Version=$(BuildVersion);ThirdPartyNotice=$(ThirdPartyNotice);ProductVersion=$(ProductVersion) 35 | $(BaseIntermediateOutputPath)$(Configuration)\ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft.VSIX/Microsoft.CredentialProvider.vsmanproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | true 8 | true 9 | v4.8 10 | 11 | 12 | 13 | .\bin\$(Configuration)\ 14 | $(OutputPath) 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft.VSIX/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft.VSIX/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/CredentialProvider.Microsoft.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | NuGetCredentialProvider 6 | latest 7 | helpericons.ico 8 | $(CredentialProviderVersion) 9 | CredentialProvider.Microsoft.nuspec 10 | 11 | $(NuspecProperties);Configuration=$(Configuration);Version=$(VersionPrefix) 12 | $(NuspecProperties);Configuration=$(Configuration);Version=$(VersionPrefix)-$(VersionSuffix) 13 | AnyCPU 14 | prompt 15 | 4 16 | $(NoWarn);NU5100 17 | 18 | 19 | 20 | true 21 | portable 22 | false 23 | $(DefineConstants);DEBUG;TRACE 24 | 25 | 26 | pdbonly 27 | true 28 | $(DefineConstants);TRACE 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/CredentialProvider.Microsoft.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Microsoft.NuGet.CredentialProvider 5 | $Version$ 6 | Microsoft Credential Provider for NuGet 7 | Microsoft 8 | microsoft,nugetvss 9 | http://go.microsoft.com/fwlink/?LinkId=329770 10 | http://go.microsoft.com/fwlink/?LinkId=623217 11 | http://go.microsoft.com/fwlink/?LinkId=615568 12 | true 13 | The Microsoft Credential Provider enables NuGet.exe 4.8+ to connect to Visual Studio Team Services Package Management NuGet feeds. See the NuGet docs for more info: http://docs.nuget.org/Consume/Credential-Providers 14 | © Microsoft Corporation. All rights reserved. 15 | Microsoft Visual Studio Team Services VSTS VSO VisualStudio Package Management NuGet 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/CredentialProviderArgs.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using NuGet.Common; 7 | using PowerArgs; 8 | 9 | namespace NuGetCredentialProvider 10 | { 11 | [ArgDescription("The Azure Artifacts credential provider can be used to acquire credentials for Azure Artifacts.\n" + 12 | "\n" + 13 | "Note: The Azure Artifacts Credential Provider is mainly intended for use via integrations with development tools such as .NET Core and nuget.exe.\n" + 14 | "While it can be used via this CLI in \"stand-alone mode\", please pay special attention to certain options such as -IsRetry below.\n" + 15 | "Failing to do so may result in obtaining invalid credentials.")] 16 | internal class CredentialProviderArgs 17 | { 18 | [ArgDescription("Used by nuget to run the credential helper in plugin mode")] 19 | public bool Plugin { get; set; } 20 | 21 | [ArgDescription("The package source URI for which credentials will be filled")] 22 | public Uri Uri { get; set; } 23 | 24 | [ArgDescription("If present and true, providers will not issue interactive prompts")] 25 | public bool NonInteractive { get; set; } 26 | 27 | [ArgDescription("If false / unset, INVALID CREDENTIALS MAY BE RETURNED. The caller is required to validate returned credentials themselves, and if invalid, should call the credential provider again with -IsRetry set. If true, the credential provider will obtain new credentials instead of returning potentially invalid credentials from the cache.")] 28 | public bool IsRetry { get; set; } 29 | 30 | [ArgDefaultValue(LogLevel.Information)] 31 | [ArgDescription("Display this amount of detail in the output")] 32 | public LogLevel Verbosity { get; set; } 33 | 34 | [ArgDescription("Prevents writing the password to standard output (for troubleshooting purposes)")] 35 | public bool RedactPassword { get; set; } 36 | 37 | [ArgShortcut("?")] 38 | [ArgShortcut("h")] 39 | [ArgDescription("Prints this help message")] 40 | public bool Help { get; set; } 41 | 42 | [ArgDefaultValue(true)] 43 | [ArgDescription("If true, user can be prompted with credentials through UI, if false, device flow must be used")] 44 | public bool CanShowDialog { get; set; } 45 | 46 | [ArgDescription("In standalone mode, format the results for human readability or as JSON. If JSON is selected, then logging (which may include Device Code instructions) will be logged to standard error instead of standard output.")] 47 | [ArgShortcut("F")] 48 | public OutputFormat OutputFormat { get; set; } 49 | } 50 | 51 | public enum OutputFormat 52 | { 53 | HumanReadable = 0, 54 | Json = 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/CredentialProviders/CredentialProviderBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using NuGet.Protocol.Plugins; 9 | using NuGetCredentialProvider.Logging; 10 | using NuGetCredentialProvider.Util; 11 | 12 | namespace NuGetCredentialProvider.CredentialProviders 13 | { 14 | /// 15 | /// Represents a base class for credential providers. 16 | /// 17 | public abstract class CredentialProviderBase : ICredentialProvider 18 | { 19 | /// 20 | /// A for when credentials could not be retrieved. 21 | /// 22 | protected static readonly GetAuthenticationCredentialsResponse NotFoundGetAuthenticationCredentialsResponse = new GetAuthenticationCredentialsResponse( 23 | username: null, 24 | password: null, 25 | message: null, 26 | authenticationTypes: null, 27 | responseCode: MessageResponseCode.NotFound); 28 | 29 | /// 30 | /// Initializes a new instance of the class. 31 | /// 32 | /// A to use for logging. 33 | protected CredentialProviderBase(ILogger logger) 34 | { 35 | Logger = logger; 36 | } 37 | 38 | public virtual bool IsCachable { get { return true; } } 39 | 40 | protected abstract string LoggingName { get; } 41 | 42 | /// 43 | /// Gets a to use for logging. 44 | /// 45 | protected ILogger Logger { get; } 46 | 47 | /// 48 | public abstract Task CanProvideCredentialsAsync(Uri uri); 49 | 50 | /// 51 | public virtual void Dispose() 52 | { 53 | } 54 | 55 | /// 56 | public abstract Task HandleRequestAsync(GetAuthenticationCredentialsRequest request, CancellationToken cancellationToken); 57 | 58 | protected void Error(string message) 59 | { 60 | Logger.Error($"{LoggingName} - {message}"); 61 | } 62 | 63 | protected void Warning(string message) 64 | { 65 | Logger.Warning($"{LoggingName} - {message}"); 66 | } 67 | 68 | protected void Info(string message) 69 | { 70 | Logger.Info($"{LoggingName} - {message}"); 71 | } 72 | 73 | protected void Verbose(string message) 74 | { 75 | Logger.Verbose($"{LoggingName} - {message}"); 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/CredentialProviders/ICredentialProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using NuGet.Protocol.Plugins; 9 | 10 | namespace NuGetCredentialProvider.CredentialProviders 11 | { 12 | /// 13 | /// Represents an interface for implementation of credential providers. 14 | /// 15 | internal interface ICredentialProvider : IDisposable 16 | { 17 | /// 18 | /// Determines whether this implementation can provide credentials for the specified . 19 | /// 20 | /// The of the package feed. 21 | /// true if this implementation can provide credentials, otherwise false. 22 | Task CanProvideCredentialsAsync(Uri uri); 23 | 24 | /// 25 | /// Handles a . 26 | /// 27 | /// A object containing details about the request. 28 | /// A used for signaling cancellation. 29 | /// A object containg details about a response. 30 | Task HandleRequestAsync(GetAuthenticationCredentialsRequest request, CancellationToken cancellationToken); 31 | 32 | bool IsCachable { get; } 33 | } 34 | } -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/CredentialProviders/Vsts/IVstsSessionTokenClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace NuGetCredentialProvider.CredentialProviders.Vsts 10 | { 11 | public interface IVstsSessionTokenClient 12 | { 13 | Task CreateSessionTokenAsync(VstsTokenType tokenType, DateTime validTo, CancellationToken cancellationToken); 14 | } 15 | } -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/CredentialProviders/Vsts/IVstsSessionTokenFromBearerTokenProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using NuGet.Protocol.Plugins; 7 | 8 | namespace NuGetCredentialProvider.CredentialProviders.Vsts 9 | { 10 | public interface IAzureDevOpsSessionTokenFromBearerTokenProvider 11 | { 12 | Task GetAzureDevOpsSessionTokenFromBearerToken( 13 | GetAuthenticationCredentialsRequest request, 14 | string bearerToken, 15 | bool bearerTokenObtainedInteractively, 16 | CancellationToken cancellationToken); 17 | } 18 | } -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/CredentialProviders/Vsts/MsalTokenProvidersFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading.Tasks; 8 | using Microsoft.Artifacts.Authentication; 9 | using Microsoft.Extensions.Logging; 10 | using Microsoft.Identity.Client.Extensions.Msal; 11 | using NuGetCredentialProvider.Util; 12 | 13 | namespace NuGetCredentialProvider.CredentialProviders.Vsts 14 | { 15 | internal class MsalTokenProvidersFactory : ITokenProvidersFactory 16 | { 17 | private readonly ILogger logger; 18 | private MsalCacheHelper cache; 19 | 20 | public MsalTokenProvidersFactory(ILogger logger) 21 | { 22 | this.logger = logger; 23 | } 24 | 25 | public async Task> GetAsync(Uri authority) 26 | { 27 | if (cache == null && EnvUtil.MsalFileCacheEnabled()) 28 | { 29 | cache = await MsalCache.GetMsalCacheHelperAsync(EnvUtil.GetMsalCacheLocation(), logger); 30 | } 31 | 32 | var app = AzureArtifacts.CreateDefaultBuilder(authority) 33 | .WithBroker(EnvUtil.MsalAllowBrokerEnabled(), logger) 34 | .WithHttpClientFactory(HttpClientFactory.Default) 35 | .WithLogging( 36 | (Microsoft.Identity.Client.LogLevel level, string message, bool containsPii) => 37 | { 38 | // We ignore containsPii param because we are passing in enablePiiLogging below. 39 | logger.LogTrace("MSAL Log ({level}): {message}", level, message); 40 | }, 41 | enablePiiLogging: EnvUtil.GetLogPIIEnabled() 42 | ) 43 | .Build(); 44 | 45 | cache?.RegisterCache(app.UserTokenCache); 46 | 47 | return MsalTokenProviders.Get(app, logger); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsSessionToken.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using System.Runtime.Serialization; 7 | 8 | namespace NuGetCredentialProvider.CredentialProviders.Vsts 9 | { 10 | [DataContract] 11 | public class VstsSessionToken 12 | { 13 | [DataMember] 14 | public string DisplayName { get; set; } 15 | 16 | [DataMember] 17 | public string Scope { get; set; } 18 | 19 | [DataMember] 20 | public DateTime? ValidTo { get; set; } 21 | 22 | [DataMember] 23 | public string Token { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsSessionTokenClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using System.Net.Http; 7 | using System.Net.Http.Headers; 8 | using System.Text; 9 | using System.Text.Json; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using NuGetCredentialProvider.Logging; 13 | using NuGetCredentialProvider.Util; 14 | 15 | namespace NuGetCredentialProvider.CredentialProviders.Vsts 16 | { 17 | public class VstsSessionTokenClient : IVstsSessionTokenClient 18 | { 19 | private const string TokenScope = "vso.packaging_write vso.drop_write"; 20 | 21 | private static readonly JsonSerializerOptions options = new JsonSerializerOptions 22 | { 23 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 24 | PropertyNameCaseInsensitive = true 25 | }; 26 | 27 | private readonly Uri vstsUri; 28 | private readonly string bearerToken; 29 | private readonly IAuthUtil authUtil; 30 | private readonly ILogger logger; 31 | 32 | public VstsSessionTokenClient(Uri vstsUri, string bearerToken, IAuthUtil authUtil, ILogger logger) 33 | { 34 | this.vstsUri = vstsUri ?? throw new ArgumentNullException(nameof(vstsUri)); 35 | this.bearerToken = bearerToken ?? throw new ArgumentNullException(nameof(bearerToken)); 36 | this.authUtil = authUtil ?? throw new ArgumentNullException(nameof(authUtil)); 37 | this.logger = logger ?? throw new ArgumentException(nameof(logger)); 38 | } 39 | 40 | private HttpRequestMessage CreateRequest(Uri uri, DateTime? validTo) 41 | { 42 | var request = new HttpRequestMessage(HttpMethod.Post, uri); 43 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 44 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken); 45 | 46 | var tokenRequest = new VstsSessionToken() 47 | { 48 | DisplayName = "Azure DevOps Artifacts Credential Provider", 49 | Scope = TokenScope, 50 | ValidTo = validTo 51 | }; 52 | 53 | request.Content = new StringContent( 54 | JsonSerializer.Serialize(tokenRequest, options), 55 | Encoding.UTF8, 56 | "application/json"); 57 | 58 | return request; 59 | } 60 | 61 | public async Task CreateSessionTokenAsync(VstsTokenType tokenType, DateTime validTo, CancellationToken cancellationToken) 62 | { 63 | var spsEndpoint = await authUtil.GetAuthorizationEndpoint(vstsUri, cancellationToken); 64 | if (spsEndpoint == null) 65 | { 66 | return null; 67 | } 68 | 69 | var uriBuilder = new UriBuilder(spsEndpoint) 70 | { 71 | Query = $"tokenType={tokenType}&api-version=5.0-preview.1" 72 | }; 73 | 74 | uriBuilder.Path = uriBuilder.Path.TrimEnd('/') + "/_apis/Token/SessionTokens"; 75 | 76 | var httpClient = HttpClientFactory.Default.GetHttpClient(); 77 | 78 | using (var request = CreateRequest(uriBuilder.Uri, validTo)) 79 | using (var response = await httpClient.SendAsync(request, cancellationToken)) 80 | { 81 | logger.LogResponse(NuGet.Common.LogLevel.Verbose, true, response); 82 | 83 | string serializedResponse; 84 | if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) 85 | { 86 | request.Dispose(); 87 | response.Dispose(); 88 | 89 | logger.Log(NuGet.Common.LogLevel.Verbose, true, "Re-trying with service-defined valid-time."); 90 | using (var request2 = CreateRequest(uriBuilder.Uri, validTo: null)) 91 | using(var response2 = await httpClient.SendAsync(request2, cancellationToken)) 92 | { 93 | response2.EnsureSuccessStatusCode(); 94 | logger.LogResponse(NuGet.Common.LogLevel.Verbose, true, response2); 95 | 96 | serializedResponse = await response2.Content.ReadAsStringAsync(); 97 | } 98 | } 99 | else 100 | { 101 | response.EnsureSuccessStatusCode(); 102 | serializedResponse = await response.Content.ReadAsStringAsync(); 103 | } 104 | 105 | var responseToken = JsonSerializer.Deserialize(serializedResponse, options); 106 | 107 | if (validTo.Subtract(responseToken.ValidTo.Value).TotalHours > 1.0) 108 | { 109 | logger.Log(NuGet.Common.LogLevel.Information, true, $"Requested {validTo} but received {responseToken.ValidTo}"); 110 | } 111 | 112 | return responseToken.Token; 113 | } 114 | } 115 | } 116 | 117 | public enum VstsTokenType 118 | { 119 | Compact, // Personal Access Token (PAT) 120 | SelfDescribing // Session Token (JWT) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsSessionTokenFromBearerTokenProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using NuGet.Protocol.Plugins; 9 | using NuGetCredentialProvider.Logging; 10 | using NuGetCredentialProvider.Util; 11 | 12 | namespace NuGetCredentialProvider.CredentialProviders.Vsts 13 | { 14 | public class VstsSessionTokenFromBearerTokenProvider : IAzureDevOpsSessionTokenFromBearerTokenProvider 15 | { 16 | private const double DefaultSessionTimeHours = 4; 17 | private const double DefaultPersonalAccessTimeHours = 2160; // 90 days 18 | private readonly IAuthUtil authUtil; 19 | private readonly ILogger logger; 20 | 21 | public VstsSessionTokenFromBearerTokenProvider(IAuthUtil authUtil, ILogger logger) 22 | { 23 | this.authUtil = authUtil; 24 | this.logger = logger; 25 | } 26 | 27 | public async Task GetAzureDevOpsSessionTokenFromBearerToken( 28 | GetAuthenticationCredentialsRequest request, 29 | string bearerToken, 30 | bool bearerTokenObtainedInteractively, 31 | CancellationToken cancellationToken) 32 | { 33 | // Allow the user to choose their token type 34 | // If they don't and interactive auth was required, then prefer a PAT so we can safely default to a much longer validity period 35 | VstsTokenType tokenType = EnvUtil.GetVstsTokenType() ?? 36 | (bearerTokenObtainedInteractively 37 | ? VstsTokenType.Compact 38 | : VstsTokenType.SelfDescribing); 39 | 40 | // Allow the user to override the validity period 41 | TimeSpan? preferredTokenTime = EnvUtil.GetSessionTimeFromEnvironment(logger); 42 | TimeSpan sessionTimeSpan; 43 | if (tokenType == VstsTokenType.Compact) 44 | { 45 | // Allow Personal Access Tokens to be as long as SPS will grant, since they're easily revokable 46 | sessionTimeSpan = preferredTokenTime ?? TimeSpan.FromHours(DefaultPersonalAccessTimeHours); 47 | } 48 | else 49 | { 50 | // But limit self-describing session tokens to a strict 24 hours, since they're harder to revoke 51 | sessionTimeSpan = preferredTokenTime ?? TimeSpan.FromHours(DefaultSessionTimeHours); 52 | if (sessionTimeSpan >= TimeSpan.FromHours(24)) 53 | { 54 | sessionTimeSpan = TimeSpan.FromHours(24); 55 | } 56 | } 57 | 58 | DateTime endTime = DateTime.UtcNow + sessionTimeSpan; 59 | logger.Verbose(string.Format(Resources.VSTSSessionTokenValidity, tokenType.ToString(), sessionTimeSpan.ToString(), endTime.ToUniversalTime().ToString())); 60 | VstsSessionTokenClient sessionTokenClient = new VstsSessionTokenClient(request.Uri, bearerToken, authUtil, this.logger); 61 | return await sessionTokenClient.CreateSessionTokenAsync(tokenType, endTime, cancellationToken); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/CredentialProviders/VstsBuildTask/VstsBuildTaskCredentialProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using NuGet.Protocol.Plugins; 11 | using NuGetCredentialProvider.Logging; 12 | using NuGetCredentialProvider.Util; 13 | using ILogger = NuGetCredentialProvider.Logging.ILogger; 14 | 15 | namespace NuGetCredentialProvider.CredentialProviders.VstsBuildTask 16 | { 17 | public sealed class VstsBuildTaskCredentialProvider : CredentialProviderBase 18 | { 19 | private const string Username = "VssSessionToken"; 20 | 21 | public VstsBuildTaskCredentialProvider(ILogger logger) 22 | : base(logger) 23 | { 24 | } 25 | 26 | protected override string LoggingName => nameof(VstsBuildTaskCredentialProvider); 27 | 28 | public override bool IsCachable { get { return false; } } 29 | 30 | public override Task CanProvideCredentialsAsync(Uri uri) 31 | { 32 | string uriPrefixesString = Environment.GetEnvironmentVariable(EnvUtil.BuildTaskUriPrefixes); 33 | string accessToken = Environment.GetEnvironmentVariable(EnvUtil.BuildTaskAccessToken); 34 | 35 | bool useBuildTaskCredProvider = string.IsNullOrWhiteSpace(uriPrefixesString) == false && string.IsNullOrWhiteSpace(accessToken) == false; 36 | if (useBuildTaskCredProvider == true) 37 | { 38 | return Task.FromResult(true); 39 | } 40 | 41 | Verbose(Resources.BuildTaskEnvVarError); 42 | return Task.FromResult(false); 43 | } 44 | 45 | public override Task HandleRequestAsync(GetAuthenticationCredentialsRequest request, CancellationToken cancellationToken) 46 | { 47 | cancellationToken.ThrowIfCancellationRequested(); 48 | 49 | string uriPrefixesString = Environment.GetEnvironmentVariable(EnvUtil.BuildTaskUriPrefixes); 50 | string accessToken = Environment.GetEnvironmentVariable(EnvUtil.BuildTaskAccessToken); 51 | 52 | Verbose(string.Format(Resources.IsRetry, request.IsRetry)); 53 | 54 | string[] uriPrefixes = uriPrefixesString.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); 55 | Verbose(Resources.BuildTaskUriPrefixes); 56 | foreach (var uriPrefix in uriPrefixes) 57 | { 58 | Verbose($"{uriPrefix}"); 59 | } 60 | 61 | string uriString = request.Uri.AbsoluteUri; 62 | string matchedPrefix = uriPrefixes.FirstOrDefault(prefix => uriString.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); 63 | Verbose(string.Format(Resources.BuildTaskMatchedPrefix, matchedPrefix != null ? matchedPrefix : Resources.BuildTaskNoMatchingPrefixes)); 64 | 65 | if (matchedPrefix == null) 66 | { 67 | Verbose(Resources.BuildTaskNoPrefixMatch); 68 | return this.GetResponse( 69 | null, 70 | null, 71 | Resources.BuildTaskNoPrefixMatch, 72 | MessageResponseCode.Error); 73 | } 74 | 75 | Verbose(string.Format(Resources.BuildTaskEndpointMatchingUrlFound, uriString)); 76 | return this.GetResponse( 77 | Username, 78 | accessToken, 79 | null, 80 | MessageResponseCode.Success); 81 | } 82 | 83 | private Task GetResponse(string username, string password, string message, MessageResponseCode responseCode) 84 | { 85 | return Task.FromResult(new GetAuthenticationCredentialsResponse( 86 | username: username, 87 | password: password, 88 | message: message, 89 | authenticationTypes: new List 90 | { 91 | "Basic" 92 | }, 93 | responseCode: responseCode)); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/CredentialProviders/VstsBuildTaskServiceEndpoint/VstsBuildTaskMsalTokenProvidersFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading.Tasks; 8 | using Microsoft.Artifacts.Authentication; 9 | using Microsoft.Extensions.Logging; 10 | using Microsoft.Identity.Client; 11 | using NuGetCredentialProvider.Util; 12 | 13 | namespace NuGetCredentialProvider.CredentialProviders.VstsBuildTaskServiceEndpoint; 14 | 15 | internal class VstsBuildTaskMsalTokenProvidersFactory : ITokenProvidersFactory 16 | { 17 | private readonly ILogger logger; 18 | 19 | public VstsBuildTaskMsalTokenProvidersFactory(ILogger logger) 20 | { 21 | this.logger = logger; 22 | } 23 | 24 | public Task> GetAsync(Uri authority) 25 | { 26 | var app = AzureArtifacts.CreateDefaultBuilder(authority) 27 | .WithBroker(EnvUtil.MsalAllowBrokerEnabled(), logger) 28 | .WithHttpClientFactory(HttpClientFactory.Default) 29 | .WithLogging( 30 | (level, message, containsPii) => 31 | { 32 | // We ignore containsPii param because we are passing in enablePiiLogging below. 33 | logger.LogTrace("MSAL Log ({level}): {message}", level, message); 34 | }, 35 | enablePiiLogging: EnvUtil.GetLogPIIEnabled() 36 | ) 37 | .Build(); 38 | 39 | return Task.FromResult(ConstructTokenProvidersList(app)); 40 | } 41 | 42 | private IEnumerable ConstructTokenProvidersList(IPublicClientApplication app) 43 | { 44 | yield return new MsalServicePrincipalTokenProvider(app, logger); 45 | yield return new MsalManagedIdentityTokenProvider(app, logger); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/EULA_Microsoft Visual Studio Team Services Credential Provider.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/artifacts-credprovider/107acd99393a8ba94be8a3280be18773c0549de4/CredentialProvider.Microsoft/EULA_Microsoft Visual Studio Team Services Credential Provider.docx -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/Logging/ConsoleLoggers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using System.IO; 7 | using NuGet.Common; 8 | 9 | namespace NuGetCredentialProvider.Logging 10 | { 11 | /// 12 | /// Logs messages to standard output, with log level included 13 | /// 14 | public class StandardOutputLogger : HumanFriendlyTextWriterLogger 15 | { 16 | public StandardOutputLogger() : base(Console.Out, writesToConsole: true) { } 17 | } 18 | 19 | /// 20 | /// Logs messages to standard error, with log level included 21 | /// 22 | public class StandardErrorLogger : HumanFriendlyTextWriterLogger 23 | { 24 | public StandardErrorLogger() : base(Console.Error, writesToConsole: true) { } 25 | } 26 | 27 | /// 28 | /// Emits a log message in a human-readable format to a TextWriter, including the log level 29 | /// 30 | public class HumanFriendlyTextWriterLogger : LoggerBase 31 | { 32 | private readonly TextWriter writer; 33 | 34 | public HumanFriendlyTextWriterLogger(TextWriter writer, bool writesToConsole) 35 | : base(writesToConsole) 36 | { 37 | this.writer = writer; 38 | } 39 | 40 | protected override void WriteLog(LogLevel logLevel, string message) 41 | { 42 | writer.WriteLine($"[{logLevel}] {message}"); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/Logging/CredentialResult.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace NuGetCredentialProvider.Logging 4 | { 5 | [DataContract] 6 | public class CredentialResult 7 | { 8 | public CredentialResult(string username, string password) 9 | { 10 | Username = username; 11 | Password = password; 12 | } 13 | 14 | [DataMember] 15 | public string Username { get; } 16 | 17 | [DataMember] 18 | public string Password { get; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/Logging/ILogger.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System.Linq; 6 | using System.Net.Http; 7 | using NuGet.Common; 8 | 9 | namespace NuGetCredentialProvider.Logging 10 | { 11 | public interface ILogger 12 | { 13 | void Log(LogLevel level, bool allowOnConsole, string message); 14 | 15 | void SetLogLevel(LogLevel newLogLevel); 16 | } 17 | 18 | public static class LoggerExtensions 19 | { 20 | public static void LogResponse(this ILogger logger, LogLevel level, bool allowOnConsole, HttpResponseMessage response) 21 | { 22 | logger.Log(NuGet.Common.LogLevel.Verbose, true, $"Response: {response.StatusCode}"); 23 | if (response.Headers.TryGetValues("ActivityId", out var activityIds)) 24 | { 25 | string activityId = activityIds.FirstOrDefault(); 26 | if (activityId != null) 27 | { 28 | logger.Log(NuGet.Common.LogLevel.Verbose, true, $" ActivityId: {activityId}"); 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/Logging/LogEveryMessageFileLogger.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.Threading; 9 | using NuGet.Common; 10 | 11 | namespace NuGetCredentialProvider.Logging 12 | { 13 | internal class LogEveryMessageFileLogger : ILogger 14 | { 15 | private static readonly int Pid = Process.GetCurrentProcess().Id; 16 | 17 | private readonly string filePath; 18 | 19 | internal LogEveryMessageFileLogger(string filePath) 20 | { 21 | this.filePath = filePath; 22 | Log(LogLevel.Minimal, allowOnConsole: false, string.Format(Resources.LogStartsAt, DateTime.UtcNow.ToString("u"))); 23 | } 24 | 25 | public void Log(LogLevel level, bool allowOnConsole, string message) 26 | { 27 | try 28 | { 29 | for (int i = 0; i < 3; ++i) 30 | { 31 | try 32 | { 33 | File.AppendAllText( 34 | filePath, 35 | $"[{DateTime.UtcNow:HH:mm:ss.fff} {Pid,5} {GetLevelKeyword(level),7}] {message}\n"); 36 | return; 37 | } 38 | catch (IOException) 39 | { 40 | // retry IOExceptions a couple of times. Could be another instance (or thread) of the plugin locking the file 41 | Thread.Sleep(TimeSpan.FromMilliseconds(20)); 42 | } 43 | } 44 | } 45 | catch 46 | { 47 | // don't crash the process just because logging failed. 48 | } 49 | } 50 | 51 | private string GetLevelKeyword(LogLevel level) 52 | { 53 | switch (level) 54 | { 55 | case LogLevel.Debug: 56 | return "Debug"; 57 | 58 | case LogLevel.Verbose: 59 | return "Verbose"; 60 | 61 | case LogLevel.Information: 62 | return "Info"; 63 | 64 | case LogLevel.Minimal: 65 | return "Minimal"; 66 | 67 | case LogLevel.Warning: 68 | return "Warning"; 69 | 70 | case LogLevel.Error: 71 | return "Error"; 72 | 73 | default: 74 | throw new ArgumentOutOfRangeException(nameof(level), level, null); 75 | } 76 | } 77 | 78 | void ILogger.SetLogLevel(LogLevel newLogLevel) 79 | { 80 | // no-op. This logger always logs all messages. 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/Logging/LoggerBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Threading; 8 | using NuGet.Common; 9 | 10 | namespace NuGetCredentialProvider.Logging 11 | { 12 | public abstract class LoggerBase : ILogger 13 | { 14 | private LogLevel minLogLevel = LogLevel.Debug; 15 | private bool allowLogWrites = false; 16 | private ConcurrentQueue> bufferedLogs = new ConcurrentQueue>(); 17 | private readonly bool writesToConsole; 18 | 19 | protected LoggerBase(bool writesToConsole) 20 | { 21 | this.writesToConsole = writesToConsole; 22 | } 23 | 24 | public void Log(LogLevel level, bool allowOnConsole, string message) 25 | { 26 | if (writesToConsole && !allowOnConsole) 27 | { 28 | return; 29 | } 30 | 31 | if (!allowLogWrites) 32 | { 33 | // cheap reserve, if it swaps out after we add, meh, we miss one log 34 | var buffer = bufferedLogs; 35 | if (buffer != null) 36 | { 37 | buffer.Enqueue(Tuple.Create(level, message, DateTime.Now)); 38 | } 39 | 40 | // we could pass this through if buffer is null since the Set message has already come through to unblock us, but 41 | // the race should be rare and we don't know exactly how nuget will behave with the parallelism so 42 | // opt to ignore this one racing log message. 43 | return; 44 | } 45 | else if (bufferedLogs != null) 46 | { 47 | ConcurrentQueue> buffer = null; 48 | buffer = Interlocked.CompareExchange(ref bufferedLogs, null, bufferedLogs); 49 | 50 | if (buffer != null) 51 | { 52 | foreach (var log in buffer) 53 | { 54 | if (log.Item1 >= minLogLevel) 55 | { 56 | WriteLog(log.Item1, GetLogPrefix(log.Item3) + log.Item2); 57 | } 58 | } 59 | } 60 | } 61 | 62 | if (level >= minLogLevel) 63 | { 64 | WriteLog(level, GetLogPrefix(null) + message); 65 | } 66 | } 67 | 68 | public void SetLogLevel(LogLevel newLogLevel) 69 | { 70 | minLogLevel = newLogLevel; 71 | allowLogWrites = true; 72 | } 73 | 74 | protected abstract void WriteLog(LogLevel logLevel, string message); 75 | 76 | private string GetLogPrefix(DateTime? bufferedLogTime) 77 | { 78 | string timeString = bufferedLogTime.HasValue 79 | ? bufferedLogTime.Value.ToString(".HHmmss") 80 | : null; 81 | 82 | return $"[CredentialProvider{timeString}]"; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/Logging/MultiLogger.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System.Collections.Generic; 6 | using NuGet.Common; 7 | 8 | namespace NuGetCredentialProvider.Logging 9 | { 10 | internal class MultiLogger : List, ILogger 11 | { 12 | private LogLevel? minLogLevel = null; 13 | 14 | public void Log(LogLevel level, bool allowOnConsole, string message) 15 | { 16 | foreach (var logger in this) 17 | { 18 | logger.Log(level, allowOnConsole, message); 19 | } 20 | } 21 | 22 | public void SetLogLevel(LogLevel newLogLevel) 23 | { 24 | minLogLevel = newLogLevel; 25 | 26 | foreach (var logger in this) 27 | { 28 | logger.SetLogLevel(newLogLevel); 29 | } 30 | } 31 | 32 | public new void Add(ILogger logger) 33 | { 34 | if (minLogLevel.HasValue) 35 | { 36 | logger.SetLogLevel(minLogLevel.Value); 37 | } 38 | 39 | base.Add(logger); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/Logging/NuGetLoggerAdapter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using NuGet.Common; 7 | 8 | namespace NuGetCredentialProvider.Logging; 9 | 10 | internal class NuGetLoggerAdapter : Microsoft.Extensions.Logging.ILogger 11 | { 12 | private readonly ILogger logger; 13 | private readonly LogLevel logLevel; 14 | 15 | public NuGetLoggerAdapter(ILogger logger, LogLevel logLevel) 16 | { 17 | this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); 18 | this.logLevel = logLevel; 19 | } 20 | 21 | public IDisposable BeginScope(TState state) 22 | { 23 | throw new NotImplementedException(); 24 | } 25 | 26 | public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) 27 | { 28 | return GetNuGetLogLevel(logLevel) >= this.logLevel; 29 | } 30 | 31 | public void Log(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, TState state, Exception exception, Func formatter) 32 | { 33 | logger.Log(GetNuGetLogLevel(logLevel), true, formatter(state, exception)); 34 | } 35 | 36 | private LogLevel GetNuGetLogLevel(Microsoft.Extensions.Logging.LogLevel logLevel) 37 | { 38 | switch (logLevel) 39 | { 40 | // Seems backwards, but NuGet defines a different order for log levels 41 | case Microsoft.Extensions.Logging.LogLevel.Trace: 42 | return LogLevel.Debug; 43 | case Microsoft.Extensions.Logging.LogLevel.Debug: 44 | return LogLevel.Verbose; 45 | case Microsoft.Extensions.Logging.LogLevel.Information: 46 | return LogLevel.Information; 47 | case Microsoft.Extensions.Logging.LogLevel.Warning: 48 | return LogLevel.Warning; 49 | case Microsoft.Extensions.Logging.LogLevel.Error: 50 | return LogLevel.Error; 51 | case Microsoft.Extensions.Logging.LogLevel.Critical: 52 | return LogLevel.Error; 53 | case Microsoft.Extensions.Logging.LogLevel.None: 54 | return LogLevel.Error; 55 | default: 56 | return LogLevel.Minimal; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/Logging/PluginConnectionLogger.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using NuGet.Common; 8 | using NuGet.Protocol.Plugins; 9 | 10 | namespace NuGetCredentialProvider.Logging 11 | { 12 | internal class PluginConnectionLogger : LoggerBase 13 | { 14 | private readonly IConnection connection; 15 | 16 | internal PluginConnectionLogger(IConnection connection) 17 | : base(writesToConsole: true) 18 | { 19 | this.connection = connection; 20 | } 21 | 22 | protected override void WriteLog(LogLevel logLevel, string message) 23 | { 24 | // intentionally not awaiting here -- don't want to block forward progress just because we tried to log. 25 | connection.SendRequestAndReceiveResponseAsync( 26 | MessageMethod.Log, 27 | new LogRequest(logLevel, $" {message}"), 28 | CancellationToken.None) 29 | // "observe" any exceptions to avoid unobserved exception escalation, which may terminate the process 30 | .ContinueWith(x => x.Exception, TaskContinuationOptions.OnlyOnFaulted); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/RequestHandlers/GetOperationClaimsRequestHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading.Tasks; 8 | using NuGet.Protocol.Plugins; 9 | using NuGetCredentialProvider.CredentialProviders; 10 | using NuGetCredentialProvider.Logging; 11 | 12 | namespace NuGetCredentialProvider.RequestHandlers 13 | { 14 | /// 15 | /// Handles a and replies with the supported operations. 16 | /// 17 | internal class GetOperationClaimsRequestHandler : RequestHandlerBase 18 | { 19 | /// 20 | /// A when a registered credential provider can provide credentials for the current request. 21 | /// 22 | private static readonly GetOperationClaimsResponse CanProvideCredentialsResponse = new GetOperationClaimsResponse(new List 23 | { 24 | OperationClaim.Authentication 25 | }); 26 | 27 | /// 28 | /// A when no registered credential providers can provide credentials for the current request. 29 | /// 30 | private static readonly GetOperationClaimsResponse EmptyGetOperationClaimsResponse = new GetOperationClaimsResponse(new List()); 31 | 32 | private readonly IReadOnlyCollection _credentialProviders; 33 | 34 | /// 35 | /// Initializes a new instance of the class. 36 | /// 37 | /// A to use for logging. 38 | /// An containing credential providers. 39 | public GetOperationClaimsRequestHandler(ILogger logger, IReadOnlyCollection credentialProviders) 40 | : base(logger) 41 | { 42 | _credentialProviders = credentialProviders ?? throw new ArgumentNullException(nameof(credentialProviders)); 43 | } 44 | 45 | public override Task HandleRequestAsync(GetOperationClaimsRequest request) 46 | { 47 | if (request.PackageSourceRepository != null || request.ServiceIndex != null) 48 | { 49 | return Task.FromResult(EmptyGetOperationClaimsResponse); 50 | } 51 | 52 | return Task.FromResult(CanProvideCredentialsResponse); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/RequestHandlers/InitializeRequestHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System.Threading.Tasks; 6 | using NuGet.Protocol.Plugins; 7 | using NuGetCredentialProvider.Logging; 8 | 9 | namespace NuGetCredentialProvider.RequestHandlers 10 | { 11 | /// 12 | /// Handles an . 13 | /// 14 | internal class InitializeRequestHandler : RequestHandlerBase 15 | { 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// A to use for logging. 20 | public InitializeRequestHandler(ILogger logger) 21 | : base(logger) 22 | { 23 | } 24 | 25 | public override Task HandleRequestAsync(InitializeRequest request) 26 | { 27 | return Task.FromResult(new InitializeResponse(MessageResponseCode.Success)); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/RequestHandlers/RequestHandlerBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using System.Diagnostics; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Newtonsoft.Json; 10 | using NuGet.Common; 11 | using NuGet.Protocol.Plugins; 12 | using NuGetCredentialProvider.Util; 13 | using ILogger = NuGetCredentialProvider.Logging.ILogger; 14 | 15 | namespace NuGetCredentialProvider.RequestHandlers 16 | { 17 | /// 18 | /// A base class for implementations of . 19 | /// 20 | /// The type of the request message. 21 | /// The type of the response message. 22 | internal abstract class RequestHandlerBase : IRequestHandler 23 | where TResponse : class 24 | { 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | /// A to use for logging. 29 | protected RequestHandlerBase(ILogger logger) 30 | { 31 | Logger = logger ?? throw new ArgumentNullException(nameof(logger)); 32 | } 33 | 34 | /// 35 | /// Gets a to use. 36 | /// 37 | public virtual CancellationToken CancellationToken { get; private set; } = CancellationToken.None; 38 | 39 | /// 40 | /// Gets the current . 41 | /// 42 | public IConnection Connection { get; private set; } 43 | 44 | /// 45 | /// Gets the current to use for logging. 46 | /// 47 | public ILogger Logger { get; } 48 | 49 | /// 50 | public async Task HandleResponseAsync(IConnection connection, Message message, IResponseHandler responseHandler, CancellationToken cancellationToken) 51 | { 52 | Stopwatch timer = new Stopwatch(); 53 | timer.Start(); 54 | 55 | Connection = connection; 56 | 57 | TRequest request = MessageUtilities.DeserializePayload(message); 58 | 59 | try { 60 | TResponse response = null; 61 | Logger.Verbose(string.Format(Resources.HandlingRequest, message.Type, message.Method, timer.ElapsedMilliseconds, message.Payload.ToString(Formatting.None))); 62 | try { 63 | using (GetProgressReporter(connection, message, cancellationToken)) 64 | { 65 | response = await HandleRequestAsync(request).ConfigureAwait(continueOnCapturedContext: false); 66 | } 67 | } 68 | catch (Exception ex) when (cancellationToken.IsCancellationRequested) 69 | { 70 | // NuGet will handle canceling event but verbose logs in this case might be interesting. 71 | Logger.Verbose(string.Format(Resources.RequestHandlerCancelingExceptionMessage, ex.InnerException, ex.Message)); 72 | return; 73 | } 74 | Logger.Verbose(string.Format(Resources.SendingResponse, message.Type, message.Method, timer.ElapsedMilliseconds)); 75 | // If we did not send a cancel message, we must submit the response even if cancellationToken is canceled. 76 | await responseHandler.SendResponseAsync(message, response, CancellationToken.None).ConfigureAwait(continueOnCapturedContext: false); 77 | 78 | Logger.Verbose(string.Format(Resources.TimeElapsedAfterSendingResponse, message.Type, message.Method, timer.ElapsedMilliseconds)); 79 | } 80 | catch (Exception ex) 81 | { 82 | // don't report cancellations to the console during shutdown, they're most likely not interesting. 83 | bool cancelingDuringShutdown = ex is OperationCanceledException && Program.IsShuttingDown; 84 | 85 | if (cancelingDuringShutdown) 86 | { 87 | Logger.Log(LogLevel.Verbose, allowOnConsole: false, Resources.WhileShuttingDown); 88 | } 89 | 90 | Logger.Log(LogLevel.Verbose, allowOnConsole: !cancelingDuringShutdown, string.Format(Resources.ResponseHandlerException, message.Method, message.RequestId)); 91 | Logger.Log(LogLevel.Verbose, allowOnConsole: !cancelingDuringShutdown, ex.ToString()); 92 | 93 | throw; 94 | } 95 | 96 | timer.Stop(); 97 | } 98 | 99 | public abstract Task HandleRequestAsync(TRequest request); 100 | 101 | protected virtual AutomaticProgressReporter GetProgressReporter(IConnection connection, Message message, CancellationToken cancellationToken) 102 | { 103 | return null; 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/RequestHandlers/RequestHandlerCollection.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using System.Collections.Concurrent; 7 | using NuGet.Protocol.Plugins; 8 | 9 | namespace NuGetCredentialProvider.RequestHandlers 10 | { 11 | /// 12 | /// Represents a collection of NuGet plug-in request handlers. 13 | /// 14 | /// This custom collection is used instead of because it inherits 15 | /// which allows for the initializer syntax. 16 | internal class RequestHandlerCollection : ConcurrentDictionary, IRequestHandlers 17 | { 18 | public void Add(MessageMethod method, IRequestHandler handler) 19 | { 20 | TryAdd(method, handler); 21 | } 22 | 23 | public void AddOrUpdate(MessageMethod method, Func addHandlerFunc, Func updateHandlerFunc) 24 | { 25 | AddOrUpdate(method, messageMethod => addHandlerFunc(), (messageMethod, requestHandler) => updateHandlerFunc(requestHandler)); 26 | } 27 | 28 | public bool TryGet(MessageMethod method, out IRequestHandler requestHandler) 29 | { 30 | return TryGetValue(method, out requestHandler); 31 | } 32 | 33 | public bool TryRemove(MessageMethod method) 34 | { 35 | return TryRemove(method, out IRequestHandler _); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/RequestHandlers/SetCredentialsRequestHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System.Threading.Tasks; 6 | using NuGet.Protocol.Plugins; 7 | using NuGetCredentialProvider.Logging; 8 | 9 | namespace NuGetCredentialProvider.RequestHandlers 10 | { 11 | /// 12 | /// Handles a 13 | /// 14 | internal class SetCredentialsRequestHandler : RequestHandlerBase 15 | { 16 | private static readonly SetCredentialsResponse SuccessResponse = new SetCredentialsResponse(MessageResponseCode.Success); 17 | 18 | public SetCredentialsRequestHandler(ILogger logger) 19 | : base(logger) 20 | { 21 | } 22 | 23 | public override Task HandleRequestAsync(SetCredentialsRequest request) 24 | { 25 | // There's currently no way to handle proxies, so nothing we can do here 26 | return Task.FromResult(SuccessResponse); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/RequestHandlers/SetLogLevelRequestHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System.Threading.Tasks; 6 | using NuGet.Protocol.Plugins; 7 | using NuGetCredentialProvider.Logging; 8 | 9 | namespace NuGetCredentialProvider.RequestHandlers 10 | { 11 | internal class SetLogLevelRequestHandler : RequestHandlerBase 12 | { 13 | private static readonly SetLogLevelResponse SuccessResponse = new SetLogLevelResponse(MessageResponseCode.Success); 14 | 15 | public SetLogLevelRequestHandler(ILogger logger) 16 | : base(logger) 17 | { 18 | } 19 | 20 | public override Task HandleRequestAsync(SetLogLevelRequest request) 21 | { 22 | Logger.SetLogLevel(request.LogLevel); 23 | return Task.FromResult(SuccessResponse); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/ThirdPartyNotices.txt: -------------------------------------------------------------------------------- 1 | 2 | THIRD-PARTY SOFTWARE NOTICES AND INFORMATION 3 | Do Not Translate or Localize 4 | 5 | Microsoft Visual Studio Team Services Credential Provider for NuGet is based on or incorporates material from the projects listed below (Third Party OSS). The original copyright notice and the license under which Microsoft received such Third Party OSS, are set forth below. Such licenses and notices are provided for informational purposes only. Microsoft licenses the Third Party OSS to you under the licensing terms for the Microsoft product or service. Microsoft reserves all other rights not expressly granted under this agreement, whether by implication, estoppel or otherwise. 6 | 7 | 8 | 1. Json.NET (Newtonsoft.Json) (https://github.com/JamesNK/Newtonsoft.Json) 9 | 2. Powerargs (https://github.com/adamabdelhamed/PowerArgs) 10 | 11 | 12 | %% Json.NET (Newtonsoft.Json) NOTICES AND INFORMATION BEGIN HERE 13 | ========================================= 14 | The MIT License (MIT) 15 | 16 | Copyright (c) 2007 James Newton-King 17 | 18 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | ========================================= 25 | END OF Json.NET (Newtonsoft.Json) NOTICES AND INFORMATION 26 | 27 | %% Powerargs NOTICES AND INFORMATION BEGIN HERE 28 | ========================================= 29 | Copyright (c) 2013 Adam Abdelhamed 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 32 | 33 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 34 | 35 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 36 | ========================================= 37 | END OF Powerargs NOTICES AND INFORMATION 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/Util/CertificateUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Security.Cryptography.X509Certificates; 4 | using ILogger = NuGetCredentialProvider.Logging.ILogger; 5 | 6 | namespace NuGetCredentialProvider.Util; 7 | 8 | internal static class CertificateUtil 9 | { 10 | public static X509Certificate2 GetCertificateBySubjectName(ILogger logger, string subjectName) 11 | { 12 | if (string.IsNullOrWhiteSpace(subjectName)) 13 | { 14 | logger.Info(message: Resources.InvalidCertificateInput); 15 | return null; 16 | } 17 | 18 | var locations = new []{ StoreLocation.CurrentUser, StoreLocation.LocalMachine }; 19 | foreach (var location in locations) 20 | { 21 | var store = new X509Store(StoreName.My, location); 22 | try 23 | { 24 | store.Open(OpenFlags.ReadOnly); 25 | var cert = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName , subjectName, false); 26 | 27 | if (cert.Count > 0) 28 | { 29 | logger.Verbose(string.Format(Resources.ClientCertificateFound, subjectName)); 30 | return cert[0]; 31 | } 32 | } 33 | catch (Exception ex) 34 | { 35 | logger.Error(string.Format(Resources.ClientCertificateError, ex, ex.Message)); 36 | continue; 37 | } 38 | finally 39 | { 40 | store.Close(); 41 | } 42 | } 43 | 44 | logger.Info(string.Format(Resources.ClientCertificateSubjectNameNotFound, subjectName)); 45 | return null; 46 | } 47 | 48 | public static X509Certificate2 GetCertificateByFilePath(ILogger logger, string filePath) 49 | { 50 | if (string.IsNullOrWhiteSpace(filePath)) 51 | { 52 | logger.Info(message: Resources.InvalidCertificateInput); 53 | return null; 54 | } 55 | 56 | try 57 | { 58 | var fileType = Path.GetExtension(filePath); 59 | X509Certificate2 certificate; 60 | switch (fileType) 61 | { 62 | case ".pfx": 63 | certificate = new X509Certificate2(filePath); 64 | break; 65 | case ".pem": 66 | #if NET6_0_OR_GREATER 67 | certificate= X509Certificate2.CreateFromPemFile(filePath); 68 | break; 69 | #endif 70 | throw new NotSupportedException(Resources.ClientCertificatePemFilesNotSupported); 71 | default: 72 | throw new NotSupportedException(Resources.ClientCertificateFileTypeNotSupported); 73 | } 74 | 75 | if (certificate == null) 76 | { 77 | logger.Verbose(string.Format(Resources.ClientCertificateFilePathNotFound, filePath)); 78 | return null; 79 | } 80 | 81 | logger.Verbose(string.Format(Resources.ClientCertificateFound, filePath)); 82 | return certificate; 83 | } 84 | catch (Exception ex) 85 | { 86 | logger.Error(string.Format(Resources.ClientCertificateError, ex, ex.Message)); 87 | return null; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/Util/EncryptedFileWithPermissions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using System.IO; 7 | using System.Runtime.InteropServices; 8 | using System.Security.AccessControl; 9 | using System.Security.Cryptography; 10 | using System.Security.Principal; 11 | using Microsoft.Identity.Client.Extensions.Msal; 12 | using Microsoft.Win32.SafeHandles; 13 | 14 | namespace NuGetCredentialProvider.Util 15 | { 16 | public class EncryptedFileWithPermissions 17 | { 18 | #region Unix specific 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 | public static byte[] ReadFileBytes(string filePath, bool readUnencrypted = false) 33 | { 34 | try 35 | { 36 | return File.Exists(filePath) ? ProtectedData.Unprotect(File.ReadAllBytes(filePath), null, DataProtectionScope.CurrentUser) : null; 37 | } 38 | catch (NotSupportedException) 39 | { 40 | if (readUnencrypted) 41 | { 42 | return File.Exists(filePath) ? File.ReadAllBytes(filePath) : null; 43 | } 44 | 45 | throw; 46 | } 47 | } 48 | 49 | public static void WriteFileBytes(string filePath, byte[] bytes, bool writeUnencrypted = false) 50 | { 51 | try 52 | { 53 | EnsureDirectoryExists(filePath); 54 | 55 | WriteToNewFileWithOwnerRWPermissions(filePath, ProtectedData.Protect(bytes, null, DataProtectionScope.CurrentUser)); 56 | } 57 | catch (NotSupportedException) 58 | { 59 | if (writeUnencrypted) 60 | { 61 | WriteToNewFileWithOwnerRWPermissions(filePath, bytes); 62 | return; 63 | } 64 | 65 | throw; 66 | } 67 | } 68 | 69 | private static void EnsureDirectoryExists(string filePath) 70 | { 71 | var directory = Path.GetDirectoryName(filePath); 72 | 73 | if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) 74 | { 75 | Directory.CreateDirectory(directory); 76 | } 77 | } 78 | 79 | /// 80 | /// Based on https://stackoverflow.com/questions/45132081/file-permissions-on-linux-unix-with-net-core and on 81 | /// https://github.com/NuGet/NuGet.Client/commit/d62db666c710bf95121fe8f5c6a6cbe01985456f and 82 | /// https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/b299b2581da87af50fde751e689f1bd4114516ce/src/client/Microsoft.Identity.Client.Extensions.Msal/Accessors/FileWithPermissions.cs 83 | /// 84 | private static void WriteToNewFileWithOwnerRWPermissions(string path, byte[] bytes) 85 | { 86 | 87 | if (SharedUtilities.IsWindowsPlatform()) 88 | { 89 | WriteToNewFileWithOwnerRWPermissionsWindows(path, bytes); 90 | } 91 | else if (SharedUtilities.IsMacPlatform() || SharedUtilities.IsLinuxPlatform()) 92 | { 93 | WriteToNewFileWithOwnerRWPermissionsUnix(path, bytes); 94 | } 95 | else 96 | { 97 | throw new PlatformNotSupportedException(); 98 | } 99 | } 100 | 101 | private static void WriteToNewFileWithOwnerRWPermissionsUnix(string path, byte[] bytes) 102 | { 103 | int _0600 = Convert.ToInt32("600", 8); 104 | 105 | int fileDescriptor = PosixCreate(path, _0600); 106 | 107 | // if creat() fails, then try to use File.Create because it will throw a meaningful exception. 108 | if (fileDescriptor == -1) 109 | { 110 | int posixCreateError = Marshal.GetLastWin32Error(); 111 | using (File.Create(path)) 112 | { 113 | // File.Create() should have thrown an exception with an appropriate error message 114 | } 115 | File.Delete(path); 116 | throw new InvalidOperationException($"libc creat() failed with last error code {posixCreateError}, but File.Create did not"); 117 | } 118 | 119 | var safeFileHandle = new SafeFileHandle((IntPtr)fileDescriptor, ownsHandle: true); 120 | using var fileStream = new FileStream(safeFileHandle, FileAccess.ReadWrite); 121 | fileStream.Write(bytes, 0, bytes.Length); 122 | } 123 | 124 | #pragma warning disable CA1416 // Validate platform compatibility 125 | private static void WriteToNewFileWithOwnerRWPermissionsWindows(string filePath, byte[] bytes) 126 | { 127 | FileSecurity security = new(); 128 | 129 | var rights = FileSystemRights.Read | FileSystemRights.Write; 130 | 131 | security.AddAccessRule( 132 | new FileSystemAccessRule( 133 | WindowsIdentity.GetCurrent().Name, 134 | rights, 135 | InheritanceFlags.None, 136 | PropagationFlags.NoPropagateInherit, 137 | AccessControlType.Allow)); 138 | 139 | security.SetAccessRuleProtection(isProtected: true, preserveInheritance: false); 140 | 141 | FileStream fs = null; 142 | 143 | try 144 | { 145 | #if NET45_OR_GREATER 146 | if (File.Exists(filePath)) 147 | { 148 | File.Delete(filePath); 149 | } 150 | 151 | fs = File.Create(filePath, bytes.Length, FileOptions.None, security); 152 | #else 153 | FileInfo info = new FileInfo(filePath); 154 | fs = info.Create(FileMode.Create, rights, FileShare.Read, bytes.Length, FileOptions.None, security); 155 | #endif 156 | 157 | fs.Write(bytes, 0, bytes.Length); 158 | } 159 | finally 160 | { 161 | fs?.Dispose(); 162 | } 163 | } 164 | #pragma warning restore CA1416 // Validate platform compatibility 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/Util/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System; 6 | using System.Text; 7 | using NuGet.Common; 8 | using ILogger = NuGetCredentialProvider.Logging.ILogger; 9 | 10 | namespace NuGetCredentialProvider.Util 11 | { 12 | /// 13 | /// Represents the set of extension methods used by this project. 14 | /// 15 | internal static class ExtensionMethods 16 | { 17 | /// 18 | /// Writes a event message to the using the specified message. 19 | /// 20 | /// A instance to write the message to. 21 | /// The level to log at. 22 | /// The message. 23 | public static void Log(this ILogger logger, LogLevel logLevel, string message) 24 | { 25 | logger.Log(logLevel, true, message); 26 | } 27 | 28 | /// 29 | /// Writes a event message to the using the specified message. 30 | /// 31 | /// A instance to write the message to. 32 | /// The message. 33 | public static void Error(this ILogger logger, string message) 34 | { 35 | logger.Log(LogLevel.Error, message); 36 | } 37 | 38 | /// 39 | /// Writes a event message to the using the specified message. 40 | /// 41 | /// A instance to write the message to. 42 | /// The message. 43 | public static void Warning(this ILogger logger, string message) 44 | { 45 | logger.Log(LogLevel.Warning, message); 46 | } 47 | 48 | /// 49 | /// Writes a event message to the using the specified message. 50 | /// 51 | /// A instance to write the message to. 52 | /// The message. 53 | public static void Minimal(this ILogger logger, string message) 54 | { 55 | logger.Log(LogLevel.Minimal, message); 56 | } 57 | 58 | /// 59 | /// Writes a event message to the using the specified message. 60 | /// 61 | /// A instance to write the message to. 62 | /// The message. 63 | public static void Info(this ILogger logger, string message) 64 | { 65 | logger.Log(LogLevel.Information, message); 66 | } 67 | 68 | /// 69 | /// Writes a event message to the using the specified message. 70 | /// 71 | /// A instance to write the message to. 72 | /// The message. 73 | public static void Verbose(this ILogger logger, string message) 74 | { 75 | logger.Log(LogLevel.Verbose, message); 76 | } 77 | 78 | /// 79 | /// Converts the current with just the host by discarding the other parts like path and querystrings. 80 | /// 81 | /// The current to convert. 82 | /// A with only the host. 83 | public static Uri ToHostOnly(this Uri uri) 84 | { 85 | return uri.Segments.Length > 1 86 | ? new Uri($"{uri.Scheme}://{uri.Host}") 87 | : uri; 88 | } 89 | 90 | /// 91 | /// Converts the current string to a JSON web access token (JWT) as a string. 92 | /// 93 | /// The current access token as a string. 94 | /// A JWT as a JSON string. 95 | public static string ToJsonWebTokenString(this string accessToken) 96 | { 97 | // Effictively this splits by '.' and converts from a base-64 encoded string. Splitting creates new strings so this just calculates 98 | // a substring instead to reduce memory overhead. 99 | int start = accessToken.IndexOf(".", StringComparison.Ordinal) + 1; 100 | 101 | if (start < 0) 102 | { 103 | return null; 104 | } 105 | 106 | int length = accessToken.IndexOf(".", start, StringComparison.Ordinal) - start; 107 | 108 | return start > 0 && length < accessToken.Length 109 | ? Encoding.UTF8.GetString( 110 | Convert.FromBase64String( 111 | accessToken.Substring(start, length) 112 | .PadRight(length + (length % 4), '='))) 113 | : null; 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/Util/FeedEndpointCredentialsParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | using Newtonsoft.Json; 6 | using ILogger = NuGetCredentialProvider.Logging.ILogger; 7 | 8 | namespace NuGetCredentialProvider.Util; 9 | public class ExternalEndpointCredentials 10 | { 11 | [JsonProperty("endpoint")] 12 | public string Endpoint { get; set; } 13 | [JsonProperty("username")] 14 | public string Username { get; set; } 15 | [JsonProperty("password")] 16 | public string Password { get; set; } 17 | } 18 | 19 | public class ExternalEndpointCredentialsContainer 20 | { 21 | [JsonProperty("endpointCredentials")] 22 | public ExternalEndpointCredentials[] EndpointCredentials { get; set; } 23 | } 24 | 25 | public class EndpointCredentials 26 | { 27 | [JsonPropertyName("endpoint")] 28 | public string Endpoint { get; set; } 29 | [JsonPropertyName("clientId")] 30 | public string ClientId { get; set; } 31 | [JsonPropertyName("clientCertificateFilePath")] 32 | public string CertificateFilePath { get; set; } 33 | [JsonPropertyName("clientCertificateSubjectName")] 34 | public string CertificateSubjectName { get; set; } 35 | } 36 | 37 | public class EndpointCredentialsContainer 38 | { 39 | [JsonPropertyName("endpointCredentials")] 40 | public EndpointCredentials[] EndpointCredentials { get; set; } 41 | } 42 | 43 | public static class FeedEndpointCredentialsParser 44 | { 45 | private static readonly JsonSerializerOptions options = new JsonSerializerOptions 46 | { 47 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 48 | PropertyNameCaseInsensitive = true, 49 | }; 50 | 51 | public static Dictionary ParseFeedEndpointsJsonToDictionary(ILogger logger) 52 | { 53 | string feedEndpointsJson = Environment.GetEnvironmentVariable(EnvUtil.EndpointCredentials); 54 | if (string.IsNullOrWhiteSpace(feedEndpointsJson)) 55 | { 56 | return new Dictionary(StringComparer.OrdinalIgnoreCase); 57 | } 58 | 59 | try 60 | { 61 | logger.Verbose(Resources.ParsingJson); 62 | Dictionary credsResult = new Dictionary(StringComparer.OrdinalIgnoreCase); 63 | EndpointCredentialsContainer endpointCredentials = System.Text.Json.JsonSerializer.Deserialize(feedEndpointsJson, options); 64 | if (endpointCredentials == null) 65 | { 66 | logger.Verbose(Resources.NoEndpointsFound); 67 | return credsResult; 68 | } 69 | 70 | foreach (var credentials in endpointCredentials.EndpointCredentials) 71 | { 72 | if (credentials == null) 73 | { 74 | logger.Verbose(Resources.EndpointParseFailure); 75 | break; 76 | } 77 | 78 | if (credentials.ClientId == null) 79 | { 80 | logger.Verbose(Resources.EndpointParseFailure); 81 | break; 82 | } 83 | 84 | if (credentials.CertificateSubjectName != null && credentials.CertificateFilePath != null) 85 | { 86 | logger.Verbose(Resources.EndpointParseFailure); 87 | break; 88 | } 89 | 90 | if (!Uri.TryCreate(credentials.Endpoint, UriKind.Absolute, out var endpointUri)) 91 | { 92 | logger.Verbose(Resources.EndpointParseFailure); 93 | break; 94 | } 95 | 96 | var urlEncodedEndpoint = endpointUri.AbsoluteUri; 97 | if (!credsResult.ContainsKey(urlEncodedEndpoint)) 98 | { 99 | credsResult.Add(urlEncodedEndpoint, credentials); 100 | } 101 | } 102 | 103 | return credsResult; 104 | } 105 | catch (Exception ex) 106 | { 107 | logger.Verbose(string.Format(Resources.VstsBuildTaskExternalCredentialCredentialProviderError, ex)); 108 | return new Dictionary(StringComparer.OrdinalIgnoreCase); ; 109 | } 110 | } 111 | 112 | public static Dictionary ParseExternalFeedEndpointsJsonToDictionary(ILogger logger) 113 | { 114 | string feedEndpointsJson = Environment.GetEnvironmentVariable(EnvUtil.BuildTaskExternalEndpoints); 115 | if (string.IsNullOrWhiteSpace(feedEndpointsJson)) 116 | { 117 | return new Dictionary(StringComparer.OrdinalIgnoreCase); 118 | } 119 | 120 | try 121 | { 122 | logger.Verbose(Resources.ParsingJson); 123 | if (feedEndpointsJson.Contains("':")) 124 | { 125 | logger.Warning(Resources.InvalidJsonWarning); 126 | } 127 | 128 | Dictionary credsResult = new Dictionary(StringComparer.OrdinalIgnoreCase); 129 | ExternalEndpointCredentialsContainer endpointCredentials = JsonConvert.DeserializeObject(feedEndpointsJson); 130 | if (endpointCredentials == null) 131 | { 132 | logger.Verbose(Resources.NoEndpointsFound); 133 | return credsResult; 134 | } 135 | 136 | foreach (var credentials in endpointCredentials.EndpointCredentials) 137 | { 138 | if (credentials == null) 139 | { 140 | logger.Verbose(Resources.EndpointParseFailure); 141 | break; 142 | } 143 | 144 | if (credentials.Username == null) 145 | { 146 | credentials.Username = "VssSessionToken"; 147 | } 148 | 149 | if (!Uri.TryCreate(credentials.Endpoint, UriKind.Absolute, out var endpointUri)) 150 | { 151 | logger.Verbose(Resources.EndpointParseFailure); 152 | break; 153 | } 154 | 155 | var urlEncodedEndpoint = endpointUri.AbsoluteUri; 156 | if (!credsResult.ContainsKey(urlEncodedEndpoint)) 157 | { 158 | credsResult.Add(urlEncodedEndpoint, credentials); 159 | } 160 | } 161 | 162 | return credsResult; 163 | } 164 | catch (Exception ex) 165 | { 166 | logger.Verbose(string.Format(Resources.VstsBuildTaskExternalCredentialCredentialProviderError, ex)); 167 | return new Dictionary(StringComparer.OrdinalIgnoreCase); 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/Util/HttpClientFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System.Net.Http; 6 | using System.Net.Http.Headers; 7 | using Microsoft.Artifacts.Authentication; 8 | 9 | namespace NuGetCredentialProvider.Util 10 | { 11 | public class HttpClientFactory : MsalHttpClientFactory 12 | { 13 | private static readonly HttpClientFactory httpClientFactory; 14 | 15 | public static HttpClientFactory Default => httpClientFactory; 16 | 17 | public HttpClientFactory(HttpClient httpClient) : base(httpClient) 18 | { 19 | } 20 | 21 | static HttpClientFactory() 22 | { 23 | var httpClient = new HttpClient(new HttpClientHandler 24 | { 25 | // This is needed to make IWA work. See: 26 | // https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/e15befb5f0a6cf4757c5abcaa6487e28e7ebd1bb/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs#LL26C1-L27C73 27 | UseDefaultCredentials = true 28 | }); 29 | 30 | // Add program context to headers if available 31 | if (ProgramContext != null) 32 | { 33 | httpClient.DefaultRequestHeaders.UserAgent.Add(ProgramContext); 34 | } 35 | 36 | httpClientFactory = new(httpClient); 37 | } 38 | 39 | private static ProductInfoHeaderValue ProgramContext 40 | { 41 | get 42 | { 43 | var context = EnvUtil.GetProgramContextFromEnvironment(); 44 | return context != null 45 | ? new ProductInfoHeaderValue($"({context})") 46 | : null; 47 | } 48 | } 49 | } 50 | 51 | public enum Context 52 | { 53 | Maven, 54 | NuGet, 55 | Pip, 56 | Conda, 57 | Npm 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/Util/ICache.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | namespace NuGetCredentialProvider.Util 6 | { 7 | public interface ICache 8 | { 9 | TValue this[TKey key] { get; set; } 10 | 11 | bool ContainsKey(TKey key); 12 | 13 | bool TryGetValue(TKey key, out TValue value); 14 | 15 | void Remove(TKey key); 16 | } 17 | 18 | public class NoOpCache : ICache 19 | { 20 | public TValue this[TKey key] 21 | { 22 | get => default; 23 | set 24 | { 25 | } 26 | } 27 | 28 | public bool ContainsKey(TKey key) 29 | { 30 | return false; 31 | } 32 | 33 | public void Remove(TKey key) 34 | { 35 | } 36 | 37 | public bool TryGetValue(TKey key, out TValue value) 38 | { 39 | value = default; 40 | 41 | return false; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/helpericons.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/artifacts-credprovider/107acd99393a8ba94be8a3280be18773c0549de4/CredentialProvider.Microsoft/helpericons.ico -------------------------------------------------------------------------------- /CredentialProvider.Microsoft/runtimeconfig.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "rollForward": "Major" 3 | } -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | true 8 | $(NoWarn);CS1574;CS1591 9 | 10 | 11 | 12 | true 13 | true 14 | $(MSBuildThisFileDirectory)keys\FinalPublicKey.snk 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | PrepareResources;$(CoreCompileDependsOn) 13 | 14 | 15 | 16 | 17 | MSBuild:Compile 18 | $(RootNamespace)$([MSBuild]::ValueOrDefault('.%(RelativeDir)', '').Replace('\', '.').Replace('/', '.').TrimEnd('.')) 19 | $(IntermediateOutputPath)%(ResourceNamespace).%(Filename).g$(DefaultLanguageSourceExtension) 20 | $(Language) 21 | %(ResourceNamespace) 22 | %(Filename) 23 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /MicrosoftCredentialProvider.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29411.138 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CredentialProvider.Microsoft", "CredentialProvider.Microsoft\CredentialProvider.Microsoft.csproj", "{E991747F-A42E-4502-A797-7886FFB14841}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CredentialProvider.Microsoft.Tests", "CredentialProvider.Microsoft.Tests\CredentialProvider.Microsoft.Tests.csproj", "{22ACC99A-B915-4DEC-A525-0DB6C8171639}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3A10D4CB-5F08-4267-9199-B1C811498912}" 11 | ProjectSection(SolutionItems) = preProject 12 | Build.props = Build.props 13 | EndProjectSection 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{37ABB2C5-BCCA-4760-BA5A-2171606062D4}" 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Artifacts.Authentication", "src\Authentication\Microsoft.Artifacts.Authentication.csproj", "{505FEC6B-0A7E-45C1-A78A-71C7694F3CAD}" 18 | EndProject 19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Artifacts.Authentication.Tests", "src\Authentication.Tests\Microsoft.Artifacts.Authentication.Tests.csproj", "{7B44129A-0105-445E-8620-567EE7E8F815}" 20 | EndProject 21 | Global 22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 23 | Debug|Any CPU = Debug|Any CPU 24 | Release|Any CPU = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {E991747F-A42E-4502-A797-7886FFB14841}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {E991747F-A42E-4502-A797-7886FFB14841}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {E991747F-A42E-4502-A797-7886FFB14841}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {E991747F-A42E-4502-A797-7886FFB14841}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {22ACC99A-B915-4DEC-A525-0DB6C8171639}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {22ACC99A-B915-4DEC-A525-0DB6C8171639}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {22ACC99A-B915-4DEC-A525-0DB6C8171639}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {22ACC99A-B915-4DEC-A525-0DB6C8171639}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {505FEC6B-0A7E-45C1-A78A-71C7694F3CAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {505FEC6B-0A7E-45C1-A78A-71C7694F3CAD}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {505FEC6B-0A7E-45C1-A78A-71C7694F3CAD}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {505FEC6B-0A7E-45C1-A78A-71C7694F3CAD}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {7B44129A-0105-445E-8620-567EE7E8F815}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {7B44129A-0105-445E-8620-567EE7E8F815}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {7B44129A-0105-445E-8620-567EE7E8F815}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {7B44129A-0105-445E-8620-567EE7E8F815}.Release|Any CPU.Build.0 = Release|Any CPU 43 | EndGlobalSection 44 | GlobalSection(SolutionProperties) = preSolution 45 | HideSolutionNode = FALSE 46 | EndGlobalSection 47 | GlobalSection(ExtensibilityGlobals) = postSolution 48 | SolutionGuid = {B8C4B4A0-5840-4475-86EA-05AF868057AF} 49 | EndGlobalSection 50 | GlobalSection(NestedProjects) = preSolution 51 | {505FEC6B-0A7E-45C1-A78A-71C7694F3CAD} = {37ABB2C5-BCCA-4760-BA5A-2171606062D4} 52 | {7B44129A-0105-445E-8620-567EE7E8F815} = {37ABB2C5-BCCA-4760-BA5A-2171606062D4} 53 | EndGlobalSection 54 | EndGlobal 55 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /build/validate-install-script-bash.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: repo 3 | type: string 4 | - name: scriptEnvVariables 5 | type: string 6 | - name: expectedCredentialProviderVersion 7 | type: string 8 | 9 | steps: 10 | - checkout: ${{ parameters.repo }} 11 | - bash: | 12 | ${{ parameters.scriptEnvVariables }} 13 | ./helpers/installcredprovider.sh -Force >> ./output.log 14 | cat ./output.log 15 | 16 | if ! grep ${{ parameters.expectedCredentialProviderVersion }} ./output.log; then 17 | echo "Expected credential provider not found" 18 | exit 1 19 | fi 20 | if [ ! -d "$HOME/.nuget/plugins/netcore/CredentialProvider.Microsoft" ]; then 21 | echo "Credential provider plugin directory not found" 22 | exit 1 23 | fi 24 | 25 | echo "Credential provider installed successfully" 26 | workingDirectory: $(Build.SourcesDirectory) 27 | displayName: Validate Install Script 28 | - bash: | 29 | if grep $'\r' ./helpers/installcredprovider.sh; then 30 | echo "CRLF line ending found" 31 | exit 1 32 | fi 33 | workingDirectory: $(Build.SourcesDirectory) 34 | displayName: Check Line Endings -------------------------------------------------------------------------------- /build/validate-install-script-powershell.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: repo 3 | type: string 4 | - name: scriptInputs 5 | type: string 6 | - name: expectedCredentialProviderVersion 7 | type: string 8 | 9 | steps: 10 | - checkout: ${{ parameters.repo }} 11 | - task: PowerShell@2 12 | displayName: Validate Install Script 13 | inputs: 14 | targetType: 'inline' 15 | script: | 16 | ./helpers/installcredprovider.ps1 ${{ parameters.scriptInputs }} -Force 6>> ./output.log 17 | cat ./output.log 18 | if( (Select-String -Path .\output.log -Pattern ${{ parameters.expectedCredentialProviderVersion }}) -eq $null) {echo "Expected credential provider file not found."; exit 1} 19 | workingDirectory: $(Build.SourcesDirectory) -------------------------------------------------------------------------------- /build/validate-install-script.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - job: WindowsInstallDefault 3 | pool: 4 | vmImage: windows-latest 5 | steps: 6 | - template: validate-install-script-powershell.yml@self 7 | parameters: 8 | repo: self 9 | scriptInputs: '' 10 | expectedCredentialProviderVersion: 'Microsoft.Net6.NuGet.CredentialProvider' 11 | - job: WindowsInstallNet8 12 | pool: 13 | vmImage: windows-latest 14 | steps: 15 | - template: validate-install-script-powershell.yml@self 16 | parameters: 17 | repo: self 18 | scriptInputs: '-InstallNet8' 19 | expectedCredentialProviderVersion: 'Microsoft.Net8.NuGet.CredentialProvider' 20 | - job: WindowsInstallNet8winx64 21 | pool: 22 | vmImage: windows-latest 23 | steps: 24 | - template: validate-install-script-powershell.yml@self 25 | parameters: 26 | repo: self 27 | scriptInputs: '-InstallNet8 -RuntimeIdentifier win-x64' 28 | expectedCredentialProviderVersion: 'Microsoft.Net8.win-x64.NuGet.CredentialProvider' 29 | - job: WindowsInstallNet8winx64NoParams 30 | pool: 31 | vmImage: windows-latest 32 | steps: 33 | - template: validate-install-script-powershell.yml@self 34 | parameters: 35 | repo: self 36 | scriptInputs: '-RuntimeIdentifier win-x64' 37 | expectedCredentialProviderVersion: 'Microsoft.Net8.win-x64.NuGet.CredentialProvider' 38 | - job: WindowsInstallNet8winx64Net6Params 39 | pool: 40 | vmImage: windows-latest 41 | steps: 42 | - template: validate-install-script-powershell.yml@self 43 | parameters: 44 | repo: self 45 | scriptInputs: '-InstallNet6 -RuntimeIdentifier win-x64' 46 | expectedCredentialProviderVersion: 'Microsoft.Net8.win-x64.NuGet.CredentialProvider' 47 | - job: WindowsInstallNetfxDefault 48 | pool: 49 | vmImage: windows-latest 50 | steps: 51 | - template: validate-install-script-powershell.yml@self 52 | parameters: 53 | repo: self 54 | scriptInputs: '-AddNetfx' 55 | expectedCredentialProviderVersion: 'Microsoft.NuGet.CredentialProvider' 56 | - job: WindowsInstallNetfx48 57 | pool: 58 | vmImage: windows-latest 59 | steps: 60 | - template: validate-install-script-powershell.yml@self 61 | parameters: 62 | repo: self 63 | scriptInputs: '-AddNetFx48' 64 | expectedCredentialProviderVersion: 'Microsoft.NetFx48.NuGet.CredentialProvider' 65 | - job: LinuxInstallDefault 66 | pool: 67 | vmImage: ubuntu-latest 68 | steps: 69 | - template: validate-install-script-bash.yml@self 70 | parameters: 71 | repo: self 72 | scriptEnvVariables: '' 73 | expectedCredentialProviderVersion: 'Microsoft.Net6.NuGet.CredentialProvider' 74 | - job: LinuxInstallNet8 75 | pool: 76 | vmImage: ubuntu-latest 77 | steps: 78 | - template: validate-install-script-bash.yml@self 79 | parameters: 80 | repo: self 81 | scriptEnvVariables: | 82 | export USE_NET6_ARTIFACTS_CREDENTIAL_PROVIDER=false 83 | export USE_NET8_ARTIFACTS_CREDENTIAL_PROVIDER=true 84 | expectedCredentialProviderVersion: 'Microsoft.Net8.NuGet.CredentialProvider' 85 | - job: LinuxInstallNet8Net6Param 86 | pool: 87 | vmImage: ubuntu-latest 88 | steps: 89 | - template: validate-install-script-bash.yml@self 90 | parameters: 91 | repo: self 92 | scriptEnvVariables: | 93 | export USE_NET6_ARTIFACTS_CREDENTIAL_PROVIDER=true 94 | export USE_NET8_ARTIFACTS_CREDENTIAL_PROVIDER=true 95 | expectedCredentialProviderVersion: 'Microsoft.Net8.NuGet.CredentialProvider' 96 | - job: LinuxInstallNetFx 97 | pool: 98 | vmImage: ubuntu-latest 99 | steps: 100 | - template: validate-install-script-bash.yml@self 101 | parameters: 102 | repo: self 103 | scriptEnvVariables: | 104 | export USE_NET6_ARTIFACTS_CREDENTIAL_PROVIDER=false 105 | export USE_NET8_ARTIFACTS_CREDENTIAL_PROVIDER=false 106 | expectedCredentialProviderVersion: 'Microsoft.NuGet.CredentialProvider' 107 | - job: LinuxInstallNet8linuxx64 108 | pool: 109 | vmImage: ubuntu-latest 110 | steps: 111 | - template: validate-install-script-bash.yml@self 112 | parameters: 113 | repo: self 114 | scriptEnvVariables: | 115 | export USE_NET6_ARTIFACTS_CREDENTIAL_PROVIDER=false 116 | export USE_NET8_ARTIFACTS_CREDENTIAL_PROVIDER=true 117 | export ARTIFACTS_CREDENTIAL_PROVIDER_RID=linux-x64 118 | expectedCredentialProviderVersion: 'Microsoft.Net8.linux-x64.NuGet.CredentialProvider' 119 | - job: LinuxInstallNet8linuxarm64 120 | pool: 121 | vmImage: ubuntu-latest 122 | steps: 123 | - template: validate-install-script-bash.yml@self 124 | parameters: 125 | repo: self 126 | scriptEnvVariables: | 127 | export USE_NET6_ARTIFACTS_CREDENTIAL_PROVIDER=false 128 | export USE_NET8_ARTIFACTS_CREDENTIAL_PROVIDER=true 129 | export ARTIFACTS_CREDENTIAL_PROVIDER_RID=linux-arm64 130 | expectedCredentialProviderVersion: 'Microsoft.Net8.linux-arm64.NuGet.CredentialProvider' 131 | - job: macOSInstallNet8osxarm64 132 | pool: 133 | vmImage: macOS-latest 134 | steps: 135 | - template: validate-install-script-bash.yml@self 136 | parameters: 137 | repo: self 138 | scriptEnvVariables: | 139 | export USE_NET6_ARTIFACTS_CREDENTIAL_PROVIDER=false 140 | export USE_NET8_ARTIFACTS_CREDENTIAL_PROVIDER=true 141 | export ARTIFACTS_CREDENTIAL_PROVIDER_RID=osx-arm64 142 | expectedCredentialProviderVersion: 'Microsoft.Net8.osx-arm64.NuGet.CredentialProvider' 143 | - job: macOSnstallNet8osxx64 144 | pool: 145 | vmImage: macOS-latest 146 | steps: 147 | - template: validate-install-script-bash.yml@self 148 | parameters: 149 | repo: self 150 | scriptEnvVariables: | 151 | export USE_NET6_ARTIFACTS_CREDENTIAL_PROVIDER=false 152 | export USE_NET8_ARTIFACTS_CREDENTIAL_PROVIDER=true 153 | export ARTIFACTS_CREDENTIAL_PROVIDER_RID=osx-x64 154 | expectedCredentialProviderVersion: 'Microsoft.Net8.osx-x64.NuGet.CredentialProvider' 155 | - job: macOSInstallNet8osxx64NoParams 156 | pool: 157 | vmImage: macOS-latest 158 | steps: 159 | - template: validate-install-script-bash.yml@self 160 | parameters: 161 | repo: self 162 | scriptEnvVariables: | 163 | export ARTIFACTS_CREDENTIAL_PROVIDER_RID=osx-x64 164 | expectedCredentialProviderVersion: 'Microsoft.Net8.osx-x64.NuGet.CredentialProvider' 165 | - job: macOSnstallNet8osxx64Net6Params 166 | pool: 167 | vmImage: macOS-latest 168 | steps: 169 | - template: validate-install-script-bash.yml@self 170 | parameters: 171 | repo: self 172 | scriptEnvVariables: | 173 | export USE_NET6_ARTIFACTS_CREDENTIAL_PROVIDER=true 174 | export ARTIFACTS_CREDENTIAL_PROVIDER_RID=osx-x64 175 | expectedCredentialProviderVersion: 'Microsoft.Net8.osx-x64.NuGet.CredentialProvider' 176 | -------------------------------------------------------------------------------- /helpers/installcredprovider.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # DESCRIPTION: A simple shell script designed to fetch a version 3 | # of the artifacts credential provider plugin and install it into $HOME/.nuget/plugins. 4 | # Readme: https://github.com/Microsoft/artifacts-credprovider/blob/master/README.md 5 | 6 | # Default version to install is the latest version. 7 | # To install a release other than `latest`, set the `AZURE_ARTIFACTS_CREDENTIAL_PROVIDER_VERSION` environment 8 | # variable to match the TAG NAME of a supported release, e.g. "v1.0.1". 9 | # Releases: https://github.com/microsoft/artifacts-credprovider/releases 10 | 11 | REPO="Microsoft/artifacts-credprovider" 12 | NUGET_PLUGIN_DIR="$HOME/.nuget/plugins" 13 | 14 | # If a RuntimeID (RID) is set, install the self-contained version of the .NET 8 credential provider. 15 | # To install a release with a specific runtime version set the `ARTIFACTS_CREDENTIAL_PROVIDER_RID` enviornment variable. 16 | if [ ! -z ${ARTIFACTS_CREDENTIAL_PROVIDER_RID} ]; then 17 | echo "INFO: ARTIFACTS_CREDENTIAL_PROVIDER_RID variable set, defaulting to NET8 installation." 18 | 19 | # If the RID is osx-*, use the zip file otherwise use the tar.gz file. 20 | case ${ARTIFACTS_CREDENTIAL_PROVIDER_RID} in osx-* ) 21 | FILE="Microsoft.Net8.${ARTIFACTS_CREDENTIAL_PROVIDER_RID}.NuGet.CredentialProvider.zip" 22 | ;; 23 | *) 24 | FILE="Microsoft.Net8.${ARTIFACTS_CREDENTIAL_PROVIDER_RID}.NuGet.CredentialProvider.tar.gz" 25 | ;; 26 | esac 27 | 28 | if [ -z ${USE_NET6_ARTIFACTS_CREDENTIAL_PROVIDER} ]; then 29 | echo "WARNING: The USE_NET6_ARTIFACTS_CREDENTIAL_PROVIDER variable is set, but ARTIFACTS_CREDENTIAL_PROVIDER_RID variable is defined. The NET8 version of the credential provider will be installed." 30 | fi 31 | 32 | # throw if version starts < 1.4.0. (self-contained not supported) 33 | case ${AZURE_ARTIFACTS_CREDENTIAL_PROVIDER_VERSION} in 34 | 0.*|v0.*|1.0.*|v1.0.*|1.1.*|v1.1.*|1.2.*|v1.2.*|1.3.*|v1.3.*) 35 | echo "ERROR: To install NET8 cred provider using the ARTIFACTS_CREDENTIAL_PROVIDER_RID variable, version to be installed must be 1.4.0 or greater. Check your AZURE_ARTIFACTS_CREDENTIAL_PROVIDER_VERSION variable." 36 | exit 1 37 | ;; 38 | esac 39 | # If .NET 8 variable is set, install the .NET 8 version of the credential provider even if USE_NET6_ARTIFACTS_CREDENTIAL_PROVIDER is true. 40 | elif [ ! -z ${USE_NET8_ARTIFACTS_CREDENTIAL_PROVIDER} ] && [ ${USE_NET8_ARTIFACTS_CREDENTIAL_PROVIDER} != "false" ]; then 41 | # Default to the full zip file since ARTIFACTS_CREDENTIAL_PROVIDER_RID is not specified. 42 | FILE="Microsoft.Net8.NuGet.CredentialProvider.tar.gz" 43 | 44 | if [ -z ${USE_NET6_ARTIFACTS_CREDENTIAL_PROVIDER} ]; then 45 | echo "WARNING: The USE_NET6_ARTIFACTS_CREDENTIAL_PROVIDER variable is set, but USE_NET8_ARTIFACTS_CREDENTIAL_PROVIDER variable is true. The NET8 version of the credential provider will be installed." 46 | fi 47 | 48 | # throw if version starts < 1.3.0. (net8 not supported) 49 | case ${AZURE_ARTIFACTS_CREDENTIAL_PROVIDER_VERSION} in 50 | 0.*|v0.*|1.0.*|v1.0.*|1.1.*|v1.1.*|1.2.*|v1.2.*) 51 | echo "ERROR: To install NET8 cred provider using the USE_NET8_ARTIFACTS_CREDENTIAL_PROVIDER variable, version to be installed must be 1.3.0 or greater. Check your AZURE_ARTIFACTS_CREDENTIAL_PROVIDER_VERSION variable." 52 | exit 1 53 | ;; 54 | esac 55 | # .NET 6 is the default installation, attempt to install unless set to false. 56 | elif [ -z ${USE_NET6_ARTIFACTS_CREDENTIAL_PROVIDER} ] || [ ${USE_NET6_ARTIFACTS_CREDENTIAL_PROVIDER} != "false" ]; then 57 | FILE="Microsoft.Net6.NuGet.CredentialProvider.tar.gz" 58 | 59 | # throw if version starts with 0. (net6 not supported) 60 | case ${AZURE_ARTIFACTS_CREDENTIAL_PROVIDER_VERSION} in 61 | 0.*|v0.*) 62 | echo "ERROR: To install NET6 cred provider using the USE_NET6_ARTIFACTS_CREDENTIAL_PROVIDER variable, version to be installed must be 1.0.0 or greater. Check your AZURE_ARTIFACTS_CREDENTIAL_PROVIDER_VERSION variable." 63 | exit 1 64 | ;; 65 | esac 66 | # If .NET 6 is disabled and .NET 8 isn't explicitly enabled, fall back to the legacy .NET Framework. 67 | else 68 | echo "WARNING: The .Net Framework 3.1 version of the Credential Provider is deprecated and will be removed in the next major release. Please migrate to the .Net Framework 4.8 or .Net Core versions." 69 | FILE="Microsoft.NuGet.CredentialProvider.tar.gz" 70 | fi 71 | 72 | # If AZURE_ARTIFACTS_CREDENTIAL_PROVIDER_VERSION is set, install the version specified, otherwise install latest 73 | if [ ! -z ${AZURE_ARTIFACTS_CREDENTIAL_PROVIDER_VERSION} ] && [ ${AZURE_ARTIFACTS_CREDENTIAL_PROVIDER_VERSION} != "latest" ]; then 74 | # browser_download_url from https://api.github.com/repos/Microsoft/artifacts-credprovider/releases/latest 75 | URI="https://github.com/$REPO/releases/download/${AZURE_ARTIFACTS_CREDENTIAL_PROVIDER_VERSION}/$FILE" 76 | else 77 | # URL pattern to get latest documented at https://help.github.com/en/articles/linking-to-releases as of 2019-03-29 78 | URI="https://github.com/$REPO/releases/latest/download/$FILE" 79 | fi 80 | 81 | # Ensure plugin directory exists 82 | if [ ! -d "${NUGET_PLUGIN_DIR}" ]; then 83 | echo "INFO: Creating the nuget plugin directory (i.e. ${NUGET_PLUGIN_DIR}). " 84 | if ! mkdir -p "${NUGET_PLUGIN_DIR}"; then 85 | echo "ERROR: Unable to create nuget plugins directory (i.e. ${NUGET_PLUGIN_DIR})." 86 | exit 1 87 | fi 88 | fi 89 | 90 | echo "Downloading from $URI" 91 | # Extract netcore from the .tar.gz into the plugin directory 92 | 93 | #Fetch the file 94 | if ! curl -H "Accept: application/octet-stream" \ 95 | -s \ 96 | -S \ 97 | -L \ 98 | "$URI" | tar xz -C "$HOME/.nuget/" "plugins/netcore"; then 99 | exit 1 100 | fi 101 | 102 | echo "INFO: credential provider netcore plugin extracted to $HOME/.nuget/" -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /samples/dockerfile.sample.txt: -------------------------------------------------------------------------------- 1 | # This sample is for Artifacts Credential Provider version 0.1.28 and it is incompatible with greater versions due to the .NET Core version this sample uses. 2 | 3 | # downloading the dotnet sdk image. Could be any docker sdk image with sdk > 2.1.500 4 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS dotnet-builder 5 | ARG FEED_URL 6 | ARG PAT 7 | 8 | # download and install latest credential provider. Not required after https://github.com/dotnet/dotnet-docker/issues/878 9 | RUN wget -qO- https://aka.ms/install-artifacts-credprovider.sh | bash 10 | 11 | # Optional 12 | WORKDIR /workdir 13 | COPY ./ . 14 | 15 | # Optional: Sometimes the http client hangs because of a .NET issue. Setting this in dockerfile helps 16 | ENV DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER=0 17 | 18 | # Environment variable to enable seesion token cache. More on this here: https://github.com/Microsoft/artifacts-credprovider#help 19 | ENV NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED true 20 | 21 | # Environment variable for adding endpoint credentials. More on this here: https://github.com/Microsoft/artifacts-credprovider#help 22 | # Add "FEED_URL" AND "PAT" using --build-arg in docker build step. "endpointCredentials" field is an array, you can add multiple endpoint configurations. 23 | # Make sure that you *do not* hard code the "PAT" here. That is a sensitive information and must not be checked in. 24 | ENV VSS_NUGET_EXTERNAL_FEED_ENDPOINTS {\"endpointCredentials\": [{\"endpoint\":\"${FEED_URL}\", \"username\":\"ArtifactsDocker\", \"password\":\"${PAT}\"}]} 25 | 26 | # Use this if you have a nuget.config file with all the endpoints. 27 | RUN dotnet restore 28 | 29 | # Optional: Extended step to build the app using dotnet msbuild. 30 | RUN dotnet build dirs.proj 31 | -------------------------------------------------------------------------------- /src/Authentication.Tests/AccountPriorityTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | namespace Microsoft.Artifacts.Authentication.Tests; 6 | 7 | [TestClass] 8 | public class AccountPriorityTests 9 | { 10 | private class TestAccount : IAccount 11 | { 12 | public string? Username { get; set; } 13 | public string? Environment { get; set; } 14 | public AccountId? HomeAccountId { get; set; } 15 | } 16 | 17 | private static readonly Guid ContosoTenant = Guid.NewGuid(); 18 | private static readonly Guid FabrikamTenant = Guid.NewGuid(); 19 | 20 | private static readonly IAccount FabrikamUser = new TestAccount 21 | { 22 | Username = "billg@fabrikam.com", 23 | Environment = string.Empty, 24 | HomeAccountId = new AccountId(string.Empty, string.Empty, FabrikamTenant.ToString()), 25 | }; 26 | 27 | private static readonly IAccount ContosoUser = new TestAccount 28 | { 29 | Username = "billg@contoso.com", 30 | Environment = string.Empty, 31 | HomeAccountId = new AccountId(string.Empty, string.Empty, ContosoTenant.ToString()), 32 | }; 33 | 34 | private static readonly IAccount MsaUser = new TestAccount 35 | { 36 | Username = "bill.gates@live.com", 37 | Environment = string.Empty, 38 | HomeAccountId = new AccountId(string.Empty, string.Empty, MsalConstants.MsaAccountTenant.ToString()), 39 | }; 40 | 41 | private static readonly List> Permutations = new List>() 42 | { 43 | new List { ContosoUser, MsaUser, FabrikamUser }, 44 | new List { ContosoUser, FabrikamUser, MsaUser }, 45 | new List { MsaUser, ContosoUser, FabrikamUser }, 46 | new List { MsaUser, FabrikamUser, ContosoUser }, 47 | new List { FabrikamUser, MsaUser, ContosoUser }, 48 | new List { FabrikamUser, ContosoUser, MsaUser }, 49 | }; 50 | 51 | [TestMethod] 52 | public void MsaMatchesMsa() 53 | { 54 | foreach (var accounts in Permutations) 55 | { 56 | var applicable = MsalExtensions.GetApplicableAccounts(accounts, MsalConstants.FirstPartyTenant, loginHint: null); 57 | Assert.AreEqual(applicable[0].Item1.Username, MsaUser.Username); 58 | } 59 | } 60 | 61 | [TestMethod] 62 | public void ContosoMatchesContoso() 63 | { 64 | foreach (var accounts in Permutations) 65 | { 66 | var applicable = MsalExtensions.GetApplicableAccounts(accounts, ContosoTenant, loginHint: null); 67 | Assert.AreEqual(applicable[0].Item1.Username, ContosoUser.Username); 68 | } 69 | } 70 | 71 | [TestMethod] 72 | public void FabrikamMatchesFabrikam() 73 | { 74 | foreach (var accounts in Permutations) 75 | { 76 | var applicable = MsalExtensions.GetApplicableAccounts(accounts, FabrikamTenant, loginHint: null); 77 | Assert.AreEqual(applicable[0].Item1.Username, FabrikamUser.Username); 78 | } 79 | } 80 | 81 | [TestMethod] 82 | public void LoginHintOverride() 83 | { 84 | foreach (var accounts in Permutations) 85 | { 86 | foreach (var tenantId in Permutations[0].Select(a => Guid.Parse(a.HomeAccountId.TenantId))) 87 | { 88 | foreach (var loginHint in Permutations[0].Select(a => a.Username)) 89 | { 90 | var applicable = MsalExtensions.GetApplicableAccounts(accounts, tenantId, loginHint); 91 | Assert.AreEqual(applicable[0].Item1.Username, loginHint); 92 | } 93 | } 94 | } 95 | } 96 | 97 | [TestMethod] 98 | public void UnknownAuthorityTenantPrefersMsa() 99 | { 100 | foreach (var accounts in Permutations) 101 | { 102 | var applicable = MsalExtensions.GetApplicableAccounts(accounts, Guid.Empty, loginHint: null); 103 | Assert.AreEqual(applicable[0].Item1.Username, MsaUser.Username); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Authentication.Tests/Microsoft.Artifacts.Authentication.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Microsoft.Artifacts.Authentication.Tests 5 | Microsoft.Artifacts.Authentication.Tests 6 | net6.0 7 | enable 8 | enable 9 | 10 | false 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Authentication.Tests/MsalAuthenticationTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | namespace Microsoft.Artifacts.Authentication.Tests; 6 | 7 | // Tests are meant to be run manually as it requires interactive logins, multiple accounts, and domain joined machines. 8 | [Ignore] 9 | [TestClass] 10 | public class MsalAuthenticationTests 11 | { 12 | private static readonly Uri PackageUri = new Uri("https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/artifacts-credprovider/nuget/v3/index.json"); 13 | private static readonly Uri AuthorityUri = new Uri("https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47"); 14 | private static ILogger logger = LoggerFactory 15 | .Create(builder => 16 | { 17 | builder.SetMinimumLevel(Extensions.Logging.LogLevel.Trace); 18 | builder.AddDebug(); 19 | builder.AddConsole(); 20 | }) 21 | .CreateLogger(nameof(MsalAuthenticationTests)); 22 | 23 | private static MsalCacheHelper cache = MsalCache 24 | .GetMsalCacheHelperAsync(MsalCache.DefaultMsalCacheLocation, logger).GetAwaiter().GetResult(); 25 | 26 | private IPublicClientApplication app = AzureArtifacts 27 | .CreateDefaultBuilder(AuthorityUri) 28 | .WithBroker(true, logger) 29 | // The test hosting process (testhost.exe) may not have an associated console window, so use 30 | // the foreground window which is correct enough when debugging and running tests locally. 31 | .WithParentActivityOrWindow(() => GetForegroundWindow()) 32 | .Build(cache); 33 | 34 | [TestMethod] 35 | public async Task MsalAcquireTokenSilentTest() 36 | { 37 | var tokenProvider = new MsalSilentTokenProvider(app, logger); 38 | var tokenRequest = new TokenRequest(); 39 | 40 | var result = await tokenProvider.GetTokenAsync(tokenRequest); 41 | 42 | Assert.IsNotNull(result); 43 | Assert.AreEqual(TokenSource.Broker, result.AuthenticationResultMetadata.TokenSource); 44 | } 45 | 46 | [TestMethod] 47 | public async Task MsalAcquireTokenByIntegratedWindowsAuthTest() 48 | { 49 | var tokenProvider = new MsalIntegratedWindowsAuthTokenProvider(app, logger); 50 | var tokenRequest = new TokenRequest(); 51 | 52 | var result = await tokenProvider.GetTokenAsync(tokenRequest); 53 | 54 | Assert.IsNotNull(result); 55 | Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); 56 | } 57 | 58 | [TestMethod] 59 | public async Task MsalAcquireTokenInteractiveTest() 60 | { 61 | var tokenProvider = new MsalInteractiveTokenProvider(app, logger); 62 | var tokenRequest = new TokenRequest(); 63 | 64 | var result = await tokenProvider.GetTokenAsync(tokenRequest); 65 | 66 | Assert.IsNotNull(result); 67 | Assert.AreEqual(TokenSource.Broker, result.AuthenticationResultMetadata.TokenSource); 68 | } 69 | 70 | [TestMethod] 71 | public async Task MsalAcquireTokenWithDeviceCodeTest() 72 | { 73 | var tokenProvider = new MsalDeviceCodeTokenProvider(app, logger); 74 | var tokenRequest = new TokenRequest(); 75 | 76 | var result = await tokenProvider.GetTokenAsync(tokenRequest); 77 | 78 | Assert.IsNotNull(result); 79 | Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); 80 | } 81 | 82 | [TestMethod] 83 | public async Task MsalAquireTokenWithManagedIdentity() 84 | { 85 | var tokenProvider = new MsalManagedIdentityTokenProvider(app, logger); 86 | var tokenRequest = new TokenRequest(); 87 | tokenRequest.ClientId = Environment.GetEnvironmentVariable("ARTIFACTS_CREDENTIALPROVIDER_TEST_CLIENTID"); 88 | 89 | var result = await tokenProvider.GetTokenAsync(tokenRequest); 90 | 91 | Assert.IsNotNull(result); 92 | Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); 93 | } 94 | 95 | [TestMethod] 96 | public async Task MsalAquireTokenWithServicePrincipal() 97 | { 98 | var tokenProvider = new MsalServicePrincipalTokenProvider(app, logger); 99 | var tokenRequest = new TokenRequest(); 100 | tokenRequest.ClientId = Environment.GetEnvironmentVariable("ARTIFACTS_CREDENTIALPROVIDER_TEST_CLIENTID"); 101 | tokenRequest.ClientCertificate = new X509Certificate2(Environment.GetEnvironmentVariable("ARTIFACTS_CREDENTIALPROVIDER_TEST_CERT_PATH") ?? string.Empty); 102 | 103 | var result = await tokenProvider.GetTokenAsync(tokenRequest); 104 | 105 | Assert.IsNotNull(result); 106 | Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); 107 | } 108 | 109 | [System.Runtime.InteropServices.DllImport("user32.dll")] 110 | private static extern IntPtr GetForegroundWindow(); 111 | } 112 | 113 | internal static class MsalCacheExtensions 114 | { 115 | public static IPublicClientApplication Build(this PublicClientApplicationBuilder builder, MsalCacheHelper? cache = null) 116 | { 117 | IPublicClientApplication app = builder.Build(); 118 | 119 | cache?.RegisterCache(app.UserTokenCache); 120 | 121 | return app; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Authentication.Tests/MsalHttpClientFactoryTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | namespace Microsoft.Artifacts.Authentication.Tests; 6 | 7 | [TestClass] 8 | public class MsalHttpClientFactoryTests 9 | { 10 | [TestMethod] 11 | public void GetHttpClientTest() 12 | { 13 | var httpClient = new HttpClient(); 14 | var httpClientFactory = new MsalHttpClientFactory(httpClient); 15 | 16 | Assert.AreEqual(httpClient, httpClientFactory.GetHttpClient()); 17 | } 18 | 19 | [TestMethod] 20 | public void UserAgentTest() 21 | { 22 | var httpClientFactory = new MsalHttpClientFactory(new HttpClient()); 23 | var httpClient = httpClientFactory.GetHttpClient(); 24 | var userAgent = httpClient.DefaultRequestHeaders.UserAgent; 25 | 26 | Assert.AreEqual(4, userAgent.Count); 27 | 28 | var programProduct = userAgent.ElementAt(0); 29 | Assert.IsNotNull(programProduct.Product); 30 | Assert.AreEqual(MsalHttpClientFactory.ProgramProduct, programProduct); 31 | Assert.AreEqual(PlatformInformation.GetProgramName(), programProduct.Product.Name); 32 | Assert.AreEqual(PlatformInformation.GetProgramVersion(), programProduct.Product.Version); 33 | 34 | var programComment = userAgent.ElementAt(1); 35 | Assert.AreEqual(MsalHttpClientFactory.ProgramComment, programComment); 36 | Assert.AreEqual($"({PlatformInformation.GetOSType()}; {PlatformInformation.GetCpuArchitecture()}; {PlatformInformation.GetOsDescription()})", programComment.Comment); 37 | 38 | var clrProduct = userAgent.ElementAt(2); 39 | Assert.IsNotNull(clrProduct.Product); 40 | Assert.AreEqual(MsalHttpClientFactory.ClrProduct, clrProduct); 41 | Assert.AreEqual("CLR", clrProduct.Product.Name); 42 | Assert.AreEqual(PlatformInformation.GetClrVersion(), clrProduct.Product.Version); 43 | 44 | var clrComment = userAgent.ElementAt(3); 45 | Assert.AreEqual(MsalHttpClientFactory.ClrComment, clrComment); 46 | Assert.AreEqual($"({PlatformInformation.GetClrFramework()}; {PlatformInformation.GetClrRuntime()}; {PlatformInformation.GetClrDescription()})", clrComment.Comment); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Authentication.Tests/TokenProviderTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | namespace Microsoft.Artifacts.Authentication.Tests; 6 | 7 | [TestClass] 8 | public class TokenProviderTests 9 | { 10 | private static readonly Uri PackageUri = new Uri("https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/artifacts-credprovider/nuget/v3/index.json"); 11 | 12 | private Mock appMock = new Mock(MockBehavior.Strict); 13 | private Mock loggerMock = new Mock(); 14 | 15 | [TestMethod] 16 | public void MsalSilentContractTest() 17 | { 18 | var tokenProvider = new MsalSilentTokenProvider(appMock.Object, loggerMock.Object); 19 | var tokenRequest = new TokenRequest(); 20 | 21 | Assert.IsNotNull(tokenProvider.Name); 22 | Assert.IsFalse(tokenProvider.IsInteractive); 23 | Assert.IsTrue(tokenProvider.CanGetToken(tokenRequest)); 24 | 25 | tokenRequest.IsRetry = true; 26 | Assert.IsTrue(tokenProvider.CanGetToken(tokenRequest)); 27 | } 28 | 29 | [TestMethod] 30 | public void MsalIntegratedWindowsAuthContractTest() 31 | { 32 | var tokenProvider = new MsalIntegratedWindowsAuthTokenProvider(appMock.Object, loggerMock.Object); 33 | var tokenRequest = new TokenRequest(); 34 | var windowsIntegratedAuthSupported = Environment.OSVersion.Platform == PlatformID.Win32NT; 35 | 36 | Assert.IsNotNull(tokenProvider.Name); 37 | Assert.IsFalse(tokenProvider.IsInteractive); 38 | 39 | tokenRequest.IsWindowsIntegratedAuthEnabled = false; 40 | Assert.IsFalse(tokenProvider.CanGetToken(tokenRequest)); 41 | 42 | tokenRequest.IsWindowsIntegratedAuthEnabled = true; 43 | Assert.AreEqual(windowsIntegratedAuthSupported, tokenProvider.CanGetToken(tokenRequest)); 44 | } 45 | 46 | [TestMethod] 47 | public void MsalInteractiveContractTest() 48 | { 49 | var tokenProvider = new MsalInteractiveTokenProvider(appMock.Object, loggerMock.Object); 50 | var tokenRequest = new TokenRequest(); 51 | 52 | Assert.IsNotNull(tokenProvider.Name); 53 | Assert.IsTrue(tokenProvider.IsInteractive); 54 | 55 | tokenRequest.IsInteractive = false; 56 | tokenRequest.CanShowDialog = false; 57 | Assert.IsFalse(tokenProvider.CanGetToken(tokenRequest)); 58 | 59 | tokenRequest.IsInteractive = false; 60 | tokenRequest.CanShowDialog = true; 61 | Assert.IsFalse(tokenProvider.CanGetToken(tokenRequest)); 62 | 63 | tokenRequest.IsInteractive = true; 64 | tokenRequest.CanShowDialog = false; 65 | Assert.IsFalse(tokenProvider.CanGetToken(tokenRequest)); 66 | 67 | tokenRequest.IsInteractive = true; 68 | tokenRequest.CanShowDialog = true; 69 | Assert.IsTrue(tokenProvider.CanGetToken(tokenRequest)); 70 | 71 | // non-interactive variants for those who don't not avoid framework guidelines 72 | tokenRequest.IsNonInteractive = false; 73 | tokenRequest.CanShowDialog = true; 74 | Assert.IsTrue(tokenProvider.CanGetToken(tokenRequest)); 75 | 76 | tokenRequest.IsNonInteractive = true; 77 | tokenRequest.CanShowDialog = true; 78 | Assert.IsFalse(tokenProvider.CanGetToken(tokenRequest)); 79 | } 80 | 81 | [TestMethod] 82 | public void MsalDeviceCodeFlowContractTest() 83 | { 84 | var tokenProvider = new MsalDeviceCodeTokenProvider(appMock.Object, loggerMock.Object); 85 | var tokenRequest = new TokenRequest(); 86 | 87 | Assert.IsNotNull(tokenProvider.Name); 88 | Assert.IsTrue(tokenProvider.IsInteractive); 89 | 90 | tokenRequest.IsInteractive = false; 91 | Assert.IsFalse(tokenProvider.CanGetToken(tokenRequest)); 92 | 93 | tokenRequest.IsInteractive = true; 94 | Assert.IsTrue(tokenProvider.CanGetToken(tokenRequest)); 95 | } 96 | 97 | [TestMethod] 98 | public void MsalServicePrincipalContractTest() 99 | { 100 | appMock.Setup(x => x.AppConfig) 101 | .Returns(new Mock().Object); 102 | 103 | var tokenProvider = new MsalServicePrincipalTokenProvider(appMock.Object, loggerMock.Object); 104 | var tokenRequest = new TokenRequest(); 105 | 106 | Assert.IsNotNull(tokenProvider.Name); 107 | Assert.IsFalse(tokenProvider.IsInteractive); 108 | 109 | tokenRequest.IsInteractive = true; 110 | Assert.IsFalse(tokenProvider.CanGetToken(tokenRequest)); 111 | 112 | tokenRequest.IsInteractive = false; 113 | tokenRequest.ClientId = "clientId"; 114 | tokenRequest.ClientCertificate = Mock.Of(); 115 | Assert.IsTrue(tokenProvider.CanGetToken(tokenRequest)); 116 | 117 | tokenRequest.IsInteractive = false; 118 | tokenRequest.ClientId = null; 119 | tokenRequest.ClientCertificate = Mock.Of(); 120 | Assert.IsFalse(tokenProvider.CanGetToken(tokenRequest)); 121 | 122 | tokenRequest.IsInteractive = false; 123 | tokenRequest.ClientId = "clientId"; 124 | tokenRequest.ClientCertificate = null; 125 | Assert.IsFalse(tokenProvider.CanGetToken(tokenRequest)); 126 | } 127 | 128 | 129 | [TestMethod] 130 | public void MsalManagedIdentityContractTest() 131 | { 132 | appMock.Setup(x => x.AppConfig) 133 | .Returns(new Mock().Object); 134 | 135 | var tokenProvider = new MsalManagedIdentityTokenProvider(appMock.Object, loggerMock.Object); 136 | var tokenRequest = new TokenRequest(); 137 | 138 | Assert.IsNotNull(tokenProvider.Name); 139 | Assert.IsFalse(tokenProvider.IsInteractive); 140 | 141 | tokenRequest.IsInteractive = true; 142 | Assert.IsFalse(tokenProvider.CanGetToken(tokenRequest)); 143 | 144 | tokenRequest.IsInteractive = false; 145 | tokenRequest.ClientId = "clientId"; 146 | Assert.IsTrue(tokenProvider.CanGetToken(tokenRequest)); 147 | 148 | tokenRequest.IsInteractive = false; 149 | tokenRequest.ClientId = null; 150 | Assert.IsFalse(tokenProvider.CanGetToken(tokenRequest)); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Authentication.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | global using System.Security.Cryptography.X509Certificates; 6 | global using Microsoft.Extensions.Logging; 7 | global using Microsoft.Identity.Client; 8 | global using Microsoft.Identity.Client.Extensions.Msal; 9 | global using Microsoft.VisualStudio.TestTools.UnitTesting; 10 | global using Moq; 11 | -------------------------------------------------------------------------------- /src/Authentication/AzureArtifacts.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System.Runtime.InteropServices; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Identity.Client; 8 | using Microsoft.Identity.Client.Broker; 9 | 10 | namespace Microsoft.Artifacts.Authentication; 11 | 12 | public static class AzureArtifacts 13 | { 14 | /// 15 | /// Azure Artifacts application ID. 16 | /// 17 | public const string ClientId = "d5a56ea4-7369-46b8-a538-c370805301bf"; 18 | 19 | /// 20 | /// Visual Studio application ID. 21 | /// 22 | private const string LegacyClientId = "872cd9fa-d31f-45e0-9eab-6e460a02d1f1"; 23 | 24 | public static PublicClientApplicationBuilder CreateDefaultBuilder(Uri authority) 25 | { 26 | // Azure Artifacts is not yet present in PPE, so revert to the old app in that case 27 | bool prod = !authority.Host.Equals("login.windows-ppe.net", StringComparison.OrdinalIgnoreCase); 28 | 29 | var builder = PublicClientApplicationBuilder.Create(prod ? AzureArtifacts.ClientId : AzureArtifacts.LegacyClientId) 30 | .WithAuthority(authority) 31 | .WithRedirectUri("http://localhost"); 32 | 33 | return builder; 34 | } 35 | 36 | public static PublicClientApplicationBuilder WithBroker(this PublicClientApplicationBuilder builder, bool enableBroker, ILogger logger) 37 | { 38 | // Eventually will be rolled into CreateDefaultBuilder as using the brokers is desirable 39 | if (!enableBroker || !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 40 | { 41 | return builder; 42 | } 43 | 44 | logger.LogTrace(Resources.MsalUsingWamBroker); 45 | 46 | return builder 47 | .WithBroker( 48 | new BrokerOptions(BrokerOptions.OperatingSystems.Windows) 49 | { 50 | Title = "Azure DevOps Artifacts", 51 | ListOperatingSystemAccounts = true, 52 | MsaPassthrough = true 53 | }) 54 | .WithParentActivityOrWindow(() => GetConsoleOrTerminalWindow()); 55 | } 56 | 57 | public static PublicClientApplicationBuilder WithHttpClient(this PublicClientApplicationBuilder builder, HttpClient? httpClient = null) 58 | { 59 | // Default HttpClient is only meant for .NET Framework clients that can't use the SocketsHttpHandler 60 | return builder.WithHttpClientFactory(new MsalHttpClientFactory(httpClient ?? new HttpClient(new HttpClientHandler 61 | { 62 | // Important for IWA 63 | UseDefaultCredentials = true 64 | }))); 65 | } 66 | 67 | #region https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/3590 68 | enum GetAncestorFlags 69 | { 70 | GetParent = 1, 71 | GetRoot = 2, 72 | /// 73 | /// Retrieves the owned root window by walking the chain of parent and owner windows returned by GetParent. 74 | /// 75 | GetRootOwner = 3 76 | } 77 | 78 | /// 79 | /// Retrieves the handle to the ancestor of the specified window. 80 | /// 81 | /// A handle to the window whose ancestor is to be retrieved. 82 | /// If this parameter is the desktop window, the function returns NULL. 83 | /// The ancestor to be retrieved. 84 | /// The return value is the handle to the ancestor window. 85 | [DllImport("user32.dll", ExactSpelling = true)] 86 | static extern IntPtr GetAncestor(IntPtr hwnd, GetAncestorFlags flags); 87 | 88 | [DllImport("kernel32.dll")] 89 | static extern IntPtr GetConsoleWindow(); 90 | 91 | private static IntPtr GetConsoleOrTerminalWindow() 92 | { 93 | IntPtr consoleHandle = GetConsoleWindow(); 94 | IntPtr handle = GetAncestor(consoleHandle, GetAncestorFlags.GetRootOwner); 95 | 96 | return handle; 97 | } 98 | #endregion 99 | } 100 | -------------------------------------------------------------------------------- /src/Authentication/ITokenProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using Microsoft.Identity.Client; 6 | 7 | namespace Microsoft.Artifacts.Authentication; 8 | 9 | public interface ITokenProvider 10 | { 11 | string Name { get; } 12 | 13 | bool IsInteractive { get; } 14 | 15 | bool CanGetToken(TokenRequest tokenRequest); 16 | 17 | Task GetTokenAsync(TokenRequest tokenRequest, CancellationToken cancellationToken = default); 18 | } 19 | -------------------------------------------------------------------------------- /src/Authentication/ITokenProvidersFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | namespace Microsoft.Artifacts.Authentication; 6 | 7 | public interface ITokenProvidersFactory 8 | { 9 | Task> GetAsync(Uri authority); 10 | } 11 | -------------------------------------------------------------------------------- /src/Authentication/Microsoft.Artifacts.Authentication.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Microsoft.Artifacts.Authentication 5 | Microsoft.Artifacts.Authentication 6 | netstandard2.0 7 | latest 8 | enable 9 | enable 10 | 0.2.3 11 | Microsoft 12 | Microsoft 13 | Azure Artifacts authentication library for credential providers. 14 | © Microsoft Corporation. All rights reserved. 15 | artifacts-icon.png 16 | README.md 17 | https://github.com/microsoft/artifacts-credprovider 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/Authentication/MsalCache.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System.Runtime.InteropServices; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Identity.Client.Extensions.Msal; 8 | 9 | namespace Microsoft.Artifacts.Authentication; 10 | 11 | public static class MsalCache 12 | { 13 | private static readonly string LocalAppDataLocation = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.Create); 14 | 15 | // from https://github.com/GitCredentialManager/git-credential-manager/blob/df90676d1249759eef8cec57155c27e869503225/src/shared/Microsoft.Git.CredentialManager/Authentication/MicrosoftAuthentication.cs#L277 16 | // The Visual Studio MSAL cache is located at "%LocalAppData%\.IdentityService\msal.cache" on Windows. 17 | // We use the MSAL extension library to provide us consistent cache file access semantics (synchronization, etc) 18 | // as Visual Studio itself follows, as well as other Microsoft developer tools such as the Azure PowerShell CLI. 19 | public static string DefaultMsalCacheLocation 20 | { 21 | get 22 | { 23 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 24 | { 25 | // The shared MSAL cache is located at "%LocalAppData%\.IdentityService\msal.cache" on Windows. 26 | return Path.Combine(LocalAppDataLocation, ".IdentityService", "msal.cache"); 27 | } 28 | else 29 | { 30 | // The shared MSAL cache metadata is located at "~/.local/.IdentityService/msal.cache" on UNIX. 31 | return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", ".IdentityService", "msal.cache"); 32 | } 33 | } 34 | } 35 | 36 | public static async Task GetMsalCacheHelperAsync(string cacheLocation, ILogger logger) 37 | { 38 | MsalCacheHelper? helper = null; 39 | 40 | logger.LogTrace(Resources.MsalCacheLocation, cacheLocation); 41 | 42 | var fileName = Path.GetFileName(cacheLocation); 43 | var directory = Path.GetDirectoryName(cacheLocation); 44 | 45 | // Copied from GCM https://github.com/GitCredentialManager/git-credential-manager/blob/bdc20d91d325d66647f2837ffb4e2b2fe98d7e70/src/shared/Core/Authentication/MicrosoftAuthentication.cs#L371-L407 46 | try 47 | { 48 | var storageProps = CreateTokenCacheProperties(useLinuxFallback: false); 49 | 50 | helper = await MsalCacheHelper.CreateAsync(storageProps); 51 | 52 | helper.VerifyPersistence(); 53 | } 54 | catch (MsalCachePersistenceException ex) 55 | { 56 | logger.LogWarning(Resources.MsalCachePersistenceWarning); 57 | logger.LogTrace(ex.ToString()); 58 | 59 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 60 | { 61 | // On macOS sometimes the Keychain returns the "errSecAuthFailed" error - we don't know why 62 | // but it appears to be something to do with not being able to access the keychain. 63 | // Locking and unlocking (or restarting) often fixes this. 64 | logger.LogError(Resources.MsalCachePersistenceMacOsWarning); 65 | } 66 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 67 | { 68 | // On Linux the SecretService/keyring might not be available so we must fall-back to a plaintext file. 69 | logger.LogWarning(Resources.MsalCacheUnprotectedFileWarning); 70 | 71 | var storageProps = CreateTokenCacheProperties(useLinuxFallback: true); 72 | helper = await MsalCacheHelper.CreateAsync(storageProps); 73 | } 74 | } 75 | 76 | StorageCreationProperties CreateTokenCacheProperties(bool useLinuxFallback) 77 | { 78 | var builder = new StorageCreationPropertiesBuilder(fileName, directory) 79 | .WithMacKeyChain("Microsoft.Developer.IdentityService", "MSALCache"); 80 | 81 | if (useLinuxFallback) 82 | { 83 | builder.WithLinuxUnprotectedFile(); 84 | } 85 | else 86 | { 87 | // The SecretService/keyring is used on Linux with the following collection name and attributes 88 | builder.WithLinuxKeyring(fileName, 89 | "default", "MSALCache", 90 | new KeyValuePair("MsalClientID", "Microsoft.Developer.IdentityService"), 91 | new KeyValuePair("Microsoft.Developer.IdentityService", "1.0.0.0")); 92 | } 93 | 94 | return builder.Build(); 95 | } 96 | 97 | return helper!; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Authentication/MsalConstants.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | namespace Microsoft.Artifacts.Authentication; 6 | 7 | public static class MsalConstants 8 | { 9 | public const string AzureDevOpsResource = "499b84ac-1321-427f-aa17-267ca6975798/.default"; 10 | public static readonly IEnumerable AzureDevOpsScopes = Array.AsReadOnly(new[] { AzureDevOpsResource }); 11 | 12 | public static readonly Guid FirstPartyTenant = Guid.Parse("f8cdef31-a31e-4b4a-93e4-5f571e91255a"); 13 | public static readonly Guid MsaAccountTenant = Guid.Parse("9188040d-6c67-4c5b-b112-36a304b66dad"); 14 | } 15 | -------------------------------------------------------------------------------- /src/Authentication/MsalDeviceCodeTokenProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Identity.Client; 7 | 8 | namespace Microsoft.Artifacts.Authentication; 9 | 10 | public class MsalDeviceCodeTokenProvider : ITokenProvider 11 | { 12 | private readonly IPublicClientApplication app; 13 | private readonly ILogger logger; 14 | 15 | public MsalDeviceCodeTokenProvider(IPublicClientApplication app, ILogger logger) 16 | { 17 | this.app = app ?? throw new ArgumentNullException(nameof(app)); 18 | this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); 19 | } 20 | 21 | public string Name => "MSAL Device Code"; 22 | 23 | public bool IsInteractive => true; 24 | 25 | public bool CanGetToken(TokenRequest tokenRequest) 26 | { 27 | return tokenRequest.IsInteractive; 28 | } 29 | 30 | public async Task GetTokenAsync(TokenRequest tokenRequest, CancellationToken cancellationToken = default) 31 | { 32 | using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); 33 | cts.CancelAfter(tokenRequest.InteractiveTimeout); 34 | 35 | try 36 | { 37 | var result = await app.AcquireTokenWithDeviceCode(MsalConstants.AzureDevOpsScopes, tokenRequest.DeviceCodeResultCallback ?? ((DeviceCodeResult deviceCodeResult) => 38 | { 39 | logger.LogInformation(deviceCodeResult.Message); 40 | 41 | return Task.CompletedTask; 42 | })) 43 | .ExecuteAsync(cts.Token); 44 | 45 | return result; 46 | } 47 | catch (OperationCanceledException ex) when (cts.IsCancellationRequested) 48 | { 49 | logger.LogWarning(ex.Message); 50 | return null; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Authentication/MsalExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using Microsoft.Identity.Client; 6 | 7 | namespace Microsoft.Artifacts.Authentication; 8 | 9 | public static partial class MsalExtensions 10 | { 11 | public static List<(IAccount Account, string CanonicalName)> GetApplicableAccounts(IEnumerable accounts, Guid authorityTenantId, string? loginHint) 12 | { 13 | var applicableAccounts = new List<(IAccount, string)>(); 14 | 15 | foreach (var account in accounts) 16 | { 17 | string canonicalName = $"{account.HomeAccountId?.TenantId}\\{account.Username}"; 18 | 19 | // If a login hint is provided and matches, try that first 20 | if (!string.IsNullOrEmpty(loginHint) && account.Username == loginHint) 21 | { 22 | applicableAccounts.Insert(0, (account, canonicalName)); 23 | continue; 24 | } 25 | 26 | if (Guid.TryParse(account.HomeAccountId?.TenantId, out Guid accountTenantId)) 27 | { 28 | if (accountTenantId == authorityTenantId) 29 | { 30 | applicableAccounts.Add((account, canonicalName)); 31 | } 32 | else if (accountTenantId == MsalConstants.MsaAccountTenant && (authorityTenantId == MsalConstants.FirstPartyTenant || authorityTenantId == Guid.Empty)) 33 | { 34 | applicableAccounts.Add((account, canonicalName)); 35 | } 36 | } 37 | } 38 | 39 | return applicableAccounts; 40 | } 41 | 42 | public static AcquireTokenSilentParameterBuilder WithAccountTenantId(this AcquireTokenSilentParameterBuilder builder, IAccount account) 43 | { 44 | // Workaround for https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/3077 45 | // Even if using the organizations tenant the presence of an MSA will attempt to use the consumers tenant 46 | // which is not supported by the Azure DevOps application. Detect this case and use the first party tenant. 47 | 48 | if (Guid.TryParse(account.HomeAccountId?.TenantId, out Guid accountTenantId) && accountTenantId == MsalConstants.MsaAccountTenant) 49 | { 50 | builder = builder.WithTenantId(MsalConstants.FirstPartyTenant.ToString()); 51 | } 52 | 53 | return builder; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Authentication/MsalHttpClientFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System.Net.Http.Headers; 6 | using Microsoft.Identity.Client; 7 | 8 | namespace Microsoft.Artifacts.Authentication; 9 | 10 | public class MsalHttpClientFactory : IMsalHttpClientFactory 11 | { 12 | private readonly HttpClient httpClient; 13 | 14 | public MsalHttpClientFactory(HttpClient httpClient) 15 | { 16 | this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); 17 | 18 | var userAgent = this.httpClient.DefaultRequestHeaders.UserAgent; 19 | userAgent.Add(ProgramProduct); 20 | userAgent.Add(ProgramComment); 21 | userAgent.Add(ClrProduct); 22 | userAgent.Add(ClrComment); 23 | } 24 | 25 | public static ProductInfoHeaderValue ProgramProduct => 26 | new ProductInfoHeaderValue(PlatformInformation.GetProgramName(), PlatformInformation.GetProgramVersion()); 27 | 28 | public static ProductInfoHeaderValue ProgramComment => 29 | new ProductInfoHeaderValue($"({PlatformInformation.GetOSType()}; {PlatformInformation.GetCpuArchitecture()}; {PlatformInformation.GetOsDescription()})"); 30 | 31 | public static ProductInfoHeaderValue ClrProduct => 32 | new ProductInfoHeaderValue("CLR", PlatformInformation.GetClrVersion()); 33 | 34 | public static ProductInfoHeaderValue ClrComment => 35 | new ProductInfoHeaderValue($"({PlatformInformation.GetClrFramework()}; {PlatformInformation.GetClrRuntime()}; {PlatformInformation.GetClrDescription()})"); 36 | 37 | // Produces a value similar to the following: 38 | // CredentialProvider.Microsoft/1.0.4+aae4981de95d543b7935811c05474e393dd9e144 (Windows; X64; Microsoft Windows 10.0.19045) CLR/6.0.16 (.NETCoreApp,Version=v6.0; win10-x64; .NET 6.0.16) 39 | public static IEnumerable UserAgent => 40 | Array.AsReadOnly(new[] 41 | { 42 | ProgramProduct, 43 | ProgramComment, 44 | ClrProduct, 45 | ClrComment 46 | }); 47 | 48 | public HttpClient GetHttpClient() 49 | { 50 | return httpClient; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Authentication/MsalIntegratedWindowsAuthTokenProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System.Runtime.InteropServices; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Identity.Client; 8 | 9 | namespace Microsoft.Artifacts.Authentication; 10 | 11 | public class MsalIntegratedWindowsAuthTokenProvider : ITokenProvider 12 | { 13 | private readonly IPublicClientApplication app; 14 | private readonly ILogger logger; 15 | 16 | public MsalIntegratedWindowsAuthTokenProvider(IPublicClientApplication app, ILogger logger) 17 | { 18 | this.app = app ?? throw new ArgumentNullException(nameof(app)); 19 | this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); 20 | } 21 | 22 | public string Name => "MSAL Windows Integrated Authentication"; 23 | 24 | public bool IsInteractive => false; 25 | 26 | public bool CanGetToken(TokenRequest tokenRequest) 27 | { 28 | return WindowsIntegratedAuth.IsSupported() && tokenRequest.IsWindowsIntegratedAuthEnabled; 29 | } 30 | 31 | public async Task GetTokenAsync(TokenRequest tokenRequest, CancellationToken cancellationToken = default) 32 | { 33 | try 34 | { 35 | string? upn = WindowsIntegratedAuth.GetUserPrincipalName(); 36 | if (upn == null) 37 | { 38 | logger.LogTrace(Resources.MsalUserPrincipalNameError, Marshal.GetLastWin32Error()); 39 | return null; 40 | } 41 | 42 | var result = await app.AcquireTokenByIntegratedWindowsAuth(MsalConstants.AzureDevOpsScopes) 43 | .WithUsername(upn) 44 | .ExecuteAsync(cancellationToken); 45 | 46 | return result; 47 | } 48 | catch (MsalClientException ex) when (ex.ErrorCode is MsalError.WsTrustEndpointNotFoundInMetadataDocument or MsalError.IntegratedWindowsAuthNotSupportedForManagedUser) 49 | { 50 | logger.LogTrace(ex.Message); 51 | return null; 52 | } 53 | catch (MsalUiRequiredException ex) 54 | { 55 | logger.LogTrace(ex.Message); 56 | return null; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Authentication/MsalInteractiveTokenProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Identity.Client; 7 | 8 | namespace Microsoft.Artifacts.Authentication; 9 | 10 | public class MsalInteractiveTokenProvider : ITokenProvider 11 | { 12 | private readonly IPublicClientApplication app; 13 | private readonly ILogger logger; 14 | 15 | public MsalInteractiveTokenProvider(IPublicClientApplication app, ILogger logger) 16 | { 17 | this.app = app ?? throw new ArgumentNullException(nameof(app)); 18 | this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); 19 | } 20 | 21 | public string Name => "MSAL Interactive"; 22 | 23 | public bool IsInteractive => true; 24 | 25 | public bool CanGetToken(TokenRequest tokenRequest) 26 | { 27 | // MSAL will use the system browser, this will work on all OS's 28 | return tokenRequest.IsInteractive && tokenRequest.CanShowDialog; 29 | } 30 | 31 | public async Task GetTokenAsync(TokenRequest tokenRequest, CancellationToken cancellationToken = default) 32 | { 33 | if (!app.IsUserInteractive()) 34 | { 35 | logger.LogTrace(Resources.MsalNotUserInteractive); 36 | return null; 37 | } 38 | 39 | using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); 40 | cts.CancelAfter(tokenRequest.InteractiveTimeout); 41 | 42 | try 43 | { 44 | logger.LogInformation(Resources.MsalInteractivePrompt); 45 | var result = await app.AcquireTokenInteractive(MsalConstants.AzureDevOpsScopes) 46 | .WithPrompt(Prompt.SelectAccount) 47 | .WithUseEmbeddedWebView(false) 48 | .ExecuteAsync(cts.Token); 49 | 50 | return result; 51 | } 52 | catch (MsalClientException ex) when (ex.ErrorCode == MsalError.AuthenticationCanceledError) 53 | { 54 | logger.LogWarning(ex.Message); 55 | return null; 56 | } 57 | catch (OperationCanceledException ex) when (cts.IsCancellationRequested) 58 | { 59 | logger.LogWarning(ex.Message); 60 | return null; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Authentication/MsalManagedIdentityTokenProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Microsoft.Identity.Client; 3 | using Microsoft.Identity.Client.AppConfig; 4 | 5 | namespace Microsoft.Artifacts.Authentication; 6 | 7 | public class MsalManagedIdentityTokenProvider : ITokenProvider 8 | { 9 | private readonly ILogger logger; 10 | private readonly IAppConfig appConfig; 11 | 12 | public MsalManagedIdentityTokenProvider(IPublicClientApplication app, ILogger logger) 13 | { 14 | this.appConfig = app.AppConfig; 15 | this.logger = logger; 16 | } 17 | 18 | public string Name => "MSAL Managed Identity"; 19 | 20 | public bool IsInteractive => false; 21 | 22 | public bool CanGetToken(TokenRequest tokenRequest) => 23 | !string.IsNullOrWhiteSpace(tokenRequest.ClientId); 24 | 25 | public async Task GetTokenAsync(TokenRequest tokenRequest, CancellationToken cancellationToken = default) 26 | { 27 | try 28 | { 29 | if (string.IsNullOrWhiteSpace(tokenRequest.ClientId)) 30 | { 31 | logger.LogTrace(string.Format(Resources.MsalClientIdError, tokenRequest.ClientId)); 32 | return null; 33 | } 34 | 35 | IManagedIdentityApplication app = ManagedIdentityApplicationBuilder.Create(CreateManagedIdentityId(tokenRequest.ClientId!)) 36 | .WithHttpClientFactory(appConfig.HttpClientFactory) 37 | .WithLogging(appConfig.LoggingCallback, appConfig.LogLevel, appConfig.EnablePiiLogging, appConfig.IsDefaultPlatformLoggingEnabled) 38 | .Build(); 39 | 40 | AuthenticationResult result = await app.AcquireTokenForManagedIdentity(MsalConstants.AzureDevOpsResource) 41 | .ExecuteAsync() 42 | .ConfigureAwait(false); 43 | 44 | return result; 45 | } 46 | catch (MsalServiceException ex) when (ex.ErrorCode is MsalError.ManagedIdentityRequestFailed) 47 | { 48 | logger.LogTrace(ex.Message); 49 | return null; 50 | } 51 | catch (MsalServiceException ex) when (ex.ErrorCode is MsalError.ManagedIdentityUnreachableNetwork) 52 | { 53 | logger.LogTrace(ex.Message); 54 | return null; 55 | } 56 | } 57 | 58 | private ManagedIdentityId CreateManagedIdentityId(string clientId) 59 | { 60 | return Guid.TryParse(clientId, out var id) 61 | ? ManagedIdentityId.WithUserAssignedClientId(id.ToString()) 62 | : ManagedIdentityId.SystemAssigned; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Authentication/MsalServicePrincipalTokenProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Microsoft.Identity.Client; 3 | 4 | namespace Microsoft.Artifacts.Authentication 5 | { 6 | public class MsalServicePrincipalTokenProvider : ITokenProvider 7 | { 8 | public string Name => "MSAL Service Principal"; 9 | 10 | public bool IsInteractive => false; 11 | 12 | private readonly ILogger logger; 13 | private readonly IAppConfig appConfig; 14 | 15 | public MsalServicePrincipalTokenProvider(IPublicClientApplication app, ILogger logger) 16 | { 17 | this.appConfig = app.AppConfig; 18 | this.logger = logger; 19 | } 20 | 21 | public bool CanGetToken(TokenRequest tokenRequest) 22 | { 23 | return !string.IsNullOrWhiteSpace(tokenRequest.ClientId) 24 | && tokenRequest.ClientCertificate != null; 25 | } 26 | 27 | public async Task GetTokenAsync(TokenRequest tokenRequest, CancellationToken cancellationToken = default) 28 | { 29 | try 30 | { 31 | if (!CanGetToken(tokenRequest)) 32 | { 33 | logger.LogTrace("InvalidInputs"); 34 | return null; 35 | } 36 | 37 | var app = ConfidentialClientApplicationBuilder.Create(tokenRequest.ClientId) 38 | .WithHttpClientFactory(appConfig.HttpClientFactory) 39 | .WithLogging(appConfig.LoggingCallback, appConfig.LogLevel, appConfig.EnablePiiLogging, appConfig.IsDefaultPlatformLoggingEnabled) 40 | .WithCertificate(tokenRequest.ClientCertificate, sendX5C: true) 41 | .WithTenantId(tokenRequest.TenantId) 42 | .Build(); 43 | 44 | var result = await app.AcquireTokenForClient(MsalConstants.AzureDevOpsScopes) 45 | .ExecuteAsync() 46 | .ConfigureAwait(false); 47 | 48 | return result; 49 | } 50 | catch (Exception ex) 51 | { 52 | logger.LogTrace(ex.Message); 53 | return null; 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Authentication/MsalSilentTokenProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System.Runtime.InteropServices; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Identity.Client; 8 | 9 | namespace Microsoft.Artifacts.Authentication; 10 | 11 | public class MsalSilentTokenProvider : ITokenProvider 12 | { 13 | private readonly IPublicClientApplication app; 14 | private readonly ILogger logger; 15 | 16 | public MsalSilentTokenProvider(IPublicClientApplication app, ILogger logger) 17 | { 18 | this.app = app ?? throw new ArgumentNullException(nameof(app)); 19 | this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); 20 | } 21 | 22 | public string Name => "MSAL Silent"; 23 | 24 | public bool IsInteractive => false; 25 | 26 | public bool CanGetToken(TokenRequest tokenRequest) 27 | { 28 | // Always run this and rely on MSAL to return a valid token. Previously, AcquireTokenByIntegratedWindowsAuth 29 | // would return cached tokens based on the user principal name, so this token provider could be skipped. Now, 30 | // cached and broker accounts and any cached tokens are returned via AcquireTokenSilent. MSAL will ensure any 31 | // returned token is refreshed as needed to be valid upon return. 32 | return true; 33 | } 34 | 35 | public async Task GetTokenAsync(TokenRequest tokenRequest, CancellationToken cancellationToken = default) 36 | { 37 | var accounts = await app.GetAccountsAsync(); 38 | 39 | foreach (var account in accounts) 40 | { 41 | this.logger.LogTrace(Resources.MsalAccountInCache, $"{account.HomeAccountId?.TenantId}\\{account.Username}"); 42 | } 43 | 44 | var authority = new Uri(app.Authority); 45 | 46 | if (!Guid.TryParse(authority.AbsolutePath.Trim('/'), out Guid authorityTenantId)) 47 | { 48 | this.logger.LogTrace(Resources.MsalNoAuthorityTenant, authority); 49 | } 50 | 51 | var applicableAccounts = MsalExtensions.GetApplicableAccounts(accounts, authorityTenantId, tokenRequest.LoginHint); 52 | 53 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && app.AppConfig.IsBrokerEnabled) 54 | { 55 | applicableAccounts.Add((PublicClientApplication.OperatingSystemAccount, PublicClientApplication.OperatingSystemAccount.HomeAccountId.Identifier)); 56 | } 57 | 58 | foreach ((IAccount account, string canonicalName) in applicableAccounts) 59 | { 60 | try 61 | { 62 | this.logger.LogTrace(Resources.MsalAccountAttempt, canonicalName); 63 | 64 | var result = await app.AcquireTokenSilent(MsalConstants.AzureDevOpsScopes, account) 65 | .WithAccountTenantId(account) 66 | .ExecuteAsync(cancellationToken); 67 | 68 | return result; 69 | } 70 | catch (MsalUiRequiredException ex) 71 | { 72 | this.logger.LogTrace(ex.Message); 73 | } 74 | catch (MsalServiceException ex) 75 | { 76 | this.logger.LogWarning(ex.Message); 77 | } 78 | } 79 | 80 | return null; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Authentication/MsalTokenProviders.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Identity.Client; 7 | 8 | namespace Microsoft.Artifacts.Authentication; 9 | 10 | public class MsalTokenProviders 11 | { 12 | public static IEnumerable Get(IPublicClientApplication app, ILogger logger) 13 | { 14 | yield return new MsalServicePrincipalTokenProvider(app, logger); 15 | yield return new MsalManagedIdentityTokenProvider(app, logger); 16 | 17 | // TODO: Would be more useful if MsalSilentTokenProvider enumerated over each account from the outside 18 | yield return new MsalSilentTokenProvider(app, logger); 19 | 20 | if (WindowsIntegratedAuth.IsSupported()) 21 | { 22 | yield return new MsalIntegratedWindowsAuthTokenProvider(app, logger); 23 | } 24 | 25 | yield return new MsalInteractiveTokenProvider(app, logger); 26 | yield return new MsalDeviceCodeTokenProvider(app, logger); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Authentication/PlatformInformation.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System.Reflection; 6 | using System.Runtime.InteropServices; 7 | 8 | namespace Microsoft.Artifacts.Authentication; 9 | 10 | public static class PlatformInformation 11 | { 12 | private static string? programName = null; 13 | private static string? programVersion = null; 14 | private static string? runtimeIdentifier = null; 15 | 16 | private static AssemblyName CurrentAssemblyName => typeof(PlatformInformation).Assembly.GetName(); 17 | 18 | public static string GetProgramName() 19 | { 20 | return programName ??= Assembly 21 | .GetEntryAssembly()? 22 | .GetCustomAttribute()?.Title ?? CurrentAssemblyName.Name; 23 | } 24 | 25 | public static string GetProgramVersion() 26 | { 27 | return programVersion ??= Assembly 28 | .GetEntryAssembly()? 29 | .GetCustomAttribute()?.InformationalVersion ?? CurrentAssemblyName.Version.ToString(); 30 | } 31 | 32 | public static string GetOSType() 33 | { 34 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 35 | { 36 | return nameof(OSPlatform.Windows); 37 | } 38 | 39 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 40 | { 41 | return nameof(OSPlatform.Linux); 42 | } 43 | 44 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 45 | { 46 | return nameof(OSPlatform.OSX); 47 | } 48 | 49 | return "Unknown"; 50 | } 51 | 52 | public static string GetCpuArchitecture() 53 | { 54 | return RuntimeInformation.OSArchitecture.ToString(); 55 | } 56 | 57 | public static string GetOsDescription() 58 | { 59 | return RuntimeInformation.OSDescription; 60 | } 61 | 62 | public static string GetClrVersion() 63 | { 64 | return Environment.Version.ToString(); 65 | } 66 | 67 | public static string GetClrFramework() 68 | { 69 | return AppContext.TargetFrameworkName; 70 | } 71 | 72 | public static string GetClrRuntime() 73 | { 74 | // RuntimeInformation.RuntimeIdentifier not available on .NET Standard 75 | return runtimeIdentifier ??= AppContext.GetData("RUNTIME_IDENTIFIER") as string ?? "win-x64"; 76 | } 77 | 78 | public static string GetClrDescription() 79 | { 80 | return RuntimeInformation.FrameworkDescription; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Authentication/README.md: -------------------------------------------------------------------------------- 1 | ## Azure Artifacts authentication library for credential providers 2 | 3 | The Azure Artifacts credential provider authentication library provides extension methods and defaults for using the Microsoft Authentication Library (MSAL) for Azure Artifacts. 4 | 5 | ## Examples 6 | 7 | ### Basic usage 8 | 9 | Example usage to create a PublicClientApplication with recommended settings and defaults for Azure Artifacts and enumerate providers: 10 | 11 | ```csharp 12 | var app = AzureArtifacts.CreateDefaultBuilder(authority) 13 | .WithBroker(true, logger) 14 | .WithLogging((LogLevel level, string message, bool containsPii) => 15 | { 16 | // Application specific logging 17 | }) 18 | .Build(); 19 | 20 | // Can use MsalTokenProviders which works for most cases, or compose the token providers manually 21 | var providers = MsalTokenProviders.Get(app, logger); 22 | 23 | var tokenRequest = new TokenRequest 24 | { 25 | IsInteractive = true 26 | }; 27 | 28 | foreach (var provider in providers) 29 | { 30 | if (!provider.CanGetToken(tokenRequest)) 31 | continue; 32 | 33 | var result = await provider.GetTokenAsync(tokenRequest); 34 | } 35 | ``` 36 | 37 | ### Token cache 38 | 39 | The MSAL cache must be initialized for the WAM Broker to return cached accounts, and for non-Windows the token cache ensures users are not prompted when the session token expires. 40 | 41 | ```csharp 42 | var cacheLocation = MsalCache.DefaultMsalCacheLocation; 43 | var cache = await MsalCache.GetMsalCacheHelperAsync(cacheLocation, logger); 44 | 45 | var app = AzureArtifacts.CreateDefaultBuilder(authority, logger).Build(); 46 | 47 | cache.RegisterCache(app.UserTokenCache); 48 | ``` 49 | -------------------------------------------------------------------------------- /src/Authentication/TokenRequest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System.Security.Cryptography.X509Certificates; 6 | using Microsoft.Identity.Client; 7 | 8 | namespace Microsoft.Artifacts.Authentication; 9 | 10 | public class TokenRequest 11 | { 12 | [Obsolete($"The uri parameter is unused and unnecessary. Use the parameterless constructor instead.")] 13 | public TokenRequest(Uri uri) 14 | { 15 | } 16 | 17 | public TokenRequest() 18 | { 19 | } 20 | 21 | public bool IsRetry { get; set; } 22 | 23 | public bool IsInteractive { get; set; } 24 | 25 | // Provided for back-compat to make migration easier 26 | public bool IsNonInteractive { get => !IsInteractive; set => IsInteractive = !value; } 27 | 28 | public bool CanShowDialog { get; set; } = true; 29 | 30 | public bool IsWindowsIntegratedAuthEnabled { get; set; } = true; 31 | 32 | public string? LoginHint { get; set; } = null; 33 | 34 | public TimeSpan InteractiveTimeout { get; set; } = TimeSpan.FromMinutes(2); 35 | 36 | public Func? DeviceCodeResultCallback { get; set; } = null; 37 | 38 | public string? ClientId { get; set; } = null; 39 | 40 | public string? TenantId { get; set; } = null; 41 | 42 | public X509Certificate2? ClientCertificate { get; set; } = null; 43 | } 44 | -------------------------------------------------------------------------------- /src/Authentication/WindowsIntegratedAuth.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // 3 | // Licensed under the MIT license. 4 | 5 | using System.Runtime.InteropServices; 6 | using System.Text; 7 | 8 | namespace Microsoft.Artifacts.Authentication; 9 | 10 | internal static class WindowsIntegratedAuth 11 | { 12 | // Adapted from https://github.com/AzureAD/azure-activedirectory-library-for-dotnet/blob/dev/src/Microsoft.IdentityModel.Clients.ActiveDirectory/Platforms/net45/NetDesktopPlatformProxy.cs 13 | [DllImport("secur32.dll", CharSet = CharSet.Unicode, SetLastError = true)] 14 | [return: MarshalAs(UnmanagedType.U1)] 15 | private static extern bool GetUserNameEx(int nameFormat, StringBuilder? userName, ref uint userNameSize); 16 | 17 | public static bool IsSupported() 18 | { 19 | return Environment.OSVersion.Platform == PlatformID.Win32NT; 20 | } 21 | 22 | public static string? GetUserPrincipalName() 23 | { 24 | try 25 | { 26 | const int NameUserPrincipal = 8; 27 | uint userNameSize = 0; 28 | GetUserNameEx(NameUserPrincipal, null, ref userNameSize); 29 | if (userNameSize == 0) 30 | { 31 | return null; 32 | } 33 | 34 | StringBuilder sb = new StringBuilder((int)userNameSize); 35 | if (!GetUserNameEx(NameUserPrincipal, sb, ref userNameSize)) 36 | { 37 | return null; 38 | } 39 | 40 | return sb.ToString(); 41 | } 42 | catch 43 | { 44 | return null; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Authentication/artifacts-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/artifacts-credprovider/107acd99393a8ba94be8a3280be18773c0549de4/src/Authentication/artifacts-icon.png -------------------------------------------------------------------------------- /src/artifacts-credprovider-conda/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Microsoft 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 | -------------------------------------------------------------------------------- /src/artifacts-credprovider-conda/README.md: -------------------------------------------------------------------------------- 1 | # artifacts-credprovider-conda 2 | Provide conda protocol authentication support for Azure Artifacts 3 | -------------------------------------------------------------------------------- /src/artifacts-credprovider-conda/recipe/bld.bat: -------------------------------------------------------------------------------- 1 | for %%F in (activate deactivate) DO ( 2 | if not exist %PREFIX%\etc\conda\%%F.d mkdir %PREFIX%\etc\conda\%%F.d 3 | copy %SRC_DIR%\src\%%F\*.* %PREFIX%\etc\conda\%%F.d\ 4 | ) -------------------------------------------------------------------------------- /src/artifacts-credprovider-conda/recipe/build.sh: -------------------------------------------------------------------------------- 1 | for CHANGE in "activate" "deactivate" 2 | do 3 | mkdir -p "${PREFIX}/etc/conda/${CHANGE}.d" 4 | cp "${SRC_DIR}/src/${CHANGE}/*" "${PREFIX}/etc/conda/${CHANGE}.d/" 5 | done -------------------------------------------------------------------------------- /src/artifacts-credprovider-conda/recipe/meta.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: artifacts-credprovider-conda 3 | version: 0.0.1 4 | 5 | source: 6 | path: ../src 7 | folder: src 8 | 9 | build: 10 | noarch: generic 11 | 12 | about: 13 | home: https://github.com/microsoft/artifacts-credprovider 14 | license: MIT 15 | summary: Conda credential provider wrapper to authenticate against Azure Artifacts feeds. 16 | description: | 17 | The artifacts-credprovider-conda package provides authentication to consume 18 | Conda pacakges from Azure Artifacts feed within Azure DevOps. -------------------------------------------------------------------------------- /src/artifacts-credprovider-conda/src/activate/artifacts-cred-activate.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | @for /f %%i in ('conda config --show --json ^| conda run --name base --no-capture-output %CONDA_PYTHON_EXE% %~dp0artifacts-cred.py') do set ARTIFACTS_CONDA_TOKEN=%%i -------------------------------------------------------------------------------- /src/artifacts-credprovider-conda/src/activate/artifacts-cred-activate.ps1: -------------------------------------------------------------------------------- 1 | $token = conda config --show --json | & $Env:CONDA_EXE run --name base --no-capture-output python (Join-Path $PSScriptRoot 'artifacts-cred.py') 2 | $env:ARTIFACTS_CONDA_TOKEN = $token -------------------------------------------------------------------------------- /src/artifacts-credprovider-conda/src/activate/artifacts-cred-activate.sh: -------------------------------------------------------------------------------- 1 | export ARTIFACTS_CONDA_TOKEN=$(eval conda config --show --json | eval $CONDA_PYTHON_EXE $(dirname $BASH_SOURCE)/artifacts-cred.py) -------------------------------------------------------------------------------- /src/artifacts-credprovider-conda/src/activate/artifacts-cred.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | import requests 5 | import subprocess 6 | import sys 7 | import warnings 8 | import json 9 | 10 | from subprocess import Popen 11 | 12 | if not hasattr(Popen, "__enter__"): 13 | # Handle Python 2.x not making Popen a context manager 14 | class Popen(Popen): 15 | def __enter__(self): 16 | return self 17 | 18 | def __exit__(self, ex_type, ex_value, ex_tb): 19 | pass 20 | 21 | try: 22 | from urllib.parse import urlsplit 23 | except ImportError: 24 | from urlparse import urlsplit 25 | 26 | # A wrapper similar as https://github.com/microsoft/artifacts-keyring 27 | class CredentialProvider(object): 28 | _NON_INTERACTIVE_VAR_NAME = "ARTIFACTS_CONDA_NONINTERACTIVE_MODE" 29 | 30 | def __init__(self): 31 | if sys.platform.startswith("win"): 32 | tool_path = os.path.join( 33 | os.path.abspath(os.environ['UserProfile']), 34 | ".nuget", 35 | "plugins", 36 | "netfx", 37 | "CredentialProvider.Microsoft", 38 | "CredentialProvider.Microsoft.exe", 39 | ) 40 | self.exe = [tool_path] 41 | else: 42 | try: 43 | sys_version = tuple(int(i) for i in 44 | subprocess.check_output(["dotnet", "--version"]).decode().strip().partition("-")[0].split(".")) 45 | get_runtime_path = lambda: "dotnet" 46 | except Exception as e: 47 | message = ( 48 | "Unable to find dependency dotnet, please manually install" 49 | " the .NET SDK and ensure 'dotnet' is in your PATH. Error: " 50 | ) 51 | raise Exception(message + str(e)) 52 | 53 | tool_path = os.path.join( 54 | os.path.abspath(os.environ['HOME']), 55 | ".nuget", 56 | "plugins", 57 | "netcore", 58 | "CredentialProvider.Microsoft", 59 | "CredentialProvider.Microsoft.dll", 60 | ) 61 | self.exe = [get_runtime_path(), "exec", tool_path] 62 | 63 | if not os.path.exists(tool_path): 64 | raise RuntimeError("Unable to find credential provider in the expected path: " + tool_path) 65 | 66 | 67 | def get_credentials(self, url): 68 | # Public feed short circuit: return nothing if the endpoint is public (can authenticate without credentials). 69 | if self._can_authenticate(url, None): 70 | return None, None 71 | 72 | # Getting credentials with IsRetry=false; the credentials may come from the cache 73 | username, password = self._get_credentials_from_credential_provider(url, is_retry=False) 74 | 75 | # Do not attempt to validate if the credentials could not be obtained 76 | if username is None or password is None: 77 | return username, password 78 | 79 | # Make sure the credentials are still valid (i.e. not expired) 80 | if self._can_authenticate(url, (username, password)): 81 | return username, password 82 | 83 | # The cached credentials are expired; get fresh ones with IsRetry=true 84 | return self._get_credentials_from_credential_provider(url, is_retry=True) 85 | 86 | def _can_authenticate(self, url, auth): 87 | response = requests.get(url, auth=auth) 88 | 89 | return response.status_code < 500 and \ 90 | response.status_code != 401 and \ 91 | response.status_code != 403 92 | 93 | 94 | def _get_credentials_from_credential_provider(self, url, is_retry): 95 | non_interactive = self._NON_INTERACTIVE_VAR_NAME in os.environ and \ 96 | os.environ[self._NON_INTERACTIVE_VAR_NAME] and \ 97 | str(os.environ[self._NON_INTERACTIVE_VAR_NAME]).lower() == "true" 98 | 99 | proc = Popen( 100 | self.exe + [ 101 | "-Uri", url, 102 | "-IsRetry", str(is_retry), 103 | "-NonInteractive", str(non_interactive), 104 | "-CanShowDialog", str(non_interactive), 105 | "-OutputFormat", "Json" 106 | ], 107 | stdin=subprocess.PIPE, 108 | stdout=subprocess.PIPE, 109 | stderr=subprocess.PIPE 110 | ) 111 | 112 | # Read all standard error first, which may either display 113 | # errors from the credential provider or instructions 114 | # from it for Device Flow authentication. 115 | for stderr_line in iter(proc.stderr.readline, b''): 116 | line = stderr_line.decode("utf-8", "ignore") 117 | sys.stderr.write(line) 118 | sys.stderr.flush() 119 | 120 | proc.wait() 121 | 122 | if proc.returncode != 0: 123 | stderr = proc.stderr.read().decode("utf-8", "ignore") 124 | raise RuntimeError("Failed to get credentials: process with PID {pid} exited with code {code}; additional error message: {error}" 125 | .format(pid=proc.pid, code=proc.returncode, error=stderr)) 126 | 127 | try: 128 | # stdout is expected to be UTF-8 encoded JSON, so decoding errors are not ignored here. 129 | payload = proc.stdout.read().decode("utf-8") 130 | except ValueError: 131 | raise RuntimeError("Failed to get credentials: the Credential Provider's output could not be decoded using UTF-8.") 132 | 133 | try: 134 | parsed = json.loads(payload) 135 | return parsed["Username"], parsed["Password"] 136 | except ValueError: 137 | raise RuntimeError("Failed to get credentials: the Credential Provider's output could not be parsed as JSON.") 138 | 139 | class ArtifactsKeyringBackend(): 140 | SUPPORTED_NETLOC = ( 141 | "pkgs.dev.azure.com", 142 | "pkgs.visualstudio.com", 143 | "pkgs.codedev.ms", 144 | "pkgs.vsts.me" 145 | ) 146 | _PROVIDER = CredentialProvider 147 | 148 | priority = 9.9 149 | 150 | def get_credential(self, service, username): 151 | try: 152 | parsed = urlsplit(service) 153 | except Exception as exc: 154 | warnings.warn(str(exc)) 155 | return None 156 | 157 | netloc = parsed.netloc.rpartition("@")[-1] 158 | 159 | if netloc is None or not netloc.endswith(self.SUPPORTED_NETLOC): 160 | return None 161 | 162 | provider = self._PROVIDER() 163 | 164 | username, password = provider.get_credentials(service) 165 | 166 | if username and password: 167 | return password 168 | 169 | result = sys.stdin.buffer.read() 170 | jsonResult = json.loads(result) 171 | resultUrl = jsonResult['channel_alias']['scheme'] + "://" + jsonResult['channel_alias']['location'] 172 | cred = ArtifactsKeyringBackend() 173 | token = cred.get_credential(resultUrl,None) 174 | print(token) # pipe the result back to the shell script -------------------------------------------------------------------------------- /src/artifacts-credprovider-conda/src/deactivate/artifacts-cred-deactivate.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set ARTIFACTS_CONDA_TOKEN= -------------------------------------------------------------------------------- /src/artifacts-credprovider-conda/src/deactivate/artifacts-cred-deactivate.ps1: -------------------------------------------------------------------------------- 1 | $env:ARTIFACTS_CONDA_TOKEN = $null -------------------------------------------------------------------------------- /src/artifacts-credprovider-conda/src/deactivate/artifacts-cred-deactivate.sh: -------------------------------------------------------------------------------- 1 | unset ARTIFACTS_CONDA_TOKEN -------------------------------------------------------------------------------- /test.bat: -------------------------------------------------------------------------------- 1 | @echo OFF 2 | SETLOCAL EnableDelayedExpansion 3 | 4 | @REM A Windows domain user should be able to run this against a feed in an AAD-back AzDO org 5 | @REM and all scenarios should succeed non-interactively. 6 | 7 | IF "%~1" == "" ( 8 | echo "Please specify an AzDO organization package feed URL as the first parameter." 9 | exit /b 1 10 | ) 11 | 12 | set TEST_FEED=%1 13 | set NUGET_CREDENTIALPROVIDER_MSAL_FILECACHE_ENABLED=true 14 | set NUGET_CREDENTIALPROVIDER_MSAL_FILECACHE_LOCATION=%TEMP%\msal.cache 15 | IF EXIST %NUGET_CREDENTIALPROVIDER_MSAL_FILECACHE_LOCATION% (del /q %NUGET_CREDENTIALPROVIDER_MSAL_FILECACHE_LOCATION%) 16 | 17 | echo "Testing MSAL with broker" 18 | set NUGET_CREDENTIALPROVIDER_MSAL_ALLOW_BROKER=true 19 | CALL :TEST_FRAMEWORKS 20 | IF %ERRORLEVEL% NEQ 0 ( 21 | echo "Failed: %ERRORLEVEL%" 22 | exit /b %ERRORLEVEL% 23 | ) 24 | 25 | echo "Testing MSAL without broker" 26 | set NUGET_CREDENTIALPROVIDER_MSAL_ALLOW_BROKER=false 27 | CALL :TEST_FRAMEWORKS 28 | IF %ERRORLEVEL% NEQ 0 ( 29 | echo "Failed: %ERRORLEVEL%" 30 | exit /b %ERRORLEVEL% 31 | ) 32 | 33 | echo "All tests passed!" 34 | exit /b 0 35 | 36 | 37 | :TEST_FRAMEWORKS 38 | for %%I in ("netcoreapp3.1","net461","net481","net6.0","net8.0") DO ( 39 | del /q "!UserProfile!\AppData\Local\MicrosoftCredentialProvider\*.dat" 2>NUL 40 | del /q "%NUGET_CREDENTIALPROVIDER_MSAL_FILECACHE_LOCATION%" 2>NUL 41 | echo Testing %%I with NUGET_CREDENTIALPROVIDER_MSAL_ALLOW_BROKER=!NUGET_CREDENTIALPROVIDER_MSAL_ALLOW_BROKER! 42 | echo dotnet run --no-restore --no-build -f %%I --project CredentialProvider.Microsoft\CredentialProvider.Microsoft.csproj -- -C -U !TEST_FEED! -V Debug 43 | dotnet run --no-restore --no-build -f %%I --project CredentialProvider.Microsoft\CredentialProvider.Microsoft.csproj -- -C -U !TEST_FEED! -V Debug ^ 44 | > test.%%I.%NUGET_CREDENTIALPROVIDER_MSAL_ALLOW_BROKER%.log 45 | IF !ERRORLEVEL! NEQ 0 ( 46 | echo "Previous command execution failed: !ERRORLEVEL!" 47 | dotnet run --no-restore --no-build -f %%I --project CredentialProvider.Microsoft\CredentialProvider.Microsoft.csproj -- -C -U !TEST_FEED! -V Debug 48 | exit /b !ERRORLEVEL! 49 | ) 50 | ) 51 | --------------------------------------------------------------------------------