e42f5488fa
- Add dynamic client unit tests for the upstream OIDC callback and POST login endpoints. - Enhance a few log statements to print the full fosite error messages into the logs where they were previously only printing the name of the error type.
971 lines
49 KiB
Go
971 lines
49 KiB
Go
// 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"
|
|
"golang.org/x/crypto/bcrypt"
|
|
"k8s.io/apiserver/pkg/authentication/user"
|
|
"k8s.io/client-go/kubernetes/fake"
|
|
|
|
supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/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"
|
|
downstreamPinnipedCLIClientID = "pinniped-cli"
|
|
downstreamDynamicClientID = "client.oauth.pinniped.dev-test-name"
|
|
downstreamDynamicClientUID = "fake-client-uid"
|
|
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", "groups"}
|
|
happyDownstreamScopesGranted := []string{"openid", "groups"}
|
|
|
|
happyDownstreamRequestParamsQuery := url.Values{
|
|
"response_type": []string{"code"},
|
|
"scope": []string{strings.Join(happyDownstreamScopesRequested, " ")},
|
|
"client_id": []string{downstreamPinnipedCLIClientID},
|
|
"state": []string{happyDownstreamState},
|
|
"nonce": []string{downstreamNonce},
|
|
"code_challenge": []string{downstreamPKCEChallenge},
|
|
"code_challenge_method": []string{downstreamPKCEChallengeMethod},
|
|
"redirect_uri": []string{downstreamRedirectURI},
|
|
}
|
|
happyDownstreamRequestParams := happyDownstreamRequestParamsQuery.Encode()
|
|
|
|
happyDownstreamRequestParamsQueryForDynamicClient := shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
|
map[string]string{"client_id": downstreamDynamicClientID},
|
|
)
|
|
happyDownstreamRequestParamsForDynamicClient := happyDownstreamRequestParamsQueryForDynamicClient.Encode()
|
|
|
|
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(©OfHappyLDAPDecodedState)
|
|
return ©OfHappyLDAPDecodedState
|
|
}
|
|
|
|
happyLDAPDecodedStateForDynamicClient := modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
|
|
data.AuthParams = happyDownstreamRequestParamsForDynamicClient
|
|
})
|
|
|
|
happyActiveDirectoryDecodedState := modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
|
|
data.UpstreamName = activeDirectoryUpstreamName
|
|
data.UpstreamType = activeDirectoryUpstreamType
|
|
})
|
|
|
|
happyActiveDirectoryDecodedStateForDynamicClient := modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
|
|
data.AuthParams = happyDownstreamRequestParamsForDynamicClient
|
|
data.UpstreamName = activeDirectoryUpstreamName
|
|
data.UpstreamType = activeDirectoryUpstreamType
|
|
})
|
|
|
|
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\+groups&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
|
|
}
|
|
|
|
addFullyCapableDynamicClientAndSecretToKubeResources := func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
|
|
oidcClient, secret := testutil.FullyCapableOIDCClientAndStorageSecret(t,
|
|
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost})
|
|
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
|
|
require.NoError(t, kubeClient.Tracker().Add(secret))
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
idps *oidctestutil.UpstreamIDPListerBuilder
|
|
kubeResources func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset)
|
|
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
|
|
wantBodyFormResponseRegexp string // for form_post html page matching instead
|
|
wantDownstreamRedirectURI string
|
|
wantDownstreamGrantedScopes []string
|
|
wantDownstreamIDTokenSubject string
|
|
wantDownstreamIDTokenUsername string
|
|
wantDownstreamIDTokenGroups []string
|
|
wantDownstreamRequestedScopes []string
|
|
wantDownstreamPKCEChallenge string
|
|
wantDownstreamPKCEChallengeMethod string
|
|
wantDownstreamNonce string
|
|
wantDownstreamClient 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,
|
|
wantDownstreamClient: downstreamPinnipedCLIClientID,
|
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
|
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
|
|
},
|
|
{
|
|
name: "happy LDAP login with dynamic client",
|
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().
|
|
WithLDAP(&upstreamLDAPIdentityProvider). // should pick this one
|
|
WithActiveDirectory(&erroringUpstreamLDAPIdentityProvider),
|
|
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
|
|
decodedState: happyLDAPDecodedStateForDynamicClient,
|
|
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,
|
|
wantDownstreamClient: downstreamDynamicClientID,
|
|
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,
|
|
wantDownstreamClient: downstreamPinnipedCLIClientID,
|
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
|
wantDownstreamCustomSessionData: expectedHappyActiveDirectoryUpstreamCustomSession,
|
|
},
|
|
{
|
|
name: "happy AD login with dynamic client",
|
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().
|
|
WithLDAP(&erroringUpstreamLDAPIdentityProvider).
|
|
WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), // should pick this one
|
|
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
|
|
decodedState: happyActiveDirectoryDecodedStateForDynamicClient,
|
|
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,
|
|
wantDownstreamClient: downstreamDynamicClientID,
|
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
|
wantDownstreamCustomSessionData: expectedHappyActiveDirectoryUpstreamCustomSession,
|
|
},
|
|
{
|
|
name: "happy LDAP login when downstream response_mode=form_post returns 200 with HTML+JS form",
|
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
|
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
|
map[string]string{"response_mode": "form_post"},
|
|
).Encode()
|
|
}),
|
|
formParams: happyUsernamePasswordFormParams,
|
|
wantStatus: http.StatusOK,
|
|
wantContentType: htmlContentType,
|
|
wantBodyFormResponseRegexp: `(?s)<html.*<script>.*To finish logging in, paste this authorization code` +
|
|
`.*<form>.*<code id="manual-auth-code">(.+)</code>.*</html>`, // "(?s)" means match "." across newlines
|
|
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
|
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
|
|
wantDownstreamIDTokenGroups: happyLDAPGroups,
|
|
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
|
wantDownstreamRedirectURI: downstreamRedirectURI,
|
|
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
|
|
wantDownstreamNonce: downstreamNonce,
|
|
wantDownstreamClient: downstreamPinnipedCLIClientID,
|
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
|
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
|
|
},
|
|
{
|
|
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) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
|
map[string]string{"redirect_uri": "http://127.0.0.1:4242/callback"},
|
|
).Encode()
|
|
}),
|
|
formParams: happyUsernamePasswordFormParams,
|
|
wantStatus: http.StatusSeeOther,
|
|
wantContentType: htmlContentType,
|
|
wantBodyString: "",
|
|
wantRedirectLocationRegexp: "http://127.0.0.1:4242/callback" + `\?code=([^&]+)&scope=openid\+groups&state=` + happyDownstreamState,
|
|
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
|
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
|
|
wantDownstreamIDTokenGroups: happyLDAPGroups,
|
|
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
|
wantDownstreamRedirectURI: "http://127.0.0.1:4242/callback",
|
|
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
|
|
wantDownstreamNonce: downstreamNonce,
|
|
wantDownstreamClient: downstreamPinnipedCLIClientID,
|
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
|
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
|
|
},
|
|
{
|
|
name: "happy LDAP login when downstream redirect uri matches what is configured for client except for the port number with dynamic client",
|
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
|
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
|
|
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient,
|
|
map[string]string{"redirect_uri": "http://127.0.0.1:4242/callback"},
|
|
).Encode()
|
|
}),
|
|
formParams: happyUsernamePasswordFormParams,
|
|
wantStatus: http.StatusSeeOther,
|
|
wantContentType: htmlContentType,
|
|
wantBodyString: "",
|
|
wantRedirectLocationRegexp: "http://127.0.0.1:4242/callback" + `\?code=([^&]+)&scope=openid\+groups&state=` + happyDownstreamState,
|
|
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
|
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
|
|
wantDownstreamIDTokenGroups: happyLDAPGroups,
|
|
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
|
wantDownstreamRedirectURI: "http://127.0.0.1:4242/callback",
|
|
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
|
|
wantDownstreamNonce: downstreamNonce,
|
|
wantDownstreamClient: downstreamDynamicClientID,
|
|
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) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
|
map[string]string{"scope": "openid offline_access pinniped:request-audience"},
|
|
).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,
|
|
wantDownstreamClient: downstreamPinnipedCLIClientID,
|
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
|
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
|
|
},
|
|
{
|
|
name: "happy LDAP login when there are additional allowed downstream requested scopes with dynamic client",
|
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
|
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
|
|
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient,
|
|
map[string]string{"scope": "openid offline_access pinniped:request-audience"},
|
|
).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,
|
|
wantDownstreamClient: downstreamDynamicClientID,
|
|
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) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
|
map[string]string{
|
|
"scope": "email",
|
|
// The following prompt value is illegal when openid is requested, but note that openid is not requested.
|
|
"prompt": "none login",
|
|
},
|
|
).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,
|
|
wantDownstreamClient: downstreamPinnipedCLIClientID,
|
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
|
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
|
|
},
|
|
{
|
|
name: "happy LDAP login when groups scope is not requested",
|
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().
|
|
WithLDAP(&upstreamLDAPIdentityProvider). // should pick this one
|
|
WithActiveDirectory(&erroringUpstreamLDAPIdentityProvider),
|
|
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
|
map[string]string{"scope": "openid"},
|
|
).Encode()
|
|
}),
|
|
formParams: happyUsernamePasswordFormParams,
|
|
wantStatus: http.StatusSeeOther,
|
|
wantContentType: htmlContentType,
|
|
wantBodyString: "",
|
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid&state=` + happyDownstreamState,
|
|
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
|
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
|
|
wantDownstreamRequestedScopes: []string{"openid"},
|
|
wantDownstreamRedirectURI: downstreamRedirectURI,
|
|
wantDownstreamGrantedScopes: []string{"openid"},
|
|
wantDownstreamNonce: downstreamNonce,
|
|
wantDownstreamClient: downstreamPinnipedCLIClientID,
|
|
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) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
|
map[string]string{"redirect_uri": "http://127.0.0.1/wrong_callback"},
|
|
).Encode()
|
|
}),
|
|
formParams: happyUsernamePasswordFormParams,
|
|
wantErr: "error using state downstream auth params",
|
|
},
|
|
{
|
|
name: "downstream redirect uri does not match what is configured for client with dynamic client",
|
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
|
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
|
|
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient,
|
|
map[string]string{"redirect_uri": "http://127.0.0.1/wrong_callback"},
|
|
).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) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
|
map[string]string{"client_id": "wrong_client_id"},
|
|
).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) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
|
map[string]string{"client_id": ""},
|
|
).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) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
|
map[string]string{"response_type": "unsupported"},
|
|
).Encode()
|
|
}),
|
|
formParams: happyUsernamePasswordFormParams,
|
|
wantErr: "error using state downstream auth params",
|
|
},
|
|
{
|
|
name: "response type form_post is unsupported for dynamic clients",
|
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
|
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
|
|
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient,
|
|
map[string]string{"response_type": "form_post"},
|
|
).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) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
|
map[string]string{"response_type": ""},
|
|
).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) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
|
map[string]string{"code_challenge": ""},
|
|
).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) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
|
map[string]string{"code_challenge_method": "this-is-not-a-valid-pkce-alg"},
|
|
).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) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
|
map[string]string{"code_challenge_method": "plain"}, // plain is not allowed
|
|
).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) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
|
map[string]string{"code_challenge_method": ""},
|
|
).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 with dynamic client",
|
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
|
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
|
|
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient,
|
|
map[string]string{"code_challenge_method": ""},
|
|
).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) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
|
map[string]string{"prompt": "none login"},
|
|
).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) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
|
map[string]string{"state": "short"},
|
|
).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) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
|
map[string]string{"scope": "openid offline_access pinniped:request-audience scope_not_allowed"},
|
|
).Encode()
|
|
}),
|
|
formParams: happyUsernamePasswordFormParams,
|
|
wantErr: "error using state downstream auth params",
|
|
},
|
|
{
|
|
name: "downstream scopes do not match what is configured for client with dynamic client",
|
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
|
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
|
|
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
|
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient,
|
|
map[string]string{"scope": "openid offline_access pinniped:request-audience scope_not_allowed"},
|
|
).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) {
|
|
t.Parallel()
|
|
|
|
kubeClient := fake.NewSimpleClientset()
|
|
supervisorClient := supervisorfake.NewSimpleClientset()
|
|
secretsClient := kubeClient.CoreV1().Secrets("some-namespace")
|
|
oidcClientsClient := supervisorClient.ConfigV1alpha1().OIDCClients("some-namespace")
|
|
|
|
if tt.kubeResources != nil {
|
|
tt.kubeResources(t, supervisorClient, kubeClient)
|
|
}
|
|
|
|
// 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()
|
|
// Use lower minimum required bcrypt cost than we would use in production to keep unit the tests fast.
|
|
kubeOauthStore := oidc.NewKubeStorage(secretsClient, oidcClientsClient, timeoutsConfiguration, bcrypt.MinCost)
|
|
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, oidctestutil.FilterClientSecretCreateActions(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)
|
|
|
|
actualLocation := rsp.Header().Get("Location")
|
|
|
|
switch {
|
|
case tt.wantRedirectLocationRegexp != "":
|
|
// Expecting a success redirect to the client.
|
|
require.Equal(t, tt.wantBodyString, rsp.Body.String())
|
|
require.Len(t, rsp.Header().Values("Location"), 1)
|
|
oidctestutil.RequireAuthCodeRegexpMatch(
|
|
t,
|
|
actualLocation,
|
|
tt.wantRedirectLocationRegexp,
|
|
kubeClient,
|
|
secretsClient,
|
|
kubeOauthStore,
|
|
tt.wantDownstreamGrantedScopes,
|
|
tt.wantDownstreamIDTokenSubject,
|
|
tt.wantDownstreamIDTokenUsername,
|
|
tt.wantDownstreamIDTokenGroups,
|
|
tt.wantDownstreamRequestedScopes,
|
|
tt.wantDownstreamPKCEChallenge,
|
|
tt.wantDownstreamPKCEChallengeMethod,
|
|
tt.wantDownstreamNonce,
|
|
tt.wantDownstreamClient,
|
|
tt.wantDownstreamRedirectURI,
|
|
tt.wantDownstreamCustomSessionData,
|
|
)
|
|
case tt.wantRedirectToLoginPageError != "":
|
|
// Expecting an error redirect to the login UI page.
|
|
require.Equal(t, tt.wantBodyString, rsp.Body.String())
|
|
expectedLocation := downstreamIssuer + oidc.PinnipedLoginPath +
|
|
"?err=" + tt.wantRedirectToLoginPageError + "&state=" + happyEncodedUpstreamState
|
|
require.Equal(t, expectedLocation, actualLocation)
|
|
require.Len(t, oidctestutil.FilterClientSecretCreateActions(kubeClient.Actions()), tt.wantUnnecessaryStoredRecords)
|
|
case tt.wantRedirectLocationString != "":
|
|
// Expecting an error redirect to the client.
|
|
require.Equal(t, tt.wantBodyString, rsp.Body.String())
|
|
require.Equal(t, tt.wantRedirectLocationString, actualLocation)
|
|
require.Len(t, oidctestutil.FilterClientSecretCreateActions(kubeClient.Actions()), tt.wantUnnecessaryStoredRecords)
|
|
case tt.wantBodyFormResponseRegexp != "":
|
|
// Expecting the body of the response to be a html page with a form (for "response_mode=form_post").
|
|
_, hasLocationHeader := rsp.Header()["Location"]
|
|
require.False(t, hasLocationHeader)
|
|
oidctestutil.RequireAuthCodeRegexpMatch(
|
|
t,
|
|
rsp.Body.String(),
|
|
tt.wantBodyFormResponseRegexp,
|
|
kubeClient,
|
|
secretsClient,
|
|
kubeOauthStore,
|
|
tt.wantDownstreamGrantedScopes,
|
|
tt.wantDownstreamIDTokenSubject,
|
|
tt.wantDownstreamIDTokenUsername,
|
|
tt.wantDownstreamIDTokenGroups,
|
|
tt.wantDownstreamRequestedScopes,
|
|
tt.wantDownstreamPKCEChallenge,
|
|
tt.wantDownstreamPKCEChallengeMethod,
|
|
tt.wantDownstreamNonce,
|
|
tt.wantDownstreamClient,
|
|
tt.wantDownstreamRedirectURI,
|
|
tt.wantDownstreamCustomSessionData,
|
|
)
|
|
default:
|
|
require.Failf(t, "test should have expected a redirect or form body",
|
|
"actual location was %q", actualLocation)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func shallowCopyAndModifyQuery(query url.Values, modifications map[string]string) url.Values {
|
|
copied := url.Values{}
|
|
for key, value := range query {
|
|
copied[key] = value
|
|
}
|
|
for key, value := range modifications {
|
|
if value == "" {
|
|
copied.Del(key)
|
|
} else {
|
|
copied[key] = []string{value}
|
|
}
|
|
}
|
|
return copied
|
|
}
|