-
-
Notifications
You must be signed in to change notification settings - Fork 964
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
Conversation
GetHMACCode() string | ||
} | ||
|
||
func useOneTimeCode[P any, U interface { |
There was a problem hiding this comment.
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.
code
strategy (magic code login)
a1677c6
to
aa2703e
Compare
Codecov Report
@@ Coverage Diff @@
## feat-login-code #3456 +/- ##
==================================================
Coverage ? 78.14%
==================================================
Files ? 339
Lines ? 22370
Branches ? 0
==================================================
Hits ? 17481
Misses ? 3577
Partials ? 1312 |
There was a problem hiding this 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, | |||
}, | |||
{ |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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()
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())) | ||
} |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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:
- Go to
/registration
- Submit form
- Wait for URL to be
registration
(it already is registration though!) - 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.
expect(filtered.length).to.equal(expectedCount) | ||
mailItem = filtered[0] | ||
} else { | ||
expect(count).to.equal(expectedCount) |
There was a problem hiding this comment.
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 }) |
There was a problem hiding this comment.
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.
Mobile e2e tests