strongmind-auth 1.1.142 → 1.2.0

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: 285c55e9de3ac83079fe636603534b725591b48ee91dd73fd3ddf456040ed2f0
4
- data.tar.gz: 6f36438062533f4b83062a04fcdb976a1ac51227018dafdd8bb831729b88f095
3
+ metadata.gz: b1c87c87745c0a96b93591482bfb073b7aee75ad305af4acc7d32632e52f2e51
4
+ data.tar.gz: 5f199cf554635e03c7949ab35ae0e8740ccfef983c1d92fe5d1c72b839367d50
5
5
  SHA512:
6
- metadata.gz: 0b049b9ced1a5e3898e22f512b73456e9ca9dfc562420b8d3a27de1ba58d0b20403a037bf16c9b8d26aae6974434cda1a440c121e3a557e579822c58cb218ecd
7
- data.tar.gz: b973f2023815ddd688c345da9a7db8385c337e3d736bb8a80eefd7638efbdd1d7780217fac292b3c8c337cfa0b4c941f8df0b25f6e68eb7e1481991fcd5aae86
6
+ metadata.gz: c71f32bab0c99a7c87d0b62121d26f61ad8c67841ad67ac80968d2db28cbf1c14e089ea2afc147aaad36a8e2c6040d40fa206db831e9f8f453468d58374a3ef1
7
+ data.tar.gz: 9aeae95af157a89828bd820983c86b4452b6ad0b48f4096836d69d24d41a533d943a7f74d3eeceec4c209a1155539d9cff0579e18c892feccd59c0d377381a9f
@@ -4,6 +4,10 @@
4
4
  module JwtUtilities
5
5
  extend ActiveSupport::Concern
6
6
 
7
+ # Matches Zitadel's role-grant claim, e.g. "urn:zitadel:iam:org:project:123:roles"
8
+ # or the project-agnostic "urn:zitadel:iam:org:project:roles".
9
+ ZITADEL_ROLES_CLAIM_PATTERN = /\Aurn:zitadel:iam:org:project:(?:[^:]+:)?roles\z/.freeze
10
+
7
11
  def jwt_valid?(jwt, condition_key = nil, scopes = [], attributes = [])
8
12
  payload = decode_jwt(jwt)
9
13
  return false unless payload
@@ -41,6 +45,10 @@ module JwtUtilities
41
45
  verify_iat: true,
42
46
  verify_iss: true,
43
47
  verify_sub: true,
48
+ # RS256 matches Zitadel's current default signing algorithm.
49
+ # While Zitadel supports EdDSA (Ed25519), the platform is not currently
50
+ # configured to use it. When/if that changes, migrate this to EdDSA for
51
+ # post-quantum resistance. See also: go/auth/middleware.go (strongmind-platform).
44
52
  algorithm: 'RS256',
45
53
  iss: ENV['IDENTITY_BASE_URL'],
46
54
  leeway: 60
@@ -48,7 +56,70 @@ module JwtUtilities
48
56
  end
49
57
 
50
58
  def scope_valid?(payload, scopes)
51
- scopes.empty? || (payload['scope'].present? && scopes.all? { |scope| payload['scope'].include?(scope) })
59
+ return true if scopes.empty?
60
+
61
+ legacy_scope_valid?(payload, scopes) || roles_valid?(payload, scopes)
62
+ end
63
+
64
+ def legacy_scope_valid?(payload, scopes)
65
+ payload['scope'].present? && scopes.all? { |scope| payload['scope'].include?(scope) }
66
+ end
67
+
68
+ # Zitadel roles are provisioned 1-to-1 with the legacy scope strings, so a
69
+ # required scope is satisfied by a same-named grant in the roles claim.
70
+ # Comparison is case-insensitive to handle different casing conventions.
71
+ def roles_valid?(payload, scopes)
72
+ project_id = ENV['IDENTITY_PROJECT_ID']
73
+ return false if project_id.blank?
74
+ return false unless audience_valid?(payload)
75
+ return false unless project_id_valid?(payload)
76
+
77
+ roles = zitadel_roles(payload, project_id)
78
+ return false unless roles.present?
79
+
80
+ normalized_roles = roles.keys.map(&:downcase).to_set
81
+ scopes.all? { |scope| normalized_roles.include?(scope.downcase) }
82
+ end
83
+
84
+ def zitadel_roles(payload, project_id = nil)
85
+ payload.keys
86
+ .select { |key| key.to_s.match?(ZITADEL_ROLES_CLAIM_PATTERN) }
87
+ .select do |key|
88
+ claim_project_id = extract_project_id(key)
89
+ project_id.blank? || claim_project_id.nil? || claim_project_id == project_id
90
+ end
91
+ .each_with_object({}) do |key, acc|
92
+ value = payload[key]
93
+ acc.merge!(value) if value.is_a?(Hash)
94
+ end
95
+ .presence
96
+ end
97
+
98
+ def audience_valid?(payload)
99
+ aud_list = Array(payload['aud'])
100
+
101
+ if ENV['IDENTITY_PROJECT_ID'].present?
102
+ aud_list.include?(ENV['IDENTITY_PROJECT_ID'])
103
+ elsif ENV['IDENTITY_CLIENT_ID'].present?
104
+ aud_list.include?(ENV['IDENTITY_CLIENT_ID'])
105
+ else
106
+ true
107
+ end
108
+ end
109
+
110
+ def project_id_valid?(payload)
111
+ roles_claims = payload.keys.select { |key| key.to_s.match?(ZITADEL_ROLES_CLAIM_PATTERN) }
112
+ return true if roles_claims.empty?
113
+ return true if ENV['IDENTITY_PROJECT_ID'].blank?
114
+
115
+ roles_claims.any? do |claim_key|
116
+ project_id = extract_project_id(claim_key)
117
+ project_id.nil? || project_id == ENV['IDENTITY_PROJECT_ID']
118
+ end
119
+ end
120
+
121
+ def extract_project_id(claim_key)
122
+ claim_key.to_s[/project:([^:]+):/, 1]
52
123
  end
53
124
 
54
125
  def nonce_valid?(payload)
@@ -1,5 +1,5 @@
1
1
  module Strongmind
2
2
  module Auth
3
- VERSION = "1.1.142"
3
+ VERSION = "1.2.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: strongmind-auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.142
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Team Belding
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-01 00:00:00.000000000 Z
11
+ date: 2026-07-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails