logi_auth 1.0.0 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c3ae64ef8bf56e00d3edeef71184c59da5ce8ff9a8d4df3aae11c1556f222ba
4
- data.tar.gz: 8838cb06f5305d4b1e231d52c8aeeed10fec22a9658370c2d98af878b7d7aaa8
3
+ metadata.gz: 831ea9a87a2ab041f8056110a66b5bdb70edfb5922c24e611f2928812a90a192
4
+ data.tar.gz: a81b696e656399b2aebcffe49346f6278b95af02bcde4fd535a7de921a394aee
5
5
  SHA512:
6
- metadata.gz: afd06609d41ff98473b53a913174c7a2e2101428a03892f6d41f39dc598b0a73ecbd987cb2eb33ecd2cf951283dcc2947f08484c824834a21f158f681b95be6c
7
- data.tar.gz: f587f3dd60e2dcbcb23a67e3eaa0d7a383521c07c90af24b49c346b222f4c47e2980949b0f52068a994158b32972e5cd99af8e55592d7cb15cc720bb55002820
6
+ metadata.gz: 0cbd469cbd055f391654f6c968692a3ca3d8d74d7a6c124daddd0225cfc54d0cc8e1f5a91279d8ba7bff83ea1317d28ca923c6bdd755dafd20c6eac954571cff
7
+ data.tar.gz: e66aa214038aac5f916c9a0af991edd2af705c7322c958551c255043e68f62ac720e8ecdc859733136cc8fc9227aaf7fbdef2cd8c9a020df2c3fcd7f1694d13d
@@ -10,6 +10,7 @@ module LogiAuth
10
10
  CODES = %w[
11
11
  malformed missing_kid unknown_kid bad_signature
12
12
  iss_mismatch aud_mismatch expired nonce_mismatch missing_claim
13
+ at_hash_mismatch
13
14
  ].freeze
14
15
 
15
16
  attr_reader :code
@@ -24,12 +24,15 @@ module LogiAuth
24
24
  module_function
25
25
 
26
26
  # Verify +id_token+ and return a Result. Raises IdTokenError on any failure.
27
- # Claim order: signature -> iss -> aud -> exp -> iat -> nonce -> sub.
27
+ # Claim order: signature -> iss -> aud -> exp -> iat -> nonce -> sub -> at_hash.
28
28
  #
29
- # jwks: Hash with "keys" => [ {"kty","n","e","kid",...}, ... ]
30
- # expected: { issuer:, client_id:, nonce: } (nonce optional)
31
- # now: Unix seconds; defaults to Time.now. Injectable for tests.
32
- def verify(id_token, jwks:, expected:, now: nil, clock_skew_sec: 60)
29
+ # jwks: Hash with "keys" => [ {"kty","n","e","kid",...}, ... ]
30
+ # expected: { issuer:, client_id:, nonce: } (nonce optional)
31
+ # now: Unix seconds; defaults to Time.now. Injectable for tests.
32
+ # access_token: OAuth access_token string. When both the payload carries an
33
+ # `at_hash` and this is provided, OIDC §3.1.3.6 at_hash binding is enforced
34
+ # (present-only): default nil skips the check, preserving backward compat.
35
+ def verify(id_token, jwks:, expected:, now: nil, clock_skew_sec: 60, access_token: nil)
33
36
  now ||= Time.now.to_i
34
37
 
35
38
  parts = id_token.split(".")
@@ -46,7 +49,15 @@ module LogiAuth
46
49
  kid = header["kid"]
47
50
  raise IdTokenError, "missing_kid" unless kid.is_a?(String) && !kid.empty?
48
51
 
49
- jwk = Array(jwks["keys"]).find { |k| k["kid"] == kid }
52
+ # Tolerant JWKS key selection: pick the RS256 signing RSA key matching kid.
53
+ # Guards against a future EC (or encryption) key sharing the kid space —
54
+ # exact-match kty/use/alg filter keeps login working when JWKS gains keys.
55
+ jwk = Array(jwks["keys"]).find do |k|
56
+ k["kty"] == "RSA" &&
57
+ (k["use"].nil? || k["use"] == "sig") &&
58
+ (k["alg"].nil? || k["alg"] == "RS256") &&
59
+ k["kid"] == kid
60
+ end
50
61
  raise IdTokenError, "unknown_kid" if jwk.nil?
51
62
 
52
63
  signature = base64url_decode(parts[2])
@@ -82,9 +93,24 @@ module LogiAuth
82
93
  sub = payload["sub"]
83
94
  raise IdTokenError, "missing_claim" unless sub.is_a?(String) && !sub.empty?
84
95
 
96
+ # OIDC §3.1.3.6 at_hash — enforced last, present-only. If the payload
97
+ # carries an at_hash and an access_token was supplied, they must bind:
98
+ # base64url_nopad(SHA256(access_token bytes)[0...16]) == at_hash. Absent
99
+ # at_hash or absent access_token skips the check (backward compatible).
100
+ at_hash = payload["at_hash"]
101
+ if access_token && at_hash.is_a?(String) && !at_hash.empty?
102
+ raise IdTokenError, "at_hash_mismatch" unless at_hash == compute_at_hash(access_token)
103
+ end
104
+
85
105
  Result.new(sub: sub, claims: payload)
86
106
  end
87
107
 
108
+ # left-most 128 bits of SHA256(access_token), base64url without padding.
109
+ def compute_at_hash(access_token)
110
+ digest = OpenSSL::Digest::SHA256.digest(access_token.encode(Encoding::UTF_8))
111
+ Base64.urlsafe_encode64(digest[0, 16], padding: false)
112
+ end
113
+
88
114
  def verify_rs256(signing_input, signature, jwk)
89
115
  key = rsa_public_key(jwk)
90
116
  return false unless key
@@ -92,7 +92,10 @@ module LogiAuth
92
92
  raise ServerError.new("missing_id_token", "Token response had no id_token — was `openid` in the scopes?")
93
93
  end
94
94
 
95
- verified = verify_with_rotation_retry(id_token, nonce)
95
+ # Thread the access_token so OIDC §3.1.3.6 at_hash binding is enforced
96
+ # before the Session is returned (present-only: no-op when the id_token
97
+ # carries no at_hash). parse_token_body guarantees a String access_token.
98
+ verified = verify_with_rotation_retry(id_token, nonce, tokens["access_token"])
96
99
  email = verified.claims["email"]
97
100
  expires_in = tokens["expires_in"]
98
101
 
@@ -110,17 +113,17 @@ module LogiAuth
110
113
 
111
114
  private
112
115
 
113
- def verify_with_rotation_retry(id_token, nonce)
116
+ def verify_with_rotation_retry(id_token, nonce, access_token = nil)
114
117
  expected = { issuer: @token_issuer, client_id: @client_id, nonce: nonce }
115
118
  jwks, from_cache = fetch_jwks(force: false)
116
119
  begin
117
- IdTokenVerifier.verify(id_token, jwks: jwks, expected: expected)
120
+ IdTokenVerifier.verify(id_token, jwks: jwks, expected: expected, access_token: access_token)
118
121
  rescue IdTokenError => e
119
122
  # Key rotation within the cache TTL — bust + refetch once.
120
123
  if e.code == "unknown_kid" && from_cache
121
124
  fresh, = fetch_jwks(force: true)
122
125
  begin
123
- IdTokenVerifier.verify(id_token, jwks: fresh, expected: expected)
126
+ IdTokenVerifier.verify(id_token, jwks: fresh, expected: expected, access_token: access_token)
124
127
  rescue IdTokenError => retry_err
125
128
  raise as_id_token_invalid(retry_err)
126
129
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LogiAuth
4
- VERSION = "1.0.0"
4
+ VERSION = "1.0.1"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logi_auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dcode
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-07-01 00:00:00.000000000 Z
10
+ date: 2026-07-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: minitest