pnp / cli-microsoft365

Manage Microsoft 365 and SharePoint Framework projects on any platform
https://aka.ms/cli-m365
MIT License
911 stars 319 forks source link

Signing in through a federated identity #5816

Open martinlingstuyl opened 8 months ago

martinlingstuyl commented 8 months ago

We all love managed identity, right? The super clean and safe route to authenticate without using credentials like certificates or secrets.

Federated identity has been around for a short while I believe, and concerns a route to use managed identity from other (non-Azure) systems. I believe it works by trusting authorization requests from trusted 'servers', like GitHub. Just today, I implemented this for publishing my own blog using GH actions to a storage account. It's very easy to set up there, using the azure CLI and an app registration.

Check out this interesting blog post around the topic as well: https://svrooij.io/2023/11/07/github-actions-federated-credentials-explained/

What I'd like for us to achieve is to have people deploy SPFx apps using these federated identities. Because that's super safe. It should not be hard, but we would need to add some new route to sign in. I believe we can choose two routes:

  1. Update the m365 login command with an additional authType: federatedIdentity. We should probably add some extra options as well, to make it possible to influence the token request.

  2. Update the GH cli login action with code to request the token from GitHub using federated identity. This will result in an access token. We should then add an additional way to set this access token for usage in the CLI. By using m365 login --authType accessToken, or some other route. I believe we discussed this before, not sure though what the conclusion was then.

However it is, it would be nice if we could support this way of authentication. The obvious scenario that's immediately useful, concerns deployment through GitHub, but possible other scenarios might show up before long, which is why my idea would to take route 1. We'd have to spec this out to see how it would work in detail. But if though to check first what you all think of this.

Implementation

I suggest we add a new authType: accessToken. Additionally, we add an --accessToken option, which is required when using this new authType.

When signing in using an access token, the user is responsible for retrieving the access token. Which is OK for scenario's like federated identity, where every implementation has different specifics.

The GitHub implementation of federated identity can then be added to the CLI GH login action. It will retrieve the access token for the user and feed it into the new --accessToken option.

Internally, we get the oid, tenant and audience from the access token, so we're able to set these on the active connection. We then save this auth state. The flow works a lot like how we do managed identity. the only difference is the access token is coming from outside the CLI, instead of being retrieved from within.

$token = #....code to get some access token using federated identity
m365 login --authType accessToken --accessToken $token

The --accessToken option will be a string array option. We can add multiple accesstokens at the same time, for example for SharePoint and for the Microsoft Graph. (--accessToken ey... --accessToken ey...)

waldekmastykarz commented 8 months ago

Cool idea! I suggest we start with a specific scenario (the one you mentioned is a great match) and see what it would take to get it to work with the CLI. If we could then take another scenario, it would help us ensure that whatever solution we choose is flexible enough to accommodate the different use cases.

martinlingstuyl commented 7 months ago

@waldekmastykarz There are a couple of options in Entra ID for federated credentials:

image

The idea is that you call the login.microsoftonline.com endpoint with a client assertion (check out the blog post from Stefan) I can see how that would work, the only challenge is coming up with a single route. Because we don't know how other services would generate the assertion. In the case of github, it's by calling a local Github URL with a specific GH token, a combination of two Environment variables can be used, together an Entra-defined audience: api://AzureADTokenExchange.

My proposition is that we would do something like the following:

m365 login --authType GitHub --audience "api://AzureADTokenExchange"

The --audience option would be optional, defaulting to the mentioned value, and only to be used when authType is GitHub. The login command would use the GH environment variables and the audience to get a client assertion, and then we call login.microsoftonline.com with it.

waldekmastykarz commented 7 months ago

I can see how that would work, the only challenge is coming up with a single route. Because we don't know how other services would generate the assertion. In the case of github, it's by calling a local Github URL with a specific GH token, a combination of two Environment variables can be used, together an Entra-defined audience: api://AzureADTokenExchange

On one hand this feels like something that we should leave out and simply let users pass the assertion into CLI. How they get to it, is up to them. It would allow for greater reusability across different services. The downside is, that it comes at the cost of complexity. On the other hand, if we implement support for GitHub, then we won't support other services like ADO, GitLab, etc. where folks might have similar needs.

Thoughts?

martinlingstuyl commented 7 months ago

Maybe we should go a different route: add the relevant GH-implementation to the cli login action, as that would be a more logical place I'm now thinking. As that's the only place the code is needed. We could let it retrieve an access token from entra id there.

We would then need a way to sign in based on just an access token in the CLI.

How about:

m365 login --authType accessToken --accessToken eyetc...
Adam-it commented 7 months ago

Sorry for coming late to the party. I love the idea and I think it is an awesome improvement that might support pipelines so I am all in on that.

What you @martinlingstuyl proposed to just extend GH actions to support this is some start but I think the first idea to I extend the login command and include this flexibility there would be a better approach since then we would also give the possibility to use it in ADO or other (next on my list is gitlab)

martinlingstuyl commented 7 months ago

Hi @Adam-it, we first need to decide the route we'll take. If we use the accessToken approach I'm mentioning above, we'll definitely have to implement it in the CLI first of course 😂

Adam-it commented 7 months ago

Hi @Adam-it, we first need to decide the route we'll take. If we use the accessToken approach I'm mentioning above, we'll definitely have to implement it in the CLI first of course 😂

Sure, Passing access token is very flexible 🙂. But I think till now CLI kinda helped obtaining it. What if we do both 🤔? So support passing the access token and also new auth type with audience?

martinlingstuyl commented 7 months ago

and also new auth type with audience?

What do you mean with this?

The whole point with federated identity is that I'm not sure how different scenarios will be able to be solved using a single route.

Therefore I'm suggesting we only support it using an access token that the user will have to acquire and supply. In the case of our cli GH action, we can arrange it for the user so he doesn't have to write code on that. I think this would be a flexible route that we need anyway.

Adam-it commented 7 months ago

ok gotcha 👍

martinlingstuyl commented 7 months ago

So what do you think of my idea @pnp/cli-for-microsoft-365-maintainers? Adding an accessToken signin flow?

waldekmastykarz commented 7 months ago

So what do you think of my idea @pnp/cli-for-microsoft-365-maintainers? Adding an accessToken signin flow?

I wonder if it will work. Our auth is implemented in such a way, that it automatically retrieves access token for other resources, or when the token expires. If all we've got is an access token, we can't do either one, so there's a chance running CLI will break and there's no way for you to recover.

martinlingstuyl commented 7 months ago

That's correct. So the user would have to know for what audience he would need a token. Requires a bit more background. PnP.PowerShell implemented this the same by the way. In the CLI this would mean you would not be able to request or refresh a token. But that's just the known drawback then.

waldekmastykarz commented 6 months ago

So the user would have to know for what audience he would need a token.

This is tricky, because right now, we don't document which APIs are used in which command. So while it's theoretically possible, in practice, it requires quite some preparation and validation from the user.

martinlingstuyl commented 6 months ago

This is tricky, because right now, we don't document which APIs are used in which command. So while it's theoretically possible, in practice, it requires quite some preparation and validation from the user.

Correct, it is... it's an advanced practice, but for that it opens interesting advanced scenarios as well. The question would be: is the scenario worth it.

Deploying through GH would be worth it would be my opinion.

waldekmastykarz commented 6 months ago

Let's think some more of how we could best support it. The ability to pass a single access token is the most obvious, but it comes with the limitation, that it would only work with commands that only use APIs from that one resource. If a command uses two resources, like Graph and SPO, you couldn't use them with this approach, so perhaps we need to consider a different approach, where we support passing multiple access tokens.

martinlingstuyl commented 6 months ago

I'm not sure how many commands use multiple API's, but I think your idea would be fine. We could pass multiple accessTokens indeed, like m365 login --accessToken ey... --accessToken ey...

martinlingstuyl commented 6 months ago

Ok @waldekmastykarz, I've added a remark on it being a string array option. I've picked the way we've discussed recently to handle array options. This was what we decided then I believe (right?) seems to me a great addition, this. How about we open this up?

martinlingstuyl commented 1 month ago

Let me see if I can build this one. It seems to me it would be useful...

martinlingstuyl commented 19 hours ago

Marking this as on hold in favor of investigating another route using Azure.Identity.