jwt_auth_cognito 0.1.1 → 1.0.0.pre.beta.1
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/.rubocop.yml +78 -0
- data/BITBUCKET-DEPLOYMENT.md +290 -0
- data/CHANGELOG.md +65 -0
- data/CLAUDE.md +189 -9
- data/Gemfile +5 -5
- data/README.md +147 -1
- data/Rakefile +108 -5
- data/VERSIONING.md +244 -0
- data/bitbucket-pipelines.yml +273 -0
- data/jwt_auth_cognito.gemspec +42 -39
- data/lib/generators/jwt_auth_cognito/install_generator.rb +25 -25
- data/lib/jwt_auth_cognito/api_key_validator.rb +79 -0
- data/lib/jwt_auth_cognito/configuration.rb +38 -21
- data/lib/jwt_auth_cognito/error_utils.rb +110 -0
- data/lib/jwt_auth_cognito/jwks_service.rb +46 -50
- data/lib/jwt_auth_cognito/jwt_validator.rb +319 -92
- data/lib/jwt_auth_cognito/railtie.rb +3 -3
- data/lib/jwt_auth_cognito/redis_service.rb +90 -51
- data/lib/jwt_auth_cognito/ssm_service.rb +109 -0
- data/lib/jwt_auth_cognito/token_blacklist_service.rb +10 -12
- data/lib/jwt_auth_cognito/user_data_service.rb +332 -0
- data/lib/jwt_auth_cognito/version.rb +2 -2
- data/lib/jwt_auth_cognito.rb +42 -10
- data/lib/tasks/jwt_auth_cognito.rake +69 -70
- metadata +63 -26
@@ -5,50 +5,50 @@ require 'rails/generators'
|
|
5
5
|
module JwtAuthCognito
|
6
6
|
module Generators
|
7
7
|
class InstallGenerator < Rails::Generators::Base
|
8
|
-
desc
|
9
|
-
|
8
|
+
desc 'Genera el archivo de configuración inicial para jwt_auth_cognito'
|
9
|
+
|
10
10
|
source_root File.expand_path('templates', __dir__)
|
11
|
-
|
11
|
+
|
12
12
|
def create_initializer
|
13
|
-
say
|
13
|
+
say 'Creando archivo de configuración jwt_auth_cognito...'
|
14
14
|
template 'jwt_auth_cognito.rb.erb', 'config/initializers/jwt_auth_cognito.rb'
|
15
|
-
say
|
15
|
+
say '✓ Archivo de configuración creado en config/initializers/jwt_auth_cognito.rb', :green
|
16
16
|
end
|
17
17
|
|
18
18
|
def create_env_example
|
19
19
|
if File.exist?('.env.example')
|
20
20
|
append_to_file '.env.example', env_variables
|
21
|
-
say
|
21
|
+
say '✓ Variables de entorno agregadas a .env.example', :green
|
22
22
|
else
|
23
23
|
create_file '.env.example', env_variables
|
24
|
-
say
|
24
|
+
say '✓ Archivo .env.example creado con variables de entorno', :green
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
28
|
def show_next_steps
|
29
|
-
say "\n
|
30
|
-
say
|
31
|
-
say
|
29
|
+
say "\n#{'=' * 60}", :blue
|
30
|
+
say '🎉 ¡Configuración de jwt_auth_cognito completada!', :green
|
31
|
+
say '=' * 60, :blue
|
32
32
|
say "\n📋 PRÓXIMOS PASOS:", :yellow
|
33
33
|
say "\n1. Configura las variables de entorno:", :cyan
|
34
|
-
say
|
35
|
-
say
|
36
|
-
|
34
|
+
say ' - Copia .env.example a .env (si usas dotenv)'
|
35
|
+
say ' - Configura las variables de AWS Cognito y Redis'
|
36
|
+
|
37
37
|
say "\n2. Variables de entorno requeridas:", :cyan
|
38
|
-
say
|
39
|
-
say
|
40
|
-
say
|
41
|
-
|
38
|
+
say ' COGNITO_USER_POOL_ID=us-east-1_abcdef123'
|
39
|
+
say ' COGNITO_REGION=us-east-1'
|
40
|
+
say ' COGNITO_CLIENT_ID=tu-client-id'
|
41
|
+
|
42
42
|
say "\n3. Variables de entorno opcionales:", :cyan
|
43
|
-
say
|
44
|
-
say
|
45
|
-
say
|
46
|
-
|
43
|
+
say ' COGNITO_CLIENT_SECRET=tu-client-secret (para mayor seguridad)'
|
44
|
+
say ' REDIS_TLS=true (para Redis con TLS)'
|
45
|
+
say ' REDIS_CA_CERT_PATH=/ruta/a/certificados'
|
46
|
+
|
47
47
|
say "\n4. Reinicia tu aplicación Rails", :cyan
|
48
|
-
|
48
|
+
|
49
49
|
say "\n📖 Documentación completa:", :yellow
|
50
|
-
say
|
51
|
-
say "\n
|
50
|
+
say ' Revisa el README.md de la gema para ejemplos de uso'
|
51
|
+
say "\n#{'=' * 60}", :blue
|
52
52
|
end
|
53
53
|
|
54
54
|
private
|
@@ -85,4 +85,4 @@ module JwtAuthCognito
|
|
85
85
|
end
|
86
86
|
end
|
87
87
|
end
|
88
|
-
end
|
88
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module JwtAuthCognito
|
6
|
+
class ApiKeyValidator
|
7
|
+
def initialize(config)
|
8
|
+
@config = config
|
9
|
+
@redis_service = RedisService.new(config)
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate_api_key(api_key)
|
13
|
+
# Validate basic format (64 hex characters)
|
14
|
+
return { valid: false, error: 'Invalid API key format' } unless api_key&.match?(/\A[a-fA-F0-9]{64}\z/)
|
15
|
+
|
16
|
+
begin
|
17
|
+
key_data = @redis_service.get("api-keys:#{api_key}")
|
18
|
+
return { valid: false, error: 'API key not found' } unless key_data
|
19
|
+
|
20
|
+
parsed = JSON.parse(key_data)
|
21
|
+
|
22
|
+
# Verify it's active
|
23
|
+
return { valid: false, error: 'API key is inactive' } unless parsed['isActive']
|
24
|
+
|
25
|
+
# Update last used (fire and forget for performance)
|
26
|
+
update_last_used(api_key, parsed)
|
27
|
+
|
28
|
+
{
|
29
|
+
valid: true,
|
30
|
+
key_data: {
|
31
|
+
name: parsed['name'],
|
32
|
+
permissions: parsed['permissions'],
|
33
|
+
app_id: parsed['appId'],
|
34
|
+
scope: parsed['scope'],
|
35
|
+
created_at: parsed['createdAt'],
|
36
|
+
last_used: parsed['lastUsed'],
|
37
|
+
is_active: parsed['isActive'],
|
38
|
+
metadata: parsed['metadata'] || {}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
rescue StandardError => e
|
42
|
+
puts "Error validating API key: #{e.message}"
|
43
|
+
{ valid: false, error: 'API key validation failed' }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def has_permission?(key_data, permission)
|
48
|
+
key_data[:permissions]&.include?(permission) || false
|
49
|
+
end
|
50
|
+
|
51
|
+
def system_api_key?(key_data)
|
52
|
+
key_data[:scope] == 'system'
|
53
|
+
end
|
54
|
+
|
55
|
+
def client_api_key?(key_data)
|
56
|
+
key_data[:scope] == 'client'
|
57
|
+
end
|
58
|
+
|
59
|
+
def can_access_app?(key_data, app_id)
|
60
|
+
# System API keys can access any app
|
61
|
+
return true if key_data[:scope] == 'system'
|
62
|
+
|
63
|
+
# App API keys can only access their specific app
|
64
|
+
context_app_id = key_data[:app_id] || key_data[:metadata]&.dig('appId')
|
65
|
+
context_app_id == app_id
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def update_last_used(api_key, key_data)
|
71
|
+
Thread.new do
|
72
|
+
key_data['lastUsed'] = (Time.now.to_f * 1000).to_i
|
73
|
+
@redis_service.set("api-keys:#{api_key}", key_data.to_json)
|
74
|
+
rescue StandardError => e
|
75
|
+
puts "Error updating last used timestamp: #{e.message}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -7,34 +7,40 @@ module JwtAuthCognito
|
|
7
7
|
:redis_ssl, :redis_timeout, :redis_connect_timeout, :redis_read_timeout,
|
8
8
|
:redis_ca_cert_path, :redis_ca_cert_name, :redis_verify_mode,
|
9
9
|
:redis_tls_min_version, :redis_tls_max_version,
|
10
|
-
:
|
10
|
+
:redis_ca_cert_ssm_path, :redis_ca_cert_ssm_name,
|
11
|
+
:jwks_cache_ttl, :validation_mode, :environment,
|
12
|
+
:enable_api_key_validation, :enable_user_data_retrieval
|
11
13
|
|
12
14
|
def initialize
|
13
|
-
@cognito_region = ENV['COGNITO_REGION'] || ENV['AWS_REGION'] ||
|
14
|
-
@cognito_user_pool_id = ENV
|
15
|
-
@cognito_client_id = ENV
|
16
|
-
@cognito_client_secret = ENV
|
17
|
-
|
15
|
+
@cognito_region = ENV['COGNITO_REGION'] || ENV['AWS_REGION'] || 'us-east-1'
|
16
|
+
@cognito_user_pool_id = ENV.fetch('COGNITO_USER_POOL_ID', nil)
|
17
|
+
@cognito_client_id = ENV.fetch('COGNITO_CLIENT_ID', nil)
|
18
|
+
@cognito_client_secret = ENV.fetch('COGNITO_CLIENT_SECRET', nil)
|
19
|
+
|
18
20
|
# Redis configuration with environment variables
|
19
|
-
@redis_host = ENV['REDIS_HOST'] ||
|
21
|
+
@redis_host = ENV['REDIS_HOST'] || 'localhost'
|
20
22
|
@redis_port = (ENV['REDIS_PORT'] || 6379).to_i
|
21
|
-
@redis_password = ENV
|
23
|
+
@redis_password = ENV.fetch('REDIS_PASSWORD', nil)
|
22
24
|
@redis_db = (ENV['REDIS_DB'] || 0).to_i
|
23
25
|
@redis_ssl = ENV['REDIS_TLS'] == 'true' || ENV['REDIS_SSL'] == 'true'
|
24
26
|
@redis_timeout = (ENV['REDIS_TIMEOUT'] || 5).to_i
|
25
27
|
@redis_connect_timeout = (ENV['REDIS_CONNECT_TIMEOUT'] || 10).to_i
|
26
28
|
@redis_read_timeout = (ENV['REDIS_READ_TIMEOUT'] || 10).to_i
|
27
|
-
|
29
|
+
|
28
30
|
# TLS specific configuration
|
29
|
-
@redis_ca_cert_path = ENV
|
30
|
-
@redis_ca_cert_name = ENV
|
31
|
+
@redis_ca_cert_path = ENV.fetch('REDIS_CA_CERT_PATH', nil)
|
32
|
+
@redis_ca_cert_name = ENV.fetch('REDIS_CA_CERT_NAME', nil)
|
33
|
+
@redis_ca_cert_ssm_path = ENV.fetch('REDIS_CA_CERT_SSM_PATH', nil)
|
34
|
+
@redis_ca_cert_ssm_name = ENV.fetch('REDIS_CA_CERT_SSM_NAME', nil)
|
31
35
|
@redis_verify_mode = ENV['REDIS_VERIFY_MODE'] || 'peer'
|
32
36
|
@redis_tls_min_version = ENV['REDIS_TLS_MIN_VERSION'] || 'TLSv1.2'
|
33
37
|
@redis_tls_max_version = ENV['REDIS_TLS_MAX_VERSION'] || 'TLSv1.3'
|
34
|
-
|
38
|
+
|
35
39
|
@jwks_cache_ttl = (ENV['JWKS_CACHE_TTL'] || 3600).to_i # 1 hour
|
36
40
|
@environment = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || ENV['NODE_ENV'] || 'development'
|
37
41
|
@validation_mode = production? ? :secure : :basic
|
42
|
+
@enable_api_key_validation = ENV['ENABLE_API_KEY_VALIDATION'] == 'true'
|
43
|
+
@enable_user_data_retrieval = ENV['ENABLE_USER_DATA_RETRIEVAL'] == 'true'
|
38
44
|
end
|
39
45
|
|
40
46
|
def production?
|
@@ -54,9 +60,9 @@ module JwtAuthCognito
|
|
54
60
|
end
|
55
61
|
|
56
62
|
def validate!
|
57
|
-
raise ConfigurationError,
|
58
|
-
raise ConfigurationError,
|
59
|
-
raise ConfigurationError,
|
63
|
+
raise ConfigurationError, 'cognito_user_pool_id is required' unless cognito_user_pool_id
|
64
|
+
raise ConfigurationError, 'cognito_region is required' unless cognito_region
|
65
|
+
raise ConfigurationError, 'redis_host is required' unless redis_host
|
60
66
|
end
|
61
67
|
|
62
68
|
def has_client_secret?
|
@@ -64,20 +70,31 @@ module JwtAuthCognito
|
|
64
70
|
end
|
65
71
|
|
66
72
|
def calculate_secret_hash(identifier)
|
67
|
-
return
|
68
|
-
return
|
73
|
+
return '' unless has_client_secret?
|
74
|
+
return '' unless cognito_client_id
|
69
75
|
|
70
76
|
message = identifier + cognito_client_id
|
71
|
-
|
77
|
+
|
72
78
|
require 'openssl'
|
73
79
|
require 'base64'
|
74
|
-
|
80
|
+
|
75
81
|
begin
|
76
82
|
hmac = OpenSSL::HMAC.digest('SHA256', cognito_client_secret, message)
|
77
83
|
Base64.encode64(hmac).strip
|
78
|
-
rescue => e
|
84
|
+
rescue StandardError => e
|
79
85
|
raise ConfigurationError, "Error calculating secret hash: #{e.message}"
|
80
86
|
end
|
81
87
|
end
|
88
|
+
|
89
|
+
def user_data_config
|
90
|
+
{
|
91
|
+
enable_user_data_retrieval: enable_user_data_retrieval,
|
92
|
+
include_applications: ENV['INCLUDE_APPLICATIONS'] != 'false',
|
93
|
+
include_organizations: ENV['INCLUDE_ORGANIZATIONS'] != 'false',
|
94
|
+
include_roles: ENV['INCLUDE_ROLES'] != 'false',
|
95
|
+
include_effective_permissions: ENV['INCLUDE_EFFECTIVE_PERMISSIONS'] == 'true',
|
96
|
+
cache_timeout: (ENV['USER_DATA_CACHE_TIMEOUT'] || 300).to_i
|
97
|
+
}
|
98
|
+
end
|
82
99
|
end
|
83
|
-
end
|
100
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JwtAuthCognito
|
4
|
+
module ErrorUtils
|
5
|
+
JWT_ERROR_MESSAGES = {
|
6
|
+
'TOKEN_EXPIRED' => 'Token has expired',
|
7
|
+
'INVALID_TOKEN' => 'Invalid token format',
|
8
|
+
'TOKEN_NOT_ACTIVE' => 'Token not active yet',
|
9
|
+
'INVALID_SIGNATURE' => 'Invalid token signature',
|
10
|
+
'SIGNATURE_VERIFICATION_FAILED' => 'Token signature verification failed',
|
11
|
+
'INVALID_AUDIENCE' => 'Invalid token audience',
|
12
|
+
'INVALID_ISSUER' => 'Invalid token issuer',
|
13
|
+
'VALIDATION_TIMEOUT' => 'Token validation timeout',
|
14
|
+
'TOKEN_REVOKED' => 'Token has been revoked',
|
15
|
+
'API_KEY_INVALID' => 'Invalid API key',
|
16
|
+
'API_KEY_EXPIRED' => 'API key has expired',
|
17
|
+
'INITIALIZATION_FAILED' => 'Service initialization failed',
|
18
|
+
'REDIS_CONNECTION_FAILED' => 'Redis connection failed',
|
19
|
+
'USER_DATA_RETRIEVAL_FAILED' => 'User data retrieval failed'
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
def self.extract_error_details(error, context = nil)
|
23
|
+
message = 'Unknown error occurred'
|
24
|
+
code = nil
|
25
|
+
|
26
|
+
case error
|
27
|
+
when JwtAuthCognito::TokenExpiredError
|
28
|
+
message = 'Token has expired'
|
29
|
+
code = 'TOKEN_EXPIRED'
|
30
|
+
when JwtAuthCognito::TokenNotActiveError
|
31
|
+
message = 'Token not active yet'
|
32
|
+
code = 'TOKEN_NOT_ACTIVE'
|
33
|
+
when JwtAuthCognito::TokenFormatError
|
34
|
+
message = 'Invalid token format'
|
35
|
+
code = 'INVALID_TOKEN'
|
36
|
+
when JwtAuthCognito::TokenRevokedError
|
37
|
+
message = 'Token has been revoked'
|
38
|
+
code = 'TOKEN_REVOKED'
|
39
|
+
when JwtAuthCognito::JWKSError
|
40
|
+
message = 'Invalid token signature'
|
41
|
+
code = 'INVALID_SIGNATURE'
|
42
|
+
when JwtAuthCognito::RedisConnectionError
|
43
|
+
message = 'Redis connection failed'
|
44
|
+
code = 'REDIS_CONNECTION_FAILED'
|
45
|
+
when StandardError
|
46
|
+
message = error.message
|
47
|
+
|
48
|
+
# Check message content for specific error patterns
|
49
|
+
case message
|
50
|
+
when /expired/i
|
51
|
+
message = 'Token has expired'
|
52
|
+
code = 'TOKEN_EXPIRED'
|
53
|
+
when /invalid.*signature/i, /signature.*verification.*failed/i
|
54
|
+
message = 'Invalid token signature'
|
55
|
+
code = 'INVALID_SIGNATURE'
|
56
|
+
when /invalid.*audience/i, /aud/
|
57
|
+
message = 'Invalid token audience'
|
58
|
+
code = 'INVALID_AUDIENCE'
|
59
|
+
when /invalid.*issuer/i, /iss/
|
60
|
+
message = 'Invalid token issuer'
|
61
|
+
code = 'INVALID_ISSUER'
|
62
|
+
when /not.*active/i, /nbf/
|
63
|
+
message = 'Token not active yet'
|
64
|
+
code = 'TOKEN_NOT_ACTIVE'
|
65
|
+
when /invalid.*format/i, /malformed/i
|
66
|
+
message = 'Invalid token format'
|
67
|
+
code = 'INVALID_TOKEN'
|
68
|
+
end
|
69
|
+
when String
|
70
|
+
message = error
|
71
|
+
else
|
72
|
+
message = error.to_s
|
73
|
+
end
|
74
|
+
|
75
|
+
{
|
76
|
+
message: message,
|
77
|
+
code: code,
|
78
|
+
context: context
|
79
|
+
}.compact
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.log_error(error, context = nil)
|
83
|
+
details = extract_error_details(error, context)
|
84
|
+
log_message = if details[:context]
|
85
|
+
"#{details[:context]}: #{details[:message]}"
|
86
|
+
else
|
87
|
+
details[:message]
|
88
|
+
end
|
89
|
+
|
90
|
+
log_message += " (#{details[:code]})" if details[:code]
|
91
|
+
|
92
|
+
puts "ERROR: #{log_message}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.get_user_friendly_error_message(error)
|
96
|
+
details = extract_error_details(error)
|
97
|
+
details[:message]
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.format_validation_error(error, context = nil)
|
101
|
+
details = extract_error_details(error, context)
|
102
|
+
|
103
|
+
{
|
104
|
+
valid: false,
|
105
|
+
error: details[:message],
|
106
|
+
error_code: details[:code]
|
107
|
+
}.compact
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -1,10 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
3
|
+
require 'net/http'
|
4
|
+
require 'json'
|
5
|
+
require 'jwt'
|
6
|
+
require 'openssl'
|
7
|
+
require 'base64'
|
8
8
|
|
9
9
|
module JwtAuthCognito
|
10
10
|
class JwksService
|
@@ -16,35 +16,35 @@ module JwtAuthCognito
|
|
16
16
|
|
17
17
|
def validate_token_with_jwks(token)
|
18
18
|
@config.validate!
|
19
|
-
|
19
|
+
|
20
20
|
header = JWT.decode(token, nil, false).last
|
21
|
-
kid = header[
|
22
|
-
|
23
|
-
raise ValidationError,
|
24
|
-
|
21
|
+
kid = header['kid']
|
22
|
+
|
23
|
+
raise ValidationError, 'Token missing key ID (kid)' unless kid
|
24
|
+
|
25
25
|
public_key = get_public_key(kid)
|
26
26
|
decoded_token = JWT.decode(
|
27
27
|
token,
|
28
28
|
public_key,
|
29
29
|
true,
|
30
30
|
{
|
31
|
-
algorithm:
|
31
|
+
algorithm: 'RS256',
|
32
32
|
iss: @config.cognito_issuer,
|
33
33
|
verify_iss: true,
|
34
34
|
aud: @config.cognito_client_id,
|
35
35
|
verify_aud: @config.cognito_client_id ? true : false
|
36
36
|
}
|
37
37
|
)
|
38
|
-
|
38
|
+
|
39
39
|
payload = decoded_token.first
|
40
40
|
validate_token_claims(payload)
|
41
|
-
|
41
|
+
|
42
42
|
{
|
43
43
|
valid: true,
|
44
44
|
payload: payload,
|
45
|
-
sub: payload[
|
46
|
-
username: payload[
|
47
|
-
token_use: payload[
|
45
|
+
sub: payload['sub'],
|
46
|
+
username: payload['cognito:username'] || payload['username'],
|
47
|
+
token_use: payload['token_use']
|
48
48
|
}
|
49
49
|
rescue JWT::DecodeError => e
|
50
50
|
{ valid: false, error: "JWT decode error: #{e.message}" }
|
@@ -58,35 +58,33 @@ module JwtAuthCognito
|
|
58
58
|
|
59
59
|
def get_public_key(kid)
|
60
60
|
# Check cache first
|
61
|
-
if @cache[kid] && cache_valid?(kid)
|
62
|
-
return @cache[kid]
|
63
|
-
end
|
61
|
+
return @cache[kid] if @cache[kid] && cache_valid?(kid)
|
64
62
|
|
65
63
|
# Fetch JWKS
|
66
64
|
jwks = fetch_jwks
|
67
|
-
key_data = jwks[
|
68
|
-
|
69
|
-
raise ValidationError,
|
70
|
-
|
65
|
+
key_data = jwks['keys'].find { |key| key['kid'] == kid }
|
66
|
+
|
67
|
+
raise ValidationError, 'Key ID not found in JWKS' unless key_data
|
68
|
+
|
71
69
|
# Convert JWK to PEM
|
72
70
|
public_key = jwk_to_pem(key_data)
|
73
|
-
|
71
|
+
|
74
72
|
# Cache the key
|
75
73
|
@cache[kid] = public_key
|
76
74
|
@cache_timestamps[kid] = Time.now
|
77
|
-
|
75
|
+
|
78
76
|
public_key
|
79
77
|
end
|
80
78
|
|
81
79
|
def fetch_jwks
|
82
80
|
uri = URI(@config.jwks_url)
|
83
|
-
|
84
|
-
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme ==
|
81
|
+
|
82
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 10, read_timeout: 10) do |http|
|
85
83
|
request = Net::HTTP::Get.new(uri)
|
86
84
|
response = http.request(request)
|
87
|
-
|
88
|
-
raise ValidationError, "Failed to fetch JWKS: #{response.code}" unless response.code ==
|
89
|
-
|
85
|
+
|
86
|
+
raise ValidationError, "Failed to fetch JWKS: #{response.code}" unless response.code == '200'
|
87
|
+
|
90
88
|
JSON.parse(response.body)
|
91
89
|
end
|
92
90
|
rescue JSON::ParserError => e
|
@@ -97,45 +95,43 @@ module JwtAuthCognito
|
|
97
95
|
|
98
96
|
def jwk_to_pem(key_data)
|
99
97
|
# Convert JWK RSA key to PEM format
|
100
|
-
n = base64url_decode(key_data[
|
101
|
-
e = base64url_decode(key_data[
|
102
|
-
|
98
|
+
n = base64url_decode(key_data['n'])
|
99
|
+
e = base64url_decode(key_data['e'])
|
100
|
+
|
103
101
|
key = OpenSSL::PKey::RSA.new
|
104
102
|
key.n = OpenSSL::BN.new(n, 2)
|
105
103
|
key.e = OpenSSL::BN.new(e, 2)
|
106
|
-
|
104
|
+
|
107
105
|
key
|
108
106
|
end
|
109
107
|
|
110
108
|
def base64url_decode(str)
|
111
|
-
str +=
|
112
|
-
Base64.decode64(str.tr(
|
109
|
+
str += '=' * (4 - str.length.modulo(4))
|
110
|
+
Base64.decode64(str.tr('-_', '+/'))
|
113
111
|
end
|
114
112
|
|
115
113
|
def cache_valid?(kid)
|
116
114
|
return false unless @cache_timestamps[kid]
|
117
|
-
|
115
|
+
|
118
116
|
Time.now - @cache_timestamps[kid] < @config.jwks_cache_ttl
|
119
117
|
end
|
120
118
|
|
121
119
|
def validate_token_claims(payload)
|
122
120
|
now = Time.now.to_i
|
123
|
-
|
121
|
+
|
124
122
|
# Check expiration
|
125
|
-
raise ValidationError,
|
126
|
-
|
123
|
+
raise ValidationError, 'Token has expired' if payload['exp'] && payload['exp'] < now
|
124
|
+
|
127
125
|
# Check not before
|
128
|
-
raise ValidationError,
|
129
|
-
|
126
|
+
raise ValidationError, 'Token not yet valid' if payload['nbf'] && payload['nbf'] > now
|
127
|
+
|
130
128
|
# Check issued at (allow some clock skew)
|
131
|
-
if payload[
|
132
|
-
|
133
|
-
end
|
134
|
-
|
129
|
+
raise ValidationError, 'Token issued in the future' if payload['iat'] && payload['iat'] > now + 300
|
130
|
+
|
135
131
|
# Check token use
|
136
|
-
|
137
|
-
|
138
|
-
|
132
|
+
return if %w[access id].include?(payload['token_use'])
|
133
|
+
|
134
|
+
raise ValidationError, 'Invalid token_use claim'
|
139
135
|
end
|
140
136
|
end
|
141
|
-
end
|
137
|
+
end
|