alflokken / PSAuthClient

PowerShell OAuth2.0/OpenID Connect (OIDC) Client.
MIT License
56 stars 8 forks source link

Use with pregenerated auth URLs from OIDC RPs (e.g. HCP Vault) #3

Closed sean-r-williams closed 3 months ago

sean-r-williams commented 4 months ago

We're trying to authenticate to Hashicorp Vault using an OIDC provider (AAD in this case, but I reckon the exact OP/IdP isn't relevant).

The API login method for Vault (for OIDC clients) looks something like this:

This seems (from a naive perspective) to operate on the edge of what OIDC is designed for, since the client is neither generating nor performing the actual code-exchange (just issuing the auth code proper). Vault's own CLI handles this very well with vault login, but bundling a CLI binary alongside PowerShell scripts feels unwieldy.

Can you provide some clarity on whether this is even a supported scenario with PSAuthClient? Would feeding the data from this generated auth URL into Invoke-OAuth2AuthorizationEndpoint even function as expected, given the client isn't doing PKCE itself? Would feeding the extra PKCE challenge parameters in through -customParameters with -UsePkce:$false be a supported scenario for this?

alflokken commented 4 months ago

Hi Sean, I'm not familiar with Vault, but when using AAD as the identity provider, you'll likely need to obtain a JWT using your Azure AD app registration (client), then post this to the /auth/azure/login endpoint.

In order to use a pre-built authorization url it would probably be better to use Invoke-WebView2 directly (this function is not exported by default). Invoke-OAuth2AuthorizationEndpoint generates state/nonce values automatically, at least for now this would lead to duplicates if provided through customParameters.

sean-r-williams commented 4 months ago

@alflokken Thanks for the context.

Unfortunately, the azure login method is (AFAICT, based on this doc) specific to Managed Service Identity login - that is, a VM, Function app, etc. can call the IMDS to get a token, then send that to /auth/azure/login to authenticate as the VM itself.

We may be able to send an AAD-issued JWT directly to the JWT login endpoint (I think this defaults to /auth/jwt/login) since OIDC and JWT auth share some config elements, but the docs are pretty vague in this area so I'll need to test this in practice.

HDYF about adjusting Invoke-OAuth2AuthorizationEndpoint to support this scenario? I saw you mention "at least for now", so I'd like to understand whether you see this as something within the scope of this module.

I see two possible options for supporting this:

Happy to file a PR for either of these options if you see value in supporting a workflow like this.

alflokken commented 4 months ago

Although I'm uncertain about the specific use-case, and I secretly think that this might be what you are looking for. 😆 I do see the value of supporting the workflow, after all PSAuthClient is supposed to be a flexible client.

However, I would prefer to refactor how Invoke-OAuth2AuthorizationEndpoint constructs the request URI, giving priority to parameters specified in customParameters instead of adding any new parameters.

I appreciate your suggestion and the offer to submit a pull request. However, I prefer to handle the changes myself to maintain familiarity with the code base. I can't specify when I'll get to this due to my current workload, but I will add it to my to-do list. If you need this change urgently, please feel free to fork the repository.

sean-r-williams commented 4 months ago

this might be what you are looking for

In a way, you're absolutely right. The OIDC auth method in the Vault web UI (they appear to be using a locally-hosted Vault instance) will call the same auth_url endpoint on the API, then redirect the browser to that endpoint (with a redirect_uri back to oidc/callback directly).

The complication arises when inside a script/other client library abstracting Vault into another human-driven process. (In our case, we're importing certs into a KV engine with a specific format - we're writing scripts to parse/ingest the certificate without manually keying all of it into the UI.) For some reason, Vault API clients also use the same auth_url/callback mechanism instead of just sending an access token to a login endpoint. Vault's own client (in Go) supports this with vault login -method=oidc. The vault client launches a web browser, accepts the auth code from the OP via a localhost redirect_uri, then calls oidc/callback to exchange that auth_code for a Vault login token. You can see an equivalent Python implementation here.

I'll abstain from submitting a PR in that case - please let me know if you need help testing this scenario in the future.

alflokken commented 4 months ago

Thanks for providing details. As briefly mentioned, I think a quick fix could involve honoring 'state', 'nonce', 'code_challenge', and 'code_challenge_method' when provided in customParameters.

This would require you to parse your authorization URL and input values into the respective parameters of Invoke-OAuth2AuthorizationEndpoint. I believe this approach maintains the function's general purpose and idea, while still allowing flexibility for your scenario. However, it's important to note that you would not be able to send, for example, OIDC requests without a nonce or OAuth requests without a state. I'm not entirely sure how I want to address this and if it's useful at all. Perhaps it's better to leave it as is, since the code is already highly modifiable for anyone wanting to fork it.

Would this solve your problem?

sean-r-williams commented 3 months ago

I think a quick fix could involve honoring 'state', 'nonce', 'code_challenge', and 'code_challenge_method' when provided in customParameters.

Yes, I think that would be a suitable alternative.

However, it's important to note that you would not be able to send, for example, OIDC requests without a nonce or OAuth requests without a state. I'm not entirely sure how I want to address this

Conditionalizing lines 95 and 100 in Invoke-OAuth2AuthorizationEndpoint based on whether ($null -ne $customParameters) -and ($CustomParameters.ContainsKey("...")) is probably the lowest-friction method. This would only add in nonce/state if they're not already defined in the $customParameters hashtable.