Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSS-7081 Add OAuth-specific methods to secrets store #1175

Merged
merged 31 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e9c4283
Add `Get/Put` OAuth key methods to Vault store
babakks Feb 6, 2024
18aee68
Add `Get/Put` OAuth key methods to Postgres store
babakks Feb 6, 2024
909992b
Add `Get/Put` OAuth key methods to in-memory store
babakks Feb 7, 2024
d9bf1d8
Add `Get/Put` OAuth key methods to mock store
babakks Mar 13, 2024
727971e
Add `Get/Put` OAuth key methods to credential store interface
babakks Feb 7, 2024
bd8c6d6
Expose underlying `CredentialStore` via a `JIMM` interface method
babakks Feb 7, 2024
3ec909f
Use `New*` method to instantiate in-memory credential store
babakks Feb 7, 2024
853c193
Use credential store to retrieve OAuth session JWT secret key
babakks Mar 13, 2024
1632b93
Update `CredentialStore` godoc
babakks Feb 12, 2024
029187f
Add test to verify `GetOAuthKey` returns not found error
babakks Feb 13, 2024
efbda2d
Add test to verify `GetOAuthKey` returns not found error
babakks Feb 13, 2024
e1377b5
Add `CheckOrGenerateOAuthKey` method
babakks Feb 13, 2024
eeb683b
Generate OAuth key on the leader unit
babakks Mar 13, 2024
3ff81da
Update suite to generate OAuth key as well
babakks Feb 13, 2024
4d83a15
Add package godoc
babakks Feb 13, 2024
2c8dde0
Reuse shared `JWTTestSecret` in `JimmCmdSuite`
babakks Mar 13, 2024
d26e977
Fix godoc
babakks Mar 13, 2024
ca263af
Add `CleanupOAuth` to Postgres store
babakks Mar 14, 2024
efbca17
Add `CleanupOAuth` to Vault store
babakks Mar 14, 2024
fad18b4
Add `CleanupOAuth` to mock store
babakks Mar 14, 2024
599d23c
Add `CleanupOAuth` to in-memory store
babakks Mar 14, 2024
18b43ae
Add `CleanupOAuth` to credential store interface
babakks Mar 14, 2024
3a47319
Use same const secret for in-memory store
babakks Mar 14, 2024
86cbcbe
fix tests with populating OAuth key secrets in store
babakks Mar 14, 2024
b73e427
Use `*WithContext` variants for read/write methods
babakks Mar 14, 2024
695eced
Use `net.Listen` to find an available TCP port
babakks Mar 14, 2024
7fe0205
Rename `CleanupOAuth` to `CleanupOAuthSecrets`
babakks Mar 15, 2024
b1ae7de
Rename credential store `*OAuthKey` methods to `*OAuthSecret`
babakks Mar 18, 2024
aaa14c5
Run `go mod tidy`
babakks Mar 18, 2024
951086f
Merge branch 'feature-oidc' into css-7081/use-secret-store
babakks Mar 18, 2024
5d9b965
Merge branch 'feature-oidc' into css-7081/use-secret-store
babakks Mar 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions cmd/jimmsrv/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,17 @@ func start(ctx context.Context, s *service.Service) error {
s.Go(func() error { return jimmsvc.WatchModelSummaries(ctx) })

if os.Getenv("JIMM_ENABLE_JWKS_ROTATOR") != "" {
babakks marked this conversation as resolved.
Show resolved Hide resolved
zapctx.Info(ctx, "attempting to start JWKS rotator")
zapctx.Info(ctx, "attempting to start JWKS rotator and generate OAuth secret key")
s.Go(func() error {
err := jimmsvc.StartJWKSRotator(ctx, time.NewTicker(time.Hour).C, time.Now().UTC().AddDate(0, 3, 0))
if err != nil {
if err := jimmsvc.StartJWKSRotator(ctx, time.NewTicker(time.Hour).C, time.Now().UTC().AddDate(0, 3, 0)); err != nil {
zapctx.Error(ctx, "failed to start JWKS rotator", zap.Error(err))
return err
}
return err
if err := jimmsvc.CheckOrGenerateOAuthKey(ctx); err != nil {
zapctx.Error(ctx, "failed to check/generate OAuth secret key", zap.Error(err))
return err
}
return nil
})
}

Expand Down
3 changes: 3 additions & 0 deletions internal/cmdtest/jimmsuite.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) {
err = s.Service.StartJWKSRotator(ctx, time.NewTicker(time.Hour).C, time.Now().UTC().AddDate(0, 3, 0))
c.Assert(err, gc.Equals, nil)

err = s.Service.CheckOrGenerateOAuthKey(ctx)
c.Assert(err, gc.Equals, nil)

s.HTTP.StartTLS()

// NOW we can set up the juju conn suites
Expand Down
2 changes: 2 additions & 0 deletions internal/db/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ var (
JwksPublicKeyTag = jwksPublicKeyTag
JwksPrivateKeyTag = jwksPrivateKeyTag
JwksExpiryTag = jwksExpiryTag
OAuthKind = oauthKind
OAuthKeyTag = oauthKeyTag
)
32 changes: 32 additions & 0 deletions internal/db/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const (
jwksPublicKeyTag = "jwksPublicKey"
jwksPrivateKeyTag = "jwksPrivateKey"
jwksExpiryTag = "jwksExpiry"
oauthKind = "oauth"
oauthKeyTag = "oauthKey"
)

// UpsertSecret stores secret information.
Expand Down Expand Up @@ -280,3 +282,33 @@ func (d *Database) PutJWKSExpiry(ctx context.Context, expiry time.Time) error {
secret := dbmodel.NewSecret(jwksKind, jwksExpiryTag, expiryJson)
return d.UpsertSecret(ctx, &secret)
}

// GetOAuthKey returns the current HS256 (symmetric) key used to sign OAuth session tokens.
func (d *Database) GetOAuthKey(ctx context.Context) ([]byte, error) {
const op = errors.Op("database.GetOAuthKey")
secret := dbmodel.NewSecret(oauthKind, oauthKeyTag, nil)
err := d.GetSecret(ctx, &secret)
if err != nil {
zapctx.Error(ctx, "failed to get oauth key", zap.Error(err))
return nil, errors.E(op, err)
}
var pem []byte
err = json.Unmarshal(secret.Data, &pem)
if err != nil {
zapctx.Error(ctx, "failed to unmarshal pem data", zap.Error(err))
return nil, errors.E(op, err)
}
return pem, nil
}

// PutOAuthKey puts a HS256 key into the credentials store for signing OAuth session tokens.
func (d *Database) PutOAuthKey(ctx context.Context, raw []byte) error {
const op = errors.Op("database.PutOAuthKey")
oauthKey, err := json.Marshal(raw)
babakks marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
zapctx.Error(ctx, "failed to marshal pem data", zap.Error(err))
return errors.E(op, err, "failed to marshal oauth key")
}
secret := dbmodel.NewSecret(oauthKind, oauthKeyTag, oauthKey)
return d.UpsertSecret(ctx, &secret)
}
28 changes: 28 additions & 0 deletions internal/db/secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,31 @@ func (s *dbSuite) TestCleanupJWKS(c *qt.C) {
c.Assert(s.Database.DB.Model(&dbmodel.Secret{}).Count(&count).Error, qt.IsNil)
c.Assert(count, qt.Equals, int64(0))
}

func (s *dbSuite) TestPutAndGetOAuthKey(c *qt.C) {
err := s.Database.Migrate(context.Background(), true)
c.Assert(err, qt.Equals, nil)
ctx := context.Background()
key := []byte(uuid.NewString())
c.Assert(s.Database.PutOAuthKey(ctx, key), qt.IsNil)

secret := dbmodel.Secret{}
tx := s.Database.DB.First(&secret)
c.Assert(tx.Error, qt.IsNil)
c.Assert(secret.Type, qt.Equals, db.OAuthKind)
c.Assert(secret.Tag, qt.Equals, db.OAuthKeyTag)

retrievedKey, err := s.Database.GetOAuthKey(ctx)
c.Assert(err, qt.IsNil)
c.Assert(retrievedKey, qt.DeepEquals, key)
}

func (s *dbSuite) TestGetOAuthKeyFailsIfNotFound(c *qt.C) {
err := s.Database.Migrate(context.Background(), true)
c.Assert(err, qt.Equals, nil)
ctx := context.Background()

retrieved, err := s.Database.GetOAuthKey(ctx)
c.Assert(err, qt.ErrorMatches, "secret not found")
c.Assert(retrieved, qt.IsNil)
}
8 changes: 8 additions & 0 deletions internal/jimm/cloudcredential_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1770,3 +1770,11 @@ func (s testCloudCredentialAttributeStore) PutJWKSExpiry(ctx context.Context, ex
func (s testCloudCredentialAttributeStore) CleanupJWKS(ctx context.Context) error {
return errors.E(errors.CodeNotImplemented)
}

func (s testCloudCredentialAttributeStore) GetOAuthKey(ctx context.Context) ([]byte, error) {
return nil, errors.E(errors.CodeNotImplemented)
}

func (s testCloudCredentialAttributeStore) PutOAuthKey(ctx context.Context, raw []byte) error {
return errors.E(errors.CodeNotImplemented)
}
9 changes: 9 additions & 0 deletions internal/jimm/credentials/credentials.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright 2023 canonical.

// Package credentials provides abstractions/definitions for credential storage
// backends and caching mechanisms.
package credentials

import (
Expand All @@ -16,6 +18,7 @@ import (
// - JWK Set
// - JWK expiry
// - JWK private key
// - OAuth session signing secret
type CredentialStore interface {
// Get retrieves the stored attributes of a cloud credential.
Get(context.Context, names.CloudCredentialTag) (map[string]string, error)
Expand Down Expand Up @@ -51,4 +54,10 @@ type CredentialStore interface {

// PutJWKSExpiry sets the expiry time for the current JWKS within the store.
PutJWKSExpiry(ctx context.Context, expiry time.Time) error

// GetOAuthKey returns the current HS256 (symmetric) key used to sign OAuth session tokens.
GetOAuthKey(ctx context.Context) ([]byte, error)

// PutOAuthKey puts a HS256 (symmetric) key into the credentials store for signing OAuth session tokens.
PutOAuthKey(ctx context.Context, raw []byte) error
}
5 changes: 5 additions & 0 deletions internal/jimm/jimm.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@ type OAuthAuthenticator interface {
VerifyClientCredentials(ctx context.Context, clientID string, clientSecret string) error
}

// GetCredentialStore return the OpenFGA client used by JIMM.
babakks marked this conversation as resolved.
Show resolved Hide resolved
func (j *JIMM) GetCredentialStore() credentials.CredentialStore {
return j.CredentialStore
}

type permission struct {
resource string
relation string
Expand Down
4 changes: 2 additions & 2 deletions internal/jimm/jimm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,7 @@ func TestFillMigrationTarget(t *testing.T) {
err := db.Migrate(ctx, false)
c.Assert(err, qt.IsNil)

store := &jimmtest.InMemoryCredentialStore{}
store := jimmtest.NewInMemoryCredentialStore()
err = store.PutControllerCredentials(context.Background(), test.controllerName, "admin", "test-secret")
c.Assert(err, qt.IsNil)

Expand Down Expand Up @@ -775,7 +775,7 @@ func TestInitiateInternalMigration(t *testing.T) {
c.Patch(jimm.InitiateMigration, func(ctx context.Context, j *jimm.JIMM, user *openfga.User, spec jujuparams.MigrationSpec, targetID uint) (jujuparams.InitiateMigrationResult, error) {
return jujuparams.InitiateMigrationResult{}, nil
})
store := &jimmtest.InMemoryCredentialStore{}
store := jimmtest.NewInMemoryCredentialStore()
err := store.PutControllerCredentials(context.Background(), test.migrateInfo.TargetController, "admin", "test-secret")
c.Assert(err, qt.IsNil)

Expand Down
8 changes: 8 additions & 0 deletions internal/jimmtest/jimm_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/canonical/jimm/internal/dbmodel"
"github.com/canonical/jimm/internal/errors"
"github.com/canonical/jimm/internal/jimm"
jimmcreds "github.com/canonical/jimm/internal/jimm/credentials"
"github.com/canonical/jimm/internal/openfga"
ofganames "github.com/canonical/jimm/internal/openfga/names"
"github.com/canonical/jimm/internal/pubsub"
Expand Down Expand Up @@ -59,6 +60,7 @@ type JIMM struct {
GetCloudCredential_ func(ctx context.Context, user *openfga.User, tag names.CloudCredentialTag) (*dbmodel.CloudCredential, error)
GetCloudCredentialAttributes_ func(ctx context.Context, u *openfga.User, cred *dbmodel.CloudCredential, hidden bool) (attrs map[string]string, redacted []string, err error)
GetControllerConfig_ func(ctx context.Context, u *dbmodel.Identity) (*dbmodel.ControllerConfig, error)
GetCredentialStore_ func() jimmcreds.CredentialStore
GetJimmControllerAccess_ func(ctx context.Context, user *openfga.User, tag names.UserTag) (string, error)
GetUser_ func(ctx context.Context, username string) (*openfga.User, error)
GetOpenFGAUserAndAuthorise_ func(ctx context.Context, email string) (*openfga.User, error)
Expand Down Expand Up @@ -300,6 +302,12 @@ func (j *JIMM) GetControllerConfig(ctx context.Context, u *dbmodel.Identity) (*d
}
return j.GetControllerConfig_(ctx, u)
}
func (j *JIMM) GetCredentialStore() jimmcreds.CredentialStore {
if j.GetCredentialStore_ == nil {
return nil
}
return j.GetCredentialStore_()
}
func (j *JIMM) GetJimmControllerAccess(ctx context.Context, user *openfga.User, tag names.UserTag) (string, error) {
if j.GetJimmControllerAccess_ == nil {
return "", errors.E(errors.CodeNotImplemented)
Expand Down
35 changes: 35 additions & 0 deletions internal/jimmtest/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,19 @@ type InMemoryCredentialStore struct {
jwks jwk.Set
privateKey []byte
expiry time.Time
oauthKey []byte
controllerCredentials map[string]controllerCredentials
cloudCredentialAttributes map[string]map[string]string
}

// NewInMemoryCredentialStore returns a new instance of `InMemoryCredentialStore`
// with some secrets/keys being populated.
func NewInMemoryCredentialStore() *InMemoryCredentialStore {
return &InMemoryCredentialStore{
oauthKey: []byte("secret-oauth-key"),
babakks marked this conversation as resolved.
Show resolved Hide resolved
}
}

// Get retrieves the stored attributes of a cloud credential.
func (s *InMemoryCredentialStore) Get(ctx context.Context, credTag names.CloudCredentialTag) (map[string]string, error) {
s.mu.Lock()
Expand Down Expand Up @@ -177,3 +186,29 @@ func (s *InMemoryCredentialStore) PutJWKSExpiry(ctx context.Context, expiry time

return nil
}

// GetOAuthKey returns the current HS256 (symmetric) key used to sign OAuth session tokens.
func (s *InMemoryCredentialStore) GetOAuthKey(ctx context.Context) ([]byte, error) {
s.mu.RLock()
defer s.mu.RUnlock()

if s.oauthKey == nil || len(s.oauthKey) == 0 {
return nil, errors.E(errors.CodeNotFound)
}

key := make([]byte, len(s.oauthKey))
copy(key, s.oauthKey)

return key, nil
}

// PutOAuthKey puts a HS256 key into the credentials store for signing OAuth session tokens.
func (s *InMemoryCredentialStore) PutOAuthKey(ctx context.Context, raw []byte) error {
s.mu.Lock()
defer s.mu.Unlock()

s.oauthKey = make([]byte, len(raw))
copy(s.oauthKey, raw)

return nil
}
2 changes: 1 addition & 1 deletion internal/jimmtest/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func (s *JIMMSuite) SetUpTest(c *gc.C) {
Database: db.Database{
DB: PostgresDB(GocheckTester{c}, nil),
},
CredentialStore: &InMemoryCredentialStore{},
CredentialStore: NewInMemoryCredentialStore(),
Pubsub: &pubsub.Hub{MaxConcurrency: 10},
UUID: ControllerUUID,
OpenFGAClient: s.OFGAClient,
Expand Down
18 changes: 12 additions & 6 deletions internal/jujuapi/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,12 @@ func (r *controllerRoot) GetDeviceSessionToken(ctx context.Context) (params.GetD
return response, errors.E(op, err)
}

// TODO(ale8k): Add vault logic to get secret key and generate one
// on start up.
encToken, err := authSvc.MintSessionToken(email, "test-secret")
secretKey, err := r.jimm.GetCredentialStore().GetOAuthKey(ctx)
if err != nil {
return response, errors.E(op, err, "failed to retrieve oauth secret key")
}

encToken, err := authSvc.MintSessionToken(email, string(secretKey))
if err != nil {
return response, errors.E(op, err)
}
Expand All @@ -110,9 +113,12 @@ func (r *controllerRoot) LoginWithSessionToken(ctx context.Context, req params.L
authenticationSvc := r.jimm.OAuthAuthenticationService()

// Verify the session token
// TODO(CSS-7081): Ensure for tests that the secret key can be configured.
// Or configure cmd tests to use the configured secret.
jwtToken, err := authenticationSvc.VerifySessionToken(req.SessionToken, "test-secret")
secretKey, err := r.jimm.GetCredentialStore().GetOAuthKey(ctx)
babakks marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return jujuparams.LoginResult{}, errors.E(op, err, "failed to retrieve oauth secret key")
}

jwtToken, err := authenticationSvc.VerifySessionToken(req.SessionToken, string(secretKey))
if err != nil {
var aerr *auth.AuthenticationError
if stderrors.As(err, &aerr) {
Expand Down
2 changes: 2 additions & 0 deletions internal/jujuapi/controllerroot.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/canonical/jimm/internal/dbmodel"
"github.com/canonical/jimm/internal/errors"
"github.com/canonical/jimm/internal/jimm"
"github.com/canonical/jimm/internal/jimm/credentials"
"github.com/canonical/jimm/internal/jujuapi/rpc"
"github.com/canonical/jimm/internal/openfga"
ofganames "github.com/canonical/jimm/internal/openfga/names"
Expand Down Expand Up @@ -57,6 +58,7 @@ type JIMM interface {
GetCloudCredential(ctx context.Context, user *openfga.User, tag names.CloudCredentialTag) (*dbmodel.CloudCredential, error)
GetCloudCredentialAttributes(ctx context.Context, u *openfga.User, cred *dbmodel.CloudCredential, hidden bool) (attrs map[string]string, redacted []string, err error)
GetControllerConfig(ctx context.Context, u *dbmodel.Identity) (*dbmodel.ControllerConfig, error)
GetCredentialStore() credentials.CredentialStore
GetJimmControllerAccess(ctx context.Context, user *openfga.User, tag names.UserTag) (string, error)
GetUser(ctx context.Context, username string) (*openfga.User, error)
GetUserCloudAccess(ctx context.Context, user *openfga.User, cloud names.CloudTag) (string, error)
Expand Down
54 changes: 54 additions & 0 deletions internal/vault/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,60 @@ func (s *VaultStore) getJWKSExpiryPath() string {
return path.Join(s.getWellKnownPath(), "jwks-expiry")
}

// GetOAuthKey returns the current HS256 (symmetric) key used to sign OAuth session tokens.
func (s *VaultStore) GetOAuthKey(ctx context.Context) ([]byte, error) {
const op = errors.Op("vault.GetOAuthKey")

client, err := s.client(ctx)
if err != nil {
return nil, errors.E(op, err)
}

secret, err := client.Logical().Read(s.GetOAuthKeyPath())
if err != nil {
return nil, errors.E(op, err)
}

if secret == nil {
msg := "no OAuth key exists"
zapctx.Debug(ctx, msg)
return nil, errors.E(op, errors.CodeNotFound, msg)
}

keyPemB64 := secret.Data["key"].(string)

keyPem, err := base64.StdEncoding.DecodeString(keyPemB64)
babakks marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, errors.E(op, err)
}

return keyPem, nil
}

// PutOAuthKey puts a HS256 key into the credentials store for signing OAuth session tokens.
func (s *VaultStore) PutOAuthKey(ctx context.Context, raw []byte) error {
const op = errors.Op("vault.PutOAuthKey")

client, err := s.client(ctx)
if err != nil {
return errors.E(op, err)
}

if _, err := client.Logical().Write(
s.GetOAuthKeyPath(),
map[string]interface{}{"key": raw},
); err != nil {
return errors.E(op, err)
}
return nil
}

// GetOAuthKeyPath returns a hardcoded suffixed vault path (dependent on
// the initial KVPath) to the OAuth JWK location.
func (s *VaultStore) GetOAuthKeyPath() string {
return path.Join(s.KVPath, "creds", "oauth", "key")
}

// deleteControllerCredentials removes the credentials associated with the controller in
// the vault service.
func (s *VaultStore) deleteControllerCredentials(ctx context.Context, controllerName string) error {
Expand Down
Loading
Loading