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

chore: code review for code strategy (magic code login) #3456

Merged
merged 11 commits into from
Aug 29, 2023

Conversation

aeneasr
Copy link
Member

@aeneasr aeneasr commented Aug 25, 2023

  • Unify code validation logic in the persister in one method
  • Remove state engine and simplifies code structure
  • Remove SMS fillers (we will add them when they are needed)
  • Write tests for email address normalization (capitalization)
  • Add unit tests
  • Fix failing e2e tests
  • Fix flaking e2e mail processing
  • Mobile e2e tests

GetHMACCode() string
}

func useOneTimeCode[P any, U interface {
Copy link
Member Author

Choose a reason for hiding this comment

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

This code was the same for login, verification, recovery, and sign up, so I moved it into a common function. Uses some generic magic to make it work nicely. Tried it with duck typing only at first, but it caused issues and would have required reflection.

@aeneasr aeneasr changed the title Login code review chore: code review for code strategy (magic code login) Aug 25, 2023
@codecov
Copy link

codecov bot commented Aug 28, 2023

Codecov Report

❗ No coverage uploaded for pull request base (feat-login-code@f1a0fae). Click here to learn what that means.
The diff coverage is n/a.

❗ Current head fb06b82 differs from pull request most recent head 55fa3eb. Consider uploading reports for the commit 55fa3eb to get more accurate results

@@                Coverage Diff                 @@
##             feat-login-code    #3456   +/-   ##
==================================================
  Coverage                   ?   78.14%           
==================================================
  Files                      ?      339           
  Lines                      ?    22370           
  Branches                   ?        0           
==================================================
  Hits                       ?    17481           
  Misses                     ?     3577           
  Partials                   ?     1312           

Copy link
Member Author

@aeneasr aeneasr left a comment

Choose a reason for hiding this comment

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

@Benehiko fyi

@@ -72,6 +72,21 @@ func TestSchemaExtensionCredentials(t *testing.T) {
},
ct: identity.CredentialsTypeWebAuthn,
},
{
Copy link
Member Author

Choose a reason for hiding this comment

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

These did not have tests

@@ -70,9 +70,14 @@ func (s *Sender) SendCode(ctx context.Context, f flow.Flow, id *identity.Identit
WithSensitiveField("address", addresses).
Debugf("Preparing %s code", f.GetFlowName())

// We generate the code once and use it for all addresses. This is important because
Copy link
Member Author

Choose a reason for hiding this comment

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

Generate one code -> less likely to be brute forced

Copy link
Member Author

Choose a reason for hiding this comment

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

My bad, we actually need to generate unique codes per address, or it will not be possible to distinguish which address was verified, when multiple addresses are sent a code. So yeah, we must make sure to send an individual code for every distinct address.

Copy link
Member Author

Choose a reason for hiding this comment

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

My bad, we actually need to generate unique codes per address, or it will not be possible to distinguish which address was verified, when multiple addresses are sent a code. So yeah, we must make sure to send an individual code for every distinct address.

func useOneTimeCode[P any, U interface {
*P
oneTimeCodeProvider
}](ctx context.Context, p *Persister, flowID uuid.UUID, userProvidedCode string, flowTableName string, foreignKeyName string, opts ...codeOption) (U, error) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Here, Generics can not be avoided because we need to work with and modify the concrete underlying data type, and interfaces have certain limitations such as dealing with uninitialized values.


func (s *Strategy) NextFlowState(f flow.Flow) {
Copy link
Member Author

Choose a reason for hiding this comment

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

This code already exists in flow.NextState()

Comment on lines +138 to +151
case flow.StateChooseMethod:
if err := s.loginSendEmail(ctx, w, r, f, &p); err != nil {
return nil, s.HandleLoginError(r, f, &p, err)
}
return nil, nil
case flow.StateEmailSent:
i, err := s.loginVerifyCode(ctx, r, f, &p)
if err != nil {
return err
return nil, s.HandleLoginError(r, f, &p, err)
}
return i, nil
case flow.StatePassedChallenge:
return nil, s.HandleLoginError(r, f, &p, errors.WithStack(schema.NewNoLoginStrategyResponsible()))
}
Copy link
Member Author

Choose a reason for hiding this comment

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

IMO, this is easier to read and understand what's going on. Is state choose_method? Do this. Is state passed_challenge? Do that.

With the callbacks, it wasn't very clear what is executed when, because CreateCodeHandler does not indicate that the state is choose_method. Plus, we save a lot of code complexity :)

@@ -20,9 +20,9 @@ context("Login error messages with code method", () => {
].forEach(({ route, profile, app }) => {
describe(`for app ${app}`, () => {
before(() => {
cy.proxy(app)
cy.useConfigProfile(profile)
Copy link
Member Author

Choose a reason for hiding this comment

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

Load the config profile first, as that takes the longest to propagate.

return req
tries++
cy.wait(pollInterval)
return req()
Copy link
Member Author

Choose a reason for hiding this comment

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

This caused flakes - because no waiting and req returned as a function, not a function call.

// address was used to verify the code.
//
// See also [this discussion](https://github.com/ory/kratos/pull/3456#discussion_r1307560988).
rawCode := GenerateCode()
Copy link
Member Author

Choose a reason for hiding this comment

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

My bad :D

cy.get('[data-testid="ui/message/1040005"]').should(
"contain",
"An email containing a code has been sent to the email address you provided",
)

cy.get(' input[name="code"]').type("invalid-code")
cy.get('input[name="code"]').type("invalid-code")
Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure what the spaces are here, but they're not needed.

@@ -75,12 +77,14 @@ context("Registration error messages with code method", () => {

cy.get('input[name="traits.email"]').type(email)
cy.submitCodeForm()

cy.url().should("contain", "registration")
cy.get('[data-testid="ui/message/1040005"]').should(
Copy link
Member Author

Choose a reason for hiding this comment

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

@Benehiko the most important aspect of e2e tests are wait assertions. Before:

  1. Go to /registration
  2. Submit form
  3. Wait for URL to be registration (it already is registration though!)
  4. Flaking test failure because sometimes the wait condition is done before the submit is done

Instead, use proper wait conditions that wait for e.g. specific elements such as this one.

Comment on lines +1282 to +1285
expect(filtered.length).to.equal(expectedCount)
mailItem = filtered[0]
} else {
expect(count).to.equal(expectedCount)
Copy link
Member Author

Choose a reason for hiding this comment

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

Counting did not work correctly, caused flakes.

cy.get("button[name=method][value=code]").click()
})

cy.deleteMail({ atLeast: 1 })
Copy link
Member Author

Choose a reason for hiding this comment

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

All methods like cy.getRegistrationCodeFromEmail remove mail automatically. This condition leads to us waiting for about a minute to delete an email, which then times out (and passes silently) because no email was found.

@aeneasr aeneasr marked this pull request as ready for review August 29, 2023 07:06
@aeneasr aeneasr merged commit d8a5c2f into feat-login-code Aug 29, 2023
17 checks passed
@aeneasr aeneasr deleted the login-code-review branch August 29, 2023 07:58
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.

1 participant