Passing signing key through to the token endpoint
This commit is contained in:
parent
58237d0e7d
commit
0bb2b10b3b
@ -5,6 +5,9 @@ package oidc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/constable"
|
||||||
|
|
||||||
"github.com/ory/fosite"
|
"github.com/ory/fosite"
|
||||||
"github.com/ory/fosite/compose"
|
"github.com/ory/fosite/compose"
|
||||||
@ -15,7 +18,6 @@ import (
|
|||||||
|
|
||||||
// TODO: doc me.
|
// TODO: doc me.
|
||||||
type dynamicOpenIDConnectECDSAStrategy struct {
|
type dynamicOpenIDConnectECDSAStrategy struct {
|
||||||
issuer string
|
|
||||||
fositeConfig *compose.Config
|
fositeConfig *compose.Config
|
||||||
jwksProvider jwks.DynamicJWKSProvider
|
jwksProvider jwks.DynamicJWKSProvider
|
||||||
}
|
}
|
||||||
@ -23,12 +25,10 @@ type dynamicOpenIDConnectECDSAStrategy struct {
|
|||||||
var _ openid.OpenIDConnectTokenStrategy = &dynamicOpenIDConnectECDSAStrategy{}
|
var _ openid.OpenIDConnectTokenStrategy = &dynamicOpenIDConnectECDSAStrategy{}
|
||||||
|
|
||||||
func newDynamicOpenIDConnectECDSAStrategy(
|
func newDynamicOpenIDConnectECDSAStrategy(
|
||||||
issuer string,
|
|
||||||
fositeConfig *compose.Config,
|
fositeConfig *compose.Config,
|
||||||
jwksProvider jwks.DynamicJWKSProvider,
|
jwksProvider jwks.DynamicJWKSProvider,
|
||||||
) *dynamicOpenIDConnectECDSAStrategy {
|
) *dynamicOpenIDConnectECDSAStrategy {
|
||||||
return &dynamicOpenIDConnectECDSAStrategy{
|
return &dynamicOpenIDConnectECDSAStrategy{
|
||||||
issuer: issuer,
|
|
||||||
fositeConfig: fositeConfig,
|
fositeConfig: fositeConfig,
|
||||||
jwksProvider: jwksProvider,
|
jwksProvider: jwksProvider,
|
||||||
}
|
}
|
||||||
@ -38,5 +38,15 @@ func (s *dynamicOpenIDConnectECDSAStrategy) GenerateIDToken(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
requester fosite.Requester,
|
requester fosite.Requester,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
return "", nil
|
_, activeJwk := s.jwksProvider.GetJWKS(s.fositeConfig.IDTokenIssuer)
|
||||||
|
if activeJwk == nil {
|
||||||
|
return "", constable.Error("No JWK found for issuer")
|
||||||
|
}
|
||||||
|
key, ok := activeJwk.Key.(*ecdsa.PrivateKey)
|
||||||
|
if !ok {
|
||||||
|
return "", constable.Error("JWK must be of type ecdsa")
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo write story/issue about caching this strategy
|
||||||
|
return compose.NewOpenIDConnectECDSAStrategy(s.fositeConfig, key).GenerateIDToken(ctx, requester)
|
||||||
}
|
}
|
||||||
|
@ -16,15 +16,20 @@ import (
|
|||||||
coreosoidc "github.com/coreos/go-oidc"
|
coreosoidc "github.com/coreos/go-oidc"
|
||||||
"github.com/ory/fosite"
|
"github.com/ory/fosite"
|
||||||
"github.com/ory/fosite/compose"
|
"github.com/ory/fosite/compose"
|
||||||
|
"github.com/ory/fosite/handler/openid"
|
||||||
|
"github.com/ory/fosite/token/jwt"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.pinniped.dev/internal/oidc/jwks"
|
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/oidc/jwks"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDynamicOpenIDConnectECDSAStrategy(t *testing.T) {
|
func TestDynamicOpenIDConnectECDSAStrategy(t *testing.T) {
|
||||||
const (
|
const (
|
||||||
goodIssuer = "https://some-good-issuer.com"
|
goodIssuer = "https://some-good-issuer.com"
|
||||||
clientID = "some-client-id"
|
clientID = "some-client-id"
|
||||||
|
goodSubject = "some-subject"
|
||||||
|
goodUsername = "some-username"
|
||||||
)
|
)
|
||||||
|
|
||||||
ecPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
ecPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
@ -60,7 +65,7 @@ func TestDynamicOpenIDConnectECDSAStrategy(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "jwks provider does not contain signing key for issuer",
|
name: "jwks provider does not contain signing key for issuer",
|
||||||
issuer: goodIssuer,
|
issuer: goodIssuer,
|
||||||
wantError: "some unkonwn key error",
|
wantError: "No JWK found for issuer",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "jwks provider contains signing key of wrong type for issuer",
|
name: "jwks provider contains signing key of wrong type for issuer",
|
||||||
@ -75,7 +80,7 @@ func TestDynamicOpenIDConnectECDSAStrategy(t *testing.T) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
wantError: "some invalid key type error",
|
wantError: "JWK must be of type ecdsa",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
@ -85,13 +90,22 @@ func TestDynamicOpenIDConnectECDSAStrategy(t *testing.T) {
|
|||||||
if test.jwksProvider != nil {
|
if test.jwksProvider != nil {
|
||||||
test.jwksProvider(jwksProvider)
|
test.jwksProvider(jwksProvider)
|
||||||
}
|
}
|
||||||
s := newDynamicOpenIDConnectECDSAStrategy(test.issuer, &compose.Config{}, jwksProvider)
|
s := newDynamicOpenIDConnectECDSAStrategy(
|
||||||
|
&compose.Config{IDTokenIssuer: test.issuer},
|
||||||
|
jwksProvider,
|
||||||
|
)
|
||||||
|
|
||||||
requester := &fosite.Request{
|
requester := &fosite.Request{
|
||||||
Client: &fosite.DefaultClient{
|
Client: &fosite.DefaultClient{
|
||||||
ID: clientID,
|
ID: clientID,
|
||||||
},
|
},
|
||||||
// Session: fositeopenid.DefaultSession{},
|
Session: &openid.DefaultSession{
|
||||||
|
Claims: &jwt.IDTokenClaims{
|
||||||
|
Subject: goodSubject,
|
||||||
|
},
|
||||||
|
Subject: goodSubject,
|
||||||
|
Username: goodUsername,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
idToken, err := s.GenerateIDToken(context.Background(), requester)
|
idToken, err := s.GenerateIDToken(context.Background(), requester)
|
||||||
if test.wantError != "" {
|
if test.wantError != "" {
|
||||||
|
@ -122,7 +122,7 @@ func FositeOauth2Helper(
|
|||||||
&compose.CommonStrategy{
|
&compose.CommonStrategy{
|
||||||
// Note that Fosite requires the HMAC secret to be at least 32 bytes.
|
// Note that Fosite requires the HMAC secret to be at least 32 bytes.
|
||||||
CoreStrategy: compose.NewOAuth2HMACStrategy(oauthConfig, hmacSecretOfLengthAtLeast32, nil),
|
CoreStrategy: compose.NewOAuth2HMACStrategy(oauthConfig, hmacSecretOfLengthAtLeast32, nil),
|
||||||
OpenIDConnectTokenStrategy: newDynamicOpenIDConnectECDSAStrategy(issuer, oauthConfig, jwksProvider),
|
OpenIDConnectTokenStrategy: newDynamicOpenIDConnectECDSAStrategy(oauthConfig, jwksProvider),
|
||||||
// OpenIDConnectTokenStrategy: compose.NewOpenIDConnectECDSAStrategy(oauthConfig, jwtSigningKey),
|
// OpenIDConnectTokenStrategy: compose.NewOpenIDConnectECDSAStrategy(oauthConfig, jwtSigningKey),
|
||||||
},
|
},
|
||||||
nil, // hasher, defaults to using BCrypt when nil. Used for hashing client secrets.
|
nil, // hasher, defaults to using BCrypt when nil. Used for hashing client secrets.
|
||||||
|
@ -82,7 +82,7 @@ func (m *Manager) SetProviders(oidcProviders ...*provider.OIDCProvider) {
|
|||||||
oauthHelperWithNullStorage := oidc.FositeOauth2Helper(oidc.NullStorage{}, issuer, fositeHMACSecretForThisProvider, nil)
|
oauthHelperWithNullStorage := oidc.FositeOauth2Helper(oidc.NullStorage{}, issuer, fositeHMACSecretForThisProvider, nil)
|
||||||
|
|
||||||
// For all the other endpoints, make another oauth helper with exactly the same settings except use real storage.
|
// For all the other endpoints, make another oauth helper with exactly the same settings except use real storage.
|
||||||
oauthHelperWithKubeStorage := oidc.FositeOauth2Helper(oidc.NewKubeStorage(m.secretsClient), issuer, fositeHMACSecretForThisProvider, nil)
|
oauthHelperWithKubeStorage := oidc.FositeOauth2Helper(oidc.NewKubeStorage(m.secretsClient), issuer, fositeHMACSecretForThisProvider, m.dynamicJWKSProvider)
|
||||||
|
|
||||||
// TODO use different codecs for the state and the cookie, because:
|
// TODO use different codecs for the state and the cookie, because:
|
||||||
// 1. we would like to state to have an embedded expiration date while the cookie does not need that
|
// 1. we would like to state to have an embedded expiration date while the cookie does not need that
|
||||||
|
@ -159,7 +159,7 @@ func TestManager(t *testing.T) {
|
|||||||
return actualLocationQueryParams.Get("code")
|
return actualLocationQueryParams.Get("code")
|
||||||
}
|
}
|
||||||
|
|
||||||
requireTokenRequestToBeHandled := func(requestIssuer, authCode string, jwks *jose.JSONWebKeySet) {
|
requireTokenRequestToBeHandled := func(requestIssuer, authCode string, jwks *jose.JSONWebKeySet, jwkIssuer string) {
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
numberOfKubeActionsBeforeThisRequest := len(kubeClient.Actions())
|
numberOfKubeActionsBeforeThisRequest := len(kubeClient.Actions())
|
||||||
@ -194,7 +194,7 @@ func TestManager(t *testing.T) {
|
|||||||
|
|
||||||
keySet := newStaticKeySet(privateKey.Public())
|
keySet := newStaticKeySet(privateKey.Public())
|
||||||
verifyConfig := coreosoidc.Config{ClientID: downstreamClientID, SupportedSigningAlgs: []string{coreosoidc.ES256}}
|
verifyConfig := coreosoidc.Config{ClientID: downstreamClientID, SupportedSigningAlgs: []string{coreosoidc.ES256}}
|
||||||
verifier := coreosoidc.NewVerifier(requestIssuer, keySet, &verifyConfig)
|
verifier := coreosoidc.NewVerifier(jwkIssuer, keySet, &verifyConfig)
|
||||||
_, err := verifier.Verify(context.Background(), idToken)
|
_, err := verifier.Verify(context.Background(), idToken)
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
|
|
||||||
@ -326,16 +326,16 @@ func TestManager(t *testing.T) {
|
|||||||
downstreamAuthCode1 := requireCallbackRequestToBeHandled(issuer1, callbackRequestParams, csrfCookieValue)
|
downstreamAuthCode1 := requireCallbackRequestToBeHandled(issuer1, callbackRequestParams, csrfCookieValue)
|
||||||
downstreamAuthCode2 := requireCallbackRequestToBeHandled(issuer2, callbackRequestParams, csrfCookieValue)
|
downstreamAuthCode2 := requireCallbackRequestToBeHandled(issuer2, callbackRequestParams, csrfCookieValue)
|
||||||
|
|
||||||
// // Hostnames are case-insensitive, so test that we can handle that.
|
// Hostnames are case-insensitive, so test that we can handle that.
|
||||||
downstreamAuthCode3 := requireCallbackRequestToBeHandled(issuer1DifferentCaseHostname, callbackRequestParams, csrfCookieValue)
|
downstreamAuthCode3 := requireCallbackRequestToBeHandled(issuer1DifferentCaseHostname, callbackRequestParams, csrfCookieValue)
|
||||||
downstreamAuthCode4 := requireCallbackRequestToBeHandled(issuer2DifferentCaseHostname, callbackRequestParams, csrfCookieValue)
|
downstreamAuthCode4 := requireCallbackRequestToBeHandled(issuer2DifferentCaseHostname, callbackRequestParams, csrfCookieValue)
|
||||||
|
|
||||||
requireTokenRequestToBeHandled(issuer1, downstreamAuthCode1, issuer1JWKS)
|
requireTokenRequestToBeHandled(issuer1, downstreamAuthCode1, issuer1JWKS, issuer1)
|
||||||
requireTokenRequestToBeHandled(issuer2, downstreamAuthCode2, issuer2JWKS)
|
requireTokenRequestToBeHandled(issuer2, downstreamAuthCode2, issuer2JWKS, issuer2)
|
||||||
|
|
||||||
// Hostnames are case-insensitive, so test that we can handle that.
|
// Hostnames are case-insensitive, so test that we can handle that.
|
||||||
requireTokenRequestToBeHandled(issuer1DifferentCaseHostname, downstreamAuthCode3, issuer1JWKS)
|
requireTokenRequestToBeHandled(issuer1DifferentCaseHostname, downstreamAuthCode3, issuer1JWKS, issuer1)
|
||||||
requireTokenRequestToBeHandled(issuer2DifferentCaseHostname, downstreamAuthCode4, issuer2JWKS)
|
requireTokenRequestToBeHandled(issuer2DifferentCaseHostname, downstreamAuthCode4, issuer2JWKS, issuer2)
|
||||||
}
|
}
|
||||||
|
|
||||||
when("given some valid providers via SetProviders()", func() {
|
when("given some valid providers via SetProviders()", func() {
|
||||||
|
Loading…
Reference in New Issue
Block a user