@@ -499,6 +499,9 @@ func ConvertConfig(instrument *promoauth.Factory, entries []codersdk.ExternalAut
499
499
if entry .Type == string (codersdk .EnhancedExternalAuthProviderAzureDevops ) {
500
500
oauthConfig = & jwtConfig {oc }
501
501
}
502
+ if entry .Type == string (codersdk .EnhancedExternalAuthProviderAzureDevopsEntra ) {
503
+ oauthConfig = & entraV1Oauth {oc }
504
+ }
502
505
if entry .Type == string (codersdk .EnhancedExternalAuthProviderJFrog ) {
503
506
oauthConfig = & exchangeWithClientSecret {oc }
504
507
}
@@ -569,6 +572,9 @@ func applyDefaultsToConfig(config *codersdk.ExternalAuthConfig) {
569
572
case codersdk .EnhancedExternalAuthProviderGitea :
570
573
copyDefaultSettings (config , giteaDefaults (config ))
571
574
return
575
+ case codersdk .EnhancedExternalAuthProviderAzureDevopsEntra :
576
+ copyDefaultSettings (config , azureDevopsEntraDefaults (config ))
577
+ return
572
578
default :
573
579
// No defaults for this type. We still want to run this apply with
574
580
// an empty set of defaults.
@@ -730,6 +736,41 @@ func giteaDefaults(config *codersdk.ExternalAuthConfig) codersdk.ExternalAuthCon
730
736
return defaults
731
737
}
732
738
739
+ func azureDevopsEntraDefaults (config * codersdk.ExternalAuthConfig ) codersdk.ExternalAuthConfig {
740
+ defaults := codersdk.ExternalAuthConfig {
741
+ DisplayName : "Azure DevOps (Entra)" ,
742
+ DisplayIcon : "/icon/azure-devops.svg" ,
743
+ Regex : `^(https?://)?dev\.azure\.com(/.*)?$` ,
744
+ }
745
+ // The tenant ID is required for urls and is in the auth url.
746
+ if config .AuthURL == "" {
747
+ // No auth url, means we cannot guess the urls.
748
+ return defaults
749
+ }
750
+
751
+ auth , err := url .Parse (config .AuthURL )
752
+ if err != nil {
753
+ // We need a valid URL to continue with.
754
+ return defaults
755
+ }
756
+
757
+ // Only extract the tenant ID if the path is what we expect.
758
+ // The path should be /{tenantId}/oauth2/authorize.
759
+ parts := strings .Split (auth .Path , "/" )
760
+ if len (parts ) < 4 && parts [2 ] != "oauth2" || parts [3 ] != "authorize" {
761
+ // Not sure what this path is, abort.
762
+ return defaults
763
+ }
764
+ tenantID := parts [1 ]
765
+
766
+ tokenURL := auth .ResolveReference (& url.URL {Path : fmt .Sprintf ("/%s/oauth2/token" , tenantID )})
767
+ defaults .TokenURL = tokenURL .String ()
768
+
769
+ // TODO: Discover a validate url for Azure DevOps.
770
+
771
+ return defaults
772
+ }
773
+
733
774
var staticDefaults = map [codersdk.EnhancedExternalAuthProvider ]codersdk.ExternalAuthConfig {
734
775
codersdk .EnhancedExternalAuthProviderAzureDevops : {
735
776
AuthURL : "https://app.vssps.visualstudio.com/oauth2/authorize" ,
@@ -811,6 +852,26 @@ func (c *jwtConfig) Exchange(ctx context.Context, code string, opts ...oauth2.Au
811
852
)
812
853
}
813
854
855
+ // When authenticating via Entra ID ADO only supports v1 tokens that requires the 'resource' rather than scopes
856
+ // When ADO gets support for V2 Entra ID tokens this struct and functions can be removed
857
+ type entraV1Oauth struct {
858
+ * oauth2.Config
859
+ }
860
+
861
+ const azureDevOpsAppID = "499b84ac-1321-427f-aa17-267ca6975798"
862
+
863
+ func (c * entraV1Oauth ) AuthCodeURL (state string , opts ... oauth2.AuthCodeOption ) string {
864
+ return c .Config .AuthCodeURL (state , append (opts , oauth2 .SetAuthURLParam ("resource" , azureDevOpsAppID ))... )
865
+ }
866
+
867
+ func (c * entraV1Oauth ) Exchange (ctx context.Context , code string , opts ... oauth2.AuthCodeOption ) (* oauth2.Token , error ) {
868
+ return c .Config .Exchange (ctx , code ,
869
+ append (opts ,
870
+ oauth2 .SetAuthURLParam ("resource" , azureDevOpsAppID ),
871
+ )... ,
872
+ )
873
+ }
874
+
814
875
// exchangeWithClientSecret wraps an OAuth config and adds the client secret
815
876
// to the Exchange request as a Bearer header. This is used by JFrog Artifactory.
816
877
type exchangeWithClientSecret struct {
0 commit comments