# frozen_string_literal: true require 'jwt' require 'json/jwt' require 'drillbit/configuration' require 'drillbit/tokens/json_web_tokens/invalid' require 'drillbit/tokens/json_web_tokens/null' module Drillbit module Tokens class JsonWebToken TRANSFORMATION_EXCEPTIONS = [ JSON::JWT::Exception, JSON::JWT::InvalidFormat, JSON::JWT::VerificationFailed, JSON::JWT::UnexpectedAlgorithm, JWT::DecodeError, JWT::VerificationError, JWT::ExpiredSignature, JWT::IncorrectAlgorithm, JWT::ImmatureSignature, JWT::InvalidIssuerError, JWT::InvalidIatError, JWT::InvalidAudError, JWT::InvalidSubError, JWT::InvalidJtiError, OpenSSL::PKey::RSAError, OpenSSL::Cipher::CipherError, ].freeze attr_accessor :data, :headers, :private_key def initialize(data:, headers: {}, private_key: Drillbit.configuration.token_private_key) self.data = data self.headers = headers self.private_key = private_key end def self.build_from_request(request_token) return Tokens::JsonWebTokens::Null.instance unless request_token data, headers = *request_token new(data: data, headers: headers) end # rubocop:disable Metrics/ParameterLists, Metrics/AbcSize, Metrics/LineLength def self.build(id: SecureRandom.uuid, audience: Drillbit.configuration.default_token_audience, expiration: Time.now.utc.to_i + (60 * Drillbit.configuration.default_token_expiration_in_minutes), issuer: Drillbit.configuration.default_token_issuer || 'Drillbit', issued_at: Time.now.utc, not_before: Time.now.utc, owner: nil, roles: Drillbit.configuration.default_token_roles, subject: Drillbit.configuration.default_token_subject, subject_id:, token_private_key: Drillbit.configuration.token_private_key) owner ||= subject_id new( private_key: token_private_key, data: { 'aud' => audience, 'exp' => expiration.to_i, 'iat' => issued_at.to_i, 'iss' => issuer, 'jti' => id, 'nbf' => not_before.to_i, 'own' => owner, 'rol' => roles.join(','), 'sid' => subject_id, 'sub' => subject, }, ) end # rubocop:enable Metrics/ParameterLists, Metrics/AbcSize, Metrics/LineLength def valid? true end def blank? data.empty? end def present? data.any? end def empty? data.empty? end def to_h [data, headers] end def audience data['aud'] end def issued_at data['iat'] end def issuer data['iss'] end def expiration data['exp'] end def id data['jti'] end def not_before data['nbf'] end def owner_id data['own'] end def subject_id data['sid'] end def subject data['sub'] end Drillbit.configuration.available_token_roles.each do |role| define_method("#{role}?") do roles.include? role end end def roles @roles ||= data.fetch('rol', '').split(',') end def to_jwt @jwt ||= JSON::JWT.new(data) end def to_jwt_s @jwt_s ||= to_jwt.to_s end def to_jws @jws ||= to_jwt.sign(private_key, 'RS256') end def to_jws_s @jws_s ||= to_jws.to_s end def to_jwe @jwe ||= to_jws.encrypt(private_key, 'RSA-OAEP', 'A256GCM') end def to_jwe_s @jwe_s ||= to_jwe.to_s end def self.from_jwe(encrypted_token, private_key: Drillbit.configuration.token_private_key) return JsonWebTokens::Null.instance if encrypted_token.to_s == '' decrypted_token = JSON::JWT. decode(encrypted_token, private_key). plain_text from_jws(decrypted_token, private_key: private_key) rescue *TRANSFORMATION_EXCEPTIONS JsonWebTokens::Invalid.instance end def self.from_jws(signed_token, private_key: Drillbit.configuration.token_private_key) return JsonWebTokens::Null.instance if signed_token.to_s == '' decoded = JWT.decode( signed_token, private_key, true, algorithm: 'RS256', verify_expiration: true, verify_not_before: true, verify_iat: true, leeway: 5, ) data, headers = *decoded new(data: data, headers: headers, private_key: private_key) rescue *TRANSFORMATION_EXCEPTIONS JsonWebTokens::Invalid.instance end end end end