lib/rodauth/features/oauth_jwt.rb in rodauth-oauth-0.10.4 vs lib/rodauth/features/oauth_jwt.rb in rodauth-oauth-1.0.0.pre.beta1
- old
+ new
@@ -1,762 +1,126 @@
# frozen_string_literal: true
-require "rodauth/oauth/version"
-require "rodauth/oauth/ttl_store"
+require "rodauth/oauth"
+require "rodauth/oauth/http_extensions"
module Rodauth
Feature.define(:oauth_jwt, :OauthJwt) do
- depends :oauth
+ depends :oauth_jwt_base, :oauth_jwt_jwks
- JWKS = OAuth::TtlStore.new
+ auth_value_method :oauth_jwt_access_tokens, true
- # Recommended to have hmac_secret as well
-
- auth_value_method :oauth_jwt_subject_type, "public" # fallback subject type: public, pairwise
- auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
-
- auth_value_method :oauth_jwt_token_issuer, nil
-
- configuration_module_eval do
- define_method :oauth_applications_jws_jwk_column do
- warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_column`"
- oauth_applications_jwks_column
- end
- define_method :oauth_applications_jws_jwk_label do
- warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_label`"
- oauth_applications_jws_jwk_label
- end
- define_method :oauth_application_jws_jwk_param do
- warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_param`"
- oauth_applications_jwks_param
- end
- end
-
- auth_value_method :oauth_applications_subject_type_column, :subject_type
- auth_value_method :oauth_applications_jwt_public_key_column, :jwt_public_key
- auth_value_method :oauth_applications_request_object_signing_alg_column, :request_object_signing_alg
- auth_value_method :oauth_applications_request_object_encryption_alg_column, :request_object_encryption_alg
- auth_value_method :oauth_applications_request_object_encryption_enc_column, :request_object_encryption_enc
-
- translatable_method :oauth_applications_jwt_public_key_label, "Public key"
-
- auth_value_method :oauth_application_jwt_public_key_param, "jwt_public_key"
- auth_value_method :oauth_application_jwks_param, "jwks"
-
- auth_value_method :oauth_jwt_keys, {}
- auth_value_method :oauth_jwt_key, nil
- auth_value_method :oauth_jwt_public_keys, {}
- auth_value_method :oauth_jwt_public_key, nil
- auth_value_method :oauth_jwt_algorithm, "RS256"
-
- auth_value_method :oauth_jwt_jwe_keys, {}
- auth_value_method :oauth_jwt_jwe_key, nil
- auth_value_method :oauth_jwt_jwe_public_keys, {}
- auth_value_method :oauth_jwt_jwe_public_key, nil
- auth_value_method :oauth_jwt_jwe_algorithm, nil
- auth_value_method :oauth_jwt_jwe_encryption_method, nil
-
- # values used for rotating keys
- auth_value_method :oauth_jwt_legacy_public_key, nil
- auth_value_method :oauth_jwt_legacy_algorithm, nil
-
- auth_value_method :oauth_jwt_jwe_copyright, nil
- auth_value_method :oauth_jwt_audience, nil
-
- translatable_method :request_uri_not_supported_message, "request uri is unsupported"
- translatable_method :invalid_request_object_message, "request object is invalid"
-
- auth_value_methods(
- :jwt_encode,
- :jwt_decode,
- :jwks_set,
- :generate_jti
- )
-
- route(:jwks) do |r|
- next unless is_authorization_server?
-
- r.get do
- json_response_success({ keys: jwks_set }, true)
- end
- end
-
def require_oauth_authorization(*scopes)
+ return super unless oauth_jwt_access_tokens
+
authorization_required unless authorization_token
- scopes << oauth_application_default_scope if scopes.empty?
-
token_scopes = authorization_token["scope"].split(" ")
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
end
def oauth_token_subject
+ return super unless oauth_jwt_access_tokens
+
return unless authorization_token
authorization_token["sub"]
end
- private
+ def current_oauth_account
+ subject = oauth_token_subject
- def issuer
- @issuer ||= oauth_jwt_token_issuer || authorization_server_url
+ return if subject == authorization_token["client_id"]
+
+ oauth_account_ds(subject).first
end
+ def current_oauth_application
+ db[oauth_applications_table].where(
+ oauth_applications_client_id_column => authorization_token["client_id"]
+ ).first
+ end
+
+ private
+
def authorization_token
+ return super unless oauth_jwt_access_tokens
+
return @authorization_token if defined?(@authorization_token)
@authorization_token = begin
bearer_token = fetch_access_token
return unless bearer_token
- jwt_token = jwt_decode(bearer_token)
+ jwt_claims = jwt_decode(bearer_token)
- return unless jwt_token
+ return unless jwt_claims
- return if jwt_token["iss"] != issuer ||
- (oauth_jwt_audience && jwt_token["aud"] != oauth_jwt_audience) ||
- !jwt_token["sub"]
+ return unless jwt_claims["sub"]
- jwt_token
+ return unless jwt_claims["aud"]
+
+ jwt_claims
end
end
- # /authorize
+ # /token
- def validate_authorize_params
- # TODO: add support for requst_uri
- redirect_response_error("request_uri_not_supported") if param_or_nil("request_uri")
+ def create_token_from_token(_grant, update_params)
+ oauth_grant = super
- request_object = param_or_nil("request")
-
- return super unless request_object && oauth_application
-
- if (jwks = oauth_application_jwks)
- jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
- else
- redirect_response_error("invalid_request_object")
+ if oauth_jwt_access_tokens
+ access_token = _generate_jwt_access_token(oauth_grant)
+ oauth_grant[oauth_grants_token_column] = access_token
end
+ oauth_grant
+ end
- request_sig_enc_opts = {
- jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
- jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
- jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
- }.compact
-
- claims = jwt_decode(request_object, jwks: jwks, verify_jti: false, **request_sig_enc_opts)
-
- redirect_response_error("invalid_request_object") unless claims
-
- # If signed, the Authorization Request
- # Object SHOULD contain the Claims "iss" (issuer) and "aud" (audience)
- # as members, with their semantics being the same as defined in the JWT
- # [RFC7519] specification. The value of "aud" should be the value of
- # the Authorization Server (AS) "issuer" as defined in RFC8414
- # [RFC8414].
- claims.delete("iss")
- audience = claims.delete("aud")
-
- redirect_response_error("invalid_request_object") if audience && audience != issuer
-
- claims.each do |k, v|
- request.params[k.to_s] = v
+ def generate_token(_grant_params = {}, should_generate_refresh_token = true)
+ oauth_grant = super
+ if oauth_jwt_access_tokens
+ access_token = _generate_jwt_access_token(oauth_grant)
+ oauth_grant[oauth_grants_token_column] = access_token
end
-
- super
+ oauth_grant
end
- # /token
+ def _generate_jwt_access_token(oauth_grant)
+ claims = jwt_claims(oauth_grant)
- def create_oauth_token_from_token(oauth_token, update_params)
- otoken = super
- access_token = _generate_jwt_access_token(otoken)
- otoken[oauth_tokens_token_column] = access_token
- otoken
- end
-
- def generate_oauth_token(params = {}, should_generate_refresh_token = true)
- oauth_token = super
- access_token = _generate_jwt_access_token(oauth_token)
- oauth_token[oauth_tokens_token_column] = access_token
- oauth_token
- end
-
- def _generate_jwt_access_token(oauth_token)
- claims = jwt_claims(oauth_token)
-
# one of the points of using jwt is avoiding database lookups, so we put here all relevant
# token data.
- claims[:scope] = oauth_token[oauth_tokens_scopes_column]
+ claims[:scope] = oauth_grant[oauth_grants_scopes_column]
jwt_encode(claims)
end
def _generate_access_token(*)
- # no op
+ return super unless oauth_jwt_access_tokens
end
- def jwt_claims(oauth_token)
+ def jwt_claims(oauth_grant)
issued_at = Time.now.to_i
{
- iss: issuer, # issuer
+ iss: oauth_jwt_issuer, # issuer
iat: issued_at, # issued at
#
# sub REQUIRED - as defined in section 4.1.2 of [RFC7519]. In case of
# access tokens obtained through grants where a resource owner is
# involved, such as the authorization code grant, the value of "sub"
# SHOULD correspond to the subject identifier of the resource owner.
# In case of access tokens obtained through grants where no resource
# owner is involved, such as the client credentials grant, the value
# of "sub" SHOULD correspond to an identifier the authorization
# server uses to indicate the client application.
- sub: jwt_subject(oauth_token),
+ sub: jwt_subject(oauth_grant),
client_id: oauth_application[oauth_applications_client_id_column],
- exp: issued_at + oauth_token_expires_in,
- aud: (oauth_jwt_audience || oauth_application[oauth_applications_client_id_column])
+ exp: issued_at + oauth_access_token_expires_in,
+ aud: oauth_jwt_audience
}
- end
-
- def jwt_subject(oauth_token)
- subject_type = if oauth_application
- oauth_application[oauth_applications_subject_type_column] || oauth_jwt_subject_type
- else
- oauth_jwt_subject_type
- end
- case subject_type
- when "public"
- oauth_token[oauth_tokens_account_id_column]
- when "pairwise"
- id = oauth_token[oauth_tokens_account_id_column]
- application_id = oauth_token[oauth_tokens_oauth_application_id_column]
- Digest::SHA256.hexdigest("#{id}#{application_id}#{oauth_jwt_subject_secret}")
- else
- raise StandardError, "unexpected subject (#{subject_type})"
- end
- end
-
- def oauth_token_by_token(token)
- jwt_decode(token)
- end
-
- def token_from_application?(grant_or_claims, oauth_application)
- return super if grant_or_claims[oauth_tokens_id_column]
-
- if grant_or_claims["client_id"]
- grant_or_claims["client_id"] == oauth_application[oauth_applications_client_id_column]
- else
- Array(grant_or_claims["aud"]).include?(oauth_application[oauth_applications_client_id_column])
- end
- end
-
- def json_token_introspect_payload(oauth_token)
- return { active: false } unless oauth_token
-
- return super unless oauth_token["sub"] # naive check on whether it's a jwt token
-
- {
- active: true,
- scope: oauth_token["scope"],
- client_id: oauth_token["client_id"],
- # username
- token_type: "access_token",
- exp: oauth_token["exp"],
- iat: oauth_token["iat"],
- nbf: oauth_token["nbf"],
- sub: oauth_token["sub"],
- aud: oauth_token["aud"],
- iss: oauth_token["iss"],
- jti: oauth_token["jti"]
- }
- end
-
- def oauth_server_metadata_body(path = nil)
- metadata = super
- metadata.merge! \
- jwks_uri: jwks_url,
- token_endpoint_auth_signing_alg_values_supported: (oauth_jwt_keys.keys + [oauth_jwt_algorithm]).uniq
- metadata
- end
-
- def _jwt_key
- @_jwt_key ||= oauth_jwt_key || begin
- if oauth_application
-
- if (jwks = oauth_application_jwks)
- jwks = JSON.parse(jwks, symbolize_names: true) if jwks && jwks.is_a?(String)
- jwks
- else
- oauth_application[oauth_applications_jwt_public_key_column]
- end
- end
- end
- end
-
- def _jwt_public_key
- @_jwt_public_key ||= oauth_jwt_public_key || begin
- if oauth_application
- jwks || oauth_application[oauth_applications_jwt_public_key_column]
- else
- _jwt_key
- end
- end
- end
-
- # Resource Server only!
- #
- # returns the jwks set from the authorization server.
- def auth_server_jwks_set
- metadata = authorization_server_metadata
-
- return unless metadata && (jwks_uri = metadata[:jwks_uri])
-
- jwks_uri = URI(jwks_uri)
-
- jwks = JWKS[jwks_uri]
-
- return jwks if jwks
-
- JWKS.set(jwks_uri) do
- http = Net::HTTP.new(jwks_uri.host, jwks_uri.port)
- http.use_ssl = jwks_uri.scheme == "https"
-
- request = Net::HTTP::Get.new(jwks_uri.request_uri)
- request["accept"] = json_response_content_type
- response = http.request(request)
- authorization_required unless response.code.to_i == 200
-
- # time-to-live
- ttl = if response.key?("cache-control")
- cache_control = response["cache-control"]
- cache_control[/max-age=(\d+)/, 1].to_i
- elsif response.key?("expires")
- Time.parse(response["expires"]).to_i - Time.now.to_i
- end
-
- [JSON.parse(response.body, symbolize_names: true), ttl]
- end
- end
-
- def generate_jti(payload)
- # Use the key and iat to create a unique key per request to prevent replay attacks
- jti_raw = [
- payload[:aud] || payload["aud"],
- payload[:iat] || payload["iat"]
- ].join(":").to_s
- Digest::SHA256.hexdigest(jti_raw)
- end
-
- def verify_jti(jti, claims)
- generate_jti(claims) == jti
- end
-
- def verify_aud(expected_aud, aud)
- expected_aud == aud
- end
-
- def oauth_application_jwks
- jwks = oauth_application[oauth_applications_jwks_column]
-
- if jwks
- jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
- return jwks
- end
-
- jwks_uri = oauth_application[oauth_applications_jwks_uri_column]
-
- return unless jwks_uri
-
- jwks_uri = URI(jwks_uri)
-
- jwks = JWKS[jwks_uri]
-
- return jwks if jwks
-
- JWKS.set(jwks_uri) do
- http = Net::HTTP.new(jwks_uri.host, jwks_uri.port)
- http.use_ssl = jwks_uri.scheme == "https"
-
- request = Net::HTTP::Get.new(jwks_uri.request_uri)
- request["accept"] = json_response_content_type
- response = http.request(request)
- return unless response.code.to_i == 200
-
- # time-to-live
- ttl = if response.key?("cache-control")
- cache_control = response["cache-control"]
- cache_control[/max-age=(\d+)/, 1].to_i
- elsif response.key?("expires")
- Time.parse(response["expires"]).to_i - Time.now.to_i
- end
-
- [JSON.parse(response.body, symbolize_names: true), ttl]
- end
- end
-
- if defined?(JSON::JWT)
- # json-jwt
-
- auth_value_method :oauth_jwt_algorithms_supported, %w[
- HS256 HS384 HS512
- RS256 RS384 RS512
- PS256 PS384 PS512
- ES256 ES384 ES512 ES256K
- ]
- auth_value_method :oauth_jwt_jwe_algorithms_supported, %w[
- RSA1_5 RSA-OAEP dir A128KW A256KW
- ]
- auth_value_method :oauth_jwt_jwe_encryption_methods_supported, %w[
- A128GCM A256GCM A128CBC-HS256 A256CBC-HS512
- ]
-
- def jwk_import(data)
- JSON::JWK.new(data)
- end
-
- def jwt_encode(payload,
- jwks: nil,
- encryption_algorithm: oauth_jwt_jwe_algorithm,
- encryption_method: oauth_jwt_jwe_encryption_method,
- jwe_key: oauth_jwt_jwe_keys[[encryption_algorithm,
- encryption_method]] || oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
- signing_algorithm: oauth_jwt_algorithm || oauth_jwt_keys.keys.first)
- payload[:jti] = generate_jti(payload)
- jwt = JSON::JWT.new(payload)
-
- key = oauth_jwt_keys[signing_algorithm] || _jwt_key
- key = key.first if key.is_a?(Array)
-
- jwk = JSON::JWK.new(key || "")
-
- jwt = jwt.sign(jwk, signing_algorithm)
- jwt.kid = jwk.thumbprint
-
- if jwks && (jwk = jwks.find { |k| k[:use] == "enc" && k[:alg] == encryption_algorithm && k[:enc] == encryption_method })
- jwk = JSON::JWK.new(jwk)
- jwe = jwt.encrypt(jwk, encryption_algorithm.to_sym, encryption_method.to_sym)
- jwe.to_s
- elsif jwe_key
- jwe_key = jwe_key.first if jwe_key.is_a?(Array)
- algorithm = encryption_algorithm.to_sym if encryption_algorithm
- meth = encryption_method.to_sym if encryption_method
- jwt.encrypt(jwe_key, algorithm, meth)
- else
- jwt.to_s
- end
- end
-
- def jwt_decode(
- token,
- jwks: nil,
- jws_algorithm: oauth_jwt_algorithm || oauth_jwt_public_key.keys.first || oauth_jwt_keys.keys.first,
- jws_key: oauth_jwt_public_key || oauth_jwt_keys[jws_algorithm] || _jwt_key,
- jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
- jws_encryption_method: oauth_jwt_jwe_encryption_method,
- jwe_key: oauth_jwt_jwe_keys[[jws_encryption_algorithm, jws_encryption_method]] || oauth_jwt_jwe_key,
- verify_claims: true,
- verify_jti: true,
- verify_iss: true,
- verify_aud: false,
- **
- )
- jws_key = jws_key.first if jws_key.is_a?(Array)
-
- if jwe_key
- jwe_key = jwe_key.first if jwe_key.is_a?(Array)
- token = JSON::JWT.decode(token, jwe_key).plain_text
- end
-
- claims = if is_authorization_server?
- if oauth_jwt_legacy_public_key
- JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set }))
- elsif jwks
- enc_algs = [jws_encryption_algorithm].compact
- enc_meths = [jws_encryption_method].compact
- sig_algs = [jws_algorithm].compact.map(&:to_sym)
- jws = JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks }), enc_algs + sig_algs, enc_meths)
- jws = JSON::JWT.decode(jws.plain_text, JSON::JWK::Set.new({ keys: jwks }), sig_algs) if jws.is_a?(JSON::JWE)
- jws
- elsif jws_key
- JSON::JWT.decode(token, jws_key)
- end
- elsif (jwks = auth_server_jwks_set)
- JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
- end
-
- now = Time.now
- if verify_claims && (
- (!claims[:exp] || Time.at(claims[:exp]) < now) &&
- (claims[:nbf] && Time.at(claims[:nbf]) < now) &&
- (claims[:iat] && Time.at(claims[:iat]) < now) &&
- (verify_iss && claims[:iss] != issuer) &&
- (verify_aud && !verify_aud(claims[:aud], claims[:client_id])) &&
- (verify_jti && !verify_jti(claims[:jti], claims))
- )
- return
- end
-
- claims
- rescue JSON::JWT::Exception
- nil
- end
-
- def jwks_set
- @jwks_set ||= [
- *(
- unless oauth_jwt_public_keys.empty?
- oauth_jwt_public_keys.flat_map { |algo, pkeys| pkeys.map { |pkey| JSON::JWK.new(pkey).merge(use: "sig", alg: algo) } }
- end
- ),
- *(
- unless oauth_jwt_jwe_public_keys.empty?
- oauth_jwt_jwe_public_keys.flat_map do |(algo, _enc), pkeys|
- pkeys.map do |pkey|
- JSON::JWK.new(pkey).merge(use: "enc", alg: algo)
- end
- end
- end
- ),
- # legacy
- (JSON::JWK.new(oauth_jwt_public_key).merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
- (JSON::JWK.new(oauth_jwt_legacy_public_key).merge(use: "sig", alg: oauth_jwt_legacy_algorithm) if oauth_jwt_legacy_public_key),
- (JSON::JWK.new(oauth_jwt_jwe_public_key).merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
- ].compact
- end
-
- elsif defined?(JWT)
- # ruby-jwt
- require "rodauth/oauth/jwe_extensions" if defined?(JWE)
-
- auth_value_method :oauth_jwt_algorithms_supported, %w[
- HS256 HS384 HS512 HS512256
- RS256 RS384 RS512
- ED25519
- ES256 ES384 ES512
- PS256 PS384 PS512
- ]
-
- auth_value_methods(
- :oauth_jwt_jwe_algorithms_supported,
- :oauth_jwt_jwe_encryption_methods_supported
- )
-
- def oauth_jwt_jwe_algorithms_supported
- JWE::VALID_ALG
- end
-
- def oauth_jwt_jwe_encryption_methods_supported
- JWE::VALID_ENC
- end
-
- def jwk_import(data)
- JWT::JWK.import(data).keypair
- end
-
- def jwt_encode(payload,
- signing_algorithm: oauth_jwt_algorithm || oauth_jwt_keys.keys.first)
- headers = {}
-
- key = oauth_jwt_keys[signing_algorithm] || _jwt_key
- key = key.first if key.is_a?(Array)
-
- case key
- when OpenSSL::PKey::PKey
- jwk = JWT::JWK.new(key)
- headers[:kid] = jwk.kid
-
- key = jwk.keypair
- end
-
- # @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
- payload[:jti] = generate_jti(payload)
- JWT.encode(payload, key, signing_algorithm, headers)
- end
-
- if defined?(JWE)
- def jwt_encode_with_jwe(
- payload,
- jwks: nil,
- encryption_algorithm: oauth_jwt_jwe_algorithm,
- encryption_method: oauth_jwt_jwe_encryption_method,
- jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_keys[[encryption_algorithm, encryption_method]] || oauth_jwt_jwe_key,
- **args
- )
- token = jwt_encode_without_jwe(payload, **args)
-
- return token unless encryption_algorithm && encryption_method
-
- if jwks && jwks.any? { |k| k[:use] == "enc" }
- JWE.__rodauth_oauth_encrypt_from_jwks(token, jwks, alg: encryption_algorithm, enc: encryption_method)
- elsif jwe_key
- jwe_key = jwe_key.first if jwe_key.is_a?(Array)
- params = {
- zip: "DEF",
- copyright: oauth_jwt_jwe_copyright
- }
- params[:enc] = encryption_method if encryption_method
- params[:alg] = encryption_algorithm if encryption_algorithm
- JWE.encrypt(token, jwe_key, **params)
- else
- token
- end
- end
-
- alias_method :jwt_encode_without_jwe, :jwt_encode
- alias_method :jwt_encode, :jwt_encode_with_jwe
- end
-
- def jwt_decode(
- token,
- jwks: nil,
- jws_algorithm: oauth_jwt_algorithm || oauth_jwt_public_key.keys.first || oauth_jwt_keys.keys.first,
- jws_key: oauth_jwt_public_key || oauth_jwt_keys[jws_algorithm] || _jwt_key,
- verify_claims: true,
- verify_jti: true,
- verify_iss: true,
- verify_aud: false
- )
- jws_key = jws_key.first if jws_key.is_a?(Array)
-
- # verifying the JWT implies verifying:
- #
- # issuer: check that server generated the token
- # aud: check the audience field (client is who he says he is)
- # iat: check that the token didn't expire
- #
- # subject can't be verified automatically without having access to the account id,
- # which we don't because that's the whole point.
- #
- verify_claims_params = if verify_claims
- {
- verify_iss: verify_iss,
- iss: issuer,
- # can't use stock aud verification, as it's dependent on the client application id
- verify_aud: false,
- verify_jti: (verify_jti ? method(:verify_jti) : false),
- verify_iat: true
- }
- else
- {}
- end
-
- # decode jwt
- claims = if is_authorization_server?
- if oauth_jwt_legacy_public_key
- algorithms = jwks_set.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
- JWT.decode(token, nil, true, jwks: { keys: jwks_set }, algorithms: algorithms, **verify_claims_params).first
- elsif jwks
- JWT.decode(token, nil, true, algorithms: [jws_algorithm], jwks: { keys: jwks }, **verify_claims_params).first
- elsif jws_key
- JWT.decode(token, jws_key, true, algorithms: [jws_algorithm], **verify_claims_params).first
- end
- elsif (jwks = auth_server_jwks_set)
- algorithms = jwks[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
- JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms, **verify_claims_params).first
- end
-
- return if verify_claims && verify_aud && !verify_aud(claims["aud"], claims["client_id"])
-
- claims
- rescue JWT::DecodeError, JWT::JWKError
- nil
- end
-
- if defined?(JWE)
- def jwt_decode_with_jwe(
- token,
- jwks: nil,
- jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
- jws_encryption_method: oauth_jwt_jwe_encryption_method,
- jwe_key: oauth_jwt_jwe_keys[[jws_encryption_algorithm, jws_encryption_method]] || oauth_jwt_jwe_key,
- **args
- )
-
- token = if jwks && jwks.any? { |k| k[:use] == "enc" }
- JWE.__rodauth_oauth_decrypt_from_jwks(token, jwks, alg: jws_encryption_algorithm, enc: jws_encryption_method)
- elsif jwe_key
- jwe_key = jwe_key.first if jwe_key.is_a?(Array)
- JWE.decrypt(token, jwe_key)
- else
- token
- end
-
- jwt_decode_without_jwe(token, jwks: jwks, **args)
- rescue JWE::DecodeError => e
- jwt_decode_without_jwe(token, jwks: jwks, **args) if e.message.include?("Not enough or too many segments")
- end
-
- alias_method :jwt_decode_without_jwe, :jwt_decode
- alias_method :jwt_decode, :jwt_decode_with_jwe
- end
-
- def jwks_set
- @jwks_set ||= [
- *(
- unless oauth_jwt_public_keys.empty?
- oauth_jwt_public_keys.flat_map { |algo, pkeys| pkeys.map { |pkey| JWT::JWK.new(pkey).export.merge(use: "sig", alg: algo) } }
- end
- ),
- *(
- unless oauth_jwt_jwe_public_keys.empty?
- oauth_jwt_jwe_public_keys.flat_map do |(algo, _enc), pkeys|
- pkeys.map do |pkey|
- JWT::JWK.new(pkey).export.merge(use: "enc", alg: algo)
- end
- end
- end
- ),
- # legacy
- (JWT::JWK.new(oauth_jwt_public_key).export.merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
- (
- if oauth_jwt_legacy_public_key
- JWT::JWK.new(oauth_jwt_legacy_public_key).export.merge(use: "sig", alg: oauth_jwt_legacy_algorithm)
- end
- ),
- (JWT::JWK.new(oauth_jwt_jwe_public_key).export.merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
- ].compact
- end
- else
- # :nocov:
- def jwk_import(_data)
- raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
- end
-
- def jwt_encode(_token)
- raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
- end
-
- def jwt_decode(_token, **)
- raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
- end
-
- def jwks_set
- raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
- end
- # :nocov:
- end
-
- def validate_oauth_revoke_params
- token_hint = param_or_nil("token_type_hint")
-
- throw(:rodauth_error) if !token_hint || token_hint == "access_token"
-
- super
- end
-
- def jwt_response_success(jwt, cache = false)
- response.status = 200
- response["Content-Type"] ||= "application/jwt"
- if cache
- # defaulting to 1-day for everyone, for now at least
- max_age = 60 * 60 * 24
- response["Cache-Control"] = "private, max-age=#{max_age}"
- else
- response["Cache-Control"] = "no-store"
- response["Pragma"] = "no-cache"
- end
- return_response(jwt)
end
end
end