lex-identity-entra 0.6.1 → 0.7.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: e7076abeb6fcf3e837854a909a38bbe46ab78ff6dd8372fb0df53d507484170e
4
- data.tar.gz: 6d9347e6aedc921ee6c16fc8a2004d0eb986e15eb8faf150a3a26787363e0d04
3
+ metadata.gz: d2664847cdf119f5e114612d95e09b67f81d6964f466960a56cd246f01cbfa4d
4
+ data.tar.gz: 62c37fb1dabee26a49dfa806f995b6b5b8d144a8dd2e1559bb304947886ebfa4
5
5
  SHA512:
6
- metadata.gz: 6245ce699161036398ccfb11d141587f0188ebabe38944862fe693fcf7f6210c935cf4a493119de46d1b0b42691aa69a5f6459c2f0e11dc2d39b81bde8dad706
7
- data.tar.gz: 0b2a7fae7d074dc4fe580f7289c79105ca6761227b6bbbd7045844d4e75b0fb0be90a4c46f92af04fd2ff4a112fa86a3dd12440f1cb6b805d2174cd52b809999
6
+ metadata.gz: 33c9ee548f2fd6a709a3b8a48411f74fcd47f69cc41a566993f9cb6077461041cb0b3a6dcc7e22054d238dae66c9b7968aec0272a999bd979a67f2221187a27c
7
+ data.tar.gz: b05f2770649c3321adfebf21c7bc352e75fce1872768fe8425fa65a08385bba5b07b96a027d1d21632e129f2157d11fa1a3b3ec7db3fe726be08edc093c6a430
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.0] - 2026-10-31
4
+ ### Security
5
+ - Qualify delegated identity keys by verified Graph token tenant and object ID (`tenant:oid`), including multi-account discovery. A successful `/me` response must match the accepted token's object ID; opaque or mismatched token subjects cannot resolve an authenticated shared identity.
6
+ - Preserve human canonical names without treating them as cross-tenant account-linking evidence. Existing unscoped object-ID links require explicit tenant verification before migration; they are not automatically merged by name.
7
+
3
8
  ## [0.6.1] - 2026-10-19
4
9
  ### Fixed
5
10
  - **Browser tab storm**: `Delegated::Actor::TokenRefresher` opened a new login tab on every 15-minute tick once the refresh token went bad (about 50 tabs overnight). It now respects `browser_auth.auto_authenticate`, and every interactive attempt goes through the new `Helpers::InteractiveAuthGuard`, which provides:
data/README.md CHANGED
@@ -133,11 +133,24 @@ identity:
133
133
  canonical_name: "jdoe", # normalized from onPremisesSamAccountName or mailNickname
134
134
  kind: :human,
135
135
  source: :entra_delegated,
136
- provider_identity: "object-id-guid",
137
- profile: { ... } # full Graph /me response
136
+ provider_identity: "tenant-id-guid:object-id-guid",
137
+ tenant_id: "tenant-id-guid",
138
+ object_id: "object-id-guid",
139
+ profile: { ... } # normalized Graph /me profile and verified subject context
138
140
  }
139
141
  ```
140
142
 
143
+ The tenant/object key is derived only after Graph accepts the exact bearer token and
144
+ the token's `oid` matches `/me.id`. Opaque tokens still allow profile retrieval but
145
+ cannot establish a shared authenticated identity without verifiable tenant context.
146
+ Canonical names and display names are not account-linking evidence. LegionIO 1.9.53+
147
+ reuses a discovered Teams principal through this immutable key without granting
148
+ access to the discovering reader's stored Teams content.
149
+
150
+ Existing object-ID-only identity rows are not automatically adopted: their tenant
151
+ must be established explicitly before linking them to the new scoped key. No tokens
152
+ or source profiles are copied into the shared identity registry by Teams discovery.
153
+
141
154
  ## Development
142
155
 
143
156
  ```bash
@@ -28,6 +28,7 @@ Gem::Specification.new do |spec|
28
28
  spec.require_paths = ['lib']
29
29
 
30
30
  # Core framework dependencies
31
+ spec.add_dependency 'base64', '>= 0.2'
31
32
  spec.add_dependency 'concurrent-ruby', '>= 1.2'
32
33
  spec.add_dependency 'faraday', '>= 2.0'
33
34
  spec.add_dependency 'legion-crypt', '>= 1.5.13'
@@ -31,6 +31,11 @@ module Legion
31
31
  return nil
32
32
  end
33
33
 
34
+ unless profile[:provider_identity_key]
35
+ log.warn('Delegated::Identity.resolve: no verified tenant/object subject in profile')
36
+ return nil
37
+ end
38
+
34
39
  canonical = profile[:on_premises_sam_account_name] || profile[:mail_nickname]
35
40
  if canonical.nil? || canonical.empty?
36
41
  log.warn('Delegated::Identity.resolve: no canonical name in profile')
@@ -42,7 +47,9 @@ module Legion
42
47
  canonical_name: normalize(canonical),
43
48
  kind: :human,
44
49
  source: :entra_delegated,
45
- provider_identity: profile[:id],
50
+ provider_identity: profile[:provider_identity_key],
51
+ tenant_id: profile[:tenant_id],
52
+ object_id: profile[:object_id],
46
53
  profile: profile,
47
54
  employee_id: profile[:employee_id]
48
55
  }
@@ -43,7 +43,7 @@ module Legion
43
43
  next unless token
44
44
 
45
45
  profile = Legion::Extensions::Identity::Entra::Helpers::GraphClient.fetch_me(token)
46
- next unless profile
46
+ next unless profile && profile[:provider_identity_key]
47
47
 
48
48
  canonical = profile[:on_premises_sam_account_name] || profile[:mail_nickname]
49
49
  next if canonical.nil? || canonical.empty?
@@ -54,7 +54,9 @@ module Legion
54
54
  source: :entra,
55
55
  qualifier: qualifier,
56
56
  account_type: account_type_for(qualifier, canonical),
57
- provider_identity: profile[:id],
57
+ provider_identity: profile[:provider_identity_key],
58
+ tenant_id: profile[:tenant_id],
59
+ object_id: profile[:object_id],
58
60
  profile: profile,
59
61
  employee_id: profile[:employee_id]
60
62
  }
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'faraday'
4
+ require 'base64'
4
5
 
5
6
  module Legion
6
7
  module Extensions
@@ -26,7 +27,8 @@ module Legion
26
27
  end
27
28
 
28
29
  log.debug('GraphClient.fetch_me: profile fetched successfully')
29
- parse_profile(json_load(response.body))
30
+ profile = parse_profile(json_load(response.body))
31
+ profile.merge(authenticated_subject(access_token: access_token, object_id: profile[:id]))
30
32
  rescue StandardError => e
31
33
  handle_exception(e, level: :warn, operation: 'graph_client.fetch_me')
32
34
  nil
@@ -55,6 +57,29 @@ module Legion
55
57
  f.options.timeout = 10
56
58
  end
57
59
  end
60
+
61
+ private
62
+
63
+ # Graph has accepted this exact bearer token. Only then may its tenant/object
64
+ # claims qualify the /me subject; configured tenants and names are not evidence.
65
+ def authenticated_subject(access_token:, object_id:)
66
+ parts = access_token.to_s.split('.')
67
+ return {} unless parts.length == 3
68
+
69
+ claims = json_load(Base64.urlsafe_decode64(parts[1]))
70
+ tenant = claims[:tid].to_s.downcase
71
+ oid = claims[:oid].to_s.downcase
72
+ guid = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/
73
+ unless guid.match?(tenant) && guid.match?(oid) && oid == object_id.to_s.downcase
74
+ log.warn('Graph /me subject does not have matching tenant/object token claims')
75
+ return {}
76
+ end
77
+
78
+ { tenant_id: tenant, object_id: oid, provider_identity_key: "#{tenant}:#{oid}" }
79
+ rescue StandardError => e
80
+ log.warn("Graph /me subject context unavailable: #{e.class}")
81
+ {}
82
+ end
58
83
  end
59
84
  end
60
85
  end
@@ -106,7 +106,7 @@ module Legion
106
106
  out, status = Open3.capture2('/usr/sbin/ioreg', '-n', 'Root', '-d1')
107
107
  status.success? && out.include?('"CGSSessionScreenIsLocked"=Yes')
108
108
  rescue StandardError => e
109
- handle_exception(e, level: :debug, operation: 'entra.auth_guard.screen_locked?')
109
+ handle_exception(e, level: :warn, operation: 'entra.auth_guard.screen_locked?')
110
110
  false
111
111
  end
112
112
 
@@ -120,7 +120,7 @@ module Legion
120
120
  match = out.match(/"HIDIdleTime"\s*=\s*(\d+)/)
121
121
  match ? match[1].to_i / 1_000_000_000 : nil
122
122
  rescue StandardError => e
123
- handle_exception(e, level: :debug, operation: 'entra.auth_guard.idle_seconds')
123
+ handle_exception(e, level: :warn, operation: 'entra.auth_guard.idle_seconds')
124
124
  nil
125
125
  end
126
126
 
@@ -316,7 +316,7 @@ module Legion
316
316
  def setting(key)
317
317
  Legion::Settings.dig(:identity, :entra, :delegated, :browser_auth, key)
318
318
  rescue StandardError => e
319
- handle_exception(e, level: :debug, operation: 'entra.auth_guard.setting', key: key)
319
+ handle_exception(e, level: :warn, operation: 'entra.auth_guard.setting', key: key)
320
320
  nil
321
321
  end
322
322
 
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Identity
6
6
  module Entra
7
- VERSION = '0.6.1'
7
+ VERSION = '0.7.0'
8
8
  end
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-identity-entra
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -9,6 +9,20 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0.2'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: concurrent-ruby
14
28
  requirement: !ruby/object:Gem::Requirement