Implement post_login_handler.go to accept form post and auth to LDAP/AD

Also extract some helpers from auth_handler.go so they can be shared
with the new handler.
This commit is contained in:
Ryan Richard 2022-04-29 16:01:51 -07:00
parent 646c6ec9ed
commit 69e5169fc5
8 changed files with 1069 additions and 214 deletions

View File

@ -7,24 +7,21 @@ package auth
import (
"fmt"
"net/http"
"net/url"
"time"
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/felixge/httpsnoop"
"github.com/ory/fosite"
"github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/token/jwt"
"github.com/pkg/errors"
"golang.org/x/oauth2"
supervisoroidc "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
"go.pinniped.dev/internal/authenticators"
"go.pinniped.dev/internal/httputil/httperr"
"go.pinniped.dev/internal/httputil/securityheader"
"go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/oidc/csrftoken"
"go.pinniped.dev/internal/oidc/downstreamsession"
"go.pinniped.dev/internal/oidc/login"
"go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/psession"
@ -127,36 +124,19 @@ func handleAuthRequestForLDAPUpstreamCLIFlow(
return httperr.New(http.StatusBadGateway, "unexpected error during upstream authentication")
}
if !authenticated {
return writeAuthorizeError(w, oauthHelper, authorizeRequester,
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithHintf("Username/password not accepted by LDAP provider."), true)
return nil
}
subject := downstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse)
subject := downstreamsession.DownstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse)
username = authenticateResponse.User.GetName()
groups := authenticateResponse.User.GetGroups()
dn := authenticateResponse.DN
customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse)
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData)
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, true)
customSessionData := &psession.CustomSessionData{
ProviderUID: ldapUpstream.GetResourceUID(),
ProviderName: ldapUpstream.GetName(),
ProviderType: idpType,
}
if idpType == psession.ProviderTypeLDAP {
customSessionData.LDAP = &psession.LDAPSessionData{
UserDN: dn,
ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes,
}
}
if idpType == psession.ProviderTypeActiveDirectory {
customSessionData.ActiveDirectory = &psession.ActiveDirectorySessionData{
UserDN: dn,
ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes,
}
}
return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w,
oauthHelper, authorizeRequester, subject, username, groups, customSessionData)
return nil
}
func handleAuthRequestForLDAPUpstreamBrowserFlow(
@ -191,20 +171,7 @@ func handleAuthRequestForLDAPUpstreamBrowserFlow(
return nil
}
loginURL, err := url.Parse(downstreamIssuer + "/login")
if err != nil {
return err
}
q := loginURL.Query()
q.Set("state", encodedStateParamValue)
loginURL.RawQuery = q.Encode()
http.Redirect(w, r,
loginURL.String(),
http.StatusSeeOther, // match fosite and https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11
)
return nil
return login.RedirectToLoginPage(r, w, downstreamIssuer, encodedStateParamValue, login.ShowNoError)
}
func handleAuthRequestForOIDCUpstreamPasswordGrant(
@ -225,9 +192,10 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant(
if !oidcUpstream.AllowsPasswordGrant() {
// Return a user-friendly error for this case which is entirely within our control.
return writeAuthorizeError(w, oauthHelper, authorizeRequester,
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithHint(
"Resource owner password credentials grant is not allowed for this upstream provider according to its configuration."), true)
return nil
}
token, err := oidcUpstream.PasswordCredentialsGrantAndValidateTokens(r.Context(), username, password)
@ -239,26 +207,33 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant(
// However, the exact response is undefined in the sense that there is no such thing as a password grant in
// the OIDC spec, so we don't try too hard to read the upstream errors in this case. (E.g. Dex departs from the
// spec and returns something other than an "invalid_grant" error for bad resource owner credentials.)
return writeAuthorizeError(w, oauthHelper, authorizeRequester,
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithDebug(err.Error()), true) // WithDebug hides the error from the client
return nil
}
subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(oidcUpstream, token.IDToken.Claims)
if err != nil {
// Return a user-friendly error for this case which is entirely within our control.
return writeAuthorizeError(w, oauthHelper, authorizeRequester,
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), true,
)
return nil
}
customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(oidcUpstream, token)
if err != nil {
return writeAuthorizeError(w, oauthHelper, authorizeRequester,
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), true,
)
return nil
}
return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, subject, username, groups, customSessionData)
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData)
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, true)
return nil
}
func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
@ -322,78 +297,11 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
return nil
}
func writeAuthorizeError(w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester, err error, isBrowserless bool) error {
if plog.Enabled(plog.LevelTrace) {
// When trace level logging is enabled, include the stack trace in the log message.
keysAndValues := oidc.FositeErrorForLog(err)
errWithStack := errors.WithStack(err)
keysAndValues = append(keysAndValues, "errWithStack")
// klog always prints error values using %s, which does not include stack traces,
// so convert the error to a string which includes the stack trace here.
keysAndValues = append(keysAndValues, fmt.Sprintf("%+v", errWithStack))
plog.Trace("authorize response error", keysAndValues...)
} else {
plog.Info("authorize response error", oidc.FositeErrorForLog(err)...)
}
if isBrowserless {
w = rewriteStatusSeeOtherToStatusFoundForBrowserless(w)
}
// Return an error according to OIDC spec 3.1.2.6 (second paragraph).
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
return nil
}
func makeDownstreamSessionAndReturnAuthcodeRedirect(
r *http.Request,
w http.ResponseWriter,
oauthHelper fosite.OAuth2Provider,
authorizeRequester fosite.AuthorizeRequester,
subject string,
username string,
groups []string,
customSessionData *psession.CustomSessionData,
) error {
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData)
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
if err != nil {
return writeAuthorizeError(w, oauthHelper, authorizeRequester, err, true)
}
w = rewriteStatusSeeOtherToStatusFoundForBrowserless(w)
oauthHelper.WriteAuthorizeResponse(w, authorizeRequester, authorizeResponder)
return nil
}
func rewriteStatusSeeOtherToStatusFoundForBrowserless(w http.ResponseWriter) http.ResponseWriter {
// rewrite http.StatusSeeOther to http.StatusFound for backwards compatibility with old pinniped CLIs.
// we can drop this in a few releases once we feel enough time has passed for users to update.
//
// WriteAuthorizeResponse/WriteAuthorizeError calls used to result in http.StatusFound until
// https://github.com/ory/fosite/pull/636 changed it to http.StatusSeeOther to address
// https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11
// Safari has the bad behavior in the case of http.StatusFound and not just http.StatusTemporaryRedirect.
//
// in the browserless flows, the OAuth client is the pinniped CLI and it already has access to the user's
// password. Thus there is no security issue with using http.StatusFound vs. http.StatusSeeOther.
return httpsnoop.Wrap(w, httpsnoop.Hooks{
WriteHeader: func(delegate httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc {
return func(code int) {
if code == http.StatusSeeOther {
code = http.StatusFound
}
delegate(code)
}
},
})
}
func requireNonEmptyUsernameAndPasswordHeaders(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester) (string, string, bool) {
username := r.Header.Get(supervisoroidc.AuthorizeUsernameHeaderName)
password := r.Header.Get(supervisoroidc.AuthorizePasswordHeaderName)
if username == "" || password == "" {
_ = writeAuthorizeError(w, oauthHelper, authorizeRequester,
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithHintf("Missing or blank username or password."), true)
return "", "", false
}
@ -403,7 +311,7 @@ func requireNonEmptyUsernameAndPasswordHeaders(r *http.Request, w http.ResponseW
func newAuthorizeRequest(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, isBrowserless bool) (fosite.AuthorizeRequester, bool) {
authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), r)
if err != nil {
_ = writeAuthorizeError(w, oauthHelper, authorizeRequester, err, isBrowserless)
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, err, isBrowserless)
return nil, false
}
@ -435,7 +343,8 @@ func readCSRFCookie(r *http.Request, codec oidc.Decoder) csrftoken.CSRFToken {
return csrfFromCookie
}
// Select either an OIDC, an LDAP or an AD IDP, or return an error.
// chooseUpstreamIDP selects either an OIDC, an LDAP, or an AD IDP, or returns an error.
// Note that AD and LDAP IDPs both return the same interface type, but different ProviderTypes values.
func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider.UpstreamOIDCIdentityProviderI, provider.UpstreamLDAPIdentityProviderI, psession.ProviderType, error) {
oidcUpstreams := idpLister.GetOIDCIdentityProviders()
ldapUpstreams := idpLister.GetLDAPIdentityProviders()
@ -503,7 +412,8 @@ func handleBrowserAuthRequest(
},
})
if err != nil {
return "", "", "", writeAuthorizeError(w, oauthHelper, authorizeRequester, err, false)
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, err, false)
return "", "", "", nil
}
csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE)
@ -532,7 +442,8 @@ func handleBrowserAuthRequest(
promptParam := r.Form.Get(promptParamName)
if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) {
return "", "", "", writeAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired, false)
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired, false)
return "", "", "", nil
}
if csrfFromCookie == "" {
@ -608,8 +519,3 @@ func addCSRFSetCookieHeader(w http.ResponseWriter, csrfValue csrftoken.CSRFToken
return nil
}
func downstreamSubjectFromUpstreamLDAP(ldapUpstream provider.UpstreamLDAPIdentityProviderI, authenticateResponse *authenticators.Response) string {
ldapURL := *ldapUpstream.GetURL()
return downstreamsession.DownstreamLDAPSubject(authenticateResponse.User.GetUID(), ldapURL)
}

View File

@ -70,6 +70,8 @@ func TestAuthorizationEndpoint(t *testing.T) {
downstreamClientID = "pinniped-cli"
upstreamLDAPURL = "ldaps://some-ldap-host:123?base=ou%3Dusers%2Cdc%3Dpinniped%2Cdc%3Ddev"
htmlContentType = "text/html; charset=utf-8"
jsonContentType = "application/json; charset=utf-8"
formContentType = "application/x-www-form-urlencoded"
)
require.Len(t, happyState, 8, "we expect fosite to allow 8 byte state params, so we want to test that boundary case")
@ -718,7 +720,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
cookieEncoder: happyCookieEncoder,
method: http.MethodPost,
path: "/some/path",
contentType: "application/x-www-form-urlencoded",
contentType: formContentType,
body: encodeQuery(happyGetRequestQueryMap),
wantStatus: http.StatusSeeOther,
wantContentType: "",
@ -737,7 +739,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
cookieEncoder: happyCookieEncoder,
method: http.MethodPost,
path: "/some/path",
contentType: "application/x-www-form-urlencoded",
contentType: formContentType,
body: encodeQuery(happyGetRequestQueryMap),
wantStatus: http.StatusSeeOther,
wantContentType: "",
@ -756,7 +758,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
cookieEncoder: happyCookieEncoder,
method: http.MethodPost,
path: "/some/path",
contentType: "application/x-www-form-urlencoded",
contentType: formContentType,
body: encodeQuery(happyGetRequestQueryMap),
wantStatus: http.StatusSeeOther,
wantContentType: "",
@ -770,7 +772,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
method: http.MethodPost,
path: "/some/path",
contentType: "application/x-www-form-urlencoded",
contentType: formContentType,
body: encodeQuery(happyGetRequestQueryMap),
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
@ -794,7 +796,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
method: http.MethodPost,
path: "/some/path",
contentType: "application/x-www-form-urlencoded",
contentType: formContentType,
body: encodeQuery(happyGetRequestQueryMap),
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
@ -817,7 +819,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodPost,
path: "/some/path",
contentType: "application/x-www-form-urlencoded",
contentType: formContentType,
body: encodeQuery(happyGetRequestQueryMap),
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
@ -845,7 +847,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
cookieEncoder: happyCookieEncoder,
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "login"}),
contentType: "application/x-www-form-urlencoded",
contentType: formContentType,
body: encodeQuery(happyGetRequestQueryMap),
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
@ -864,7 +866,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
cookieEncoder: happyCookieEncoder,
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "login"}),
contentType: "application/x-www-form-urlencoded",
contentType: formContentType,
body: encodeQuery(happyGetRequestQueryMap),
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
@ -883,10 +885,10 @@ func TestAuthorizationEndpoint(t *testing.T) {
cookieEncoder: happyCookieEncoder,
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none"}),
contentType: "application/x-www-form-urlencoded",
contentType: formContentType,
body: encodeQuery(happyGetRequestQueryMap),
wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeLoginRequiredErrorQuery),
wantBodyString: "",
},
@ -1113,7 +1115,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
Password: "wrong-password",
}},
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedErrorQuery),
wantBodyString: "",
},
@ -1125,7 +1127,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr("wrong-password"),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery),
wantBodyString: "",
},
@ -1137,7 +1139,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr("wrong-password"),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery),
wantBodyString: "",
},
@ -1149,7 +1151,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr("wrong-username"),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery),
wantBodyString: "",
},
@ -1161,7 +1163,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr("wrong-username"),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery),
wantBodyString: "",
},
@ -1173,7 +1175,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: nil, // do not send header
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery),
wantBodyString: "",
},
@ -1185,7 +1187,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: nil, // do not send header
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery),
wantBodyString: "",
},
@ -1197,7 +1199,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: nil, // do not send header
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery),
wantBodyString: "",
},
@ -1209,7 +1211,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: nil, // do not send header
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery),
wantBodyString: "",
},
@ -1221,7 +1223,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: nil, // do not send header
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery),
wantBodyString: "",
},
@ -1234,7 +1236,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUserInfoEndpointErrorQuery),
wantBodyString: "",
},
@ -1247,7 +1249,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUserInfoEndpointErrorQuery),
wantBodyString: "",
},
@ -1260,7 +1262,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery),
wantBodyString: "",
},
@ -1273,7 +1275,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery),
wantBodyString: "",
},
@ -1286,7 +1288,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery),
wantBodyString: "",
},
@ -1299,7 +1301,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery),
wantBodyString: "",
},
@ -1311,7 +1313,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: nil, // do not send header
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery),
wantBodyString: "",
},
@ -1323,7 +1325,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithPasswordGrantDisallowedHintErrorQuery),
wantBodyString: "",
},
@ -1340,7 +1342,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
"redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client",
}),
wantStatus: http.StatusBadRequest,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidRedirectURIErrorBody,
},
{
@ -1353,7 +1355,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusBadRequest,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidRedirectURIErrorBody,
},
{
@ -1366,7 +1368,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusBadRequest,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidRedirectURIErrorBody,
},
{
@ -1379,7 +1381,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusBadRequest,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidRedirectURIErrorBody,
},
{
@ -1393,7 +1395,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}),
wantStatus: http.StatusUnauthorized,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidClientErrorBody,
},
{
@ -1404,7 +1406,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusUnauthorized,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidClientErrorBody,
},
{
@ -1413,7 +1415,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}),
wantStatus: http.StatusUnauthorized,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidClientErrorBody,
},
{
@ -1422,7 +1424,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}),
wantStatus: http.StatusUnauthorized,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidClientErrorBody,
},
{
@ -1436,7 +1438,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}),
wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
wantBodyString: "",
},
@ -1448,7 +1450,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
wantBodyString: "",
},
@ -1460,7 +1462,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
wantBodyString: "",
},
@ -1470,7 +1472,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}),
wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
wantBodyString: "",
},
@ -1482,7 +1484,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
wantBodyString: "",
},
@ -1492,7 +1494,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}),
wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
wantBodyString: "",
},
@ -1507,7 +1509,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid profile email tuna"}),
wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery),
wantBodyString: "",
},
@ -1519,7 +1521,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery),
wantBodyString: "",
},
@ -1531,7 +1533,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery),
wantBodyString: "",
},
@ -1543,7 +1545,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery),
wantBodyString: "",
},
@ -1558,7 +1560,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}),
wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
wantBodyString: "",
},
@ -1570,7 +1572,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
wantBodyString: "",
},
@ -1582,7 +1584,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
wantBodyString: "",
},
@ -1592,7 +1594,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}),
wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
wantBodyString: "",
},
@ -1604,7 +1606,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
wantBodyString: "",
},
@ -1614,7 +1616,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}),
wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
wantBodyString: "",
},
@ -1629,7 +1631,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"client_id": ""}),
wantStatus: http.StatusUnauthorized,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidClientErrorBody,
},
{
@ -1640,7 +1642,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusUnauthorized,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidClientErrorBody,
},
{
@ -1649,7 +1651,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"client_id": ""}),
wantStatus: http.StatusUnauthorized,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidClientErrorBody,
},
{
@ -1663,7 +1665,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge": ""}),
wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery),
wantBodyString: "",
},
@ -1676,7 +1678,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery),
wantBodyString: "",
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
@ -1689,7 +1691,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery),
wantBodyString: "",
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
@ -1705,7 +1707,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "this-is-not-a-valid-pkce-alg"}),
wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery),
wantBodyString: "",
},
@ -1718,7 +1720,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery),
wantBodyString: "",
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
@ -1731,7 +1733,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery),
wantBodyString: "",
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
@ -1747,7 +1749,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "plain"}),
wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
wantBodyString: "",
},
@ -1760,7 +1762,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
wantBodyString: "",
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
@ -1773,7 +1775,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
wantBodyString: "",
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
@ -1789,7 +1791,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": ""}),
wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
wantBodyString: "",
},
@ -1802,7 +1804,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
wantBodyString: "",
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
@ -1815,7 +1817,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
wantBodyString: "",
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
@ -1833,7 +1835,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login"}),
wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery),
wantBodyString: "",
},
@ -1848,7 +1850,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery),
wantBodyString: "",
wantUnnecessaryStoredRecords: 1, // fosite already stored the authcode before it noticed the error
@ -1863,7 +1865,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery),
wantBodyString: "",
wantUnnecessaryStoredRecords: 1, // fosite already stored the authcode before it noticed the error
@ -2052,7 +2054,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithInvalidEmailVerifiedHintErrorQuery),
wantBodyString: "",
},
@ -2070,7 +2072,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithFalseEmailVerifiedHintErrorQuery),
wantBodyString: "",
},
@ -2159,7 +2161,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimMissingHintErrorQuery),
wantBodyString: "",
},
@ -2198,7 +2200,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery),
wantBodyString: "",
},
@ -2213,7 +2215,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimEmptyHintErrorQuery),
wantBodyString: "",
},
@ -2228,7 +2230,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimMissingHintErrorQuery),
wantBodyString: "",
},
@ -2243,7 +2245,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimEmptyHintErrorQuery),
wantBodyString: "",
},
@ -2258,7 +2260,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery),
wantBodyString: "",
},
@ -2273,7 +2275,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimMissingHintErrorQuery),
wantBodyString: "",
},
@ -2288,7 +2290,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimEmptyHintErrorQuery),
wantBodyString: "",
},
@ -2303,7 +2305,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery),
wantBodyString: "",
},
@ -2318,7 +2320,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery),
wantBodyString: "",
},
@ -2333,7 +2335,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery),
wantBodyString: "",
},
@ -2348,7 +2350,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery),
wantBodyString: "",
},
@ -2363,7 +2365,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"state": "short"}),
wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery),
wantBodyString: "",
},
@ -2375,7 +2377,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery),
wantBodyString: "",
},
@ -2387,7 +2389,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery),
wantBodyString: "",
},

View File

@ -16,6 +16,7 @@ import (
"github.com/ory/fosite/token/jwt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"go.pinniped.dev/internal/authenticators"
"go.pinniped.dev/internal/constable"
"go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/oidc/provider"
@ -61,6 +62,34 @@ func MakeDownstreamSession(subject string, username string, groups []string, cus
return openIDSession
}
func MakeDownstreamLDAPOrADCustomSessionData(
ldapUpstream provider.UpstreamLDAPIdentityProviderI,
idpType psession.ProviderType,
authenticateResponse *authenticators.Response,
) *psession.CustomSessionData {
customSessionData := &psession.CustomSessionData{
ProviderUID: ldapUpstream.GetResourceUID(),
ProviderName: ldapUpstream.GetName(),
ProviderType: idpType,
}
if idpType == psession.ProviderTypeLDAP {
customSessionData.LDAP = &psession.LDAPSessionData{
UserDN: authenticateResponse.DN,
ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes,
}
}
if idpType == psession.ProviderTypeActiveDirectory {
customSessionData.ActiveDirectory = &psession.ActiveDirectorySessionData{
UserDN: authenticateResponse.DN,
ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes,
}
}
return customSessionData
}
func MakeDownstreamOIDCCustomSessionData(oidcUpstream provider.UpstreamOIDCIdentityProviderI, token *oidctypes.Token) (*psession.CustomSessionData, error) {
upstreamSubject, err := ExtractStringClaimValue(oidc.IDTokenSubjectClaim, oidcUpstream.GetName(), token.IDToken.Claims)
if err != nil {
@ -228,6 +257,11 @@ func ExtractStringClaimValue(claimName string, upstreamIDPName string, idTokenCl
return valueAsString, nil
}
func DownstreamSubjectFromUpstreamLDAP(ldapUpstream provider.UpstreamLDAPIdentityProviderI, authenticateResponse *authenticators.Response) string {
ldapURL := *ldapUpstream.GetURL()
return DownstreamLDAPSubject(authenticateResponse.User.GetUID(), ldapURL)
}
func DownstreamLDAPSubject(uid string, ldapURL url.URL) string {
q := ldapURL.Query()
q.Set(oidc.IDTokenSubjectClaim, uid)

View File

@ -5,6 +5,7 @@ package login
import (
"net/http"
"net/url"
idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
"go.pinniped.dev/internal/httputil/httperr"
@ -13,6 +14,19 @@ import (
"go.pinniped.dev/internal/plog"
)
type ErrorParamValue string
const (
usernameParamName = "username"
passwordParamName = "password"
stateParamName = "state"
errParamName = "err"
ShowNoError ErrorParamValue = ""
ShowInternalError ErrorParamValue = "internal_error"
ShowBadUserPassErr ErrorParamValue = "login_error"
)
// HandlerFunc is a function that can handle either a GET or POST request for the login endpoint.
type HandlerFunc func(
w http.ResponseWriter,
@ -66,3 +80,30 @@ func NewHandler(
return securityheader.Wrap(loginHandler)
}
func RedirectToLoginPage(
r *http.Request,
w http.ResponseWriter,
downstreamIssuer string,
encodedStateParamValue string,
errToDisplay ErrorParamValue,
) error {
loginURL, err := url.Parse(downstreamIssuer + oidc.PinnipedLoginPath)
if err != nil {
return err
}
q := loginURL.Query()
q.Set(stateParamName, encodedStateParamValue)
if errToDisplay != ShowNoError {
q.Set(errParamName, string(errToDisplay))
}
loginURL.RawQuery = q.Encode()
http.Redirect(w, r,
loginURL.String(),
http.StatusSeeOther, // match fosite and https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11
)
return nil
}

View File

@ -5,15 +5,84 @@ package login
import (
"net/http"
"net/url"
"github.com/ory/fosite"
"go.pinniped.dev/internal/httputil/httperr"
"go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/oidc/downstreamsession"
"go.pinniped.dev/internal/plog"
)
func NewPostHandler(upstreamIDPs oidc.UpstreamIdentityProvidersLister, oauthHelper fosite.OAuth2Provider) HandlerFunc {
func NewPostHandler(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvidersLister, oauthHelper fosite.OAuth2Provider) HandlerFunc {
return func(w http.ResponseWriter, r *http.Request, encodedState string, decodedState *oidc.UpstreamStateParamData) error {
// TODO
// Note that the login handler prevents this handler from being called with OIDC upstreams.
_, ldapUpstream, idpType, err := oidc.FindUpstreamIDPByNameAndType(upstreamIDPs, decodedState.UpstreamName, decodedState.UpstreamType)
if err != nil {
// This shouldn't normally happen because the authorization endpoint ensured that this provider existed
// at that time. It would be possible in the unlikely event that the provider was deleted during the login.
plog.Error("error finding upstream provider", err)
return httperr.Wrap(http.StatusUnprocessableEntity, "error finding upstream provider", err)
}
// Get the original params that were used at the authorization endpoint.
downstreamAuthParams, err := url.ParseQuery(decodedState.AuthParams)
if err != nil {
// This shouldn't really happen because the authorization endpoint encoded these query params correctly.
plog.Error("error reading state downstream auth params", err)
return httperr.New(http.StatusBadRequest, "error reading state downstream auth params")
}
// Recreate enough of the original authorize request so we can pass it to NewAuthorizeRequest().
reconstitutedAuthRequest := &http.Request{Form: downstreamAuthParams}
authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), reconstitutedAuthRequest)
if err != nil {
// This shouldn't really happen because the authorization endpoint has already validated these params
// by calling NewAuthorizeRequest() itself.
plog.Error("error using state downstream auth params", err)
return httperr.New(http.StatusBadRequest, "error using state downstream auth params")
}
// Automatically grant the openid, offline_access, and pinniped:request-audience scopes, but only if they were requested.
downstreamsession.GrantScopesIfRequested(authorizeRequester)
// Get the username and password form params from the POST body.
username := r.PostFormValue(usernameParamName)
password := r.PostFormValue(passwordParamName)
// Treat blank username or password as a bad username/password combination, as opposed to an internal error.
if username == "" || password == "" {
// User forgot to enter one of the required fields.
// The user may try to log in again if they'd like, so redirect back to the login page with an error.
return RedirectToLoginPage(r, w, issuerURL, encodedState, ShowBadUserPassErr)
}
// Attempt to authenticate the user with the upstream IDP.
authenticateResponse, authenticated, err := ldapUpstream.AuthenticateUser(r.Context(), username, password)
if err != nil {
plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", ldapUpstream.GetName())
// There was some problem during authentication with the upstream, aside from bad username/password.
// The user may try to log in again if they'd like, so redirect back to the login page with an error.
return RedirectToLoginPage(r, w, issuerURL, encodedState, ShowInternalError)
}
if !authenticated {
// The upstream did not accept the username/password combination.
// The user may try to log in again if they'd like, so redirect back to the login page with an error.
return RedirectToLoginPage(r, w, issuerURL, encodedState, ShowBadUserPassErr)
}
// We had previously interrupted the regular steps of the OIDC authcode flow to show the login page UI.
// Now the upstream IDP has authenticated the user, so now we're back into the regular OIDC authcode flow steps.
// Both success and error responses from this point onwards should look like the usual fosite redirect
// responses, and a happy redirect response will include a downstream authcode.
subject := downstreamsession.DownstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse)
username = authenticateResponse.User.GetName()
groups := authenticateResponse.User.GetGroups()
customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse)
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData)
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, false)
return nil
}
}

View File

@ -0,0 +1,693 @@
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package login
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/require"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/kubernetes/fake"
"go.pinniped.dev/internal/authenticators"
"go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/oidc/jwks"
"go.pinniped.dev/internal/psession"
"go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/testutil/oidctestutil"
)
func TestPostLoginEndpoint(t *testing.T) {
const (
htmlContentType = "text/html; charset=utf-8"
happyDownstreamCSRF = "test-csrf"
happyDownstreamPKCE = "test-pkce"
happyDownstreamNonce = "test-nonce"
happyDownstreamStateVersion = "2"
happyEncodedUpstreamState = "fake-encoded-state-param-value"
downstreamIssuer = "https://my-downstream-issuer.com/path"
downstreamRedirectURI = "http://127.0.0.1/callback"
downstreamClientID = "pinniped-cli"
happyDownstreamState = "8b-state"
downstreamNonce = "some-nonce-value"
downstreamPKCEChallenge = "some-challenge"
downstreamPKCEChallengeMethod = "S256"
ldapUpstreamName = "some-ldap-idp"
ldapUpstreamType = "ldap"
ldapUpstreamResourceUID = "ldap-resource-uid"
activeDirectoryUpstreamName = "some-active-directory-idp"
activeDirectoryUpstreamType = "activedirectory"
activeDirectoryUpstreamResourceUID = "active-directory-resource-uid"
upstreamLDAPURL = "ldaps://some-ldap-host:123?base=ou%3Dusers%2Cdc%3Dpinniped%2Cdc%3Ddev"
userParam = "username"
passParam = "password"
badUserPassErrParamValue = "login_error"
internalErrParamValue = "internal_error"
)
var (
fositeMissingCodeChallengeErrorQuery = map[string]string{
"error": "invalid_request",
"error_description": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Clients must include a code_challenge when performing the authorize code flow, but it is missing.",
"state": happyDownstreamState,
}
fositeInvalidCodeChallengeErrorQuery = map[string]string{
"error": "invalid_request",
"error_description": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. The code_challenge_method is not supported, use S256 instead.",
"state": happyDownstreamState,
}
fositeMissingCodeChallengeMethodErrorQuery = map[string]string{
"error": "invalid_request",
"error_description": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Clients must use code_challenge_method=S256, plain is not allowed.",
"state": happyDownstreamState,
}
fositePromptHasNoneAndOtherValueErrorQuery = map[string]string{
"error": "invalid_request",
"error_description": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Parameter 'prompt' was set to 'none', but contains other values as well which is not allowed.",
"state": happyDownstreamState,
}
)
happyDownstreamScopesRequested := []string{"openid"}
happyDownstreamScopesGranted := []string{"openid"}
happyDownstreamRequestParamsQuery := url.Values{
"response_type": []string{"code"},
"scope": []string{strings.Join(happyDownstreamScopesRequested, " ")},
"client_id": []string{downstreamClientID},
"state": []string{happyDownstreamState},
"nonce": []string{downstreamNonce},
"code_challenge": []string{downstreamPKCEChallenge},
"code_challenge_method": []string{downstreamPKCEChallengeMethod},
"redirect_uri": []string{downstreamRedirectURI},
}
happyDownstreamRequestParams := happyDownstreamRequestParamsQuery.Encode()
copyOfHappyDownstreamRequestParamsQuery := func() url.Values {
params := url.Values{}
for k, v := range happyDownstreamRequestParamsQuery {
params[k] = make([]string, len(v))
copy(params[k], v)
}
return params
}
happyLDAPDecodedState := &oidc.UpstreamStateParamData{
AuthParams: happyDownstreamRequestParams,
UpstreamName: ldapUpstreamName,
UpstreamType: ldapUpstreamType,
Nonce: happyDownstreamNonce,
CSRFToken: happyDownstreamCSRF,
PKCECode: happyDownstreamPKCE,
FormatVersion: happyDownstreamStateVersion,
}
modifyHappyLDAPDecodedState := func(edit func(*oidc.UpstreamStateParamData)) *oidc.UpstreamStateParamData {
copyOfHappyLDAPDecodedState := *happyLDAPDecodedState
edit(&copyOfHappyLDAPDecodedState)
return &copyOfHappyLDAPDecodedState
}
happyActiveDirectoryDecodedState := &oidc.UpstreamStateParamData{
AuthParams: happyDownstreamRequestParams,
UpstreamName: activeDirectoryUpstreamName,
UpstreamType: activeDirectoryUpstreamType,
Nonce: happyDownstreamNonce,
CSRFToken: happyDownstreamCSRF,
PKCECode: happyDownstreamPKCE,
FormatVersion: happyDownstreamStateVersion,
}
happyLDAPUsername := "some-ldap-user"
happyLDAPUsernameFromAuthenticator := "some-mapped-ldap-username"
happyLDAPPassword := "some-ldap-password" //nolint:gosec
happyLDAPUID := "some-ldap-uid"
happyLDAPUserDN := "cn=foo,dn=bar"
happyLDAPGroups := []string{"group1", "group2", "group3"}
happyLDAPExtraRefreshAttribute := "some-refresh-attribute"
happyLDAPExtraRefreshValue := "some-refresh-attribute-value"
parsedUpstreamLDAPURL, err := url.Parse(upstreamLDAPURL)
require.NoError(t, err)
ldapAuthenticateFunc := func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) {
if username == "" || password == "" {
return nil, false, fmt.Errorf("should not have passed empty username or password to the authenticator")
}
if username == happyLDAPUsername && password == happyLDAPPassword {
return &authenticators.Response{
User: &user.DefaultInfo{
Name: happyLDAPUsernameFromAuthenticator,
UID: happyLDAPUID,
Groups: happyLDAPGroups,
},
DN: happyLDAPUserDN,
ExtraRefreshAttributes: map[string]string{
happyLDAPExtraRefreshAttribute: happyLDAPExtraRefreshValue,
},
}, true, nil
}
return nil, false, nil
}
upstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: ldapUpstreamName,
ResourceUID: ldapUpstreamResourceUID,
URL: parsedUpstreamLDAPURL,
AuthenticateFunc: ldapAuthenticateFunc,
}
upstreamActiveDirectoryIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: activeDirectoryUpstreamName,
ResourceUID: activeDirectoryUpstreamResourceUID,
URL: parsedUpstreamLDAPURL,
AuthenticateFunc: ldapAuthenticateFunc,
}
erroringUpstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: ldapUpstreamName,
ResourceUID: ldapUpstreamResourceUID,
AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) {
return nil, false, fmt.Errorf("some ldap upstream auth error")
},
}
expectedHappyActiveDirectoryUpstreamCustomSession := &psession.CustomSessionData{
ProviderUID: activeDirectoryUpstreamResourceUID,
ProviderName: activeDirectoryUpstreamName,
ProviderType: psession.ProviderTypeActiveDirectory,
OIDC: nil,
LDAP: nil,
ActiveDirectory: &psession.ActiveDirectorySessionData{
UserDN: happyLDAPUserDN,
ExtraRefreshAttributes: map[string]string{happyLDAPExtraRefreshAttribute: happyLDAPExtraRefreshValue},
},
}
expectedHappyLDAPUpstreamCustomSession := &psession.CustomSessionData{
ProviderUID: ldapUpstreamResourceUID,
ProviderName: ldapUpstreamName,
ProviderType: psession.ProviderTypeLDAP,
OIDC: nil,
LDAP: &psession.LDAPSessionData{
UserDN: happyLDAPUserDN,
ExtraRefreshAttributes: map[string]string{happyLDAPExtraRefreshAttribute: happyLDAPExtraRefreshValue},
},
ActiveDirectory: nil,
}
// Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it
happyAuthcodeDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid&state=` + happyDownstreamState
happyUsernamePasswordFormParams := url.Values{userParam: []string{happyLDAPUsername}, passParam: []string{happyLDAPPassword}}
encodeQuery := func(query map[string]string) string {
values := url.Values{}
for k, v := range query {
values[k] = []string{v}
}
return values.Encode()
}
urlWithQuery := func(baseURL string, query map[string]string) string {
urlToReturn := fmt.Sprintf("%s?%s", baseURL, encodeQuery(query))
_, err := url.Parse(urlToReturn)
require.NoError(t, err, "urlWithQuery helper was used to create an illegal URL")
return urlToReturn
}
tests := []struct {
name string
idps *oidctestutil.UpstreamIDPListerBuilder
decodedState *oidc.UpstreamStateParamData
formParams url.Values
reqURIQuery url.Values
wantStatus int
wantContentType string
wantBodyString string
wantErr string
// Assertion that the response should be a redirect to the login page with an error param.
wantRedirectToLoginPageError string
// Assertions for when an authcode should be returned, i.e. the request was authenticated by an
// upstream LDAP or AD provider.
wantRedirectLocationRegexp string // for loose matching
wantRedirectLocationString string // for exact matching instead
wantDownstreamRedirectURI string
wantDownstreamGrantedScopes []string
wantDownstreamIDTokenSubject string
wantDownstreamIDTokenUsername string
wantDownstreamIDTokenGroups []string
wantDownstreamRequestedScopes []string
wantDownstreamPKCEChallenge string
wantDownstreamPKCEChallengeMethod string
wantDownstreamNonce string
wantDownstreamCustomSessionData *psession.CustomSessionData
// Authorization requests for either a successful OIDC upstream or for an error with any upstream
// should never use Kube storage. There is only one exception to this rule, which is that certain
// OIDC validations are checked in fosite after the OAuth authcode (and sometimes the OIDC session)
// is stored, so it is possible with an LDAP upstream to store objects and then return an error to
// the client anyway (which makes the stored objects useless, but oh well).
wantUnnecessaryStoredRecords int
}{
{
name: "happy LDAP login",
idps: oidctestutil.NewUpstreamIDPListerBuilder().
WithLDAP(&upstreamLDAPIdentityProvider). // should pick this one
WithActiveDirectory(&erroringUpstreamLDAPIdentityProvider),
decodedState: happyLDAPDecodedState,
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
wantDownstreamIDTokenGroups: happyLDAPGroups,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
},
{
name: "happy AD login",
idps: oidctestutil.NewUpstreamIDPListerBuilder().
WithLDAP(&erroringUpstreamLDAPIdentityProvider).
WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), // should pick this one
decodedState: happyActiveDirectoryDecodedState,
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
wantDownstreamIDTokenGroups: happyLDAPGroups,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyActiveDirectoryUpstreamCustomSession,
},
{
name: "happy LDAP login when downstream redirect uri matches what is configured for client except for the port number",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["redirect_uri"] = []string{"http://127.0.0.1:4242/callback"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationRegexp: "http://127.0.0.1:4242/callback" + `\?code=([^&]+)&scope=openid&state=` + happyDownstreamState,
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
wantDownstreamIDTokenGroups: happyLDAPGroups,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: "http://127.0.0.1:4242/callback",
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
},
{
name: "happy LDAP login when there are additional allowed downstream requested scopes",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["scope"] = []string{"openid offline_access pinniped:request-audience"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+pinniped%3Arequest-audience&state=` + happyDownstreamState,
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
wantDownstreamIDTokenGroups: happyLDAPGroups,
wantDownstreamRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
},
{
name: "happy LDAP when downstream OIDC validations are skipped because the openid scope was not requested",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["scope"] = []string{"email"}
// The following prompt value is illegal when openid is requested, but note that openid is not requested.
query["prompt"] = []string{"none login"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=&state=` + happyDownstreamState, // no scopes granted
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
wantDownstreamIDTokenGroups: happyLDAPGroups,
wantDownstreamRequestedScopes: []string{"email"}, // only email was requested
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: []string{}, // no scopes granted
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
},
{
name: "bad username LDAP login",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: happyLDAPDecodedState,
formParams: url.Values{userParam: []string{"wrong!"}, passParam: []string{happyLDAPPassword}},
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectToLoginPageError: badUserPassErrParamValue,
},
{
name: "bad password LDAP login",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: happyLDAPDecodedState,
formParams: url.Values{userParam: []string{happyLDAPUsername}, passParam: []string{"wrong!"}},
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectToLoginPageError: badUserPassErrParamValue,
},
{
name: "blank username LDAP login",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: happyLDAPDecodedState,
formParams: url.Values{userParam: []string{""}, passParam: []string{happyLDAPPassword}},
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectToLoginPageError: badUserPassErrParamValue,
},
{
name: "blank password LDAP login",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: happyLDAPDecodedState,
formParams: url.Values{userParam: []string{happyLDAPUsername}, passParam: []string{""}},
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectToLoginPageError: badUserPassErrParamValue,
},
{
name: "username and password sent as URI query params should be ignored since they are expected in form post body",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: happyLDAPDecodedState,
reqURIQuery: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectToLoginPageError: badUserPassErrParamValue,
},
{
name: "error during upstream LDAP authentication",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&erroringUpstreamLDAPIdentityProvider),
decodedState: happyLDAPDecodedState,
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectToLoginPageError: internalErrParamValue,
},
{
name: "downstream redirect uri does not match what is configured for client",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["redirect_uri"] = []string{"http://127.0.0.1/wrong_callback"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params",
},
{
name: "downstream client does not exist",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["client_id"] = []string{"wrong_client_id"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params",
},
{
name: "downstream client is missing",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
delete(query, "client_id")
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params",
},
{
name: "response type is unsupported",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["response_type"] = []string{"unsupported"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params",
},
{
name: "response type is missing",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
delete(query, "response_type")
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params",
},
{
name: "PKCE code_challenge is missing",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
delete(query, "code_challenge")
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationString: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery),
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
},
{
name: "PKCE code_challenge_method is invalid",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["code_challenge_method"] = []string{"this-is-not-a-valid-pkce-alg"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationString: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery),
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
},
{
name: "PKCE code_challenge_method is `plain`",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["code_challenge_method"] = []string{"plain"} // plain is not allowed
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationString: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
},
{
name: "PKCE code_challenge_method is missing",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
delete(query, "code_challenge_method")
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationString: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
},
{
name: "prompt param is not allowed to have none and another legal value at the same time",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["prompt"] = []string{"none login"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationString: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery),
wantUnnecessaryStoredRecords: 1, // fosite already stored the authcode before it noticed the error
},
{
name: "downstream state does not have enough entropy",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["state"] = []string{"short"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params",
},
{
name: "downstream scopes do not match what is configured for client",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["scope"] = []string{"openid offline_access pinniped:request-audience scope_not_allowed"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params",
},
{
name: "no upstream providers are configured or provider cannot be found by name",
idps: oidctestutil.NewUpstreamIDPListerBuilder(), // empty
decodedState: happyLDAPDecodedState,
formParams: happyUsernamePasswordFormParams,
wantErr: "error finding upstream provider: provider not found",
},
{
name: "upstream provider cannot be found by name and type",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: happyActiveDirectoryDecodedState, // correct upstream IDP name, but wrong upstream IDP type
formParams: happyUsernamePasswordFormParams,
wantErr: "error finding upstream provider: provider not found",
},
}
for _, test := range tests {
tt := test
t.Run(tt.name, func(t *testing.T) {
kubeClient := fake.NewSimpleClientset()
secretsClient := kubeClient.CoreV1().Secrets("some-namespace")
// Configure fosite the same way that the production code would.
// Inject this into our test subject at the last second so we get a fresh storage for every test.
timeoutsConfiguration := oidc.DefaultOIDCTimeoutsConfiguration()
kubeOauthStore := oidc.NewKubeStorage(secretsClient, timeoutsConfiguration)
hmacSecretFunc := func() []byte { return []byte("some secret - must have at least 32 bytes") }
require.GreaterOrEqual(t, len(hmacSecretFunc()), 32, "fosite requires that hmac secrets have at least 32 bytes")
jwksProviderIsUnused := jwks.NewDynamicJWKSProvider()
oauthHelper := oidc.FositeOauth2Helper(kubeOauthStore, downstreamIssuer, hmacSecretFunc, jwksProviderIsUnused, timeoutsConfiguration)
req := httptest.NewRequest(http.MethodPost, "/ignored", strings.NewReader(tt.formParams.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if tt.reqURIQuery != nil {
req.URL.RawQuery = tt.reqURIQuery.Encode()
}
rsp := httptest.NewRecorder()
subject := NewPostHandler(downstreamIssuer, tt.idps.Build(), oauthHelper)
err := subject(rsp, req, happyEncodedUpstreamState, tt.decodedState)
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
require.Empty(t, kubeClient.Actions())
return // the http response doesn't matter when the function returns an error, because the caller should handle the error
}
// Otherwise, expect no error.
require.NoError(t, err)
require.Equal(t, tt.wantStatus, rsp.Code)
testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), tt.wantContentType)
require.Equal(t, test.wantBodyString, rsp.Body.String())
actualLocation := rsp.Header().Get("Location")
switch {
case tt.wantRedirectLocationRegexp != "":
require.Len(t, rsp.Header().Values("Location"), 1)
oidctestutil.RequireAuthCodeRegexpMatch(
t,
actualLocation,
test.wantRedirectLocationRegexp,
kubeClient,
secretsClient,
kubeOauthStore,
test.wantDownstreamGrantedScopes,
test.wantDownstreamIDTokenSubject,
test.wantDownstreamIDTokenUsername,
test.wantDownstreamIDTokenGroups,
test.wantDownstreamRequestedScopes,
test.wantDownstreamPKCEChallenge,
test.wantDownstreamPKCEChallengeMethod,
test.wantDownstreamNonce,
downstreamClientID,
test.wantDownstreamRedirectURI,
test.wantDownstreamCustomSessionData,
)
case tt.wantRedirectToLoginPageError != "":
expectedLocation := downstreamIssuer + oidc.PinnipedLoginPath +
"?err=" + tt.wantRedirectToLoginPageError + "&state=" + happyEncodedUpstreamState
require.Equal(t, expectedLocation, actualLocation)
require.Len(t, kubeClient.Actions(), test.wantUnnecessaryStoredRecords)
case tt.wantRedirectLocationString != "":
require.Equal(t, tt.wantRedirectLocationString, actualLocation)
require.Len(t, kubeClient.Actions(), test.wantUnnecessaryStoredRecords)
default:
require.Failf(t, "test should have expected a redirect",
"actual location was %q", actualLocation)
}
})
}
}

View File

@ -6,18 +6,25 @@ package oidc
import (
"crypto/subtle"
"errors"
"fmt"
"net/http"
"time"
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/felixge/httpsnoop"
"github.com/ory/fosite"
"github.com/ory/fosite/compose"
errorsx "github.com/pkg/errors"
"go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
"go.pinniped.dev/internal/httputil/httperr"
"go.pinniped.dev/internal/oidc/csrftoken"
"go.pinniped.dev/internal/oidc/jwks"
"go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/oidc/provider/formposthtml"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/psession"
"go.pinniped.dev/pkg/oidcclient/nonce"
"go.pinniped.dev/pkg/oidcclient/pkce"
)
@ -365,3 +372,106 @@ func validateCSRFValue(state *UpstreamStateParamData, csrfCookieValue csrftoken.
}
return nil
}
// FindUpstreamIDPByNameAndType finds the requested IDP by name and type, or returns an error.
// Note that AD and LDAP IDPs both return the same interface type, but different ProviderTypes values.
func FindUpstreamIDPByNameAndType(
idpLister UpstreamIdentityProvidersLister,
upstreamName string,
upstreamType string,
) (
provider.UpstreamOIDCIdentityProviderI,
provider.UpstreamLDAPIdentityProviderI,
psession.ProviderType,
error,
) {
switch upstreamType {
case string(v1alpha1.IDPTypeOIDC):
for _, p := range idpLister.GetOIDCIdentityProviders() {
if p.GetName() == upstreamName {
return p, nil, psession.ProviderTypeOIDC, nil
}
}
case string(v1alpha1.IDPTypeLDAP):
for _, p := range idpLister.GetLDAPIdentityProviders() {
if p.GetName() == upstreamName {
return nil, p, psession.ProviderTypeLDAP, nil
}
}
case string(v1alpha1.IDPTypeActiveDirectory):
for _, p := range idpLister.GetActiveDirectoryIdentityProviders() {
if p.GetName() == upstreamName {
return nil, p, psession.ProviderTypeActiveDirectory, nil
}
}
}
return nil, nil, "", errors.New("provider not found")
}
// WriteAuthorizeError writes an authorization error as it should be returned by the authorization endpoint and other
// similar endpoints that are the end of the downstream authcode flow. Errors responses are written in the usual fosite style.
func WriteAuthorizeError(w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester, err error, isBrowserless bool) {
if plog.Enabled(plog.LevelTrace) {
// When trace level logging is enabled, include the stack trace in the log message.
keysAndValues := FositeErrorForLog(err)
errWithStack := errorsx.WithStack(err)
keysAndValues = append(keysAndValues, "errWithStack")
// klog always prints error values using %s, which does not include stack traces,
// so convert the error to a string which includes the stack trace here.
keysAndValues = append(keysAndValues, fmt.Sprintf("%+v", errWithStack))
plog.Trace("authorize response error", keysAndValues...)
} else {
plog.Info("authorize response error", FositeErrorForLog(err)...)
}
if isBrowserless {
w = rewriteStatusSeeOtherToStatusFoundForBrowserless(w)
}
// Return an error according to OIDC spec 3.1.2.6 (second paragraph).
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
}
// PerformAuthcodeRedirect successfully completes a downstream login by creating a session and
// writing the authcode redirect response as it should be returned by the authorization endpoint and other
// similar endpoints that are the end of the downstream authcode flow.
func PerformAuthcodeRedirect(
r *http.Request,
w http.ResponseWriter,
oauthHelper fosite.OAuth2Provider,
authorizeRequester fosite.AuthorizeRequester,
openIDSession *psession.PinnipedSession,
isBrowserless bool,
) {
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
if err != nil {
plog.WarningErr("error while generating and saving authcode", err)
WriteAuthorizeError(w, oauthHelper, authorizeRequester, err, isBrowserless)
return
}
if isBrowserless {
w = rewriteStatusSeeOtherToStatusFoundForBrowserless(w)
}
oauthHelper.WriteAuthorizeResponse(w, authorizeRequester, authorizeResponder)
}
func rewriteStatusSeeOtherToStatusFoundForBrowserless(w http.ResponseWriter) http.ResponseWriter {
// rewrite http.StatusSeeOther to http.StatusFound for backwards compatibility with old pinniped CLIs.
// we can drop this in a few releases once we feel enough time has passed for users to update.
//
// WriteAuthorizeResponse/WriteAuthorizeError calls used to result in http.StatusFound until
// https://github.com/ory/fosite/pull/636 changed it to http.StatusSeeOther to address
// https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11
// Safari has the bad behavior in the case of http.StatusFound and not just http.StatusTemporaryRedirect.
//
// in the browserless flows, the OAuth client is the pinniped CLI and it already has access to the user's
// password. Thus there is no security issue with using http.StatusFound vs. http.StatusSeeOther.
return httpsnoop.Wrap(w, httpsnoop.Hooks{
WriteHeader: func(delegate httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc {
return func(code int) {
if code == http.StatusSeeOther {
code = http.StatusFound
}
delegate(code)
}
},
})
}

View File

@ -140,7 +140,7 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs
upstreamStateEncoder,
csrfCookieEncoder,
login.NewGetHandler(m.upstreamIDPs),
login.NewPostHandler(m.upstreamIDPs, oauthHelperWithKubeStorage),
login.NewPostHandler(issuer, m.upstreamIDPs, oauthHelperWithKubeStorage),
)
plog.Debug("oidc provider manager added or updated issuer", "issuer", issuer)