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 +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +15 -2
- data/lex-identity-entra.gemspec +1 -0
- data/lib/legion/extensions/identity/entra/delegated/identity.rb +8 -1
- data/lib/legion/extensions/identity/entra/helpers/account_discovery.rb +4 -2
- data/lib/legion/extensions/identity/entra/helpers/graph_client.rb +26 -1
- data/lib/legion/extensions/identity/entra/helpers/interactive_auth_guard.rb +3 -3
- data/lib/legion/extensions/identity/entra/version.rb +1 -1
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d2664847cdf119f5e114612d95e09b67f81d6964f466960a56cd246f01cbfa4d
|
|
4
|
+
data.tar.gz: 62c37fb1dabee26a49dfa806f995b6b5b8d144a8dd2e1559bb304947886ebfa4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
data/lex-identity-entra.gemspec
CHANGED
|
@@ -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[:
|
|
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[:
|
|
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: :
|
|
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: :
|
|
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: :
|
|
319
|
+
handle_exception(e, level: :warn, operation: 'entra.auth_guard.setting', key: key)
|
|
320
320
|
nil
|
|
321
321
|
end
|
|
322
322
|
|
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.
|
|
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
|