apigen: api key authentication implemented

Implemented account management api key authentication.
Extended IsAuthenticated service method to include both cookie and api key authorization.

Change-Id: I6f2d01fdc6115cb860f2e49c74980a39155afe7e
This commit is contained in:
Vitalii 2022-03-27 13:16:46 +03:00 committed by Vitalii Shpital
parent 8cdadae124
commit 67b5b07730
8 changed files with 120 additions and 49 deletions

View File

@ -11,5 +11,5 @@ import (
// Auth exposes methods to control authentication process for each endpoint.
type Auth interface {
// IsAuthenticated checks if request is performed with all needed authorization credentials.
IsAuthenticated(ctx context.Context, r *http.Request) (context.Context, error)
IsAuthenticated(ctx context.Context, r *http.Request, isCookieAuth, isKeyAuth bool) (context.Context, error)
}

View File

@ -168,8 +168,16 @@ func (a *API) generateGo() ([]byte, error) {
p("w.Header().Set(\"Content-Type\", \"application/json\")")
p("")
if !endpoint.NoCookieAuth {
p("ctx, err = h.auth.IsAuthenticated(ctx, r)")
if !endpoint.NoCookieAuth || !endpoint.NoAPIAuth {
if !endpoint.NoCookieAuth && !endpoint.NoAPIAuth {
p("ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)")
}
if endpoint.NoCookieAuth && !endpoint.NoAPIAuth {
p("ctx, err = h.auth.IsAuthenticated(ctx, r, false, true)")
}
if !endpoint.NoCookieAuth && endpoint.NoAPIAuth {
p("ctx, err = h.auth.IsAuthenticated(ctx, r, true, false)")
}
p("if err != nil {")
p("api.ServeError(h.log, w, http.StatusUnauthorized, err)")
p("return")
@ -177,9 +185,6 @@ func (a *API) generateGo() ([]byte, error) {
p("")
}
// TODO to be implemented
// if !endpoint.NoAPIAuth {}
for _, param := range endpoint.Params {
switch param.Type {
case reflect.TypeOf(uuid.UUID{}):

View File

@ -66,9 +66,11 @@ func TestAccountManagementAPIKeys(t *testing.T) {
err = json.Unmarshal(responseBody, &output)
require.NoError(t, err)
userID, err := keyService.GetUserFromKey(ctx, output.APIKey)
userID, exp, err := keyService.GetUserAndExpirationFromKey(ctx, output.APIKey)
require.NoError(t, err)
require.Equal(t, user.ID, userID)
require.False(t, exp.IsZero())
require.False(t, exp.Before(now))
// check the expiration is around the time we expect
defaultExpiration := satellite.Config.AccountManagementAPIKeys.DefaultExpiration
@ -103,9 +105,11 @@ func TestAccountManagementAPIKeys(t *testing.T) {
err = json.Unmarshal(responseBody, &output)
require.NoError(t, err)
userID, err := keyService.GetUserFromKey(ctx, output.APIKey)
userID, exp, err := keyService.GetUserAndExpirationFromKey(ctx, output.APIKey)
require.NoError(t, err)
require.Equal(t, user.ID, userID)
require.False(t, exp.IsZero())
require.False(t, exp.Before(now))
// check the expiration is around the time we expect
durationTime, err := time.ParseDuration(durationString)
@ -136,7 +140,7 @@ func TestAccountManagementAPIKeys(t *testing.T) {
require.Equal(t, http.StatusOK, response.StatusCode)
require.NoError(t, response.Body.Close())
_, err = keyService.GetUserFromKey(ctx, apiKey)
_, _, err = keyService.GetUserAndExpirationFromKey(ctx, apiKey)
require.Error(t, err)
})
})

View File

@ -136,22 +136,22 @@ func (s *Service) InsertIntoDB(ctx context.Context, oAuthToken oidc.OAuthToken,
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) {
// GetUserAndExpirationFromKey gets the userID and expiration date attached to an account management api key.
func (s *Service) GetUserAndExpirationFromKey(ctx context.Context, apiKey string) (userID uuid.UUID, exp time.Time, err error) {
defer mon.Task()(&ctx)(&err)
hash, err := s.HashKey(ctx, apiKey)
if err != nil {
return uuid.UUID{}, err
return uuid.UUID{}, time.Now(), 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{}, time.Now(), Error.Wrap(ErrInvalidKey.New("invalid account management api key"))
}
return uuid.UUID{}, err
return uuid.UUID{}, time.Now(), err
}
return keyInfo.UserID, err
return keyInfo.UserID, keyInfo.ExpiresAt, err
}
// Revoke revokes an account management api key.

View File

@ -25,17 +25,19 @@ func TestAccountManagementAPIKeys(t *testing.T) {
service := sat.API.AccountManagementAPIKeys.Service
id := testrand.UUID()
now := time.Now()
expires := time.Hour
apiKey, _, err := service.Create(ctx, id, expires)
require.NoError(t, err)
// test GetUserFromKey
userID, err := service.GetUserFromKey(ctx, apiKey)
userID, exp, err := service.GetUserAndExpirationFromKey(ctx, apiKey)
require.NoError(t, err)
require.Equal(t, id, userID)
require.False(t, exp.IsZero())
require.False(t, exp.Before(now))
// make sure an error is returned from duplicate apikey
now := time.Now()
hash, err := service.HashKey(ctx, apiKey)
require.NoError(t, err)
_, err = service.InsertIntoDB(ctx, oidc.OAuthToken{
@ -57,7 +59,7 @@ func TestAccountManagementAPIKeys(t *testing.T) {
require.Error(t, err)
// test GetUserFromKey non existent key
_, err = service.GetUserFromKey(ctx, nonexistent)
_, _, err = service.GetUserAndExpirationFromKey(ctx, nonexistent)
require.True(t, accountmanagementapikeys.ErrInvalidKey.Has(err))
})
}

View File

@ -33,7 +33,7 @@ type APIKeys interface {
// AccountManagementAPIKeys is an interface for account management api key operations.
type AccountManagementAPIKeys interface {
Create(ctx context.Context, userID uuid.UUID, expiration time.Duration) (apiKey string, expiresAt time.Time, err error)
GetUserFromKey(ctx context.Context, apiKey string) (userID uuid.UUID, err error)
GetUserAndExpirationFromKey(ctx context.Context, apiKey string) (userID uuid.UUID, exp time.Time, err error)
Revoke(ctx context.Context, apiKey string) (err error)
}

View File

@ -50,6 +50,31 @@ func NewProjectManagement(log *zap.Logger, service ProjectManagementService, rou
return handler
}
func (h *Handler) handleGenGetUsersProjects(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
if err != nil {
api.ServeError(h.log, w, http.StatusUnauthorized, err)
return
}
retVal, httpErr := h.service.GenGetUsersProjects(ctx)
if httpErr.Err != nil {
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
return
}
err = json.NewEncoder(w).Encode(retVal)
if err != nil {
h.log.Debug("failed to write json GenGetUsersProjects response", zap.Error(ErrProjectsAPI.Wrap(err)))
}
}
func (h *Handler) handleGenGetSingleBucketUsageRollup(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
@ -57,7 +82,7 @@ func (h *Handler) handleGenGetSingleBucketUsageRollup(w http.ResponseWriter, r *
w.Header().Set("Content-Type", "application/json")
ctx, err = h.auth.IsAuthenticated(ctx, r)
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
if err != nil {
api.ServeError(h.log, w, http.StatusUnauthorized, err)
return
@ -110,7 +135,7 @@ func (h *Handler) handleGenGetBucketUsageRollups(w http.ResponseWriter, r *http.
w.Header().Set("Content-Type", "application/json")
ctx, err = h.auth.IsAuthenticated(ctx, r)
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
if err != nil {
api.ServeError(h.log, w, http.StatusUnauthorized, err)
return
@ -149,28 +174,3 @@ func (h *Handler) handleGenGetBucketUsageRollups(w http.ResponseWriter, r *http.
h.log.Debug("failed to write json GenGetBucketUsageRollups response", zap.Error(ErrProjectsAPI.Wrap(err)))
}
}
func (h *Handler) handleGenGetUsersProjects(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
ctx, err = h.auth.IsAuthenticated(ctx, r)
if err != nil {
api.ServeError(h.log, w, http.StatusUnauthorized, err)
return
}
retVal, httpErr := h.service.GenGetUsersProjects(ctx)
if httpErr.Err != nil {
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
return
}
err = json.NewEncoder(w).Encode(retVal)
if err != nil {
h.log.Debug("failed to write json GenGetUsersProjects response", zap.Error(ErrProjectsAPI.Wrap(err)))
}
}

View File

@ -1988,18 +1988,78 @@ func (s *Service) Authorize(ctx context.Context) (a Authorization, err error) {
}, nil
}
// IsAuthenticated checks if request has an authorization cookie.
func (s *Service) IsAuthenticated(ctx context.Context, r *http.Request) (context.Context, error) {
// IsAuthenticated checks if request has authorization credentials.
func (s *Service) IsAuthenticated(ctx context.Context, r *http.Request, isCookieAuth, isKeyAuth bool) (context.Context, error) {
var err error
if isCookieAuth && isKeyAuth {
ctx, err = s.cookieAuth(ctx, r)
if err != nil {
ctx, err = s.keyAuth(ctx, r)
if err != nil {
return nil, err
}
}
} else if isCookieAuth {
ctx, err = s.cookieAuth(ctx, r)
if err != nil {
return nil, err
}
} else if isKeyAuth {
ctx, err = s.keyAuth(ctx, r)
if err != nil {
return nil, err
}
}
return ctx, nil
}
// cookieAuth checks if request has an authorization cookie.
func (s *Service) cookieAuth(ctx context.Context, r *http.Request) (context.Context, error) {
cookie, err := r.Cookie("_tokenKey")
if err != nil {
return nil, err
return ctx, err
}
auth, err := s.Authorize(consoleauth.WithAPIKey(ctx, []byte(cookie.Value)))
if err != nil {
return ctx, err
}
return WithAuth(ctx, auth), nil
}
// keyAuth checks if request has an authorization api key.
func (s *Service) keyAuth(ctx context.Context, r *http.Request) (context.Context, error) {
apikey := r.Header.Get("Authorization")
if apikey == "" {
return nil, errs.New("no authorization key was provided")
}
ctx = consoleauth.WithAPIKey(ctx, []byte(apikey))
userID, exp, err := s.accountManagementAPIKeys.GetUserAndExpirationFromKey(ctx, apikey)
if err != nil {
return nil, err
}
claims := &consoleauth.Claims{
ID: userID,
Email: "",
Expiration: exp,
}
user, err := s.authorize(ctx, claims)
if err != nil {
return nil, err
}
auth := Authorization{
User: *user,
Claims: *claims,
}
return WithAuth(ctx, auth), nil
}