Skip to content

feat: add support for Entra ID auth when using Azure DevOps external auth #12201

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 4, 2024

Conversation

alexwilcox9
Copy link
Contributor

Very much a work in progress at the moment to prove the concept but I've hit a barrier and perhaps you'll be able to help.

To set a bit of context there are a few reasons I think Coder should support this method of authenticating to Azure DevOps:

So far I've just been editing the existing ADO connector, I appreciate this would need to be an additional connector to be merged as to not break anyone's existing deployments.

Entra app registration manifest

{
	"id": "<GUID>",
	"acceptMappedClaims": null,
	"accessTokenAcceptedVersion": null,
	"addIns": [],
	"allowPublicClient": null,
	"appId": "1f71dbec-936b-49d2-9552-e220fb29260a",
	"appRoles": [],
	"oauth2AllowUrlPathMatching": false,
	"createdDateTime": "2024-02-12T14:38:45Z",
	"description": null,
	"certification": null,
	"disabledByMicrosoftStatus": null,
	"groupMembershipClaims": null,
	"identifierUris": [],
	"informationalUrls": {
		"termsOfService": null,
		"support": null,
		"privacy": null,
		"marketing": null
	},
	"keyCredentials": [],
	"knownClientApplications": [],
	"logoUrl": null,
	"logoutUrl": null,
	"name": "Coder ADO Test",
	"notes": null,
	"oauth2AllowIdTokenImplicitFlow": false,
	"oauth2AllowImplicitFlow": false,
	"oauth2Permissions": [],
	"oauth2RequirePostResponse": false,
	"optionalClaims": null,
	"orgRestrictions": [],
	"parentalControlSettings": {
		"countriesBlockedForMinors": [],
		"legalAgeGroupRule": "Allow"
	},
	"passwordCredentials": [
		{
			"customKeyIdentifier": null,
			"endDate": "2024-08-10T13:39:18.311Z",
			"keyId": "442081e8-b295-4259-b1dc-5e0258745cc6",
			"startDate": "2024-02-12T14:39:18.311Z",
			"value": null,
			"createdOn": "2024-02-12T14:39:20.1029847Z",
			"hint": "iY~",
			"displayName": "test"
		}
	],
	"preAuthorizedApplications": [],
	"publisherDomain": "cloudsecure.ltd",
	"replyUrlsWithType": [
		{
			"url": "https://<ID>.pit-1.try.coder.app/external-auth/entra-azure-devops/callback",
			"type": "Web"
		}
	],
	"requiredResourceAccess": [
		{
			"resourceAppId": "499b84ac-1321-427f-aa17-267ca6975798",
			"resourceAccess": [
				{
					"id": "028ffaf1-6f06-490a-979e-38011f92fb7c",
					"type": "Scope"
				}
			]
		}
	],
	"samlMetadataUrl": null,
	"signInUrl": null,
	"signInAudience": "AzureADMyOrg",
	"tags": [],
	"tokenEncryptionKeyId": null
}

Signing in seems to work and I get a JWT back but the git clone operation always fails with fatal: Authentication failed for...

Any tips would be very welcome

@cdr-bot cdr-bot bot added the community Pull Requests and issues created by the community. label Feb 18, 2024
Copy link

github-actions bot commented Feb 18, 2024

CLA Assistant Lite bot All contributors have signed the CLA ✍️ ✅

@alexwilcox9
Copy link
Contributor Author

I have read the CLA Document and I hereby sign the CLA

cdrcommunity added a commit to coder/cla that referenced this pull request Feb 18, 2024
@alexwilcox9
Copy link
Contributor Author

Good news, I got it working!

Turns out ADO doesn't support V2 of the Entra ID tokens so I had to switch from these endpoints:

https://login.microsoftonline.com/<TENANT ID>/oauth2/v2.0/authorize
https://login.microsoftonline.com/<TENANT ID>/oauth2/v2.0/token

To these endpoints

https://login.microsoftonline.com/<TENANT ID>/oauth2/authorize
https://login.microsoftonline.com/<TENANT ID>/oauth2/token

In this configuration rather than specifying a scope you specify the GUID of the ADO resource and that gives you whatever scopes you have set in your app registration (still only requires vso.code_write)

I'm now going to look at making this its own provider to go alongside the existing ADO connection

@alexwilcox9 alexwilcox9 changed the title Feat: Adding support for Entra ID auth when using Azure DevOps external auth feat: Adding support for Entra ID auth when using Azure DevOps external auth Feb 23, 2024
@alexwilcox9 alexwilcox9 changed the title feat: Adding support for Entra ID auth when using Azure DevOps external auth feat: add support for Entra ID auth when using Azure DevOps external auth Feb 23, 2024
@matifali matifali requested a review from Emyrk February 24, 2024 05:43
@alexwilcox9 alexwilcox9 marked this pull request as ready for review February 25, 2024 11:27
Copy link
Member

@Emyrk Emyrk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appreciate the contribution! I will have to do some quick reading on this, but it all is looking pretty good.

I will allocate some time today to look this over.

Copy link
Member

@Emyrk Emyrk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the PoC part now. I did not notice the resource was a constant uuid.

@alexwilcox9 is it alright if I just build off your commits here?

@alexwilcox9
Copy link
Contributor Author

Hey, thanks for having a look!
Feel free to commit on top of my changes, I'm on UK time so won't be looking at this properly until tomorrow

Comment on lines 788 to 873
// When authenticating via Entra ID ADO only supports v1 tokens that requires the 'resource' rather than scopes
type jwtConfigEntra struct {
*oauth2.Config
}

func (c *jwtConfigEntra) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
return c.Config.AuthCodeURL(state, append(opts, oauth2.SetAuthURLParam("resource", "499b84ac-1321-427f-aa17-267ca6975798"))...)
}

func (c *jwtConfigEntra) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
return c.Config.Exchange(ctx, code,
append(opts,
oauth2.SetAuthURLParam("client_id", c.ClientID),
oauth2.SetAuthURLParam("resource", "499b84ac-1321-427f-aa17-267ca6975798"),
oauth2.SetAuthURLParam("client_secret", c.ClientSecret),
)...,
)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexwilcox9 I am curious why we need the jwtConfigEntra? By default client_secret and client_id are added to the requests, so the only additional field here is resource.

In these docs: https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/manage-personal-access-tokens-via-api?view=azure-devops#configure-a-quickstart-application

Update the SCOPE configuration variable to "499b84ac-1321-427f-aa17-267ca6975798/.default" to refer to the Azure DevOps resource and all of its scopes.

It appears we can send this as a scope: export CODER_EXTERNAL_AUTH_2_SCOPES="vso.code_write,499b84ac-1321-427f-aa17-267ca6975798/.default"

I am having issues with my setup, so I don't have the resources to test this at present. Can you attempt to configure this without jwtConfigEntra just using the SCOPE env var I provided above?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Emyrk

When I try and remove these functions and just set the scope I start getting the wrong audience in my tokens. It defaults to MS Graph because the resource isn't set

Happy to try some more configurations if you have more ideas as it'd be nice to get rid of this extra function

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, if you are able to get it to work as I suggested with the scopes rather than the resource lmk.

Their docs are not the easiest to follow I find. If we can get this to work without the extra jwt struct, we can still pull in the sane defaults for entra.

Copy link
Contributor Author

@alexwilcox9 alexwilcox9 Feb 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right about the docs not being easy to follow.. I've been trying to find something official that says ADO only supports V1 tokens (which aren't the standard OIDC) but all I've got is the original Stack Overflow post that got me working in the first place
https://stackoverflow.com/questions/53788182/how-to-get-valid-aad-v2-token-using-msal-js-for-azure-devops

It looks like Azure DevOps is a v1.0 application

Then I found this article from Microsoft about the differences between v1 and v2 that shows resource is now scopes
https://learn.microsoft.com/en-us/previous-versions/azure/active-directory/azuread-dev/azure-ad-endpoint-comparison#scopes-not-resources

I haven't been able to find the official spec for the v1 authorize endpoint that would tell us if there was a valid way to use scopes

Hopefully ADO gets support for v2 tokens and this extra struct and functions can be removed.
I've removed the client_secret and client_id like you suggested and it still works so we've got that.

Is having this extra struct a blocker for getting this functionality?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extra struct is not a blocker, I just want to make sure it is necessary.

That stackoverflow link has an answer with this scope: 499b84ac-1321-427f-aa17-267ca6975798/user_impersonation. Worth trying that too?


To confirm though, with the struct everything is working for you? And this is specifically Entra + ADO? (I'm not very familiar with all microsoft's offerings). We should rename the struct to something that references the V1 api from Entra.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just tried setting the scope to 499b84ac-1321-427f-aa17-267ca6975798/user_impersonation and commented out the new functions and sadly it didn't work. The audience defaults back to MS Graph and the token isn't any good for ADO

I'll rename the struct/functions now to include V1 in the name and add a comment noting that it can be removed once ADO gets support for V2 tokens

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunate 😢

Can you cherry-pick this commit I made to make this dynamic?

d86efad

And do you know any good ValidateURL we could select?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I couldn't get cherry pick to work so just copied your code over, I'll have a google and see if I can find a validate URL

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Np on the code copy. Working across origins is a bit annoying for git actions.

@alexwilcox9 alexwilcox9 force-pushed the poc-entra-ado-auth branch 3 times, most recently from 83ef463 to e12e6e3 Compare February 29, 2024 14:40
Copy link
Member

@Emyrk Emyrk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can get in your approach 👍.

If in the future we figure out a way around this, or they implement the v2 apis, we can adjust from there.

CODER_EXTERNAL_AUTH_0_TOKEN_URL="https://login.microsoftonline.com/<TENANT ID>/oauth2/token"
```

> Note: Your app registration in Entra ID requires the `vso.code_write` scope
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this a default scope? I made it one in my commit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean by this? Are you saying replace vso.code_write with a default scope?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just checked, I did not make it a default scope in my commit. Whoops!

If this scope should always be set, we can include it in the defaults.

	defaults := codersdk.ExternalAuthConfig{
		DisplayName: "Azure DevOps (Entra)",
		DisplayIcon: "/icon/azure-devops.svg",
		Regex:       `^(https?://)?dev\.azure\.com(/.*)?$`,
		Scopes:      []string{"vso.code_write"},
	}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah that's not what I mean by my comment in the docs, I mean when you create the App Registration you have to give it vso.code_write in the Entra ID portal
image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! Understood, so there are no default scope recommendations on the Coder end?

Copy link
Contributor Author

@alexwilcox9 alexwilcox9 Feb 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right, as I understand it we just request the 'resource' and receive a token with all scopes that have been set in the app registration

@alexwilcox9 alexwilcox9 force-pushed the poc-entra-ado-auth branch 2 times, most recently from 1d8ce22 to be68ebd Compare February 29, 2024 16:32
@alexwilcox9
Copy link
Contributor Author

I had a dig into the endpoints to see if there was anything obvious we could use the validate that the token is working and so far all the endpoints that only require the vso.code_write scope require you to know the exact address of a project.
For example:
https://learn.microsoft.com/en-us/rest/api/azure/devops/git/repositories/list?view=azure-devops-rest-6.0&tabs=HTTP
https://dev.azure.com/{organization}/{project}/_apis/git/repositories?api-version=6.0

Alternatively the profile endpoint doesn't require that knowledge but does require an additional scope (vso.profile) being set on the app registration which is otherwise unused by Coder
https://learn.microsoft.com/en-us/rest/api/azure/devops/profile/profiles/get?view=azure-devops-rest-6.0&tabs=HTTP
https://app.vssps.visualstudio.com/_apis/profile/profiles/me

I hope that's useful! I'm about to go on holiday for a week so won't be getting back to this until w/c 11th March
Feel free to build on top of this if you need changes made in my absence.

Copy link
Member

@Emyrk Emyrk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving this 👍

Would be nice to find a validate url in the future, but that is not required.

@Emyrk Emyrk merged commit 320c2ea into coder:main Mar 4, 2024
@github-actions github-actions bot locked and limited conversation to collaborators Mar 4, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
community Pull Requests and issues created by the community.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants