diff --git a/CredentialProvider.Microsoft/CredentialProviders/VstsBuildTaskServiceEndpoint/VstsBuildTaskServiceEndpointCredentialProvider.cs b/CredentialProvider.Microsoft/CredentialProviders/VstsBuildTaskServiceEndpoint/VstsBuildTaskServiceEndpointCredentialProvider.cs index fbcf820a..e97b4318 100644 --- a/CredentialProvider.Microsoft/CredentialProviders/VstsBuildTaskServiceEndpoint/VstsBuildTaskServiceEndpointCredentialProvider.cs +++ b/CredentialProvider.Microsoft/CredentialProviders/VstsBuildTaskServiceEndpoint/VstsBuildTaskServiceEndpointCredentialProvider.cs @@ -101,7 +101,8 @@ public override async Task 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) diff --git a/CredentialProvider.Microsoft/Util/FeedEndpointCredentialsParser.cs b/CredentialProvider.Microsoft/Util/FeedEndpointCredentialsParser.cs index 3487bc0e..fe3b445e 100644 --- a/CredentialProvider.Microsoft/Util/FeedEndpointCredentialsParser.cs +++ b/CredentialProvider.Microsoft/Util/FeedEndpointCredentialsParser.cs @@ -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 @@ -72,25 +74,25 @@ public static Dictionary 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; @@ -105,7 +107,7 @@ public static Dictionary ParseFeedEndpointsJsonToDi catch (Exception ex) { logger.Verbose(string.Format(Resources.VstsBuildTaskExternalCredentialCredentialProviderError, ex)); - return new Dictionary(StringComparer.OrdinalIgnoreCase); ; + return new Dictionary(StringComparer.OrdinalIgnoreCase); } } diff --git a/README.md b/README.md index 795b7d11..96cb887d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/Authentication/MsalServicePrincipalTokenProvider.cs b/src/Authentication/MsalServicePrincipalTokenProvider.cs index a3c9973e..90f40a19 100644 --- a/src/Authentication/MsalServicePrincipalTokenProvider.cs +++ b/src/Authentication/MsalServicePrincipalTokenProvider.cs @@ -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 GetTokenAsync(TokenRequest tokenRequest, CancellationToken cancellationToken = default) @@ -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(); @@ -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()!); + } + } + } } diff --git a/src/Authentication/TokenRequest.cs b/src/Authentication/TokenRequest.cs index c59093be..42c675a0 100644 --- a/src/Authentication/TokenRequest.cs +++ b/src/Authentication/TokenRequest.cs @@ -40,4 +40,14 @@ public TokenRequest() public string? TenantId { get; set; } = null; public X509Certificate2? ClientCertificate { get; set; } = null; + + public ClientSecret? ClientSecret { get; set; } = null; +} + +/// +/// Wraps a client secret string to avoid accidental logging. +/// +public class ClientSecret(string secret) +{ + public string GetSecretString() => secret; }