2022-01-07 23:04:58 +00:00
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
2020-11-19 04:30:05 +00:00
// SPDX-License-Identifier: Apache-2.0
package authorizationcode
import (
"context"
"crypto/ed25519"
"crypto/x509"
"encoding/json"
2020-12-02 22:10:41 +00:00
"fmt"
2020-11-19 04:30:05 +00:00
"math/rand"
"net/url"
"strings"
"testing"
"time"
fuzz "github.com/google/gofuzz"
"github.com/ory/fosite"
2020-12-10 22:47:58 +00:00
"github.com/ory/fosite/handler/oauth2"
2021-10-22 21:32:26 +00:00
"github.com/ory/fosite/handler/openid"
2020-12-02 01:18:32 +00:00
"github.com/pkg/errors"
2020-11-19 04:30:05 +00:00
"github.com/stretchr/testify/require"
"gopkg.in/square/go-jose.v2"
corev1 "k8s.io/api/core/v1"
2020-12-10 22:47:58 +00:00
apierrors "k8s.io/apimachinery/pkg/api/errors"
2020-11-19 04:30:05 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2020-12-10 22:47:58 +00:00
"k8s.io/apimachinery/pkg/runtime"
2020-11-19 04:30:05 +00:00
"k8s.io/apimachinery/pkg/runtime/schema"
2021-10-08 22:48:21 +00:00
"k8s.io/apimachinery/pkg/types"
2020-11-19 04:30:05 +00:00
"k8s.io/client-go/kubernetes/fake"
2020-12-10 22:47:58 +00:00
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
2020-12-02 22:10:41 +00:00
kubetesting "k8s.io/client-go/testing"
2021-12-10 22:22:36 +00:00
clocktesting "k8s.io/utils/clock/testing"
2020-12-01 22:53:22 +00:00
"go.pinniped.dev/internal/fositestorage"
2021-06-15 16:27:30 +00:00
"go.pinniped.dev/internal/oidc/clientregistry"
2021-10-06 22:28:13 +00:00
"go.pinniped.dev/internal/psession"
"go.pinniped.dev/internal/testutil"
2020-11-19 04:30:05 +00:00
)
2020-12-02 01:18:32 +00:00
const namespace = "test-ns"
2020-12-10 20:15:40 +00:00
var fakeNow = time . Date ( 2030 , time . January , 1 , 0 , 0 , 0 , 0 , time . UTC )
2020-12-10 22:47:58 +00:00
var lifetime = time . Minute * 10
var fakeNowPlusLifetimeAsString = metav1 . Time { Time : fakeNow . Add ( lifetime ) } . Format ( time . RFC3339 )
2020-12-10 20:15:40 +00:00
2020-12-02 01:18:32 +00:00
func TestAuthorizationCodeStorage ( t * testing . T ) {
2020-11-19 04:30:05 +00:00
secretsGVR := schema . GroupVersionResource {
Group : "" ,
Version : "v1" ,
Resource : "secrets" ,
}
2020-12-02 22:10:41 +00:00
wantActions := [ ] kubetesting . Action {
kubetesting . NewCreateAction ( secretsGVR , namespace , & corev1 . Secret {
2020-12-02 01:18:32 +00:00
ObjectMeta : metav1 . ObjectMeta {
Name : "pinniped-storage-authcode-pwu5zs7lekbhnln2w4" ,
ResourceVersion : "" ,
Labels : map [ string ] string {
2020-12-04 22:39:11 +00:00
"storage.pinniped.dev/type" : "authcode" ,
2020-12-02 01:18:32 +00:00
} ,
2020-12-10 20:15:40 +00:00
Annotations : map [ string ] string {
2020-12-10 22:47:58 +00:00
"storage.pinniped.dev/garbage-collect-after" : fakeNowPlusLifetimeAsString ,
2020-12-10 20:15:40 +00:00
} ,
2020-11-19 04:30:05 +00:00
} ,
2020-12-02 01:18:32 +00:00
Data : map [ string ] [ ] byte {
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
"pinniped-storage-data" : [ ] byte ( ` { "active":true,"request": { "id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client": { "id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form": { "key":["val"]},"session": { "fosite": { "Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom": { "username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc": { "upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"3"} ` ) ,
2020-12-02 01:18:32 +00:00
"pinniped-storage-version" : [ ] byte ( "1" ) ,
2020-11-19 04:30:05 +00:00
} ,
2020-12-02 01:18:32 +00:00
Type : "storage.pinniped.dev/authcode" ,
} ) ,
2020-12-02 22:10:41 +00:00
kubetesting . NewGetAction ( secretsGVR , namespace , "pinniped-storage-authcode-pwu5zs7lekbhnln2w4" ) ,
kubetesting . NewGetAction ( secretsGVR , namespace , "pinniped-storage-authcode-pwu5zs7lekbhnln2w4" ) ,
kubetesting . NewUpdateAction ( secretsGVR , namespace , & corev1 . Secret {
2020-12-02 01:18:32 +00:00
ObjectMeta : metav1 . ObjectMeta {
Name : "pinniped-storage-authcode-pwu5zs7lekbhnln2w4" ,
ResourceVersion : "" ,
Labels : map [ string ] string {
2020-12-04 22:39:11 +00:00
"storage.pinniped.dev/type" : "authcode" ,
2020-11-19 04:30:05 +00:00
} ,
2020-12-10 20:15:40 +00:00
Annotations : map [ string ] string {
2020-12-10 22:47:58 +00:00
"storage.pinniped.dev/garbage-collect-after" : fakeNowPlusLifetimeAsString ,
2020-12-10 20:15:40 +00:00
} ,
2020-11-19 04:30:05 +00:00
} ,
2020-12-02 01:18:32 +00:00
Data : map [ string ] [ ] byte {
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
"pinniped-storage-data" : [ ] byte ( ` { "active":false,"request": { "id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client": { "id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form": { "key":["val"]},"session": { "fosite": { "Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom": { "username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc": { "upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"3"} ` ) ,
2020-12-02 01:18:32 +00:00
"pinniped-storage-version" : [ ] byte ( "1" ) ,
} ,
Type : "storage.pinniped.dev/authcode" ,
} ) ,
}
2020-12-10 22:47:58 +00:00
ctx , client , _ , storage := makeTestSubject ( )
2020-12-02 01:18:32 +00:00
request := & fosite . Request {
ID : "abcd-1" ,
RequestedAt : time . Time { } ,
2021-06-15 16:27:30 +00:00
Client : & clientregistry . Client {
DefaultOpenIDConnectClient : fosite . DefaultOpenIDConnectClient {
DefaultClient : & fosite . DefaultClient {
ID : "pinny" ,
Secret : nil ,
RedirectURIs : nil ,
GrantTypes : nil ,
ResponseTypes : nil ,
Scopes : nil ,
Audience : nil ,
Public : true ,
} ,
JSONWebKeysURI : "where" ,
JSONWebKeys : nil ,
TokenEndpointAuthMethod : "something" ,
RequestURIs : nil ,
RequestObjectSigningAlgorithm : "" ,
TokenEndpointAuthSigningAlgorithm : "" ,
2020-12-02 01:18:32 +00:00
} ,
2020-11-19 04:30:05 +00:00
} ,
2021-10-06 22:28:13 +00:00
RequestedScope : nil ,
GrantedScope : nil ,
Form : url . Values { "key" : [ ] string { "val" } } ,
Session : testutil . NewFakePinnipedSession ( ) ,
2020-12-02 01:18:32 +00:00
RequestedAudience : nil ,
GrantedAudience : nil ,
2020-11-19 04:30:05 +00:00
}
2020-12-02 01:18:32 +00:00
err := storage . CreateAuthorizeCodeSession ( ctx , "fancy-signature" , request )
require . NoError ( t , err )
newRequest , err := storage . GetAuthorizeCodeSession ( ctx , "fancy-signature" , nil )
require . NoError ( t , err )
require . Equal ( t , request , newRequest )
err = storage . InvalidateAuthorizeCodeSession ( ctx , "fancy-signature" )
require . NoError ( t , err )
2021-10-06 22:28:13 +00:00
testutil . LogActualJSONFromCreateAction ( t , client , 0 ) // makes it easier to update expected values when needed
testutil . LogActualJSONFromUpdateAction ( t , client , 3 ) // makes it easier to update expected values when needed
2020-12-02 01:18:32 +00:00
require . Equal ( t , wantActions , client . Actions ( ) )
2020-12-02 22:10:41 +00:00
// Doing a Get on an invalidated session should still return the session, but also return an error.
invalidatedRequest , err := storage . GetAuthorizeCodeSession ( ctx , "fancy-signature" , nil )
require . EqualError ( t , err , "authorization code session for fancy-signature has already been used: Authorization code has ben invalidated" )
require . Equal ( t , "abcd-1" , invalidatedRequest . GetID ( ) )
2020-12-02 01:18:32 +00:00
}
func TestGetNotFound ( t * testing . T ) {
2020-12-10 22:47:58 +00:00
ctx , _ , _ , storage := makeTestSubject ( )
2020-12-02 01:18:32 +00:00
_ , notFoundErr := storage . GetAuthorizeCodeSession ( ctx , "non-existent-signature" , nil )
require . EqualError ( t , notFoundErr , "not_found" )
require . True ( t , errors . Is ( notFoundErr , fosite . ErrNotFound ) )
}
2020-12-02 22:10:41 +00:00
func TestInvalidateWhenNotFound ( t * testing . T ) {
2020-12-10 22:47:58 +00:00
ctx , _ , _ , storage := makeTestSubject ( )
2020-12-02 22:10:41 +00:00
notFoundErr := storage . InvalidateAuthorizeCodeSession ( ctx , "non-existent-signature" )
require . EqualError ( t , notFoundErr , "not_found" )
require . True ( t , errors . Is ( notFoundErr , fosite . ErrNotFound ) )
}
func TestInvalidateWhenConflictOnUpdateHappens ( t * testing . T ) {
2020-12-10 22:47:58 +00:00
ctx , client , _ , storage := makeTestSubject ( )
2020-12-02 22:10:41 +00:00
client . PrependReactor ( "update" , "secrets" , func ( _ kubetesting . Action ) ( bool , runtime . Object , error ) {
return true , nil , apierrors . NewConflict ( schema . GroupResource {
Group : "" ,
Resource : "secrets" ,
} , "some-secret-name" , fmt . Errorf ( "there was a conflict" ) )
} )
request := & fosite . Request {
ID : "some-request-id" ,
2021-06-15 16:27:30 +00:00
Client : & clientregistry . Client { } ,
2021-10-06 22:28:13 +00:00
Session : testutil . NewFakePinnipedSession ( ) ,
2020-12-02 22:10:41 +00:00
}
err := storage . CreateAuthorizeCodeSession ( ctx , "fancy-signature" , request )
require . NoError ( t , err )
err = storage . InvalidateAuthorizeCodeSession ( ctx , "fancy-signature" )
require . EqualError ( t , err , ` The request could not be completed due to concurrent access: failed to update authcode for signature fancy-signature at resource version : Operation cannot be fulfilled on secrets "some-secret-name": there was a conflict ` )
}
2020-12-02 01:18:32 +00:00
func TestWrongVersion ( t * testing . T ) {
2020-12-10 22:47:58 +00:00
ctx , _ , secrets , storage := makeTestSubject ( )
2020-12-02 01:18:32 +00:00
secret := & corev1 . Secret {
ObjectMeta : metav1 . ObjectMeta {
Name : "pinniped-storage-authcode-pwu5zs7lekbhnln2w4" ,
ResourceVersion : "" ,
Labels : map [ string ] string {
2020-12-04 22:39:11 +00:00
"storage.pinniped.dev/type" : "authcode" ,
2020-12-02 01:18:32 +00:00
} ,
} ,
Data : map [ string ] [ ] byte {
2021-10-06 22:28:13 +00:00
"pinniped-storage-data" : [ ] byte ( ` { "request": { "id":"abcd-1"},"version":"not-the-right-version","active": true} ` ) ,
2020-12-02 01:18:32 +00:00
"pinniped-storage-version" : [ ] byte ( "1" ) ,
} ,
Type : "storage.pinniped.dev/authcode" ,
}
_ , err := secrets . Create ( ctx , secret , metav1 . CreateOptions { } )
require . NoError ( t , err )
_ , err = storage . GetAuthorizeCodeSession ( ctx , "fancy-signature" , nil )
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
require . EqualError ( t , err , "authorization request data has wrong version: authorization code session for fancy-signature has version not-the-right-version instead of 3" )
2020-12-02 01:18:32 +00:00
}
func TestNilSessionRequest ( t * testing . T ) {
2020-12-10 22:47:58 +00:00
ctx , _ , secrets , storage := makeTestSubject ( )
2020-12-02 01:18:32 +00:00
secret := & corev1 . Secret {
ObjectMeta : metav1 . ObjectMeta {
Name : "pinniped-storage-authcode-pwu5zs7lekbhnln2w4" ,
ResourceVersion : "" ,
Labels : map [ string ] string {
2020-12-04 22:39:11 +00:00
"storage.pinniped.dev/type" : "authcode" ,
2020-12-02 01:18:32 +00:00
} ,
} ,
Data : map [ string ] [ ] byte {
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
"pinniped-storage-data" : [ ] byte ( ` { "nonsense-key": "nonsense-value", "version":"3", "active": true} ` ) ,
2020-12-02 01:18:32 +00:00
"pinniped-storage-version" : [ ] byte ( "1" ) ,
} ,
Type : "storage.pinniped.dev/authcode" ,
2020-11-19 04:30:05 +00:00
}
2020-12-02 01:18:32 +00:00
_ , err := secrets . Create ( ctx , secret , metav1 . CreateOptions { } )
require . NoError ( t , err )
_ , err = storage . GetAuthorizeCodeSession ( ctx , "fancy-signature" , nil )
require . EqualError ( t , err , "malformed authorization code session for fancy-signature: authorization request data must be present" )
}
func TestCreateWithNilRequester ( t * testing . T ) {
2020-12-10 22:47:58 +00:00
ctx , _ , _ , storage := makeTestSubject ( )
2020-12-02 01:18:32 +00:00
err := storage . CreateAuthorizeCodeSession ( ctx , "signature-doesnt-matter" , nil )
require . EqualError ( t , err , "requester must be of type fosite.Request" )
2020-11-19 04:30:05 +00:00
}
2020-12-02 01:18:32 +00:00
func TestCreateWithWrongRequesterDataTypes ( t * testing . T ) {
2020-12-10 22:47:58 +00:00
ctx , _ , _ , storage := makeTestSubject ( )
2020-12-02 01:18:32 +00:00
request := & fosite . Request {
Session : nil ,
2021-06-15 16:27:30 +00:00
Client : & clientregistry . Client { } ,
2020-11-19 04:30:05 +00:00
}
2020-12-02 01:18:32 +00:00
err := storage . CreateAuthorizeCodeSession ( ctx , "signature-doesnt-matter" , request )
2021-10-06 22:28:13 +00:00
require . EqualError ( t , err , "requester's session must be of type PinnipedSession" )
2020-11-19 04:30:05 +00:00
2020-12-02 01:18:32 +00:00
request = & fosite . Request {
2021-10-06 22:28:13 +00:00
Session : & psession . PinnipedSession { } ,
2020-12-02 01:18:32 +00:00
Client : nil ,
}
err = storage . CreateAuthorizeCodeSession ( ctx , "signature-doesnt-matter" , request )
2021-06-15 16:27:30 +00:00
require . EqualError ( t , err , "requester's client must be of type clientregistry.Client" )
2020-11-19 04:30:05 +00:00
}
2020-12-10 22:47:58 +00:00
func makeTestSubject ( ) ( context . Context , * fake . Clientset , corev1client . SecretInterface , oauth2 . AuthorizeCodeStorage ) {
client := fake . NewSimpleClientset ( )
secrets := client . CoreV1 ( ) . Secrets ( namespace )
2021-12-10 22:22:36 +00:00
return context . Background ( ) , client , secrets , New ( secrets , clocktesting . NewFakeClock ( fakeNow ) . Now , lifetime )
2020-12-10 22:47:58 +00:00
}
2020-11-19 04:30:05 +00:00
// TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession asserts that we can correctly round trip our authorize code session.
// It will detect any changes to fosite.AuthorizeRequest and guarantees that all interface types have concrete implementations.
func TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession ( t * testing . T ) {
validSession := NewValidEmptyAuthorizeCodeSession ( )
// sanity check our valid session
2020-12-01 22:53:22 +00:00
extractedRequest , err := fositestorage . ValidateAndExtractAuthorizeRequest ( validSession . Request )
2020-11-19 04:30:05 +00:00
require . NoError ( t , err )
require . Equal ( t , validSession . Request , extractedRequest )
// checked above
2021-06-15 16:27:30 +00:00
defaultClient := validSession . Request . Client . ( * clientregistry . Client )
2021-10-06 22:28:13 +00:00
pinnipedSession := validSession . Request . Session . ( * psession . PinnipedSession )
2020-11-19 04:30:05 +00:00
// makes it easier to use a raw string
replacer := strings . NewReplacer ( "`" , "a" )
randString := func ( c fuzz . Continue ) string {
for {
s := c . RandString ( )
if len ( s ) == 0 {
continue // skip empty string
}
return replacer . Replace ( s )
}
}
2020-12-01 19:01:23 +00:00
// deterministic fuzzing of fosite.Request
2020-11-19 04:30:05 +00:00
f := fuzz . New ( ) . RandSource ( rand . NewSource ( 1 ) ) . NilChance ( 0 ) . NumElements ( 1 , 3 ) . Funcs (
// these functions guarantee that these are the only interface types we need to fill out
2020-12-01 19:01:23 +00:00
// if fosite.Request changes to add more, the fuzzer will panic
2020-11-19 04:30:05 +00:00
func ( fc * fosite . Client , c fuzz . Continue ) {
c . Fuzz ( defaultClient )
* fc = defaultClient
} ,
func ( fs * fosite . Session , c fuzz . Continue ) {
2021-10-06 22:28:13 +00:00
c . Fuzz ( pinnipedSession )
* fs = pinnipedSession
2020-11-19 04:30:05 +00:00
} ,
// these types contain an interface{} that we need to handle
2021-10-06 22:28:13 +00:00
// this is safe because we explicitly provide the PinnipedSession concrete type
2020-11-19 04:30:05 +00:00
func ( value * map [ string ] interface { } , c fuzz . Continue ) {
// cover all the JSON data types just in case
* value = map [ string ] interface { } {
randString ( c ) : float64 ( c . Intn ( 1 << 32 ) ) ,
randString ( c ) : map [ string ] interface { } {
randString ( c ) : [ ] interface { } { float64 ( c . Intn ( 1 << 32 ) ) } ,
randString ( c ) : map [ string ] interface { } {
randString ( c ) : nil ,
randString ( c ) : map [ string ] interface { } {
randString ( c ) : c . RandBool ( ) ,
} ,
} ,
} ,
}
} ,
// JWK contains an interface{} Key that we need to handle
// this is safe because JWK explicitly implements JSON marshalling and unmarshalling
func ( jwk * jose . JSONWebKey , c fuzz . Continue ) {
key , _ , err := ed25519 . GenerateKey ( c )
require . NoError ( t , err )
jwk . Key = key
// set these fields to make the .Equal comparison work
jwk . Certificates = [ ] * x509 . Certificate { }
jwk . CertificatesURL = & url . URL { }
jwk . CertificateThumbprintSHA1 = [ ] byte { }
jwk . CertificateThumbprintSHA256 = [ ] byte { }
} ,
// set this to make the .Equal comparison work
// this is safe because Time explicitly implements JSON marshalling and unmarshalling
func ( tp * time . Time , c fuzz . Continue ) {
2020-11-19 19:04:25 +00:00
* tp = time . Unix ( c . Int63n ( 1 << 32 ) , c . Int63n ( 1 << 32 ) ) . UTC ( )
2020-11-19 04:30:05 +00:00
} ,
// make random strings that do not contain any ` characters
func ( s * string , c fuzz . Continue ) {
* s = randString ( c )
} ,
// handle string type alias
func ( s * fosite . TokenType , c fuzz . Continue ) {
* s = fosite . TokenType ( randString ( c ) )
2021-10-08 22:48:21 +00:00
} ,
func ( s * types . UID , c fuzz . Continue ) {
* s = types . UID ( randString ( c ) )
2020-11-19 04:30:05 +00:00
} ,
// handle string type alias
func ( s * fosite . Arguments , c fuzz . Continue ) {
n := c . Intn ( 3 ) + 1 // 1 to 3 items
arguments := make ( fosite . Arguments , n )
for i := range arguments {
arguments [ i ] = randString ( c )
}
* s = arguments
} ,
)
f . Fuzz ( validSession )
const name = "fuzz" // value is irrelevant
ctx := context . Background ( )
secrets := fake . NewSimpleClientset ( ) . CoreV1 ( ) . Secrets ( name )
2020-12-10 22:47:58 +00:00
storage := New ( secrets , func ( ) time . Time { return fakeNow } , lifetime )
2020-11-19 04:30:05 +00:00
// issue a create using the fuzzed request to confirm that marshalling works
err = storage . CreateAuthorizeCodeSession ( ctx , name , validSession . Request )
require . NoError ( t , err )
// retrieve a copy of the fuzzed request from storage to confirm that unmarshalling works
newRequest , err := storage . GetAuthorizeCodeSession ( ctx , name , nil )
require . NoError ( t , err )
// the fuzzed request and the copy from storage should be exactly the same
require . Equal ( t , validSession . Request , newRequest )
secretList , err := secrets . List ( ctx , metav1 . ListOptions { } )
require . NoError ( t , err )
require . Len ( t , secretList . Items , 1 )
authorizeCodeSessionJSONFromStorage := string ( secretList . Items [ 0 ] . Data [ "pinniped-storage-data" ] )
// set these to match CreateAuthorizeCodeSession so that .JSONEq works
validSession . Active = true
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
validSession . Version = "3"
2020-11-19 04:30:05 +00:00
validSessionJSONBytes , err := json . MarshalIndent ( validSession , "" , "\t" )
require . NoError ( t , err )
authorizeCodeSessionJSONFromFuzzing := string ( validSessionJSONBytes )
// the fuzzed session and storage session should have identical JSON
require . JSONEq ( t , authorizeCodeSessionJSONFromFuzzing , authorizeCodeSessionJSONFromStorage )
2021-12-06 22:43:39 +00:00
// t.Log("actual value from fuzzing", authorizeCodeSessionJSONFromFuzzing) // can be useful when updating expected value
2020-11-19 04:30:05 +00:00
// while the fuzzer will panic if AuthorizeRequest changes in a way that cannot be fuzzed,
// if it adds a new field that can be fuzzed, this check will fail
// thus if AuthorizeRequest changes, we will detect it here (though we could possibly miss an omitempty field)
2021-12-10 22:22:36 +00:00
require . JSONEq ( t , ExpectedAuthorizeCodeSessionJSONFromFuzzing , authorizeCodeSessionJSONFromFuzzing , "actual:\n%s" , authorizeCodeSessionJSONFromFuzzing )
2020-11-19 04:30:05 +00:00
}
2021-10-22 21:32:26 +00:00
func TestReadFromSecret ( t * testing . T ) {
tests := [ ] struct {
name string
secret * corev1 . Secret
2021-11-10 23:34:19 +00:00
wantSession * Session
2021-10-22 21:32:26 +00:00
wantErr string
} {
{
name : "happy path" ,
secret : & corev1 . Secret {
ObjectMeta : metav1 . ObjectMeta {
Name : "pinniped-storage-authcode-pwu5zs7lekbhnln2w4" ,
ResourceVersion : "" ,
Labels : map [ string ] string {
"storage.pinniped.dev/type" : "authcode" ,
} ,
} ,
Data : map [ string ] [ ] byte {
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
"pinniped-storage-data" : [ ] byte ( ` { "request": { "id":"abcd-1","session": { "fosite": { "Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom": { "username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc": { "upstreamRefreshToken":"fake-upstream-refresh-token"}}}},"version":"3","active": true} ` ) ,
2021-10-22 21:32:26 +00:00
"pinniped-storage-version" : [ ] byte ( "1" ) ,
} ,
Type : "storage.pinniped.dev/authcode" ,
} ,
2021-11-10 23:34:19 +00:00
wantSession : & Session {
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
Version : "3" ,
2021-10-22 21:32:26 +00:00
Active : true ,
Request : & fosite . Request {
ID : "abcd-1" ,
Client : & clientregistry . Client { } ,
Session : & psession . PinnipedSession {
Fosite : & openid . DefaultSession {
Username : "snorlax" ,
Subject : "panda" ,
} ,
Custom : & psession . CustomSessionData {
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
Username : "fake-username" ,
2021-10-22 21:32:26 +00:00
ProviderUID : "fake-provider-uid" ,
ProviderName : "fake-provider-name" ,
ProviderType : "fake-provider-type" ,
OIDC : & psession . OIDCSessionData {
UpstreamRefreshToken : "fake-upstream-refresh-token" ,
} ,
} ,
} ,
} ,
} ,
} ,
{
name : "wrong secret type" ,
secret : & corev1 . Secret {
ObjectMeta : metav1 . ObjectMeta {
Name : "pinniped-storage-authcode-pwu5zs7lekbhnln2w4" ,
ResourceVersion : "" ,
Labels : map [ string ] string {
"storage.pinniped.dev/type" : "authcode" ,
} ,
} ,
Data : map [ string ] [ ] byte {
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
"pinniped-storage-data" : [ ] byte ( ` { "request": { "id":"abcd-1"},"version":"3","active": true} ` ) ,
2021-10-22 21:32:26 +00:00
"pinniped-storage-version" : [ ] byte ( "1" ) ,
} ,
Type : "storage.pinniped.dev/not-authcode" ,
} ,
wantErr : "secret storage data has incorrect type: storage.pinniped.dev/not-authcode must equal storage.pinniped.dev/authcode" ,
} ,
{
name : "wrong session version" ,
secret : & corev1 . Secret {
ObjectMeta : metav1 . ObjectMeta {
Name : "pinniped-storage-authcode-pwu5zs7lekbhnln2w4" ,
ResourceVersion : "" ,
Labels : map [ string ] string {
"storage.pinniped.dev/type" : "authcode" ,
} ,
} ,
Data : map [ string ] [ ] byte {
"pinniped-storage-data" : [ ] byte ( ` { "request": { "id":"abcd-1"},"version":"wrong-version-here","active": true} ` ) ,
"pinniped-storage-version" : [ ] byte ( "1" ) ,
} ,
Type : "storage.pinniped.dev/authcode" ,
} ,
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
wantErr : "authorization request data has wrong version: authorization code session has version wrong-version-here instead of 3" ,
2021-10-22 21:32:26 +00:00
} ,
{
name : "missing request" ,
secret : & corev1 . Secret {
ObjectMeta : metav1 . ObjectMeta {
Name : "pinniped-storage-authcode-pwu5zs7lekbhnln2w4" ,
ResourceVersion : "" ,
Labels : map [ string ] string {
"storage.pinniped.dev/type" : "authcode" ,
} ,
} ,
Data : map [ string ] [ ] byte {
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
"pinniped-storage-data" : [ ] byte ( ` { "version":"3","active": true} ` ) ,
2021-10-22 21:32:26 +00:00
"pinniped-storage-version" : [ ] byte ( "1" ) ,
} ,
Type : "storage.pinniped.dev/authcode" ,
} ,
wantErr : "malformed authorization code session: authorization request data must be present" ,
} ,
}
for _ , tt := range tests {
tt := tt
t . Run ( tt . name , func ( t * testing . T ) {
t . Parallel ( )
session , err := ReadFromSecret ( tt . secret )
if tt . wantErr == "" {
require . NoError ( t , err )
require . Equal ( t , tt . wantSession , session )
} else {
require . EqualError ( t , err , tt . wantErr )
require . Nil ( t , session )
}
} )
}
}