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 +4 -4
- data/CHANGELOG.md +37 -0
- data/lib/vert/auth/jwt_authenticatable.rb +180 -0
- data/lib/vert/configuration.rb +2 -0
- data/lib/vert/version.rb +1 -1
- data/lib/vert.rb +3 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: efcc19b5c1872506cb9d75e59020db09984c94d24cd690709c26db3c4f966194
|
|
4
|
+
data.tar.gz: a21f57805634a5f6fbb14bfcf6513c92382309d2ee004f73c955e997a7f7b932
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/vert/configuration.rb
CHANGED
|
@@ -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
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.
|
|
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:
|
|
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:
|
|
241
|
+
rubygems_version: 3.6.2
|
|
241
242
|
specification_version: 4
|
|
242
243
|
summary: Generic core library for Rails microservices
|
|
243
244
|
test_files: []
|