storj/satellite/console/accountmanagementapikeys/service.go
Cameron 84b522bc06 satellite/console: create account management api keys service
We are in the process of creating an api to allow users to manage their
accounts programmatically. We would like to use api keys for
authorization. We were originally going to create an entirely new table
for these api keys, but seeing as we already have 2 other tables for
keys/tokens, api_keys and oauth_tokens, we thought it might be better to
use one of these. We're using oauth_tokens.

We create a new oidc.OAuthTokenKind for account management api keys:
KindAccountManagementTokenV0. We made the key versioned because we
likely want to improve the implementation in the future, but we want to
get something functional out the door ASAP because the account management
api feature is highly desired.

Add a new method to oidc.OAuthTokens interface for revoking v0 account
management api keys, RevokeAccountManagementTokenV0. Add update method
to dbx implementation to allow updating the expiration. We will revoke
these keys by setting the expiration to 0 so they are expired.

Change-Id: Ideb8ae04b23aa55d5825b064b5e43e32eadc1fba
2022-03-23 17:02:20 +00:00

176 lines
5.1 KiB
Go

// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
package accountmanagementapikeys
import (
"context"
"crypto/sha256"
"database/sql"
"errors"
"time"
"github.com/spacemonkeygo/monkit/v3"
"github.com/zeebo/errs"
"storj.io/common/uuid"
"storj.io/storj/satellite/oidc"
)
var mon = monkit.Package()
var (
// Error describes internal account management api keys error.
Error = errs.Class("account management api keys service")
// ErrDuplicateKey is error type that occurs when a generated account
// management api key already exists.
ErrDuplicateKey = errs.Class("duplicate key")
// ErrInvalidKey is an error type that occurs when a user submits a key
// that does not match anything in the database.
ErrInvalidKey = errs.Class("invalid key")
)
// Config contains configuration parameters for account management api keys.
type Config struct {
DefaultExpiration time.Duration `help:"expiration to use if user does not specify an account management api key expiration" default:"720h"`
}
// Service handles operations regarding account management api keys.
type Service struct {
db oidc.OAuthTokens
config Config
}
// NewService creates a new account management api keys service.
func NewService(db oidc.OAuthTokens, config Config) *Service {
return &Service{
db: db,
config: config,
}
}
// Create creates and inserts an account management api key into the db.
func (s *Service) Create(ctx context.Context, userID uuid.UUID, expiration time.Duration) (apiKey string, expiresAt time.Time, err error) {
defer mon.Task()(&ctx)(&err)
apiKey, hash, err := s.GenerateNewKey(ctx)
if err != nil {
return "", time.Time{}, Error.Wrap(err)
}
expiresAt, err = s.InsertIntoDB(ctx, oidc.OAuthToken{
UserID: userID,
Kind: oidc.KindAccountManagementTokenV0,
Token: hash,
}, time.Now(), expiration)
if err != nil {
return "", time.Time{}, Error.Wrap(err)
}
return apiKey, expiresAt, nil
}
// GenerateNewKey generates a new account management api key.
func (s *Service) GenerateNewKey(ctx context.Context) (apiKey, hash string, err error) {
defer mon.Task()(&ctx)(&err)
id, err := uuid.New()
if err != nil {
return "", "", Error.Wrap(err)
}
apiKey = id.String()
hash = hashKeyFromUUID(ctx, id)
return apiKey, hash, nil
}
// This is used for hashing during key creation so we don't need to convert from a string back to a uuid.
func hashKeyFromUUID(ctx context.Context, apiKeyUUID uuid.UUID) string {
mon.Task()(&ctx)(nil)
hashBytes := sha256.Sum256(apiKeyUUID.Bytes())
return string(hashBytes[:])
}
// HashKey returns a hash of api key. This is used for hashing inside GetUserFromKey.
func (s *Service) HashKey(ctx context.Context, apiKey string) (hash string, err error) {
defer mon.Task()(&ctx)(&err)
id, err := uuid.FromString(apiKey)
if err != nil {
return "", Error.Wrap(err)
}
hashBytes := sha256.Sum256(id.Bytes())
return string(hashBytes[:]), nil
}
// InsertIntoDB checks OAuthTokens DB for a token before inserting. This is because OAuthTokens DB allows
// duplicate tokens, but we can't have duplicate api keys.
func (s *Service) InsertIntoDB(ctx context.Context, oAuthToken oidc.OAuthToken, now time.Time, expiration time.Duration) (expiresAt time.Time, err error) {
defer mon.Task()(&ctx)(&err)
// The token column is the key to the OAuthTokens table, but the Create method does not return an error if a duplicate token insert is attempted.
// We need to make sure a unique api key is created, so check that the value doesn't already exist.
_, err = s.db.Get(ctx, oidc.KindAccountManagementTokenV0, oAuthToken.Token)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
return time.Time{}, Error.Wrap(err)
}
} else if err == nil {
return time.Time{}, Error.Wrap(ErrDuplicateKey.New("failed to generate a unique account management api key"))
}
if expiration <= 0 {
expiration = s.config.DefaultExpiration
}
expiresAt = now.Add(expiration)
oAuthToken.CreatedAt = now
oAuthToken.ExpiresAt = expiresAt
err = s.db.Create(ctx, oAuthToken)
if err != nil {
return time.Time{}, Error.Wrap(err)
}
return expiresAt, nil
}
// GetUserFromKey gets the userID attached to an account management api key.
func (s *Service) GetUserFromKey(ctx context.Context, apiKey string) (userID uuid.UUID, err error) {
defer mon.Task()(&ctx)(&err)
hash, err := s.HashKey(ctx, apiKey)
if err != nil {
return uuid.UUID{}, err
}
keyInfo, err := s.db.Get(ctx, oidc.KindAccountManagementTokenV0, hash)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return uuid.UUID{}, Error.Wrap(ErrInvalidKey.New("invalid account management api key"))
}
return uuid.UUID{}, err
}
return keyInfo.UserID, err
}
// Revoke revokes an account management api key.
func (s *Service) Revoke(ctx context.Context, apiKey string) (err error) {
defer mon.Task()(&ctx)(&err)
hash, err := s.HashKey(ctx, apiKey)
if err != nil {
return Error.Wrap(err)
}
_, err = s.db.Get(ctx, oidc.KindAccountManagementTokenV0, hash)
if err != nil {
return Error.Wrap(err)
}
err = s.db.RevokeAccountManagementTokenV0(ctx, hash)
if err != nil {
return Error.Wrap(err)
}
return nil
}