Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ public override async Task<GetAuthenticationCredentialsResponse> HandleRequestAs
InteractiveTimeout = TimeSpan.FromSeconds(EnvUtil.GetDeviceFlowTimeoutFromEnvironmentInSeconds(Logger)),
ClientId = matchingEndpoint.ClientId,
ClientCertificate = clientCertificate,
TenantId = authInfo.EntraTenantId
TenantId = authInfo.EntraTenantId,
ClientSecret = !string.IsNullOrEmpty(matchingEndpoint.ClientSecret) ? new(matchingEndpoint.ClientSecret) : null,
};

foreach(var tokenProvider in tokenProviders)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public class EndpointCredentials
public string CertificateFilePath { get; set; }
[JsonPropertyName("clientCertificateSubjectName")]
public string CertificateSubjectName { get; set; }
[JsonPropertyName("clientSecret")]
public string ClientSecret { get; set; }
}

public class EndpointCredentialsContainer
Expand Down Expand Up @@ -72,25 +74,25 @@ public static Dictionary<string, EndpointCredentials> ParseFeedEndpointsJsonToDi
if (credentials == null)
{
logger.Verbose(Resources.EndpointParseFailure);
break;
continue;
}

if (credentials.ClientId == null)
{
logger.Verbose(Resources.EndpointParseFailure);
break;
continue;
}

if (credentials.CertificateSubjectName != null && credentials.CertificateFilePath != null)
{
logger.Verbose(Resources.EndpointParseFailure);
break;
continue;
}

if (!Uri.TryCreate(credentials.Endpoint, UriKind.Absolute, out var endpointUri))
{
logger.Verbose(Resources.EndpointParseFailure);
break;
continue;
}

var urlEncodedEndpoint = endpointUri.AbsoluteUri;
Expand All @@ -105,7 +107,7 @@ public static Dictionary<string, EndpointCredentials> ParseFeedEndpointsJsonToDi
catch (Exception ex)
{
logger.Verbose(string.Format(Resources.VstsBuildTaskExternalCredentialCredentialProviderError, ex));
return new Dictionary<string, EndpointCredentials>(StringComparer.OrdinalIgnoreCase); ;
return new Dictionary<string, EndpointCredentials>(StringComparer.OrdinalIgnoreCase);
}
}

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,14 @@ The Credential Provider accepts a set of environment variables. Not all of them

- `ARTIFACTS_CREDENTIALPROVIDER_FEED_ENDPOINTS`: Json that contains an array of endpoints, usernames and azure service principal information needed to authenticate to Azure Artifacts feed endponts. Example:
```javascript
{"endpointCredentials": [{"endpoint":"http://example.index.json", "clientId":"required", "clientCertificateSubjectName":"optional", "clientCertificateFilePath":"optional"}]}
{"endpointCredentials": [{"endpoint":"http://example.index.json", "clientId":"required", "clientCertificateSubjectName":"optional", "clientCertificateFilePath":"optional", "clientSecret":"optional"}]}
```

- `endpoint`: Required. Feed url to authenticate.
- `clientId`: Required for both Azure Managed Identites and Service Principals. For user assigned managed identities enter the Entra client id. For system assigned managed identities set the value to `system`.
- `clientCertificateSubjectName`: Subject Name of the certificate located in the CurrentUser or LocalMachine certificate store. Optional field. Only used for service principal authentication.
- `clientCertificateFilePath`: File path location of the certificate on the machine. Optional field. Only used by service principal authentication.
- `clientCertificateFilePath`: File path location of the certificate on the machine. Optional field. Only used by service principal authentication.
- `clientSecret`: Client secret used for service principal authentication. Optional field. Only used for sevice principal authentication if client certificate is not specified. Prefer using a certificate for improved security.

## Release version 1.0.0

Expand Down
22 changes: 20 additions & 2 deletions src/Authentication/MsalServicePrincipalTokenProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public MsalServicePrincipalTokenProvider(IPublicClientApplication app, ILogger l
public bool CanGetToken(TokenRequest tokenRequest)
{
return !string.IsNullOrWhiteSpace(tokenRequest.ClientId)
&& tokenRequest.ClientCertificate != null;
&& (tokenRequest.ClientCertificate != null || tokenRequest.ClientSecret != null);
}

public async Task<AuthenticationResult?> GetTokenAsync(TokenRequest tokenRequest, CancellationToken cancellationToken = default)
Expand All @@ -37,7 +37,7 @@ public bool CanGetToken(TokenRequest tokenRequest)
var app = ConfidentialClientApplicationBuilder.Create(tokenRequest.ClientId)
.WithHttpClientFactory(appConfig.HttpClientFactory)
.WithLogging(appConfig.LoggingCallback, appConfig.LogLevel, appConfig.EnablePiiLogging, appConfig.IsDefaultPlatformLoggingEnabled)
.WithCertificate(tokenRequest.ClientCertificate, sendX5C: true)
.WithCertificateOrClientSecret(tokenRequest)
.WithTenantId(tokenRequest.TenantId)
.Build();

Expand All @@ -54,4 +54,22 @@ public bool CanGetToken(TokenRequest tokenRequest)
}
}
}

public static class MsalApplicationBuilderExtensions
{
public static ConfidentialClientApplicationBuilder WithCertificateOrClientSecret(
this ConfidentialClientApplicationBuilder builder,
TokenRequest tokenRequest
)
{
if (tokenRequest.ClientCertificate != null)
{
return builder.WithCertificate(tokenRequest.ClientCertificate, sendX5C: true);
}
else
{
return builder.WithClientSecret(tokenRequest.ClientSecret?.GetSecretString()!);
}
}
}
}
10 changes: 10 additions & 0 deletions src/Authentication/TokenRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,14 @@ public TokenRequest()
public string? TenantId { get; set; } = null;

public X509Certificate2? ClientCertificate { get; set; } = null;

public ClientSecret? ClientSecret { get; set; } = null;
}

/// <summary>
/// Wraps a client secret string to avoid accidental logging.
/// </summary>
public class ClientSecret(string secret)
{
public string GetSecretString() => secret;
}