Small refactors and comments for LDAP/AD UI

This commit is contained in:
Ryan Richard 2022-05-19 16:02:08 -07:00
parent 0f2a984308
commit 39fd9ba270
13 changed files with 64 additions and 42 deletions

View File

@ -922,7 +922,9 @@ func selectUpstreamIDPFlow(discoveredIDPFlows []idpdiscoveryv1alpha1.IDPFlow, se
return discoveredIDPFlows[0], nil return discoveredIDPFlows[0], nil
default: default:
// The user did not specify a flow, and more than one was found. // The user did not specify a flow, and more than one was found.
log.Info("multiple client flows found, selecting first value as default: "+discoveredIDPFlows[0].String(), "idpName", selectedIDPName, "idpType", selectedIDPType) log.Info("multiple client flows found, selecting first value as default",
"idpName", selectedIDPName, "idpType", selectedIDPType,
"selectedFlow", discoveredIDPFlows[0].String(), "availableFlows", discoveredIDPFlows)
return discoveredIDPFlows[0], nil return discoveredIDPFlows[0], nil
} }
} }

View File

@ -1305,7 +1305,8 @@ func TestGetKubeconfig(t *testing.T) {
base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) base64.StdEncoding.EncodeToString([]byte(issuerCABundle)))
}, },
wantLogs: func(_ string, _ string) []string { wantLogs: func(_ string, _ string) []string {
return []string{"\"level\"=0 \"msg\"=\"multiple client flows found, selecting first value as default: cli_password\" \"idpName\"=\"some-ldap-idp\" \"idpType\"=\"ldap\""} return []string{`"level"=0 "msg"="multiple client flows found, selecting first value as default" ` +
`"availableFlows"=["cli_password","flow2"] "idpName"="some-ldap-idp" "idpType"="ldap" "selectedFlow"="cli_password"`}
}, },
}, },
{ {

View File

@ -159,7 +159,7 @@ func handleAuthRequestForLDAPUpstreamBrowserFlow(
upstreamStateEncoder oidc.Encoder, upstreamStateEncoder oidc.Encoder,
cookieCodec oidc.Codec, cookieCodec oidc.Codec,
) error { ) error {
encodedStateParamValue, _, _, err := handleBrowserAuthRequest( authRequestState, err := handleBrowserFlowAuthRequest(
r, r,
w, w,
oauthHelper, oauthHelper,
@ -174,11 +174,12 @@ func handleAuthRequestForLDAPUpstreamBrowserFlow(
if err != nil { if err != nil {
return err return err
} }
if encodedStateParamValue == "" { if authRequestState == nil {
// There was an error but handleBrowserFlowAuthRequest() already took care of writing the response for it.
return nil return nil
} }
return login.RedirectToLoginPage(r, w, downstreamIssuer, encodedStateParamValue, login.ShowNoError) return login.RedirectToLoginPage(r, w, downstreamIssuer, authRequestState.encodedStateParam, login.ShowNoError)
} }
func handleAuthRequestForOIDCUpstreamPasswordGrant( func handleAuthRequestForOIDCUpstreamPasswordGrant(
@ -255,7 +256,7 @@ func handleAuthRequestForOIDCUpstreamBrowserFlow(
upstreamStateEncoder oidc.Encoder, upstreamStateEncoder oidc.Encoder,
cookieCodec oidc.Codec, cookieCodec oidc.Codec,
) error { ) error {
encodedStateParamValue, pkceValue, nonceValue, err := handleBrowserAuthRequest( authRequestState, err := handleBrowserFlowAuthRequest(
r, r,
w, w,
oauthHelper, oauthHelper,
@ -270,7 +271,8 @@ func handleAuthRequestForOIDCUpstreamBrowserFlow(
if err != nil { if err != nil {
return err return err
} }
if encodedStateParamValue == "" { if authRequestState == nil {
// There was an error but handleBrowserFlowAuthRequest() already took care of writing the response for it.
return nil return nil
} }
@ -284,9 +286,9 @@ func handleAuthRequestForOIDCUpstreamBrowserFlow(
} }
authCodeOptions := []oauth2.AuthCodeOption{ authCodeOptions := []oauth2.AuthCodeOption{
nonceValue.Param(), authRequestState.nonce.Param(),
pkceValue.Challenge(), authRequestState.pkce.Challenge(),
pkceValue.Method(), authRequestState.pkce.Method(),
} }
for key, val := range oidcUpstream.GetAdditionalAuthcodeParams() { for key, val := range oidcUpstream.GetAdditionalAuthcodeParams() {
@ -295,7 +297,7 @@ func handleAuthRequestForOIDCUpstreamBrowserFlow(
http.Redirect(w, r, http.Redirect(w, r,
upstreamOAuthConfig.AuthCodeURL( upstreamOAuthConfig.AuthCodeURL(
encodedStateParamValue, authRequestState.encodedStateParam,
authCodeOptions..., authCodeOptions...,
), ),
http.StatusSeeOther, // match fosite and https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11 http.StatusSeeOther, // match fosite and https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11
@ -387,10 +389,21 @@ func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider
} }
} }
// handleBrowserAuthRequest performs the shared validations and setup between browser based auth requests type browserFlowAuthRequestState struct {
// regardless of IDP type-- LDAP, Active Directory and OIDC. encodedStateParam string
pkce pkce.Code
nonce nonce.Nonce
}
// handleBrowserFlowAuthRequest performs the shared validations and setup between browser based
// auth requests regardless of IDP type-- LDAP, Active Directory and OIDC.
// It generates the state param, sets the CSRF cookie, and validates the prompt param. // It generates the state param, sets the CSRF cookie, and validates the prompt param.
func handleBrowserAuthRequest( // It returns an error when it encounters an error without handling it, leaving it to
// the caller to decide how to handle it.
// It returns nil with no error when it encounters an error and also has already handled writing
// the error response to the ResponseWriter, in which case the caller should not also try to
// write the error response.
func handleBrowserFlowAuthRequest(
r *http.Request, r *http.Request,
w http.ResponseWriter, w http.ResponseWriter,
oauthHelper fosite.OAuth2Provider, oauthHelper fosite.OAuth2Provider,
@ -401,10 +414,10 @@ func handleBrowserAuthRequest(
idpType psession.ProviderType, idpType psession.ProviderType,
cookieCodec oidc.Codec, cookieCodec oidc.Codec,
upstreamStateEncoder oidc.Encoder, upstreamStateEncoder oidc.Encoder,
) (string, pkce.Code, nonce.Nonce, error) { ) (*browserFlowAuthRequestState, error) {
authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper, false) authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper, false)
if !created { if !created {
return "", "", "", nil return nil, nil // already wrote the error response, don't return error
} }
now := time.Now() now := time.Now()
@ -420,13 +433,13 @@ func handleBrowserAuthRequest(
}) })
if err != nil { if err != nil {
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, err, false) oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, err, false)
return "", "", "", nil return nil, nil // already wrote the error response, don't return error
} }
csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE) csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE)
if err != nil { if err != nil {
plog.Error("authorize generate error", err) plog.Error("authorize generate error", err)
return "", "", "", err return nil, err
} }
csrfFromCookie := readCSRFCookie(r, cookieCodec) csrfFromCookie := readCSRFCookie(r, cookieCodec)
if csrfFromCookie != "" { if csrfFromCookie != "" {
@ -444,13 +457,13 @@ func handleBrowserAuthRequest(
) )
if err != nil { if err != nil {
plog.Error("authorize upstream state param error", err) plog.Error("authorize upstream state param error", err)
return "", "", "", err return nil, err
} }
promptParam := r.Form.Get(promptParamName) promptParam := r.Form.Get(promptParamName)
if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) { if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) {
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired, false) oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired, false)
return "", "", "", nil return nil, nil // already wrote the error response, don't return error
} }
if csrfFromCookie == "" { if csrfFromCookie == "" {
@ -458,10 +471,15 @@ func handleBrowserAuthRequest(
err = addCSRFSetCookieHeader(w, csrfValue, cookieCodec) err = addCSRFSetCookieHeader(w, csrfValue, cookieCodec)
if err != nil { if err != nil {
plog.Error("error setting CSRF cookie", err) plog.Error("error setting CSRF cookie", err)
return "", "", "", err return nil, err
} }
} }
return encodedStateParamValue, pkceValue, nonceValue, nil
return &browserFlowAuthRequestState{
encodedStateParam: encodedStateParamValue,
pkce: pkceValue,
nonce: nonceValue,
}, nil
} }
func generateValues( func generateValues(

View File

@ -15,12 +15,12 @@ const (
incorrectUsernameOrPasswordErrorMessage = "Incorrect username or password." incorrectUsernameOrPasswordErrorMessage = "Incorrect username or password."
) )
func NewGetHandler() HandlerFunc { func NewGetHandler(loginPath string) HandlerFunc {
return func(w http.ResponseWriter, r *http.Request, encodedState string, decodedState *oidc.UpstreamStateParamData) error { return func(w http.ResponseWriter, r *http.Request, encodedState string, decodedState *oidc.UpstreamStateParamData) error {
alertMessage, hasAlert := getAlert(r) alertMessage, hasAlert := getAlert(r)
pageInputs := &loginhtml.PageData{ pageInputs := &loginhtml.PageData{
PostPath: r.URL.Path, // the path for POST is the same as for GET PostPath: loginPath,
State: encodedState, State: encodedState,
IDPName: decodedState.UpstreamName, IDPName: decodedState.UpstreamName,
HasAlertError: hasAlert, HasAlertError: hasAlert,

View File

@ -96,7 +96,7 @@ func TestGetLogin(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
handler := NewGetHandler() handler := NewGetHandler(testPath)
target := testPath + "?state=" + tt.encodedState target := testPath + "?state=" + tt.encodedState
if tt.errParam != "" { if tt.errParam != "" {
target += "&err=" + tt.errParam target += "&err=" + tt.errParam

View File

@ -94,6 +94,9 @@ func wrapSecurityHeaders(handler http.Handler) http.Handler {
}) })
} }
// RedirectToLoginPage redirects to the GET /login page of the specified issuer.
// The specified issuer should never end with a "/", which is validated by
// provider.FederationDomainIssuer when the issuer string comes from that type.
func RedirectToLoginPage( func RedirectToLoginPage(
r *http.Request, r *http.Request,
w http.ResponseWriter, w http.ResponseWriter,

View File

@ -18,7 +18,7 @@ import (
var ( var (
//go:embed login_form.css //go:embed login_form.css
rawCSS string rawCSS string
minifiedCSS = mustMinify(minify.CSS(rawCSS)) minifiedCSS = panicOnError(minify.CSS(rawCSS))
//go:embed login_form.gohtml //go:embed login_form.gohtml
rawHTMLTemplate string rawHTMLTemplate string
@ -26,7 +26,7 @@ var (
// Parse the Go templated HTML and inject functions providing the minified inline CSS and JS. // Parse the Go templated HTML and inject functions providing the minified inline CSS and JS.
var parsedHTMLTemplate = template.Must(template.New("login_form.gohtml").Funcs(template.FuncMap{ var parsedHTMLTemplate = template.Must(template.New("login_form.gohtml").Funcs(template.FuncMap{
"minifiedCSS": func() template.CSS { return template.CSS(minifiedCSS) }, "minifiedCSS": func() template.CSS { return template.CSS(CSS()) },
}).Parse(rawHTMLTemplate)) }).Parse(rawHTMLTemplate))
// Generate the CSP header value once since it's effectively constant. // Generate the CSP header value once since it's effectively constant.
@ -36,7 +36,7 @@ var cspValue = strings.Join([]string{
`frame-ancestors 'none'`, `frame-ancestors 'none'`,
}, "; ") }, "; ")
func mustMinify(s string, err error) string { func panicOnError(s string, err error) string {
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -63,6 +63,6 @@ func TestCSS(t *testing.T) {
} }
func TestHelpers(t *testing.T) { func TestHelpers(t *testing.T) {
require.Equal(t, "test", mustMinify("test", nil)) require.Equal(t, "test", panicOnError("test", nil))
require.PanicsWithError(t, "some error", func() { mustMinify("", fmt.Errorf("some error")) }) require.PanicsWithError(t, "some error", func() { panicOnError("", fmt.Errorf("some error")) })
} }

View File

@ -43,6 +43,9 @@ const (
// Just in case we need to make a breaking change to the format of the upstream state param, // Just in case we need to make a breaking change to the format of the upstream state param,
// we are including a format version number. This gives the opportunity for a future version of Pinniped // we are including a format version number. This gives the opportunity for a future version of Pinniped
// to have the consumer of this format decide to reject versions that it doesn't understand. // to have the consumer of this format decide to reject versions that it doesn't understand.
//
// Version 1 was the original version.
// Version 2 added the UpstreamType field to the UpstreamStateParamData struct.
UpstreamStateParamFormatVersion = "2" UpstreamStateParamFormatVersion = "2"
// The `name` passed to the encoder for encoding the upstream state param value. This name is short // The `name` passed to the encoder for encoding the upstream state param value. This name is short

View File

@ -18,11 +18,11 @@ import (
var ( var (
//go:embed form_post.css //go:embed form_post.css
rawCSS string rawCSS string
minifiedCSS = mustMinify(minify.CSS(rawCSS)) minifiedCSS = panicOnError(minify.CSS(rawCSS))
//go:embed form_post.js //go:embed form_post.js
rawJS string rawJS string
minifiedJS = mustMinify(minify.JS(rawJS)) minifiedJS = panicOnError(minify.JS(rawJS))
//go:embed form_post.gohtml //go:embed form_post.gohtml
rawHTMLTemplate string rawHTMLTemplate string
@ -44,7 +44,7 @@ var cspValue = strings.Join([]string{
`frame-ancestors 'none'`, `frame-ancestors 'none'`,
}, "; ") }, "; ")
func mustMinify(s string, err error) string { func panicOnError(s string, err error) string {
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -93,6 +93,6 @@ func TestContentSecurityPolicyHashes(t *testing.T) {
} }
func TestHelpers(t *testing.T) { func TestHelpers(t *testing.T) {
require.Equal(t, "test", mustMinify("test", nil)) require.Equal(t, "test", panicOnError("test", nil))
require.PanicsWithError(t, "some error", func() { mustMinify("", fmt.Errorf("some error")) }) require.PanicsWithError(t, "some error", func() { panicOnError("", fmt.Errorf("some error")) })
} }

View File

@ -8,8 +8,6 @@ import (
"strings" "strings"
"sync" "sync"
"go.pinniped.dev/internal/oidc/login"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1" corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
"go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc"
@ -20,6 +18,7 @@ import (
"go.pinniped.dev/internal/oidc/dynamiccodec" "go.pinniped.dev/internal/oidc/dynamiccodec"
"go.pinniped.dev/internal/oidc/idpdiscovery" "go.pinniped.dev/internal/oidc/idpdiscovery"
"go.pinniped.dev/internal/oidc/jwks" "go.pinniped.dev/internal/oidc/jwks"
"go.pinniped.dev/internal/oidc/login"
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/oidc/token" "go.pinniped.dev/internal/oidc/token"
"go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/plog"
@ -139,7 +138,7 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs
m.providerHandlers[(issuerHostWithPath + oidc.PinnipedLoginPath)] = login.NewHandler( m.providerHandlers[(issuerHostWithPath + oidc.PinnipedLoginPath)] = login.NewHandler(
upstreamStateEncoder, upstreamStateEncoder,
csrfCookieEncoder, csrfCookieEncoder,
login.NewGetHandler(), login.NewGetHandler(incomingProvider.IssuerPath()+oidc.PinnipedLoginPath),
login.NewPostHandler(issuer, m.upstreamIDPs, oauthHelperWithKubeStorage), login.NewPostHandler(issuer, m.upstreamIDPs, oauthHelperWithKubeStorage),
) )

View File

@ -118,10 +118,6 @@ stringData:
clientSecret: "<your-client-secret>" clientSecret: "<your-client-secret>"
``` ```
Note that the `metadata.name` of the OIDCIdentityProvider resource may be visible to end users at login prompts
if you choose to enable `allowPasswordGrant`, so choose a name which will be understood by your end users.
For example, if you work at Acme Corp, choose something like `acme-corporate-workspace-one` over `my-idp`.
The following claims are returned by Workspace ONE Access. The `group` scope is required to use the The following claims are returned by Workspace ONE Access. The `group` scope is required to use the
`group_ids` and `group_names` claims. The `email` scope is required to use the `email` claim. The `group_ids` and `group_names` claims. The `email` scope is required to use the `email` claim. The
remaining claims are always available. remaining claims are always available.