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
Conversation
- 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
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 Report
@@ 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
Continue to review full report at Codecov.
|
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. |
// 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). |
There was a problem hiding this comment.
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.
// 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). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do.
cmd/pinniped/cmd/kubeconfig.go
Outdated
type pinnipedIDPResponse struct { | ||
Name string `json:"name"` | ||
Type string `json:"type"` | ||
Name string `json:"name"` | ||
Type string `json:"type"` | ||
Flows []string `json:"flows,omitempty"` | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
pinniped/internal/plog/plog.go
Lines 13 to 17 in b5889f3
// 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("") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
skipNonceValidation := nonce.Nonce("") | |
const skipNonceValidation nonce.Nonce = "" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do.
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")) | ||
} |
There was a problem hiding this comment.
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() { ... }
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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.
AllowPasswordGrant
boolean field to OIDCIdentityProvider's specAllowPasswordGrant
into the configuration of the cached provider"cli_password"
and/or"browser_authcode"
)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.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)Release note: