int128 / kubelogin

kubectl plugin for Kubernetes OpenID Connect authentication (kubectl oidc-login)
Apache License 2.0
1.76k stars 195 forks source link

Azure AD Single Page Application should include `Origin` header for CORS support #1048

Open adkafka opened 9 months ago

adkafka commented 9 months ago

Purpose of the feature (why)

We would like to use Azure AD Single Page Application with PKCE and no client secret. However, after configuring kubelogin for such a setup, we receive an error:

I0212 11:53:00.490607    8300 cmd.go:66] stacktrace: get-token: authentication error: authcode-browser error: authentication error: authorization code flow error: oauth2 error: could not exchange the code and token: oauth2: "invalid_request" "AADSTS9002327: Tokens issued for the 'Single-Page Application' client-type may only be redeemed via cross-origin requests. ..."

After digging a bit more, it seems that Azure requires the request to populate the Origin header for this flow to succeed. See:

For a related github issue, see https://github.com/int128/kubelogin/issues/858

Your idea (how)

I'm hoping that we can include a Origin header in these requests to resolve this error. A couple options to consider for this are: 1) Add CORS support in the HTTP client. I'm unsure how difficult this is. We can put this behind a feature flag if we would like. 2) Populate the Origin header manually in the HTTP client. We can either put this behind a feature flag, or just choose to add this header to all requests. 3) Support adding arbitrary HTTP headers as a CLI parameter (ie --extra-header="Origin: localhost:8000")

If we decide on a solution, I'm happy to take an initial pass at making the PR.

Workaround

This issue can be worked around by instead configuring the Azure Application to be a "Mobile and desktop applications". However, this requires us to include the "Client Secret" in all client's kubeconfig files, which is not desirable.

adkafka commented 9 months ago

Hmm, it appears I found a better workaround. If I remove the "Single Page Application" completely in Azure, and instead use a "Mobile and desktop applications", the Client Secret is not required. Simply adding the "Mobile and desktop applications" was not sufficient, I had to also remove the "Single Page Application". We can succeed simply with:

$ kubelogin get-token \
  --oidc-issuer-url "https://login.microsoftonline.com/<REDACTED>/v2.0" \
  --oidc-client-id <REDACTED> \
  --oidc-use-pkce \
  --force-refresh \
  --listen-address 127.0.0.1:8000

So, perhaps the guidance should be to use Mobile and desktop applications in Azure, and avoid Single Page Application completely. This is confusing, because the Azure docs only mention PKCE with Single Page Application.

adkafka commented 7 months ago

Revisiting this now... I learned recently that Azure AD applications of type Mobile and desktop applications are not in scope for Conditional Access Policies (see documentation here), meaning in my case they do not trigger an MFA enforcement. This has be coming back to this issue, because using Single Page Application DOES trigger an MFA enforcement, if we can make it work with kubelogin.

I confirmed that simply adding a header in the request solves this issue. Here is an example:

This command fails:

$ curl "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token" \
  -X POST \
  -d 'client_id=<client-id>&code=<authorization-code>&code_verifier=<verification-code>=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A8000'
...
{"error":"invalid_request","error_description":"AADSTS9002327: Tokens issued for the 'Single-Page Application' client-type may only be redeemed via cross-origin requests. Trace ID: <omitted> Correlation ID: <omitted> Timestamp: 2024-04-22 19:39:04Z","error_codes":[9002327],"timestamp":"2024-04-22 19:39:04Z","trace_id":"<omitted>","correlation_id":"<omitted>"}

This command succeeds (simply adding Origin: localhost:1800 as a header):

curl "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token" \
  -H 'Origin: localhost:1800' \
  -X POST \
  -d 'client_id=<client-id>&code=<authorization-code>&code_verifier=<verification-code>=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A8000'
...
{"token_type":"Bearer","scope":"email openid profile User.Read","expires_in":4422,"ext_expires_in":4422,"access_token"...

Therefore, I would like to bring this issue back up for consideration. I'm happy to make a PR after receiving guidance for how best to proceed.

adkafka commented 7 months ago

In order to implement this code, I think we need to implement the change 2 libraries upstream. This library uses https://github.com/int128/oauth2cli/tree/master which in turn uses https://github.com/golang/oauth2/tree/master. The logic to actually request the token is here: https://github.com/golang/oauth2/blob/master/internal/token.go#L183-L202. Plumbing through the call back URL as a parameter seems like a relatively large lift in this case. We do have a context.Context object that gets passed through though. Maybe we could use that to set this header?