detached d22746b

refactor(auth): remove password authentication from domain and service layer

Remove AuthTypePassword, ValidatePassword, GeneratePasswordHash, and all
password-related code paths from the domain, service interface, service
implementation, HTTP handler, middleware, CLI, and app wiring. The
/auth/password endpoint now returns 410 Gone. Regenerated mocks and
updated all tests.
commit
d22746b20ddc9c3471b2cfb4ae294785448c604f
author
brice · · 2026-03-22 06:20 UTC
committer
brice · · 2026-03-22 17:35 UTC
parent
28746f8
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
13 files · +48 -727
M go.mod +0 -1
@@ -22,7 +22,6 @@ require (
2222 go.opentelemetry.io/otel/sdk v1.42.0
2323 go.opentelemetry.io/otel/sdk/log v0.18.0
2424 go.opentelemetry.io/otel/sdk/metric v1.42.0
25- golang.org/x/crypto v0.49.0
2625 golang.org/x/mod v0.34.0
2726 golang.org/x/term v0.41.0
2827 gopkg.in/natefinch/lumberjack.v2 v2.2.1
@@ -22,7 +22,6 @@ require (
22 go.opentelemetry.io/otel/sdk v1.42.022 go.opentelemetry.io/otel/sdk v1.42.0
23 go.opentelemetry.io/otel/sdk/log v0.18.023 go.opentelemetry.io/otel/sdk/log v0.18.0
24 go.opentelemetry.io/otel/sdk/metric v1.42.024 go.opentelemetry.io/otel/sdk/metric v1.42.0
25- golang.org/x/crypto v0.49.0
26 golang.org/x/mod v0.34.025 golang.org/x/mod v0.34.0
27 golang.org/x/term v0.41.026 golang.org/x/term v0.41.0
28 gopkg.in/natefinch/lumberjack.v2 v2.2.127 gopkg.in/natefinch/lumberjack.v2 v2.2.1
M go.sum +0 -2 generated file: collapsed
M internal/adapters/in/cli/auth.go +0 -76
@@ -44,7 +44,6 @@ func newAuthCmd() *cobra.Command {
4444 }
4545
4646 cmd.AddCommand(newAuthTokenCmd())
47- cmd.AddCommand(newAuthPasswordCmd())
4847 cmd.AddCommand(newAuthInternalCmd())
4948 cmd.AddCommand(newAuthLoginCmd())
5049 cmd.AddCommand(newAuthStatusCmd())
@@ -191,19 +190,6 @@ Examples:
191190 return cmd
192191 }
193192
194-// newAuthPasswordCmd creates the password subcommand group.
195-func newAuthPasswordCmd() *cobra.Command {
196- cmd := &cobra.Command{
197- Use: "password",
198- Short: "Password utilities",
199- Long: `Utilities for managing password authentication.`,
200- }
201-
202- cmd.AddCommand(newPasswordHashCmd())
203-
204- return cmd
205-}
206-
207193 // newAuthInternalCmd creates the internal credentials command.
208194 func newAuthInternalCmd() *cobra.Command {
209195 return &cobra.Command{
@@ -420,24 +406,6 @@ func runShowInternalAuth() error {
420406 return nil
421407 }
422408
423-// newPasswordHashCmd creates the password hash command.
424-func newPasswordHashCmd() *cobra.Command {
425- return &cobra.Command{
426- Use: "hash",
427- Short: "Generate a bcrypt hash for a password",
428- Long: `Interactively prompt for a password and output its bcrypt hash.
429-
430-The hash can be stored in your secrets backend and referenced in the config:
431-
432- [auth]
433- type = "password"
434- password_hash = "github.com/bnema/gordon/auth/password_hash"`,
435- RunE: func(cmd *cobra.Command, args []string) error {
436- return runPasswordHash()
437- },
438- }
439-}
440-
441409 // runTokenGenerate generates a new authentication token.
442410 // parseAndConvertScopes parses a comma-separated scope string and converts
443411 // simple scopes (push, pull, *) to Docker v2 format (repository:*:action).
@@ -646,50 +614,6 @@ func runTokenRevokeAll(configPath string) error {
646614 return nil
647615 }
648616
649-// runPasswordHash generates a bcrypt hash for a password.
650-func runPasswordHash() error {
651- fmt.Print("Enter password: ")
652-
653- // Read password without echo (use os.Stdin.Fd() for better compatibility)
654- fd, err := stdinFD()
655- if err != nil {
656- return fmt.Errorf("failed to get stdin fd: %w", err)
657- }
658- passwordBytes, err := term.ReadPassword(fd)
659- if err != nil {
660- // Fallback for non-terminal input
661- reader := bufio.NewReader(os.Stdin)
662- password, err := reader.ReadString('\n')
663- if err != nil {
664- return fmt.Errorf("failed to read password: %w", err)
665- }
666- passwordBytes = []byte(strings.TrimSpace(password))
667- }
668- fmt.Println()
669-
670- if len(passwordBytes) == 0 {
671- return fmt.Errorf("password cannot be empty")
672- }
673-
674- // Create a temporary auth service to generate the hash
675- authSvc := auth.NewService(auth.Config{}, nil, zerowrap.Default())
676- hash, err := authSvc.GeneratePasswordHash(string(passwordBytes))
677- if err != nil {
678- return fmt.Errorf("failed to generate hash: %w", err)
679- }
680-
681- fmt.Println()
682- fmt.Println("Bcrypt hash (store in your secrets backend):")
683- fmt.Println(hash)
684- fmt.Println()
685- fmt.Println("Then reference the path in your config:")
686- fmt.Println(" [auth]")
687- fmt.Println(" type = \"password\"")
688- fmt.Println(" password_hash = \"github.com/bnema/gordon/auth/password_hash\"")
689-
690- return nil
691-}
692-
693617 // cliConfig holds the configuration needed for CLI commands.
694618 type cliConfig struct {
695619 Backend domain.SecretsBackend
@@ -44,7 +44,6 @@ func newAuthCmd() *cobra.Command {
44 }44 }
45 45
46 cmd.AddCommand(newAuthTokenCmd())46 cmd.AddCommand(newAuthTokenCmd())
47- cmd.AddCommand(newAuthPasswordCmd())
48 cmd.AddCommand(newAuthInternalCmd())47 cmd.AddCommand(newAuthInternalCmd())
49 cmd.AddCommand(newAuthLoginCmd())48 cmd.AddCommand(newAuthLoginCmd())
50 cmd.AddCommand(newAuthStatusCmd())49 cmd.AddCommand(newAuthStatusCmd())
@@ -191,19 +190,6 @@ Examples:
191 return cmd190 return cmd
192 }191 }
193 192
194-// newAuthPasswordCmd creates the password subcommand group.
195-func newAuthPasswordCmd() *cobra.Command {
196- cmd := &cobra.Command{
197- Use: "password",
198- Short: "Password utilities",
199- Long: `Utilities for managing password authentication.`,
200- }
201-
202- cmd.AddCommand(newPasswordHashCmd())
203-
204- return cmd
205-}
206-
207 // newAuthInternalCmd creates the internal credentials command.193 // newAuthInternalCmd creates the internal credentials command.
208 func newAuthInternalCmd() *cobra.Command {194 func newAuthInternalCmd() *cobra.Command {
209 return &cobra.Command{195 return &cobra.Command{
@@ -420,24 +406,6 @@ func runShowInternalAuth() error {
420 return nil406 return nil
421 }407 }
422 408
423-// newPasswordHashCmd creates the password hash command.
424-func newPasswordHashCmd() *cobra.Command {
425- return &cobra.Command{
426- Use: "hash",
427- Short: "Generate a bcrypt hash for a password",
428- Long: `Interactively prompt for a password and output its bcrypt hash.
429-
430-The hash can be stored in your secrets backend and referenced in the config:
431-
432- [auth]
433- type = "password"
434- password_hash = "github.com/bnema/gordon/auth/password_hash"`,
435- RunE: func(cmd *cobra.Command, args []string) error {
436- return runPasswordHash()
437- },
438- }
439-}
440-
441 // runTokenGenerate generates a new authentication token.409 // runTokenGenerate generates a new authentication token.
442 // parseAndConvertScopes parses a comma-separated scope string and converts410 // parseAndConvertScopes parses a comma-separated scope string and converts
443 // simple scopes (push, pull, *) to Docker v2 format (repository:*:action).411 // simple scopes (push, pull, *) to Docker v2 format (repository:*:action).
@@ -646,50 +614,6 @@ func runTokenRevokeAll(configPath string) error {
646 return nil614 return nil
647 }615 }
648 616
649-// runPasswordHash generates a bcrypt hash for a password.
650-func runPasswordHash() error {
651- fmt.Print("Enter password: ")
652-
653- // Read password without echo (use os.Stdin.Fd() for better compatibility)
654- fd, err := stdinFD()
655- if err != nil {
656- return fmt.Errorf("failed to get stdin fd: %w", err)
657- }
658- passwordBytes, err := term.ReadPassword(fd)
659- if err != nil {
660- // Fallback for non-terminal input
661- reader := bufio.NewReader(os.Stdin)
662- password, err := reader.ReadString('\n')
663- if err != nil {
664- return fmt.Errorf("failed to read password: %w", err)
665- }
666- passwordBytes = []byte(strings.TrimSpace(password))
667- }
668- fmt.Println()
669-
670- if len(passwordBytes) == 0 {
671- return fmt.Errorf("password cannot be empty")
672- }
673-
674- // Create a temporary auth service to generate the hash
675- authSvc := auth.NewService(auth.Config{}, nil, zerowrap.Default())
676- hash, err := authSvc.GeneratePasswordHash(string(passwordBytes))
677- if err != nil {
678- return fmt.Errorf("failed to generate hash: %w", err)
679- }
680-
681- fmt.Println()
682- fmt.Println("Bcrypt hash (store in your secrets backend):")
683- fmt.Println(hash)
684- fmt.Println()
685- fmt.Println("Then reference the path in your config:")
686- fmt.Println(" [auth]")
687- fmt.Println(" type = \"password\"")
688- fmt.Println(" password_hash = \"github.com/bnema/gordon/auth/password_hash\"")
689-
690- return nil
691-}
692-
693 // cliConfig holds the configuration needed for CLI commands.617 // cliConfig holds the configuration needed for CLI commands.
694 type cliConfig struct {618 type cliConfig struct {
695 Backend domain.SecretsBackend619 Backend domain.SecretsBackend
M internal/adapters/in/http/auth/handler.go +6 -123
@@ -39,21 +39,6 @@ func NewHandler(authSvc in.AuthService, internalAuth InternalAuth, log zerowrap.
3939 }
4040 }
4141
42-// PasswordRequest represents the request body for POST /auth/password.
43-type PasswordRequest struct {
44- Username string `json:"username"`
45- Password string `json:"password"`
46-}
47-
48-// PasswordResponse represents the response from POST /auth/password.
49-type PasswordResponse struct {
50- Token string `json:"token"`
51- ExpiresIn int `json:"expires_in"`
52- IssuedAt string `json:"issued_at"`
53-}
54-
55-const passwordSessionTTL = 24 * time.Hour
56-
5742 func setNoStoreHeaders(w http.ResponseWriter) {
5843 w.Header().Set("Cache-Control", "no-store")
5944 w.Header().Set("Pragma", "no-cache")
@@ -76,107 +61,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
7661 }
7762
7863 // handlePassword handles POST /auth/password requests.
79-// Validates username/password and returns a daily session JWT.
80-// Only works when auth type is "password" - returns error for "token" auth type.
64+// Password authentication has been removed; this endpoint always returns Gone.
8165 func (h *Handler) handlePassword(w http.ResponseWriter, r *http.Request) {
8266 setNoStoreHeaders(w)
83-
84- ctx := zerowrap.CtxWithFields(r.Context(), map[string]any{
85- zerowrap.FieldLayer: "adapter",
86- zerowrap.FieldAdapter: "http",
87- zerowrap.FieldHandler: "auth",
88- zerowrap.FieldMethod: r.Method,
89- zerowrap.FieldPath: r.URL.Path,
90- })
91- log := zerowrap.FromCtx(ctx)
92-
93- if r.Method != http.MethodPost {
94- w.Header().Set("Content-Type", "application/json")
95- w.WriteHeader(http.StatusMethodNotAllowed)
96- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "method not allowed"})
97- return
98- }
99-
100- // Check if auth is enabled
101- if h.authSvc == nil || !h.authSvc.IsEnabled() {
102- w.Header().Set("Content-Type", "application/json")
103- w.WriteHeader(http.StatusServiceUnavailable)
104- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "authentication is required"})
105- return
106- }
107-
108- // Password endpoint only works with password auth type
109- if h.authSvc.GetAuthType() != domain.AuthTypePassword {
110- w.Header().Set("Content-Type", "application/json")
111- w.WriteHeader(http.StatusBadRequest)
112- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{
113- Error: "password authentication not configured, use token-based auth",
114- })
115- return
116- }
117-
118- // Limit request body size
119- r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB
120-
121- // Parse request body
122- var req PasswordRequest
123- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
124- w.Header().Set("Content-Type", "application/json")
125- w.WriteHeader(http.StatusBadRequest)
126- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "invalid request body"})
127- return
128- }
129-
130- if req.Username == "" || req.Password == "" {
131- w.Header().Set("Content-Type", "application/json")
132- w.WriteHeader(http.StatusBadRequest)
133- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "username and password are required"})
134- return
135- }
136-
137- // Validate password
138- if !h.authSvc.ValidatePassword(ctx, req.Username, req.Password) {
139- log.Debug().
140- Str("username", req.Username).
141- Msg("password authentication failed")
142- w.Header().Set("WWW-Authenticate", `Basic realm="Gordon"`)
143- w.Header().Set("Content-Type", "application/json")
144- w.WriteHeader(http.StatusUnauthorized)
145- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "invalid credentials"})
146- return
147- }
148-
149- // Generate daily session token with full access (push, pull, admin).
150- // This enforces regular re-authentication similar to short-session UX.
151- expiry := passwordSessionTTL
152- scopes := []string{"push", "pull", "admin:*:*"}
153-
154- token, err := h.authSvc.GenerateToken(ctx, req.Username, scopes, expiry)
155- if err != nil {
156- log.Error().Err(err).Msg("failed to generate token")
157- w.Header().Set("Content-Type", "application/json")
158- w.WriteHeader(http.StatusInternalServerError)
159- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "failed to generate token"})
160- return
161- }
162-
163- response := PasswordResponse{
164- Token: token,
165- ExpiresIn: int(expiry.Seconds()),
166- IssuedAt: time.Now().UTC().Format(time.RFC3339),
167- }
168-
16967 w.Header().Set("Content-Type", "application/json")
170- w.WriteHeader(http.StatusOK)
171-
172- if err := json.NewEncoder(w).Encode(response); err != nil {
173- log.Error().Err(err).Msg("failed to encode password response")
174- }
175-
176- log.Debug().
177- Str("username", req.Username).
178- Int("expires_in", response.ExpiresIn).
179- Msg("long-lived token issued via password auth")
68+ w.WriteHeader(http.StatusGone)
69+ _ = json.NewEncoder(w).Encode(dto.ErrorResponse{
70+ Error: "password authentication has been removed, use token-based auth",
71+ })
18072 }
18173
18274 // handleToken handles GET /auth/token requests.
@@ -278,21 +170,12 @@ func (h *Handler) authenticateTokenCredentials(ctx context.Context, r *http.Requ
278170 return true, nil
279171 }
280172
281- // Always try JWT token validation first — even in password-auth mode the
282- // server issues JWTs on login, and the CLI sends them back via Basic Auth
283- // for registry token exchange.
173+ // Validate JWT token sent via Basic Auth password field.
284174 claims, err := h.authSvc.ValidateToken(ctx, password)
285175 if err == nil && claims.Subject == username {
286176 return true, claims
287177 }
288178
289- // Fall back to password validation for direct username/password credentials.
290- if h.authSvc.GetAuthType() == domain.AuthTypePassword {
291- if h.authSvc.ValidatePassword(ctx, username, password) {
292- return true, nil
293- }
294- }
295-
296179 return false, nil
297180 }
298181
@@ -39,21 +39,6 @@ func NewHandler(authSvc in.AuthService, internalAuth InternalAuth, log zerowrap.
39 }39 }
40 }40 }
41 41
42-// PasswordRequest represents the request body for POST /auth/password.
43-type PasswordRequest struct {
44- Username string `json:"username"`
45- Password string `json:"password"`
46-}
47-
48-// PasswordResponse represents the response from POST /auth/password.
49-type PasswordResponse struct {
50- Token string `json:"token"`
51- ExpiresIn int `json:"expires_in"`
52- IssuedAt string `json:"issued_at"`
53-}
54-
55-const passwordSessionTTL = 24 * time.Hour
56-
57 func setNoStoreHeaders(w http.ResponseWriter) {42 func setNoStoreHeaders(w http.ResponseWriter) {
58 w.Header().Set("Cache-Control", "no-store")43 w.Header().Set("Cache-Control", "no-store")
59 w.Header().Set("Pragma", "no-cache")44 w.Header().Set("Pragma", "no-cache")
@@ -76,107 +61,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
76 }61 }
77 62
78 // handlePassword handles POST /auth/password requests.63 // handlePassword handles POST /auth/password requests.
79-// Validates username/password and returns a daily session JWT.64+// Password authentication has been removed; this endpoint always returns Gone.
80-// Only works when auth type is "password" - returns error for "token" auth type.
81 func (h *Handler) handlePassword(w http.ResponseWriter, r *http.Request) {65 func (h *Handler) handlePassword(w http.ResponseWriter, r *http.Request) {
82 setNoStoreHeaders(w)66 setNoStoreHeaders(w)
83-
84- ctx := zerowrap.CtxWithFields(r.Context(), map[string]any{
85- zerowrap.FieldLayer: "adapter",
86- zerowrap.FieldAdapter: "http",
87- zerowrap.FieldHandler: "auth",
88- zerowrap.FieldMethod: r.Method,
89- zerowrap.FieldPath: r.URL.Path,
90- })
91- log := zerowrap.FromCtx(ctx)
92-
93- if r.Method != http.MethodPost {
94- w.Header().Set("Content-Type", "application/json")
95- w.WriteHeader(http.StatusMethodNotAllowed)
96- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "method not allowed"})
97- return
98- }
99-
100- // Check if auth is enabled
101- if h.authSvc == nil || !h.authSvc.IsEnabled() {
102- w.Header().Set("Content-Type", "application/json")
103- w.WriteHeader(http.StatusServiceUnavailable)
104- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "authentication is required"})
105- return
106- }
107-
108- // Password endpoint only works with password auth type
109- if h.authSvc.GetAuthType() != domain.AuthTypePassword {
110- w.Header().Set("Content-Type", "application/json")
111- w.WriteHeader(http.StatusBadRequest)
112- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{
113- Error: "password authentication not configured, use token-based auth",
114- })
115- return
116- }
117-
118- // Limit request body size
119- r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB
120-
121- // Parse request body
122- var req PasswordRequest
123- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
124- w.Header().Set("Content-Type", "application/json")
125- w.WriteHeader(http.StatusBadRequest)
126- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "invalid request body"})
127- return
128- }
129-
130- if req.Username == "" || req.Password == "" {
131- w.Header().Set("Content-Type", "application/json")
132- w.WriteHeader(http.StatusBadRequest)
133- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "username and password are required"})
134- return
135- }
136-
137- // Validate password
138- if !h.authSvc.ValidatePassword(ctx, req.Username, req.Password) {
139- log.Debug().
140- Str("username", req.Username).
141- Msg("password authentication failed")
142- w.Header().Set("WWW-Authenticate", `Basic realm="Gordon"`)
143- w.Header().Set("Content-Type", "application/json")
144- w.WriteHeader(http.StatusUnauthorized)
145- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "invalid credentials"})
146- return
147- }
148-
149- // Generate daily session token with full access (push, pull, admin).
150- // This enforces regular re-authentication similar to short-session UX.
151- expiry := passwordSessionTTL
152- scopes := []string{"push", "pull", "admin:*:*"}
153-
154- token, err := h.authSvc.GenerateToken(ctx, req.Username, scopes, expiry)
155- if err != nil {
156- log.Error().Err(err).Msg("failed to generate token")
157- w.Header().Set("Content-Type", "application/json")
158- w.WriteHeader(http.StatusInternalServerError)
159- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "failed to generate token"})
160- return
161- }
162-
163- response := PasswordResponse{
164- Token: token,
165- ExpiresIn: int(expiry.Seconds()),
166- IssuedAt: time.Now().UTC().Format(time.RFC3339),
167- }
168-
169 w.Header().Set("Content-Type", "application/json")67 w.Header().Set("Content-Type", "application/json")
170- w.WriteHeader(http.StatusOK)68+ w.WriteHeader(http.StatusGone)
171-69+ _ = json.NewEncoder(w).Encode(dto.ErrorResponse{
172- if err := json.NewEncoder(w).Encode(response); err != nil {70+ Error: "password authentication has been removed, use token-based auth",
173- log.Error().Err(err).Msg("failed to encode password response")71+ })
174- }
175-
176- log.Debug().
177- Str("username", req.Username).
178- Int("expires_in", response.ExpiresIn).
179- Msg("long-lived token issued via password auth")
180 }72 }
181 73
182 // handleToken handles GET /auth/token requests.74 // handleToken handles GET /auth/token requests.
@@ -278,21 +170,12 @@ func (h *Handler) authenticateTokenCredentials(ctx context.Context, r *http.Requ
278 return true, nil170 return true, nil
279 }171 }
280 172
281- // Always try JWT token validation first — even in password-auth mode the173+ // Validate JWT token sent via Basic Auth password field.
282- // server issues JWTs on login, and the CLI sends them back via Basic Auth
283- // for registry token exchange.
284 claims, err := h.authSvc.ValidateToken(ctx, password)174 claims, err := h.authSvc.ValidateToken(ctx, password)
285 if err == nil && claims.Subject == username {175 if err == nil && claims.Subject == username {
286 return true, claims176 return true, claims
287 }177 }
288 178
289- // Fall back to password validation for direct username/password credentials.
290- if h.authSvc.GetAuthType() == domain.AuthTypePassword {
291- if h.authSvc.ValidatePassword(ctx, username, password) {
292- return true, nil
293- }
294- }
295-
296 return false, nil179 return false, nil
297 }180 }
298 181
M internal/adapters/in/http/auth/handler_test.go +11 -132
@@ -22,79 +22,8 @@ func testLogger() zerowrap.Logger {
2222 return zerowrap.Default()
2323 }
2424
25-func TestHandler_Password_Success(t *testing.T) {
26- authSvc := mocks.NewMockAuthService(t)
27-
28- authSvc.EXPECT().IsEnabled().Return(true)
29- authSvc.EXPECT().GetAuthType().Return(domain.AuthTypePassword)
30- authSvc.EXPECT().ValidatePassword(mock.Anything, "admin", "secret").Return(true)
31- authSvc.EXPECT().GenerateToken(mock.Anything, "admin", []string{"push", "pull", "admin:*:*"}, 24*time.Hour).
32- Return("jwt-token-here", nil)
33-
34- handler := NewHandler(authSvc, InternalAuth{}, testLogger())
35-
36- body := `{"username":"admin","password":"secret"}`
37- req := httptest.NewRequest(http.MethodPost, "/auth/password", bytes.NewBufferString(body))
38- req.Header.Set("Content-Type", "application/json")
39- rec := httptest.NewRecorder()
40-
41- handler.ServeHTTP(rec, req)
42-
43- assert.Equal(t, http.StatusOK, rec.Code)
44-
45- var resp PasswordResponse
46- err := json.NewDecoder(rec.Body).Decode(&resp)
47- assert.NoError(t, err)
48- assert.Equal(t, "jwt-token-here", resp.Token)
49- assert.Equal(t, 24*60*60, resp.ExpiresIn)
50- assert.NotEmpty(t, resp.IssuedAt)
51-}
52-
53-func TestHandler_Password_InvalidCredentials(t *testing.T) {
54- authSvc := mocks.NewMockAuthService(t)
55-
56- authSvc.EXPECT().IsEnabled().Return(true)
57- authSvc.EXPECT().GetAuthType().Return(domain.AuthTypePassword)
58- authSvc.EXPECT().ValidatePassword(mock.Anything, "admin", "wrongpassword").Return(false)
59-
60- handler := NewHandler(authSvc, InternalAuth{}, testLogger())
61-
62- body := `{"username":"admin","password":"wrongpassword"}`
63- req := httptest.NewRequest(http.MethodPost, "/auth/password", bytes.NewBufferString(body))
64- req.Header.Set("Content-Type", "application/json")
65- rec := httptest.NewRecorder()
66-
67- handler.ServeHTTP(rec, req)
68-
69- assert.Equal(t, http.StatusUnauthorized, rec.Code)
70- assert.Contains(t, rec.Body.String(), "invalid credentials")
71-}
72-
73-func TestHandler_Password_AuthTypeToken(t *testing.T) {
74- authSvc := mocks.NewMockAuthService(t)
75-
76- authSvc.EXPECT().IsEnabled().Return(true)
77- authSvc.EXPECT().GetAuthType().Return(domain.AuthTypeToken)
78-
79- handler := NewHandler(authSvc, InternalAuth{}, testLogger())
80-
81- body := `{"username":"admin","password":"secret"}`
82- req := httptest.NewRequest(http.MethodPost, "/auth/password", bytes.NewBufferString(body))
83- req.Header.Set("Content-Type", "application/json")
84- rec := httptest.NewRecorder()
85-
86- handler.ServeHTTP(rec, req)
87-
88- assert.Equal(t, http.StatusBadRequest, rec.Code)
89- assert.Contains(t, rec.Body.String(), "password authentication not configured")
90-}
91-
92-func TestHandler_Password_AuthDisabled(t *testing.T) {
93- authSvc := mocks.NewMockAuthService(t)
94-
95- authSvc.EXPECT().IsEnabled().Return(false)
96-
97- handler := NewHandler(authSvc, InternalAuth{}, testLogger())
25+func TestHandler_Password_AlwaysGone(t *testing.T) {
26+ handler := NewHandler(nil, InternalAuth{}, testLogger())
9827
9928 body := `{"username":"admin","password":"secret"}`
10029 req := httptest.NewRequest(http.MethodPost, "/auth/password", bytes.NewBufferString(body))
@@ -103,73 +32,25 @@ func TestHandler_Password_AuthDisabled(t *testing.T) {
10332
10433 handler.ServeHTTP(rec, req)
10534
106- assert.Equal(t, http.StatusServiceUnavailable, rec.Code)
107- assert.Contains(t, rec.Body.String(), "authentication is required")
108-}
109-
110-func TestHandler_Password_MissingFields(t *testing.T) {
111- authSvc := mocks.NewMockAuthService(t)
112-
113- authSvc.EXPECT().IsEnabled().Return(true)
114- authSvc.EXPECT().GetAuthType().Return(domain.AuthTypePassword)
115-
116- handler := NewHandler(authSvc, InternalAuth{}, testLogger())
117-
118- tests := []struct {
119- name string
120- body string
121- }{
122- {"missing username", `{"password":"secret"}`},
123- {"missing password", `{"username":"admin"}`},
124- {"empty username", `{"username":"","password":"secret"}`},
125- {"empty password", `{"username":"admin","password":""}`},
126- }
127-
128- for _, tt := range tests {
129- t.Run(tt.name, func(t *testing.T) {
130- req := httptest.NewRequest(http.MethodPost, "/auth/password", bytes.NewBufferString(tt.body))
131- req.Header.Set("Content-Type", "application/json")
132- rec := httptest.NewRecorder()
133-
134- handler.ServeHTTP(rec, req)
135-
136- assert.Equal(t, http.StatusBadRequest, rec.Code)
137- })
138- }
139-}
140-
141-func TestHandler_Password_MethodNotAllowed(t *testing.T) {
142- handler := NewHandler(nil, InternalAuth{}, testLogger())
143-
144- methods := []string{http.MethodGet, http.MethodPut, http.MethodDelete, http.MethodPatch}
145-
146- for _, method := range methods {
147- t.Run(method, func(t *testing.T) {
148- req := httptest.NewRequest(method, "/auth/password", nil)
149- rec := httptest.NewRecorder()
150-
151- handler.ServeHTTP(rec, req)
152-
153- assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
154- })
155- }
35+ assert.Equal(t, http.StatusGone, rec.Code)
36+ assert.Contains(t, rec.Body.String(), "password authentication has been removed")
15637 }
15738
15839 func TestHandler_Token_Success(t *testing.T) {
15940 authSvc := mocks.NewMockAuthService(t)
16041
16142 authSvc.EXPECT().IsEnabled().Return(true)
162- // ValidateToken is tried first; plain password fails JWT parsing
163- authSvc.EXPECT().ValidateToken(mock.Anything, "secret").Return(nil, fmt.Errorf("not a token"))
164- authSvc.EXPECT().GetAuthType().Return(domain.AuthTypePassword)
165- authSvc.EXPECT().ValidatePassword(mock.Anything, "admin", "secret").Return(true)
43+ authSvc.EXPECT().ValidateToken(mock.Anything, "existing-jwt-token").Return(&domain.TokenClaims{
44+ Subject: "admin",
45+ Scopes: []string{"repository:myrepo:pull"},
46+ }, nil)
16647 authSvc.EXPECT().GenerateAccessToken(mock.Anything, "admin", []string{"repository:myrepo:pull"}, 5*time.Minute).
16748 Return("access-token", nil)
16849
16950 handler := NewHandler(authSvc, InternalAuth{}, testLogger())
17051
17152 req := httptest.NewRequest(http.MethodGet, "/auth/token?scope=repository:myrepo:pull", nil)
172- req.SetBasicAuth("admin", "secret")
53+ req.SetBasicAuth("admin", "existing-jwt-token")
17354 rec := httptest.NewRecorder()
17455
17556 handler.ServeHTTP(rec, req)
@@ -218,14 +99,12 @@ func TestHandler_Token_InvalidCredentials(t *testing.T) {
21899 authSvc := mocks.NewMockAuthService(t)
219100
220101 authSvc.EXPECT().IsEnabled().Return(true)
221- authSvc.EXPECT().ValidateToken(mock.Anything, "wrongpassword").Return(nil, fmt.Errorf("not a token"))
222- authSvc.EXPECT().GetAuthType().Return(domain.AuthTypePassword)
223- authSvc.EXPECT().ValidatePassword(mock.Anything, "admin", "wrongpassword").Return(false)
102+ authSvc.EXPECT().ValidateToken(mock.Anything, "invalid-token").Return(nil, fmt.Errorf("invalid token"))
224103
225104 handler := NewHandler(authSvc, InternalAuth{}, testLogger())
226105
227106 req := httptest.NewRequest(http.MethodGet, "/auth/token", nil)
228- req.SetBasicAuth("admin", "wrongpassword")
107+ req.SetBasicAuth("admin", "invalid-token")
229108 rec := httptest.NewRecorder()
230109
231110 handler.ServeHTTP(rec, req)
@@ -22,79 +22,8 @@ func testLogger() zerowrap.Logger {
22 return zerowrap.Default()22 return zerowrap.Default()
23 }23 }
24 24
25-func TestHandler_Password_Success(t *testing.T) {25+func TestHandler_Password_AlwaysGone(t *testing.T) {
26- authSvc := mocks.NewMockAuthService(t)26+ handler := NewHandler(nil, InternalAuth{}, testLogger())
27-
28- authSvc.EXPECT().IsEnabled().Return(true)
29- authSvc.EXPECT().GetAuthType().Return(domain.AuthTypePassword)
30- authSvc.EXPECT().ValidatePassword(mock.Anything, "admin", "secret").Return(true)
31- authSvc.EXPECT().GenerateToken(mock.Anything, "admin", []string{"push", "pull", "admin:*:*"}, 24*time.Hour).
32- Return("jwt-token-here", nil)
33-
34- handler := NewHandler(authSvc, InternalAuth{}, testLogger())
35-
36- body := `{"username":"admin","password":"secret"}`
37- req := httptest.NewRequest(http.MethodPost, "/auth/password", bytes.NewBufferString(body))
38- req.Header.Set("Content-Type", "application/json")
39- rec := httptest.NewRecorder()
40-
41- handler.ServeHTTP(rec, req)
42-
43- assert.Equal(t, http.StatusOK, rec.Code)
44-
45- var resp PasswordResponse
46- err := json.NewDecoder(rec.Body).Decode(&resp)
47- assert.NoError(t, err)
48- assert.Equal(t, "jwt-token-here", resp.Token)
49- assert.Equal(t, 24*60*60, resp.ExpiresIn)
50- assert.NotEmpty(t, resp.IssuedAt)
51-}
52-
53-func TestHandler_Password_InvalidCredentials(t *testing.T) {
54- authSvc := mocks.NewMockAuthService(t)
55-
56- authSvc.EXPECT().IsEnabled().Return(true)
57- authSvc.EXPECT().GetAuthType().Return(domain.AuthTypePassword)
58- authSvc.EXPECT().ValidatePassword(mock.Anything, "admin", "wrongpassword").Return(false)
59-
60- handler := NewHandler(authSvc, InternalAuth{}, testLogger())
61-
62- body := `{"username":"admin","password":"wrongpassword"}`
63- req := httptest.NewRequest(http.MethodPost, "/auth/password", bytes.NewBufferString(body))
64- req.Header.Set("Content-Type", "application/json")
65- rec := httptest.NewRecorder()
66-
67- handler.ServeHTTP(rec, req)
68-
69- assert.Equal(t, http.StatusUnauthorized, rec.Code)
70- assert.Contains(t, rec.Body.String(), "invalid credentials")
71-}
72-
73-func TestHandler_Password_AuthTypeToken(t *testing.T) {
74- authSvc := mocks.NewMockAuthService(t)
75-
76- authSvc.EXPECT().IsEnabled().Return(true)
77- authSvc.EXPECT().GetAuthType().Return(domain.AuthTypeToken)
78-
79- handler := NewHandler(authSvc, InternalAuth{}, testLogger())
80-
81- body := `{"username":"admin","password":"secret"}`
82- req := httptest.NewRequest(http.MethodPost, "/auth/password", bytes.NewBufferString(body))
83- req.Header.Set("Content-Type", "application/json")
84- rec := httptest.NewRecorder()
85-
86- handler.ServeHTTP(rec, req)
87-
88- assert.Equal(t, http.StatusBadRequest, rec.Code)
89- assert.Contains(t, rec.Body.String(), "password authentication not configured")
90-}
91-
92-func TestHandler_Password_AuthDisabled(t *testing.T) {
93- authSvc := mocks.NewMockAuthService(t)
94-
95- authSvc.EXPECT().IsEnabled().Return(false)
96-
97- handler := NewHandler(authSvc, InternalAuth{}, testLogger())
98 27
99 body := `{"username":"admin","password":"secret"}`28 body := `{"username":"admin","password":"secret"}`
100 req := httptest.NewRequest(http.MethodPost, "/auth/password", bytes.NewBufferString(body))29 req := httptest.NewRequest(http.MethodPost, "/auth/password", bytes.NewBufferString(body))
@@ -103,73 +32,25 @@ func TestHandler_Password_AuthDisabled(t *testing.T) {
103 32
104 handler.ServeHTTP(rec, req)33 handler.ServeHTTP(rec, req)
105 34
106- assert.Equal(t, http.StatusServiceUnavailable, rec.Code)35+ assert.Equal(t, http.StatusGone, rec.Code)
107- assert.Contains(t, rec.Body.String(), "authentication is required")36+ assert.Contains(t, rec.Body.String(), "password authentication has been removed")
108-}
109-
110-func TestHandler_Password_MissingFields(t *testing.T) {
111- authSvc := mocks.NewMockAuthService(t)
112-
113- authSvc.EXPECT().IsEnabled().Return(true)
114- authSvc.EXPECT().GetAuthType().Return(domain.AuthTypePassword)
115-
116- handler := NewHandler(authSvc, InternalAuth{}, testLogger())
117-
118- tests := []struct {
119- name string
120- body string
121- }{
122- {"missing username", `{"password":"secret"}`},
123- {"missing password", `{"username":"admin"}`},
124- {"empty username", `{"username":"","password":"secret"}`},
125- {"empty password", `{"username":"admin","password":""}`},
126- }
127-
128- for _, tt := range tests {
129- t.Run(tt.name, func(t *testing.T) {
130- req := httptest.NewRequest(http.MethodPost, "/auth/password", bytes.NewBufferString(tt.body))
131- req.Header.Set("Content-Type", "application/json")
132- rec := httptest.NewRecorder()
133-
134- handler.ServeHTTP(rec, req)
135-
136- assert.Equal(t, http.StatusBadRequest, rec.Code)
137- })
138- }
139-}
140-
141-func TestHandler_Password_MethodNotAllowed(t *testing.T) {
142- handler := NewHandler(nil, InternalAuth{}, testLogger())
143-
144- methods := []string{http.MethodGet, http.MethodPut, http.MethodDelete, http.MethodPatch}
145-
146- for _, method := range methods {
147- t.Run(method, func(t *testing.T) {
148- req := httptest.NewRequest(method, "/auth/password", nil)
149- rec := httptest.NewRecorder()
150-
151- handler.ServeHTTP(rec, req)
152-
153- assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
154- })
155- }
156 }37 }
157 38
158 func TestHandler_Token_Success(t *testing.T) {39 func TestHandler_Token_Success(t *testing.T) {
159 authSvc := mocks.NewMockAuthService(t)40 authSvc := mocks.NewMockAuthService(t)
160 41
161 authSvc.EXPECT().IsEnabled().Return(true)42 authSvc.EXPECT().IsEnabled().Return(true)
162- // ValidateToken is tried first; plain password fails JWT parsing43+ authSvc.EXPECT().ValidateToken(mock.Anything, "existing-jwt-token").Return(&domain.TokenClaims{
163- authSvc.EXPECT().ValidateToken(mock.Anything, "secret").Return(nil, fmt.Errorf("not a token"))44+ Subject: "admin",
164- authSvc.EXPECT().GetAuthType().Return(domain.AuthTypePassword)45+ Scopes: []string{"repository:myrepo:pull"},
165- authSvc.EXPECT().ValidatePassword(mock.Anything, "admin", "secret").Return(true)46+ }, nil)
166 authSvc.EXPECT().GenerateAccessToken(mock.Anything, "admin", []string{"repository:myrepo:pull"}, 5*time.Minute).47 authSvc.EXPECT().GenerateAccessToken(mock.Anything, "admin", []string{"repository:myrepo:pull"}, 5*time.Minute).
167 Return("access-token", nil)48 Return("access-token", nil)
168 49
169 handler := NewHandler(authSvc, InternalAuth{}, testLogger())50 handler := NewHandler(authSvc, InternalAuth{}, testLogger())
170 51
171 req := httptest.NewRequest(http.MethodGet, "/auth/token?scope=repository:myrepo:pull", nil)52 req := httptest.NewRequest(http.MethodGet, "/auth/token?scope=repository:myrepo:pull", nil)
172- req.SetBasicAuth("admin", "secret")53+ req.SetBasicAuth("admin", "existing-jwt-token")
173 rec := httptest.NewRecorder()54 rec := httptest.NewRecorder()
174 55
175 handler.ServeHTTP(rec, req)56 handler.ServeHTTP(rec, req)
@@ -218,14 +99,12 @@ func TestHandler_Token_InvalidCredentials(t *testing.T) {
218 authSvc := mocks.NewMockAuthService(t)99 authSvc := mocks.NewMockAuthService(t)
219 100
220 authSvc.EXPECT().IsEnabled().Return(true)101 authSvc.EXPECT().IsEnabled().Return(true)
221- authSvc.EXPECT().ValidateToken(mock.Anything, "wrongpassword").Return(nil, fmt.Errorf("not a token"))102+ authSvc.EXPECT().ValidateToken(mock.Anything, "invalid-token").Return(nil, fmt.Errorf("invalid token"))
222- authSvc.EXPECT().GetAuthType().Return(domain.AuthTypePassword)
223- authSvc.EXPECT().ValidatePassword(mock.Anything, "admin", "wrongpassword").Return(false)
224 103
225 handler := NewHandler(authSvc, InternalAuth{}, testLogger())104 handler := NewHandler(authSvc, InternalAuth{}, testLogger())
226 105
227 req := httptest.NewRequest(http.MethodGet, "/auth/token", nil)106 req := httptest.NewRequest(http.MethodGet, "/auth/token", nil)
228- req.SetBasicAuth("admin", "wrongpassword")107+ req.SetBasicAuth("admin", "invalid-token")
229 rec := httptest.NewRecorder()108 rec := httptest.NewRecorder()
230 109
231 handler.ServeHTTP(rec, req)110 handler.ServeHTTP(rec, req)
M internal/adapters/in/http/middleware/auth.go +1 -44
@@ -93,22 +93,7 @@ func RegistryAuthV2(authSvc in.AuthService, internalAuth InternalRegistryAuth, l
9393 // (allows CI/CD tokens while still supporting interactive password login)
9494 var authenticated bool
9595 var tokenClaims *domain.TokenClaims
96- switch authSvc.GetAuthType() {
97- case domain.AuthTypePassword:
98- // Try password auth first
99- authenticated = authenticatePassword(ctx, r, authSvc, log)
100- if !authenticated {
101- // Fall back to token auth (for CI/CD tokens)
102- authenticated, tokenClaims = authenticateToken(ctx, r, authSvc, log)
103- }
104- case domain.AuthTypeToken:
105- authenticated, tokenClaims = authenticateToken(ctx, r, authSvc, log)
106- default:
107- log.Error().
108- Str("auth_type", string(authSvc.GetAuthType())).
109- Msg("unknown auth type")
110- authenticated = false
111- }
96+ authenticated, tokenClaims = authenticateToken(ctx, r, authSvc, log)
11297
11398 if !authenticated {
11499 sendUnauthorized(w, authSvc.GetAuthType(), r.Host, log, r)
@@ -132,34 +117,6 @@ func RegistryAuthV2(authSvc in.AuthService, internalAuth InternalRegistryAuth, l
132117 }
133118 }
134119
135-// authenticatePassword handles password-based authentication.
136-func authenticatePassword(ctx context.Context, r *http.Request, authSvc in.AuthService, log zerowrap.Logger) bool {
137- username, password, ok := r.BasicAuth()
138- if !ok {
139- log.Debug().
140- Str(zerowrap.FieldMethod, r.Method).
141- Str(zerowrap.FieldPath, r.URL.Path).
142- Msg("no basic auth provided")
143- return false
144- }
145-
146- if authSvc.ValidatePassword(ctx, username, password) {
147- log.Debug().
148- Str("username", username).
149- Str(zerowrap.FieldMethod, r.Method).
150- Str(zerowrap.FieldPath, r.URL.Path).
151- Msg("password authentication successful")
152- return true
153- }
154-
155- log.Debug().
156- Str("provided_username", redactUsername(username)).
157- Str(zerowrap.FieldMethod, r.Method).
158- Str(zerowrap.FieldPath, r.URL.Path).
159- Msg("password authentication failed")
160- return false
161-}
162-
163120 // authenticateToken handles token-based authentication.
164121 // It supports both Bearer token in Authorization header and token-as-password for CI.
165122 // Returns (authenticated, tokenClaims) where tokenClaims may be nil on failure.
@@ -93,22 +93,7 @@ func RegistryAuthV2(authSvc in.AuthService, internalAuth InternalRegistryAuth, l
93 // (allows CI/CD tokens while still supporting interactive password login)93 // (allows CI/CD tokens while still supporting interactive password login)
94 var authenticated bool94 var authenticated bool
95 var tokenClaims *domain.TokenClaims95 var tokenClaims *domain.TokenClaims
96- switch authSvc.GetAuthType() {96+ authenticated, tokenClaims = authenticateToken(ctx, r, authSvc, log)
97- case domain.AuthTypePassword:
98- // Try password auth first
99- authenticated = authenticatePassword(ctx, r, authSvc, log)
100- if !authenticated {
101- // Fall back to token auth (for CI/CD tokens)
102- authenticated, tokenClaims = authenticateToken(ctx, r, authSvc, log)
103- }
104- case domain.AuthTypeToken:
105- authenticated, tokenClaims = authenticateToken(ctx, r, authSvc, log)
106- default:
107- log.Error().
108- Str("auth_type", string(authSvc.GetAuthType())).
109- Msg("unknown auth type")
110- authenticated = false
111- }
112 97
113 if !authenticated {98 if !authenticated {
114 sendUnauthorized(w, authSvc.GetAuthType(), r.Host, log, r)99 sendUnauthorized(w, authSvc.GetAuthType(), r.Host, log, r)
@@ -132,34 +117,6 @@ func RegistryAuthV2(authSvc in.AuthService, internalAuth InternalRegistryAuth, l
132 }117 }
133 }118 }
134 119
135-// authenticatePassword handles password-based authentication.
136-func authenticatePassword(ctx context.Context, r *http.Request, authSvc in.AuthService, log zerowrap.Logger) bool {
137- username, password, ok := r.BasicAuth()
138- if !ok {
139- log.Debug().
140- Str(zerowrap.FieldMethod, r.Method).
141- Str(zerowrap.FieldPath, r.URL.Path).
142- Msg("no basic auth provided")
143- return false
144- }
145-
146- if authSvc.ValidatePassword(ctx, username, password) {
147- log.Debug().
148- Str("username", username).
149- Str(zerowrap.FieldMethod, r.Method).
150- Str(zerowrap.FieldPath, r.URL.Path).
151- Msg("password authentication successful")
152- return true
153- }
154-
155- log.Debug().
156- Str("provided_username", redactUsername(username)).
157- Str(zerowrap.FieldMethod, r.Method).
158- Str(zerowrap.FieldPath, r.URL.Path).
159- Msg("password authentication failed")
160- return false
161-}
162-
163 // authenticateToken handles token-based authentication.120 // authenticateToken handles token-based authentication.
164 // It supports both Bearer token in Authorization header and token-as-password for CI.121 // It supports both Bearer token in Authorization header and token-as-password for CI.
165 // Returns (authenticated, tokenClaims) where tokenClaims may be nil on failure.122 // Returns (authenticated, tokenClaims) where tokenClaims may be nil on failure.
M internal/adapters/in/http/middleware/auth_test.go +23 -35
@@ -20,13 +20,13 @@ func testLogger() zerowrap.Logger {
2020
2121 func TestRegistryAuthV2_LocalhostBypassRequiresInternalAuth(t *testing.T) {
2222 log := testLogger()
23- called := false
23+ tokenCalled := false
2424 authSvc := stubAuthService{
2525 enabled: true,
26- authType: domain.AuthTypePassword,
27- validatePassword: func(_ context.Context, _ string, _ string) bool {
28- called = true
29- return false
26+ authType: domain.AuthTypeToken,
27+ validateToken: func(_ context.Context, _ string) (*domain.TokenClaims, error) {
28+ tokenCalled = true
29+ return nil, errors.New("not a token")
3030 },
3131 }
3232 internalAuth := InternalRegistryAuth{
@@ -46,18 +46,18 @@ func TestRegistryAuthV2_LocalhostBypassRequiresInternalAuth(t *testing.T) {
4646 handler.ServeHTTP(rec, req)
4747
4848 assert.Equal(t, http.StatusOK, rec.Code)
49- assert.False(t, called, "auth service should not be used for internal loopback auth")
49+ assert.False(t, tokenCalled, "auth service should not be used for internal loopback auth")
5050 }
5151
5252 func TestRegistryAuthV2_LocalhostWrongInternalAuthFallsBack(t *testing.T) {
5353 log := testLogger()
54- called := false
54+ tokenCalled := false
5555 authSvc := stubAuthService{
5656 enabled: true,
57- authType: domain.AuthTypePassword,
58- validatePassword: func(_ context.Context, _ string, _ string) bool {
59- called = true
60- return false
57+ authType: domain.AuthTypeToken,
58+ validateToken: func(_ context.Context, _ string) (*domain.TokenClaims, error) {
59+ tokenCalled = true
60+ return nil, errors.New("invalid token")
6161 },
6262 }
6363 internalAuth := InternalRegistryAuth{
@@ -77,19 +77,19 @@ func TestRegistryAuthV2_LocalhostWrongInternalAuthFallsBack(t *testing.T) {
7777 handler.ServeHTTP(rec, req)
7878
7979 assert.Equal(t, http.StatusUnauthorized, rec.Code)
80- assert.Equal(t, `Basic realm="Gordon Registry"`, rec.Header().Get("WWW-Authenticate"))
81- assert.True(t, called, "auth service should be used when internal auth fails")
80+ assert.Contains(t, rec.Header().Get("WWW-Authenticate"), "Bearer")
81+ assert.True(t, tokenCalled, "auth service should be used when internal auth fails")
8282 }
8383
8484 func TestRegistryAuthV2_NonLocalhostIgnoresInternalAuth(t *testing.T) {
8585 log := testLogger()
86- called := false
86+ tokenCalled := false
8787 authSvc := stubAuthService{
8888 enabled: true,
89- authType: domain.AuthTypePassword,
90- validatePassword: func(_ context.Context, _ string, _ string) bool {
91- called = true
92- return false
89+ authType: domain.AuthTypeToken,
90+ validateToken: func(_ context.Context, _ string) (*domain.TokenClaims, error) {
91+ tokenCalled = true
92+ return nil, errors.New("invalid token")
9393 },
9494 }
9595 internalAuth := InternalRegistryAuth{
@@ -109,15 +109,14 @@ func TestRegistryAuthV2_NonLocalhostIgnoresInternalAuth(t *testing.T) {
109109 handler.ServeHTTP(rec, req)
110110
111111 assert.Equal(t, http.StatusUnauthorized, rec.Code)
112- assert.Equal(t, `Basic realm="Gordon Registry"`, rec.Header().Get("WWW-Authenticate"))
113- assert.True(t, called, "auth service should be used for non-localhost requests")
112+ assert.Contains(t, rec.Header().Get("WWW-Authenticate"), "Bearer")
113+ assert.True(t, tokenCalled, "auth service should be used for non-localhost requests")
114114 }
115115
116116 type stubAuthService struct {
117- enabled bool
118- authType domain.AuthType
119- validatePassword func(ctx context.Context, username, password string) bool
120- validateToken func(ctx context.Context, tokenString string) (*domain.TokenClaims, error)
117+ enabled bool
118+ authType domain.AuthType
119+ validateToken func(ctx context.Context, tokenString string) (*domain.TokenClaims, error)
121120 }
122121
123122 func (s stubAuthService) GetAuthType() domain.AuthType {
@@ -128,13 +127,6 @@ func (s stubAuthService) IsEnabled() bool {
128127 return s.enabled
129128 }
130129
131-func (s stubAuthService) ValidatePassword(ctx context.Context, username, password string) bool {
132- if s.validatePassword != nil {
133- return s.validatePassword(ctx, username, password)
134- }
135- return false
136-}
137-
138130 func (s stubAuthService) ValidateToken(ctx context.Context, tokenString string) (*domain.TokenClaims, error) {
139131 if s.validateToken != nil {
140132 return s.validateToken(ctx, tokenString)
@@ -162,10 +154,6 @@ func (s stubAuthService) ListTokens(context.Context) ([]domain.Token, error) {
162154 return nil, errors.New("not implemented")
163155 }
164156
165-func (s stubAuthService) GeneratePasswordHash(string) (string, error) {
166- return "", errors.New("not implemented")
167-}
168-
169157 func (s stubAuthService) GetAuthStatus(context.Context) (*domain.AuthStatus, error) {
170158 return nil, errors.New("not implemented")
171159 }
@@ -20,13 +20,13 @@ func testLogger() zerowrap.Logger {
20 20
21 func TestRegistryAuthV2_LocalhostBypassRequiresInternalAuth(t *testing.T) {21 func TestRegistryAuthV2_LocalhostBypassRequiresInternalAuth(t *testing.T) {
22 log := testLogger()22 log := testLogger()
23- called := false23+ tokenCalled := false
24 authSvc := stubAuthService{24 authSvc := stubAuthService{
25 enabled: true,25 enabled: true,
26- authType: domain.AuthTypePassword,26+ authType: domain.AuthTypeToken,
27- validatePassword: func(_ context.Context, _ string, _ string) bool {27+ validateToken: func(_ context.Context, _ string) (*domain.TokenClaims, error) {
28- called = true28+ tokenCalled = true
29- return false29+ return nil, errors.New("not a token")
30 },30 },
31 }31 }
32 internalAuth := InternalRegistryAuth{32 internalAuth := InternalRegistryAuth{
@@ -46,18 +46,18 @@ func TestRegistryAuthV2_LocalhostBypassRequiresInternalAuth(t *testing.T) {
46 handler.ServeHTTP(rec, req)46 handler.ServeHTTP(rec, req)
47 47
48 assert.Equal(t, http.StatusOK, rec.Code)48 assert.Equal(t, http.StatusOK, rec.Code)
49- assert.False(t, called, "auth service should not be used for internal loopback auth")49+ assert.False(t, tokenCalled, "auth service should not be used for internal loopback auth")
50 }50 }
51 51
52 func TestRegistryAuthV2_LocalhostWrongInternalAuthFallsBack(t *testing.T) {52 func TestRegistryAuthV2_LocalhostWrongInternalAuthFallsBack(t *testing.T) {
53 log := testLogger()53 log := testLogger()
54- called := false54+ tokenCalled := false
55 authSvc := stubAuthService{55 authSvc := stubAuthService{
56 enabled: true,56 enabled: true,
57- authType: domain.AuthTypePassword,57+ authType: domain.AuthTypeToken,
58- validatePassword: func(_ context.Context, _ string, _ string) bool {58+ validateToken: func(_ context.Context, _ string) (*domain.TokenClaims, error) {
59- called = true59+ tokenCalled = true
60- return false60+ return nil, errors.New("invalid token")
61 },61 },
62 }62 }
63 internalAuth := InternalRegistryAuth{63 internalAuth := InternalRegistryAuth{
@@ -77,19 +77,19 @@ func TestRegistryAuthV2_LocalhostWrongInternalAuthFallsBack(t *testing.T) {
77 handler.ServeHTTP(rec, req)77 handler.ServeHTTP(rec, req)
78 78
79 assert.Equal(t, http.StatusUnauthorized, rec.Code)79 assert.Equal(t, http.StatusUnauthorized, rec.Code)
80- assert.Equal(t, `Basic realm="Gordon Registry"`, rec.Header().Get("WWW-Authenticate"))80+ assert.Contains(t, rec.Header().Get("WWW-Authenticate"), "Bearer")
81- assert.True(t, called, "auth service should be used when internal auth fails")81+ assert.True(t, tokenCalled, "auth service should be used when internal auth fails")
82 }82 }
83 83
84 func TestRegistryAuthV2_NonLocalhostIgnoresInternalAuth(t *testing.T) {84 func TestRegistryAuthV2_NonLocalhostIgnoresInternalAuth(t *testing.T) {
85 log := testLogger()85 log := testLogger()
86- called := false86+ tokenCalled := false
87 authSvc := stubAuthService{87 authSvc := stubAuthService{
88 enabled: true,88 enabled: true,
89- authType: domain.AuthTypePassword,89+ authType: domain.AuthTypeToken,
90- validatePassword: func(_ context.Context, _ string, _ string) bool {90+ validateToken: func(_ context.Context, _ string) (*domain.TokenClaims, error) {
91- called = true91+ tokenCalled = true
92- return false92+ return nil, errors.New("invalid token")
93 },93 },
94 }94 }
95 internalAuth := InternalRegistryAuth{95 internalAuth := InternalRegistryAuth{
@@ -109,15 +109,14 @@ func TestRegistryAuthV2_NonLocalhostIgnoresInternalAuth(t *testing.T) {
109 handler.ServeHTTP(rec, req)109 handler.ServeHTTP(rec, req)
110 110
111 assert.Equal(t, http.StatusUnauthorized, rec.Code)111 assert.Equal(t, http.StatusUnauthorized, rec.Code)
112- assert.Equal(t, `Basic realm="Gordon Registry"`, rec.Header().Get("WWW-Authenticate"))112+ assert.Contains(t, rec.Header().Get("WWW-Authenticate"), "Bearer")
113- assert.True(t, called, "auth service should be used for non-localhost requests")113+ assert.True(t, tokenCalled, "auth service should be used for non-localhost requests")
114 }114 }
115 115
116 type stubAuthService struct {116 type stubAuthService struct {
117- enabled bool117+ enabled bool
118- authType domain.AuthType118+ authType domain.AuthType
119- validatePassword func(ctx context.Context, username, password string) bool119+ validateToken func(ctx context.Context, tokenString string) (*domain.TokenClaims, error)
120- validateToken func(ctx context.Context, tokenString string) (*domain.TokenClaims, error)
121 }120 }
122 121
123 func (s stubAuthService) GetAuthType() domain.AuthType {122 func (s stubAuthService) GetAuthType() domain.AuthType {
@@ -128,13 +127,6 @@ func (s stubAuthService) IsEnabled() bool {
128 return s.enabled127 return s.enabled
129 }128 }
130 129
131-func (s stubAuthService) ValidatePassword(ctx context.Context, username, password string) bool {
132- if s.validatePassword != nil {
133- return s.validatePassword(ctx, username, password)
134- }
135- return false
136-}
137-
138 func (s stubAuthService) ValidateToken(ctx context.Context, tokenString string) (*domain.TokenClaims, error) {130 func (s stubAuthService) ValidateToken(ctx context.Context, tokenString string) (*domain.TokenClaims, error) {
139 if s.validateToken != nil {131 if s.validateToken != nil {
140 return s.validateToken(ctx, tokenString)132 return s.validateToken(ctx, tokenString)
@@ -162,10 +154,6 @@ func (s stubAuthService) ListTokens(context.Context) ([]domain.Token, error) {
162 return nil, errors.New("not implemented")154 return nil, errors.New("not implemented")
163 }155 }
164 156
165-func (s stubAuthService) GeneratePasswordHash(string) (string, error) {
166- return "", errors.New("not implemented")
167-}
168-
169 func (s stubAuthService) GetAuthStatus(context.Context) (*domain.AuthStatus, error) {157 func (s stubAuthService) GetAuthStatus(context.Context) (*domain.AuthStatus, error) {
170 return nil, errors.New("not implemented")158 return nil, errors.New("not implemented")
171 }159 }
M internal/app/run.go +2 -51
@@ -1024,24 +1024,8 @@ func createAuthService(ctx context.Context, cfg Config, log zerowrap.Logger) (ou
10241024 }
10251025
10261026 // resolveAuthType determines the auth type from config.
1027-// If password_hash is configured, password auth is available (plus tokens).
1028-// If only token_secret is configured, token-only mode.
1029-// The explicit "type" field is deprecated but still respected for backwards compat.
1030-func resolveAuthType(cfg Config) domain.AuthType {
1031- // Explicit type takes precedence (backwards compatibility)
1032- if cfg.Auth.Type == "token" {
1033- return domain.AuthTypeToken
1034- }
1035- if cfg.Auth.Type == "password" {
1036- return domain.AuthTypePassword
1037- }
1038-
1039- // Infer from config: password auth if password_hash is configured
1040- if cfg.Auth.PasswordHash != "" || cfg.Auth.Password != "" {
1041- return domain.AuthTypePassword
1042- }
1043-
1044- // Default to token-only
1027+// Token-only authentication is the only supported mode.
1028+func resolveAuthType(_ Config) domain.AuthType {
10451029 return domain.AuthTypeToken
10461030 }
10471031
@@ -1104,42 +1088,9 @@ func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType,
11041088 authConfig.TokenSecret = secret
11051089 authConfig.TokenExpiry = expiry
11061090
1107- // Password config only needed for password auth mode
1108- if authType == domain.AuthTypePassword {
1109- hash, err := loadPasswordHash(ctx, cfg, backend, dataDir, log)
1110- if err != nil {
1111- return auth.Config{}, err
1112- }
1113- if hash == "" {
1114- return auth.Config{}, errAuthNotConfigured()
1115- }
1116- authConfig.PasswordHash = hash
1117- }
1118-
11191091 return authConfig, nil
11201092 }
11211093
1122-// errAuthNotConfigured returns an error when auth is enabled but credentials are not configured.
1123-func errAuthNotConfigured() error {
1124- return fmt.Errorf("auth is enabled by default, configure auth type and secrets backend. See: https://gordon.bnema.dev/docs/config/auth")
1125-}
1126-
1127-func loadPasswordHash(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (string, error) {
1128- if cfg.Auth.PasswordHash != "" {
1129- hash, err := loadSecret(ctx, backend, cfg.Auth.PasswordHash, dataDir, log)
1130- if err != nil {
1131- return "", log.WrapErr(err, "failed to load password hash")
1132- }
1133- return hash, nil
1134- }
1135-
1136- if cfg.Auth.Password != "" {
1137- return "", fmt.Errorf("auth.password is no longer supported; use auth.password_hash with a secrets backend instead")
1138- }
1139-
1140- return "", nil
1141-}
1142-
11431094 func loadTokenConfig(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) ([]byte, time.Duration, error) {
11441095 secret, err := loadTokenSecret(ctx, cfg, backend, dataDir, log)
11451096 if err != nil {
@@ -1024,24 +1024,8 @@ func createAuthService(ctx context.Context, cfg Config, log zerowrap.Logger) (ou
1024 }1024 }
1025 1025
1026 // resolveAuthType determines the auth type from config.1026 // resolveAuthType determines the auth type from config.
1027-// If password_hash is configured, password auth is available (plus tokens).1027+// Token-only authentication is the only supported mode.
1028-// If only token_secret is configured, token-only mode.1028+func resolveAuthType(_ Config) domain.AuthType {
1029-// The explicit "type" field is deprecated but still respected for backwards compat.
1030-func resolveAuthType(cfg Config) domain.AuthType {
1031- // Explicit type takes precedence (backwards compatibility)
1032- if cfg.Auth.Type == "token" {
1033- return domain.AuthTypeToken
1034- }
1035- if cfg.Auth.Type == "password" {
1036- return domain.AuthTypePassword
1037- }
1038-
1039- // Infer from config: password auth if password_hash is configured
1040- if cfg.Auth.PasswordHash != "" || cfg.Auth.Password != "" {
1041- return domain.AuthTypePassword
1042- }
1043-
1044- // Default to token-only
1045 return domain.AuthTypeToken1029 return domain.AuthTypeToken
1046 }1030 }
1047 1031
@@ -1104,42 +1088,9 @@ func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType,
1104 authConfig.TokenSecret = secret1088 authConfig.TokenSecret = secret
1105 authConfig.TokenExpiry = expiry1089 authConfig.TokenExpiry = expiry
1106 1090
1107- // Password config only needed for password auth mode
1108- if authType == domain.AuthTypePassword {
1109- hash, err := loadPasswordHash(ctx, cfg, backend, dataDir, log)
1110- if err != nil {
1111- return auth.Config{}, err
1112- }
1113- if hash == "" {
1114- return auth.Config{}, errAuthNotConfigured()
1115- }
1116- authConfig.PasswordHash = hash
1117- }
1118-
1119 return authConfig, nil1091 return authConfig, nil
1120 }1092 }
1121 1093
1122-// errAuthNotConfigured returns an error when auth is enabled but credentials are not configured.
1123-func errAuthNotConfigured() error {
1124- return fmt.Errorf("auth is enabled by default, configure auth type and secrets backend. See: https://gordon.bnema.dev/docs/config/auth")
1125-}
1126-
1127-func loadPasswordHash(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (string, error) {
1128- if cfg.Auth.PasswordHash != "" {
1129- hash, err := loadSecret(ctx, backend, cfg.Auth.PasswordHash, dataDir, log)
1130- if err != nil {
1131- return "", log.WrapErr(err, "failed to load password hash")
1132- }
1133- return hash, nil
1134- }
1135-
1136- if cfg.Auth.Password != "" {
1137- return "", fmt.Errorf("auth.password is no longer supported; use auth.password_hash with a secrets backend instead")
1138- }
1139-
1140- return "", nil
1141-}
1142-
1143 func loadTokenConfig(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) ([]byte, time.Duration, error) {1094 func loadTokenConfig(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) ([]byte, time.Duration, error) {
1144 secret, err := loadTokenSecret(ctx, cfg, backend, dataDir, log)1095 secret, err := loadTokenSecret(ctx, cfg, backend, dataDir, log)
1145 if err != nil {1096 if err != nil {
M internal/boundaries/in/auth.go +0 -8
@@ -15,11 +15,6 @@ type AuthService interface {
1515 // IsEnabled returns whether authentication is enabled.
1616 IsEnabled() bool
1717
18- // Password authentication
19- // ValidatePassword checks if the username and password are valid.
20- ValidatePassword(ctx context.Context, username, password string) bool
21-
22- // Token authentication
2318 // ValidateToken validates a JWT token and returns its claims.
2419 ValidateToken(ctx context.Context, tokenString string) (*domain.TokenClaims, error)
2520
@@ -40,9 +35,6 @@ type AuthService interface {
4035 // ListTokens returns all stored tokens.
4136 ListTokens(ctx context.Context) ([]domain.Token, error)
4237
43- // GeneratePasswordHash generates a bcrypt hash for a password.
44- GeneratePasswordHash(password string) (string, error)
45-
4638 // GetAuthStatus returns authentication status from context.
4739 // Extracts and validates token claims that were set by auth middleware.
4840 GetAuthStatus(ctx context.Context) (*domain.AuthStatus, error)
@@ -15,11 +15,6 @@ type AuthService interface {
15 // IsEnabled returns whether authentication is enabled.15 // IsEnabled returns whether authentication is enabled.
16 IsEnabled() bool16 IsEnabled() bool
17 17
18- // Password authentication
19- // ValidatePassword checks if the username and password are valid.
20- ValidatePassword(ctx context.Context, username, password string) bool
21-
22- // Token authentication
23 // ValidateToken validates a JWT token and returns its claims.18 // ValidateToken validates a JWT token and returns its claims.
24 ValidateToken(ctx context.Context, tokenString string) (*domain.TokenClaims, error)19 ValidateToken(ctx context.Context, tokenString string) (*domain.TokenClaims, error)
25 20
@@ -40,9 +35,6 @@ type AuthService interface {
40 // ListTokens returns all stored tokens.35 // ListTokens returns all stored tokens.
41 ListTokens(ctx context.Context) ([]domain.Token, error)36 ListTokens(ctx context.Context) ([]domain.Token, error)
42 37
43- // GeneratePasswordHash generates a bcrypt hash for a password.
44- GeneratePasswordHash(password string) (string, error)
45-
46 // GetAuthStatus returns authentication status from context.38 // GetAuthStatus returns authentication status from context.
47 // Extracts and validates token claims that were set by auth middleware.39 // Extracts and validates token claims that were set by auth middleware.
48 GetAuthStatus(ctx context.Context) (*domain.AuthStatus, error)40 GetAuthStatus(ctx context.Context) (*domain.AuthStatus, error)
M internal/boundaries/in/mocks/mock_auth_service.go +0 -123
@@ -183,66 +183,6 @@ func (_c *MockAuthService_GenerateAccessToken_Call) RunAndReturn(run func(ctx co
183183 return _c
184184 }
185185
186-// GeneratePasswordHash provides a mock function for the type MockAuthService
187-func (_mock *MockAuthService) GeneratePasswordHash(password string) (string, error) {
188- ret := _mock.Called(password)
189-
190- if len(ret) == 0 {
191- panic("no return value specified for GeneratePasswordHash")
192- }
193-
194- var r0 string
195- var r1 error
196- if returnFunc, ok := ret.Get(0).(func(string) (string, error)); ok {
197- return returnFunc(password)
198- }
199- if returnFunc, ok := ret.Get(0).(func(string) string); ok {
200- r0 = returnFunc(password)
201- } else {
202- r0 = ret.Get(0).(string)
203- }
204- if returnFunc, ok := ret.Get(1).(func(string) error); ok {
205- r1 = returnFunc(password)
206- } else {
207- r1 = ret.Error(1)
208- }
209- return r0, r1
210-}
211-
212-// MockAuthService_GeneratePasswordHash_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GeneratePasswordHash'
213-type MockAuthService_GeneratePasswordHash_Call struct {
214- *mock.Call
215-}
216-
217-// GeneratePasswordHash is a helper method to define mock.On call
218-// - password string
219-func (_e *MockAuthService_Expecter) GeneratePasswordHash(password interface{}) *MockAuthService_GeneratePasswordHash_Call {
220- return &MockAuthService_GeneratePasswordHash_Call{Call: _e.mock.On("GeneratePasswordHash", password)}
221-}
222-
223-func (_c *MockAuthService_GeneratePasswordHash_Call) Run(run func(password string)) *MockAuthService_GeneratePasswordHash_Call {
224- _c.Call.Run(func(args mock.Arguments) {
225- var arg0 string
226- if args[0] != nil {
227- arg0 = args[0].(string)
228- }
229- run(
230- arg0,
231- )
232- })
233- return _c
234-}
235-
236-func (_c *MockAuthService_GeneratePasswordHash_Call) Return(s string, err error) *MockAuthService_GeneratePasswordHash_Call {
237- _c.Call.Return(s, err)
238- return _c
239-}
240-
241-func (_c *MockAuthService_GeneratePasswordHash_Call) RunAndReturn(run func(password string) (string, error)) *MockAuthService_GeneratePasswordHash_Call {
242- _c.Call.Return(run)
243- return _c
244-}
245-
246186 // GenerateToken provides a mock function for the type MockAuthService
247187 func (_mock *MockAuthService) GenerateToken(ctx context.Context, subject string, scopes []string, expiry time.Duration) (string, error) {
248188 ret := _mock.Called(ctx, subject, scopes, expiry)
@@ -650,69 +590,6 @@ func (_c *MockAuthService_RevokeToken_Call) RunAndReturn(run func(ctx context.Co
650590 return _c
651591 }
652592
653-// ValidatePassword provides a mock function for the type MockAuthService
654-func (_mock *MockAuthService) ValidatePassword(ctx context.Context, username string, password string) bool {
655- ret := _mock.Called(ctx, username, password)
656-
657- if len(ret) == 0 {
658- panic("no return value specified for ValidatePassword")
659- }
660-
661- var r0 bool
662- if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) bool); ok {
663- r0 = returnFunc(ctx, username, password)
664- } else {
665- r0 = ret.Get(0).(bool)
666- }
667- return r0
668-}
669-
670-// MockAuthService_ValidatePassword_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ValidatePassword'
671-type MockAuthService_ValidatePassword_Call struct {
672- *mock.Call
673-}
674-
675-// ValidatePassword is a helper method to define mock.On call
676-// - ctx context.Context
677-// - username string
678-// - password string
679-func (_e *MockAuthService_Expecter) ValidatePassword(ctx interface{}, username interface{}, password interface{}) *MockAuthService_ValidatePassword_Call {
680- return &MockAuthService_ValidatePassword_Call{Call: _e.mock.On("ValidatePassword", ctx, username, password)}
681-}
682-
683-func (_c *MockAuthService_ValidatePassword_Call) Run(run func(ctx context.Context, username string, password string)) *MockAuthService_ValidatePassword_Call {
684- _c.Call.Run(func(args mock.Arguments) {
685- var arg0 context.Context
686- if args[0] != nil {
687- arg0 = args[0].(context.Context)
688- }
689- var arg1 string
690- if args[1] != nil {
691- arg1 = args[1].(string)
692- }
693- var arg2 string
694- if args[2] != nil {
695- arg2 = args[2].(string)
696- }
697- run(
698- arg0,
699- arg1,
700- arg2,
701- )
702- })
703- return _c
704-}
705-
706-func (_c *MockAuthService_ValidatePassword_Call) Return(b bool) *MockAuthService_ValidatePassword_Call {
707- _c.Call.Return(b)
708- return _c
709-}
710-
711-func (_c *MockAuthService_ValidatePassword_Call) RunAndReturn(run func(ctx context.Context, username string, password string) bool) *MockAuthService_ValidatePassword_Call {
712- _c.Call.Return(run)
713- return _c
714-}
715-
716593 // ValidateToken provides a mock function for the type MockAuthService
717594 func (_mock *MockAuthService) ValidateToken(ctx context.Context, tokenString string) (*domain.TokenClaims, error) {
718595 ret := _mock.Called(ctx, tokenString)
@@ -183,66 +183,6 @@ func (_c *MockAuthService_GenerateAccessToken_Call) RunAndReturn(run func(ctx co
183 return _c183 return _c
184 }184 }
185 185
186-// GeneratePasswordHash provides a mock function for the type MockAuthService
187-func (_mock *MockAuthService) GeneratePasswordHash(password string) (string, error) {
188- ret := _mock.Called(password)
189-
190- if len(ret) == 0 {
191- panic("no return value specified for GeneratePasswordHash")
192- }
193-
194- var r0 string
195- var r1 error
196- if returnFunc, ok := ret.Get(0).(func(string) (string, error)); ok {
197- return returnFunc(password)
198- }
199- if returnFunc, ok := ret.Get(0).(func(string) string); ok {
200- r0 = returnFunc(password)
201- } else {
202- r0 = ret.Get(0).(string)
203- }
204- if returnFunc, ok := ret.Get(1).(func(string) error); ok {
205- r1 = returnFunc(password)
206- } else {
207- r1 = ret.Error(1)
208- }
209- return r0, r1
210-}
211-
212-// MockAuthService_GeneratePasswordHash_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GeneratePasswordHash'
213-type MockAuthService_GeneratePasswordHash_Call struct {
214- *mock.Call
215-}
216-
217-// GeneratePasswordHash is a helper method to define mock.On call
218-// - password string
219-func (_e *MockAuthService_Expecter) GeneratePasswordHash(password interface{}) *MockAuthService_GeneratePasswordHash_Call {
220- return &MockAuthService_GeneratePasswordHash_Call{Call: _e.mock.On("GeneratePasswordHash", password)}
221-}
222-
223-func (_c *MockAuthService_GeneratePasswordHash_Call) Run(run func(password string)) *MockAuthService_GeneratePasswordHash_Call {
224- _c.Call.Run(func(args mock.Arguments) {
225- var arg0 string
226- if args[0] != nil {
227- arg0 = args[0].(string)
228- }
229- run(
230- arg0,
231- )
232- })
233- return _c
234-}
235-
236-func (_c *MockAuthService_GeneratePasswordHash_Call) Return(s string, err error) *MockAuthService_GeneratePasswordHash_Call {
237- _c.Call.Return(s, err)
238- return _c
239-}
240-
241-func (_c *MockAuthService_GeneratePasswordHash_Call) RunAndReturn(run func(password string) (string, error)) *MockAuthService_GeneratePasswordHash_Call {
242- _c.Call.Return(run)
243- return _c
244-}
245-
246 // GenerateToken provides a mock function for the type MockAuthService186 // GenerateToken provides a mock function for the type MockAuthService
247 func (_mock *MockAuthService) GenerateToken(ctx context.Context, subject string, scopes []string, expiry time.Duration) (string, error) {187 func (_mock *MockAuthService) GenerateToken(ctx context.Context, subject string, scopes []string, expiry time.Duration) (string, error) {
248 ret := _mock.Called(ctx, subject, scopes, expiry)188 ret := _mock.Called(ctx, subject, scopes, expiry)
@@ -650,69 +590,6 @@ func (_c *MockAuthService_RevokeToken_Call) RunAndReturn(run func(ctx context.Co
650 return _c590 return _c
651 }591 }
652 592
653-// ValidatePassword provides a mock function for the type MockAuthService
654-func (_mock *MockAuthService) ValidatePassword(ctx context.Context, username string, password string) bool {
655- ret := _mock.Called(ctx, username, password)
656-
657- if len(ret) == 0 {
658- panic("no return value specified for ValidatePassword")
659- }
660-
661- var r0 bool
662- if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) bool); ok {
663- r0 = returnFunc(ctx, username, password)
664- } else {
665- r0 = ret.Get(0).(bool)
666- }
667- return r0
668-}
669-
670-// MockAuthService_ValidatePassword_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ValidatePassword'
671-type MockAuthService_ValidatePassword_Call struct {
672- *mock.Call
673-}
674-
675-// ValidatePassword is a helper method to define mock.On call
676-// - ctx context.Context
677-// - username string
678-// - password string
679-func (_e *MockAuthService_Expecter) ValidatePassword(ctx interface{}, username interface{}, password interface{}) *MockAuthService_ValidatePassword_Call {
680- return &MockAuthService_ValidatePassword_Call{Call: _e.mock.On("ValidatePassword", ctx, username, password)}
681-}
682-
683-func (_c *MockAuthService_ValidatePassword_Call) Run(run func(ctx context.Context, username string, password string)) *MockAuthService_ValidatePassword_Call {
684- _c.Call.Run(func(args mock.Arguments) {
685- var arg0 context.Context
686- if args[0] != nil {
687- arg0 = args[0].(context.Context)
688- }
689- var arg1 string
690- if args[1] != nil {
691- arg1 = args[1].(string)
692- }
693- var arg2 string
694- if args[2] != nil {
695- arg2 = args[2].(string)
696- }
697- run(
698- arg0,
699- arg1,
700- arg2,
701- )
702- })
703- return _c
704-}
705-
706-func (_c *MockAuthService_ValidatePassword_Call) Return(b bool) *MockAuthService_ValidatePassword_Call {
707- _c.Call.Return(b)
708- return _c
709-}
710-
711-func (_c *MockAuthService_ValidatePassword_Call) RunAndReturn(run func(ctx context.Context, username string, password string) bool) *MockAuthService_ValidatePassword_Call {
712- _c.Call.Return(run)
713- return _c
714-}
715-
716 // ValidateToken provides a mock function for the type MockAuthService593 // ValidateToken provides a mock function for the type MockAuthService
717 func (_mock *MockAuthService) ValidateToken(ctx context.Context, tokenString string) (*domain.TokenClaims, error) {594 func (_mock *MockAuthService) ValidateToken(ctx context.Context, tokenString string) (*domain.TokenClaims, error) {
718 ret := _mock.Called(ctx, tokenString)595 ret := _mock.Called(ctx, tokenString)
M internal/domain/auth.go +0 -2
@@ -12,8 +12,6 @@ import (
1212 type AuthType string
1313
1414 const (
15- // AuthTypePassword uses bcrypt-hashed password authentication.
16- AuthTypePassword AuthType = "password"
1715 // AuthTypeToken uses JWT token-based authentication.
1816 AuthTypeToken AuthType = "token"
1917 )
@@ -12,8 +12,6 @@ import (
12 type AuthType string12 type AuthType string
13 13
14 const (14 const (
15- // AuthTypePassword uses bcrypt-hashed password authentication.
16- AuthTypePassword AuthType = "password"
17 // AuthTypeToken uses JWT token-based authentication.15 // AuthTypeToken uses JWT token-based authentication.
18 AuthTypeToken AuthType = "token"16 AuthTypeToken AuthType = "token"
19 )17 )
M internal/usecase/auth/service.go +5 -44
@@ -3,7 +3,6 @@ package auth
33
44 import (
55 "context"
6- "crypto/subtle"
76 "errors"
87 "fmt"
98 "time"
@@ -11,7 +10,6 @@ import (
1110 "github.com/bnema/zerowrap"
1211 "github.com/golang-jwt/jwt/v5"
1312 "github.com/google/uuid"
14- "golang.org/x/crypto/bcrypt"
1513
1614 "github.com/bnema/gordon/internal/boundaries/out"
1715 "github.com/bnema/gordon/internal/domain"
@@ -20,8 +18,6 @@ import (
2018 const (
2119 // TokenIssuer is the issuer claim for generated tokens.
2220 TokenIssuer = "gordon-registry"
23- // DefaultBcryptCost is the default cost for bcrypt hashing.
24- DefaultBcryptCost = 12
2521 // MaxAccessTokenLifetime is the maximum lifetime for ephemeral access tokens.
2622 // Tokens with this lifetime or less skip store validation.
2723 MaxAccessTokenLifetime = 5 * time.Minute
@@ -38,12 +34,11 @@ const (
3834
3935 // Config holds the authentication configuration.
4036 type Config struct {
41- Enabled bool
42- AuthType domain.AuthType
43- Username string
44- PasswordHash string // bcrypt hash for password auth
45- TokenSecret []byte // signing secret for token auth
46- TokenExpiry time.Duration // default token expiry (0 = never)
37+ Enabled bool
38+ AuthType domain.AuthType
39+ Username string
40+ TokenSecret []byte // signing secret for token auth
41+ TokenExpiry time.Duration // default token expiry (0 = never)
4742 }
4843
4944 // Service implements the AuthService interface.
@@ -72,31 +67,6 @@ func (s *Service) IsEnabled() bool {
7267 return s.config.Enabled
7368 }
7469
75-// ValidatePassword checks if the username and password are valid.
76-func (s *Service) ValidatePassword(ctx context.Context, username, password string) bool {
77- ctx = zerowrap.CtxWithFields(ctx, map[string]any{
78- zerowrap.FieldLayer: "usecase",
79- zerowrap.FieldUseCase: "ValidatePassword",
80- "username": username,
81- })
82- log := zerowrap.FromCtx(ctx)
83-
84- // Constant-time username comparison
85- usernameMatch := subtle.ConstantTimeCompare([]byte(username), []byte(s.config.Username)) == 1
86-
87- // Bcrypt comparison (already constant-time)
88- err := bcrypt.CompareHashAndPassword([]byte(s.config.PasswordHash), []byte(password))
89- passwordMatch := err == nil
90-
91- if !usernameMatch || !passwordMatch {
92- log.Debug().Bool("username_match", usernameMatch).Msg("password validation failed")
93- return false
94- }
95-
96- log.Debug().Msg("password validation successful")
97- return true
98-}
99-
10070 // ValidateToken validates a JWT token and returns its claims.
10171 func (s *Service) ValidateToken(ctx context.Context, tokenString string) (*domain.TokenClaims, error) {
10272 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
@@ -560,15 +530,6 @@ func (s *Service) ExtendToken(ctx context.Context, tokenString string) (string,
560530 return newTokenString, nil
561531 }
562532
563-// GeneratePasswordHash generates a bcrypt hash for a password.
564-func (s *Service) GeneratePasswordHash(password string) (string, error) {
565- hash, err := bcrypt.GenerateFromPassword([]byte(password), DefaultBcryptCost)
566- if err != nil {
567- return "", err
568- }
569- return string(hash), nil
570-}
571-
572533 // getStringClaim safely extracts a string claim from JWT claims.
573534 func getStringClaim(claims jwt.MapClaims, key string) string {
574535 if val, ok := claims[key].(string); ok {
@@ -3,7 +3,6 @@ package auth
3 3
4 import (4 import (
5 "context"5 "context"
6- "crypto/subtle"
7 "errors"6 "errors"
8 "fmt"7 "fmt"
9 "time"8 "time"
@@ -11,7 +10,6 @@ import (
11 "github.com/bnema/zerowrap"10 "github.com/bnema/zerowrap"
12 "github.com/golang-jwt/jwt/v5"11 "github.com/golang-jwt/jwt/v5"
13 "github.com/google/uuid"12 "github.com/google/uuid"
14- "golang.org/x/crypto/bcrypt"
15 13
16 "github.com/bnema/gordon/internal/boundaries/out"14 "github.com/bnema/gordon/internal/boundaries/out"
17 "github.com/bnema/gordon/internal/domain"15 "github.com/bnema/gordon/internal/domain"
@@ -20,8 +18,6 @@ import (
20 const (18 const (
21 // TokenIssuer is the issuer claim for generated tokens.19 // TokenIssuer is the issuer claim for generated tokens.
22 TokenIssuer = "gordon-registry"20 TokenIssuer = "gordon-registry"
23- // DefaultBcryptCost is the default cost for bcrypt hashing.
24- DefaultBcryptCost = 12
25 // MaxAccessTokenLifetime is the maximum lifetime for ephemeral access tokens.21 // MaxAccessTokenLifetime is the maximum lifetime for ephemeral access tokens.
26 // Tokens with this lifetime or less skip store validation.22 // Tokens with this lifetime or less skip store validation.
27 MaxAccessTokenLifetime = 5 * time.Minute23 MaxAccessTokenLifetime = 5 * time.Minute
@@ -38,12 +34,11 @@ const (
38 34
39 // Config holds the authentication configuration.35 // Config holds the authentication configuration.
40 type Config struct {36 type Config struct {
41- Enabled bool37+ Enabled bool
42- AuthType domain.AuthType38+ AuthType domain.AuthType
43- Username string39+ Username string
44- PasswordHash string // bcrypt hash for password auth40+ TokenSecret []byte // signing secret for token auth
45- TokenSecret []byte // signing secret for token auth41+ TokenExpiry time.Duration // default token expiry (0 = never)
46- TokenExpiry time.Duration // default token expiry (0 = never)
47 }42 }
48 43
49 // Service implements the AuthService interface.44 // Service implements the AuthService interface.
@@ -72,31 +67,6 @@ func (s *Service) IsEnabled() bool {
72 return s.config.Enabled67 return s.config.Enabled
73 }68 }
74 69
75-// ValidatePassword checks if the username and password are valid.
76-func (s *Service) ValidatePassword(ctx context.Context, username, password string) bool {
77- ctx = zerowrap.CtxWithFields(ctx, map[string]any{
78- zerowrap.FieldLayer: "usecase",
79- zerowrap.FieldUseCase: "ValidatePassword",
80- "username": username,
81- })
82- log := zerowrap.FromCtx(ctx)
83-
84- // Constant-time username comparison
85- usernameMatch := subtle.ConstantTimeCompare([]byte(username), []byte(s.config.Username)) == 1
86-
87- // Bcrypt comparison (already constant-time)
88- err := bcrypt.CompareHashAndPassword([]byte(s.config.PasswordHash), []byte(password))
89- passwordMatch := err == nil
90-
91- if !usernameMatch || !passwordMatch {
92- log.Debug().Bool("username_match", usernameMatch).Msg("password validation failed")
93- return false
94- }
95-
96- log.Debug().Msg("password validation successful")
97- return true
98-}
99-
100 // ValidateToken validates a JWT token and returns its claims.70 // ValidateToken validates a JWT token and returns its claims.
101 func (s *Service) ValidateToken(ctx context.Context, tokenString string) (*domain.TokenClaims, error) {71 func (s *Service) ValidateToken(ctx context.Context, tokenString string) (*domain.TokenClaims, error) {
102 ctx = zerowrap.CtxWithFields(ctx, map[string]any{72 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
@@ -560,15 +530,6 @@ func (s *Service) ExtendToken(ctx context.Context, tokenString string) (string,
560 return newTokenString, nil530 return newTokenString, nil
561 }531 }
562 532
563-// GeneratePasswordHash generates a bcrypt hash for a password.
564-func (s *Service) GeneratePasswordHash(password string) (string, error) {
565- hash, err := bcrypt.GenerateFromPassword([]byte(password), DefaultBcryptCost)
566- if err != nil {
567- return "", err
568- }
569- return string(hash), nil
570-}
571-
572 // getStringClaim safely extracts a string claim from JWT claims.533 // getStringClaim safely extracts a string claim from JWT claims.
573 func getStringClaim(claims jwt.MapClaims, key string) string {534 func getStringClaim(claims jwt.MapClaims, key string) string {
574 if val, ok := claims[key].(string); ok {535 if val, ok := claims[key].(string); ok {
M internal/usecase/auth/service_test.go +0 -86
@@ -11,7 +11,6 @@ import (
1111 "github.com/stretchr/testify/assert"
1212 "github.com/stretchr/testify/mock"
1313 "github.com/stretchr/testify/require"
14- "golang.org/x/crypto/bcrypt"
1514
1615 "github.com/bnema/gordon/internal/boundaries/out/mocks"
1716 "github.com/bnema/gordon/internal/domain"
@@ -26,7 +25,6 @@ func TestService_GetAuthType(t *testing.T) {
2625 name string
2726 authType domain.AuthType
2827 }{
29- {"password type", domain.AuthTypePassword},
3028 {"token type", domain.AuthTypeToken},
3129 }
3230
@@ -55,53 +53,6 @@ func TestService_IsEnabled(t *testing.T) {
5553 }
5654 }
5755
58-func TestService_ValidatePassword_Success(t *testing.T) {
59- password := "testpassword123"
60- hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
61- require.NoError(t, err)
62-
63- svc := NewService(Config{
64- Enabled: true,
65- AuthType: domain.AuthTypePassword,
66- Username: "testuser",
67- PasswordHash: string(hash),
68- }, nil, zerowrap.Default())
69-
70- ctx := testContext()
71- assert.True(t, svc.ValidatePassword(ctx, "testuser", password))
72-}
73-
74-func TestService_ValidatePassword_WrongPassword(t *testing.T) {
75- hash, err := bcrypt.GenerateFromPassword([]byte("correctpassword"), bcrypt.DefaultCost)
76- require.NoError(t, err)
77-
78- svc := NewService(Config{
79- Enabled: true,
80- AuthType: domain.AuthTypePassword,
81- Username: "testuser",
82- PasswordHash: string(hash),
83- }, nil, zerowrap.Default())
84-
85- ctx := testContext()
86- assert.False(t, svc.ValidatePassword(ctx, "testuser", "wrongpassword"))
87-}
88-
89-func TestService_ValidatePassword_WrongUsername(t *testing.T) {
90- password := "testpassword123"
91- hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
92- require.NoError(t, err)
93-
94- svc := NewService(Config{
95- Enabled: true,
96- AuthType: domain.AuthTypePassword,
97- Username: "testuser",
98- PasswordHash: string(hash),
99- }, nil, zerowrap.Default())
100-
101- ctx := testContext()
102- assert.False(t, svc.ValidatePassword(ctx, "wronguser", password))
103-}
104-
10556 func TestService_GenerateToken_Success(t *testing.T) {
10657 tokenStore := mocks.NewMockTokenStore(t)
10758
@@ -720,43 +671,6 @@ func TestService_ListTokens(t *testing.T) {
720671 assert.True(t, tokens[1].ExpiresAt.IsZero(), "CI bot token should never expire")
721672 }
722673
723-func TestService_GeneratePasswordHash(t *testing.T) {
724- svc := NewService(Config{}, nil, zerowrap.Default())
725-
726- password := "securePassword123!"
727- hash, err := svc.GeneratePasswordHash(password)
728-
729- assert.NoError(t, err)
730- assert.NotEmpty(t, hash)
731- assert.NotEqual(t, password, hash, "hash should not equal plain password")
732-
733- // Verify the hash works with bcrypt
734- err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
735- assert.NoError(t, err)
736-
737- // Verify wrong password fails
738- err = bcrypt.CompareHashAndPassword([]byte(hash), []byte("wrongpassword"))
739- assert.Error(t, err)
740-}
741-
742-func TestService_GeneratePasswordHash_DifferentHashesForSamePassword(t *testing.T) {
743- svc := NewService(Config{}, nil, zerowrap.Default())
744-
745- password := "testpassword"
746- hash1, err := svc.GeneratePasswordHash(password)
747- require.NoError(t, err)
748-
749- hash2, err := svc.GeneratePasswordHash(password)
750- require.NoError(t, err)
751-
752- // Bcrypt should generate different hashes each time due to salt
753- assert.NotEqual(t, hash1, hash2, "bcrypt should generate unique hashes")
754-
755- // But both should validate correctly
756- assert.NoError(t, bcrypt.CompareHashAndPassword([]byte(hash1), []byte(password)))
757- assert.NoError(t, bcrypt.CompareHashAndPassword([]byte(hash2), []byte(password)))
758-}
759-
760674 func TestService_GenerateToken_HasNbfClaim(t *testing.T) {
761675 // Test that generated tokens include the nbf (not-before) claim
762676 // This is a security requirement per SEC-MED-006
@@ -11,7 +11,6 @@ import (
11 "github.com/stretchr/testify/assert"11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/mock"12 "github.com/stretchr/testify/mock"
13 "github.com/stretchr/testify/require"13 "github.com/stretchr/testify/require"
14- "golang.org/x/crypto/bcrypt"
15 14
16 "github.com/bnema/gordon/internal/boundaries/out/mocks"15 "github.com/bnema/gordon/internal/boundaries/out/mocks"
17 "github.com/bnema/gordon/internal/domain"16 "github.com/bnema/gordon/internal/domain"
@@ -26,7 +25,6 @@ func TestService_GetAuthType(t *testing.T) {
26 name string25 name string
27 authType domain.AuthType26 authType domain.AuthType
28 }{27 }{
29- {"password type", domain.AuthTypePassword},
30 {"token type", domain.AuthTypeToken},28 {"token type", domain.AuthTypeToken},
31 }29 }
32 30
@@ -55,53 +53,6 @@ func TestService_IsEnabled(t *testing.T) {
55 }53 }
56 }54 }
57 55
58-func TestService_ValidatePassword_Success(t *testing.T) {
59- password := "testpassword123"
60- hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
61- require.NoError(t, err)
62-
63- svc := NewService(Config{
64- Enabled: true,
65- AuthType: domain.AuthTypePassword,
66- Username: "testuser",
67- PasswordHash: string(hash),
68- }, nil, zerowrap.Default())
69-
70- ctx := testContext()
71- assert.True(t, svc.ValidatePassword(ctx, "testuser", password))
72-}
73-
74-func TestService_ValidatePassword_WrongPassword(t *testing.T) {
75- hash, err := bcrypt.GenerateFromPassword([]byte("correctpassword"), bcrypt.DefaultCost)
76- require.NoError(t, err)
77-
78- svc := NewService(Config{
79- Enabled: true,
80- AuthType: domain.AuthTypePassword,
81- Username: "testuser",
82- PasswordHash: string(hash),
83- }, nil, zerowrap.Default())
84-
85- ctx := testContext()
86- assert.False(t, svc.ValidatePassword(ctx, "testuser", "wrongpassword"))
87-}
88-
89-func TestService_ValidatePassword_WrongUsername(t *testing.T) {
90- password := "testpassword123"
91- hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
92- require.NoError(t, err)
93-
94- svc := NewService(Config{
95- Enabled: true,
96- AuthType: domain.AuthTypePassword,
97- Username: "testuser",
98- PasswordHash: string(hash),
99- }, nil, zerowrap.Default())
100-
101- ctx := testContext()
102- assert.False(t, svc.ValidatePassword(ctx, "wronguser", password))
103-}
104-
105 func TestService_GenerateToken_Success(t *testing.T) {56 func TestService_GenerateToken_Success(t *testing.T) {
106 tokenStore := mocks.NewMockTokenStore(t)57 tokenStore := mocks.NewMockTokenStore(t)
107 58
@@ -720,43 +671,6 @@ func TestService_ListTokens(t *testing.T) {
720 assert.True(t, tokens[1].ExpiresAt.IsZero(), "CI bot token should never expire")671 assert.True(t, tokens[1].ExpiresAt.IsZero(), "CI bot token should never expire")
721 }672 }
722 673
723-func TestService_GeneratePasswordHash(t *testing.T) {
724- svc := NewService(Config{}, nil, zerowrap.Default())
725-
726- password := "securePassword123!"
727- hash, err := svc.GeneratePasswordHash(password)
728-
729- assert.NoError(t, err)
730- assert.NotEmpty(t, hash)
731- assert.NotEqual(t, password, hash, "hash should not equal plain password")
732-
733- // Verify the hash works with bcrypt
734- err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
735- assert.NoError(t, err)
736-
737- // Verify wrong password fails
738- err = bcrypt.CompareHashAndPassword([]byte(hash), []byte("wrongpassword"))
739- assert.Error(t, err)
740-}
741-
742-func TestService_GeneratePasswordHash_DifferentHashesForSamePassword(t *testing.T) {
743- svc := NewService(Config{}, nil, zerowrap.Default())
744-
745- password := "testpassword"
746- hash1, err := svc.GeneratePasswordHash(password)
747- require.NoError(t, err)
748-
749- hash2, err := svc.GeneratePasswordHash(password)
750- require.NoError(t, err)
751-
752- // Bcrypt should generate different hashes each time due to salt
753- assert.NotEqual(t, hash1, hash2, "bcrypt should generate unique hashes")
754-
755- // But both should validate correctly
756- assert.NoError(t, bcrypt.CompareHashAndPassword([]byte(hash1), []byte(password)))
757- assert.NoError(t, bcrypt.CompareHashAndPassword([]byte(hash2), []byte(password)))
758-}
759-
760 func TestService_GenerateToken_HasNbfClaim(t *testing.T) {674 func TestService_GenerateToken_HasNbfClaim(t *testing.T) {
761 // Test that generated tokens include the nbf (not-before) claim675 // Test that generated tokens include the nbf (not-before) claim
762 // This is a security requirement per SEC-MED-006676 // This is a security requirement per SEC-MED-006

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close