Skip to content

Commit

Permalink
Managed Identity and Service Principal Support (#492)
Browse files Browse the repository at this point in the history
# Overview
- Added MSAL Managed Identity and Service Principal Token Providers to
Microsoft.Artifacts.Authentication Library.
- Created new endpoint `ARTIFACTS_CREDENTIALPROVIDER_FEED_ENDPOINTS`
environment variable with new json schema for MI/SP required fields.
- Updated VstsBuildTaskServiceEndpointCredentialProvider to call
Microsoft.Artifacts.Authentication for MI/SP token providers.
- Reverted #485 Changes to use system.text.json for de/serialization
everywhere except for the `VSS_NUGET_EXTERNAL_FEED_ENDPOINTS`
environment variable.

## Design Decisions
- Intentionally not supporting SP secrets authentication to promote
security best practices.
- The new environment variable name and json schema were created instead
of reusing or extending the existing `VSS_NUGET_EXTERNAL_FEED_ENDPOINTS`
to reduce password usage and clarify the environment variable will be
available to our other credproviders such as the
[artifacs-keyring](https://github.com/microsoft/artifacts-keyring) not
just NuGet.

## Environment Variable
 `ARTIFACTS_CREDENTIALPROVIDER_FEED_ENDPOINTS` 
```javascript
 {"endpointCredentials": [{"endpoint":"http://example.index.json", "clientId":"required", "clientCertificateSubjectName":"optional", "clientCertificateFilePath":"optional"}]}
```
- `endpoint`: required. Feed url to authenticate against. 
- `clientId`: required for both MI/SP. For user assigned managed
identities enter the Entra client id. For system assigned variables set
the value to `system`.
- `clientCertificateSubjectName`: Subject Name of the certificate
located in the My/ CurrentUser or LocalMachine certificate store.
Optional field. Only used by SP authentication.
- `clientCertificateFilePath`: File path location of the certificate on
the machine. Optional field. Only used by SP authentication.

Will throw error if both `clientCertificateSubjectName` or
`clientCertificateFilePath` are specified.
  • Loading branch information
embetten authored Jun 10, 2024
1 parent 74fe273 commit ae7cd59
Show file tree
Hide file tree
Showing 26 changed files with 1,269 additions and 369 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<PropertyGroup>
<LangVersion>latest</LangVersion>
<IsPackable>false</IsPackable>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>

<ItemGroup>
Expand All @@ -18,5 +20,4 @@
<ProjectReference Include="..\CredentialProvider.Microsoft\CredentialProvider.Microsoft.csproj" />
<ProjectReference Include="..\src\Authentication\Microsoft.Artifacts.Authentication.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -45,27 +45,27 @@ public void TestCleanup()
}

[TestMethod]
public async Task GetAadAuthorityUri_WithoutAuthenticateHeaders_ReturnsCorrectAuthority()
public async Task GetAuthorizationInfoAsync_WithoutAuthenticateHeaders_ReturnsCorrectAuthority()
{
var requestUri = new Uri("https://example.pkgs.visualstudio.com/_packaging/feed/nuget/v3/index.json");

var authorityUri = await authUtil.GetAadAuthorityUriAsync(requestUri, cancellationToken);
var authInfo = await authUtil.GetAuthorizationInfoAsync(requestUri, cancellationToken);

authorityUri.Should().Be(organizationsAuthority);
authInfo.EntraAuthorityUri.Should().Be(organizationsAuthority);
}

[TestMethod]
public async Task GetAadAuthorityUri_WithoutAuthenticateHeadersAndPpe_ReturnsCorrectAuthority()
public async Task GetAuthorizationInfoAsync_WithoutAuthenticateHeadersAndPpe_ReturnsCorrectAuthority()
{
var requestUri = new Uri("https://example.pkgs.vsts.me/_packaging/feed/nuget/v3/index.json");

var authorityUri = await authUtil.GetAadAuthorityUriAsync(requestUri, cancellationToken);
var authInfo = await authUtil.GetAuthorizationInfoAsync(requestUri, cancellationToken);

authorityUri.Should().Be(new Uri("https://login.windows-ppe.net/organizations"));
authInfo.EntraAuthorityUri.Should().Be(new Uri("https://login.windows-ppe.net/organizations"));
}

[TestMethod]
public async Task GetAadAuthorityUri_WithoutAuthenticateHeadersAndPpeAndPpeOverride_ReturnsCorrectAuthority()
public async Task GetAuthorizationInfoAsync_WithoutAuthenticateHeadersAndPpeAndPpeOverride_ReturnsCorrectAuthority()
{
var ppeUris = new[]
{
Expand All @@ -79,36 +79,49 @@ public async Task GetAadAuthorityUri_WithoutAuthenticateHeadersAndPpeAndPpeOverr

foreach (var ppeUri in ppeUris)
{
var authorityUri = await authUtil.GetAadAuthorityUriAsync(ppeUri, cancellationToken);
var authInfo = await authUtil.GetAuthorizationInfoAsync(ppeUri, cancellationToken);

authorityUri.Should().Be(new Uri("https://login.windows-ppe.net/organizations"));
authInfo.EntraAuthorityUri.Should().Be(new Uri("https://login.windows-ppe.net/organizations"));
}
}

[TestMethod]
public async Task GetAadAuthorityUri_WithAuthenticateHeaders_ReturnsCorrectAuthority()
public async Task GetAuthorizationInfoAsync_WithAuthenticateHeaders_ReturnsCorrectAuthority()
{
var requestUri = new Uri("https://example.pkgs.visualstudio.com/_packaging/feed/nuget/v3/index.json");

MockAadAuthorityHeaders(testAuthority);

var authorityUri = await authUtil.GetAadAuthorityUriAsync(requestUri, cancellationToken);
var authInfo = await authUtil.GetAuthorizationInfoAsync(requestUri, cancellationToken);

authorityUri.Should().Be(testAuthority);
authInfo.EntraAuthorityUri.Should().Be(testAuthority);
}

[TestMethod]
public async Task GetAadAuthorityUri_WithAuthenticateHeadersAndEnvironmentOverride_ReturnsOverrideAuthority()
public async Task GetAuthorizationInfoAsync_WithAuthenticateHeadersAndEnvironmentOverride_ReturnsOverrideAuthority()
{
var requestUri = new Uri("https://example.pkgs.visualstudio.com/_packaging/feed/nuget/v3/index.json");
var overrideAuthority = new Uri("https://override.aad.authority.com");

MockAadAuthorityHeaders(testAuthority);

Environment.SetEnvironmentVariable(EnvUtil.MsalAuthorityEnvVar, overrideAuthority.ToString());
var authorityUri = await authUtil.GetAadAuthorityUriAsync(requestUri, cancellationToken);
var authInfo = await authUtil.GetAuthorizationInfoAsync(requestUri, cancellationToken);

authorityUri.Should().Be(overrideAuthority);
authInfo.EntraAuthorityUri.Should().Be(overrideAuthority);
}

[TestMethod]
public async Task GetAuthorizationInfoAsync_WithTenantHeaders_ReturnsCorrectTenantId()
{
var requestUri = new Uri("https://example.pkgs.visualstudio.com/_packaging/feed/nuget/v3/index.json");

var testTenant = Guid.NewGuid();
MockVssResourceTenantHeader(testTenant);

var authInfo = await authUtil.GetAuthorizationInfoAsync(requestUri, cancellationToken);

authInfo.EntraTenantId.Should().Be(testTenant.ToString());
}

[TestMethod]
Expand Down Expand Up @@ -203,9 +216,9 @@ private void MockResponseHeaders(string key, string value)
authUtil.HttpResponseHeaders.Add(key, value);
}

private void MockVssResourceTenantHeader()
private void MockVssResourceTenantHeader(Guid? guid = null)
{
MockResponseHeaders(AuthUtil.VssResourceTenant, Guid.NewGuid().ToString());
MockResponseHeaders(AuthUtil.VssResourceTenant,(guid ?? Guid.NewGuid()).ToString());
}

private void MockVssAuthorizationEndpointHeader()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ public void TestInitialize()

mockAuthUtil = new Mock<IAuthUtil>();
mockAuthUtil
.Setup(x => x.GetAadAuthorityUriAsync(It.IsAny<Uri>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(testAuthority));
.Setup(x => x.GetAuthorizationInfoAsync(It.IsAny<Uri>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(new AuthorizationInfo() { EntraAuthorityUri = testAuthority }));

vstsCredentialProvider = new VstsCredentialProvider(
mockLogger.Object,
Expand Down
Loading

0 comments on commit ae7cd59

Please sign in to comment.