# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ class Token < ApplicationModel include Token::Permissions include Token::TriggersSubscriptions before_create :generate_token belongs_to :user, optional: true store :preferences scope :without_sensitive_columns, -> { select(column_names - %w[persistent token]) } =begin create new token token = Token.create(action: 'PasswordReset', user_id: user.id) returns the token create new persistent token token = Token.create( action: 'api', persistent: true, user_id: user.id, preferences: { permission: { 'user_preferences.calendar' => true, } } ) in case if you use it via an controller, e. g. you can verify via "curl -H "Authorization: Token token=…" http://… returns the token =end =begin check token user = Token.check(action: 'PasswordReset', token: '123abc12qweads') check api token with permissions user = Token.check(action: 'api', token: '123abc12qweads', permission: 'admin.session') user = Token.check(action: 'api', token: '123abc12qweads', permission: ['admin.session', 'ticket.agent']) returns user for who this token was created =end def self.check(action:, token:, permission: nil, inactive_user: false) # fetch token token = Token.find_by(action:, token:) return if !token token.user if token.check?(permission:, inactive_user:) end # Check token instance validity # Invalid non-persistant instance is removed # # @param data [Hash] check options # @option data [Boolean] :inactive_user skip checking if referenced user is active # @option data [String, Array] :permission check if token has given permissions # # @return [Boolean] def check?(permission: nil, inactive_user: false) if !persistent && created_at < 1.day.ago destroy return false end # persistent token not valid if user is inactive return false if !inactive_user && persistent && user.active == false # add permission check return false if permission && !permissions?(permission) true end =begin clean up all old non-persistent tokens that are older than 30 days or have their expiration date set in the past Token.cleanup =end def self.cleanup Token.where(persistent: false, created_at: ...30.days.ago).delete_all Token.where(persistent: false, expires_at: ...Time.zone.now).delete_all true end # allows to evaluate token permissions in context of given user instead of owner # @param [User] user to use as context for the given block # @param block to evaluate in given context def with_context(user:, &block) @effective_user = user instance_eval(&block) if block ensure @effective_user = nil end # fetch token for a user with a given action # checks token validity # # @param [String] action name # @param [Integer, User] user # # @return [Token, nil] def self.fetch(action, user_id = UserInfo.current_user_id) token = find_by(action: action, user_id: user_id) token if token&.check? end # Validates whether token exists, belongs to expected user and has not yet expired. # Error is raised if token is not valid # # @param action [String, Symbol] action name # @param token [String, Symbol] token value # @param user [User, Integer] expected owner of the token # # @raise [Token::InvalidToken] Raised if token is not valid # @return [Token] returns token object if all checks pass def self.validate!(action:, token:, user: UserInfo.current_user_id) token = find_by(action:, token:, user:) raise TokenAbsent if !token raise TokenExpired if token.expired? token end class TokenInvalid < StandardError; end class TokenExpired < TokenInvalid; end class TokenAbsent < TokenInvalid; end # creates or returns existing token # # @param [String] action name # @param [Integer, User] user # # @return [String] def self.ensure_token!(action, user_id = UserInfo.current_user_id, persistent: false) instance = fetch(action, user_id) return instance.token if instance.present? create!(action: action, user_id: user_id, persistent: persistent).token end # regenerates an existing token # # @param [String] action name # @param [Integer, User] user # # @return [String] def self.renew_token!(action, user_id = UserInfo.current_user_id, persistent: false) instance = fetch(action, user_id) return create(action: action, user_id: user_id, persistent: persistent).token if !instance instance.renew_token! end # regenerates an existing token # # @return [String] def renew_token! generate_token save! token end def visible_in_frontend? action == 'api' && persistent end def expired? return false if expires_at.nil? Time.zone.now >= expires_at end private def generate_token loop do self.token = SecureRandom.urlsafe_base64(48) break if !Token.exists?(token: token) end true end # token owner or user set by #with_context def effective_user @effective_user || user end end