vert-core 1.0.12 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ae6a0dbd4ab8e8531cae0d3ac0bab155efcdc3b05d7deeebc1d8960296e0cff
4
- data.tar.gz: 573c33470d90dd165f78434919fef156bc55f05e955991e624ba2aae8357ff43
3
+ metadata.gz: efcc19b5c1872506cb9d75e59020db09984c94d24cd690709c26db3c4f966194
4
+ data.tar.gz: a21f57805634a5f6fbb14bfcf6513c92382309d2ee004f73c955e997a7f7b932
5
5
  SHA512:
6
- metadata.gz: 382265549bfa3ae6806f5fba101cc7ff59765c428b1cb0553f13164c2b6dbcd1d114f95f24649b4bd6213604a063eda5728c08b5e2da8998a4adfa4951f83589
7
- data.tar.gz: e43a3dd92ab7900d34baff3cf7ac831d3604a0cc5c583f8fcba1ce39c6b56175afb48d26b9cd65b1294d6928eda8553eb9faaafd49e6dc7de22fe800071c7b0a
6
+ metadata.gz: 8fe2af573b8f87f8ee20c65944a9aa1fc74c0f3ceab41ae355d627d41d294d1c90e195fd77c0ae2a2813d6bbdf8638ac6c5f724beee917b0d21e1527eb75c54e
7
+ data.tar.gz: 709adf6c940017b3cf322a35d810cedff4fe73126c5e3483668592cb21cfffb830d0889b09ddd791fd95c2bdbc7508cad129b4d67bfe3adbb68caf49fc5176e2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.13] - 2026-05-24
4
+
5
+ ### Added
6
+
7
+ - `Vert::Auth::JwtAuthenticatable`: nova concern de autenticação JWT Bearer que estabelece o contexto multi-tenant via `Vert::Current` lendo **exclusivamente** os claims do JWT (regra inviolável 2 do projeto — nunca confiar em parâmetros do cliente).
8
+ - Valida o header `X-Tenant-ID` (defesa em profundidade): se presente, precisa ser igual ao `tenant_id` do JWT, caso contrário responde **403 Forbidden** e dispara o hook `on_tenant_mismatch` (override-able por serviço para gravar em `audit_logs`).
9
+ - Header ausente → JWT manda silenciosamente.
10
+ - Hooks override-áveis: `jwt_secret`, `jwt_algorithm`, `tenant_header_name`, `on_tenant_mismatch`, `on_jwt_invalid`, `current_jwt_user`.
11
+ - `skip_jwt_authentication` para opt-out em endpoints públicos (ex: `/auth/sign_in`, `/health`).
12
+ - Opt-in via `Vert.config.enable_jwt_auth = true` no initializer.
13
+ - `Configuration#enable_jwt_auth`: nova flag (default `false`).
14
+
15
+ ### Migration guide
16
+
17
+ ```ruby
18
+ # config/initializers/vert.rb
19
+ Vert.configure { |c| c.enable_jwt_auth = true }
20
+
21
+ # app/controllers/api/base_controller.rb
22
+ class Api::BaseController < ApplicationController
23
+ include Vert::Auth::JwtAuthenticatable
24
+
25
+ # opcional: gravar mismatch em audit_logs
26
+ def on_tenant_mismatch(jwt_tenant_id:, header_tenant_id:, user_id:)
27
+ super
28
+ AuditLog.create!(
29
+ event_type: "security.tenant_mismatch",
30
+ user_id: user_id,
31
+ payload: { jwt: jwt_tenant_id, header: header_tenant_id,
32
+ ip: request.remote_ip, path: request.fullpath }
33
+ )
34
+ end
35
+ end
36
+ ```
37
+
38
+ Corrige fragmentação dos 5 patterns de auth JWT distribuídos pelos 23 serviços do monorepo (alguns aceitavam `X-Tenant-ID` como fallback ou — pior — como fonte primária, sobrescrevendo o JWT).
39
+
3
40
  ## [1.0.7] - 2026-03-21
4
41
 
5
42
  ### Added
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vert
4
+ module Auth
5
+ # JwtAuthenticatable
6
+ # ------------------
7
+ # Concern para controllers Rails que autenticam via JWT Bearer e estabelecem
8
+ # contexto multi-tenant através de Vert::Current.
9
+ #
10
+ # Política de segurança (regra invioláv. 2 do projeto):
11
+ # - tenant_id, company_id e user_id SÃO LIDOS APENAS DO JWT.
12
+ # - O header `X-Tenant-ID`, quando presente, é tratado como defesa em
13
+ # profundidade: precisa ser igual ao `tenant_id` do JWT, senão a request
14
+ # é rejeitada com 403 (potencial tentativa cross-tenant).
15
+ # - Se o header estiver ausente, o JWT manda silenciosamente.
16
+ #
17
+ # Uso típico:
18
+ #
19
+ # # config/initializers/vert.rb
20
+ # Vert.configure { |c| c.enable_jwt_auth = true }
21
+ #
22
+ # # app/controllers/api/base_controller.rb
23
+ # class Api::BaseController < ApplicationController
24
+ # include Vert::Auth::JwtAuthenticatable
25
+ # end
26
+ #
27
+ # Opt-out por controller:
28
+ #
29
+ # class Api::PublicController < Api::BaseController
30
+ # skip_jwt_authentication
31
+ # end
32
+ #
33
+ # Hooks override-áveis (por serviço):
34
+ #
35
+ # - `jwt_secret` → default `ENV["JWT_SECRET"]`
36
+ # - `jwt_algorithm` → default `ENV.fetch("JWT_ALGORITHM", "HS256")`
37
+ # - `tenant_header_name` → default `"X-Tenant-ID"`
38
+ # - `on_tenant_mismatch` → default só faz `Rails.logger.warn`. Cada
39
+ # serviço pode override para gravar em
40
+ # `audit_logs` / `security_events`.
41
+ # - `on_jwt_invalid` → default `Rails.logger.warn`.
42
+ # - `current_jwt_user` → default `nil`. Serviços que tenham model
43
+ # `User` podem retornar o registro para uso
44
+ # em controllers.
45
+ module JwtAuthenticatable
46
+ extend ActiveSupport::Concern
47
+
48
+ class Error < StandardError; end
49
+ class MissingTokenError < Error; end
50
+ class InvalidTokenError < Error; end
51
+ class TenantMismatchError < Error; end
52
+
53
+ included do
54
+ before_action :authenticate_jwt!
55
+ end
56
+
57
+ class_methods do
58
+ # Permite que um controller filho opt-out do filtro
59
+ # (ex: endpoints públicos como /auth/sign_in, /health).
60
+ def skip_jwt_authentication(**options)
61
+ skip_before_action :authenticate_jwt!, **options
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def authenticate_jwt!
68
+ token = extract_bearer_token
69
+ if token.blank?
70
+ render_jwt_error(:unauthorized, "Missing authorization token")
71
+ return
72
+ end
73
+
74
+ @jwt_payload = decode_jwt(token)
75
+ if @jwt_payload.nil?
76
+ render_jwt_error(:unauthorized, "Invalid or expired token")
77
+ return
78
+ end
79
+
80
+ jwt_tenant_id = @jwt_payload["tenant_id"]
81
+ header_tenant = request.headers[tenant_header_name].presence
82
+
83
+ if header_tenant.present? && jwt_tenant_id.present? &&
84
+ header_tenant.to_s != jwt_tenant_id.to_s
85
+ on_tenant_mismatch(
86
+ jwt_tenant_id: jwt_tenant_id,
87
+ header_tenant_id: header_tenant,
88
+ user_id: @jwt_payload["sub"]
89
+ )
90
+ render_jwt_error(:forbidden, "Tenant header mismatch")
91
+ return
92
+ end
93
+
94
+ Vert::Current.set_context(
95
+ tenant_id: jwt_tenant_id,
96
+ company_id: @jwt_payload["company_id"],
97
+ user_id: @jwt_payload["sub"],
98
+ request_id: request.request_id
99
+ )
100
+
101
+ true
102
+ end
103
+
104
+ def extract_bearer_token
105
+ header = request.headers["Authorization"]
106
+ return nil unless header.is_a?(String) && header.start_with?("Bearer ")
107
+
108
+ header.split(" ", 2).last.to_s.strip.presence
109
+ end
110
+
111
+ def decode_jwt(token)
112
+ require "jwt" unless defined?(JWT)
113
+
114
+ ::JWT.decode(
115
+ token,
116
+ jwt_secret,
117
+ true,
118
+ algorithm: jwt_algorithm
119
+ ).first
120
+ rescue ::JWT::DecodeError, ::JWT::ExpiredSignature, ::JWT::VerificationError => e
121
+ on_jwt_invalid(error: e)
122
+ nil
123
+ rescue LoadError
124
+ Rails.logger.error("[Vert::Auth] gem 'jwt' não carregada — adicione `gem \"jwt\"` ao Gemfile do serviço")
125
+ nil
126
+ end
127
+
128
+ def jwt_secret
129
+ ENV.fetch("JWT_SECRET")
130
+ end
131
+
132
+ def jwt_algorithm
133
+ ENV.fetch("JWT_ALGORITHM", "HS256")
134
+ end
135
+
136
+ def tenant_header_name
137
+ "X-Tenant-ID"
138
+ end
139
+
140
+ # Hook: chamado quando header X-Tenant-ID diverge do JWT.
141
+ # Override em ApplicationController para gravar em audit_logs:
142
+ #
143
+ # def on_tenant_mismatch(jwt_tenant_id:, header_tenant_id:, user_id:)
144
+ # super
145
+ # AuditLog.create!(
146
+ # event_type: "security.tenant_mismatch",
147
+ # user_id: user_id,
148
+ # payload: { jwt_tenant_id:, header_tenant_id:, ip: request.remote_ip, path: request.fullpath }
149
+ # )
150
+ # end
151
+ def on_tenant_mismatch(jwt_tenant_id:, header_tenant_id:, user_id:)
152
+ Rails.logger.warn(
153
+ "[Vert::Auth] Tenant mismatch: " \
154
+ "user=#{user_id} jwt=#{jwt_tenant_id} header=#{header_tenant_id} " \
155
+ "ip=#{request.remote_ip} path=#{request.fullpath}"
156
+ )
157
+ end
158
+
159
+ def on_jwt_invalid(error:)
160
+ Rails.logger.warn("[Vert::Auth] JWT decode error: #{error.class}: #{error.message}")
161
+ end
162
+
163
+ def render_jwt_error(status, message)
164
+ render json: { error: message }, status: status
165
+ end
166
+
167
+ # Payload bruto do JWT (claims), disponível nos controllers
168
+ # após `authenticate_jwt!`.
169
+ def current_jwt_payload
170
+ @jwt_payload
171
+ end
172
+
173
+ # Override no serviço para retornar o registro User correspondente
174
+ # ao `sub` do JWT.
175
+ def current_jwt_user
176
+ nil
177
+ end
178
+ end
179
+ end
180
+ end
@@ -16,6 +16,7 @@ module Vert
16
16
  :enable_outbox,
17
17
  :enable_health,
18
18
  :enable_authorization,
19
+ :enable_jwt_auth,
19
20
  :enable_multi_tenant,
20
21
  :enable_auditable,
21
22
  :enable_soft_deletable,
@@ -43,6 +44,7 @@ module Vert
43
44
  @enable_outbox = false
44
45
  @enable_health = true
45
46
  @enable_authorization = false
47
+ @enable_jwt_auth = false
46
48
  @enable_multi_tenant = false
47
49
  @enable_auditable = false
48
50
  @enable_soft_deletable = false
data/lib/vert/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vert
4
- VERSION = "1.0.12"
4
+ VERSION = "1.0.13"
5
5
  end
data/lib/vert.rb CHANGED
@@ -36,6 +36,9 @@ require_relative "vert/authorization/dynamic_policy"
36
36
  require_relative "vert/authorization/policy_finder"
37
37
  require_relative "vert/authorization/controller_methods"
38
38
 
39
+ # Auth
40
+ require_relative "vert/auth/jwt_authenticatable"
41
+
39
42
  # Railtie
40
43
  require_relative "vert/railtie" if defined?(Rails::Railtie)
41
44
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vert-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.12
4
+ version: 1.0.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vert Team
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
10
+ date: 2026-05-24 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -180,6 +180,7 @@ files:
180
180
  - README.md
181
181
  - SECURITY_AND_QUALITY_AUDIT.md
182
182
  - lib/vert.rb
183
+ - lib/vert/auth/jwt_authenticatable.rb
183
184
  - lib/vert/authorization/controller_methods.rb
184
185
  - lib/vert/authorization/dynamic_policy.rb
185
186
  - lib/vert/authorization/permission_resolver.rb
@@ -237,7 +238,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
237
238
  - !ruby/object:Gem::Version
238
239
  version: '0'
239
240
  requirements: []
240
- rubygems_version: 4.0.8
241
+ rubygems_version: 3.6.2
241
242
  specification_version: 4
242
243
  summary: Generic core library for Rails microservices
243
244
  test_files: []