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"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/constable"
|
"go.pinniped.dev/internal/constable"
|
||||||
@ -34,10 +35,11 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Storage interface {
|
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)
|
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)
|
Update(ctx context.Context, signature, resourceVersion string, data JSON) (newResourceVersion string, err error)
|
||||||
Delete(ctx context.Context, signature string) 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
|
type JSON interface{} // document that we need valid JSON types
|
||||||
@ -58,8 +60,8 @@ type secretsStorage struct {
|
|||||||
secrets corev1client.SecretInterface
|
secrets corev1client.SecretInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *secretsStorage) Create(ctx context.Context, signature string, data JSON) (string, error) {
|
func (s *secretsStorage) Create(ctx context.Context, signature string, data JSON, additionalLabels map[string]string) (string, error) {
|
||||||
secret, err := s.toSecret(signature, "", data)
|
secret, err := s.toSecret(signature, "", data, additionalLabels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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) {
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -116,6 +118,28 @@ func (s *secretsStorage) Delete(ctx context.Context, signature string) error {
|
|||||||
return nil
|
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
|
//nolint: gochecknoglobals
|
||||||
var b32 = base32.StdEncoding.WithPadding(base32.NoPadding)
|
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)
|
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)
|
buf, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to encode secret data for %s: %w", s.getName(signature), err)
|
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{
|
return &corev1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: s.getName(signature),
|
Name: s.getName(signature),
|
||||||
ResourceVersion: resourceVersion,
|
ResourceVersion: resourceVersion,
|
||||||
Labels: map[string]string{
|
Labels: labels,
|
||||||
secretLabelKey: s.resource, // make it easier to find this stuff via kubectl
|
|
||||||
},
|
|
||||||
OwnerReferences: nil,
|
OwnerReferences: nil,
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
|
@ -6,6 +6,7 @@ package crud
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ory/fosite/compose"
|
"github.com/ory/fosite/compose"
|
||||||
@ -87,6 +88,7 @@ func TestStorage(t *testing.T) {
|
|||||||
wantSecrets: nil,
|
wantSecrets: nil,
|
||||||
wantErr: `failed to delete tokens for signature not-a-token: secrets "pinniped-storage-tokens-t2fx427lnci6s" not found`,
|
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",
|
name: "create and get",
|
||||||
resource: "access-tokens",
|
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
|
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
||||||
|
|
||||||
data := &testJSON{Data: "create-and-get"}
|
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.Empty(t, rv1) // fake client does not set this
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -145,6 +147,68 @@ func TestStorage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantErr: "",
|
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",
|
name: "get existing",
|
||||||
resource: "pandas-are-best",
|
resource: "pandas-are-best",
|
||||||
@ -325,6 +389,207 @@ func TestStorage(t *testing.T) {
|
|||||||
wantSecrets: nil,
|
wantSecrets: nil,
|
||||||
wantErr: "",
|
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",
|
name: "invalid exiting secret type",
|
||||||
resource: "candies",
|
resource: "candies",
|
||||||
@ -582,8 +847,11 @@ func checkSecretActionNames(t *testing.T, actions []coretesting.Action) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
for _, action := range actions {
|
for _, action := range actions {
|
||||||
name := getName(t, action)
|
_, ok := action.(coretesting.ListActionImpl)
|
||||||
assertValidName(t, name)
|
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.
|
// of the consent authorization request. It is used to identify the session.
|
||||||
// signature for lookup in the DB
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,9 +11,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ErrInvalidRequestType = constable.Error("requester must be of type fosite.Request")
|
ErrInvalidRequestType = constable.Error("requester must be of type fosite.Request")
|
||||||
ErrInvalidClientType = constable.Error("requester's client must be of type fosite.DefaultOpenIDConnectClient")
|
ErrInvalidClientType = constable.Error("requester's client must be of type fosite.DefaultOpenIDConnectClient")
|
||||||
ErrInvalidSessionType = constable.Error("requester's session must be of type openid.DefaultSession")
|
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) {
|
func ValidateAndExtractAuthorizeRequest(requester fosite.Requester) (*fosite.Request, error) {
|
||||||
|
@ -52,7 +52,7 @@ func (a *openIDConnectRequestStorage) CreateOpenIDConnectSession(ctx context.Con
|
|||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ func (a *pkceStorage) CreatePKCERequestSession(ctx context.Context, signature st
|
|||||||
return err
|
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
|
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"
|
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/constable"
|
"go.pinniped.dev/internal/constable"
|
||||||
|
"go.pinniped.dev/internal/fositestorage/accesstoken"
|
||||||
"go.pinniped.dev/internal/fositestorage/authorizationcode"
|
"go.pinniped.dev/internal/fositestorage/authorizationcode"
|
||||||
"go.pinniped.dev/internal/fositestorage/openidconnect"
|
"go.pinniped.dev/internal/fositestorage/openidconnect"
|
||||||
"go.pinniped.dev/internal/fositestorage/pkce"
|
"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.")
|
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
|
authorizationCodeStorage oauth2.AuthorizeCodeStorage
|
||||||
pkceStorage fositepkce.PKCERequestStorage
|
pkceStorage fositepkce.PKCERequestStorage
|
||||||
oidcStorage openid.OpenIDConnectRequestStorage
|
oidcStorage openid.OpenIDConnectRequestStorage
|
||||||
|
accessTokenStorage accesstoken.RevocationStorage
|
||||||
|
refreshTokenStorage refreshtoken.RevocationStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKubeStorage(secrets corev1client.SecretInterface) *KubeStorage {
|
func NewKubeStorage(secrets corev1client.SecretInterface) *KubeStorage {
|
||||||
@ -32,39 +36,41 @@ func NewKubeStorage(secrets corev1client.SecretInterface) *KubeStorage {
|
|||||||
authorizationCodeStorage: authorizationcode.New(secrets),
|
authorizationCodeStorage: authorizationcode.New(secrets),
|
||||||
pkceStorage: pkce.New(secrets),
|
pkceStorage: pkce.New(secrets),
|
||||||
oidcStorage: openidconnect.New(secrets),
|
oidcStorage: openidconnect.New(secrets),
|
||||||
|
accessTokenStorage: accesstoken.New(secrets),
|
||||||
|
refreshTokenStorage: refreshtoken.New(secrets),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (KubeStorage) RevokeRefreshToken(_ context.Context, _ string) error {
|
func (k KubeStorage) RevokeRefreshToken(ctx context.Context, requestID string) error {
|
||||||
return errKubeStorageNotImplemented
|
return k.refreshTokenStorage.RevokeRefreshToken(ctx, requestID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (KubeStorage) RevokeAccessToken(_ context.Context, _ string) error {
|
func (k KubeStorage) RevokeAccessToken(ctx context.Context, requestID string) error {
|
||||||
return errKubeStorageNotImplemented
|
return k.accessTokenStorage.RevokeAccessToken(ctx, requestID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (KubeStorage) CreateRefreshTokenSession(_ context.Context, _ string, _ fosite.Requester) (err error) {
|
func (k KubeStorage) CreateRefreshTokenSession(ctx context.Context, signature string, request fosite.Requester) (err error) {
|
||||||
return nil
|
return k.refreshTokenStorage.CreateRefreshTokenSession(ctx, signature, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (KubeStorage) GetRefreshTokenSession(_ context.Context, _ string, _ fosite.Session) (request fosite.Requester, err error) {
|
func (k KubeStorage) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) {
|
||||||
return nil, errKubeStorageNotImplemented
|
return k.refreshTokenStorage.GetRefreshTokenSession(ctx, signature, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (KubeStorage) DeleteRefreshTokenSession(_ context.Context, _ string) (err error) {
|
func (k KubeStorage) DeleteRefreshTokenSession(ctx context.Context, signature string) (err error) {
|
||||||
return errKubeStorageNotImplemented
|
return k.refreshTokenStorage.DeleteRefreshTokenSession(ctx, signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (KubeStorage) CreateAccessTokenSession(_ context.Context, _ string, _ fosite.Requester) (err error) {
|
func (k KubeStorage) CreateAccessTokenSession(ctx context.Context, signature string, requester fosite.Requester) (err error) {
|
||||||
return nil
|
return k.accessTokenStorage.CreateAccessTokenSession(ctx, signature, requester)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (KubeStorage) GetAccessTokenSession(_ context.Context, _ string, _ fosite.Session) (request fosite.Requester, err error) {
|
func (k KubeStorage) GetAccessTokenSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) {
|
||||||
return nil, errKubeStorageNotImplemented
|
return k.accessTokenStorage.GetAccessTokenSession(ctx, signature, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (KubeStorage) DeleteAccessTokenSession(_ context.Context, _ string) (err error) {
|
func (k KubeStorage) DeleteAccessTokenSession(ctx context.Context, signature string) (err error) {
|
||||||
return errKubeStorageNotImplemented
|
return k.accessTokenStorage.DeleteAccessTokenSession(ctx, signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k KubeStorage) CreateOpenIDConnectSession(ctx context.Context, authcode string, requester fosite.Requester) error {
|
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)
|
oidctestutil.VerifyECDSAIDToken(t, jwkIssuer, downstreamClientID, privateKey, idToken)
|
||||||
|
|
||||||
// Make sure that we wired up the callback endpoint to use kube storage for fosite sessions.
|
// 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")
|
"did not perform any kube actions during the callback request, but should have")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,8 +8,6 @@ import (
|
|||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -24,10 +22,11 @@ import (
|
|||||||
"github.com/ory/fosite/handler/oauth2"
|
"github.com/ory/fosite/handler/oauth2"
|
||||||
"github.com/ory/fosite/handler/openid"
|
"github.com/ory/fosite/handler/openid"
|
||||||
"github.com/ory/fosite/handler/pkce"
|
"github.com/ory/fosite/handler/pkce"
|
||||||
"github.com/ory/fosite/storage"
|
|
||||||
"github.com/ory/fosite/token/jwt"
|
"github.com/ory/fosite/token/jwt"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/here"
|
"go.pinniped.dev/internal/here"
|
||||||
"go.pinniped.dev/internal/oidc"
|
"go.pinniped.dev/internal/oidc"
|
||||||
@ -55,8 +54,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
goodAuthTime = time.Date(1, 2, 3, 4, 5, 6, 7, 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.Local)
|
goodRequestedAtTime = time.Date(7, 6, 5, 4, 3, 2, 1, time.UTC)
|
||||||
|
|
||||||
fositeInvalidMethodErrorBody = func(actual string) string {
|
fositeInvalidMethodErrorBody = func(actual string) string {
|
||||||
return here.Docf(`
|
return here.Docf(`
|
||||||
@ -384,15 +383,19 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
test.authRequest(authRequest)
|
test.authRequest(authRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
oauthStore := storage.NewMemoryStore()
|
client := fake.NewSimpleClientset()
|
||||||
// Add the Pinniped CLI client.
|
secrets := client.CoreV1().Secrets("some-namespace")
|
||||||
oauthStore.Clients[goodClient] = oidc.PinnipedCLIOIDCClient()
|
|
||||||
|
oauthStore := oidc.NewKubeStorage(secrets)
|
||||||
oauthHelper, authCode, jwtSigningKey := makeHappyOauthHelper(t, authRequest, oauthStore)
|
oauthHelper, authCode, jwtSigningKey := makeHappyOauthHelper(t, authRequest, oauthStore)
|
||||||
|
|
||||||
if test.storage != nil {
|
if test.storage != nil {
|
||||||
test.storage(t, oauthStore, authCode)
|
test.storage(t, oauthStore, authCode)
|
||||||
}
|
}
|
||||||
subject := NewHandler(oauthHelper)
|
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 := httptest.NewRequest("POST", "/path/shouldn't/matter", happyBody(authCode).ReadCloser())
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
if test.request != nil {
|
if test.request != nil {
|
||||||
@ -421,6 +424,8 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
if wantOpenidScope {
|
if wantOpenidScope {
|
||||||
requireValidIDToken(t, m, jwtSigningKey)
|
requireValidIDToken(t, m, jwtSigningKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO add assertions about how many of each storage type are remaining at this point
|
||||||
} else {
|
} else {
|
||||||
require.JSONEq(t, test.wantExactBody, rsp.Body.String())
|
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) {
|
t.Run("auth code is used twice", func(t *testing.T) {
|
||||||
authRequest := deepCopyRequestForm(happyAuthRequest)
|
authRequest := deepCopyRequestForm(happyAuthRequest)
|
||||||
oauthStore := storage.NewMemoryStore()
|
|
||||||
// Add the Pinniped CLI client.
|
client := fake.NewSimpleClientset()
|
||||||
oauthStore.Clients[goodClient] = oidc.PinnipedCLIOIDCClient()
|
secrets := client.CoreV1().Secrets("some-namespace")
|
||||||
|
|
||||||
|
oauthStore := oidc.NewKubeStorage(secrets)
|
||||||
oauthHelper, authCode, jwtSigningKey := makeHappyOauthHelper(t, authRequest, oauthStore)
|
oauthHelper, authCode, jwtSigningKey := makeHappyOauthHelper(t, authRequest, oauthStore)
|
||||||
subject := NewHandler(oauthHelper)
|
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 := httptest.NewRequest("POST", "/path/shouldn't/matter", happyBody(authCode).ReadCloser())
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
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)
|
requireValidOIDCStorage(t, m, code, oauthStore, wantOpenidScope)
|
||||||
requireValidIDToken(t, m, jwtSigningKey)
|
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.
|
// 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
|
// 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)
|
requireInvalidAccessTokenStorage(t, m, oauthStore)
|
||||||
requireInvalidPKCEStorage(t, code, oauthStore)
|
requireInvalidPKCEStorage(t, code, oauthStore)
|
||||||
requireValidOIDCStorage(t, m, code, oauthStore, wantOpenidScope)
|
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
|
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(
|
func requireInvalidAuthCodeStorage(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
code string,
|
code string,
|
||||||
@ -609,7 +612,7 @@ func requireInvalidAuthCodeStorage(
|
|||||||
|
|
||||||
// Make sure we have invalidated this auth code.
|
// Make sure we have invalidated this auth code.
|
||||||
_, err := storage.GetAuthorizeCodeSession(context.Background(), getFositeDataSignature(t, code), nil)
|
_, 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(
|
func requireValidAccessTokenStorage(
|
||||||
@ -656,8 +659,8 @@ func requireValidAccessTokenStorage(
|
|||||||
t,
|
t,
|
||||||
authRequest,
|
authRequest,
|
||||||
authRequest.Sanitize([]string{}).GetRequestForm(),
|
authRequest.Sanitize([]string{}).GetRequestForm(),
|
||||||
hashAccessToken(accessTokenString),
|
|
||||||
wantGrantedOpenidScope,
|
wantGrantedOpenidScope,
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -674,7 +677,7 @@ func requireInvalidAccessTokenStorage(
|
|||||||
accessTokenString, ok := accessToken.(string)
|
accessTokenString, ok := accessToken.(string)
|
||||||
require.Truef(t, ok, "wanted access_token to be a string, but got %T", accessToken)
|
require.Truef(t, ok, "wanted access_token to be a string, but got %T", accessToken)
|
||||||
_, err := storage.GetAccessTokenSession(context.Background(), getFositeDataSignature(t, accessTokenString), nil)
|
_, 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(
|
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
|
// Make sure the PKCE session has been deleted. Note that Fosite stores PKCE codes using the auth code signature
|
||||||
// as a key.
|
// as a key.
|
||||||
_, err := storage.GetPKCERequestSession(context.Background(), getFositeDataSignature(t, code), nil)
|
_, 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(
|
func requireValidOIDCStorage(
|
||||||
@ -709,16 +712,18 @@ func requireValidOIDCStorage(
|
|||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
accessTokenString, ok := accessToken.(string)
|
accessTokenString, ok := accessToken.(string)
|
||||||
require.Truef(t, ok, "wanted access_token to be a string, but got %T", accessToken)
|
require.Truef(t, ok, "wanted access_token to be a string, but got %T", accessToken)
|
||||||
|
require.NotEmpty(t, accessTokenString)
|
||||||
|
|
||||||
requireValidAuthRequest(
|
requireValidAuthRequest(
|
||||||
t,
|
t,
|
||||||
authRequest,
|
authRequest,
|
||||||
authRequest.Sanitize([]string{"nonce"}).GetRequestForm(),
|
authRequest.Sanitize([]string{"nonce"}).GetRequestForm(),
|
||||||
hashAccessToken(accessTokenString),
|
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
_, err := storage.GetOpenIDConnectSession(context.Background(), code, nil)
|
_, 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,
|
t *testing.T,
|
||||||
authRequest fosite.Requester,
|
authRequest fosite.Requester,
|
||||||
wantRequestForm url.Values,
|
wantRequestForm url.Values,
|
||||||
wantAccessTokenHash string,
|
|
||||||
wantGrantedOpenidScope bool,
|
wantGrantedOpenidScope bool,
|
||||||
|
wantAccessTokenExpiresAt bool,
|
||||||
) {
|
) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@ -755,24 +760,28 @@ func requireValidAuthRequest(
|
|||||||
if wantGrantedOpenidScope {
|
if wantGrantedOpenidScope {
|
||||||
claims := session.Claims
|
claims := session.Claims
|
||||||
require.Empty(t, claims.JTI) // When claims.JTI is empty, Fosite will generate a UUID for this field.
|
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, 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
|
// We are in charge of setting these fields. For the purpose of testing, we ensure that the
|
||||||
// sentinel test value is set correctly.
|
// sentinel test value is set correctly.
|
||||||
require.Equal(t, goodRequestedAtTime, claims.RequestedAt)
|
require.Equal(t, goodRequestedAtTime, claims.RequestedAt)
|
||||||
require.Equal(t, goodAuthTime, claims.AuthTime)
|
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.
|
// 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.AuthenticationContextClassReference)
|
||||||
require.Empty(t, claims.AuthenticationMethodsReference)
|
require.Empty(t, claims.AuthenticationMethodsReference)
|
||||||
@ -793,14 +802,20 @@ func requireValidAuthRequest(
|
|||||||
authCodeExpiresAt,
|
authCodeExpiresAt,
|
||||||
timeComparisonFudgeSeconds*time.Second,
|
timeComparisonFudgeSeconds*time.Second,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// OpenID Connect sessions do not store access token expiration information.
|
||||||
accessTokenExpiresAt, ok := session.ExpiresAt[fosite.AccessToken]
|
accessTokenExpiresAt, ok := session.ExpiresAt[fosite.AccessToken]
|
||||||
require.True(t, ok, "expected session to hold expiration time for access token")
|
if wantAccessTokenExpiresAt {
|
||||||
testutil.RequireTimeInDelta(
|
require.True(t, ok, "expected session to hold expiration time for access token")
|
||||||
t,
|
testutil.RequireTimeInDelta(
|
||||||
time.Now().UTC().Add(accessTokenExpirationSeconds*time.Second),
|
t,
|
||||||
accessTokenExpiresAt,
|
time.Now().UTC().Add(accessTokenExpirationSeconds*time.Second),
|
||||||
timeComparisonFudgeSeconds*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.
|
// Assert that the session's username and subject are correct.
|
||||||
require.Equal(t, goodUsername, session.Username)
|
require.Equal(t, goodUsername, session.Username)
|
||||||
@ -830,7 +845,10 @@ func requireValidIDToken(t *testing.T, body map[string]interface{}, jwtSigningKe
|
|||||||
RequestedAt int64 `json:"rat"`
|
RequestedAt int64 `json:"rat"`
|
||||||
AuthTime int64 `json:"auth_time"`
|
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
|
// make sure that these are the only fields in the token
|
||||||
var m map[string]interface{}
|
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.Equal(t, goodIssuer, claims.Issuer)
|
||||||
require.NotEmpty(t, claims.JTI)
|
require.NotEmpty(t, claims.JTI)
|
||||||
require.Equal(t, goodNonce, claims.Nonce)
|
require.Equal(t, goodNonce, claims.Nonce)
|
||||||
require.NotEmpty(t, claims.AccessTokenHash)
|
|
||||||
|
|
||||||
expiresAt := time.Unix(claims.ExpiresAt, 0)
|
expiresAt := time.Unix(claims.ExpiresAt, 0)
|
||||||
issuedAt := time.Unix(claims.IssuedAt, 0)
|
issuedAt := time.Unix(claims.IssuedAt, 0)
|
||||||
|
Loading…
Reference in New Issue
Block a user