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:
parent
8cdadae124
commit
67b5b07730
@ -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)
|
||||
}
|
||||
|
@ -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{}):
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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.
|
||||
|
@ -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))
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user