snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, email, extractOTPCode, gen } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
context("Account Recovery Errors", () => {
|
||||
;[
|
||||
{
|
||||
recovery: react.recovery,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
{
|
||||
recovery: express.recovery,
|
||||
app: "express" as "express",
|
||||
profile: "recovery",
|
||||
},
|
||||
].forEach(({ recovery, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.deleteMail()
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.deleteMail()
|
||||
cy.useConfig((builder) =>
|
||||
builder
|
||||
.longRecoveryLifespan()
|
||||
.longCodeLifespan()
|
||||
.disableVerification()
|
||||
.enableRecovery()
|
||||
.useRecoveryStrategy("code")
|
||||
.notifyUnknownRecipients("recovery", false),
|
||||
)
|
||||
})
|
||||
|
||||
it("should invalidate flow if wrong code is submitted too often", () => {
|
||||
const identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
cy.visit(recovery)
|
||||
cy.get(appPrefix(app) + "input[name='email']").type(identity.email)
|
||||
cy.get("button[value='code']").click()
|
||||
cy.get('[data-testid="ui/message/1060003"]').should(
|
||||
"have.text",
|
||||
"An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.",
|
||||
)
|
||||
cy.recoveryEmailWithCode({
|
||||
expect: { email: identity.email, enterCode: false },
|
||||
})
|
||||
for (let i = 0; i < 5; i++) {
|
||||
cy.get("input[name='code']").type((i + "").repeat(8)) // Invalid code
|
||||
cy.get("button[value='code']").click()
|
||||
cy.get('[data-testid="ui/message/4060006"]').should(
|
||||
"have.text",
|
||||
"The recovery code is invalid or has already been used. Please try again.",
|
||||
)
|
||||
cy.noSession()
|
||||
}
|
||||
|
||||
cy.get("input[name='code']").type("12312312") // Invalid code
|
||||
cy.get("button[value='code']").click()
|
||||
cy.get('[data-testid="ui/message/4000001"]').should(
|
||||
"have.text",
|
||||
"The request was submitted too often. Please request another code.",
|
||||
)
|
||||
cy.noSession()
|
||||
cy.get(appPrefix(app) + "input[name='email']").type(identity.email)
|
||||
cy.get("button[value='code']").click()
|
||||
cy.get('[data-testid="ui/message/1060003"]').should(
|
||||
"have.text",
|
||||
"An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.",
|
||||
)
|
||||
cy.recoveryEmailWithCode({
|
||||
expect: { email: identity.email, enterCode: false },
|
||||
})
|
||||
})
|
||||
|
||||
it("shows code expired message if expired code is submitted", () => {
|
||||
cy.visit(recovery)
|
||||
|
||||
cy.shortCodeLifespan()
|
||||
|
||||
const identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
cy.get(appPrefix(app) + "input[name='email']").type(identity.email)
|
||||
cy.get("button[value='code']").click()
|
||||
cy.recoveryEmailWithCode({ expect: { email: identity.email } })
|
||||
cy.get("button[value='code']").click()
|
||||
|
||||
cy.get('[data-testid="ui/message/4060006"]').should(
|
||||
"contain.text",
|
||||
"The recovery code is invalid or has already been used. Please try again.",
|
||||
)
|
||||
|
||||
cy.noSession()
|
||||
})
|
||||
|
||||
it("should receive a stub email when recovering a non-existent account", () => {
|
||||
cy.notifyUnknownRecipients("recovery")
|
||||
cy.visit(recovery)
|
||||
|
||||
const email = gen.email()
|
||||
cy.get(appPrefix(app) + 'input[name="email"]').type(email)
|
||||
cy.get('button[value="code"]').click()
|
||||
|
||||
cy.location("pathname").should("eq", "/recovery")
|
||||
cy.get('[data-testid="ui/message/1060003"]').should(
|
||||
"have.text",
|
||||
"An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.",
|
||||
)
|
||||
cy.get('input[name="code"]').should("be.visible")
|
||||
|
||||
cy.getMail({
|
||||
subject: "Account access attempted",
|
||||
email,
|
||||
}).should((message) => {
|
||||
expect(message.subject).to.equal("Account access attempted")
|
||||
expect(message.fromAddress.trim()).to.equal("no-reply@ory.kratos.sh")
|
||||
expect(message.toAddresses).to.have.length(1)
|
||||
expect(message.toAddresses[0].trim()).to.equal(email)
|
||||
|
||||
const code = extractOTPCode(message.body)
|
||||
expect(code).to.be.null
|
||||
})
|
||||
})
|
||||
|
||||
it("should cause form errors", () => {
|
||||
cy.visit(recovery)
|
||||
cy.removeAttribute(["input[name='email']"], "required")
|
||||
cy.get('button[value="code"]').click()
|
||||
cy.get('[data-testid="ui/message/4000002"]').should(
|
||||
"contain.text",
|
||||
"Property email is missing.",
|
||||
)
|
||||
cy.get('[name="method"][value="code"]').should("exist")
|
||||
})
|
||||
|
||||
it("is unable to recover the account if the code is incorrect", () => {
|
||||
const identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
cy.visit(recovery)
|
||||
cy.get(appPrefix(app) + "input[name='email']").type(identity.email)
|
||||
cy.get("button[value='code']").click()
|
||||
cy.get('[data-testid="ui/message/1060003"]').should(
|
||||
"have.text",
|
||||
"An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.",
|
||||
)
|
||||
cy.get("input[name='code']").type("01234567") // Invalid code
|
||||
cy.get("button[value='code']").click()
|
||||
cy.get('[data-testid="ui/message/4060006"]').should(
|
||||
"have.text",
|
||||
"The recovery code is invalid or has already been used. Please try again.",
|
||||
)
|
||||
cy.noSession()
|
||||
})
|
||||
|
||||
it("should cause non-repeating form errors after submitting empty form twice. see: #2512", () => {
|
||||
cy.visit(recovery)
|
||||
cy.location("pathname").should("eq", "/recovery")
|
||||
cy.removeAttribute(["input[name='email']"], "required")
|
||||
cy.get('button[value="code"]').click()
|
||||
cy.get('[data-testid="ui/message/4000002"]').should(
|
||||
"contain.text",
|
||||
"Property email is missing.",
|
||||
)
|
||||
cy.get("form")
|
||||
.find('[data-testid="ui/message/4000002"]')
|
||||
.should("have.length", 1)
|
||||
cy.get('[name="method"][value="code"]').should("exist")
|
||||
})
|
||||
|
||||
it("remote recovery email template (recovery_code_valid)", () => {
|
||||
cy.remoteCourierRecoveryCodeTemplates()
|
||||
const identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
cy.visit(recovery)
|
||||
cy.get(appPrefix(app) + "input[name='email']").type(identity.email)
|
||||
cy.get("button[value='code']").click()
|
||||
cy.get('[data-testid="ui/message/1060003"]').should(
|
||||
"have.text",
|
||||
"An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.",
|
||||
)
|
||||
|
||||
cy.getMail({
|
||||
subject: "recovery_code_valid REMOTE TEMPLATE SUBJECT",
|
||||
email: identity.email,
|
||||
}).then((mail) => {
|
||||
expect(mail.body).to.include("recovery_code_valid REMOTE TEMPLATE")
|
||||
})
|
||||
})
|
||||
|
||||
it("remote recovery email template (recovery_code_invalid)", () => {
|
||||
cy.notifyUnknownRecipients("recovery")
|
||||
cy.remoteCourierRecoveryCodeTemplates()
|
||||
cy.visit(recovery)
|
||||
const email = gen.email()
|
||||
cy.get(appPrefix(app) + "input[name='email']").type(email)
|
||||
cy.get("button[value='code']").click()
|
||||
cy.get('[data-testid="ui/message/1060003"]').should(
|
||||
"have.text",
|
||||
"An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.",
|
||||
)
|
||||
|
||||
cy.getMail({
|
||||
subject: "recovery_code_invalid REMOTE TEMPLATE SUBJECT",
|
||||
email: email,
|
||||
}).then((mail) => {
|
||||
expect(mail.body).to.include("recovery_code_invalid REMOTE TEMPLATE")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,201 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, assertRecoveryAddress, gen } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
context("Account Recovery With Code Success", () => {
|
||||
;[
|
||||
{
|
||||
recovery: react.recovery,
|
||||
base: react.base,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
{
|
||||
recovery: express.recovery,
|
||||
base: express.base,
|
||||
app: "express" as "express",
|
||||
profile: "recovery",
|
||||
},
|
||||
].forEach(({ recovery, profile, base, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.deleteMail()
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
let identity
|
||||
|
||||
beforeEach(() => {
|
||||
cy.deleteMail()
|
||||
|
||||
cy.useConfig((builder) =>
|
||||
builder
|
||||
.longRecoveryLifespan()
|
||||
.disableVerification()
|
||||
.enableRecovery()
|
||||
.useRecoveryStrategy("code")
|
||||
.notifyUnknownRecipients("recovery", false),
|
||||
)
|
||||
|
||||
identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
})
|
||||
|
||||
it("should contain the recovery address in the session", () => {
|
||||
cy.visit(recovery)
|
||||
cy.login({ ...identity, cookieUrl: base })
|
||||
cy.getSession().should(assertRecoveryAddress(identity))
|
||||
})
|
||||
|
||||
it("should perform a recovery flow", () => {
|
||||
cy.visit(recovery)
|
||||
cy.get(appPrefix(app) + "input[name='email']").type(identity.email)
|
||||
cy.get("button[value='code']").click()
|
||||
cy.get('[data-testid="ui/message/1060003"]').should(
|
||||
"have.text",
|
||||
"An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.",
|
||||
)
|
||||
|
||||
cy.recoveryEmailWithCode({ expect: { email: identity.email } })
|
||||
cy.get("button[value='code']").click()
|
||||
|
||||
cy.get('[data-testid="ui/message/1060001"]', { timeout: 30000 }).should(
|
||||
"contain.text",
|
||||
"You successfully recovered your account. ",
|
||||
)
|
||||
|
||||
cy.getSession()
|
||||
cy.location("pathname").should("eq", "/settings")
|
||||
|
||||
const newPassword = gen.password()
|
||||
cy.get(appPrefix(app) + 'input[name="password"]')
|
||||
.clear()
|
||||
.type(newPassword)
|
||||
cy.get('button[value="password"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
cy.get('input[name="password"]').should("be.empty")
|
||||
|
||||
cy.logout()
|
||||
cy.login({
|
||||
email: identity.email,
|
||||
password: newPassword,
|
||||
cookieUrl: base,
|
||||
})
|
||||
})
|
||||
|
||||
it("should recover account with correct code after entering wrong code", () => {
|
||||
cy.visit(recovery)
|
||||
cy.get(appPrefix(app) + "input[name='email']").type(identity.email)
|
||||
cy.get("button[value='code']").click()
|
||||
cy.get('[data-testid="ui/message/1060003"]').should(
|
||||
"have.text",
|
||||
"An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.",
|
||||
)
|
||||
cy.get("input[name='code']").type("12312312") // Invalid code
|
||||
cy.get("button[value='code']").click()
|
||||
cy.get('[data-testid="ui/message/4060006"]').should(
|
||||
"have.text",
|
||||
"The recovery code is invalid or has already been used. Please try again.",
|
||||
)
|
||||
cy.noSession()
|
||||
cy.recoveryEmailWithCode({ expect: { email: identity.email } })
|
||||
cy.get("button[value='code']").click()
|
||||
|
||||
cy.get('[data-testid="ui/message/1060001"]', { timeout: 30000 }).should(
|
||||
"contain.text",
|
||||
"You successfully recovered your account. ",
|
||||
)
|
||||
cy.getSession()
|
||||
cy.location("pathname").should("eq", "/settings")
|
||||
cy.get('input[name="traits.email"]').should(
|
||||
"have.value",
|
||||
identity.email,
|
||||
)
|
||||
})
|
||||
|
||||
it("should recover account after resending code", () => {
|
||||
cy.visit(recovery)
|
||||
cy.get(appPrefix(app) + "input[name='email']").type(identity.email)
|
||||
cy.get("button[value='code']").click()
|
||||
cy.get('[data-testid="ui/message/1060003"]').should(
|
||||
"have.text",
|
||||
"An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.",
|
||||
)
|
||||
|
||||
cy.recoveryEmailWithCode({
|
||||
expect: { email: identity.email, enterCode: false },
|
||||
})
|
||||
|
||||
cy.get("button[name='email']").click() // resend code
|
||||
cy.noSession()
|
||||
|
||||
cy.recoveryEmailWithCode({
|
||||
expect: { email: identity.email },
|
||||
})
|
||||
cy.get("button[value='code']").click()
|
||||
|
||||
cy.get('[data-testid="ui/message/1060001"]', { timeout: 30000 }).should(
|
||||
"contain.text",
|
||||
"You successfully recovered your account. ",
|
||||
)
|
||||
cy.getSession()
|
||||
cy.location("pathname").should("eq", "/settings")
|
||||
cy.get('input[name="traits.email"]').should(
|
||||
"have.value",
|
||||
identity.email,
|
||||
)
|
||||
})
|
||||
it("should not notify an unknown recipient", () => {
|
||||
const recipient = gen.email()
|
||||
|
||||
cy.visit(recovery)
|
||||
cy.get('input[name="email"]').type(recipient)
|
||||
cy.get(`[name="method"][value="code"]`).click()
|
||||
|
||||
cy.getCourierMessages().then((messages) => {
|
||||
expect(messages.map((msg) => msg.recipient)).to.not.include(recipient)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should recover, set password and be redirected", () => {
|
||||
const app = "express" as "express"
|
||||
cy.deleteMail()
|
||||
cy.useConfigProfile("recovery")
|
||||
cy.proxy(app)
|
||||
|
||||
cy.useConfig((builder) =>
|
||||
builder
|
||||
.longRecoveryLifespan()
|
||||
.longCodeLifespan()
|
||||
.disableVerification()
|
||||
.enableRecovery()
|
||||
.useRecoveryStrategy("code"),
|
||||
)
|
||||
|
||||
const identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
cy.visit(express.recovery + "?return_to=https://www.example.org/")
|
||||
cy.get("input[name='email']").type(identity.email)
|
||||
cy.get("button[value='code']").click()
|
||||
cy.get('[data-testid="ui/message/1060003"]').should(
|
||||
"have.text",
|
||||
"An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.",
|
||||
)
|
||||
|
||||
cy.recoveryEmailWithCode({ expect: { email: identity.email } })
|
||||
cy.get("button[value='code']").click()
|
||||
|
||||
cy.getSession()
|
||||
cy.location("pathname").should("eq", "/settings")
|
||||
|
||||
cy.get('input[name="password"]').clear().type(gen.password())
|
||||
cy.get('button[value="password"]').click()
|
||||
cy.url().should("eq", "https://www.example.org/")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,241 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, gen, parseHtml } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
context("Account Recovery Errors", () => {
|
||||
;[
|
||||
{
|
||||
recovery: react.recovery,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
{
|
||||
recovery: express.recovery,
|
||||
app: "express" as "express",
|
||||
profile: "recovery",
|
||||
},
|
||||
].forEach(({ recovery, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.deleteMail()
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.deleteMail()
|
||||
cy.clearAllCookies()
|
||||
|
||||
cy.useConfig((builder) =>
|
||||
builder
|
||||
.longRecoveryLifespan()
|
||||
.longLinkLifespan()
|
||||
.disableVerification()
|
||||
.enableRecovery()
|
||||
.useRecoveryStrategy("link")
|
||||
.disableRecoveryStrategy("code")
|
||||
.notifyUnknownRecipients("verification", false),
|
||||
)
|
||||
})
|
||||
|
||||
it("responds with a HTML response on link click of an API flow if the link is expired", () => {
|
||||
cy.visit(recovery)
|
||||
|
||||
cy.shortLinkLifespan()
|
||||
|
||||
const identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
cy.recoverApi({ email: identity.email })
|
||||
cy.recoverEmailButExpired({ expect: { email: identity.email } })
|
||||
|
||||
cy.get('[data-testid="ui/message/4060005"]').should(
|
||||
"contain.text",
|
||||
"The recovery flow expired",
|
||||
)
|
||||
|
||||
cy.noSession()
|
||||
})
|
||||
|
||||
it("responds with a HTML response on link click of an API flow if the flow is expired", () => {
|
||||
cy.visit(recovery)
|
||||
|
||||
cy.updateConfigFile((config) => {
|
||||
config.selfservice.flows.recovery.lifespan = "1s"
|
||||
return config
|
||||
})
|
||||
|
||||
const identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
cy.recoverApi({ email: identity.email })
|
||||
cy.wait(1000)
|
||||
|
||||
cy.getMail({
|
||||
subject: "Recover access to your account",
|
||||
email: identity.email,
|
||||
}).then((message) => {
|
||||
expect(message.subject).to.equal("Recover access to your account")
|
||||
expect(message.toAddresses[0].trim()).to.equal(identity.email)
|
||||
|
||||
const link = parseHtml(message.body).querySelector("a")
|
||||
cy.longRecoveryLifespan()
|
||||
cy.visit(link.href)
|
||||
})
|
||||
|
||||
cy.get('[data-testid="ui/message/4060005"]').should(
|
||||
"contain.text",
|
||||
"The recovery flow expired",
|
||||
)
|
||||
|
||||
cy.noSession()
|
||||
})
|
||||
|
||||
it("should receive a stub email when recovering a non-existent account", () => {
|
||||
cy.notifyUnknownRecipients("recovery")
|
||||
cy.visit(recovery)
|
||||
|
||||
const email = gen.email()
|
||||
cy.get(appPrefix(app) + 'input[name="email"]').type(email)
|
||||
cy.get('button[value="link"]').click()
|
||||
|
||||
cy.location("pathname").should("eq", "/recovery")
|
||||
cy.get('[data-testid="ui/message/1060002"]').should(
|
||||
"have.text",
|
||||
"An email containing a recovery link has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.",
|
||||
)
|
||||
cy.get('input[name="email"]').should("have.value", email)
|
||||
|
||||
cy.getMail({
|
||||
subject: "Account access attempted",
|
||||
email,
|
||||
}).then((message) => {
|
||||
expect(message.subject).to.equal("Account access attempted")
|
||||
expect(message.fromAddress.trim()).to.equal("no-reply@ory.kratos.sh")
|
||||
expect(message.toAddresses).to.have.length(1)
|
||||
expect(message.toAddresses[0].trim()).to.equal(email)
|
||||
|
||||
const link = parseHtml(message.body).querySelector("a")
|
||||
expect(link).to.be.null
|
||||
})
|
||||
})
|
||||
|
||||
it("should cause form errors", () => {
|
||||
cy.visit(recovery)
|
||||
|
||||
// we need to remove the required attribute of the element since the browser prevents us from submitting the form
|
||||
// this is to simulate the case where the form is submitted with an empty input field.
|
||||
cy.removeAttribute(["input[name='email']"], "required")
|
||||
|
||||
cy.get('button[value="link"]').click()
|
||||
cy.get('[data-testid="ui/message/4000002"]').should(
|
||||
"contain.text",
|
||||
"Property email is missing.",
|
||||
)
|
||||
cy.get('[name="method"][value="link"]').should("exist")
|
||||
})
|
||||
|
||||
it("should cause non-repeating form errors after submitting empty form twice. see: #2512", () => {
|
||||
cy.visit(recovery)
|
||||
// we need to remove the required attribute of the element since the browser prevents us from submitting the form
|
||||
// this is to simulate the case where the form is submitted with an empty input field.
|
||||
cy.removeAttribute(["input[name='email']"], "required")
|
||||
cy.get('button[value="link"]').click()
|
||||
cy.location("pathname").should("eq", "/recovery")
|
||||
|
||||
// we need to remove the required attribute of the element since the browser prevents us from submitting the form
|
||||
// this is to simulate the case where the form is submitted with an empty input field.
|
||||
cy.removeAttribute(["input[name='email']"], "required")
|
||||
cy.get('button[value="link"]').click()
|
||||
cy.get('[data-testid="ui/message/4000002"]').should(
|
||||
"contain.text",
|
||||
"Property email is missing.",
|
||||
)
|
||||
cy.get("form")
|
||||
.find('[data-testid="ui/message/4000002"]')
|
||||
.should("have.length", 1)
|
||||
cy.get('[name="method"][value="link"]').should("exist")
|
||||
})
|
||||
|
||||
it("is unable to recover the email address if the code is expired", () => {
|
||||
cy.shortLinkLifespan()
|
||||
const identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
cy.recoverApi({ email: identity.email })
|
||||
cy.recoverEmailButExpired({ expect: { email: identity.email } })
|
||||
|
||||
cy.get('[data-testid="ui/message/4060005"]').should(
|
||||
"contain.text",
|
||||
"The recovery flow expired",
|
||||
)
|
||||
|
||||
cy.noSession()
|
||||
})
|
||||
|
||||
it("is unable to recover the account if the code is incorrect", () => {
|
||||
const identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
cy.recoverApi({ email: identity.email })
|
||||
|
||||
cy.getMail({
|
||||
subject: "Recover access to your account",
|
||||
email: identity.email,
|
||||
}).then((mail) => {
|
||||
console.log(mail)
|
||||
const link = parseHtml(mail.body).querySelector("a")
|
||||
cy.visit(link.href + "-not") // add random stuff to the confirm challenge
|
||||
cy.get('[data-testid="ui/message/4060004"]').should(
|
||||
"have.text",
|
||||
"The recovery token is invalid or has already been used. Please retry the flow.",
|
||||
)
|
||||
cy.noSession()
|
||||
})
|
||||
})
|
||||
|
||||
it("is unable to recover the account using the token twice", () => {
|
||||
const identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
cy.recoverApi({ email: identity.email })
|
||||
|
||||
cy.getMail({
|
||||
subject: "Recover access to your account",
|
||||
email: identity.email,
|
||||
}).then((mail) => {
|
||||
const link = parseHtml(mail.body).querySelector("a")
|
||||
|
||||
// Workaround for cypress cy.visit limitation.
|
||||
cy.request(link.href).should((response) => {
|
||||
// add random stuff to the confirm challenge
|
||||
expect(response.status).to.eq(200)
|
||||
})
|
||||
|
||||
cy.clearAllCookies()
|
||||
|
||||
cy.visit(link.href)
|
||||
cy.get('[data-testid="ui/message/4060004"]').should(
|
||||
"have.text",
|
||||
"The recovery token is invalid or has already been used. Please retry the flow.",
|
||||
)
|
||||
cy.noSession()
|
||||
})
|
||||
})
|
||||
|
||||
it("invalid remote recovery email template", () => {
|
||||
cy.notifyUnknownRecipients("recovery")
|
||||
cy.remoteCourierRecoveryTemplates()
|
||||
const identity = gen.identityWithWebsite()
|
||||
cy.recoverApi({ email: identity.email })
|
||||
|
||||
cy.getMail({
|
||||
subject: "Account Access Attempted",
|
||||
email: identity.email,
|
||||
}).then((mail) => {
|
||||
expect(mail.body).to.include(
|
||||
"this is a remote invalid recovery template",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, assertRecoveryAddress, gen } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
context("Account Recovery Success", () => {
|
||||
;[
|
||||
{
|
||||
recovery: react.recovery,
|
||||
base: react.base,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
{
|
||||
recovery: express.recovery,
|
||||
base: express.base,
|
||||
app: "express" as "express",
|
||||
profile: "recovery",
|
||||
},
|
||||
].forEach(({ recovery, profile, base, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.deleteMail()
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
let identity
|
||||
|
||||
beforeEach(() => {
|
||||
cy.deleteMail()
|
||||
|
||||
cy.useConfig((builder) =>
|
||||
builder
|
||||
.longRecoveryLifespan()
|
||||
.longLinkLifespan()
|
||||
.disableVerification()
|
||||
.enableRecovery()
|
||||
.useRecoveryStrategy("link"),
|
||||
)
|
||||
|
||||
identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
})
|
||||
|
||||
it("should contain the recovery address in the session", () => {
|
||||
cy.visit(recovery)
|
||||
cy.login({ ...identity, cookieUrl: base })
|
||||
cy.getSession().should(assertRecoveryAddress(identity))
|
||||
})
|
||||
|
||||
it("should perform a recovery flow", () => {
|
||||
cy.recoverApi({ email: identity.email })
|
||||
|
||||
cy.recoverEmail({ expect: identity })
|
||||
|
||||
cy.getSession()
|
||||
cy.location("pathname").should("eq", "/settings")
|
||||
|
||||
const newPassword = gen.password()
|
||||
cy.get(appPrefix(app) + 'input[name="password"]')
|
||||
.clear()
|
||||
.type(newPassword)
|
||||
cy.get('button[value="password"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
cy.get('input[name="password"]').should("be.empty")
|
||||
|
||||
cy.logout()
|
||||
cy.login({
|
||||
email: identity.email,
|
||||
password: newPassword,
|
||||
cookieUrl: base,
|
||||
})
|
||||
})
|
||||
|
||||
it("should not notify an unknown recipient", () => {
|
||||
const recipient = gen.email()
|
||||
|
||||
cy.visit(recovery)
|
||||
cy.get('input[name="email"]').type(recipient)
|
||||
cy.get(`[name="method"][value="link"]`).click()
|
||||
|
||||
cy.getCourierMessages().then((messages) => {
|
||||
expect(messages.map((msg) => msg.recipient)).to.not.include(recipient)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should recover, set password and be redirected", () => {
|
||||
const app = "express" as "express"
|
||||
|
||||
cy.deleteMail()
|
||||
cy.useConfigProfile("recovery")
|
||||
cy.proxy(app)
|
||||
|
||||
cy.deleteMail()
|
||||
|
||||
cy.useConfig((builder) =>
|
||||
builder
|
||||
.longRecoveryLifespan()
|
||||
.longLinkLifespan()
|
||||
.disableVerification()
|
||||
.enableRecovery(),
|
||||
)
|
||||
|
||||
const identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
|
||||
cy.recoverApi({
|
||||
email: identity.email,
|
||||
returnTo: "https://www.example.org/",
|
||||
})
|
||||
|
||||
cy.recoverEmail({ expect: identity })
|
||||
|
||||
cy.getSession()
|
||||
cy.location("pathname").should("eq", "/settings")
|
||||
|
||||
cy.get(appPrefix(app) + 'input[name="password"]')
|
||||
.clear()
|
||||
.type(gen.password())
|
||||
cy.get('button[value="password"]').click()
|
||||
cy.url().should("eq", "https://www.example.org/")
|
||||
})
|
||||
|
||||
it("should recover even if already logged into another account", () => {
|
||||
const app = "express" as "express"
|
||||
|
||||
cy.deleteMail()
|
||||
cy.useConfigProfile("recovery")
|
||||
cy.proxy(app)
|
||||
|
||||
cy.deleteMail()
|
||||
|
||||
cy.useConfig((builder) => builder.disableVerification())
|
||||
|
||||
const identity1 = gen.identityWithWebsite()
|
||||
cy.registerApi(identity1)
|
||||
const identity2 = gen.identityWithWebsite()
|
||||
cy.registerApi(identity2)
|
||||
|
||||
cy.recoverApi({ email: identity2.email })
|
||||
|
||||
// first log in as identity1
|
||||
|
||||
cy.visit(express.login)
|
||||
|
||||
cy.get(appPrefix(app) + 'input[name="identifier"]').type(identity1.email)
|
||||
cy.get('input[name="password"]').type(identity1.password)
|
||||
cy.get('button[value="password"]').click()
|
||||
|
||||
cy.location("pathname").should("not.contain", "/login")
|
||||
|
||||
// then recover identity2, while still logged in as identity1
|
||||
|
||||
cy.recoverEmail({ expect: identity2 })
|
||||
|
||||
cy.getSession()
|
||||
cy.location("pathname").should("eq", "/settings")
|
||||
cy.get('input[name="traits.email"]').should("have.value", identity2.email)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, gen } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
import { TOTP } from "otpauth"
|
||||
|
||||
context("Recovery with `return_to`", () => {
|
||||
;[
|
||||
{
|
||||
recovery: react.recovery,
|
||||
settings: react.settings,
|
||||
base: react.base,
|
||||
app: "react" as "react",
|
||||
profile: "recovery-mfa",
|
||||
},
|
||||
{
|
||||
recovery: express.recovery,
|
||||
settings: express.settings,
|
||||
app: "express" as "express",
|
||||
profile: "recovery-mfa",
|
||||
},
|
||||
].forEach(({ app, recovery, settings, profile }) => {
|
||||
describe("Recovery with `return_to` query paramter success", () => {
|
||||
before(() => {
|
||||
cy.deleteMail()
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
let identity: any
|
||||
|
||||
beforeEach(() => {
|
||||
cy.deleteMail()
|
||||
|
||||
cy.useConfig((builder) =>
|
||||
builder
|
||||
.longRecoveryLifespan()
|
||||
.disableVerification()
|
||||
.enableRecovery()
|
||||
.useRecoveryStrategy("code")
|
||||
.notifyUnknownRecipients("recovery", false)
|
||||
.longPrivilegedSessionTime()
|
||||
.requireStrictAal(),
|
||||
)
|
||||
cy.clearAllCookies()
|
||||
identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
})
|
||||
|
||||
const doRecovery = () => {
|
||||
cy.get(appPrefix(app) + "input[name='email']").type(identity.email)
|
||||
cy.get("button[value='code']").click()
|
||||
|
||||
cy.get('[data-testid="ui/message/1060003"]').should(
|
||||
"have.text",
|
||||
"An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.",
|
||||
)
|
||||
|
||||
cy.recoveryEmailWithCode({ expect: { email: identity.email } })
|
||||
cy.get("button[value='code']").click()
|
||||
}
|
||||
|
||||
it("should return to the `return_to` url after successful account recovery and settings update", () => {
|
||||
cy.visit(recovery + "?return_to=https://www.example.org/")
|
||||
doRecovery()
|
||||
|
||||
cy.get('[data-testid="ui/message/1060001"]', { timeout: 30000 }).should(
|
||||
"contain.text",
|
||||
"You successfully recovered your account. ",
|
||||
)
|
||||
|
||||
cy.getSession()
|
||||
cy.location("pathname").should("eq", "/settings")
|
||||
|
||||
const newPassword = gen.password()
|
||||
cy.get(appPrefix(app) + 'input[name="password"]')
|
||||
.clear()
|
||||
.type(newPassword)
|
||||
cy.get('button[value="password"]').click()
|
||||
|
||||
cy.location("hostname").should("eq", "www.example.org")
|
||||
})
|
||||
|
||||
it("should return to the `return_to` url even with mfa enabled after successful account recovery and settings update", () => {
|
||||
cy.visit(settings)
|
||||
cy.get('input[name="identifier"]').type(identity.email)
|
||||
cy.get('input[name="password"]').type(identity.password)
|
||||
cy.get('button[value="password"]').click()
|
||||
cy.visit(settings)
|
||||
|
||||
// enable mfa for this account
|
||||
let secret: string
|
||||
cy.get('[data-testid="node/text/totp_secret_key/text"]').then(($e) => {
|
||||
secret = $e.text().trim()
|
||||
})
|
||||
cy.get('input[name="totp_code"]').then(($e) => {
|
||||
cy.wrap($e).type(
|
||||
new TOTP({
|
||||
secret,
|
||||
}).generate(),
|
||||
)
|
||||
})
|
||||
cy.get('*[name="method"][value="totp"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["password", "totp"],
|
||||
})
|
||||
|
||||
cy.logout()
|
||||
cy.clearAllCookies()
|
||||
|
||||
cy.visit(recovery + "?return_to=https://www.example.org/")
|
||||
doRecovery()
|
||||
|
||||
cy.shouldShow2FAScreen()
|
||||
cy.get('input[name="totp_code"]').then(($e) => {
|
||||
cy.wrap($e).type(
|
||||
new TOTP({
|
||||
secret,
|
||||
}).generate(),
|
||||
)
|
||||
})
|
||||
cy.get('*[name="method"][value="totp"]').click()
|
||||
|
||||
const newPassword = gen.password()
|
||||
cy.get(appPrefix(app) + 'input[name="password"]')
|
||||
.clear()
|
||||
.type(newPassword)
|
||||
cy.get('button[value="password"]').click()
|
||||
cy.location("hostname").should("eq", "www.example.org")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, assertRecoveryAddress, gen } from "../../../../helpers"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
|
||||
context("Account Recovery Success", () => {
|
||||
;[
|
||||
{
|
||||
settings: react.settings,
|
||||
base: react.base,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
{
|
||||
settings: express.settings,
|
||||
base: express.base,
|
||||
app: "express" as "express",
|
||||
profile: "recovery",
|
||||
},
|
||||
].forEach(({ settings, profile, base, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.deleteMail()
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
let identity
|
||||
|
||||
beforeEach(() => {
|
||||
cy.deleteMail()
|
||||
|
||||
cy.useConfig((builder) =>
|
||||
builder
|
||||
.longRecoveryLifespan()
|
||||
.longLinkLifespan()
|
||||
.disableVerification()
|
||||
.enableRecovery(),
|
||||
)
|
||||
|
||||
identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
cy.login({ ...identity, cookieUrl: base })
|
||||
})
|
||||
|
||||
it("should update the recovery address when updating the email", () => {
|
||||
cy.visit(settings)
|
||||
const email = gen.email()
|
||||
cy.get(appPrefix(app) + 'input[name="traits.email"]')
|
||||
.clear()
|
||||
.type(email)
|
||||
cy.get('button[value="profile"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
cy.get('input[name="traits.email"]').should("contain.value", email)
|
||||
|
||||
cy.getSession().should(assertRecoveryAddress({ email }))
|
||||
})
|
||||
|
||||
xit("should not show an immediate error when a recovery address already exists", () => {
|
||||
// account enumeration prevention, needs to be implemented.
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user