Update authorization endpoint to redirect to new login page
Also fix some test failures on the callback handler, register the new login handler in manager.go and add a (half baked) integration test Signed-off-by: Margo Crawford <margaretc@vmware.com>
This commit is contained in:
parent
8832362b94
commit
eb1d3812ec
@ -7,6 +7,7 @@ package auth
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
||||||
@ -75,15 +76,33 @@ func NewHandler(
|
|||||||
cookieCodec,
|
cookieCodec,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return handleAuthRequestForLDAPUpstream(r, w,
|
|
||||||
|
// we know it's an AD/LDAP upstream.
|
||||||
|
if len(r.Header.Values(supervisoroidc.AuthorizeUsernameHeaderName)) > 0 || len(r.Header.Values(supervisoroidc.AuthorizePasswordHeaderName)) > 0 {
|
||||||
|
// The client set a username header, so they are trying to log in with a username/password.
|
||||||
|
return handleAuthRequestForLDAPUpstreamCLIFlow(r, w,
|
||||||
oauthHelperWithStorage,
|
oauthHelperWithStorage,
|
||||||
ldapUpstream,
|
ldapUpstream,
|
||||||
idpType,
|
idpType,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
return handleAuthRequestForLDAPUpstreamBrowserFlow(
|
||||||
|
r,
|
||||||
|
w,
|
||||||
|
oauthHelperWithoutStorage,
|
||||||
|
generateCSRF,
|
||||||
|
generateNonce,
|
||||||
|
generatePKCE,
|
||||||
|
ldapUpstream,
|
||||||
|
idpType,
|
||||||
|
downstreamIssuer,
|
||||||
|
upstreamStateEncoder,
|
||||||
|
cookieCodec,
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleAuthRequestForLDAPUpstream(
|
func handleAuthRequestForLDAPUpstreamCLIFlow(
|
||||||
r *http.Request,
|
r *http.Request,
|
||||||
w http.ResponseWriter,
|
w http.ResponseWriter,
|
||||||
oauthHelper fosite.OAuth2Provider,
|
oauthHelper fosite.OAuth2Provider,
|
||||||
@ -138,6 +157,93 @@ func handleAuthRequestForLDAPUpstream(
|
|||||||
oauthHelper, authorizeRequester, subject, username, groups, customSessionData)
|
oauthHelper, authorizeRequester, subject, username, groups, customSessionData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleAuthRequestForLDAPUpstreamBrowserFlow(
|
||||||
|
r *http.Request,
|
||||||
|
w http.ResponseWriter,
|
||||||
|
oauthHelper fosite.OAuth2Provider,
|
||||||
|
generateCSRF func() (csrftoken.CSRFToken, error),
|
||||||
|
generateNonce func() (nonce.Nonce, error),
|
||||||
|
generatePKCE func() (pkce.Code, error),
|
||||||
|
ldapUpstream provider.UpstreamLDAPIdentityProviderI,
|
||||||
|
idpType psession.ProviderType,
|
||||||
|
downstreamIssuer string,
|
||||||
|
upstreamStateEncoder oidc.Encoder,
|
||||||
|
cookieCodec oidc.Codec,
|
||||||
|
) error {
|
||||||
|
authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper, false)
|
||||||
|
if !created {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
_, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &psession.PinnipedSession{
|
||||||
|
Fosite: &openid.DefaultSession{
|
||||||
|
Claims: &jwt.IDTokenClaims{
|
||||||
|
// Temporary claim values to allow `NewAuthorizeResponse` to perform other OIDC validations.
|
||||||
|
Subject: "none",
|
||||||
|
AuthTime: now,
|
||||||
|
RequestedAt: now,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return writeAuthorizeError(w, oauthHelper, authorizeRequester, err, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE)
|
||||||
|
if err != nil {
|
||||||
|
plog.Error("authorize generate error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
csrfFromCookie := readCSRFCookie(r, cookieCodec)
|
||||||
|
if csrfFromCookie != "" {
|
||||||
|
csrfValue = csrfFromCookie
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedStateParamValue, err := upstreamStateParam(
|
||||||
|
authorizeRequester,
|
||||||
|
ldapUpstream.GetName(),
|
||||||
|
string(idpType),
|
||||||
|
nonceValue,
|
||||||
|
csrfValue,
|
||||||
|
pkceValue,
|
||||||
|
upstreamStateEncoder,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
plog.Error("authorize upstream state param error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
promptParam := r.Form.Get(promptParamName)
|
||||||
|
if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) {
|
||||||
|
return writeAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if csrfFromCookie == "" {
|
||||||
|
// We did not receive an incoming CSRF cookie, so write a new one.
|
||||||
|
err := addCSRFSetCookieHeader(w, csrfValue, cookieCodec)
|
||||||
|
if err != nil {
|
||||||
|
plog.Error("error setting CSRF cookie", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
func handleAuthRequestForOIDCUpstreamPasswordGrant(
|
func handleAuthRequestForOIDCUpstreamPasswordGrant(
|
||||||
r *http.Request,
|
r *http.Request,
|
||||||
w http.ResponseWriter,
|
w http.ResponseWriter,
|
||||||
@ -246,6 +352,7 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
|
|||||||
encodedStateParamValue, err := upstreamStateParam(
|
encodedStateParamValue, err := upstreamStateParam(
|
||||||
authorizeRequester,
|
authorizeRequester,
|
||||||
oidcUpstream.GetName(),
|
oidcUpstream.GetName(),
|
||||||
|
string(psession.ProviderTypeOIDC),
|
||||||
nonceValue,
|
nonceValue,
|
||||||
csrfValue,
|
csrfValue,
|
||||||
pkceValue,
|
pkceValue,
|
||||||
@ -463,6 +570,7 @@ func generateValues(
|
|||||||
func upstreamStateParam(
|
func upstreamStateParam(
|
||||||
authorizeRequester fosite.AuthorizeRequester,
|
authorizeRequester fosite.AuthorizeRequester,
|
||||||
upstreamName string,
|
upstreamName string,
|
||||||
|
upstreamType string,
|
||||||
nonceValue nonce.Nonce,
|
nonceValue nonce.Nonce,
|
||||||
csrfValue csrftoken.CSRFToken,
|
csrfValue csrftoken.CSRFToken,
|
||||||
pkceValue pkce.Code,
|
pkceValue pkce.Code,
|
||||||
@ -471,6 +579,7 @@ func upstreamStateParam(
|
|||||||
stateParamData := oidc.UpstreamStateParamData{
|
stateParamData := oidc.UpstreamStateParamData{
|
||||||
AuthParams: authorizeRequester.GetRequestForm().Encode(),
|
AuthParams: authorizeRequester.GetRequestForm().Encode(),
|
||||||
UpstreamName: upstreamName,
|
UpstreamName: upstreamName,
|
||||||
|
UpstreamType: upstreamType,
|
||||||
Nonce: nonceValue,
|
Nonce: nonceValue,
|
||||||
CSRFToken: csrfValue,
|
CSRFToken: csrfValue,
|
||||||
PKCECode: pkceValue,
|
PKCECode: pkceValue,
|
||||||
|
@ -409,23 +409,20 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
return pathWithQuery("/some/path", modifiedHappyGetRequestQueryMap(queryOverrides))
|
return pathWithQuery("/some/path", modifiedHappyGetRequestQueryMap(queryOverrides))
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedUpstreamStateParam := func(queryOverrides map[string]string, csrfValueOverride, upstreamNameOverride string) string {
|
expectedUpstreamStateParam := func(queryOverrides map[string]string, csrfValueOverride, upstreamName, upstreamType string) string {
|
||||||
csrf := happyCSRF
|
csrf := happyCSRF
|
||||||
if csrfValueOverride != "" {
|
if csrfValueOverride != "" {
|
||||||
csrf = csrfValueOverride
|
csrf = csrfValueOverride
|
||||||
}
|
}
|
||||||
upstreamName := oidcUpstreamName
|
|
||||||
if upstreamNameOverride != "" {
|
|
||||||
upstreamName = upstreamNameOverride
|
|
||||||
}
|
|
||||||
encoded, err := happyStateEncoder.Encode("s",
|
encoded, err := happyStateEncoder.Encode("s",
|
||||||
oidctestutil.ExpectedUpstreamStateParamFormat{
|
oidctestutil.ExpectedUpstreamStateParamFormat{
|
||||||
P: encodeQuery(modifiedHappyGetRequestQueryMap(queryOverrides)),
|
P: encodeQuery(modifiedHappyGetRequestQueryMap(queryOverrides)),
|
||||||
U: upstreamName,
|
U: upstreamName,
|
||||||
|
T: upstreamType,
|
||||||
N: happyNonce,
|
N: happyNonce,
|
||||||
C: csrf,
|
C: csrf,
|
||||||
K: happyPKCE,
|
K: happyPKCE,
|
||||||
V: "1",
|
V: "2",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -558,7 +555,24 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantStatus: http.StatusSeeOther,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantCSRFValueInCookieHeader: happyCSRF,
|
wantCSRFValueInCookieHeader: happyCSRF,
|
||||||
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil),
|
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", oidcUpstreamName, "oidc"), nil),
|
||||||
|
wantUpstreamStateParamInLocationHeader: true,
|
||||||
|
wantBodyStringWithLocationInHref: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "LDAP upstream browser flow happy path using GET without a CSRF cookie",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
||||||
|
generateCSRF: happyCSRFGenerator,
|
||||||
|
generatePKCE: happyPKCEGenerator,
|
||||||
|
generateNonce: happyNonceGenerator,
|
||||||
|
stateEncoder: happyStateEncoder,
|
||||||
|
cookieEncoder: happyCookieEncoder,
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: happyGetRequestPath,
|
||||||
|
wantStatus: http.StatusSeeOther,
|
||||||
|
wantContentType: htmlContentType,
|
||||||
|
wantCSRFValueInCookieHeader: happyCSRF,
|
||||||
|
wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(nil, "", ldapUpstreamName, "ldap")}),
|
||||||
wantUpstreamStateParamInLocationHeader: true,
|
wantUpstreamStateParamInLocationHeader: true,
|
||||||
wantBodyStringWithLocationInHref: true,
|
wantBodyStringWithLocationInHref: true,
|
||||||
},
|
},
|
||||||
@ -639,7 +653,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ",
|
csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ",
|
||||||
wantStatus: http.StatusSeeOther,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, incomingCookieCSRFValue, ""), nil),
|
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, incomingCookieCSRFValue, oidcUpstreamName, "oidc"), nil),
|
||||||
wantUpstreamStateParamInLocationHeader: true,
|
wantUpstreamStateParamInLocationHeader: true,
|
||||||
wantBodyStringWithLocationInHref: true,
|
wantBodyStringWithLocationInHref: true,
|
||||||
},
|
},
|
||||||
@ -659,7 +673,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantContentType: "",
|
wantContentType: "",
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
wantCSRFValueInCookieHeader: happyCSRF,
|
wantCSRFValueInCookieHeader: happyCSRF,
|
||||||
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil),
|
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", oidcUpstreamName, "oidc"), nil),
|
||||||
wantUpstreamStateParamInLocationHeader: true,
|
wantUpstreamStateParamInLocationHeader: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -748,7 +762,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantBodyStringWithLocationInHref: true,
|
wantBodyStringWithLocationInHref: true,
|
||||||
wantCSRFValueInCookieHeader: happyCSRF,
|
wantCSRFValueInCookieHeader: happyCSRF,
|
||||||
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", ""), nil),
|
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", oidcUpstreamName, "oidc"), nil),
|
||||||
wantUpstreamStateParamInLocationHeader: true,
|
wantUpstreamStateParamInLocationHeader: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -767,7 +781,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantBodyStringWithLocationInHref: true,
|
wantBodyStringWithLocationInHref: true,
|
||||||
wantCSRFValueInCookieHeader: happyCSRF,
|
wantCSRFValueInCookieHeader: happyCSRF,
|
||||||
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", ""), map[string]string{"prompt": "consent", "abc": "123", "def": "456"}),
|
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", oidcUpstreamName, "oidc"), map[string]string{"prompt": "consent", "abc": "123", "def": "456"}),
|
||||||
wantUpstreamStateParamInLocationHeader: true,
|
wantUpstreamStateParamInLocationHeader: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -802,7 +816,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
// Generated a new CSRF cookie and set it in the response.
|
// Generated a new CSRF cookie and set it in the response.
|
||||||
wantCSRFValueInCookieHeader: happyCSRF,
|
wantCSRFValueInCookieHeader: happyCSRF,
|
||||||
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil),
|
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", oidcUpstreamName, "oidc"), nil),
|
||||||
wantUpstreamStateParamInLocationHeader: true,
|
wantUpstreamStateParamInLocationHeader: true,
|
||||||
wantBodyStringWithLocationInHref: true,
|
wantBodyStringWithLocationInHref: true,
|
||||||
},
|
},
|
||||||
@ -823,7 +837,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantCSRFValueInCookieHeader: happyCSRF,
|
wantCSRFValueInCookieHeader: happyCSRF,
|
||||||
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{
|
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{
|
||||||
"redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client
|
"redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client
|
||||||
}, "", ""), nil),
|
}, "", oidcUpstreamName, "oidc"), nil),
|
||||||
wantUpstreamStateParamInLocationHeader: true,
|
wantUpstreamStateParamInLocationHeader: true,
|
||||||
wantBodyStringWithLocationInHref: true,
|
wantBodyStringWithLocationInHref: true,
|
||||||
},
|
},
|
||||||
@ -889,7 +903,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantCSRFValueInCookieHeader: happyCSRF,
|
wantCSRFValueInCookieHeader: happyCSRF,
|
||||||
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{
|
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{
|
||||||
"scope": "openid offline_access",
|
"scope": "openid offline_access",
|
||||||
}, "", ""), nil),
|
}, "", oidcUpstreamName, "oidc"), nil),
|
||||||
wantUpstreamStateParamInLocationHeader: true,
|
wantUpstreamStateParamInLocationHeader: true,
|
||||||
wantBodyStringWithLocationInHref: true,
|
wantBodyStringWithLocationInHref: true,
|
||||||
},
|
},
|
||||||
@ -1063,7 +1077,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing upstream username on request for LDAP authentication",
|
name: "missing upstream username but has password on request for LDAP authentication",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: happyGetRequestPath,
|
path: happyGetRequestPath,
|
||||||
@ -1338,25 +1352,49 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "response type is unsupported when using LDAP upstream",
|
name: "response type is unsupported when using LDAP cli upstream",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}),
|
path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}),
|
||||||
|
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
|
||||||
|
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "response type is unsupported when using active directory upstream",
|
name: "response type is unsupported when using LDAP browser upstream",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}),
|
||||||
|
wantStatus: http.StatusSeeOther,
|
||||||
|
wantContentType: "application/json; charset=utf-8",
|
||||||
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
|
||||||
|
wantBodyString: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "response type is unsupported when using active directory cli upstream",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}),
|
path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}),
|
||||||
|
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||||
|
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "response type is unsupported when using active directory browser upstream",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}),
|
||||||
|
wantStatus: http.StatusSeeOther,
|
||||||
|
wantContentType: "application/json; charset=utf-8",
|
||||||
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
|
||||||
|
wantBodyString: "",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "downstream scopes do not match what is configured for client using OIDC upstream browser flow",
|
name: "downstream scopes do not match what is configured for client using OIDC upstream browser flow",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
|
||||||
@ -1436,25 +1474,49 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing response type in request using LDAP upstream",
|
name: "missing response type in request using LDAP cli upstream",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}),
|
path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}),
|
||||||
|
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||||
|
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing response type in request using Active Directory upstream",
|
name: "missing response type in request using LDAP browser upstream",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}),
|
||||||
|
wantStatus: http.StatusSeeOther,
|
||||||
|
wantContentType: "application/json; charset=utf-8",
|
||||||
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
|
||||||
|
wantBodyString: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing response type in request using Active Directory cli upstream",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}),
|
path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}),
|
||||||
|
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||||
|
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "missing response type in request using Active Directory browser upstream",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}),
|
||||||
|
wantStatus: http.StatusSeeOther,
|
||||||
|
wantContentType: "application/json; charset=utf-8",
|
||||||
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
|
||||||
|
wantBodyString: "",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "missing client id in request using OIDC upstream browser flow",
|
name: "missing client id in request using OIDC upstream browser flow",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
|
||||||
@ -1720,7 +1782,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantCSRFValueInCookieHeader: happyCSRF,
|
wantCSRFValueInCookieHeader: happyCSRF,
|
||||||
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(
|
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(
|
||||||
map[string]string{"prompt": "none login", "scope": "email"}, "", "",
|
map[string]string{"prompt": "none login", "scope": "email"}, "", oidcUpstreamName, "oidc",
|
||||||
), nil),
|
), nil),
|
||||||
wantUpstreamStateParamInLocationHeader: true,
|
wantUpstreamStateParamInLocationHeader: true,
|
||||||
wantBodyStringWithLocationInHref: true,
|
wantBodyStringWithLocationInHref: true,
|
||||||
@ -2543,7 +2605,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
"scope": "some-other-new-scope1 some-other-new-scope2", // updated expectation
|
"scope": "some-other-new-scope1 some-other-new-scope2", // updated expectation
|
||||||
"client_id": "some-other-new-client-id", // updated expectation
|
"client_id": "some-other-new-client-id", // updated expectation
|
||||||
"state": expectedUpstreamStateParam(
|
"state": expectedUpstreamStateParam(
|
||||||
nil, "", "some-other-new-idp-name",
|
nil, "", "some-other-new-idp-name", "oidc",
|
||||||
), // updated expectation
|
), // updated expectation
|
||||||
"nonce": happyNonce,
|
"nonce": happyNonce,
|
||||||
"code_challenge": expectedUpstreamCodeChallenge,
|
"code_challenge": expectedUpstreamCodeChallenge,
|
||||||
|
@ -48,7 +48,7 @@ const (
|
|||||||
happyDownstreamCSRF = "test-csrf"
|
happyDownstreamCSRF = "test-csrf"
|
||||||
happyDownstreamPKCE = "test-pkce"
|
happyDownstreamPKCE = "test-pkce"
|
||||||
happyDownstreamNonce = "test-nonce"
|
happyDownstreamNonce = "test-nonce"
|
||||||
happyDownstreamStateVersion = "1"
|
happyDownstreamStateVersion = "2"
|
||||||
|
|
||||||
downstreamIssuer = "https://my-downstream-issuer.com/path"
|
downstreamIssuer = "https://my-downstream-issuer.com/path"
|
||||||
downstreamRedirectURI = "http://127.0.0.1/callback"
|
downstreamRedirectURI = "http://127.0.0.1/callback"
|
||||||
@ -1162,6 +1162,7 @@ func happyUpstreamStateParam() *upstreamStateParamBuilder {
|
|||||||
return &upstreamStateParamBuilder{
|
return &upstreamStateParamBuilder{
|
||||||
U: happyUpstreamIDPName,
|
U: happyUpstreamIDPName,
|
||||||
P: happyDownstreamRequestParams,
|
P: happyDownstreamRequestParams,
|
||||||
|
T: "oidc",
|
||||||
N: happyDownstreamNonce,
|
N: happyDownstreamNonce,
|
||||||
C: happyDownstreamCSRF,
|
C: happyDownstreamCSRF,
|
||||||
K: happyDownstreamPKCE,
|
K: happyDownstreamPKCE,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package manager
|
package manager
|
||||||
@ -8,6 +8,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/oidc/login"
|
||||||
|
|
||||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/oidc"
|
"go.pinniped.dev/internal/oidc"
|
||||||
@ -134,6 +136,8 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs
|
|||||||
oauthHelperWithKubeStorage,
|
oauthHelperWithKubeStorage,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
m.providerHandlers[(issuerHostWithPath + oidc.PinnipedLoginPath)] = login.NewHandler()
|
||||||
|
|
||||||
plog.Debug("oidc provider manager added or updated issuer", "issuer", issuer)
|
plog.Debug("oidc provider manager added or updated issuer", "issuer", issuer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -830,6 +830,7 @@ func NewTestUpstreamOIDCIdentityProviderBuilder() *TestUpstreamOIDCIdentityProvi
|
|||||||
type ExpectedUpstreamStateParamFormat struct {
|
type ExpectedUpstreamStateParamFormat struct {
|
||||||
P string `json:"p"`
|
P string `json:"p"`
|
||||||
U string `json:"u"`
|
U string `json:"u"`
|
||||||
|
T string `json:"t"`
|
||||||
N string `json:"n"`
|
N string `json:"n"`
|
||||||
C string `json:"c"`
|
C string `json:"c"`
|
||||||
K string `json:"k"`
|
K string `json:"k"`
|
||||||
|
@ -964,6 +964,122 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo
|
|||||||
expectedGroups,
|
expectedGroups,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Add an OIDC upstream IDP and try using it to authenticate during kubectl commands.
|
||||||
|
t.Run("with Supervisor LDAP upstream IDP and browser flow", func(t *testing.T) {
|
||||||
|
testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
// Start a fresh browser driver because we don't want to share cookies between the various tests in this file.
|
||||||
|
page := browsertest.Open(t)
|
||||||
|
|
||||||
|
expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue
|
||||||
|
|
||||||
|
setupClusterForEndToEndLDAPTest(t, expectedUsername, env)
|
||||||
|
|
||||||
|
// Use a specific session cache for this test.
|
||||||
|
sessionCachePath := tempDir + "/ldap-test-sessions.yaml"
|
||||||
|
|
||||||
|
kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{
|
||||||
|
"get", "kubeconfig",
|
||||||
|
"--concierge-api-group-suffix", env.APIGroupSuffix,
|
||||||
|
"--concierge-authenticator-type", "jwt",
|
||||||
|
"--concierge-authenticator-name", authenticator.Name,
|
||||||
|
"--oidc-skip-browser",
|
||||||
|
"--oidc-ca-bundle", testCABundlePath,
|
||||||
|
"--upstream-identity-provider-flow", "browser_authcode",
|
||||||
|
"--oidc-session-cache", sessionCachePath,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Run "kubectl get namespaces" which should trigger a browser login via the plugin.
|
||||||
|
kubectlCmd := exec.CommandContext(testCtx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath, "-v", "6")
|
||||||
|
kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...)
|
||||||
|
|
||||||
|
// Wrap the stdout and stderr pipes with TeeReaders which will copy each incremental read to an
|
||||||
|
// in-memory buffer, so we can have the full output available to us at the end.
|
||||||
|
originalStderrPipe, err := kubectlCmd.StderrPipe()
|
||||||
|
require.NoError(t, err)
|
||||||
|
originalStdoutPipe, err := kubectlCmd.StdoutPipe()
|
||||||
|
require.NoError(t, err)
|
||||||
|
var stderrPipeBuf, stdoutPipeBuf bytes.Buffer
|
||||||
|
stderrPipe := io.TeeReader(originalStderrPipe, &stderrPipeBuf)
|
||||||
|
stdoutPipe := io.TeeReader(originalStdoutPipe, &stdoutPipeBuf)
|
||||||
|
|
||||||
|
t.Logf("starting kubectl subprocess")
|
||||||
|
require.NoError(t, kubectlCmd.Start())
|
||||||
|
t.Cleanup(func() {
|
||||||
|
// Consume readers so that the tee buffers will contain all the output so far.
|
||||||
|
_, stdoutReadAllErr := readAllCtx(testCtx, stdoutPipe)
|
||||||
|
_, stderrReadAllErr := readAllCtx(testCtx, stderrPipe)
|
||||||
|
|
||||||
|
// Note that Wait closes the stdout/stderr pipes, so we don't need to close them ourselves.
|
||||||
|
waitErr := kubectlCmd.Wait()
|
||||||
|
t.Logf("kubectl subprocess exited with code %d", kubectlCmd.ProcessState.ExitCode())
|
||||||
|
|
||||||
|
// Upon failure, print the full output so far of the kubectl command.
|
||||||
|
var testAlreadyFailedErr error
|
||||||
|
if t.Failed() {
|
||||||
|
testAlreadyFailedErr = errors.New("test failed prior to clean up function")
|
||||||
|
}
|
||||||
|
cleanupErrs := utilerrors.NewAggregate([]error{waitErr, stdoutReadAllErr, stderrReadAllErr, testAlreadyFailedErr})
|
||||||
|
|
||||||
|
if cleanupErrs != nil {
|
||||||
|
t.Logf("kubectl stdout was:\n----start of stdout\n%s\n----end of stdout", stdoutPipeBuf.String())
|
||||||
|
t.Logf("kubectl stderr was:\n----start of stderr\n%s\n----end of stderr", stderrPipeBuf.String())
|
||||||
|
}
|
||||||
|
require.NoErrorf(t, cleanupErrs, "kubectl process did not exit cleanly and/or the test failed. "+
|
||||||
|
"Note: if kubectl's first call to the Pinniped CLI results in the Pinniped CLI returning an error, "+
|
||||||
|
"then kubectl may call the Pinniped CLI again, which may hang because it will wait for the user "+
|
||||||
|
"to finish the login. This test will kill the kubectl process after a timeout. In this case, the "+
|
||||||
|
" kubectl output printed above will include multiple prompts for the user to enter their authcode.",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start a background goroutine to read stderr from the CLI and parse out the login URL.
|
||||||
|
loginURLChan := make(chan string, 1)
|
||||||
|
spawnTestGoroutine(testCtx, t, func() error {
|
||||||
|
reader := bufio.NewReader(testlib.NewLoggerReader(t, "stderr", stderrPipe))
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
for scanner.Scan() {
|
||||||
|
loginURL, err := url.Parse(strings.TrimSpace(scanner.Text()))
|
||||||
|
if err == nil && loginURL.Scheme == "https" {
|
||||||
|
loginURLChan <- loginURL.String() // this channel is buffered so this will not block
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("expected stderr to contain login URL")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start a background goroutine to read stdout from kubectl and return the result as a string.
|
||||||
|
kubectlOutputChan := make(chan string, 1)
|
||||||
|
spawnTestGoroutine(testCtx, t, func() error {
|
||||||
|
output, err := readAllCtx(testCtx, stdoutPipe)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.Logf("kubectl output:\n%s\n", output)
|
||||||
|
kubectlOutputChan <- string(output) // this channel is buffered so this will not block
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for the CLI to print out the login URL and open the browser to it.
|
||||||
|
t.Logf("waiting for CLI to output login URL")
|
||||||
|
var loginURL string
|
||||||
|
select {
|
||||||
|
case <-time.After(1 * time.Minute):
|
||||||
|
require.Fail(t, "timed out waiting for login URL")
|
||||||
|
case loginURL = <-loginURLChan:
|
||||||
|
}
|
||||||
|
t.Logf("navigating to login page: %q", loginURL)
|
||||||
|
require.NoError(t, page.Navigate(loginURL))
|
||||||
|
|
||||||
|
// Expect to be redirected to the supervisor's ldap login page.
|
||||||
|
t.Logf("waiting for redirect to supervisor ldap login page")
|
||||||
|
regex := regexp.MustCompile(`\A` + downstream.Spec.Issuer + `/login.+`)
|
||||||
|
browsertest.WaitForURL(t, page, regex)
|
||||||
|
|
||||||
|
// TODO actually log in :P
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupClusterForEndToEndLDAPTest(t *testing.T, username string, env *testlib.TestEnv) {
|
func setupClusterForEndToEndLDAPTest(t *testing.T, username string, env *testlib.TestEnv) {
|
||||||
|
Loading…
Reference in New Issue
Block a user