Reputation: 7804
I'm developing a Visual Studio extension (VSIX project) that needs to manage Nuget packages of a given project.
I'm already using the IVsPackageInstaller
service as documented here but this is limited and I need more features (for example get the latest version number of a given package).
I searched but didn't find anything on how to programmatically interact with the Visual Studio Package Manager so I decided to go for the Nuget API directly.
I send HTTP requests to the Nuget API using the WebRequest
class (because we can't use HttpClient
in a VSIX project) but I'm hitting a problem: the requests are going to a private Nuget feed that needs authentication! (hosted on Azure DevOps)
I used Fiddler to check the HTTP requests sent to our Azure DevOps server. I see a POST request going to https://app.vssps.visualstudio.com/_apis/Token/SessionTokens
with a token in response but this is not the Token I'm looking for.
The token passed to the Nuget API is a Basic
token that comes from I don't know where. I couldn't find this token anywhere in the HTTP responses I caught.
I can also see that some responses to our Azure DevOps server contain some headers like this (I changed the GUID)
WWW-Authenticate: Bearer authorization_uri=https://login.windows.net/ce372fcc-5e17-490b-ad99-47565dac8a84
I can find this GUID back in the %userprofile%\AppData\Local\.IdentityService\AccountStore.json
file, there is definitely something going on here. And the SessionTokens.json
file in the same folder looks reeeaaally interesting too but it's encrypted...
I also tried to dig in the Registry to see if I can find interesting information for example at the path specified in Simon's comment but it seems VS2017 doesn't store the token there anymore.
I also loaded the privateregistry.bin
file (aka the Visual Studio Settings Store
) and searched everywhere but couldn't find anything.
So instead of trying to reverse engineer Visual Studio I wanted to access its Credential Provider directly. I tried to access to several services and classes
var componentModel = await ServiceProvider.GetGlobalServiceAsync(typeof(SComponentModel)) as IComponentModel;
var credentialProvider = componentModel.GetService<IVsCredentialProvider>();
var credentialServiceProvider = componentModel.GetService<ICredentialServiceProvider>();
var defaultCredentialServiceProvider = new DefaultVSCredentialServiceProvider();
But none of them are working (return null or Exception).
I wandered in the NuGet.PackageManagement.VisualStudio project on Github but couldn't find my answer.
There are also many Nuget packages like NuGet.PackageManagement.VisualStudio
, Microsoft.VisualStudio.Services.Release.Client
, Microsoft.VisualStudio.Services.ExtensionManagement.WebApi
, Microsoft.VisualStudio.Services.InteractiveClient
just to name a few but honestly I don't know if what I'm looking for is there...
So how to access the Nuget credentials used by Visual Studio?
I take any solution that gives me access to all the reading Nuget features, for example programmatically use the Visual Studio Package Management, or decrypt this SessionTokens.json
file or access the Visual Studio Credential Provider.
The less hacky is the answer, the better it is of couse.
At this point you probably already guessed, I don't want to store the username
and password
somewhere myself. I need to create a user-friendly VS extension, that's why I want to retrieve and use the credentials already saved in Visual Studio by the users.
Thank you so much if you can solve this problem.
Upvotes: 0
Views: 1803
Reputation: 7804
Thanks a lot to Simon who pointed me in the direction of NuGet.Client.
The only documentation from Microsoft is linking a 2016 blog post from Dave Glick but they also give a nice note:
These blog posts were written shortly after the 3.4.3 version of the NuGet client SDK packages were released. Newer versions of the packages may be incompatible with the information in the blog posts.
Alright, then I guess we will do with Dave's blog...
You should install two packages: NuGet.Client and Nuget.Protocol
Then here is the code for example to get the last version of a package:
using NuGet.Configuration;
using NuGet.Protocol;
using NuGet.Protocol.Core.Types;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MyProject
{
public class NugetHelper
{
public async Task<string> GetLatestVersionNumberFromNugetFeedAsync(NugetPackage package)
{
try
{
Logger logger = new Logger(); //Just a class implementing the Nuget.Common.ILogger interface
List<Lazy<INuGetResourceProvider>> providers = new List<Lazy<INuGetResourceProvider>>();
providers.AddRange(Repository.Provider.GetCoreV3()); // Add v3 API support
PackageSource packageSource = new PackageSource(package.Source.ToString());
SourceRepository sourceRepository = new SourceRepository(packageSource, providers);
PackageMetadataResource packageMetadataResource = await sourceRepository.GetResourceAsync<PackageMetadataResource>();
var searchMetadata = await packageMetadataResource.GetMetadataAsync(package.Name, false, false, new SourceCacheContext(), logger, new CancellationToken());
var versionNumber = searchMetadata.FirstOrDefault().Identity.Version.OriginalVersion;
return versionNumber;
}
catch (Exception ex)
{
return null;
}
}
}
public class NugetPackage
{
public string Name { get; set; }
public string Version { get; set; }
public string MinimumVersion { get; set; }
public Uri Source { get; set; }
}
}
I tried to reverse engineer where Visual Studio was storing the token used in HTTP requests to the Nuget API.
I exported to text files all the different hives of the Registry including the Visual Studio Settings Store (privateregistry.bin
).
Then added a brand new Nuget feed in Visual Studio, got the login popup as expected so I logged in.
Finally I exported all the hives again to text files and compared them all with the files before authentication.
I found nothing interesting in the VS Settings Store.
The only interesting changes were
[HKEY_CURRENT_USER\Software\Microsoft\VSCommon\ConnectedUser\IdeUserV2] @="0746fb8e-4bc2-4ee5-b804-0084af725deb" "AccountsRoaming_LastAccountsSettingVersion"=dword:0000025b
[HKEY_CURRENT_USER\Software\Microsoft\VsHub\ServiceModules\Settings\PerHubName\vshub\ConnectedUser\IdeUserV2\Cache] "LastProfileVersion"=dword:10b8260a
and
[HKEY_USERS\S-1-5-21-1787888774-1556370510-3519259403-1001\Software\Microsoft\VSCommon\Keychain] "TokenStorageNameSpace"="VisualStudio"
[HKEY_USERS\S-1-5-21-1787888774-1556370510-3519259403-1001\Software\Microsoft\VsHub\ServiceModules\Settings\PerHubName\vshub\ConnectedUser\IdeUserV2\Cache] "LastProfileVersion"=dword:10b8260a
Maybe somewhere, there is the key to these encrypted SessionTokens.json
and IdentityServiceAdalCache.cache
files but having the data stored in hexadecimal makes things even harder.
I've to give up on this, almost no chance I could reverse engineer the authentication system.
The NuGet Client SDK solves my issue but doesn't actually answer to this SO question.
As I said, I tried to call
var componentModel = await ServiceProvider.GetGlobalServiceAsync(typeof(SComponentModel)) as IComponentModel;
componentModel.GetService<ICredentialServiceProvider>()
But this didn't work, so if anybody knows how to access the Visual Studio credentials provider, I would be really glad to know the answer.
Upvotes: 1