diff --git a/internal/api/api.go b/internal/api/api.go index 63e7ee1f3..ed77282d7 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -348,6 +348,7 @@ func NewAPIWithVersion(globalConfig *conf.GlobalConfiguration, db *storage.Conne r.Use(api.oauthServer.LoadOAuthServerClient) r.Get("/", api.oauthServer.OAuthServerClientGet) r.Delete("/", api.oauthServer.OAuthServerClientDelete) + r.Post("/regenerate_secret", api.oauthServer.OAuthServerClientRegenerateSecret) }) }) }) diff --git a/internal/api/api_test.go b/internal/api/api_test.go index c205503bf..9111617c8 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -62,7 +62,7 @@ func TestOAuthServerDisabledByDefault(t *testing.T) { // OAuth server should be disabled by default require.False(t, api.config.OAuthServer.Enabled) - + // OAuth server instance should not be initialized when disabled require.Nil(t, api.oauthServer) } @@ -78,7 +78,7 @@ func TestOAuthServerCanBeEnabled(t *testing.T) { // OAuth server should be enabled require.True(t, api.config.OAuthServer.Enabled) - + // OAuth server instance should be initialized when enabled require.NotNil(t, api.oauthServer) } diff --git a/internal/api/oauthserver/handlers.go b/internal/api/oauthserver/handlers.go index 7f81586e7..534929b95 100644 --- a/internal/api/oauthserver/handlers.go +++ b/internal/api/oauthserver/handlers.go @@ -190,6 +190,27 @@ func (s *Server) OAuthServerClientDelete(w http.ResponseWriter, r *http.Request) return nil } +// OAuthServerClientRegenerateSecret handles POST /admin/oauth/clients/{client_id}/regenerate_secret +func (s *Server) OAuthServerClientRegenerateSecret(w http.ResponseWriter, r *http.Request) error { + ctx := r.Context() + client := shared.GetOAuthServerClient(ctx) + + // Only confidential clients can have their secrets regenerated + if !client.IsConfidential() { + return apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "Cannot regenerate secret for public client") + } + + updatedClient, plaintextSecret, err := s.regenerateOAuthServerClientSecret(ctx, client.ID) + if err != nil { + return apierrors.NewInternalServerError("Error regenerating OAuth client secret").WithInternalError(err) + } + + response := oauthServerClientToResponse(updatedClient) + response.ClientSecret = plaintextSecret + + return shared.SendJSON(w, http.StatusOK, response) +} + // OAuthServerClientList handles GET /admin/oauth/clients func (s *Server) OAuthServerClientList(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() diff --git a/internal/api/oauthserver/service.go b/internal/api/oauthserver/service.go index f940d5f01..88f1ab8f0 100644 --- a/internal/api/oauthserver/service.go +++ b/internal/api/oauthserver/service.go @@ -141,7 +141,6 @@ func validateRedirectURI(uri string) error { return nil } - // generateClientSecret generates a secure random client secret func generateClientSecret() string { b := make([]byte, 32) @@ -248,3 +247,34 @@ func (s *Server) deleteOAuthServerClient(ctx context.Context, clientID uuid.UUID return nil } + +// regenerateOAuthServerClientSecret regenerates a client secret for confidential clients +func (s *Server) regenerateOAuthServerClientSecret(ctx context.Context, clientID uuid.UUID) (*models.OAuthServerClient, string, error) { + db := s.db.WithContext(ctx) + + client, err := models.FindOAuthServerClientByID(db, clientID) + if err != nil { + return nil, "", err + } + + // Only confidential clients can have their secrets regenerated + if !client.IsConfidential() { + return nil, "", errors.New("cannot regenerate secret for public client") + } + + // Generate new client secret + plaintextSecret := generateClientSecret() + hash, err := hashClientSecret(plaintextSecret) + if err != nil { + return nil, "", errors.Wrap(err, "failed to hash client secret") + } + + // Update client with new secret hash + client.ClientSecretHash = hash + + if err := models.UpdateOAuthServerClient(db, client); err != nil { + return nil, "", errors.Wrap(err, "failed to update OAuth client with new secret") + } + + return client, plaintextSecret, nil +}