Class: StandardId::JwtService
- Inherits:
-
Object
- Object
- StandardId::JwtService
- Defined in:
- lib/standard_id/jwt_service.rb
Constant Summary collapse
- RESERVED_JWT_KEYS =
%i[sub client_id scope grant_type exp iat aud iss nbf jti]
- BASE_SESSION_FIELDS =
%i[account_id client_id scopes grant_type aud]
- SUPPORTED_ALGORITHMS =
Supported signing algorithms categorized by type Symmetric: use shared secret (Rails.application.secret_key_base) Asymmetric: use key pairs (RSA or EC private key)
{ # HMAC (symmetric) "HS256" => { type: :symmetric }, "HS384" => { type: :symmetric }, "HS512" => { type: :symmetric }, # RSA (asymmetric) "RS256" => { type: :asymmetric, key_class: OpenSSL::PKey::RSA }, "RS384" => { type: :asymmetric, key_class: OpenSSL::PKey::RSA }, "RS512" => { type: :asymmetric, key_class: OpenSSL::PKey::RSA }, # ECDSA (asymmetric) "ES256" => { type: :asymmetric, key_class: OpenSSL::PKey::EC }, "ES384" => { type: :asymmetric, key_class: OpenSSL::PKey::EC }, "ES512" => { type: :asymmetric, key_class: OpenSSL::PKey::EC } }.freeze
- SESSION_CLASS =
Concurrent::Delay.new do Struct.new(*(BASE_SESSION_FIELDS + claim_resolver_keys), keyword_init: true) do def active? true end end end
Class Method Summary collapse
- .algorithm ⇒ Object
- .algorithm_config ⇒ Object
- .asymmetric? ⇒ Boolean
- .claim_resolver_keys ⇒ Object
- .decode(token) ⇒ Object
- .decode_session(token) ⇒ Object
- .encode(payload, expires_in: 1.hour) ⇒ Object
- .jwks ⇒ Object
- .key_id ⇒ Object
- .parse_private_key(key_source) ⇒ Object
- .reset_cached_key! ⇒ Object
- .session_class ⇒ Object
- .signing_key ⇒ Object
- .verification_key ⇒ Object
Class Method Details
.algorithm ⇒ Object
41 42 43 |
# File 'lib/standard_id/jwt_service.rb', line 41 def self.algorithm StandardId.config.oauth.signing_algorithm.to_s.upcase end |
.algorithm_config ⇒ Object
45 46 47 |
# File 'lib/standard_id/jwt_service.rb', line 45 def self.algorithm_config SUPPORTED_ALGORITHMS[algorithm] || raise(ArgumentError, "Unsupported algorithm: #{algorithm}. Supported: #{SUPPORTED_ALGORITHMS.keys.join(', ')}") end |
.asymmetric? ⇒ Boolean
49 50 51 |
# File 'lib/standard_id/jwt_service.rb', line 49 def self.asymmetric? algorithm_config[:type] == :asymmetric end |
.claim_resolver_keys ⇒ Object
149 150 151 152 153 154 155 |
# File 'lib/standard_id/jwt_service.rb', line 149 def self.claim_resolver_keys resolvers = StandardId.config.oauth.claim_resolvers keys = Hash.try_convert(resolvers)&.keys keys.compact.map(&:to_sym).uniq.excluding(*RESERVED_JWT_KEYS) rescue StandardError [] end |
.decode(token) ⇒ Object
97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/standard_id/jwt_service.rb', line 97 def self.decode(token) = { algorithm: algorithm } if StandardId.config.issuer.present? [:iss] = StandardId.config.issuer [:verify_iss] = true end decoded = JWT.decode(token, verification_key, true, ) decoded.first.with_indifferent_access rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::InvalidIatError, JWT::InvalidIssuerError nil end |
.decode_session(token) ⇒ Object
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/standard_id/jwt_service.rb', line 111 def self.decode_session(token) payload = decode(token) return unless payload scopes = if payload[:scope].is_a?(String) payload[:scope].split(" ") else Array(payload[:scope]).compact end session_class.new( **payload.slice(*claim_resolver_keys), account_id: payload[:sub], client_id: payload[:client_id], scopes: scopes, grant_type: payload[:grant_type], aud: payload[:aud] ) end |
.encode(payload, expires_in: 1.hour) ⇒ Object
86 87 88 89 90 91 92 93 94 95 |
# File 'lib/standard_id/jwt_service.rb', line 86 def self.encode(payload, expires_in: 1.hour) payload[:exp] = expires_in.from_now.to_i payload[:iat] = Time.current.to_i payload[:iss] ||= StandardId.config.issuer if StandardId.config.issuer.present? headers = {} headers[:kid] = key_id if asymmetric? JWT.encode(payload, signing_key, algorithm, headers) end |
.jwks ⇒ Object
131 132 133 134 135 136 137 138 |
# File 'lib/standard_id/jwt_service.rb', line 131 def self.jwks return nil unless asymmetric? @jwks ||= begin jwk = JWT::JWK.new(verification_key, kid: key_id) { keys: [jwk.export] } end end |
.key_id ⇒ Object
72 73 74 75 76 77 78 |
# File 'lib/standard_id/jwt_service.rb', line 72 def self.key_id return nil unless asymmetric? # Generate stable key ID from public key fingerprint # Use public_to_pem which works for both RSA and EC keys @key_id ||= Digest::SHA256.hexdigest(signing_key.public_to_pem)[0..7] end |
.parse_private_key(key_source) ⇒ Object
142 143 144 145 146 147 |
# File 'lib/standard_id/jwt_service.rb', line 142 def self.parse_private_key(key_source) pem = key_source.is_a?(Pathname) ? File.read(key_source) : key_source key_class = algorithm_config[:key_class] key_class.new(pem) end |
.reset_cached_key! ⇒ Object
80 81 82 83 84 |
# File 'lib/standard_id/jwt_service.rb', line 80 def self.reset_cached_key! @key_id = nil @signing_key_cache = nil @jwks = nil end |
.session_class ⇒ Object
37 38 39 |
# File 'lib/standard_id/jwt_service.rb', line 37 def self.session_class SESSION_CLASS.value end |
.signing_key ⇒ Object
53 54 55 56 57 58 59 |
# File 'lib/standard_id/jwt_service.rb', line 53 def self.signing_key if asymmetric? @signing_key_cache ||= parse_private_key(StandardId.config.oauth.signing_key) else Rails.application.secret_key_base end end |
.verification_key ⇒ Object
61 62 63 64 65 66 67 68 69 70 |
# File 'lib/standard_id/jwt_service.rb', line 61 def self.verification_key if asymmetric? key = signing_key # For EC keys, the key itself can be used for verification # For RSA keys, we extract the public key key.is_a?(OpenSSL::PKey::EC) ? key : key.public_key else Rails.application.secret_key_base end end |