Skip to content

Commit 488aa48

Browse files
authored
Support managed identities and service principals (#1372)
Add support for both managed identities and service principals for Azure Repos authentication. Service principals are the "service account" equivalent to normal "users" (user principals). Rather than a password+MFA, service principals (SP) authenticate either with a 'secret' or a certificate - we support both. Managed identities (MI) are an evolution of service principals whereby the secret/certificate are hidden and managed by Azure. There are two types of managed identities: system-assigned and user-assigned. System-assigned are tied to a particular VM or resource, whereas user-assigned can be shared between VMs/resources. Azure DevOps recently learned to support SPs and MIs as identities that can be members of orgs and projects. Note that this only applies to AAD-connected Azure DevOps orgs. This functionality is most compelling for automated scenarios, like CI machines, that need access to Azure Repos. Fixes #1313
2 parents a74fe18 + eff4ea6 commit 488aa48

File tree

9 files changed

+768
-33
lines changed

9 files changed

+768
-33
lines changed

docs/configuration.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,95 @@ git config --global credential.azreposCredentialType oauth
793793

794794
---
795795

796+
### credential.azreposManagedIdentity
797+
798+
Use a [Managed Identity][managed-identity] to authenticate with Azure Repos.
799+
800+
The value `system` will tell GCM to use the system-assigned Managed Identity.
801+
802+
To specify a user-assigned Managed Identity, use the format `id://{clientId}`
803+
where `{clientId}` is the client ID of the Managed Identity. Alternatively any
804+
GUID-like value will also be interpreted as a user-assigned Managed Identity
805+
client ID.
806+
807+
To specify a Managed Identity associated with an Azure resource, you can use the
808+
format `resource://{resourceId}` where `{resourceId}` is the ID of the resource.
809+
810+
For more information about managed identities, see the Azure DevOps
811+
[documentation][azrepos-sp-mid].
812+
813+
Value|Description
814+
-|-
815+
`system`|System-Assigned Managed Identity
816+
`[guid]`|User-Assigned Managed Identity with the specified client ID
817+
`id://[guid]`|User-Assigned Managed Identity with the specified client ID
818+
`resource://[guid]`|User-Assigned Managed Identity for the associated resource
819+
820+
```shell
821+
git config --global credential.azreposManagedIdentity "id://11111111-1111-1111-1111-111111111111"
822+
```
823+
824+
**Also see: [GCM_AZREPOS_MANAGEDIDENTITY][gcm-azrepos-credentialmanagedidentity]**
825+
826+
---
827+
828+
### credential.azreposServicePrincipal
829+
830+
Specify the client and tenant IDs of a [service principal][service-principal]
831+
to use when performing Microsoft authentication for Azure Repos.
832+
833+
The value of this setting should be in the format: `{tenantId}/{clientId}`.
834+
835+
You must also set at least one authentication mechanism if you set this value:
836+
837+
- [credential.azreposServicePrincipalSecret][credential-azrepos-sp-secret]
838+
- [credential.azreposServicePrincipalCertificateThumbprint][credential-azrepos-sp-cert-thumbprint]
839+
840+
For more information about service principals, see the Azure DevOps
841+
[documentation][azrepos-sp-mid].
842+
843+
#### Example
844+
845+
```shell
846+
git config --global credential.azreposServicePrincipal "11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222"
847+
```
848+
849+
**Also see: [GCM_AZREPOS_SERVICE_PRINCIPAL][gcm-azrepos-service-principal]**
850+
851+
---
852+
853+
### credential.azreposServicePrincipalSecret
854+
855+
Specifies the client secret for the [service principal][service-principal] when
856+
performing Microsoft authentication for Azure Repos with
857+
[credential.azreposServicePrincipalSecret][credential-azrepos-sp] set.
858+
859+
#### Example
860+
861+
```shell
862+
git config --global credential.azreposServicePrincipalSecret "da39a3ee5e6b4b0d3255bfef95601890afd80709"
863+
```
864+
865+
**Also see: [GCM_AZREPOS_SP_SECRET][gcm-azrepos-sp-secret]**
866+
867+
---
868+
869+
### credential.azreposServicePrincipalCertificateThumbprint
870+
871+
Specifies the thumbprint of a certificate to use when authenticating as a
872+
[service principal][service-principal] for Azure Repos when
873+
[GCM_AZREPOS_SERVICE_PRINCIPAL][credential-azrepos-sp] is set.
874+
875+
#### Example
876+
877+
```shell
878+
git config --global credential.azreposServicePrincipalCertificateThumbprint "9b6555292e4ea21cbc2ebd23e66e2f91ebbe92dc"
879+
```
880+
881+
**Also see: [GCM_AZREPOS_SP_CERT_THUMBPRINT][gcm-azrepos-sp-cert-thumbprint]**
882+
883+
---
884+
796885
### trace2.normalTarget
797886

798887
Turns on Trace2 Normal Format tracing - see [Git's Trace2 Normal Format
@@ -878,6 +967,7 @@ Defaults to disabled.
878967
[gcm-authority]: environment.md#GCM_AUTHORITY-deprecated
879968
[gcm-autodetect-timeout]: environment.md#GCM_AUTODETECT_TIMEOUT
880969
[gcm-azrepos-credentialtype]: environment.md#GCM_AZREPOS_CREDENTIALTYPE
970+
[gcm-azrepos-credentialmanagedidentity]: environment.md#GCM_AZREPOS_MANAGEDIDENTITY
881971
[gcm-bitbucket-always-refresh-credentials]: environment.md#GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS
882972
[gcm-bitbucket-authmodes]: environment.md#GCM_BITBUCKET_AUTHMODES
883973
[gcm-credential-cache-options]: environment.md#GCM_CREDENTIAL_CACHE_OPTIONS
@@ -905,6 +995,7 @@ Defaults to disabled.
905995
[http-proxy]: netconfig.md#http-proxy
906996
[autodetect]: autodetect.md
907997
[libsecret]: https://wiki.gnome.org/Projects/Libsecret
998+
[managed-identity]: https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview
908999
[provider-migrate]: migration.md#gcm_authority
9091000
[cache-options]: https://git-scm.com/docs/git-credential-cache#_options
9101001
[pass]: https://www.passwordstore.org/
@@ -915,3 +1006,11 @@ Defaults to disabled.
9151006
[trace2-performance-docs]: https://git-scm.com/docs/api-trace2#_the_performance_format_target
9161007
[trace2-performance-env]: environment.md#GIT_TRACE2_PERF
9171008
[wam]: windows-broker.md
1009+
[service-principal]: https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals
1010+
[azrepos-sp-mid]: https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity
1011+
[credential-azrepos-sp]: #credentialazreposserviceprincipal
1012+
[credential-azrepos-sp-secret]: #credentialazreposserviceprincipalsecret
1013+
[credential-azrepos-sp-cert-thumbprint]: #credentialazreposserviceprincipalcertificatethumbprint
1014+
[gcm-azrepos-service-principal]: environment.md#GCM_AZREPOS_SERVICE_PRINCIPAL
1015+
[gcm-azrepos-sp-secret]: environment.md#GCM_AZREPOS_SP_SECRET
1016+
[gcm-azrepos-sp-cert-thumbprint]: environment.md#GCM_AZREPOS_SP_CERT_THUMBPRINT

docs/environment.md

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,121 @@ export GCM_AZREPOS_CREDENTIALTYPE="oauth"
894894

895895
---
896896

897+
### GCM_AZREPOS_MANAGEDIDENTITY
898+
899+
Use a [Managed Identity][managed-identity] to authenticate with Azure Repos.
900+
901+
The value `system` will tell GCM to use the system-assigned Managed Identity.
902+
903+
To specify a user-assigned Managed Identity, use the format `id://{clientId}`
904+
where `{clientId}` is the client ID of the Managed Identity. Alternatively any
905+
GUID-like value will also be interpreted as a user-assigned Managed Identity
906+
client ID.
907+
908+
To specify a Managed Identity associated with an Azure resource, you can use the
909+
format `resource://{resourceId}` where `{resourceId}` is the ID of the resource.
910+
911+
For more information about managed identities, see the Azure DevOps
912+
[documentation][azrepos-sp-mid].
913+
914+
Value|Description
915+
-|-
916+
`system`|System-Assigned Managed Identity
917+
`[guid]`|User-Assigned Managed Identity with the specified client ID
918+
`id://[guid]`|User-Assigned Managed Identity with the specified client ID
919+
`resource://[guid]`|User-Assigned Managed Identity for the associated resource
920+
921+
#### Windows
922+
923+
```batch
924+
SET GCM_AZREPOS_MANAGEDIDENTITY="id://11111111-1111-1111-1111-111111111111"
925+
```
926+
927+
#### macOS/Linux
928+
929+
```bash
930+
export GCM_AZREPOS_MANAGEDIDENTITY="id://11111111-1111-1111-1111-111111111111"
931+
```
932+
933+
**Also see: [credential.azreposManagedIdentity][credential-azrepos-managedidentity]**
934+
935+
---
936+
937+
### GCM_AZREPOS_SERVICE_PRINCIPAL
938+
939+
Specify the client and tenant IDs of a [service principal][service-principal]
940+
to use when performing Microsoft authentication for Azure Repos.
941+
942+
The value of this setting should be in the format: `{tenantId}/{clientId}`.
943+
944+
You must also set at least one authentication mechanism if you set this value:
945+
946+
- [GCM_AZREPOS_SP_SECRET][gcm-azrepos-sp-secret]
947+
- [GCM_AZREPOS_SP_CERT_THUMBPRINT][gcm-azrepos-sp-cert-thumbprint]
948+
949+
For more information about service principals, see the Azure DevOps
950+
[documentation][azrepos-sp-mid].
951+
952+
#### Windows
953+
954+
```batch
955+
SET GCM_AZREPOS_SERVICE_PRINCIPAL="11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222"
956+
```
957+
958+
#### macOS/Linux
959+
960+
```bash
961+
export GCM_AZREPOS_SERVICE_PRINCIPAL="11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222"
962+
```
963+
964+
**Also see: [credential.azreposServicePrincipal][credential-azrepos-sp]**
965+
966+
---
967+
968+
### GCM_AZREPOS_SP_SECRET
969+
970+
Specifies the client secret for the [service principal][service-principal] when
971+
performing Microsoft authentication for Azure Repos with
972+
[GCM_AZREPOS_SERVICE_PRINCIPAL][gcm-azrepos-sp] set.
973+
974+
#### Windows
975+
976+
```batch
977+
SET GCM_AZREPOS_SP_SECRET="da39a3ee5e6b4b0d3255bfef95601890afd80709"
978+
```
979+
980+
#### macOS/Linux
981+
982+
```bash
983+
export GCM_AZREPOS_SP_SECRET="da39a3ee5e6b4b0d3255bfef95601890afd80709"
984+
```
985+
986+
**Also see: [credential.azreposServicePrincipalSecret][credential-azrepos-sp-secret]**
987+
988+
---
989+
990+
### GCM_AZREPOS_SP_CERT_THUMBPRINT
991+
992+
Specifies the thumbprint of a certificate to use when authenticating as a
993+
[service principal][service-principal] for Azure Repos when
994+
[GCM_AZREPOS_SERVICE_PRINCIPAL][gcm-azrepos-sp] is set.
995+
996+
#### Windows
997+
998+
```batch
999+
SET GCM_AZREPOS_SP_CERT_THUMBPRINT="9b6555292e4ea21cbc2ebd23e66e2f91ebbe92dc"
1000+
```
1001+
1002+
#### macOS/Linux
1003+
1004+
```bash
1005+
export GCM_AZREPOS_SP_CERT_THUMBPRINT="9b6555292e4ea21cbc2ebd23e66e2f91ebbe92dc"
1006+
```
1007+
1008+
**Also see: [credential.azreposServicePrincipalCertificateThumbprint][credential-azrepos-sp-cert-thumbprint]**
1009+
1010+
---
1011+
8971012
### GIT_TRACE2
8981013

8991014
Turns on Trace2 Normal Format tracing - see [Git's Trace2 Normal Format
@@ -985,7 +1100,8 @@ Defaults to disabled.
9851100
[credential-allowwindowsauth]: environment.md#credentialallowWindowsAuth
9861101
[credential-authority]: configuration.md#credentialauthority-deprecated
9871102
[credential-autodetecttimeout]: configuration.md#credentialautodetecttimeout
988-
[credential-azrepos-credential-type]: configuration.md#azreposcredentialtype
1103+
[credential-azrepos-credential-type]: configuration.md#credentialazreposcredentialtype
1104+
[credential-azrepos-managedidentity]: configuration.md#credentialazreposmanagedidentity
9891105
[credential-bitbucketauthmodes]: configuration.md#credentialbitbucketAuthModes
9901106
[credential-cacheoptions]: configuration.md#credentialcacheoptions
9911107
[credential-credentialstore]: configuration.md#credentialcredentialstore
@@ -1022,6 +1138,7 @@ Defaults to disabled.
10221138
[github-emu]: https://docs.github.com/en/enterprise-cloud@latest/admin/identity-and-access-management/using-enterprise-managed-users-for-iam/about-enterprise-managed-users
10231139
[network-http-proxy]: netconfig.md#http-proxy
10241140
[libsecret]: https://wiki.gnome.org/Projects/Libsecret
1141+
[managed-identity]: https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview
10251142
[migration-guide]: migration.md#gcm_authority
10261143
[passwordstore]: https://www.passwordstore.org/
10271144
[trace2-normal-docs]: https://git-scm.com/docs/api-trace2#_the_normal_format_target
@@ -1031,3 +1148,11 @@ Defaults to disabled.
10311148
[trace2-performance-docs]: https://git-scm.com/docs/api-trace2#_the_performance_format_target
10321149
[trace2-performance-config]: configuration.md#trace2perfTarget
10331150
[windows-broker]: windows-broker.md
1151+
[service-principal]: https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals
1152+
[azrepos-sp-mid]: https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity
1153+
[gcm-azrepos-sp]: #gcm_azrepos_service_principal
1154+
[gcm-azrepos-sp-secret]: #gcm_azrepos_sp_secret
1155+
[gcm-azrepos-sp-cert-thumbprint]: #gcm_azrepos_sp_cert_thumbprint
1156+
[credential-azrepos-sp]: configuration.md#credentialazreposserviceprincipal
1157+
[credential-azrepos-sp-secret]: configuration.md#credentialazreposserviceprincipalsecret
1158+
[credential-azrepos-sp-cert-thumbprint]: configuration.md#credentialazreposserviceprincipalcertificatethumbprint

src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
using System;
2+
using System.Threading.Tasks;
23
using GitCredentialManager.Authentication;
34
using GitCredentialManager.Tests.Objects;
5+
using Microsoft.Identity.Client.AppConfig;
46
using Xunit;
57

68
namespace GitCredentialManager.Tests.Authentication
79
{
810
public class MicrosoftAuthenticationTests
911
{
1012
[Fact]
11-
public async System.Threading.Tasks.Task MicrosoftAuthentication_GetAccessTokenAsync_NoInteraction_ThrowsException()
13+
public async Task MicrosoftAuthentication_GetTokenForUserAsync_NoInteraction_ThrowsException()
1214
{
1315
const string authority = "https://login.microsoftonline.com/common";
1416
const string clientId = "C9E8FDA6-1D46-484C-917C-3DBD518F27C3";
@@ -24,7 +26,48 @@ public async System.Threading.Tasks.Task MicrosoftAuthentication_GetAccessTokenA
2426
var msAuth = new MicrosoftAuthentication(context);
2527

2628
await Assert.ThrowsAsync<Trace2InvalidOperationException>(
27-
() => msAuth.GetTokenAsync(authority, clientId, redirectUri, scopes, userName, false));
29+
() => msAuth.GetTokenForUserAsync(authority, clientId, redirectUri, scopes, userName, false));
30+
}
31+
32+
[Theory]
33+
[InlineData(null)]
34+
[InlineData("")]
35+
[InlineData(" ")]
36+
[InlineData("system")]
37+
[InlineData("SYSTEM")]
38+
[InlineData("sYsTeM")]
39+
[InlineData("00000000-0000-0000-0000-000000000000")]
40+
[InlineData("id://00000000-0000-0000-0000-000000000000")]
41+
[InlineData("ID://00000000-0000-0000-0000-000000000000")]
42+
[InlineData("Id://00000000-0000-0000-0000-000000000000")]
43+
public void MicrosoftAuthentication_GetManagedIdentity_ValidSystemId_ReturnsSystemId(string str)
44+
{
45+
ManagedIdentityId actual = MicrosoftAuthentication.GetManagedIdentity(str);
46+
Assert.Equal(ManagedIdentityId.SystemAssigned, actual);
47+
}
48+
49+
[Theory]
50+
[InlineData("8B49DCA0-1298-4A0D-AD6D-934E40230839")]
51+
[InlineData("id://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
52+
[InlineData("ID://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
53+
[InlineData("Id://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
54+
[InlineData("resource://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
55+
[InlineData("RESOURCE://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
56+
[InlineData("rEsOuRcE://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
57+
[InlineData("resource://00000000-0000-0000-0000-000000000000")]
58+
public void MicrosoftAuthentication_GetManagedIdentity_ValidUserIdByClientId_ReturnsUserId(string str)
59+
{
60+
ManagedIdentityId actual = MicrosoftAuthentication.GetManagedIdentity(str);
61+
Assert.NotNull(actual);
62+
Assert.NotEqual(ManagedIdentityId.SystemAssigned, actual);
63+
}
64+
65+
[Theory]
66+
[InlineData("unknown://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
67+
[InlineData("this is a string")]
68+
public void MicrosoftAuthentication_GetManagedIdentity_Invalid_ThrowsArgumentException(string str)
69+
{
70+
Assert.Throws<ArgumentException>(() => MicrosoftAuthentication.GetManagedIdentity(str));
2871
}
2972
}
3073
}

0 commit comments

Comments
 (0)