jwt_auth_cognito 1.0.0.pre.beta.9 → 1.0.0.pre.beta.11

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: 5ec0550d58a587e152aa1a4c41cfce13691bb016f0b12e4525a671bc7fda153b
4
- data.tar.gz: 77a05aeb998b6bdeb8df90ffc93a78a3ad1c3565aeda43d4bdcf069713bb6295
3
+ metadata.gz: e8069ccdfaed845402cd9053a2db66b2bc74359649de17ca2ce7ad010a939722
4
+ data.tar.gz: 3ba1441f8016d8e2d5b1bca7913fd7700f0ae51c8470ab1e6f06a2e1a9640697
5
5
  SHA512:
6
- metadata.gz: 5a5f6a72cfebb9e1f805ba53b3561c544b2afc18f993af0759da596c02401e6bda2725a58837e2b5db1b818a5f44ae730c8b9057aa0a481530e8f3c035b70ae1
7
- data.tar.gz: 21e84bde860ae1d70dce2ecf5d32a40bcd6fb18af8eb815de9c6079de7996a9a488871c3b91eca0fd649c41d900863c155948e9c96caff507c75b3de3b9110b1
6
+ metadata.gz: 897a6f309d17ba4f312e5b9b86a8daf4ab76050a6b5cd17543eaaf728c3ba242bc1ec34a8efb25f17395095cccf5225bf918850fb4f373ba0132311c3c0e84c7
7
+ data.tar.gz: 785494e60e28e09a5b3343eaf9b2dfc0d9a70343cf8f56ddd81768b172c0753696a3b03df84f24510ba50687d9d7cb6af8a3bce72fad2331e88bc195d9dc73b5
data/CHANGELOG.md CHANGED
@@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.0-beta.11] - 2025-01-23
11
+
12
+ ### Improved
13
+
14
+ - **Documentation Enhancement**: Comprehensive documentation of token structure and validation responses
15
+ - Added detailed structure documentation for decoded token hash with all JWT claims
16
+ - Documented complete validation result structure including `api_key_data` field
17
+ - Added comprehensive `api_key_data` structure documentation with all fields explained
18
+ - Included real-world examples of Access Token and ID Token decoded structures in Ruby format
19
+ - Added comparison table showing differences between Access and ID tokens
20
+ - Documented API Key scopes (`system`, `app`, `client`) with usage patterns
21
+ - Added 4 practical examples: basic validation, scope-based authorization, metadata usage, and usage tracking
22
+ - Included documentation for enriched validation results with user data fields
23
+ - Enhanced error handling documentation with common error messages
24
+
25
+ ### Documentation
26
+
27
+ - **Token Structure**: Complete guide to JWT claims (standard, optional, and Cognito-specific)
28
+ - **Response Structure**: Detailed documentation of validation response hash format
29
+ - **API Key Details**: Full explanation of API key data including permissions, scope, and metadata
30
+ - **Practical Examples**: Real-world Ruby code examples for common use cases
31
+ - **Ruby Idioms**: Documentation adapted to Ruby conventions and patterns
32
+
33
+ ## [1.0.0-beta.10] - 2025-01-23
34
+
35
+ ### Fixed
36
+
37
+ - **Audience Validation**: Fixed overly strict audience validation for AWS Cognito access tokens
38
+ - Access tokens from Cognito typically don't include 'aud' claim, only ID tokens do
39
+ - Modified JWKS validation to only enforce audience checking for ID tokens (`token_use: 'id'`)
40
+ - Access tokens (`token_use: 'access'`) now skip audience validation as per AWS Cognito standards
41
+ - Resolves "Invalid audience. Expected [client_id], received <none>" error for access tokens
42
+ - Maintains proper security validation for ID tokens
43
+
44
+ ### Improved
45
+
46
+ - **JWT Standards Compliance**: Enhanced compatibility with AWS Cognito token specifications
47
+ - Pre-decodes tokens to determine type before applying validation rules
48
+ - Follows AWS Cognito best practices for token type-specific validation
49
+ - Maintains backward compatibility with existing ID token validation
50
+
10
51
  ## [1.0.0-beta.9] - 2025-01-22
11
52
 
12
53
  ### Fixed
data/CLAUDE.md CHANGED
@@ -304,7 +304,7 @@ JWKS_CACHE_TTL=3600 # 1 hour
304
304
 
305
305
  ## Version Compatibility
306
306
 
307
- ### ✅ **Updated January 2025 - Version 1.0.0-beta.6**
307
+ ### ✅ **Updated January 2025 - Version 1.0.0-beta.10**
308
308
 
309
309
  **Stable production-ready beta with complete pipeline compatibility**
310
310
 
data/README.md CHANGED
@@ -519,6 +519,459 @@ rescue JwtAuthCognito::BlacklistError => e
519
519
  end
520
520
  ```
521
521
 
522
+ ## 📋 Estructura del Token Decodificado
523
+
524
+ Después de una validación exitosa, el hash de resultado contiene los claims del JWT con la siguiente estructura:
525
+
526
+ ### Claims Principales
527
+
528
+ ```ruby
529
+ # Resultado de validación
530
+ {
531
+ # ============ Claims Obligatorios JWT Standard (RFC 7519) ============
532
+
533
+ # Subject - Identificador único del usuario (UUID)
534
+ sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
535
+
536
+ # Audience - Cliente/aplicación para la cual el token fue emitido
537
+ aud: "1234567890abcdefghijklmnop",
538
+
539
+ # Issuer - URL del User Pool de Cognito que emitió el token
540
+ iss: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX",
541
+
542
+ # Expiration Time - Timestamp Unix (segundos) de expiración
543
+ exp: 1735689600, # Equivale a 2025-01-01 00:00:00 UTC
544
+
545
+ # Issued At - Timestamp Unix (segundos) de emisión
546
+ iat: 1735603200, # Equivale a 2024-12-31 00:00:00 UTC
547
+
548
+ # Token Use - Tipo de token Cognito
549
+ token_use: "access", # 'access' para Access Tokens, 'id' para ID Tokens
550
+
551
+ # ============ Claims Opcionales de Usuario ============
552
+
553
+ # Email del usuario (opcional)
554
+ email: "usuario@ejemplo.com",
555
+
556
+ # Verificación de email (opcional)
557
+ email_verified: true,
558
+
559
+ # Número de teléfono (opcional)
560
+ phone_number: "+12025551234",
561
+
562
+ # Verificación de teléfono (opcional)
563
+ phone_number_verified: true,
564
+
565
+ # Nombre de usuario (opcional)
566
+ username: "john.doe",
567
+
568
+ # ============ Claims Específicos de AWS Cognito ============
569
+
570
+ # Username en formato Cognito (opcional)
571
+ "cognito:username": "john.doe",
572
+
573
+ # Grupos de Cognito (opcional)
574
+ "cognito:groups": ["admin", "users"],
575
+
576
+ # Scope OAuth2 - Solo en Access Tokens
577
+ scope: "openid email profile",
578
+
579
+ # Authentication Time - Timestamp Unix (opcional)
580
+ auth_time: 1735603200,
581
+
582
+ # JWT ID - Identificador único del token (opcional)
583
+ jti: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
584
+
585
+ # ============ Custom Attributes ============
586
+ # Cualquier atributo custom definido en Cognito
587
+ "custom:tenant_id": "company-123"
588
+ }
589
+ ```
590
+
591
+ ### Ejemplos Reales de Tokens Decodificados
592
+
593
+ #### Access Token (token_use: "access")
594
+
595
+ ```ruby
596
+ result = validator.validate_access_token(token)
597
+
598
+ # Resultado:
599
+ {
600
+ valid: true,
601
+ sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
602
+ iss: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX",
603
+ client_id: "1234567890abcdefghijklmnop",
604
+ aud: "1234567890abcdefghijklmnop",
605
+ token_use: "access",
606
+ scope: "openid email profile",
607
+ auth_time: 1735603200,
608
+ exp: 1735689600,
609
+ iat: 1735603200,
610
+ jti: "xyz-789-def-456",
611
+ username: "john.doe",
612
+ "cognito:username": "john.doe",
613
+ "cognito:groups": ["admin", "users"]
614
+ }
615
+ ```
616
+
617
+ #### ID Token (token_use: "id")
618
+
619
+ ```ruby
620
+ result = validator.validate_id_token(token)
621
+
622
+ # Resultado:
623
+ {
624
+ valid: true,
625
+ sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
626
+ iss: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX",
627
+ aud: "1234567890abcdefghijklmnop",
628
+ token_use: "id",
629
+ auth_time: 1735603200,
630
+ exp: 1735689600,
631
+ iat: 1735603200,
632
+ email: "john.doe@example.com",
633
+ email_verified: true,
634
+ phone_number: "+12025551234",
635
+ phone_number_verified: true,
636
+ "cognito:username": "john.doe",
637
+ "cognito:groups": ["admin"],
638
+ "custom:department": "engineering",
639
+ "custom:employee_id": "EMP-12345"
640
+ }
641
+ ```
642
+
643
+ ### Uso del Token Decodificado en tu Aplicación
644
+
645
+ ```ruby
646
+ result = validator.validate_token(token)
647
+
648
+ if result[:valid]
649
+ # ✅ Identificación del usuario
650
+ puts "User ID: #{result[:sub]}"
651
+ puts "Username: #{result['cognito:username'] || result[:username]}"
652
+
653
+ # ✅ Información de contacto
654
+ if result[:email]
655
+ puts "Email: #{result[:email]}"
656
+ puts "Email verificado: #{result[:email_verified]}"
657
+ end
658
+
659
+ # ✅ Autorización basada en grupos
660
+ if result['cognito:groups']&.include?('admin')
661
+ puts "Usuario es administrador"
662
+ end
663
+
664
+ # ✅ Validación de tiempo
665
+ expires_at = Time.at(result[:exp])
666
+ puts "Token expira: #{expires_at}"
667
+
668
+ issued_at = Time.at(result[:iat])
669
+ puts "Token emitido: #{issued_at}"
670
+
671
+ # ✅ Atributos custom
672
+ if result['custom:tenant_id']
673
+ puts "Tenant ID: #{result['custom:tenant_id']}"
674
+ end
675
+
676
+ # ✅ Scope OAuth2 (solo Access Tokens)
677
+ if result[:token_use] == 'access' && result[:scope]
678
+ scopes = result[:scope].split(' ')
679
+ puts "Permisos OAuth2: #{scopes}"
680
+ end
681
+ end
682
+ ```
683
+
684
+ ### Diferencias entre Access Token e ID Token
685
+
686
+ | Campo | Access Token | ID Token |
687
+ |-------|-------------|----------|
688
+ | **`token_use`** | `"access"` | `"id"` |
689
+ | **`scope`** | ✅ Incluido | ❌ No incluido |
690
+ | **`client_id`** | ✅ Incluido | ❌ No incluido |
691
+ | **`email`** | ❌ No incluido | ✅ Incluido |
692
+ | **`email_verified`** | ❌ No incluido | ✅ Incluido |
693
+ | **`phone_number`** | ❌ No incluido | ✅ Incluido |
694
+ | **Custom Attributes** | ❌ No incluido | ✅ Incluido |
695
+ | **Uso Principal** | Autorización en APIs | Información del usuario |
696
+
697
+ ### Notas Importantes
698
+
699
+ - **Claims opcionales**: La disponibilidad de campos como `email`, `phone_number`, y `custom:*` depende de tu configuración de Cognito
700
+ - **Token Use**: Usa `result[:token_use]` para determinar el tipo de token y qué campos esperar
701
+ - **Timestamps**: Los campos `exp`, `iat`, y `auth_time` están en formato Unix timestamp (segundos desde 1970-01-01)
702
+ - **Custom Attributes**: Los atributos custom de Cognito tienen el prefijo `custom:` en sus nombres
703
+ - **Grupos Cognito**: Los grupos se almacenan en el array `cognito:groups` cuando están configurados
704
+
705
+ ## 📦 Estructura de la Respuesta de Validación
706
+
707
+ ### Respuesta Básica de Validación
708
+
709
+ ```ruby
710
+ result = validator.validate_token(token)
711
+
712
+ # Estructura del resultado:
713
+ {
714
+ valid: true, # Boolean - Indica si el token es válido
715
+ sub: "user-id", # String - ID del usuario
716
+ username: "john.doe", # String - Nombre de usuario
717
+ token_use: "access", # String - Tipo de token
718
+ # ... más claims del token ...
719
+ error: nil # String - Mensaje de error (solo si valid: false)
720
+ }
721
+ ```
722
+
723
+ ### Respuesta con API Key
724
+
725
+ Cuando se valida con un API Key, el resultado incluye información adicional:
726
+
727
+ ```ruby
728
+ result = validator.validate(token, api_key: api_key)
729
+
730
+ # Resultado completo:
731
+ {
732
+ valid: true,
733
+ sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
734
+ username: "john.doe",
735
+ token_use: "access",
736
+ # ... otros claims del token ...
737
+
738
+ # Información del API Key
739
+ api_key_data: {
740
+ name: "production-api-key",
741
+ permissions: ["auth:access", "users:read"],
742
+ app_id: "my-application",
743
+ scope: "client",
744
+ created_at: 1735603200000,
745
+ last_used: 1735689600000,
746
+ is_active: true,
747
+ metadata: {
748
+ created_for: "Integration Team",
749
+ description: "API Key for production integration",
750
+ environment: "production"
751
+ }
752
+ }
753
+ }
754
+ ```
755
+
756
+ ### Estructura de api_key_data
757
+
758
+ ```ruby
759
+ {
760
+ # Nombre identificador del API Key
761
+ name: "production-api-key",
762
+
763
+ # Lista de permisos asignados
764
+ permissions: ["auth:access", "users:read"],
765
+
766
+ # ID de la aplicación asociada (opcional para scope 'system')
767
+ app_id: "my-application",
768
+
769
+ # Alcance del API Key
770
+ scope: "client", # Valores: 'app', 'system', 'client'
771
+
772
+ # Timestamp de creación (milisegundos)
773
+ created_at: 1735603200000,
774
+
775
+ # Timestamp del último uso (nil si nunca usado)
776
+ last_used: 1735689600000,
777
+
778
+ # Estado del API Key
779
+ is_active: true,
780
+
781
+ # Metadatos personalizados
782
+ metadata: {
783
+ created_for: "Integration Team",
784
+ environment: "production"
785
+ }
786
+ }
787
+ ```
788
+
789
+ ### Diferencias entre Scopes de API Keys
790
+
791
+ | Scope | Descripción | Restricciones | Uso Típico |
792
+ |-------|-------------|---------------|------------|
793
+ | **`system`** | Acceso transversal a todas las aplicaciones | Ninguna - acceso completo | Administración, integraciones de sistema |
794
+ | **`app`** | Acceso limitado a una aplicación específica | Solo puede acceder al `app_id` asociado | Integraciones de aplicaciones específicas |
795
+ | **`client`** | Acceso de cliente/frontend | Restricciones según permisos asignados | Aplicaciones frontend, móviles |
796
+
797
+ ### Ejemplos de Uso con API Key
798
+
799
+ #### 1. Validación Básica con API Key
800
+
801
+ ```ruby
802
+ result = validator.validate(token, api_key: api_key)
803
+
804
+ if result[:valid]
805
+ puts "✅ Token válido"
806
+ puts "Usuario: #{result[:username]}"
807
+
808
+ # Información del API Key
809
+ if result[:api_key_data]
810
+ key_data = result[:api_key_data]
811
+ puts "API Key: #{key_data[:name]}"
812
+ puts "Scope: #{key_data[:scope]}"
813
+ puts "Permisos: #{key_data[:permissions].join(', ')}"
814
+ puts "App ID: #{key_data[:app_id]}" if key_data[:app_id]
815
+ end
816
+ else
817
+ puts "❌ Token inválido: #{result[:error]}"
818
+ end
819
+ ```
820
+
821
+ #### 2. Autorización basada en API Key Scope
822
+
823
+ ```ruby
824
+ result = validator.validate(token, api_key: api_key)
825
+
826
+ if result[:valid] && result[:api_key_data]
827
+ key_data = result[:api_key_data]
828
+
829
+ # Verificar si tiene acceso system (transversal)
830
+ if key_data[:scope] == 'system'
831
+ puts "✅ Acceso system - puede acceder a todas las apps"
832
+ end
833
+
834
+ # Verificar si tiene acceso a app específica
835
+ if key_data[:scope] == 'app' && key_data[:app_id] == 'my-target-app'
836
+ puts "✅ Acceso autorizado a my-target-app"
837
+ end
838
+
839
+ # Verificar permisos específicos
840
+ if key_data[:permissions].include?('users:write')
841
+ puts "✅ Puede modificar usuarios"
842
+ end
843
+ end
844
+ ```
845
+
846
+ #### 3. Usar Metadata del API Key
847
+
848
+ ```ruby
849
+ result = validator.validate(token, api_key: api_key)
850
+
851
+ if result[:valid] && result[:api_key_data]
852
+ metadata = result[:api_key_data][:metadata]
853
+
854
+ # Acceder a información personalizada
855
+ puts "Creado para: #{metadata[:created_for]}"
856
+ puts "Descripción: #{metadata[:description]}"
857
+ puts "Ambiente: #{metadata[:environment]}"
858
+
859
+ # Control de acceso basado en ambiente
860
+ if metadata[:environment] == 'production'
861
+ Rails.logger.info "⚠️ API Key de producción - logging extra habilitado"
862
+ end
863
+ end
864
+ ```
865
+
866
+ #### 4. Tracking de Último Uso
867
+
868
+ ```ruby
869
+ result = validator.validate(token, api_key: api_key)
870
+
871
+ if result[:valid] && result[:api_key_data]
872
+ key_data = result[:api_key_data]
873
+
874
+ # Verificar última vez usado
875
+ if key_data[:last_used]
876
+ last_used_date = Time.at(key_data[:last_used] / 1000)
877
+ puts "Última vez usado: #{last_used_date}"
878
+
879
+ # Detectar API Keys inactivos (más de 30 días sin uso)
880
+ days_since_last_use = (Time.now - last_used_date) / (60 * 60 * 24)
881
+ if days_since_last_use > 30
882
+ puts "⚠️ API Key inactivo por más de 30 días"
883
+ end
884
+ else
885
+ puts "ℹ️ API Key nunca usado antes"
886
+ end
887
+
888
+ # Antigüedad del API Key
889
+ created_date = Time.at(key_data[:created_at] / 1000)
890
+ puts "Creado el: #{created_date}"
891
+ end
892
+ ```
893
+
894
+ ### Respuesta con Datos Enriquecidos
895
+
896
+ Cuando usas `validate_enriched()`, obtienes campos adicionales:
897
+
898
+ ```ruby
899
+ result = validator.validate_enriched(token, api_key)
900
+
901
+ # Resultado completo con datos enriquecidos:
902
+ {
903
+ valid: true,
904
+ sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
905
+ username: "john.doe",
906
+ email: "john.doe@example.com",
907
+ # ... otros campos del token ...
908
+
909
+ api_key_data: {
910
+ name: "production-api-key",
911
+ permissions: ["auth:access", "users:read"],
912
+ app_id: "my-application",
913
+ scope: "client",
914
+ created_at: 1735603200000,
915
+ last_used: 1735689600000,
916
+ is_active: true,
917
+ metadata: { environment: "production" }
918
+ },
919
+
920
+ user_permissions: {
921
+ "userId" => "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
922
+ "permissions" => {
923
+ "my-app" => {
924
+ "org-123" => {
925
+ "roles" => ["admin", "user"],
926
+ "effectivePermissions" => ["users:read", "users:write"],
927
+ "status" => "active"
928
+ }
929
+ }
930
+ }
931
+ },
932
+
933
+ user_organizations: [
934
+ {
935
+ "appId" => "my-app",
936
+ "organizationId" => "org-123",
937
+ "roles" => ["admin"],
938
+ "status" => "active",
939
+ "effectivePermissions" => ["users:read", "users:write"]
940
+ }
941
+ ],
942
+
943
+ applications: [
944
+ {
945
+ "appId" => "my-app",
946
+ "name" => "My Application",
947
+ "isActive" => true,
948
+ "createdAt" => 1735603200000,
949
+ "updatedAt" => 1735689600000
950
+ }
951
+ ]
952
+ }
953
+ ```
954
+
955
+ ### Manejo de Errores
956
+
957
+ Cuando la validación falla, obtienes un mensaje de error claro:
958
+
959
+ ```ruby
960
+ result = validator.validate_token(token)
961
+
962
+ unless result[:valid]
963
+ puts "❌ Error: #{result[:error]}"
964
+
965
+ # Ejemplos de errores comunes:
966
+ # - "Token has expired"
967
+ # - "Invalid token signature"
968
+ # - "Invalid audience"
969
+ # - "Token has been revoked"
970
+ # - "API key is inactive"
971
+ # - "No access to application"
972
+ end
973
+ ```
974
+
522
975
  ## Integración con Rails
523
976
 
524
977
  ### Ejemplo de Middleware
@@ -45,7 +45,7 @@ module JwtAuthCognito
45
45
  end
46
46
 
47
47
  def has_permission?(key_data, permission)
48
- key_data[:permissions]&.include?(permission) || false
48
+ PermissionChecker.permission_in_list?(permission, key_data[:permissions] || [])
49
49
  end
50
50
 
51
51
  def system_api_key?(key_data)
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module JwtAuthCognito
6
+ # Rails Concern that adds granular permission enforcement to controllers.
7
+ #
8
+ # Usage in ApplicationController (or any controller):
9
+ #
10
+ # include JwtAuthCognito::AuthorizationConcern
11
+ #
12
+ # # Provide the validator instance (required):
13
+ # def jwt_validator
14
+ # @jwt_validator ||= JwtAuthCognito::JwtValidator.new
15
+ # end
16
+ #
17
+ # # Provide the current user's Cognito sub (required):
18
+ # # Override jwt_user_id — typically populated by your auth before_action.
19
+ # def jwt_user_id
20
+ # @jwt_user_id ||= request.env['jwt.payload']&.dig('sub')
21
+ # end
22
+ #
23
+ # Then in any action or before_action:
24
+ #
25
+ # before_action -> { authorize_permission!('fleet:vehicles:read') }
26
+ #
27
+ # # OR require at least one of several permissions:
28
+ # before_action -> { authorize_any_permission!('fleet:read', 'fleet:vehicles:read') }
29
+ #
30
+ # appId and orgId are resolved from X-App-Id / X-Organization-Id headers,
31
+ # falling back to params[:appId] / params[:organizationId].
32
+ module AuthorizationConcern
33
+ extend ActiveSupport::Concern
34
+
35
+ included do
36
+ helper_method :current_user_permissions if respond_to?(:helper_method)
37
+ end
38
+
39
+ # Raises ForbiddenError unless the current user has ALL of the given permissions.
40
+ def authorize_permission!(*permissions)
41
+ permissions.flatten.each do |permission|
42
+ next if current_user_has_permission?(permission)
43
+
44
+ log_permission_denied(permissions)
45
+ raise JwtAuthCognito::ForbiddenError, 'Access denied'
46
+ end
47
+ end
48
+
49
+ # Raises ForbiddenError unless the current user has AT LEAST ONE of the given permissions.
50
+ def authorize_any_permission!(*permissions)
51
+ return if permissions.flatten.any? { |p| current_user_has_permission?(p) }
52
+
53
+ log_permission_denied(permissions)
54
+ raise JwtAuthCognito::ForbiddenError, 'Access denied'
55
+ end
56
+
57
+ # Returns the full list of effective permissions for the current user/app/org context.
58
+ def current_user_permissions
59
+ @current_user_permissions ||= fetch_current_user_permissions
60
+ end
61
+
62
+ private
63
+
64
+ def fetch_current_user_permissions
65
+ uid = jwt_user_id
66
+ aid = jwt_app_id
67
+ oid = jwt_org_id
68
+ return [] unless uid && aid && oid
69
+
70
+ jwt_validator.resolve_effective_permissions_for(uid, aid, oid) || []
71
+ rescue StandardError
72
+ []
73
+ end
74
+
75
+ def current_user_has_permission?(permission)
76
+ PermissionChecker.permission_in_list?(permission, current_user_permissions)
77
+ end
78
+
79
+ # Override in your ApplicationController to provide the Cognito sub.
80
+ # Typically populated by a JWT authentication before_action.
81
+ def jwt_user_id
82
+ @jwt_user_id
83
+ end
84
+
85
+ # Resolves appId from X-App-Id header → params[:appId].
86
+ def jwt_app_id
87
+ @jwt_app_id ||=
88
+ request.headers['X-App-Id'] ||
89
+ request.headers['x-app-id'] ||
90
+ params[:appId]
91
+ end
92
+
93
+ # Resolves orgId from X-Organization-Id header → params[:organizationId].
94
+ def jwt_org_id
95
+ @jwt_org_id ||=
96
+ request.headers['X-Organization-Id'] ||
97
+ request.headers['x-organization-id'] ||
98
+ params[:organizationId]
99
+ end
100
+
101
+ # Override in your ApplicationController to provide the JwtValidator instance.
102
+ def jwt_validator
103
+ raise NotImplementedError,
104
+ 'You must implement `jwt_validator` in your controller. ' \
105
+ 'Example: def jwt_validator; @jwt_validator ||= JwtAuthCognito::JwtValidator.new; end'
106
+ end
107
+
108
+ def log_permission_denied(permissions)
109
+ return unless defined?(Rails)
110
+
111
+ Rails.logger.warn(
112
+ "[PERMISSION_DENIED] user_id=#{jwt_user_id} " \
113
+ "permissions=#{Array(permissions).flatten.join(',')} " \
114
+ "app_id=#{jwt_app_id} org_id=#{jwt_org_id}"
115
+ )
116
+ end
117
+ end
118
+ end
@@ -23,6 +23,13 @@ module JwtAuthCognito
23
23
  raise ValidationError, 'Token missing key ID (kid)' unless kid
24
24
 
25
25
  public_key = get_public_key(kid)
26
+ # First decode to check token type before audience validation
27
+ payload_preview = JWT.decode(token, nil, false).first
28
+
29
+ # Only verify audience for ID tokens, not access tokens
30
+ # Access tokens from Cognito might not have 'aud' claim
31
+ should_verify_aud = @config.cognito_client_id && payload_preview['token_use'] == 'id'
32
+
26
33
  decoded_token = JWT.decode(
27
34
  token,
28
35
  public_key,
@@ -31,8 +38,8 @@ module JwtAuthCognito
31
38
  algorithm: 'RS256',
32
39
  iss: @config.cognito_issuer,
33
40
  verify_iss: true,
34
- aud: @config.cognito_client_id,
35
- verify_aud: @config.cognito_client_id ? true : false
41
+ aud: should_verify_aud ? @config.cognito_client_id : nil,
42
+ verify_aud: should_verify_aud
36
43
  }
37
44
  )
38
45
 
@@ -224,6 +224,52 @@ module JwtAuthCognito
224
224
  @config.has_client_secret?
225
225
  end
226
226
 
227
+ # ========== PERMISSION CHECKING ==========
228
+
229
+ # Returns true if the user has the given permission in the specified app/org context.
230
+ def has_permission?(user_id, app_id, org_id, permission)
231
+ permissions = resolve_effective_permissions_for(user_id, app_id, org_id)
232
+ return false unless permissions
233
+
234
+ PermissionChecker.permission_in_list?(permission, permissions)
235
+ end
236
+
237
+ # Bulk permission check. Returns a hash with :allowed, :denied, :has_all, :has_any.
238
+ def check_permissions(user_id, app_id, org_id, permissions_to_check)
239
+ effective = resolve_effective_permissions_for(user_id, app_id, org_id) || []
240
+
241
+ allowed = []
242
+ denied = []
243
+
244
+ Array(permissions_to_check).each do |perm|
245
+ if PermissionChecker.permission_in_list?(perm, effective)
246
+ allowed << perm
247
+ else
248
+ denied << perm
249
+ end
250
+ end
251
+
252
+ { allowed: allowed, denied: denied, has_all: denied.empty?, has_any: allowed.any? }
253
+ end
254
+
255
+ # Validates a Bearer token and checks a single permission in one call.
256
+ def has_permission_from_token?(token, app_id, org_id, permission)
257
+ result = validate_token(token)
258
+ return false unless result[:valid]
259
+
260
+ user_id = result[:payload]&.dig('sub')
261
+ return false unless user_id
262
+
263
+ has_permission?(user_id, app_id, org_id, permission)
264
+ end
265
+
266
+ # Exposes effective permissions via the user data service.
267
+ def resolve_effective_permissions_for(user_id, app_id, org_id)
268
+ return nil unless @user_data_service
269
+
270
+ @user_data_service.resolve_effective_permissions(user_id, app_id, org_id)
271
+ end
272
+
227
273
  def is_token_expired?(token)
228
274
  payload = decode_token(token)
229
275
  return true if payload.is_a?(Hash) && payload[:error]
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthCognito
4
+ module PermissionChecker
5
+ # Checks whether a permission string is satisfied by any entry in permission_list.
6
+ # Supports wildcard patterns:
7
+ # * — global wildcard (matches everything)
8
+ # module:* — prefix wildcard (matches module:action AND module:sub:action)
9
+ # module:submodule:* — narrow prefix wildcard
10
+ # *.action — suffix wildcard (matches last segment across any depth)
11
+ def self.permission_in_list?(permission, permission_list)
12
+ return false if permission_list.nil? || permission_list.empty?
13
+ return true if permission_list.include?(permission)
14
+ return true if permission_list.include?('*')
15
+
16
+ permission_list.each do |p|
17
+ if p.end_with?(':*')
18
+ prefix = p[0..-3]
19
+ return true if permission.start_with?("#{prefix}:")
20
+ end
21
+
22
+ if p.end_with?('.*')
23
+ prefix = p[0..-3]
24
+ return true if permission.start_with?("#{prefix}.")
25
+ end
26
+
27
+ next unless p.start_with?('*.')
28
+
29
+ action = p[2..]
30
+ return true if permission.end_with?(":#{action}") || permission.end_with?(".#{action}")
31
+ end
32
+
33
+ false
34
+ end
35
+ end
36
+ end
@@ -192,6 +192,49 @@ module JwtAuthCognito
192
192
  end
193
193
  end
194
194
 
195
+ # Resolves effective permissions for a user in a specific app/org context.
196
+ # Fallback chain:
197
+ # 1. permissions:cache:{userId}:{appId}:{orgId} (handles both plain [] and { permissions: [] })
198
+ # 2. user:permissions:{userId} → permissions[appId][orgId].effectivePermissions
199
+ # 3. Compute from roles using app:roles:{appId}:{orgId}
200
+ # Returns nil if the user has no active membership in that org.
201
+ def resolve_effective_permissions(user_id, app_id, organization_id)
202
+ # Step 1: permissions cache
203
+ begin
204
+ raw = @redis_service.get("permissions:cache:#{user_id}:#{app_id}:#{organization_id}")
205
+ if raw
206
+ parsed = JSON.parse(raw)
207
+ return parsed if parsed.is_a?(Array)
208
+ return parsed['permissions'] if parsed.is_a?(Hash) && parsed['permissions'].is_a?(Array)
209
+ end
210
+ rescue StandardError => e
211
+ puts "Error reading permissions cache for #{user_id}: #{e.message}"
212
+ end
213
+
214
+ # Steps 2 & 3: user:permissions
215
+ begin
216
+ raw = @redis_service.get("user:permissions:#{user_id}")
217
+ return nil unless raw
218
+
219
+ data = JSON.parse(raw)
220
+ org_data = data.dig('permissions', app_id, organization_id)
221
+ return nil unless org_data
222
+ return nil unless org_data['status'] == 'active'
223
+
224
+ effective = org_data['effectivePermissions']
225
+ return effective if effective.is_a?(Array)
226
+
227
+ # Step 3: compute from role definitions
228
+ roles = org_data['roles']
229
+ return [] unless roles.is_a?(Array) && !roles.empty?
230
+
231
+ compute_permissions_from_roles(app_id, organization_id, roles)
232
+ rescue StandardError => e
233
+ puts "Error resolving effective permissions for #{user_id}: #{e.message}"
234
+ nil
235
+ end
236
+ end
237
+
195
238
  def get_effective_permissions(user_id, app_id, organization_id)
196
239
  return nil unless @config[:include_effective_permissions]
197
240
 
@@ -328,5 +371,24 @@ module JwtAuthCognito
328
371
  @cache[key] = value
329
372
  @cache_timestamps[key] = Time.now.to_i
330
373
  end
374
+
375
+ def compute_permissions_from_roles(app_id, organization_id, role_names)
376
+ roles_data = get_app_roles(app_id, organization_id)
377
+ return [] unless roles_data.is_a?(Hash)
378
+
379
+ permissions = []
380
+ role_names.each do |role_name|
381
+ role = roles_data[role_name]
382
+ next unless role.is_a?(Hash)
383
+
384
+ role_permissions = role['permissions']
385
+ permissions.concat(role_permissions) if role_permissions.is_a?(Array)
386
+ end
387
+
388
+ permissions.uniq
389
+ rescue StandardError => e
390
+ puts "Error computing permissions from roles: #{e.message}"
391
+ []
392
+ end
331
393
  end
332
394
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JwtAuthCognito
4
- VERSION = '1.0.0-beta.9'
4
+ VERSION = '1.0.0-beta.11'
5
5
  end
@@ -9,6 +9,7 @@ require_relative 'jwt_auth_cognito/token_blacklist_service'
9
9
  require_relative 'jwt_auth_cognito/api_key_validator'
10
10
  require_relative 'jwt_auth_cognito/user_data_service'
11
11
  require_relative 'jwt_auth_cognito/error_utils'
12
+ require_relative 'jwt_auth_cognito/permission_checker'
12
13
  require_relative 'jwt_auth_cognito/jwt_validator'
13
14
 
14
15
  module JwtAuthCognito
@@ -16,6 +17,7 @@ module JwtAuthCognito
16
17
  class ValidationError < Error; end
17
18
  class BlacklistError < Error; end
18
19
  class ConfigurationError < Error; end
20
+ class ForbiddenError < Error; end
19
21
 
20
22
  # Specific JWT error types (matching Node.js implementation)
21
23
  class TokenExpiredError < ValidationError; end
@@ -71,3 +73,6 @@ end
71
73
 
72
74
  # Cargar tareas Rake si estamos en Rails
73
75
  require_relative 'jwt_auth_cognito/railtie' if defined?(Rails::Railtie)
76
+
77
+ # AuthorizationConcern requires ActiveSupport (available in Rails apps)
78
+ require_relative 'jwt_auth_cognito/authorization_concern' if defined?(ActiveSupport::Concern)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jwt_auth_cognito
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre.beta.9
4
+ version: 1.0.0.pre.beta.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Optimal
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-09-22 00:00:00.000000000 Z
11
+ date: 2026-06-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-ssm
@@ -189,10 +189,12 @@ files:
189
189
  - lib/generators/jwt_auth_cognito/templates/jwt_auth_cognito.rb.erb
190
190
  - lib/jwt_auth_cognito.rb
191
191
  - lib/jwt_auth_cognito/api_key_validator.rb
192
+ - lib/jwt_auth_cognito/authorization_concern.rb
192
193
  - lib/jwt_auth_cognito/configuration.rb
193
194
  - lib/jwt_auth_cognito/error_utils.rb
194
195
  - lib/jwt_auth_cognito/jwks_service.rb
195
196
  - lib/jwt_auth_cognito/jwt_validator.rb
197
+ - lib/jwt_auth_cognito/permission_checker.rb
196
198
  - lib/jwt_auth_cognito/railtie.rb
197
199
  - lib/jwt_auth_cognito/redis_service.rb
198
200
  - lib/jwt_auth_cognito/ssm_service.rb