Skip to content
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

Optionally allow OIDC password grant for CLI-based login experience #778

Merged
merged 22 commits into from Aug 24, 2021

Conversation

cfryanr
Copy link
Member

@cfryanr cfryanr commented Aug 12, 2021

This PR addresses #686 to support non-interactive password-based OIDC logins (e.g., from CI). It implements the proposed solution described by the comments of #686.

  • Add AllowPasswordGrant boolean field to OIDCIdentityProvider's spec
  • The oidc upstream watcher controller copies the value of AllowPasswordGrant into the configuration of the cached provider
  • Add password grant to the UpstreamOIDCIdentityProviderI interface which is implemented by the cached provider instance for use in the authorization endpoint
  • Enhance the IDP discovery endpoint to return the supported client "flows" for each IDP ("cli_password" and/or "browser_authcode")
  • Enhance pinniped get kubeconfig to help the user choose the desired flow for the selected IDP, and to write the flow into the resulting kubeconfg. The command offers a new --upstream-identity-provider-flow flag which can be used to resolve ambiguity when there is more than one client flow available for the selected upstream IDP.
  • Enhance pinniped login oidc to have a new --upstream-identity-provider-flow flag to tell it which client-side flow it should use for auth (CLI-based or browser-based) along with a client-determined default flow for each IDP type (browser for OIDC and CLI-based for LDAP)
  • Enhance the authorize endpoint to perform password grant when requested by the incoming headers. This commit does not include unit tests for the enhancements to the authorize endpoint, which will come in the next commit
  • Extract some shared helpers from the callback endpoint to share the code with the authorize endpoint
  • In the Dex config, allow the resource owner password grant, which Dex implements to also return ID tokens, for use in integration tests
  • Log the private debug message when logging Fosite errors
  • Add new integration tests. These depend on this Dex bug fix being included in the version of Dex used by the integration tests: Return valid JWT access token from password grant dexidp/dex#2234

Release note:

Adds `AllowPasswordGrant` boolean field to `OIDCIdentityProvider` spec.
If your OIDC provider server supports the OAuth 2.0 resource owner password credentials grant such that
it returns an ID token along with the usual other response values when the `openid` scope is requested,
then you may optionally choose to configure `AllowPasswordGrant` to `true` to allow clients to perform
this type of authentication. Clients will be prompted for their username and password on the
command-line without opening a browser window. See #686 for some discussion of scenarios where you
might like to enable or disable this capability.

- Add `AllowPasswordGrant` boolean field to OIDCIdentityProvider's spec
- The oidc upstream watcher controller copies the value of
  `AllowPasswordGrant` into the configuration of the cached provider
- Add password grant to the UpstreamOIDCIdentityProviderI interface
  which is implemented by the cached provider instance for use in the
  authorization endpoint
- Enhance the IDP discovery endpoint to return the supported "flows"
  for each IDP ("cli_password" and/or "browser_authcode")
- Enhance `pinniped get kubeconfig` to help the user choose the desired
  flow for the selected IDP, and to write the flow into the resulting
  kubeconfg
- Enhance `pinniped login oidc` to have a flow flag to tell it which
  client-side flow it should use for auth (CLI-based or browser-based)
- In the Dex config, allow the resource owner password grant, which Dex
  implements to also return ID tokens, for use in integration tests
- Enhance the authorize endpoint to perform password grant when
  requested by the incoming headers. This commit does not include unit
  tests for the enhancements to the authorize endpoint, which will come
  in the next commit
- Extract some shared helpers from the callback endpoint to share the
  code with the authorize endpoint
- Add new integration tests
@cfryanr cfryanr marked this pull request as draft August 12, 2021 18:09
After merging the new Kube 1.22 ExecCredential changes from main into
this feature branch, some of the new units test on this feature branch
needed to be update to account for the new ExecCredential "interactive"
field.
@codecov
Copy link

codecov bot commented Aug 12, 2021

Codecov Report

Merging #778 (3077034) into main (89cef2e) will increase coverage by 0.04%.
The diff coverage is 93.58%.

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #778      +/-   ##
==========================================
+ Coverage   79.37%   79.42%   +0.04%     
==========================================
  Files         127      127              
  Lines        8655     8676      +21     
==========================================
+ Hits         6870     6891      +21     
  Misses       1562     1562              
  Partials      223      223              
Impacted Files Coverage Δ
internal/oidc/oidc.go 0.00% <0.00%> (ø)
...nal/oidc/provider/dynamic_upstream_idp_provider.go 0.00% <ø> (ø)
internal/oidc/auth/auth_handler.go 96.95% <87.93%> (-3.05%) ⬇️
cmd/pinniped/cmd/kubeconfig.go 83.09% <100.00%> (+1.26%) ⬆️
cmd/pinniped/cmd/login_oidc.go 91.74% <100.00%> (+1.24%) ⬆️
...onfig/oidcupstreamwatcher/oidc_upstream_watcher.go 98.00% <100.00%> (+0.01%) ⬆️
internal/oidc/callback/callback_handler.go 100.00% <100.00%> (+5.94%) ⬆️
internal/oidc/discovery/discovery_handler.go 81.25% <100.00%> (+2.67%) ⬆️
...nternal/oidc/idpdiscovery/idp_discovery_handler.go 83.78% <100.00%> (+6.00%) ⬆️
internal/upstreamoidc/upstreamoidc.go 95.50% <100.00%> (+0.83%) ⬆️
... and 1 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 89cef2e...3077034. Read the comment docs.

@enj
Copy link
Contributor

enj commented Aug 12, 2021

Do you need to rebase on main? 69964fc should not be needed since #770 added 5678fc6.

@cfryanr
Copy link
Member Author

cfryanr commented Aug 13, 2021

Do you need to rebase on main? 69964fc should not be needed since #770 added 5678fc6.

I don't think so. See commit comment for that commit for details. TLDR: I think I needed those changes because I wrote new tests before I merged, so those new tests needed to be updated after the merge to be consistent with what's on main.

@cfryanr cfryanr marked this pull request as ready for review August 16, 2021 22:20
@cfryanr cfryanr changed the title WIP: Optionally allow OIDC password grant for CLI-based login experience Optionally allow OIDC password grant for CLI-based login experience Aug 16, 2021
// request flow with an OIDC identity provider. By default only the "openid" scope will be requested.
// request flow with an OIDC identity provider.
// In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes
// in addition to "openid" that will be requested as part of the token request (see also the AllowPasswordGrant field).
Copy link
Contributor

Choose a reason for hiding this comment

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

Generally you want to refer to the JSON tag.

Suggested change
// in addition to "openid" that will be requested as part of the token request (see also the AllowPasswordGrant field).
// in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field).

Copy link
Member Author

Choose a reason for hiding this comment

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

Will do.

Comment on lines 113 to 117
type pinnipedIDPResponse struct {
Name string `json:"name"`
Type string `json:"type"`
Name string `json:"name"`
Type string `json:"type"`
Flows []string `json:"flows,omitempty"`
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Please create a public struct in our apis package and share it between the client and server. This is part of our public API, just like our rest APIs.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is not a Kubernetes API, so we don't currently have a place to put it. Everything in the /apis directory is a .tmpl file for code generation including Kubernetes client-go code across all supported Kubernetes versions, which is not what's needed here.

We could make a new public package for this stuff. Maybe /pkg/supervisorapis or something like that?

Copy link
Member Author

Choose a reason for hiding this comment

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

I put it into the /apis directory as a .tmpl file and ran the code generator. It’s kind of strange to have a copy of the file per kube version when it is not needed, but it doesn’t hurt anything.

@@ -251,11 +251,17 @@ func FositeErrorForLog(err error) []interface{} {
rfc6749Error := fosite.ErrorToRFC6749Error(err)
Copy link
Contributor

Choose a reason for hiding this comment

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

We seem to be calling this FositeErrorForLog function at the Info level but Debug seems more appropriate. Also does this have the potential to leak any secrets into logs?

Copy link
Member Author

Choose a reason for hiding this comment

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

The use of Info predates this PR, but I think the reasoning for Info was trying to follow these logging guidelines:

// info should be reserved for "nice to know" information. It should be possible to run a production
// pinniped server at the info log level with no performance degradation due to high log volume.
// debug should be used for information targeted at developers and to aid in support cases. Care must
// be taken at this level to not leak any secrets into the log stream. That is, even though debug may
// cause performance issues in production, it must not cause security issues in production.

According to that guideline, Info is intended to be stuff that would be useful for regular users to see to help them debug their configuration, as opposed to Debug which is probably only useful for Pinniped contributors. Messages that could help you debug why your OIDCIdentityProvider is causing failed logins feels more like an Info log according to that guideline.

As far as I know, there should be no potential to leak secrets here under normal circumstances. Neither fosite error messages nor upstream OIDC/LDAP network responses should be putting secrets into error messages. The debug field inside of the Fosite error struct is intended to be things to log on the server without sending to the client, as opposed to the "hint" field which is returned to the client.


// There is no nonce to validate for a resource owner password credentials grant because it skips using
// the authorize endpoint and goes straight to the token endpoint.
skipNonceValidation := nonce.Nonce("")
Copy link
Contributor

Choose a reason for hiding this comment

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

nit:

Suggested change
skipNonceValidation := nonce.Nonce("")
const skipNonceValidation nonce.Nonce = ""

Copy link
Member Author

Choose a reason for hiding this comment

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

Will do.

internal/upstreamoidc/upstreamoidc_test.go Outdated Show resolved Hide resolved
Comment on lines 175 to 202
upstreamOIDCIdentityProvider := func() *oidctestutil.TestUpstreamOIDCIdentityProvider {
return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().
WithName(oidcUpstreamName).
WithClientID("some-client-id").
WithAuthorizationURL(*upstreamAuthURL).
WithScopes([]string{"scope1", "scope2"}). // the scopes to request when starting the upstream authorization flow
WithAllowPasswordGrant(false).
WithPasswordGrantError(errors.New("should not have used password grant on this instance")).
Build()
}

passwordGrantUpstreamOIDCIdentityProviderBuilder := func() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder {
return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().
WithName(oidcPasswordGrantUpstreamName).
WithClientID("some-client-id").
WithAuthorizationURL(*upstreamAuthURL).
WithScopes([]string{"scope1", "scope2"}). // the scopes to request when starting the upstream authorization flow
WithAllowPasswordGrant(false).
WithUsernameClaim(oidcUpstreamUsernameClaim).
WithGroupsClaim(oidcUpstreamGroupsClaim).
WithIDTokenClaim("iss", oidcUpstreamIssuer).
WithIDTokenClaim("sub", oidcUpstreamSubject).
WithIDTokenClaim(oidcUpstreamUsernameClaim, oidcUpstreamUsername).
WithIDTokenClaim(oidcUpstreamGroupsClaim, oidcUpstreamGroupMembership).
WithIDTokenClaim("other-claim", "should be ignored").
WithAllowPasswordGrant(true).
WithUpstreamAuthcodeExchangeError(errors.New("should not have tried to exchange upstream authcode on this instance"))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Please make regular functions for this kind of stuff. Over use of closures is an anti pattern from the spec tests.

Prefer the following approaches over closures:

func doSomething(t, a, b, c, d) { ... }
type foo struct { t, a, b, c, d }

func (f *foo) doSomething() { ... }

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree that over use of closures can make for confusing code, and I'm sure that I'm guilty of overdoing it in some of our older tests.

However, in this case every variable used inside the closures is either const or is effectively const (e.g. upstreamAuthURL which would be a const except that it needs to be the result of url.Parse), and the functions simply return new structs. Using a simple closure as a test helper to create a struct containing fake test data seems reasonable and helps keep each test in the test table brief and expressive.

Copy link
Contributor

Choose a reason for hiding this comment

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

keep each test in the test table brief and expressive

I think about this differently - I want the table test to be verbose and straightforward, because that is the general nature of table tests - repetition is both fine and generally preferred. So even in this case, a regular func passwordGrantUpstreamOIDCIdentityProviderBuilder(... params here...) is preferred over the closure. But a conversation for another day.

cmd/pinniped/cmd/kubeconfig_test.go Outdated Show resolved Hide resolved
cmd/pinniped/cmd/kubeconfig_test.go Show resolved Hide resolved
@enj enj enabled auto-merge August 24, 2021 16:24
@enj enj merged commit f7751d1 into main Aug 24, 2021
@enj enj deleted the oidc_password_grant branch August 24, 2021 17:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants