1
0
mirror of https://github.com/pocket-id/pocket-id.git synced 2026-03-22 16:45:08 +00:00

fix: disallow API key renewal and creation with API key authentication (#1334)

This commit is contained in:
Elias Schneider
2026-02-23 20:34:25 +01:00
committed by GitHub
parent b3fe143136
commit 0c41872cd4
5 changed files with 146 additions and 5 deletions

View File

@@ -280,6 +280,13 @@ func (e *APIKeyExpirationDateError) Error() string {
} }
func (e *APIKeyExpirationDateError) HttpStatusCode() int { return http.StatusBadRequest } func (e *APIKeyExpirationDateError) HttpStatusCode() int { return http.StatusBadRequest }
type APIKeyAuthNotAllowedError struct{}
func (e *APIKeyAuthNotAllowedError) Error() string {
return "API key authentication is not allowed for this endpoint"
}
func (e *APIKeyAuthNotAllowedError) HttpStatusCode() int { return http.StatusForbidden }
type OidcInvalidRefreshTokenError struct{} type OidcInvalidRefreshTokenError struct{}
func (e *OidcInvalidRefreshTokenError) Error() string { func (e *OidcInvalidRefreshTokenError) Error() string {

View File

@@ -26,12 +26,11 @@ func NewApiKeyController(group *gin.RouterGroup, authMiddleware *middleware.Auth
uc := &ApiKeyController{apiKeyService: apiKeyService} uc := &ApiKeyController{apiKeyService: apiKeyService}
apiKeyGroup := group.Group("/api-keys") apiKeyGroup := group.Group("/api-keys")
apiKeyGroup.Use(authMiddleware.WithAdminNotRequired().Add())
{ {
apiKeyGroup.GET("", uc.listApiKeysHandler) apiKeyGroup.GET("", authMiddleware.WithAdminNotRequired().Add(), uc.listApiKeysHandler)
apiKeyGroup.POST("", uc.createApiKeyHandler) apiKeyGroup.POST("", authMiddleware.WithAdminNotRequired().WithApiKeyAuthDisabled().Add(), uc.createApiKeyHandler)
apiKeyGroup.POST("/:id/renew", uc.renewApiKeyHandler) apiKeyGroup.POST("/:id/renew", authMiddleware.WithAdminNotRequired().WithApiKeyAuthDisabled().Add(), uc.renewApiKeyHandler)
apiKeyGroup.DELETE("/:id", uc.revokeApiKeyHandler) apiKeyGroup.DELETE("/:id", authMiddleware.WithAdminNotRequired().Add(), uc.revokeApiKeyHandler)
} }
} }

View File

@@ -18,6 +18,7 @@ type AuthMiddleware struct {
type AuthOptions struct { type AuthOptions struct {
AdminRequired bool AdminRequired bool
SuccessOptional bool SuccessOptional bool
AllowApiKeyAuth bool
} }
func NewAuthMiddleware( func NewAuthMiddleware(
@@ -31,6 +32,7 @@ func NewAuthMiddleware(
options: AuthOptions{ options: AuthOptions{
AdminRequired: true, AdminRequired: true,
SuccessOptional: false, SuccessOptional: false,
AllowApiKeyAuth: true,
}, },
} }
} }
@@ -59,6 +61,17 @@ func (m *AuthMiddleware) WithSuccessOptional() *AuthMiddleware {
return clone return clone
} }
// WithApiKeyAuthDisabled disables API key authentication fallback and requires JWT auth.
func (m *AuthMiddleware) WithApiKeyAuthDisabled() *AuthMiddleware {
clone := &AuthMiddleware{
apiKeyMiddleware: m.apiKeyMiddleware,
jwtMiddleware: m.jwtMiddleware,
options: m.options,
}
clone.options.AllowApiKeyAuth = false
return clone
}
func (m *AuthMiddleware) Add() gin.HandlerFunc { func (m *AuthMiddleware) Add() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
userID, isAdmin, err := m.jwtMiddleware.Verify(c, m.options.AdminRequired) userID, isAdmin, err := m.jwtMiddleware.Verify(c, m.options.AdminRequired)
@@ -79,6 +92,21 @@ func (m *AuthMiddleware) Add() gin.HandlerFunc {
return return
} }
if !m.options.AllowApiKeyAuth {
if m.options.SuccessOptional {
c.Next()
return
}
c.Abort()
if c.GetHeader("X-API-Key") != "" {
_ = c.Error(&common.APIKeyAuthNotAllowedError{})
return
}
_ = c.Error(err)
return
}
// JWT auth failed, try API key auth // JWT auth failed, try API key auth
userID, isAdmin, err = m.apiKeyMiddleware.Verify(c, m.options.AdminRequired) userID, isAdmin, err = m.apiKeyMiddleware.Verify(c, m.options.AdminRequired)
if err == nil { if err == nil {

View File

@@ -0,0 +1,104 @@
package middleware
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/dto"
"github.com/pocket-id/pocket-id/backend/internal/model"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
"github.com/pocket-id/pocket-id/backend/internal/service"
testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing"
)
func TestWithApiKeyAuthDisabled(t *testing.T) {
gin.SetMode(gin.TestMode)
originalEnvConfig := common.EnvConfig
defer func() {
common.EnvConfig = originalEnvConfig
}()
common.EnvConfig.AppURL = "https://test.example.com"
common.EnvConfig.EncryptionKey = []byte("0123456789abcdef0123456789abcdef")
db := testutils.NewDatabaseForTest(t)
appConfigService, err := service.NewAppConfigService(t.Context(), db)
require.NoError(t, err)
jwtService, err := service.NewJwtService(t.Context(), db, appConfigService)
require.NoError(t, err)
userService := service.NewUserService(db, jwtService, nil, nil, appConfigService, nil, nil, nil, nil)
apiKeyService, err := service.NewApiKeyService(t.Context(), db, nil)
require.NoError(t, err)
authMiddleware := NewAuthMiddleware(apiKeyService, userService, jwtService)
user := createUserForAuthMiddlewareTest(t, db)
jwtToken, err := jwtService.GenerateAccessToken(user)
require.NoError(t, err)
_, apiKeyToken, err := apiKeyService.CreateApiKey(t.Context(), user.ID, dto.ApiKeyCreateDto{
Name: "Middleware API Key",
ExpiresAt: datatype.DateTime(time.Now().Add(24 * time.Hour)),
})
require.NoError(t, err)
router := gin.New()
router.Use(NewErrorHandlerMiddleware().Add())
router.GET("/api/protected", authMiddleware.WithAdminNotRequired().WithApiKeyAuthDisabled().Add(), func(c *gin.Context) {
c.Status(http.StatusNoContent)
})
t.Run("rejects API key auth when API key auth is disabled", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/protected", nil)
req.Header.Set("X-API-Key", apiKeyToken)
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, req)
require.Equal(t, http.StatusForbidden, recorder.Code)
var body map[string]string
err := json.Unmarshal(recorder.Body.Bytes(), &body)
require.NoError(t, err)
require.Equal(t, "API key authentication is not allowed for this endpoint", body["error"])
})
t.Run("allows JWT auth when API key auth is disabled", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/protected", nil)
req.Header.Set("Authorization", "Bearer "+jwtToken)
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, req)
require.Equal(t, http.StatusNoContent, recorder.Code)
})
}
func createUserForAuthMiddlewareTest(t *testing.T, db *gorm.DB) model.User {
t.Helper()
email := "auth@example.com"
user := model.User{
Username: "auth-user",
Email: &email,
FirstName: "Auth",
LastName: "User",
DisplayName: "Auth User",
}
err := db.Create(&user).Error
require.NoError(t, err)
return user
}

View File

@@ -77,6 +77,9 @@ func (s *ApiKeyService) CreateApiKey(ctx context.Context, userID string, input d
Create(&apiKey). Create(&apiKey).
Error Error
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return model.ApiKey{}, "", &common.AlreadyInUseError{Property: "API key name"}
}
return model.ApiKey{}, "", err return model.ApiKey{}, "", err
} }