Skip to content

Conversation

@aaronskiba
Copy link
Contributor

Note

Changes proposed in this PR:

  • Added :confirmable module to User model, which implements email confirmation via Devise.

  • Added the rake task, email_confirmation:clear_all (see lib/tasks/email_confirmation.rake)

    • The rake task sets the values of all email confirmation-related columns (confirmed_at, confirmation_token, and confirmation_sent_at) to nil for all users. It then proceeds to confirm all superusers within the app.
  • Streamline the email confirmation process for existing users

    • By default, Devise's :confirmable module generates a confirmation_token and auto-sends confirmation instructions when a new user is created.
    • Because we are only implementing :confirmable now, existing users can't receive these autosent instructions. However, this PR implements autosent confirmation instructions in the following manner:
      1. A user attempts to sign into the app. (Note, this sign-in can either be performed via the system sign-in, or via the Shibboleth sign-in).
      2. The return if confirmation_instructions_missing_and_handled?(user) line of code is executed. This method belongs to the EmailConfirmationHandler concern, and works as follows:
        i) returns false if the user is either already confirmed or has an outstanding confirmation_token
        ii) Else (the user is unconfirmed AND has no outstanding confirmation_token). Generate the confirmation token and auto-send the confirmation instructions email. (Note: on subsequent sign-in attempts, attempts, the method will return false, preventing redundant emails.)
  • Customise devise.failure.unconfirmed value in various config/locales/*.yml files. The customised value includes an embedded link to /users/confirmation/new. The following is a screenshot of the customised value for :"en-CA":

    • Screenshot from 2025-04-23 13-43-21
  • Updated existing tests

    • Added confirmed_at { Time.current } to User factory
    • Added config.action_mailer.default_options = { from: '[email protected]' } to enable email confirmation tests
  • Add new tests (spec/features/email_confirmation_spec.rb)

    • These tests verify the streamlined email confirmation behaviour for existing users
      • Quite a bit of config was needed to enable testing of behaviour with shibboleth (see spec/support/helpers/omniauth_helper.rb and additions to spec/rails_helper.rb)
    • They also test the clickable link to /users/confirmation/new embedded in the customised devise.failure.unconfirmed flash message.
  • Refactoring

    • Addressed some rubocop offences within SessionsController#create and Users::OmniauthCallbacksController#handle_omniauth
  • Update spec/support/faker.rb Assignments to Use I18n.default_locale #3511

    • Update spec/support/faker.rb to replace 'en' with I18n.default_locale for assigning locales. This change should allow for more accurate testing by using the application's specified locale.
    • Prior to this PR, there was a commit in the aforementioned file that read "Keep this as :en. Faker doesn't have :en-GB". However, I18n.default_locale evaluates to :"en-GB" for this codebase, and all of the tests appear to be passing. Additionally, the branch that this PR is pointed at (upstream/aaron/add-email-confirmation), uses I18n.t(...) for several tests, and this change is required for those tests to pass.

By default, Devise's `:confirmable` module generates a `confirmation_token` and sends confirmation instructions when a new user is created. This commit enhances that behaviour to streamline the email confirmation process for **existing** users.

A new rake task (`lib/tasks/email_confirmation.rake#clear_all`) resets the following confirmation-related fields—`confirmed_at`, `confirmation_token`, and `confirmation_sent_at`—to `nil` for all non-superusers. After this reset, these users will be unable to sign in until they confirm their email.

To avoid requiring manual re-sending of confirmation instructions, we introduce a new check: `User#confirmed_or_has_confirmation_token?`, which returns `false` if a user is unconfirmed *and* has no outstanding confirmation_token.

In the `SessionsController#create` method, if a signing-in user fails the `confirmed_or_has_confirmation_token?` check, we invoke `handle_missing_confirmation_instructions(user)`. This generates a new confirmation_token and sends email instructions. On subsequent sign-in attempts, the check will return `true`, preventing redundant emails.

This approach ensures that email confirmations are triggered automatically and only once per affected user, minimising friction while preserving security.
Moved the creation of the Shibboleth identifier to a helper method
- Replaced double-negative `unless existing_user.nil?` with `if existing_user.present?`
- Used string concatenation with backslash to allow removal of disabled rubocop offence.
`config/environments/test.rb`
- Add "SMTP From address" to mock Devise's sending of confirmation instructions email at time of account creation

`spec/factories/users.rb`
- Set `confirmed_at { Time.current }` in user factory

`spec/features/registrations_spec.rb`
- Change expected path to `root_path`. This is because the user should not be able to access `plans_path` until after they confirm their email
- Add check to verify that the confirmation-related flash message was sent
- Customises Devise's default value for the key `devise.failure.unconfirmed`. The value adds a link to the email confirmation page.
- Updated via `bundle exec rake translation:sync`
Commit 6af91b6 streamlined the email confirmation process for unconfirmed users with no confirmation_token. Specifically, it targeted users attempting to sign in via the app's standard sign-in form.

This change applies the same streamlining to users attempting to sign in via SSO/Shibboleth. Specifically, when a user attempts to sign in this way, the `.confirmed_or_has_confirmation_token?` is performed on them. If it returns false, then the confirmation_token is generated for the user and they are auto-sent a confirmation instructions email. (Refer to commit 6af91b6 for more regarding this streamlined process.)
This change addresses the duplicate code shared between `app/controllers/sessions_controller.rb` and `app/controllers/users/omniauth_callbacks_controller.rb`. It does so by creating `app/models/concerns/email_confirmation_handler.rb` which allows us to better adhere to DRY principles.

- Additionally,  the `user.confirmed? || user.confirmation_token.present?` check has been moved from the User model to the concern. It made sense to have this check as a User method. However, the method itself is simply an or check of two other User methods, and only the `EmailConfirmationHandler` module is currently needing it.
This change maintains the functionality of `def handle_omniauth(scheme)` while making the code more maintainable and addressing its disabled rubocop offences.

The private methods `def handle_omniauth_for_signed_in_user(user, scheme)` and  `def handle_omniauth_for_signed_out_user(user, scheme)` have been created to handle the omniauth logic for signed in vs signed out users respectively. This change is simply a refactor and maintains the pre-existing code logic.
Commit fdb54e03bb71ffaad17bea562e0ca8d87b081059 resolved the rubocop offences within `def handle_omniauth(scheme)`. However, the created helper methods are now triggering rubocop offences as well. Although, I'm hesitant to simply disable these offences, I also don't want to go overboard with refactoring this file.
- Created `spec/support/helpers/omniauth_helper.rb` for simulating authentication with providers like Shibboleth
- Added OmniAuth test config settings in `spec/rails_helper.rb`.
- Tests the custom flow for both system and SSO sign in
- Tests the custom flash messages including the embedded link to the confirmation email request page
This change attempts to resolve the following error:

```
  1) Email Confirmation A user attempts to sign in via the "Sign in with your institutional credentials"
            button. The email is linked to a user account, however the account is
            unconfirmed and has no confirmation token.
     Failure/Error: raise ActionNotFound.new("The action '#{action}' could not be found for #{self.class.name}", self, action)

     AbstractController::ActionNotFound:
       The action 'shibboleth' could not be found for Users::OmniauthCallbacksController

```
The Users::OmniauthCallbacksController#shibboleth action is defined dynamically within the controller. Everything is working locally with commit bcc8d1f, but the test is failing when executed as a GitHub Action.
This change explicitly defines the shibboleth-related Users::OmniauthCallbacksController action
The previous configuration had a hardcoded 'en' locale with a comment stating "Keep this as :en. Faker doesn't have :en-GB". However, testing confirms that Faker now works properly with :"en-GB".

This change should allow for more accurate testing by using the application's specified locale.
- The previous comments mistakenly stated that the user had no confirmation token.
@github-actions
Copy link

</tr>
1 Error
🚫

Please include a CHANGELOG entry.

You can find it at [CHANGELOG.md](https://github.com/DMPRoadmap/roadmap/blob/main/CHANGELOG.md).

Generated by 🚫 Danger

@aaronskiba aaronskiba marked this pull request as draft November 24, 2025 22:02
`def create_shibboleth_identifier(user)` wasn't actually inside of the controller.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants