Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spike One login implementation #10107

Open
wants to merge 14 commits into
base: main
Choose a base branch
from

Conversation

CatalinVoineag
Copy link
Contributor

@CatalinVoineag CatalinVoineag commented Nov 26, 2024

Context

This is a POC, this PR won't be merged in main but a lot of the code here will be split out in different PRs once we get the green light from product.

This adds one login integration for the candidate path. The magic link implementation is not visible to the candidate.

The candidate will be redirected to one login test env where they can create an account and we will log them in apply. They can also recover their old account if their previous candidate email doesn't match the one login email.

Agreed technical flow(bottom diagram), this can change though
https://lucid.app/lucidspark/3647e5d9-5075-45d4-abd3-088ee4323fc9/edit?invitationId=inv_8027d1a6-8607-4cb9-a43e-7dc1a8975f38&page=0_0#

Changes proposed in this pull request

  • New columns and tables.
  • Candidate will only have 1 OmniAuthLogin and 1 AccountRecoveryRequest, because we use methods like candidate.create_one_login_auth which delets the old one and creates a new one
  • They are used to auth the user and to allow them to recover their account
  • The OmniAuthLogin holds the information coming back from one login
  • recovered and dismiss_recovery columns are there mostly for the Recover account banner
erDiagram
    Candidate ||--o{ OmniAuthLogin : has_one
    Candidate ||--o{ AccountRecoveryRequest : has_one
    Candidate {
      string email_address
      boolean recovered
      boolean dismiss_recovery
    }
    OmniAuthLogin {
      string token
      string email
      id candidate_id
    }
    AccountRecoveryRequest {
      int code
      string previous_account_email
      id candidate
    }
Loading
Screencast.2024-11-28.17.33.24.mp4

Guidance for review

Go on review app and try to login as a candidate.
Create a one login account and login.
Click on Recover account
Add your email gov/dfe address with a +1 before the @ so [email protected]
You should get an email with a unique code
Enter that code.
You should be logged in.

Try to break this, I have created 3 emails for our team members to recover, so +1,+2,+3
Try putting +100
Put the wrong or a bad email address
Create a new one login account and try to claim it after.
Go nuts

Things to check

  • If the code removes any existing feature flags, a data migration has also been added to delete the entry from the database
  • This code does not rely on migrations in the same Pull Request
  • If this code includes a migration adding or changing columns, it also backfills existing records for consistency
  • If this code adds a column to the DB, decide whether it needs to be in analytics yml file or analytics blocklist
  • API release notes have been updated if necessary
  • If it adds a significant user-facing change, is it documented in the CHANGELOG?
  • Required environment variables have been updated added to the Azure KeyVault
  • Inform data insights team due to database changes
  • Make sure all information from the Trello card is in here
  • Rebased main
  • Cleaned commit history
  • Tested by running locally
  • Add PR link to Trello card

class CreateAccountRecoveryRequests < ActiveRecord::Migration[8.0]
def change
create_table :account_recovery_requests do |t|
t.integer :code, null: false, index: { unique: true }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to encrypt this on the production PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the AccountRecoveryRequest table should have multiple codes attached to it as we want to allow the user to use multiple codes, all with a arbitrary expiration time

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Ha, I should have read the whole thing before I said you should encrypt the code...)

@CatalinVoineag CatalinVoineag added the deploy_v2 Deploy the review app to AKS label Nov 28, 2024
@github-actions github-actions bot temporarily deployed to review_aks-10107 November 28, 2024 11:40 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-10107 November 28, 2024 11:56 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-10107 November 28, 2024 14:15 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-10107 November 28, 2024 14:27 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-10107 November 28, 2024 14:57 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-10107 November 28, 2024 16:10 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-10107 November 28, 2024 16:31 Destroyed
@CatalinVoineag CatalinVoineag requested a review from a team November 29, 2024 09:22
@CatalinVoineag CatalinVoineag marked this pull request as ready for review November 29, 2024 09:23
@CatalinVoineag
Copy link
Contributor Author

Previously you could not login if you already had a candidate account
but no one_login_auth

Now you can
@github-actions github-actions bot temporarily deployed to review_aks-10107 November 29, 2024 11:57 Destroyed
@@ -18,6 +18,8 @@ class Candidate < ApplicationRecord
has_many :degree_qualifications, through: :application_forms
has_many :application_choices, through: :application_forms
has_many :application_references, through: :application_forms
has_one :one_login_auth, dependent: :destroy
has_one :account_recovery_request, dependent: :destroy
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add specs for these to make sure we only have 1 of each at any time.

def call
@valid_account_recovery_request = AccountRecoveryRequest.where(code:)
.where('created_at >= ?', 1.hour.ago).first
@old_candidate = Candidate.find_by(email_address: valid_account_recovery_request&.previous_account_email)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thinking of the unlikely, but possible situation where there could be the same code generated for different users in the past hours. Should we loop through them to find the candidate?

Copy link
Contributor Author

@CatalinVoineag CatalinVoineag Nov 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's a good point, I was hoping that this would always generate a unique code. I think this covers us against this edge case?

def self.generate_code
code = SecureRandom.random_number(100_000..999_999)
AccountRecoveryRequest.generate_code while AccountRecoveryRequest.exists?(code:)
code
end

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! Thanks for pointing that out. I should read the whole thing before I comment!

That said, I'm not sure this will work, or work efficiently, if we are encrypting the codes. It's something to keep in mind.

Copy link
Contributor Author

@CatalinVoineag CatalinVoineag Nov 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good point. I haven't thought about encrypting the code when I wrote this method, I'll do some testing when I create the production PR 👍🏻

normalizes :previous_account_email, with: ->(email) { email.downcase.strip }

def self.generate_code
code = SecureRandom.random_number(100_000..999_999)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means we wouldn't have any codes that start with 0. If we want 6 random digits, rather than a number between 100_000 and 999_999, we should do something like 6.times.collect { rand(0...9) }.join instead -- and it would need to be a string rather that an integer if we want to persist the leading zeros. (also, in general, things with digits aren't numbers unless we are going to do maths to them, eg phone numbers and credit card numbers are strings rather than integers)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure you will have already thought of this, but we'll also want these to be stored hashed_code, using bcrypt or something like that. I haven't done this in awhile, so I can't quite remember, but I remember it being pretty straight forward.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I agree. I'll change it to a true random 6 digit string as you suggested, thanks 👍🏻

@@ -46,6 +48,13 @@ shared:
- withdrawn_or_declined_for_candidate_by_provider
- structured_withdrawal_reasons
- school_placement_auto_selected
account_recovery_requests:
- id
- code
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we put the code in the blocked list? I suppose in practice it will be hashed, but might as well not send it at all as an extra precaution.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we should

@CatalinVoineag CatalinVoineag changed the title One login implementation Spike One login implementation Nov 29, 2024
code: AccountRecoveryRequest.generate_code,
)

AccountRecoveryMailer.send_code(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only send emails to candidates that exist in our system

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So here is the thing, I did the account recovery thing and tried using my personal email address. And it sent me a code. But there is no candidate in this application with my personal email address. I will try it again and take a video.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this just sends emails to any valid email address. I'll keep this conversation open to remind myself to only send emails to candidates that exist in our system

@CatalinVoineag
Copy link
Contributor Author

We also need to allow our users to have access to their one login account, in the header of our pages https://www.sign-in.service.gov.uk/documentation/design-recommendations/let-users-navigate-sign-out

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
deploy_v2 Deploy the review app to AKS DO NOT MERGE
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants