# frozen_string_literal: true require 'jwt' # This module implements JWT authentication module JwtAuthenticable # Module that adds jwt authentication methods to the client module Auth include Exceptions include Responses # Authenticates a user. # @raise MissingAuthScope if the jwt does not have the right scope def authenticate_user!(skip_2fa: false) validate_jwt_token! token: authorization_token!, skip_2fa: skip_2fa rescue MissingAuth, MissingAuthScope, InvalidAuthScheme, TwoFANotEnabledError, JWT::VerificationError, JWT::ExpiredSignature => e unauthorized(e.message) end def authenticate_user_without_2fa! authenticate_user!(skip_2fa: true) end # Consider any method below as private and not meant to be used by including classes # Validate that the JWT token signature and the following claims are valid: # - exp # - scope # @param token [String] JWT token string (just the token, with the header, payload and signature separated by '.') # @param skip_2fa [Boolean] When set to true it will not raise a TwoFANotEnabledError if the jwt payload does not # contain the 2fa claim. # @raise AuthorizationError if the user is trying to login with the incorrect rights. # @return [Hash] the JWT payload def validate_jwt_token!(token:, skip_2fa: false) # NOTE: it is still safe if JWT_SECRET_KEY is not set. The method will trigger a JWT exception payload = JWT.decode(token, JwtAuthenticable.config.jwt_secret_key, true, { algorithm: algorithm }).first raise TwoFANotEnabledError if JwtAuthenticable.config.enforce_2fa && !payload['2fa'] && !skip_2fa payload end # Extracts the authorization token from the Authorization header # @note For now we only support Bearer schema with JWT # @raise [Exceptions::MissingAuth] Authorization header not present or empty # @raise [Exceptions::InvalidAuthScheme] Authorization scheme not understood or not supported # @return [String] the JWT token string def authorization_token! raise InvalidIncluder unless defined? request auth_token = request.headers['Authorization'] auth_token ||= request.cookies['bearer_token'] raise MissingAuth if auth_token.nil? || auth_token == '' raise InvalidAuthScheme if auth_token[0..6] != 'Bearer ' auth_token[7..] end def algorithm supported_algos.find { |algo| algo == JwtAuthenticable.config.algorithm } || 'HS256' end def supported_algos SUPPORTED_ALGOS.flat_map { |algo_class| algo_class.const_get(:SUPPORTED) } end end end