2020-11-13 23:59:51 +00:00
|
|
|
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
2020-11-20 01:57:07 +00:00
|
|
|
package oidctestutil
|
2020-11-13 23:59:51 +00:00
|
|
|
|
2020-11-18 21:38:13 +00:00
|
|
|
import (
|
|
|
|
"context"
|
2020-12-04 15:06:55 +00:00
|
|
|
"crypto"
|
|
|
|
"crypto/ecdsa"
|
|
|
|
"fmt"
|
2020-11-18 21:38:13 +00:00
|
|
|
"net/url"
|
2020-12-04 15:06:55 +00:00
|
|
|
"testing"
|
2020-11-18 21:38:13 +00:00
|
|
|
|
2021-01-20 17:54:44 +00:00
|
|
|
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
2020-12-04 15:06:55 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
2020-11-30 23:08:27 +00:00
|
|
|
"golang.org/x/oauth2"
|
2020-12-04 15:06:55 +00:00
|
|
|
"gopkg.in/square/go-jose.v2"
|
2020-11-30 23:08:27 +00:00
|
|
|
|
2020-11-18 21:38:13 +00:00
|
|
|
"go.pinniped.dev/internal/oidc/provider"
|
2020-11-20 23:13:25 +00:00
|
|
|
"go.pinniped.dev/pkg/oidcclient/nonce"
|
2020-11-30 23:02:03 +00:00
|
|
|
"go.pinniped.dev/pkg/oidcclient/oidctypes"
|
2020-11-20 23:13:25 +00:00
|
|
|
"go.pinniped.dev/pkg/oidcclient/pkce"
|
2020-11-18 21:38:13 +00:00
|
|
|
)
|
2020-11-13 23:59:51 +00:00
|
|
|
|
|
|
|
// Test helpers for the OIDC package.
|
|
|
|
|
2020-11-19 15:20:46 +00:00
|
|
|
// ExchangeAuthcodeAndValidateTokenArgs is a POGO (plain old go object?) used to spy on calls to
|
|
|
|
// TestUpstreamOIDCIdentityProvider.ExchangeAuthcodeAndValidateTokensFunc().
|
|
|
|
type ExchangeAuthcodeAndValidateTokenArgs struct {
|
|
|
|
Ctx context.Context
|
|
|
|
Authcode string
|
|
|
|
PKCECodeVerifier pkce.Code
|
|
|
|
ExpectedIDTokenNonce nonce.Nonce
|
2020-12-02 16:36:07 +00:00
|
|
|
RedirectURI string
|
2020-11-19 15:20:46 +00:00
|
|
|
}
|
|
|
|
|
2020-11-18 21:38:13 +00:00
|
|
|
type TestUpstreamOIDCIdentityProvider struct {
|
|
|
|
Name string
|
|
|
|
ClientID string
|
|
|
|
AuthorizationURL url.URL
|
|
|
|
UsernameClaim string
|
|
|
|
GroupsClaim string
|
|
|
|
Scopes []string
|
|
|
|
ExchangeAuthcodeAndValidateTokensFunc func(
|
|
|
|
ctx context.Context,
|
|
|
|
authcode string,
|
|
|
|
pkceCodeVerifier pkce.Code,
|
|
|
|
expectedIDTokenNonce nonce.Nonce,
|
2020-12-04 21:33:36 +00:00
|
|
|
) (*oidctypes.Token, error)
|
2020-11-19 15:20:46 +00:00
|
|
|
|
|
|
|
exchangeAuthcodeAndValidateTokensCallCount int
|
|
|
|
exchangeAuthcodeAndValidateTokensArgs []*ExchangeAuthcodeAndValidateTokenArgs
|
2020-11-18 21:38:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (u *TestUpstreamOIDCIdentityProvider) GetName() string {
|
|
|
|
return u.Name
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *TestUpstreamOIDCIdentityProvider) GetClientID() string {
|
|
|
|
return u.ClientID
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *TestUpstreamOIDCIdentityProvider) GetAuthorizationURL() *url.URL {
|
|
|
|
return &u.AuthorizationURL
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *TestUpstreamOIDCIdentityProvider) GetScopes() []string {
|
|
|
|
return u.Scopes
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *TestUpstreamOIDCIdentityProvider) GetUsernameClaim() string {
|
|
|
|
return u.UsernameClaim
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *TestUpstreamOIDCIdentityProvider) GetGroupsClaim() string {
|
|
|
|
return u.GroupsClaim
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *TestUpstreamOIDCIdentityProvider) ExchangeAuthcodeAndValidateTokens(
|
|
|
|
ctx context.Context,
|
|
|
|
authcode string,
|
|
|
|
pkceCodeVerifier pkce.Code,
|
|
|
|
expectedIDTokenNonce nonce.Nonce,
|
2020-12-02 16:36:07 +00:00
|
|
|
redirectURI string,
|
2020-12-04 21:33:36 +00:00
|
|
|
) (*oidctypes.Token, error) {
|
2020-11-19 15:20:46 +00:00
|
|
|
if u.exchangeAuthcodeAndValidateTokensArgs == nil {
|
|
|
|
u.exchangeAuthcodeAndValidateTokensArgs = make([]*ExchangeAuthcodeAndValidateTokenArgs, 0)
|
|
|
|
}
|
|
|
|
u.exchangeAuthcodeAndValidateTokensCallCount++
|
|
|
|
u.exchangeAuthcodeAndValidateTokensArgs = append(u.exchangeAuthcodeAndValidateTokensArgs, &ExchangeAuthcodeAndValidateTokenArgs{
|
|
|
|
Ctx: ctx,
|
|
|
|
Authcode: authcode,
|
|
|
|
PKCECodeVerifier: pkceCodeVerifier,
|
|
|
|
ExpectedIDTokenNonce: expectedIDTokenNonce,
|
2020-12-02 16:36:07 +00:00
|
|
|
RedirectURI: redirectURI,
|
2020-11-19 15:20:46 +00:00
|
|
|
})
|
2020-11-18 21:38:13 +00:00
|
|
|
return u.ExchangeAuthcodeAndValidateTokensFunc(ctx, authcode, pkceCodeVerifier, expectedIDTokenNonce)
|
|
|
|
}
|
|
|
|
|
2020-11-19 15:20:46 +00:00
|
|
|
func (u *TestUpstreamOIDCIdentityProvider) ExchangeAuthcodeAndValidateTokensCallCount() int {
|
|
|
|
return u.exchangeAuthcodeAndValidateTokensCallCount
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *TestUpstreamOIDCIdentityProvider) ExchangeAuthcodeAndValidateTokensArgs(call int) *ExchangeAuthcodeAndValidateTokenArgs {
|
|
|
|
if u.exchangeAuthcodeAndValidateTokensArgs == nil {
|
|
|
|
u.exchangeAuthcodeAndValidateTokensArgs = make([]*ExchangeAuthcodeAndValidateTokenArgs, 0)
|
|
|
|
}
|
|
|
|
return u.exchangeAuthcodeAndValidateTokensArgs[call]
|
|
|
|
}
|
|
|
|
|
2020-12-04 21:33:36 +00:00
|
|
|
func (u *TestUpstreamOIDCIdentityProvider) ValidateToken(_ context.Context, _ *oauth2.Token, _ nonce.Nonce) (*oidctypes.Token, error) {
|
2020-11-30 23:08:27 +00:00
|
|
|
panic("implement me")
|
|
|
|
}
|
|
|
|
|
2020-11-19 15:20:46 +00:00
|
|
|
func NewIDPListGetter(upstreamOIDCIdentityProviders ...*TestUpstreamOIDCIdentityProvider) provider.DynamicUpstreamIDPProvider {
|
2020-11-13 23:59:51 +00:00
|
|
|
idpProvider := provider.NewDynamicUpstreamIDPProvider()
|
2020-11-18 21:38:13 +00:00
|
|
|
upstreams := make([]provider.UpstreamOIDCIdentityProviderI, len(upstreamOIDCIdentityProviders))
|
|
|
|
for i := range upstreamOIDCIdentityProviders {
|
2020-11-19 15:20:46 +00:00
|
|
|
upstreams[i] = provider.UpstreamOIDCIdentityProviderI(upstreamOIDCIdentityProviders[i])
|
2020-11-18 21:38:13 +00:00
|
|
|
}
|
|
|
|
idpProvider.SetIDPList(upstreams)
|
2020-11-13 23:59:51 +00:00
|
|
|
return idpProvider
|
|
|
|
}
|
|
|
|
|
|
|
|
// Declare a separate type from the production code to ensure that the state param's contents was serialized
|
|
|
|
// in the format that we expect, with the json keys that we expect, etc. This also ensure that the order of
|
|
|
|
// the serialized fields is the same, which doesn't really matter expect that we can make simpler equality
|
|
|
|
// assertions about the redirect URL in this test.
|
|
|
|
type ExpectedUpstreamStateParamFormat struct {
|
|
|
|
P string `json:"p"`
|
2020-11-20 21:14:45 +00:00
|
|
|
U string `json:"u"`
|
2020-11-13 23:59:51 +00:00
|
|
|
N string `json:"n"`
|
|
|
|
C string `json:"c"`
|
|
|
|
K string `json:"k"`
|
|
|
|
V string `json:"v"`
|
|
|
|
}
|
2020-12-04 15:06:55 +00:00
|
|
|
|
|
|
|
type staticKeySet struct {
|
|
|
|
publicKey crypto.PublicKey
|
|
|
|
}
|
|
|
|
|
|
|
|
func newStaticKeySet(publicKey crypto.PublicKey) coreosoidc.KeySet {
|
|
|
|
return &staticKeySet{publicKey}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *staticKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
|
|
|
|
jws, err := jose.ParseSigned(jwt)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("oidc: malformed jwt: %w", err)
|
|
|
|
}
|
|
|
|
return jws.Verify(s.publicKey)
|
|
|
|
}
|
|
|
|
|
|
|
|
// VerifyECDSAIDToken verifies that the provided idToken was issued via the provided jwtSigningKey.
|
|
|
|
// It also performs some light validation on the claims, i.e., it makes sure the provided idToken
|
|
|
|
// has the provided issuer and clientID.
|
|
|
|
//
|
|
|
|
// Further validation can be done via callers via the returned coreosoidc.IDToken.
|
|
|
|
func VerifyECDSAIDToken(
|
|
|
|
t *testing.T,
|
|
|
|
issuer, clientID string,
|
|
|
|
jwtSigningKey *ecdsa.PrivateKey,
|
|
|
|
idToken string,
|
|
|
|
) *coreosoidc.IDToken {
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
keySet := newStaticKeySet(jwtSigningKey.Public())
|
|
|
|
verifyConfig := coreosoidc.Config{ClientID: clientID, SupportedSigningAlgs: []string{coreosoidc.ES256}}
|
|
|
|
verifier := coreosoidc.NewVerifier(issuer, keySet, &verifyConfig)
|
|
|
|
token, err := verifier.Verify(context.Background(), idToken)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
return token
|
|
|
|
}
|