Skip to content

Authentication System

Vino-S edited this page Oct 2, 2026 · 2 revisions

Authentication System

Part of FreeREG-Core-Records (shared with FreeCEN).

Models: app/models/user.rb, app/models/userid_detail.rb · Controllers: app/controllers/users/sessions_controller.rb, app/controllers/users/passwords_controller.rb, app/controllers/manage_resources_controller.rb, app/controllers/application_controller.rb · Config: config/initializers/devise.rb, config/routes.rb (devise_for :users), lib/userid_role.rb

Authentication is Devise on a Mongoid User model. The member's profile, role and permissions live on a separate model, UseridDetail (UseridDetail-System), and the two are linked by User#userid_detail_id. Logging in authenticates a User. Everything after that (menus, roles, county/syndicate scope) works from UseridDetail, found via session and cookie values that ApplicationController keeps in step with Devise.

Much of the code still says "Refinery" (save_to_refinery, check_exists_in_refinery, current_authentication_devise_user, a refinery/authentication/devise view folder). These are leftovers from when login went through the Refinery CMS's Devise user. The Refinery gems are no longer loaded. Where the code says "Refinery user", read it as the Devise User described here.

The two records

User (Devise) UseridDetail (profile)
Identity username (= the member's userid), email userid, email_address
Password encrypted_password password (the same hash, copied across)
Devise modules database_authenticatable, registerable, trackable, recoverable, rememberable, validatable, encryptable (encryptor :freereg) none
Link userid_detail_id (String) found by username ↔ userid, case-insensitively
Role none person_role, secondary_role, county_groups, syndicate_groups, active

Keeping them in sync (app/models/userid_detail.rb):

  • after_create :save_to_refinery finds or creates the User with the same username and copies userid, email_address and the already-hashed password into it. It also sets a reset-password token, so new members can be sent a "set your password" link.
  • before_destroy :delete_refinery_user_and_userid_folder deletes the User (and the member's datafiles folder).
  • Email changes are validated against both collections, so an address can't be taken by one member's User and another member's UseridDetail.
  • after_update :update_refinery is commented out. Changing a member's userid or email on UseridDetail doesn't automatically update the User, except through the explicit code paths in UseridDetailsController. If someone can't log in after an admin edit, compare the two records.

User#before_update :inform_coordinator_of_completion_and_update_userid: when a newly registered transcriber sets their own password for the first time, the syndicate coordinator is emailed that registration is complete (finish_transcriber_creation_setup).

Passwords

Passwords are hashed by a custom Devise encryptor, Devise::Encryptable::Encryptors::Freereg (defined at the top of config/initializers/devise.rb), kept for compatibility with passwords migrated from the original FreeREG system.

Logging in

Routes (config/routes.rb): GET/POST /users/sign_in, DELETE /users/sign_out, GET /users/password/new, GET /users/password/edit, PATCH /users/password, all to the custom controllers in app/controllers/users/.

  1. Users::SessionsController#create permits login, password, remember_me and calls warden.authenticate!. User.find_for_database_authentication matches login case-insensitively against email, or username (a login containing @ is only tried as an email). Any exception is logged and shown as "Invalid email or password".
  2. ApplicationController#after_sign_in_path_for records which UseridDetail the session belongs to and sends the member to /manage_resources/new.
  3. ManageResourcesController#new (is_ok_to_render_actions?) is the gatekeeper:
    • the UseridDetail must exist and be active;
    • roles researcher and pending are refused ("functions still under development");
    • if member_open is false in mongo_config.yml (maintenance mode), only system_administrator and technical get in. The sign-in page shows a maintenance notice in the same case;
    • members whose email confirmation is due are sent to /userid_details/confirm_email_address (need_to_confirm_email_address?);
    • computer accounts (scripted uploaders) get their own minimal page.
  4. set_session stores session[:userid], session[:user_id], session[:first_name], session[:manager] and session[:role], the role the member is acting in. The menu shown is UseridRole::OPTIONS[session[:role]] (lib/userid_role.rb).

Acting role

A member can hold one person_role plus any number of secondary_roles. Members with more than one role pick the role they're acting in from the dashboard's "Change Role" drop-down, and it's stored as session[:role]. Most permission checks compare session[:role] (e.g. reject_access … unless session[:role] == 'system_administrator'), so what a member can do depends on the role they're currently acting in.

Every request: require_login

ApplicationController runs before_action :require_login on every controller unless skipped:

  • not signed in → clear session[:userid_detail_id], session[:devise] and the userid cookie, flash "You must be logged in", redirect to the search page;
  • signed in but User#userid_detail_id blank → sign out with "account is not linked to a member profile";
  • otherwise, keep the session values that older code relies on in step with the signed-in User.

get_user returns the current member's UseridDetail. get_user_info_from_userid additionally loads @user, @manager, @roles (menu for session[:role]) and favourites. Most coordinator actions start with it.

Public pages (search, Database Contents, registration, contact and feedback forms) opt out per controller with skip_before_action :require_login. When adding a controller, it requires login unless you explicitly skip it. /open requires login.

Logging out and password reset

  • Logout (ManageResourcesController#logout) calls Devise sign_out(:user), needed to clear the remember-me cookie, then deletes the userid and remember cookies and reset_session.
  • Forgotten password: Users::PasswordsController is standard Devise recoverable (email with a token valid for 6 hours → set a new password → signed in → redirected to the sign-in page). UseridDetail#change_password (the "change password" action in a member's profile) also just triggers Devise's reset email for the matching User. If no User exists, it only flashes a warning.

Troubleshooting

Symptom Check
"Invalid email or password" for a correct password Does a User exist with that username/email? Does User.encrypted_password equal UseridDetail.password?
"Your account is not linked to a member profile" User.userid_detail_id blank or pointing at a deleted UseridDetail
Logged in but bounced back with "not active" UseridDetail.active is false
"The system is presently undergoing maintenance" member_open is false in mongo_config.yml
Member sees the wrong menu session[:role]: they may be acting in a secondary role; switch with "Change Role"
Admin changed a member's email/userid but login still uses the old one update_refinery is disabled; update the User record as well

Clone this wiki locally