Add fosite kube storage for access and refresh tokens
Also switched the token_handler_test.go to use kube storage. Signed-off-by: Aram Price <pricear@vmware.com>
This commit is contained in:
parent
8d5f4a93ed
commit
ac83633888
@ -14,6 +14,7 @@ import (
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
|
||||
"go.pinniped.dev/internal/constable"
|
||||
@ -34,10 +35,11 @@ const (
|
||||
)
|
||||
|
||||
type Storage interface {
|
||||
Create(ctx context.Context, signature string, data JSON) (resourceVersion string, err error)
|
||||
Create(ctx context.Context, signature string, data JSON, additionalLabels map[string]string) (resourceVersion string, err error)
|
||||
Get(ctx context.Context, signature string, data JSON) (resourceVersion string, err error)
|
||||
Update(ctx context.Context, signature, resourceVersion string, data JSON) (newResourceVersion string, err error)
|
||||
Delete(ctx context.Context, signature string) error
|
||||
DeleteByLabel(ctx context.Context, labelName string, labelValue string) error
|
||||
}
|
||||
|
||||
type JSON interface{} // document that we need valid JSON types
|
||||
@ -58,8 +60,8 @@ type secretsStorage struct {
|
||||
secrets corev1client.SecretInterface
|
||||
}
|
||||
|
||||
func (s *secretsStorage) Create(ctx context.Context, signature string, data JSON) (string, error) {
|
||||
secret, err := s.toSecret(signature, "", data)
|
||||
func (s *secretsStorage) Create(ctx context.Context, signature string, data JSON, additionalLabels map[string]string) (string, error) {
|
||||
secret, err := s.toSecret(signature, "", data, additionalLabels)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -98,7 +100,7 @@ func (s *secretsStorage) validateSecret(secret *corev1.Secret) error {
|
||||
}
|
||||
|
||||
func (s *secretsStorage) Update(ctx context.Context, signature, resourceVersion string, data JSON) (string, error) {
|
||||
secret, err := s.toSecret(signature, resourceVersion, data)
|
||||
secret, err := s.toSecret(signature, resourceVersion, data, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -116,6 +118,28 @@ func (s *secretsStorage) Delete(ctx context.Context, signature string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *secretsStorage) DeleteByLabel(ctx context.Context, labelName string, labelValue string) error {
|
||||
list, err := s.secrets.List(ctx, metav1.ListOptions{
|
||||
LabelSelector: labels.Set{
|
||||
secretLabelKey: s.resource,
|
||||
labelName: labelValue,
|
||||
}.String(),
|
||||
})
|
||||
if err != nil {
|
||||
//nolint:err113 // there's nothing wrong with this error
|
||||
return fmt.Errorf(`failed to list secrets for resource "%s" matching label "%s=%s": %w`, s.resource, labelName, labelValue, err)
|
||||
}
|
||||
// TODO try to delete all of the items and consolidate all of the errors and return them all
|
||||
for _, secret := range list.Items {
|
||||
err = s.secrets.Delete(ctx, secret.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
//nolint:err113 // there's nothing wrong with this error
|
||||
return fmt.Errorf(`failed to delete secrets for resource "%s" matching label "%s=%s" with name %s: %w`, s.resource, labelName, labelValue, secret.Name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint: gochecknoglobals
|
||||
var b32 = base32.StdEncoding.WithPadding(base32.NoPadding)
|
||||
|
||||
@ -127,18 +151,24 @@ func (s *secretsStorage) getName(signature string) string {
|
||||
return fmt.Sprintf(secretNameFormat, s.resource, signatureAsValidName)
|
||||
}
|
||||
|
||||
func (s *secretsStorage) toSecret(signature, resourceVersion string, data JSON) (*corev1.Secret, error) {
|
||||
func (s *secretsStorage) toSecret(signature, resourceVersion string, data JSON, additionalLabels map[string]string) (*corev1.Secret, error) {
|
||||
buf, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode secret data for %s: %w", s.getName(signature), err)
|
||||
}
|
||||
|
||||
labels := map[string]string{
|
||||
secretLabelKey: s.resource, // make it easier to find this stuff via kubectl
|
||||
}
|
||||
for labelName, labelValue := range additionalLabels {
|
||||
labels[labelName] = labelValue
|
||||
}
|
||||
|
||||
return &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: s.getName(signature),
|
||||
ResourceVersion: resourceVersion,
|
||||
Labels: map[string]string{
|
||||
secretLabelKey: s.resource, // make it easier to find this stuff via kubectl
|
||||
},
|
||||
Labels: labels,
|
||||
OwnerReferences: nil,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
|
@ -6,6 +6,7 @@ package crud
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/fosite/compose"
|
||||
@ -87,6 +88,7 @@ func TestStorage(t *testing.T) {
|
||||
wantSecrets: nil,
|
||||
wantErr: `failed to delete tokens for signature not-a-token: secrets "pinniped-storage-tokens-t2fx427lnci6s" not found`,
|
||||
},
|
||||
// TODO make a delete non-existent test for DeleteByLabel
|
||||
{
|
||||
name: "create and get",
|
||||
resource: "access-tokens",
|
||||
@ -97,7 +99,7 @@ func TestStorage(t *testing.T) {
|
||||
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
||||
|
||||
data := &testJSON{Data: "create-and-get"}
|
||||
rv1, err := storage.Create(ctx, signature, data)
|
||||
rv1, err := storage.Create(ctx, signature, data, nil)
|
||||
require.Empty(t, rv1) // fake client does not set this
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -145,6 +147,68 @@ func TestStorage(t *testing.T) {
|
||||
},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "create and get with additional labels",
|
||||
resource: "access-tokens",
|
||||
mocks: nil,
|
||||
run: func(t *testing.T, storage Storage) error {
|
||||
signature := hmac.AuthorizeCodeSignature(authorizationCode1)
|
||||
require.NotEmpty(t, signature)
|
||||
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
||||
|
||||
data := &testJSON{Data: "create-and-get"}
|
||||
rv1, err := storage.Create(ctx, signature, data, map[string]string{"label1": "value1", "label2": "value2"})
|
||||
require.Empty(t, rv1) // fake client does not set this
|
||||
require.NoError(t, err)
|
||||
|
||||
out := &testJSON{}
|
||||
rv2, err := storage.Get(ctx, signature, out)
|
||||
require.Empty(t, rv2) // fake client does not set this
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, data, out)
|
||||
|
||||
return nil
|
||||
},
|
||||
wantActions: []coretesting.Action{
|
||||
coretesting.NewCreateAction(secretsGVR, namespace, &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pinniped-storage-access-tokens-i6mhp4azwdxshgsy3s2mvedxpxuh3nudh3ot3m4xamlugj4e6qoq",
|
||||
ResourceVersion: "",
|
||||
Labels: map[string]string{
|
||||
"storage.pinniped.dev": "access-tokens",
|
||||
"label1": "value1",
|
||||
"label2": "value2",
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"Data":"create-and-get"}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
Type: "storage.pinniped.dev/access-tokens",
|
||||
}),
|
||||
coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-access-tokens-i6mhp4azwdxshgsy3s2mvedxpxuh3nudh3ot3m4xamlugj4e6qoq"),
|
||||
},
|
||||
wantSecrets: []corev1.Secret{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pinniped-storage-access-tokens-i6mhp4azwdxshgsy3s2mvedxpxuh3nudh3ot3m4xamlugj4e6qoq",
|
||||
Namespace: namespace,
|
||||
ResourceVersion: "",
|
||||
Labels: map[string]string{
|
||||
"storage.pinniped.dev": "access-tokens",
|
||||
"label1": "value1",
|
||||
"label2": "value2",
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"Data":"create-and-get"}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
Type: "storage.pinniped.dev/access-tokens",
|
||||
},
|
||||
},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "get existing",
|
||||
resource: "pandas-are-best",
|
||||
@ -325,6 +389,207 @@ func TestStorage(t *testing.T) {
|
||||
wantSecrets: nil,
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "delete existing by label",
|
||||
resource: "seals",
|
||||
mocks: func(t *testing.T, mock mocker) {
|
||||
require.NoError(t, mock.Tracker().Add(&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pinniped-storage-seals-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq",
|
||||
Namespace: namespace,
|
||||
ResourceVersion: "",
|
||||
Labels: map[string]string{
|
||||
"storage.pinniped.dev": "seals",
|
||||
"additionalLabel": "matching-value",
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"Data":"sad-seal"}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
Type: "storage.pinniped.dev/seals",
|
||||
}))
|
||||
require.NoError(t, mock.Tracker().Add(&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pinniped-storage-seals-abcdywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq",
|
||||
Namespace: namespace,
|
||||
ResourceVersion: "",
|
||||
Labels: map[string]string{
|
||||
"storage.pinniped.dev": "seals",
|
||||
"additionalLabel": "matching-value",
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"Data":"happy-seal"}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
Type: "storage.pinniped.dev/seals",
|
||||
}))
|
||||
require.NoError(t, mock.Tracker().Add(&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pinniped-storage-seals-12345wdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq",
|
||||
Namespace: namespace,
|
||||
ResourceVersion: "",
|
||||
Labels: map[string]string{
|
||||
"storage.pinniped.dev": "seals", // same type as above
|
||||
"additionalLabel": "non-matching-value", // different value for the same label
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"Data":"sad-seal2"}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
Type: "storage.pinniped.dev/seals",
|
||||
}))
|
||||
require.NoError(t, mock.Tracker().Add(&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pinniped-storage-seals-54321wdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq",
|
||||
Namespace: namespace,
|
||||
ResourceVersion: "",
|
||||
Labels: map[string]string{
|
||||
"storage.pinniped.dev": "walruses", // different type from above
|
||||
"additionalLabel": "matching-value", // same value for the same label as above
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"Data":"sad-seal3"}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
Type: "storage.pinniped.dev/walruses",
|
||||
}))
|
||||
},
|
||||
run: func(t *testing.T, storage Storage) error {
|
||||
return storage.DeleteByLabel(ctx, "additionalLabel", "matching-value")
|
||||
},
|
||||
wantActions: []coretesting.Action{
|
||||
coretesting.NewListAction(secretsGVR, schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"}, namespace, metav1.ListOptions{
|
||||
LabelSelector: "storage.pinniped.dev=seals,additionalLabel=matching-value",
|
||||
}),
|
||||
coretesting.NewDeleteAction(secretsGVR, namespace, "pinniped-storage-seals-abcdywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq"),
|
||||
coretesting.NewDeleteAction(secretsGVR, namespace, "pinniped-storage-seals-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq"),
|
||||
},
|
||||
wantSecrets: []corev1.Secret{
|
||||
// the secret of the same type whose label did not match is not deleted
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pinniped-storage-seals-12345wdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq",
|
||||
Namespace: namespace,
|
||||
ResourceVersion: "",
|
||||
Labels: map[string]string{
|
||||
"storage.pinniped.dev": "seals", // same type as above
|
||||
"additionalLabel": "non-matching-value", // different value for the same label
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"Data":"sad-seal2"}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
Type: "storage.pinniped.dev/seals",
|
||||
},
|
||||
// the secrets of other types are not deleted, even if they have a matching label
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pinniped-storage-seals-54321wdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq",
|
||||
Namespace: namespace,
|
||||
ResourceVersion: "",
|
||||
Labels: map[string]string{
|
||||
"storage.pinniped.dev": "walruses", // different type from above
|
||||
"additionalLabel": "matching-value", // same value for the same label as above
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"Data":"sad-seal3"}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
Type: "storage.pinniped.dev/walruses",
|
||||
},
|
||||
},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "when there is an error performing the delete while deleting by label",
|
||||
resource: "seals",
|
||||
mocks: func(t *testing.T, mock mocker) {
|
||||
require.NoError(t, mock.Tracker().Add(&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pinniped-storage-seals-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq",
|
||||
Namespace: namespace,
|
||||
ResourceVersion: "",
|
||||
Labels: map[string]string{
|
||||
"storage.pinniped.dev": "seals",
|
||||
"additionalLabel": "matching-value",
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"Data":"sad-seal"}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
Type: "storage.pinniped.dev/seals",
|
||||
}))
|
||||
mock.PrependReactor("delete", "secrets", func(action coretesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return true, nil, fmt.Errorf("some delete error")
|
||||
})
|
||||
},
|
||||
run: func(t *testing.T, storage Storage) error {
|
||||
return storage.DeleteByLabel(ctx, "additionalLabel", "matching-value")
|
||||
},
|
||||
wantActions: []coretesting.Action{
|
||||
coretesting.NewListAction(secretsGVR, schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"}, namespace, metav1.ListOptions{
|
||||
LabelSelector: "storage.pinniped.dev=seals,additionalLabel=matching-value",
|
||||
}),
|
||||
coretesting.NewDeleteAction(secretsGVR, namespace, "pinniped-storage-seals-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq"),
|
||||
},
|
||||
wantSecrets: []corev1.Secret{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pinniped-storage-seals-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq",
|
||||
Namespace: namespace,
|
||||
ResourceVersion: "",
|
||||
Labels: map[string]string{
|
||||
"storage.pinniped.dev": "seals",
|
||||
"additionalLabel": "matching-value",
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"Data":"sad-seal"}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
Type: "storage.pinniped.dev/seals",
|
||||
},
|
||||
},
|
||||
wantErr: `failed to delete secrets for resource "seals" matching label "additionalLabel=matching-value" with name pinniped-storage-seals-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq: some delete error`,
|
||||
},
|
||||
{
|
||||
name: "when there is an error listing secrets during a delete by label operation",
|
||||
resource: "seals",
|
||||
mocks: func(t *testing.T, mock mocker) {
|
||||
mock.PrependReactor("list", "secrets", func(action coretesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
listAction := action.(coretesting.ListActionImpl)
|
||||
labelRestrictions := listAction.GetListRestrictions().Labels
|
||||
requiresExactMatch, found := labelRestrictions.RequiresExactMatch("additionalLabel")
|
||||
if !found || requiresExactMatch != "matching-value" {
|
||||
// this list action did not use label selector additionalLabel=matching-value, so allow it to proceed without intervention
|
||||
return false, nil, nil
|
||||
}
|
||||
requiresExactMatch, found = labelRestrictions.RequiresExactMatch("storage.pinniped.dev")
|
||||
if !found || requiresExactMatch != "seals" {
|
||||
// this list action did not use label selector storage.pinniped.dev=seals, so allow it to proceed without intervention
|
||||
return false, nil, nil
|
||||
}
|
||||
// this list action was the one that did use the expected label selectors so cause it to error
|
||||
return true, nil, fmt.Errorf("some listing error")
|
||||
})
|
||||
},
|
||||
run: func(t *testing.T, storage Storage) error {
|
||||
return storage.DeleteByLabel(ctx, "additionalLabel", "matching-value")
|
||||
},
|
||||
wantActions: []coretesting.Action{
|
||||
coretesting.NewListAction(secretsGVR, schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"}, namespace, metav1.ListOptions{
|
||||
LabelSelector: "storage.pinniped.dev=seals,additionalLabel=matching-value",
|
||||
}),
|
||||
},
|
||||
wantErr: `failed to list secrets for resource "seals" matching label "additionalLabel=matching-value": some listing error`,
|
||||
},
|
||||
{
|
||||
name: "invalid exiting secret type",
|
||||
resource: "candies",
|
||||
@ -582,8 +847,11 @@ func checkSecretActionNames(t *testing.T, actions []coretesting.Action) {
|
||||
t.Helper()
|
||||
|
||||
for _, action := range actions {
|
||||
name := getName(t, action)
|
||||
assertValidName(t, name)
|
||||
_, ok := action.(coretesting.ListActionImpl)
|
||||
if !ok { // list action don't have names, so skip these assertions for list actions
|
||||
name := getName(t, action)
|
||||
assertValidName(t, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
112
internal/fositestorage/accesstoken/accesstoken.go
Normal file
112
internal/fositestorage/accesstoken/accesstoken.go
Normal file
@ -0,0 +1,112 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package accesstoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
"github.com/ory/fosite/handler/oauth2"
|
||||
"github.com/ory/fosite/handler/openid"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/crud"
|
||||
"go.pinniped.dev/internal/fositestorage"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrInvalidAccessTokenRequestVersion = constable.Error("access token request data has wrong version")
|
||||
ErrInvalidAccessTokenRequestData = constable.Error("access token request data must be present")
|
||||
|
||||
accessTokenStorageVersion = "1"
|
||||
)
|
||||
|
||||
type RevocationStorage interface {
|
||||
oauth2.AccessTokenStorage
|
||||
RevokeAccessToken(ctx context.Context, requestID string) error
|
||||
}
|
||||
|
||||
var _ RevocationStorage = &accessTokenStorage{}
|
||||
|
||||
type accessTokenStorage struct {
|
||||
storage crud.Storage
|
||||
}
|
||||
|
||||
type session struct {
|
||||
Request *fosite.Request `json:"request"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func New(secrets corev1client.SecretInterface) RevocationStorage {
|
||||
return &accessTokenStorage{storage: crud.New("access-token", secrets)}
|
||||
}
|
||||
|
||||
func (a *accessTokenStorage) RevokeAccessToken(ctx context.Context, requestID string) error {
|
||||
return a.storage.DeleteByLabel(ctx, fositestorage.StorageRequestIDLabelName, requestID)
|
||||
}
|
||||
|
||||
func (a *accessTokenStorage) CreateAccessTokenSession(ctx context.Context, signature string, requester fosite.Requester) error {
|
||||
request, err := fositestorage.ValidateAndExtractAuthorizeRequest(requester)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = a.storage.Create(
|
||||
ctx,
|
||||
signature,
|
||||
&session{Request: request, Version: accessTokenStorageVersion},
|
||||
map[string]string{fositestorage.StorageRequestIDLabelName: requester.GetID()},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *accessTokenStorage) GetAccessTokenSession(ctx context.Context, signature string, _ fosite.Session) (fosite.Requester, error) {
|
||||
session, _, err := a.getSession(ctx, signature)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session.Request, err
|
||||
}
|
||||
|
||||
func (a *accessTokenStorage) DeleteAccessTokenSession(ctx context.Context, signature string) error {
|
||||
return a.storage.Delete(ctx, signature)
|
||||
}
|
||||
|
||||
func (a *accessTokenStorage) getSession(ctx context.Context, signature string) (*session, string, error) {
|
||||
session := newValidEmptyAccessTokenSession()
|
||||
rv, err := a.storage.Get(ctx, signature, session)
|
||||
|
||||
if errors.IsNotFound(err) {
|
||||
return nil, "", fosite.ErrNotFound.WithCause(err).WithDebug(err.Error())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to get access token session for %s: %w", signature, err)
|
||||
}
|
||||
|
||||
if version := session.Version; version != accessTokenStorageVersion {
|
||||
return nil, "", fmt.Errorf("%w: access token session for %s has version %s instead of %s",
|
||||
ErrInvalidAccessTokenRequestVersion, signature, version, accessTokenStorageVersion)
|
||||
}
|
||||
|
||||
if session.Request.ID == "" {
|
||||
return nil, "", fmt.Errorf("malformed access token session for %s: %w", signature, ErrInvalidAccessTokenRequestData)
|
||||
}
|
||||
|
||||
return session, rv, nil
|
||||
}
|
||||
|
||||
func newValidEmptyAccessTokenSession() *session {
|
||||
return &session{
|
||||
Request: &fosite.Request{
|
||||
Client: &fosite.DefaultOpenIDConnectClient{},
|
||||
Session: &openid.DefaultSession{},
|
||||
},
|
||||
}
|
||||
}
|
282
internal/fositestorage/accesstoken/accesstoken_test.go
Normal file
282
internal/fositestorage/accesstoken/accesstoken_test.go
Normal file
@ -0,0 +1,282 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package accesstoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
"github.com/ory/fosite/handler/openid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
coretesting "k8s.io/client-go/testing"
|
||||
)
|
||||
|
||||
const namespace = "test-ns"
|
||||
|
||||
var secretsGVR = schema.GroupVersionResource{
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Resource: "secrets",
|
||||
}
|
||||
|
||||
func TestAccessTokenStorage(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
wantActions := []coretesting.Action{
|
||||
coretesting.NewCreateAction(secretsGVR, namespace, &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pinniped-storage-access-token-pwu5zs7lekbhnln2w4",
|
||||
ResourceVersion: "",
|
||||
Labels: map[string]string{
|
||||
"storage.pinniped.dev": "access-token",
|
||||
"storage.pinniped.dev/request-id": "abcd-1",
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"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":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
Type: "storage.pinniped.dev/access-token",
|
||||
}),
|
||||
coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-access-token-pwu5zs7lekbhnln2w4"),
|
||||
coretesting.NewDeleteAction(secretsGVR, namespace, "pinniped-storage-access-token-pwu5zs7lekbhnln2w4"),
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset()
|
||||
secrets := client.CoreV1().Secrets(namespace)
|
||||
storage := New(secrets)
|
||||
|
||||
request := &fosite.Request{
|
||||
ID: "abcd-1",
|
||||
RequestedAt: time.Time{},
|
||||
Client: &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: "",
|
||||
},
|
||||
RequestedScope: nil,
|
||||
GrantedScope: nil,
|
||||
Form: url.Values{"key": []string{"val"}},
|
||||
Session: &openid.DefaultSession{
|
||||
Claims: nil,
|
||||
Headers: nil,
|
||||
ExpiresAt: nil,
|
||||
Username: "snorlax",
|
||||
Subject: "panda",
|
||||
},
|
||||
RequestedAudience: nil,
|
||||
GrantedAudience: nil,
|
||||
}
|
||||
err := storage.CreateAccessTokenSession(ctx, "fancy-signature", request)
|
||||
require.NoError(t, err)
|
||||
|
||||
newRequest, err := storage.GetAccessTokenSession(ctx, "fancy-signature", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, request, newRequest)
|
||||
|
||||
err = storage.DeleteAccessTokenSession(ctx, "fancy-signature")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, wantActions, client.Actions())
|
||||
}
|
||||
|
||||
func TestAccessTokenStorageRevocation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
wantActions := []coretesting.Action{
|
||||
coretesting.NewCreateAction(secretsGVR, namespace, &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pinniped-storage-access-token-pwu5zs7lekbhnln2w4",
|
||||
ResourceVersion: "",
|
||||
Labels: map[string]string{
|
||||
"storage.pinniped.dev": "access-token",
|
||||
"storage.pinniped.dev/request-id": "abcd-1",
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"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":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
Type: "storage.pinniped.dev/access-token",
|
||||
}),
|
||||
coretesting.NewListAction(secretsGVR, schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"}, namespace, metav1.ListOptions{
|
||||
LabelSelector: "storage.pinniped.dev=access-token,storage.pinniped.dev/request-id=abcd-1",
|
||||
}),
|
||||
coretesting.NewDeleteAction(secretsGVR, namespace, "pinniped-storage-access-token-pwu5zs7lekbhnln2w4"),
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset()
|
||||
secrets := client.CoreV1().Secrets(namespace)
|
||||
storage := New(secrets)
|
||||
|
||||
request := &fosite.Request{
|
||||
ID: "abcd-1",
|
||||
RequestedAt: time.Time{},
|
||||
Client: &fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
ID: "pinny",
|
||||
Public: true,
|
||||
},
|
||||
JSONWebKeysURI: "where",
|
||||
TokenEndpointAuthMethod: "something",
|
||||
},
|
||||
Form: url.Values{"key": []string{"val"}},
|
||||
Session: &openid.DefaultSession{
|
||||
Username: "snorlax",
|
||||
Subject: "panda",
|
||||
},
|
||||
}
|
||||
err := storage.CreateAccessTokenSession(ctx, "fancy-signature", request)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Revoke the request ID of the session that we just created
|
||||
err = storage.RevokeAccessToken(ctx, "abcd-1")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, wantActions, client.Actions())
|
||||
}
|
||||
|
||||
func TestGetNotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := fake.NewSimpleClientset()
|
||||
secrets := client.CoreV1().Secrets(namespace)
|
||||
storage := New(secrets)
|
||||
|
||||
_, notFoundErr := storage.GetAccessTokenSession(ctx, "non-existent-signature", nil)
|
||||
require.EqualError(t, notFoundErr, "not_found")
|
||||
require.True(t, errors.Is(notFoundErr, fosite.ErrNotFound))
|
||||
}
|
||||
|
||||
func TestWrongVersion(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := fake.NewSimpleClientset()
|
||||
secrets := client.CoreV1().Secrets(namespace)
|
||||
storage := New(secrets)
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pinniped-storage-access-token-pwu5zs7lekbhnln2w4",
|
||||
ResourceVersion: "",
|
||||
Labels: map[string]string{
|
||||
"storage.pinniped.dev": "access-token",
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"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":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"not-the-right-version"}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
Type: "storage.pinniped.dev/access-token",
|
||||
}
|
||||
_, err := secrets.Create(ctx, secret, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = storage.GetAccessTokenSession(ctx, "fancy-signature", nil)
|
||||
|
||||
require.EqualError(t, err, "access token request data has wrong version: access token session for fancy-signature has version not-the-right-version instead of 1")
|
||||
}
|
||||
|
||||
func TestNilSessionRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := fake.NewSimpleClientset()
|
||||
secrets := client.CoreV1().Secrets(namespace)
|
||||
storage := New(secrets)
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pinniped-storage-access-token-pwu5zs7lekbhnln2w4",
|
||||
ResourceVersion: "",
|
||||
Labels: map[string]string{
|
||||
"storage.pinniped.dev": "access-token",
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"1"}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
Type: "storage.pinniped.dev/access-token",
|
||||
}
|
||||
|
||||
_, err := secrets.Create(ctx, secret, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = storage.GetAccessTokenSession(ctx, "fancy-signature", nil)
|
||||
require.EqualError(t, err, "malformed access token session for fancy-signature: access token request data must be present")
|
||||
}
|
||||
|
||||
func TestCreateWithNilRequester(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := fake.NewSimpleClientset()
|
||||
secrets := client.CoreV1().Secrets(namespace)
|
||||
storage := New(secrets)
|
||||
|
||||
err := storage.CreateAccessTokenSession(ctx, "signature-doesnt-matter", nil)
|
||||
require.EqualError(t, err, "requester must be of type fosite.Request")
|
||||
}
|
||||
|
||||
func TestCreateWithWrongRequesterDataTypes(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := fake.NewSimpleClientset()
|
||||
secrets := client.CoreV1().Secrets(namespace)
|
||||
storage := New(secrets)
|
||||
|
||||
request := &fosite.Request{
|
||||
Session: nil,
|
||||
Client: &fosite.DefaultOpenIDConnectClient{},
|
||||
}
|
||||
err := storage.CreateAccessTokenSession(ctx, "signature-doesnt-matter", request)
|
||||
require.EqualError(t, err, "requester's session must be of type openid.DefaultSession")
|
||||
|
||||
request = &fosite.Request{
|
||||
Session: &openid.DefaultSession{},
|
||||
Client: nil,
|
||||
}
|
||||
err = storage.CreateAccessTokenSession(ctx, "signature-doesnt-matter", request)
|
||||
require.EqualError(t, err, "requester's client must be of type fosite.DefaultOpenIDConnectClient")
|
||||
}
|
||||
|
||||
func TestCreateWithoutRequesterID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := fake.NewSimpleClientset()
|
||||
secrets := client.CoreV1().Secrets(namespace)
|
||||
storage := New(secrets)
|
||||
|
||||
request := &fosite.Request{
|
||||
ID: "", // empty ID
|
||||
Session: &openid.DefaultSession{},
|
||||
Client: &fosite.DefaultOpenIDConnectClient{},
|
||||
}
|
||||
err := storage.CreateAccessTokenSession(ctx, "signature-doesnt-matter", request)
|
||||
require.NoError(t, err)
|
||||
|
||||
// the blank ID was filled in with an auto-generated ID
|
||||
require.NotEmpty(t, request.ID)
|
||||
|
||||
require.Len(t, client.Actions(), 1)
|
||||
actualAction := client.Actions()[0].(coretesting.CreateActionImpl)
|
||||
actualSecret := actualAction.GetObject().(*corev1.Secret)
|
||||
|
||||
// The generated secret was labeled with that auto-generated request ID
|
||||
require.Equal(t, request.ID, actualSecret.Labels["storage.pinniped.dev/request-id"])
|
||||
}
|
@ -64,7 +64,7 @@ func (a *authorizeCodeStorage) CreateAuthorizeCodeSession(ctx context.Context, s
|
||||
// of the consent authorization request. It is used to identify the session.
|
||||
// signature for lookup in the DB
|
||||
|
||||
_, err = a.storage.Create(ctx, signature, &AuthorizeCodeSession{Active: true, Request: request, Version: authorizeCodeStorageVersion})
|
||||
_, err = a.storage.Create(ctx, signature, &AuthorizeCodeSession{Active: true, Request: request, Version: authorizeCodeStorageVersion}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -11,9 +11,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ErrInvalidRequestType = constable.Error("requester must be of type fosite.Request")
|
||||
ErrInvalidClientType = constable.Error("requester's client must be of type fosite.DefaultOpenIDConnectClient")
|
||||
ErrInvalidSessionType = constable.Error("requester's session must be of type openid.DefaultSession")
|
||||
ErrInvalidRequestType = constable.Error("requester must be of type fosite.Request")
|
||||
ErrInvalidClientType = constable.Error("requester's client must be of type fosite.DefaultOpenIDConnectClient")
|
||||
ErrInvalidSessionType = constable.Error("requester's session must be of type openid.DefaultSession")
|
||||
StorageRequestIDLabelName = "storage.pinniped.dev/request-id" //nolint:gosec // this is not a credential
|
||||
)
|
||||
|
||||
func ValidateAndExtractAuthorizeRequest(requester fosite.Requester) (*fosite.Request, error) {
|
||||
|
@ -52,7 +52,7 @@ func (a *openIDConnectRequestStorage) CreateOpenIDConnectSession(ctx context.Con
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = a.storage.Create(ctx, signature, &session{Request: request, Version: oidcStorageVersion})
|
||||
_, err = a.storage.Create(ctx, signature, &session{Request: request, Version: oidcStorageVersion}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -46,7 +46,7 @@ func (a *pkceStorage) CreatePKCERequestSession(ctx context.Context, signature st
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = a.storage.Create(ctx, signature, &session{Request: request, Version: pkceStorageVersion})
|
||||
_, err = a.storage.Create(ctx, signature, &session{Request: request, Version: pkceStorageVersion}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
|
112
internal/fositestorage/refreshtoken/refreshtoken.go
Normal file
112
internal/fositestorage/refreshtoken/refreshtoken.go
Normal file
@ -0,0 +1,112 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package refreshtoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
"github.com/ory/fosite/handler/oauth2"
|
||||
"github.com/ory/fosite/handler/openid"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/crud"
|
||||
"go.pinniped.dev/internal/fositestorage"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrInvalidRefreshTokenRequestVersion = constable.Error("refresh token request data has wrong version")
|
||||
ErrInvalidRefreshTokenRequestData = constable.Error("refresh token request data must be present")
|
||||
|
||||
refreshTokenStorageVersion = "1"
|
||||
)
|
||||
|
||||
type RevocationStorage interface {
|
||||
oauth2.RefreshTokenStorage
|
||||
RevokeRefreshToken(ctx context.Context, requestID string) error
|
||||
}
|
||||
|
||||
var _ RevocationStorage = &refreshTokenStorage{}
|
||||
|
||||
type refreshTokenStorage struct {
|
||||
storage crud.Storage
|
||||
}
|
||||
|
||||
type session struct {
|
||||
Request *fosite.Request `json:"request"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func New(secrets corev1client.SecretInterface) RevocationStorage {
|
||||
return &refreshTokenStorage{storage: crud.New("refresh-token", secrets)}
|
||||
}
|
||||
|
||||
func (a *refreshTokenStorage) RevokeRefreshToken(ctx context.Context, requestID string) error {
|
||||
return a.storage.DeleteByLabel(ctx, fositestorage.StorageRequestIDLabelName, requestID)
|
||||
}
|
||||
|
||||
func (a *refreshTokenStorage) CreateRefreshTokenSession(ctx context.Context, signature string, requester fosite.Requester) error {
|
||||
request, err := fositestorage.ValidateAndExtractAuthorizeRequest(requester)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = a.storage.Create(
|
||||
ctx,
|
||||
signature,
|
||||
&session{Request: request, Version: refreshTokenStorageVersion},
|
||||
map[string]string{fositestorage.StorageRequestIDLabelName: requester.GetID()},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *refreshTokenStorage) GetRefreshTokenSession(ctx context.Context, signature string, _ fosite.Session) (fosite.Requester, error) {
|
||||
session, _, err := a.getSession(ctx, signature)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session.Request, err
|
||||
}
|
||||
|
||||
func (a *refreshTokenStorage) DeleteRefreshTokenSession(ctx context.Context, signature string) error {
|
||||
return a.storage.Delete(ctx, signature)
|
||||
}
|
||||
|
||||
func (a *refreshTokenStorage) getSession(ctx context.Context, signature string) (*session, string, error) {
|
||||
session := newValidEmptyRefreshTokenSession()
|
||||
rv, err := a.storage.Get(ctx, signature, session)
|
||||
|
||||
if errors.IsNotFound(err) {
|
||||
return nil, "", fosite.ErrNotFound.WithCause(err).WithDebug(err.Error())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to get refresh token session for %s: %w", signature, err)
|
||||
}
|
||||
|
||||
if version := session.Version; version != refreshTokenStorageVersion {
|
||||
return nil, "", fmt.Errorf("%w: refresh token session for %s has version %s instead of %s",
|
||||
ErrInvalidRefreshTokenRequestVersion, signature, version, refreshTokenStorageVersion)
|
||||
}
|
||||
|
||||
if session.Request.ID == "" {
|
||||
return nil, "", fmt.Errorf("malformed refresh token session for %s: %w", signature, ErrInvalidRefreshTokenRequestData)
|
||||
}
|
||||
|
||||
return session, rv, nil
|
||||
}
|
||||
|
||||
func newValidEmptyRefreshTokenSession() *session {
|
||||
return &session{
|
||||
Request: &fosite.Request{
|
||||
Client: &fosite.DefaultOpenIDConnectClient{},
|
||||
Session: &openid.DefaultSession{},
|
||||
},
|
||||
}
|
||||
}
|
282
internal/fositestorage/refreshtoken/refreshtoken_test.go
Normal file
282
internal/fositestorage/refreshtoken/refreshtoken_test.go
Normal file
@ -0,0 +1,282 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package refreshtoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
"github.com/ory/fosite/handler/openid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
coretesting "k8s.io/client-go/testing"
|
||||
)
|
||||
|
||||
const namespace = "test-ns"
|
||||
|
||||
var secretsGVR = schema.GroupVersionResource{
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Resource: "secrets",
|
||||
}
|
||||
|
||||
func TestRefreshTokenStorage(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
wantActions := []coretesting.Action{
|
||||
coretesting.NewCreateAction(secretsGVR, namespace, &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pinniped-storage-refresh-token-pwu5zs7lekbhnln2w4",
|
||||
ResourceVersion: "",
|
||||
Labels: map[string]string{
|
||||
"storage.pinniped.dev": "refresh-token",
|
||||
"storage.pinniped.dev/request-id": "abcd-1",
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"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":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
Type: "storage.pinniped.dev/refresh-token",
|
||||
}),
|
||||
coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-refresh-token-pwu5zs7lekbhnln2w4"),
|
||||
coretesting.NewDeleteAction(secretsGVR, namespace, "pinniped-storage-refresh-token-pwu5zs7lekbhnln2w4"),
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset()
|
||||
secrets := client.CoreV1().Secrets(namespace)
|
||||
storage := New(secrets)
|
||||
|
||||
request := &fosite.Request{
|
||||
ID: "abcd-1",
|
||||
RequestedAt: time.Time{},
|
||||
Client: &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: "",
|
||||
},
|
||||
RequestedScope: nil,
|
||||
GrantedScope: nil,
|
||||
Form: url.Values{"key": []string{"val"}},
|
||||
Session: &openid.DefaultSession{
|
||||
Claims: nil,
|
||||
Headers: nil,
|
||||
ExpiresAt: nil,
|
||||
Username: "snorlax",
|
||||
Subject: "panda",
|
||||
},
|
||||
RequestedAudience: nil,
|
||||
GrantedAudience: nil,
|
||||
}
|
||||
err := storage.CreateRefreshTokenSession(ctx, "fancy-signature", request)
|
||||
require.NoError(t, err)
|
||||
|
||||
newRequest, err := storage.GetRefreshTokenSession(ctx, "fancy-signature", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, request, newRequest)
|
||||
|
||||
err = storage.DeleteRefreshTokenSession(ctx, "fancy-signature")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, wantActions, client.Actions())
|
||||
}
|
||||
|
||||
func TestRefreshTokenStorageRevocation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
wantActions := []coretesting.Action{
|
||||
coretesting.NewCreateAction(secretsGVR, namespace, &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pinniped-storage-refresh-token-pwu5zs7lekbhnln2w4",
|
||||
ResourceVersion: "",
|
||||
Labels: map[string]string{
|
||||
"storage.pinniped.dev": "refresh-token",
|
||||
"storage.pinniped.dev/request-id": "abcd-1",
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"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":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
Type: "storage.pinniped.dev/refresh-token",
|
||||
}),
|
||||
coretesting.NewListAction(secretsGVR, schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"}, namespace, metav1.ListOptions{
|
||||
LabelSelector: "storage.pinniped.dev=refresh-token,storage.pinniped.dev/request-id=abcd-1",
|
||||
}),
|
||||
coretesting.NewDeleteAction(secretsGVR, namespace, "pinniped-storage-refresh-token-pwu5zs7lekbhnln2w4"),
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset()
|
||||
secrets := client.CoreV1().Secrets(namespace)
|
||||
storage := New(secrets)
|
||||
|
||||
request := &fosite.Request{
|
||||
ID: "abcd-1",
|
||||
RequestedAt: time.Time{},
|
||||
Client: &fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
ID: "pinny",
|
||||
Public: true,
|
||||
},
|
||||
JSONWebKeysURI: "where",
|
||||
TokenEndpointAuthMethod: "something",
|
||||
},
|
||||
Form: url.Values{"key": []string{"val"}},
|
||||
Session: &openid.DefaultSession{
|
||||
Username: "snorlax",
|
||||
Subject: "panda",
|
||||
},
|
||||
}
|
||||
err := storage.CreateRefreshTokenSession(ctx, "fancy-signature", request)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Revoke the request ID of the session that we just created
|
||||
err = storage.RevokeRefreshToken(ctx, "abcd-1")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, wantActions, client.Actions())
|
||||
}
|
||||
|
||||
func TestGetNotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := fake.NewSimpleClientset()
|
||||
secrets := client.CoreV1().Secrets(namespace)
|
||||
storage := New(secrets)
|
||||
|
||||
_, notFoundErr := storage.GetRefreshTokenSession(ctx, "non-existent-signature", nil)
|
||||
require.EqualError(t, notFoundErr, "not_found")
|
||||
require.True(t, errors.Is(notFoundErr, fosite.ErrNotFound))
|
||||
}
|
||||
|
||||
func TestWrongVersion(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := fake.NewSimpleClientset()
|
||||
secrets := client.CoreV1().Secrets(namespace)
|
||||
storage := New(secrets)
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pinniped-storage-refresh-token-pwu5zs7lekbhnln2w4",
|
||||
ResourceVersion: "",
|
||||
Labels: map[string]string{
|
||||
"storage.pinniped.dev": "refresh-token",
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"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":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"not-the-right-version"}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
Type: "storage.pinniped.dev/refresh-token",
|
||||
}
|
||||
_, err := secrets.Create(ctx, secret, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = storage.GetRefreshTokenSession(ctx, "fancy-signature", nil)
|
||||
|
||||
require.EqualError(t, err, "refresh token request data has wrong version: refresh token session for fancy-signature has version not-the-right-version instead of 1")
|
||||
}
|
||||
|
||||
func TestNilSessionRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := fake.NewSimpleClientset()
|
||||
secrets := client.CoreV1().Secrets(namespace)
|
||||
storage := New(secrets)
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pinniped-storage-refresh-token-pwu5zs7lekbhnln2w4",
|
||||
ResourceVersion: "",
|
||||
Labels: map[string]string{
|
||||
"storage.pinniped.dev": "refresh-token",
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"1"}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
Type: "storage.pinniped.dev/refresh-token",
|
||||
}
|
||||
|
||||
_, err := secrets.Create(ctx, secret, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = storage.GetRefreshTokenSession(ctx, "fancy-signature", nil)
|
||||
require.EqualError(t, err, "malformed refresh token session for fancy-signature: refresh token request data must be present")
|
||||
}
|
||||
|
||||
func TestCreateWithNilRequester(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := fake.NewSimpleClientset()
|
||||
secrets := client.CoreV1().Secrets(namespace)
|
||||
storage := New(secrets)
|
||||
|
||||
err := storage.CreateRefreshTokenSession(ctx, "signature-doesnt-matter", nil)
|
||||
require.EqualError(t, err, "requester must be of type fosite.Request")
|
||||
}
|
||||
|
||||
func TestCreateWithWrongRequesterDataTypes(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := fake.NewSimpleClientset()
|
||||
secrets := client.CoreV1().Secrets(namespace)
|
||||
storage := New(secrets)
|
||||
|
||||
request := &fosite.Request{
|
||||
Session: nil,
|
||||
Client: &fosite.DefaultOpenIDConnectClient{},
|
||||
}
|
||||
err := storage.CreateRefreshTokenSession(ctx, "signature-doesnt-matter", request)
|
||||
require.EqualError(t, err, "requester's session must be of type openid.DefaultSession")
|
||||
|
||||
request = &fosite.Request{
|
||||
Session: &openid.DefaultSession{},
|
||||
Client: nil,
|
||||
}
|
||||
err = storage.CreateRefreshTokenSession(ctx, "signature-doesnt-matter", request)
|
||||
require.EqualError(t, err, "requester's client must be of type fosite.DefaultOpenIDConnectClient")
|
||||
}
|
||||
|
||||
func TestCreateWithoutRequesterID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := fake.NewSimpleClientset()
|
||||
secrets := client.CoreV1().Secrets(namespace)
|
||||
storage := New(secrets)
|
||||
|
||||
request := &fosite.Request{
|
||||
ID: "", // empty ID
|
||||
Session: &openid.DefaultSession{},
|
||||
Client: &fosite.DefaultOpenIDConnectClient{},
|
||||
}
|
||||
err := storage.CreateRefreshTokenSession(ctx, "signature-doesnt-matter", request)
|
||||
require.NoError(t, err)
|
||||
|
||||
// the blank ID was filled in with an auto-generated ID
|
||||
require.NotEmpty(t, request.ID)
|
||||
|
||||
require.Len(t, client.Actions(), 1)
|
||||
actualAction := client.Actions()[0].(coretesting.CreateActionImpl)
|
||||
actualSecret := actualAction.GetObject().(*corev1.Secret)
|
||||
|
||||
// The generated secret was labeled with that auto-generated request ID
|
||||
require.Equal(t, request.ID, actualSecret.Labels["storage.pinniped.dev/request-id"])
|
||||
}
|
@ -14,9 +14,11 @@ import (
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/fositestorage/accesstoken"
|
||||
"go.pinniped.dev/internal/fositestorage/authorizationcode"
|
||||
"go.pinniped.dev/internal/fositestorage/openidconnect"
|
||||
"go.pinniped.dev/internal/fositestorage/pkce"
|
||||
"go.pinniped.dev/internal/fositestorage/refreshtoken"
|
||||
)
|
||||
|
||||
const errKubeStorageNotImplemented = constable.Error("KubeStorage does not implement this method. It should not have been called.")
|
||||
@ -25,6 +27,8 @@ type KubeStorage struct {
|
||||
authorizationCodeStorage oauth2.AuthorizeCodeStorage
|
||||
pkceStorage fositepkce.PKCERequestStorage
|
||||
oidcStorage openid.OpenIDConnectRequestStorage
|
||||
accessTokenStorage accesstoken.RevocationStorage
|
||||
refreshTokenStorage refreshtoken.RevocationStorage
|
||||
}
|
||||
|
||||
func NewKubeStorage(secrets corev1client.SecretInterface) *KubeStorage {
|
||||
@ -32,39 +36,41 @@ func NewKubeStorage(secrets corev1client.SecretInterface) *KubeStorage {
|
||||
authorizationCodeStorage: authorizationcode.New(secrets),
|
||||
pkceStorage: pkce.New(secrets),
|
||||
oidcStorage: openidconnect.New(secrets),
|
||||
accessTokenStorage: accesstoken.New(secrets),
|
||||
refreshTokenStorage: refreshtoken.New(secrets),
|
||||
}
|
||||
}
|
||||
|
||||
func (KubeStorage) RevokeRefreshToken(_ context.Context, _ string) error {
|
||||
return errKubeStorageNotImplemented
|
||||
func (k KubeStorage) RevokeRefreshToken(ctx context.Context, requestID string) error {
|
||||
return k.refreshTokenStorage.RevokeRefreshToken(ctx, requestID)
|
||||
}
|
||||
|
||||
func (KubeStorage) RevokeAccessToken(_ context.Context, _ string) error {
|
||||
return errKubeStorageNotImplemented
|
||||
func (k KubeStorage) RevokeAccessToken(ctx context.Context, requestID string) error {
|
||||
return k.accessTokenStorage.RevokeAccessToken(ctx, requestID)
|
||||
}
|
||||
|
||||
func (KubeStorage) CreateRefreshTokenSession(_ context.Context, _ string, _ fosite.Requester) (err error) {
|
||||
return nil
|
||||
func (k KubeStorage) CreateRefreshTokenSession(ctx context.Context, signature string, request fosite.Requester) (err error) {
|
||||
return k.refreshTokenStorage.CreateRefreshTokenSession(ctx, signature, request)
|
||||
}
|
||||
|
||||
func (KubeStorage) GetRefreshTokenSession(_ context.Context, _ string, _ fosite.Session) (request fosite.Requester, err error) {
|
||||
return nil, errKubeStorageNotImplemented
|
||||
func (k KubeStorage) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) {
|
||||
return k.refreshTokenStorage.GetRefreshTokenSession(ctx, signature, session)
|
||||
}
|
||||
|
||||
func (KubeStorage) DeleteRefreshTokenSession(_ context.Context, _ string) (err error) {
|
||||
return errKubeStorageNotImplemented
|
||||
func (k KubeStorage) DeleteRefreshTokenSession(ctx context.Context, signature string) (err error) {
|
||||
return k.refreshTokenStorage.DeleteRefreshTokenSession(ctx, signature)
|
||||
}
|
||||
|
||||
func (KubeStorage) CreateAccessTokenSession(_ context.Context, _ string, _ fosite.Requester) (err error) {
|
||||
return nil
|
||||
func (k KubeStorage) CreateAccessTokenSession(ctx context.Context, signature string, requester fosite.Requester) (err error) {
|
||||
return k.accessTokenStorage.CreateAccessTokenSession(ctx, signature, requester)
|
||||
}
|
||||
|
||||
func (KubeStorage) GetAccessTokenSession(_ context.Context, _ string, _ fosite.Session) (request fosite.Requester, err error) {
|
||||
return nil, errKubeStorageNotImplemented
|
||||
func (k KubeStorage) GetAccessTokenSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) {
|
||||
return k.accessTokenStorage.GetAccessTokenSession(ctx, signature, session)
|
||||
}
|
||||
|
||||
func (KubeStorage) DeleteAccessTokenSession(_ context.Context, _ string) (err error) {
|
||||
return errKubeStorageNotImplemented
|
||||
func (k KubeStorage) DeleteAccessTokenSession(ctx context.Context, signature string) (err error) {
|
||||
return k.accessTokenStorage.DeleteAccessTokenSession(ctx, signature)
|
||||
}
|
||||
|
||||
func (k KubeStorage) CreateOpenIDConnectSession(ctx context.Context, authcode string, requester fosite.Requester) error {
|
||||
|
@ -190,7 +190,7 @@ func TestManager(t *testing.T) {
|
||||
oidctestutil.VerifyECDSAIDToken(t, jwkIssuer, downstreamClientID, privateKey, idToken)
|
||||
|
||||
// Make sure that we wired up the callback endpoint to use kube storage for fosite sessions.
|
||||
r.Equal(len(kubeClient.Actions()), numberOfKubeActionsBeforeThisRequest+7,
|
||||
r.Equal(len(kubeClient.Actions()), numberOfKubeActionsBeforeThisRequest+8,
|
||||
"did not perform any kube actions during the callback request, but should have")
|
||||
}
|
||||
|
||||
|
@ -8,8 +8,6 @@ import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@ -24,10 +22,11 @@ import (
|
||||
"github.com/ory/fosite/handler/oauth2"
|
||||
"github.com/ory/fosite/handler/openid"
|
||||
"github.com/ory/fosite/handler/pkce"
|
||||
"github.com/ory/fosite/storage"
|
||||
"github.com/ory/fosite/token/jwt"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/oidc"
|
||||
@ -55,8 +54,8 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
goodAuthTime = time.Date(1, 2, 3, 4, 5, 6, 7, time.Local)
|
||||
goodRequestedAtTime = time.Date(7, 6, 5, 4, 3, 2, 1, time.Local)
|
||||
goodAuthTime = time.Date(1, 2, 3, 4, 5, 6, 7, time.UTC)
|
||||
goodRequestedAtTime = time.Date(7, 6, 5, 4, 3, 2, 1, time.UTC)
|
||||
|
||||
fositeInvalidMethodErrorBody = func(actual string) string {
|
||||
return here.Docf(`
|
||||
@ -384,15 +383,19 @@ func TestTokenEndpoint(t *testing.T) {
|
||||
test.authRequest(authRequest)
|
||||
}
|
||||
|
||||
oauthStore := storage.NewMemoryStore()
|
||||
// Add the Pinniped CLI client.
|
||||
oauthStore.Clients[goodClient] = oidc.PinnipedCLIOIDCClient()
|
||||
client := fake.NewSimpleClientset()
|
||||
secrets := client.CoreV1().Secrets("some-namespace")
|
||||
|
||||
oauthStore := oidc.NewKubeStorage(secrets)
|
||||
oauthHelper, authCode, jwtSigningKey := makeHappyOauthHelper(t, authRequest, oauthStore)
|
||||
|
||||
if test.storage != nil {
|
||||
test.storage(t, oauthStore, authCode)
|
||||
}
|
||||
subject := NewHandler(oauthHelper)
|
||||
|
||||
// TODO add assertions about how many of each storage type exist at this point as a pre-condition
|
||||
|
||||
req := httptest.NewRequest("POST", "/path/shouldn't/matter", happyBody(authCode).ReadCloser())
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
if test.request != nil {
|
||||
@ -421,6 +424,8 @@ func TestTokenEndpoint(t *testing.T) {
|
||||
if wantOpenidScope {
|
||||
requireValidIDToken(t, m, jwtSigningKey)
|
||||
}
|
||||
|
||||
// TODO add assertions about how many of each storage type are remaining at this point
|
||||
} else {
|
||||
require.JSONEq(t, test.wantExactBody, rsp.Body.String())
|
||||
}
|
||||
@ -429,12 +434,16 @@ func TestTokenEndpoint(t *testing.T) {
|
||||
|
||||
t.Run("auth code is used twice", func(t *testing.T) {
|
||||
authRequest := deepCopyRequestForm(happyAuthRequest)
|
||||
oauthStore := storage.NewMemoryStore()
|
||||
// Add the Pinniped CLI client.
|
||||
oauthStore.Clients[goodClient] = oidc.PinnipedCLIOIDCClient()
|
||||
|
||||
client := fake.NewSimpleClientset()
|
||||
secrets := client.CoreV1().Secrets("some-namespace")
|
||||
|
||||
oauthStore := oidc.NewKubeStorage(secrets)
|
||||
oauthHelper, authCode, jwtSigningKey := makeHappyOauthHelper(t, authRequest, oauthStore)
|
||||
subject := NewHandler(oauthHelper)
|
||||
|
||||
// TODO add assertions about how many of each storage type exist at this point as a pre-condition
|
||||
|
||||
req := httptest.NewRequest("POST", "/path/shouldn't/matter", happyBody(authCode).ReadCloser())
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
@ -460,6 +469,8 @@ func TestTokenEndpoint(t *testing.T) {
|
||||
requireValidOIDCStorage(t, m, code, oauthStore, wantOpenidScope)
|
||||
requireValidIDToken(t, m, jwtSigningKey)
|
||||
|
||||
// TODO add assertions about how many of each storage type are remaining at this point
|
||||
|
||||
// Second call - should be unsuccessful since auth code was already used.
|
||||
//
|
||||
// Fosite will also revoke the access token as is recommended by the OIDC spec. Currently, we don't
|
||||
@ -476,6 +487,8 @@ func TestTokenEndpoint(t *testing.T) {
|
||||
requireInvalidAccessTokenStorage(t, m, oauthStore)
|
||||
requireInvalidPKCEStorage(t, code, oauthStore)
|
||||
requireValidOIDCStorage(t, m, code, oauthStore, wantOpenidScope)
|
||||
|
||||
// TODO add assertions about how many of each storage type are remaining at this point
|
||||
})
|
||||
}
|
||||
|
||||
@ -590,16 +603,6 @@ func generateJWTSigningKeyAndJWKSProvider(t *testing.T, issuer string) (*ecdsa.P
|
||||
return key, jwksProvider
|
||||
}
|
||||
|
||||
func hashAccessToken(accessToken string) string {
|
||||
// See https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken.
|
||||
// "Access Token hash value. Its value is the base64url encoding of the left-most half of
|
||||
// the hash of the octets of the ASCII representation of the access_token value, where the
|
||||
// hash algorithm used is the hash algorithm used in the alg Header Parameter of the ID
|
||||
// Token's JOSE Header."
|
||||
b := sha256.Sum256([]byte(accessToken))
|
||||
return base64.RawURLEncoding.EncodeToString(b[:len(b)/2])
|
||||
}
|
||||
|
||||
func requireInvalidAuthCodeStorage(
|
||||
t *testing.T,
|
||||
code string,
|
||||
@ -609,7 +612,7 @@ func requireInvalidAuthCodeStorage(
|
||||
|
||||
// Make sure we have invalidated this auth code.
|
||||
_, err := storage.GetAuthorizeCodeSession(context.Background(), getFositeDataSignature(t, code), nil)
|
||||
require.Equal(t, fosite.ErrInvalidatedAuthorizeCode, err)
|
||||
require.True(t, errors.Is(err, fosite.ErrInvalidatedAuthorizeCode))
|
||||
}
|
||||
|
||||
func requireValidAccessTokenStorage(
|
||||
@ -656,8 +659,8 @@ func requireValidAccessTokenStorage(
|
||||
t,
|
||||
authRequest,
|
||||
authRequest.Sanitize([]string{}).GetRequestForm(),
|
||||
hashAccessToken(accessTokenString),
|
||||
wantGrantedOpenidScope,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
@ -674,7 +677,7 @@ func requireInvalidAccessTokenStorage(
|
||||
accessTokenString, ok := accessToken.(string)
|
||||
require.Truef(t, ok, "wanted access_token to be a string, but got %T", accessToken)
|
||||
_, err := storage.GetAccessTokenSession(context.Background(), getFositeDataSignature(t, accessTokenString), nil)
|
||||
require.Equal(t, fosite.ErrNotFound, err)
|
||||
require.True(t, errors.Is(err, fosite.ErrNotFound))
|
||||
}
|
||||
|
||||
func requireInvalidPKCEStorage(
|
||||
@ -687,7 +690,7 @@ func requireInvalidPKCEStorage(
|
||||
// Make sure the PKCE session has been deleted. Note that Fosite stores PKCE codes using the auth code signature
|
||||
// as a key.
|
||||
_, err := storage.GetPKCERequestSession(context.Background(), getFositeDataSignature(t, code), nil)
|
||||
require.Equal(t, fosite.ErrNotFound, err)
|
||||
require.True(t, errors.Is(err, fosite.ErrNotFound))
|
||||
}
|
||||
|
||||
func requireValidOIDCStorage(
|
||||
@ -709,16 +712,18 @@ func requireValidOIDCStorage(
|
||||
require.True(t, ok)
|
||||
accessTokenString, ok := accessToken.(string)
|
||||
require.Truef(t, ok, "wanted access_token to be a string, but got %T", accessToken)
|
||||
require.NotEmpty(t, accessTokenString)
|
||||
|
||||
requireValidAuthRequest(
|
||||
t,
|
||||
authRequest,
|
||||
authRequest.Sanitize([]string{"nonce"}).GetRequestForm(),
|
||||
hashAccessToken(accessTokenString),
|
||||
true,
|
||||
false,
|
||||
)
|
||||
} else {
|
||||
_, err := storage.GetOpenIDConnectSession(context.Background(), code, nil)
|
||||
require.Equal(t, fosite.ErrNotFound, err)
|
||||
require.True(t, errors.Is(err, fosite.ErrNotFound))
|
||||
}
|
||||
}
|
||||
|
||||
@ -726,8 +731,8 @@ func requireValidAuthRequest(
|
||||
t *testing.T,
|
||||
authRequest fosite.Requester,
|
||||
wantRequestForm url.Values,
|
||||
wantAccessTokenHash string,
|
||||
wantGrantedOpenidScope bool,
|
||||
wantAccessTokenExpiresAt bool,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
@ -755,24 +760,28 @@ func requireValidAuthRequest(
|
||||
if wantGrantedOpenidScope {
|
||||
claims := session.Claims
|
||||
require.Empty(t, claims.JTI) // When claims.JTI is empty, Fosite will generate a UUID for this field.
|
||||
require.Equal(t, goodIssuer, claims.Issuer)
|
||||
require.Equal(t, goodSubject, claims.Subject)
|
||||
require.Equal(t, []string{goodClient}, claims.Audience)
|
||||
require.Equal(t, goodNonce, claims.Nonce)
|
||||
testutil.RequireTimeInDelta(
|
||||
t,
|
||||
time.Now().UTC().Add(idTokenExpirationSeconds*time.Second),
|
||||
claims.ExpiresAt,
|
||||
timeComparisonFudgeSeconds*time.Second,
|
||||
)
|
||||
testutil.RequireTimeInDelta(t, time.Now().UTC(), claims.IssuedAt, timeComparisonFudgeSeconds*time.Second)
|
||||
require.Equal(t, wantAccessTokenHash, claims.AccessTokenHash)
|
||||
|
||||
// We are in charge of setting these fields. For the purpose of testing, we ensure that the
|
||||
// sentinel test value is set correctly.
|
||||
require.Equal(t, goodRequestedAtTime, claims.RequestedAt)
|
||||
require.Equal(t, goodAuthTime, claims.AuthTime)
|
||||
|
||||
// These fields will all be given good defaults by fosite at runtime and we only need to use them
|
||||
// if we want to override the default behaviors. We currently don't need to override these defaults,
|
||||
// so they do not end up being stored. Fosite sets its defaults at runtime in openid.DefaultSession's
|
||||
// GenerateIDToken() method.
|
||||
require.Empty(t, claims.Issuer)
|
||||
require.Empty(t, claims.Audience)
|
||||
require.Empty(t, claims.Nonce)
|
||||
require.Zero(t, claims.ExpiresAt)
|
||||
require.Zero(t, claims.IssuedAt)
|
||||
|
||||
// Fosite unconditionally overwrites claims.AccessTokenHash at runtime in openid.OpenIDConnectExplicitHandler's
|
||||
// PopulateTokenEndpointResponse() method, just before it calls the same GenerateIDToken() mentioned above,
|
||||
// so it does not end up saved in storage.
|
||||
require.Empty(t, claims.AccessTokenHash)
|
||||
|
||||
// At this time, we don't use any of these optional (per the OIDC spec) fields.
|
||||
require.Empty(t, claims.AuthenticationContextClassReference)
|
||||
require.Empty(t, claims.AuthenticationMethodsReference)
|
||||
@ -793,14 +802,20 @@ func requireValidAuthRequest(
|
||||
authCodeExpiresAt,
|
||||
timeComparisonFudgeSeconds*time.Second,
|
||||
)
|
||||
|
||||
// OpenID Connect sessions do not store access token expiration information.
|
||||
accessTokenExpiresAt, ok := session.ExpiresAt[fosite.AccessToken]
|
||||
require.True(t, ok, "expected session to hold expiration time for access token")
|
||||
testutil.RequireTimeInDelta(
|
||||
t,
|
||||
time.Now().UTC().Add(accessTokenExpirationSeconds*time.Second),
|
||||
accessTokenExpiresAt,
|
||||
timeComparisonFudgeSeconds*time.Second,
|
||||
)
|
||||
if wantAccessTokenExpiresAt {
|
||||
require.True(t, ok, "expected session to hold expiration time for access token")
|
||||
testutil.RequireTimeInDelta(
|
||||
t,
|
||||
time.Now().UTC().Add(accessTokenExpirationSeconds*time.Second),
|
||||
accessTokenExpiresAt,
|
||||
timeComparisonFudgeSeconds*time.Second,
|
||||
)
|
||||
} else {
|
||||
require.False(t, ok, "expected session to not hold expiration time for access token, but it did")
|
||||
}
|
||||
|
||||
// Assert that the session's username and subject are correct.
|
||||
require.Equal(t, goodUsername, session.Username)
|
||||
@ -830,7 +845,10 @@ func requireValidIDToken(t *testing.T, body map[string]interface{}, jwtSigningKe
|
||||
RequestedAt int64 `json:"rat"`
|
||||
AuthTime int64 `json:"auth_time"`
|
||||
}
|
||||
idTokenFields := []string{"sub", "aud", "iss", "jti", "nonce", "auth_time", "at_hash", "exp", "iat", "rat"}
|
||||
|
||||
// Note that there is a bug in fosite which prevents the `at_hash` claim from appearing in this ID token.
|
||||
// We can add a workaround for this later.
|
||||
idTokenFields := []string{"sub", "aud", "iss", "jti", "nonce", "auth_time", "exp", "iat", "rat"}
|
||||
|
||||
// make sure that these are the only fields in the token
|
||||
var m map[string]interface{}
|
||||
@ -846,7 +864,6 @@ func requireValidIDToken(t *testing.T, body map[string]interface{}, jwtSigningKe
|
||||
require.Equal(t, goodIssuer, claims.Issuer)
|
||||
require.NotEmpty(t, claims.JTI)
|
||||
require.Equal(t, goodNonce, claims.Nonce)
|
||||
require.NotEmpty(t, claims.AccessTokenHash)
|
||||
|
||||
expiresAt := time.Unix(claims.ExpiresAt, 0)
|
||||
issuedAt := time.Unix(claims.IssuedAt, 0)
|
||||
|
Loading…
Reference in New Issue
Block a user