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:
203
test/e2e/cypress/integration/profiles/code/login/error.spec.ts
Normal file
203
test/e2e/cypress/integration/profiles/code/login/error.spec.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { MOBILE_URL, gen } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
context("Login error messages with code method", () => {
|
||||
;[
|
||||
{
|
||||
route: express.login,
|
||||
app: "express" as "express",
|
||||
profile: "code",
|
||||
},
|
||||
{
|
||||
route: react.login,
|
||||
app: "react" as "react",
|
||||
profile: "code",
|
||||
},
|
||||
{
|
||||
route: MOBILE_URL + "/Login",
|
||||
app: "mobile" as "mobile",
|
||||
profile: "code",
|
||||
},
|
||||
].forEach(({ route, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
const Selectors = {
|
||||
mobile: {
|
||||
identity: '[data-testid="identifier"]',
|
||||
code: '[data-testid="code"]',
|
||||
},
|
||||
express: {
|
||||
identity: '[data-testid="login-flow-code"] input[name="identifier"]',
|
||||
code: 'input[name="code"]',
|
||||
},
|
||||
react: {
|
||||
identity: 'input[name="identifier"]',
|
||||
code: 'input[name="code"]',
|
||||
},
|
||||
}
|
||||
|
||||
before(() => {
|
||||
if (app !== "mobile") {
|
||||
cy.proxy(app)
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.deleteMail()
|
||||
cy.clearAllCookies()
|
||||
|
||||
const email = gen.email()
|
||||
cy.wrap(email).as("email")
|
||||
cy.registerWithCode({ email, traits: { "traits.tos": 1 } })
|
||||
|
||||
cy.deleteMail()
|
||||
cy.clearAllCookies()
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show error message when account identifier does not exist", () => {
|
||||
const email = gen.email()
|
||||
|
||||
cy.get(Selectors[app]["identity"]).type(email)
|
||||
cy.submitCodeForm(app)
|
||||
|
||||
cy.get('[data-testid="ui/message/4000035"]').should(
|
||||
"contain",
|
||||
"This account does not exist or has not setup sign in with code.",
|
||||
)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show error message when code is invalid", () => {
|
||||
cy.get("@email").then((email) => {
|
||||
cy.get(Selectors[app]["identity"]).clear().type(email.toString())
|
||||
})
|
||||
|
||||
cy.submitCodeForm(app)
|
||||
|
||||
cy.get('[data-testid="ui/message/1010014"]').should("exist")
|
||||
|
||||
cy.get(Selectors[app]["code"]).type("123456")
|
||||
cy.submitCodeForm(app)
|
||||
|
||||
cy.get('[data-testid="ui/message/4010008"]').should(
|
||||
"contain",
|
||||
"The login code is invalid or has already been used. Please try again.",
|
||||
)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show error message when identifier has changed", () => {
|
||||
cy.get("@email").then((email) => {
|
||||
cy.get(Selectors[app]["identity"]).type(email.toString())
|
||||
})
|
||||
|
||||
cy.submitCodeForm(app)
|
||||
|
||||
if (app !== "express") {
|
||||
cy.intercept("POST", "/self-service/login*", (req) => {
|
||||
req.body = {
|
||||
...req.body,
|
||||
identifier: gen.email(),
|
||||
}
|
||||
req.continue()
|
||||
}).as("login")
|
||||
} else {
|
||||
cy.get(Selectors[app]["identity"])
|
||||
.type("{selectall}{backspace}", { force: true })
|
||||
.type(gen.email(), { force: true })
|
||||
}
|
||||
|
||||
cy.get(Selectors[app]["code"]).type("123456")
|
||||
|
||||
cy.submitCodeForm(app)
|
||||
if (app !== "express") {
|
||||
cy.wait("@login")
|
||||
}
|
||||
cy.get('[data-testid="ui/message/4000035"]').should(
|
||||
"contain",
|
||||
"This account does not exist or has not setup sign in with code.",
|
||||
)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show error message when required fields are missing", () => {
|
||||
cy.removeAttribute([Selectors[app]["identity"]], "required")
|
||||
|
||||
cy.submitCodeForm(app)
|
||||
if (app === "mobile") {
|
||||
cy.get('[data-testid="field/identifier"]').should(
|
||||
"contain",
|
||||
"Property identifier is missing",
|
||||
)
|
||||
} else {
|
||||
cy.get('[data-testid="ui/message/4000002"]').should(
|
||||
"contain",
|
||||
"Property identifier is missing",
|
||||
)
|
||||
}
|
||||
|
||||
cy.get("@email").then((email) => {
|
||||
cy.get(Selectors[app]["identity"]).type(email.toString())
|
||||
})
|
||||
|
||||
cy.submitCodeForm(app)
|
||||
|
||||
cy.removeAttribute([Selectors[app]["code"]], "required")
|
||||
cy.submitCodeForm(app)
|
||||
|
||||
if (app === "mobile") {
|
||||
cy.get('[data-testid="field/code"]').should(
|
||||
"contain",
|
||||
"Property code is missing",
|
||||
)
|
||||
} else {
|
||||
cy.get('[data-testid="ui/message/4000002"]').should(
|
||||
"contain",
|
||||
"Property code is missing",
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show error message when code is expired", () => {
|
||||
cy.updateConfigFile((config) => {
|
||||
config.selfservice.methods.code = {
|
||||
passwordless_enabled: true,
|
||||
config: {
|
||||
lifespan: "1ns",
|
||||
},
|
||||
}
|
||||
return config
|
||||
}).then(() => {
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
cy.get("@email").then((email) => {
|
||||
cy.get(Selectors[app]["identity"]).type(email.toString())
|
||||
})
|
||||
cy.submitCodeForm(app)
|
||||
|
||||
cy.get("@email").then((email) => {
|
||||
cy.getLoginCodeFromEmail(email.toString()).then((code) => {
|
||||
cy.get(Selectors[app]["code"]).type(code)
|
||||
})
|
||||
})
|
||||
|
||||
cy.submitCodeForm(app)
|
||||
|
||||
cy.get('[data-testid="ui/message/4010008"]').should(
|
||||
"contain",
|
||||
"The login code is invalid or has already been used",
|
||||
)
|
||||
|
||||
cy.noSession()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
332
test/e2e/cypress/integration/profiles/code/login/success.spec.ts
Normal file
332
test/e2e/cypress/integration/profiles/code/login/success.spec.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Session } from "@ory/kratos-client"
|
||||
import { gen, MOBILE_URL } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
context("Login success with code method", () => {
|
||||
;[
|
||||
{
|
||||
route: express.login,
|
||||
app: "express" as "express",
|
||||
profile: "code",
|
||||
},
|
||||
{
|
||||
route: react.login,
|
||||
app: "react" as "react",
|
||||
profile: "code",
|
||||
},
|
||||
{
|
||||
route: MOBILE_URL + "/Login",
|
||||
app: "mobile" as "mobile",
|
||||
profile: "code",
|
||||
},
|
||||
].forEach(({ route, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
const Selectors = {
|
||||
mobile: {
|
||||
identity: '[data-testid="identifier"]',
|
||||
code: '[data-testid="code"]',
|
||||
submit: '[data-testid="field/method/code"]',
|
||||
resend: '[data-testid="field/resend/code"]',
|
||||
},
|
||||
express: {
|
||||
identity: '[data-testid="login-flow-code"] input[name="identifier"]',
|
||||
code: 'input[name="code"]',
|
||||
submit: 'button[name="method"][value="code"]',
|
||||
resend: 'button[name="resend"]',
|
||||
},
|
||||
react: {
|
||||
identity: 'input[name="identifier"]',
|
||||
code: 'input[name="code"]',
|
||||
submit: 'button[name="method"][value="code"]',
|
||||
resend: 'button[name="resend"]',
|
||||
},
|
||||
}
|
||||
|
||||
before(() => {
|
||||
cy.deleteMail()
|
||||
cy.useConfigProfile(profile)
|
||||
if (app !== "mobile") {
|
||||
cy.proxy(app)
|
||||
}
|
||||
cy.setPostCodeRegistrationHooks([])
|
||||
cy.setupHooks("login", "after", "code", [])
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
const email = gen.email()
|
||||
cy.wrap(email).as("email")
|
||||
cy.registerWithCode({ email, traits: { "traits.tos": 1 } })
|
||||
|
||||
cy.deleteMail()
|
||||
cy.clearAllCookies()
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should be able to sign in with code", () => {
|
||||
cy.get("@email").then((email) => {
|
||||
cy.get(Selectors[app]["identity"]).clear().type(email.toString())
|
||||
cy.submitCodeForm(app)
|
||||
|
||||
cy.getLoginCodeFromEmail(email.toString()).then((code) => {
|
||||
cy.get(Selectors[app]["code"]).type(code)
|
||||
|
||||
cy.get(Selectors[app]["submit"]).click()
|
||||
})
|
||||
|
||||
cy.location("pathname").should("not.contain", "login")
|
||||
|
||||
if (app === "mobile") {
|
||||
cy.get('[data-testid="session-token"]').then((token) => {
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["code"],
|
||||
token: token.text(),
|
||||
}).then((session) => {
|
||||
cy.wrap(session).as("session")
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
} else {
|
||||
cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then(
|
||||
(session) => {
|
||||
cy.wrap(session).as("session")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
cy.get<Session>("@session").then(({ identity }) => {
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.verifiable_addresses).to.have.length(1)
|
||||
expect(identity.verifiable_addresses[0].status).to.equal(
|
||||
"completed",
|
||||
)
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should be able to sign in with code on account registered with password", () => {
|
||||
const email = gen.email()
|
||||
// register account with password
|
||||
cy.register({
|
||||
email,
|
||||
password: gen.password(),
|
||||
fields: { "traits.tos": 1 },
|
||||
})
|
||||
|
||||
cy.getVerificationCodeFromEmail(email).then((code) => {
|
||||
expect(code).to.not.be.empty
|
||||
cy.deleteMail()
|
||||
})
|
||||
|
||||
cy.clearAllCookies()
|
||||
|
||||
cy.visit(route)
|
||||
|
||||
cy.get(Selectors[app]["identity"]).clear().type(email)
|
||||
cy.submitCodeForm(app)
|
||||
|
||||
cy.getLoginCodeFromEmail(email).then((code) => {
|
||||
cy.get(Selectors[app]["code"]).type(code)
|
||||
|
||||
cy.get(Selectors[app]["submit"]).click()
|
||||
})
|
||||
|
||||
if (app === "express") {
|
||||
cy.url().should("match", /\/welcome/)
|
||||
} else {
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
}
|
||||
|
||||
if (app === "mobile") {
|
||||
cy.get('[data-testid="session-token"]').then((token) => {
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["code"],
|
||||
token: token.text(),
|
||||
}).then((session) => {
|
||||
cy.wrap(session).as("session")
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
} else {
|
||||
cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then(
|
||||
(session) => {
|
||||
cy.wrap(session).as("session")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
cy.get<Session>("@session").then(({ identity }) => {
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.verifiable_addresses).to.have.length(1)
|
||||
expect(identity.verifiable_addresses[0].status).to.equal("completed")
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should be able to resend login code", () => {
|
||||
cy.get("@email").then((email) => {
|
||||
cy.get(Selectors[app]["identity"]).clear().type(email.toString())
|
||||
cy.submitCodeForm(app)
|
||||
|
||||
cy.getLoginCodeFromEmail(email.toString()).then((code) => {
|
||||
cy.wrap(code).as("code1")
|
||||
})
|
||||
|
||||
cy.get(Selectors[app]["resend"]).click()
|
||||
|
||||
cy.getLoginCodeFromEmail(email.toString()).then((code) => {
|
||||
cy.wrap(code).as("code2")
|
||||
})
|
||||
|
||||
cy.get("@code1").then((code1) => {
|
||||
cy.get("@code2").then((code2) => {
|
||||
expect(code1).to.not.equal(code2)
|
||||
})
|
||||
})
|
||||
|
||||
// attempt to submit code 1
|
||||
cy.get("@code1").then((code1) => {
|
||||
cy.get(Selectors[app]["code"]).clear().type(code1.toString())
|
||||
})
|
||||
|
||||
cy.get(Selectors[app]["submit"]).click()
|
||||
|
||||
cy.get("[data-testid='ui/message/4010008']").contains(
|
||||
"The login code is invalid or has already been used",
|
||||
)
|
||||
|
||||
// attempt to submit code 2
|
||||
cy.get("@code2").then((code2) => {
|
||||
cy.get(Selectors[app]["code"]).clear().type(code2.toString())
|
||||
})
|
||||
|
||||
cy.get(Selectors[app]["submit"]).click()
|
||||
|
||||
if (app === "express") {
|
||||
cy.url().should("match", /\/welcome/)
|
||||
} else {
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
}
|
||||
|
||||
if (app === "express") {
|
||||
cy.get('a[href*="sessions"').click()
|
||||
}
|
||||
|
||||
if (app === "mobile") {
|
||||
cy.get('[data-testid="session-token"]').then((token) => {
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["code"],
|
||||
token: token.text(),
|
||||
}).then((session) => {
|
||||
cy.wrap(session).as("session")
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
} else {
|
||||
cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then(
|
||||
(session) => {
|
||||
cy.wrap(session).as("session")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
cy.get<Session>("@session").then((session) => {
|
||||
const { identity } = session
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.verifiable_addresses).to.have.length(1)
|
||||
expect(identity.verifiable_addresses[0].status).to.equal(
|
||||
"completed",
|
||||
)
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should be able to login to un-verfied email", () => {
|
||||
const email = gen.email()
|
||||
const email2 = gen.email()
|
||||
|
||||
// Setup complex schema
|
||||
cy.setIdentitySchema(
|
||||
"file://test/e2e/profiles/code/identity.complex.traits.schema.json",
|
||||
)
|
||||
|
||||
cy.registerWithCode({
|
||||
email: email,
|
||||
traits: {
|
||||
"traits.username": Math.random().toString(36),
|
||||
"traits.email2": email2,
|
||||
},
|
||||
})
|
||||
|
||||
// There are verification emails from the registration process in the inbox that we need to deleted
|
||||
// for the assertions below to pass.
|
||||
cy.deleteMail({ atLeast: 1 })
|
||||
|
||||
cy.visit(route)
|
||||
|
||||
cy.get(Selectors[app]["identity"]).clear().type(email2)
|
||||
cy.submitCodeForm(app)
|
||||
|
||||
cy.getLoginCodeFromEmail(email2).then((code) => {
|
||||
cy.get(Selectors[app]["code"]).type(code)
|
||||
cy.get(Selectors[app]["submit"]).click()
|
||||
})
|
||||
|
||||
if (app === "express") {
|
||||
cy.url().should("match", /\/welcome/)
|
||||
} else {
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
}
|
||||
|
||||
if (app === "mobile") {
|
||||
cy.get('[data-testid="session-token"]').then((token) => {
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["code"],
|
||||
token: token.text(),
|
||||
}).then((session) => {
|
||||
cy.wrap(session).as("session")
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
cy.get('[data-testid="session-content"]').should("contain", email2)
|
||||
} else {
|
||||
cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then(
|
||||
(session) => {
|
||||
cy.wrap(session).as("session")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
cy.get<Session>("@session").then((session) => {
|
||||
expect(session?.identity?.verifiable_addresses).to.have.length(2)
|
||||
expect(session?.identity?.verifiable_addresses[0].status).to.equal(
|
||||
"completed",
|
||||
)
|
||||
expect(session.identity.verifiable_addresses[1].status).to.equal(
|
||||
"completed",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,197 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
import { UiNode } from "@ory/kratos-client"
|
||||
import { gen, MOBILE_URL } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
context("Registration error messages with code method", () => {
|
||||
;[
|
||||
{
|
||||
route: express.registration,
|
||||
app: "express" as "express",
|
||||
profile: "code",
|
||||
},
|
||||
{
|
||||
route: react.registration,
|
||||
app: "react" as "react",
|
||||
profile: "code",
|
||||
},
|
||||
{
|
||||
route: MOBILE_URL + "/Registration",
|
||||
app: "mobile" as "mobile",
|
||||
profile: "code",
|
||||
},
|
||||
].forEach(({ route, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
const Selectors = {
|
||||
mobile: {
|
||||
identifier: "[data-testid='field/identifier']",
|
||||
email: "[data-testid='field/traits.email']",
|
||||
tos: "[data-testid='traits.tos']",
|
||||
code: "[data-testid='field/code']",
|
||||
},
|
||||
express: {
|
||||
identifier:
|
||||
"[data-testid='registration-flow-code'] input[name='identifier']",
|
||||
email:
|
||||
"[data-testid='registration-flow-code'] input[name='traits.email']",
|
||||
tos: "[data-testid='registration-flow-code'] [name='traits.tos'] + label",
|
||||
code: "input[name='code']",
|
||||
},
|
||||
react: {
|
||||
identifier: "input[name='identifier']",
|
||||
email: "input[name='traits.email']",
|
||||
tos: "[name='traits.tos'] + label",
|
||||
code: "input[name='code']",
|
||||
},
|
||||
}
|
||||
|
||||
before(() => {
|
||||
if (app !== "mobile") {
|
||||
cy.proxy(app)
|
||||
}
|
||||
cy.useConfigProfile(profile)
|
||||
cy.deleteMail()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.deleteMail()
|
||||
cy.clearAllCookies()
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show error message when code is invalid", () => {
|
||||
const email = gen.email()
|
||||
|
||||
cy.get(Selectors[app]["email"]).type(email)
|
||||
cy.get(Selectors[app]["tos"]).click()
|
||||
|
||||
cy.submitCodeForm(app)
|
||||
|
||||
cy.get('[data-testid="ui/message/1040005"]').should("be.visible")
|
||||
|
||||
cy.get(Selectors[app]["code"]).type("123456")
|
||||
cy.submitCodeForm(app)
|
||||
|
||||
cy.get('[data-testid="ui/message/4040003"]').should(
|
||||
"contain",
|
||||
"The registration code is invalid or has already been used. Please try again.",
|
||||
)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show error message when traits have changed", () => {
|
||||
const email = gen.email()
|
||||
|
||||
cy.get(Selectors[app]["email"]).type(email)
|
||||
cy.get(Selectors[app]["tos"]).click()
|
||||
|
||||
cy.submitCodeForm(app)
|
||||
cy.get('[data-testid="ui/message/1040005"]').should("be.visible")
|
||||
|
||||
if (app !== "express") {
|
||||
// the mobile app doesn't render hidden fields in the DOM
|
||||
// we need to replace the request body
|
||||
cy.intercept("POST", "/self-service/registration*", (req) => {
|
||||
req.body = {
|
||||
...req.body,
|
||||
"traits.email": "changed-email@email.com",
|
||||
}
|
||||
req.continue()
|
||||
}).as("registration")
|
||||
} else {
|
||||
cy.get(Selectors[app]["email"])
|
||||
.type("{selectall}{backspace}", { force: true })
|
||||
.type("changed-email@email.com", { force: true })
|
||||
}
|
||||
|
||||
cy.get(Selectors[app]["code"]).type("123456")
|
||||
cy.submitCodeForm(app)
|
||||
|
||||
if (app !== "express") {
|
||||
cy.wait("@registration")
|
||||
}
|
||||
|
||||
cy.get('[data-testid="ui/message/4000036"]').should(
|
||||
"contain",
|
||||
"The provided traits do not match the traits previously associated with this flow.",
|
||||
)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show error message when required fields are missing", () => {
|
||||
cy.removeAttribute([Selectors[app]["email"]], "required")
|
||||
const email = gen.email()
|
||||
|
||||
cy.get(Selectors[app]["tos"]).click()
|
||||
cy.submitCodeForm(app)
|
||||
|
||||
if (app === "mobile") {
|
||||
cy.get('[data-testid="field/traits.email"]').should(
|
||||
"contain",
|
||||
"Property email is missing",
|
||||
)
|
||||
} else {
|
||||
cy.get('[data-testid="ui/message/4000002"]').should(
|
||||
"contain",
|
||||
"Property email is missing",
|
||||
)
|
||||
}
|
||||
cy.get(Selectors[app]["email"]).type(email)
|
||||
cy.submitCodeForm(app)
|
||||
|
||||
cy.get('[data-testid="ui/message/1040005"]').should("be.visible")
|
||||
|
||||
cy.removeAttribute([Selectors[app]["code"]], "required")
|
||||
cy.submitCodeForm(app)
|
||||
|
||||
if (app === "mobile") {
|
||||
cy.get('[data-testid="field/code"]').should(
|
||||
"contain",
|
||||
"Property code is missing",
|
||||
)
|
||||
} else {
|
||||
cy.get('[data-testid="ui/message/4000002"]').should(
|
||||
"contain",
|
||||
"Property code is missing",
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show error message when code is expired", () => {
|
||||
cy.updateConfigFile((config) => {
|
||||
config.selfservice.methods.code.config.lifespan = "1ns"
|
||||
return config
|
||||
})
|
||||
cy.visit(route)
|
||||
|
||||
const email = gen.email()
|
||||
cy.get(Selectors[app]["email"]).type(email)
|
||||
cy.get(Selectors[app]["tos"]).click()
|
||||
|
||||
cy.submitCodeForm(app)
|
||||
cy.get('[data-testid="ui/message/1040005"]').should("be.visible")
|
||||
|
||||
cy.getRegistrationCodeFromEmail(email).then((code) => {
|
||||
cy.get(Selectors[app]["code"]).type(code)
|
||||
cy.submitCodeForm(app)
|
||||
})
|
||||
|
||||
cy.get('[data-testid="ui/message/4040003"]').should(
|
||||
"contain",
|
||||
"The registration code is invalid or has already been used",
|
||||
)
|
||||
|
||||
cy.noSession()
|
||||
|
||||
cy.updateConfigFile((config) => {
|
||||
config.selfservice.methods.code.config.lifespan = "1h"
|
||||
return config
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,418 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Session } from "@ory/kratos-client"
|
||||
import { gen, MOBILE_URL } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
const Selectors = {
|
||||
mobile: {
|
||||
identifier: "[data-testid='field/identifier']",
|
||||
recoveryEmail: "[data-testid='field/email']",
|
||||
email: "[data-testid='traits.email']",
|
||||
email2: "[data-testid='traits.email2']",
|
||||
tos: "[data-testid='traits.tos']",
|
||||
username: "[data-testid='traits.username']",
|
||||
code: "[data-testid='field/code'] input",
|
||||
recoveryCode: "[data-testid='code']",
|
||||
submitCode: "[data-testid='field/method/code']",
|
||||
resendCode: "[data-testid='field/resend/code']",
|
||||
submitRecovery: "[data-testid='field/method/code']",
|
||||
codeHiddenMethod: "[data-testid='field/method/code']",
|
||||
},
|
||||
express: {
|
||||
identifier: "[data-testid='login-flow-code'] input[name='identifier']",
|
||||
recoveryEmail: "input[name=email]",
|
||||
email: "[data-testid='registration-flow-code'] input[name='traits.email']",
|
||||
email2:
|
||||
"[data-testid='registration-flow-code'] input[name='traits.email2']",
|
||||
tos: "[data-testid='registration-flow-code'] [name='traits.tos'] + label",
|
||||
username:
|
||||
"[data-testid='registration-flow-code'] input[name='traits.username']",
|
||||
code: "input[name='code']",
|
||||
recoveryCode: "input[name=code]",
|
||||
submitRecovery: "button[name=method][value=code]",
|
||||
submitCode: "button[name='method'][value='code']",
|
||||
resendCode: "button[name='resend'][value='code']",
|
||||
codeHiddenMethod: "input[name='method'][value='code'][type='hidden']",
|
||||
},
|
||||
react: {
|
||||
identifier: "input[name='identifier']",
|
||||
recoveryEmail: "input[name=email]",
|
||||
email: "input[name='traits.email']",
|
||||
email2: "input[name='traits.email2']",
|
||||
tos: "[name='traits.tos'] + label",
|
||||
username: "input[name='traits.username']",
|
||||
code: "input[name='code']",
|
||||
recoveryCode: "input[name=code]",
|
||||
submitRecovery: "button[name=method][value=code]",
|
||||
submitCode: "button[name='method'][value='code']",
|
||||
resendCode: "button[name='resend'][value='code']",
|
||||
codeHiddenMethod: "input[name='method'][value='code'][type='hidden']",
|
||||
},
|
||||
}
|
||||
|
||||
context("Registration success with code method", () => {
|
||||
;[
|
||||
{
|
||||
route: express.registration,
|
||||
login: express.login,
|
||||
recovery: express.recovery,
|
||||
app: "express" as "express",
|
||||
profile: "code",
|
||||
},
|
||||
{
|
||||
route: react.registration,
|
||||
login: react.login,
|
||||
recovery: react.recovery,
|
||||
app: "react" as "react",
|
||||
profile: "code",
|
||||
},
|
||||
{
|
||||
route: MOBILE_URL + "/Registration",
|
||||
login: MOBILE_URL + "/Login",
|
||||
recovery: MOBILE_URL + "/Recovery",
|
||||
app: "mobile" as "mobile",
|
||||
profile: "code",
|
||||
},
|
||||
].forEach(({ route, login, recovery, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
if (app !== "mobile") {
|
||||
cy.proxy(app)
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.deleteMail()
|
||||
cy.clearAllCookies()
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should be able to resend the registration code", () => {
|
||||
const email = gen.email()
|
||||
|
||||
cy.get(Selectors[app]["email"]).type(email)
|
||||
cy.get(Selectors[app]["tos"]).click()
|
||||
|
||||
cy.submitCodeForm(app)
|
||||
cy.get('[data-testid="ui/message/1040005"]').should("be.visible")
|
||||
|
||||
cy.getRegistrationCodeFromEmail(email).then((code) =>
|
||||
cy.wrap(code).as("code1"),
|
||||
)
|
||||
|
||||
cy.get(Selectors[app]["codeHiddenMethod"]).should("exist")
|
||||
cy.get(Selectors[app]["resendCode"]).click()
|
||||
|
||||
cy.getRegistrationCodeFromEmail(email).then((code) => {
|
||||
cy.wrap(code).as("code2")
|
||||
})
|
||||
|
||||
cy.get("@code1").then((code1) => {
|
||||
// previous code should not work
|
||||
cy.get(Selectors[app]["code"]).clear().type(code1.toString())
|
||||
|
||||
cy.submitCodeForm(app)
|
||||
cy.get('[data-testid="ui/message/4040003"]').should(
|
||||
"contain.text",
|
||||
"The registration code is invalid or has already been used. Please try again.",
|
||||
)
|
||||
})
|
||||
|
||||
cy.get("@code2").then((code2) => {
|
||||
cy.get(Selectors[app]["code"]).clear().type(code2.toString())
|
||||
cy.submitCodeForm(app)
|
||||
})
|
||||
|
||||
if (app === "mobile") {
|
||||
cy.get('[data-testid="session-token"]').then((token) => {
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["code"],
|
||||
token: token.text(),
|
||||
}).then((session) => {
|
||||
cy.wrap(session).as("session")
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
} else {
|
||||
// Wait for the post-registration UI to settle before querying
|
||||
// /sessions/whoami. cy.request does not retry the HTTP call on a
|
||||
// non-2xx status, so without this sync getSession races the
|
||||
// browser's session-cookie persistence and intermittently 401s.
|
||||
if (app === "express") {
|
||||
cy.url().should("match", /\/welcome/)
|
||||
} else {
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
}
|
||||
cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then(
|
||||
(session) => {
|
||||
cy.wrap(session).as("session")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
cy.get<Session>("@session").then(({ identity }) => {
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.verifiable_addresses).to.have.length(1)
|
||||
expect(identity.verifiable_addresses[0].status).to.equal("completed")
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should sign up and be logged in with session hook", () => {
|
||||
const email = gen.email()
|
||||
|
||||
cy.get(Selectors[app]["email"]).type(email)
|
||||
cy.get(Selectors[app]["tos"]).click()
|
||||
|
||||
cy.submitCodeForm(app)
|
||||
cy.get('[data-testid="ui/message/1040005"]').should("be.visible")
|
||||
|
||||
cy.getRegistrationCodeFromEmail(email).then((code) => {
|
||||
cy.get(Selectors[app]["code"]).type(code)
|
||||
cy.get(Selectors[app]["submitCode"]).click()
|
||||
})
|
||||
|
||||
if (app === "express") {
|
||||
cy.url().should("match", /\/welcome/)
|
||||
} else {
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
}
|
||||
if (app === "mobile") {
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
cy.get('[data-testid="session-token"]').then((token) => {
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["code"],
|
||||
token: token.text(),
|
||||
}).then((session) => {
|
||||
cy.wrap(session).as("session")
|
||||
})
|
||||
})
|
||||
} else {
|
||||
cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then(
|
||||
(session) => {
|
||||
cy.wrap(session).as("session")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
cy.get<Session>("@session").then(({ identity }) => {
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.verifiable_addresses).to.have.length(1)
|
||||
expect(identity.verifiable_addresses[0].status).to.equal("completed")
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should be able to sign up without session hook", () => {
|
||||
cy.setPostCodeRegistrationHooks([])
|
||||
const email = gen.email()
|
||||
|
||||
cy.get(Selectors[app]["email"]).type(email)
|
||||
cy.get(Selectors[app]["tos"]).click()
|
||||
|
||||
cy.submitCodeForm(app)
|
||||
cy.get('[data-testid="ui/message/1040005"]').should("be.visible")
|
||||
|
||||
cy.getRegistrationCodeFromEmail(email).then((code) => {
|
||||
cy.get(Selectors[app]["code"]).type(code)
|
||||
cy.get(Selectors[app]["submitCode"]).click()
|
||||
})
|
||||
|
||||
cy.visit(login)
|
||||
cy.get(Selectors[app]["identifier"]).type(email)
|
||||
cy.get(Selectors[app]["submitCode"]).click()
|
||||
|
||||
cy.getLoginCodeFromEmail(email).then((code) => {
|
||||
cy.get(Selectors[app]["code"]).type(code)
|
||||
cy.get(Selectors[app]["submitCode"]).click()
|
||||
})
|
||||
|
||||
if (app === "express") {
|
||||
cy.url().should("match", /\/welcome/)
|
||||
} else {
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
}
|
||||
|
||||
if (app === "mobile") {
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
cy.get('[data-testid="session-token"]').then((token) => {
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["code"],
|
||||
token: token.text(),
|
||||
}).then((session) => {
|
||||
cy.wrap(session).as("session")
|
||||
})
|
||||
})
|
||||
} else {
|
||||
cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then(
|
||||
(session) => {
|
||||
cy.wrap(session).as("session")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
cy.get<Session>("@session").then(({ identity }) => {
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.verifiable_addresses).to.have.length(1)
|
||||
expect(identity.verifiable_addresses[0].status).to.equal("completed")
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
|
||||
it("should be able to recover account when registered with code", () => {
|
||||
if (app === "mobile") {
|
||||
cy.log("WARNING: skipping test for mobile app")
|
||||
return
|
||||
}
|
||||
if (app === "react") {
|
||||
// This test is flaky on React, so we skip it for now.
|
||||
return
|
||||
}
|
||||
const email = gen.email()
|
||||
cy.registerWithCode({ email, traits: { "traits.tos": 1 } })
|
||||
|
||||
cy.clearAllCookies()
|
||||
cy.visit(recovery)
|
||||
|
||||
cy.get(Selectors[app]["recoveryEmail"]).type(email)
|
||||
cy.get(Selectors[app]["submitRecovery"]).click()
|
||||
|
||||
cy.recoveryEmailWithCode({ expect: { email, enterCode: false } }).then(
|
||||
() => {
|
||||
cy.get<string>("@recoveryCode").then((code) => {
|
||||
cy.get(Selectors[app]["recoveryCode"]).type(code)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
cy.get(Selectors[app]["submitRecovery"]).click()
|
||||
|
||||
// Wait for the post-recovery settings page to render so the session
|
||||
// cookie is in Cypress's jar before we hit /sessions/whoami.
|
||||
cy.location("pathname").should("contain", "settings")
|
||||
|
||||
cy.getSession({ expectAal: "aal1" }).then((session) => {
|
||||
cy.wrap(session).as("session")
|
||||
})
|
||||
|
||||
cy.get<Session>("@session").then(({ identity }) => {
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
|
||||
// Try keep this test as the last one, as it updates the identity schema.
|
||||
it("should be able to use multiple identifiers to signup with and sign in to", () => {
|
||||
cy.setPostCodeRegistrationHooks([
|
||||
{
|
||||
hook: "session",
|
||||
},
|
||||
{
|
||||
hook: "show_verification_ui",
|
||||
},
|
||||
])
|
||||
|
||||
// Setup complex schema
|
||||
cy.setIdentitySchema(
|
||||
"file://test/e2e/profiles/code/identity.complex.traits.schema.json",
|
||||
)
|
||||
|
||||
cy.visit(route)
|
||||
|
||||
cy.get(Selectors[app]["username"]).type(Math.random().toString(36))
|
||||
|
||||
const email = gen.email()
|
||||
cy.get(Selectors[app]["email"]).type(email)
|
||||
|
||||
const email2 = gen.email()
|
||||
cy.get(Selectors[app]["email2"]).type(email2)
|
||||
|
||||
cy.submitCodeForm(app)
|
||||
cy.get('[data-testid="ui/message/1040005"]').should("be.visible")
|
||||
|
||||
// intentionally use email 1 to sign up for the account
|
||||
cy.getRegistrationCodeFromEmail(email, { expectedCount: 1 }).then(
|
||||
(code) => {
|
||||
cy.get(Selectors[app]["code"]).type(code)
|
||||
cy.get(Selectors[app]["submitCode"]).click()
|
||||
},
|
||||
)
|
||||
cy.get('[data-testid="ui/message/1080003"]').should("be.visible")
|
||||
|
||||
if (app === "mobile") {
|
||||
cy.visit(MOBILE_URL + "/Home")
|
||||
cy.get('*[data-testid="logout"]').click()
|
||||
} else {
|
||||
cy.logout()
|
||||
}
|
||||
|
||||
// There are verification emails from the registration process in the inbox that we need to deleted
|
||||
// for the assertions below to pass.
|
||||
cy.deleteMail({ atLeast: 1 })
|
||||
|
||||
// Attempt to sign in with email 2 (should fail)
|
||||
cy.visit(login)
|
||||
cy.get(Selectors[app]["identifier"]).type(email2)
|
||||
|
||||
cy.get(Selectors[app]["submitCode"]).click()
|
||||
|
||||
cy.getLoginCodeFromEmail(email2, {
|
||||
expectedCount: 1,
|
||||
}).then((code) => {
|
||||
cy.get(Selectors[app]["code"]).type(code)
|
||||
cy.get(Selectors[app]["submitCode"]).click()
|
||||
})
|
||||
if (app === "express") {
|
||||
cy.url().should("match", /\/welcome/)
|
||||
} else {
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
}
|
||||
|
||||
if (app === "mobile") {
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
cy.get('[data-testid="session-token"]').then((token) => {
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["code"],
|
||||
token: token.text(),
|
||||
}).then((session) => {
|
||||
cy.wrap(session).as("session")
|
||||
})
|
||||
})
|
||||
} else {
|
||||
cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then(
|
||||
(session) => {
|
||||
cy.wrap(session).as("session")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
cy.get<Session>("@session").then(({ identity }) => {
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.verifiable_addresses).to.have.length(2)
|
||||
expect(
|
||||
identity.verifiable_addresses.filter((v) => v.value === email)[0]
|
||||
.status,
|
||||
).to.equal("completed")
|
||||
expect(
|
||||
identity.verifiable_addresses.filter((v) => v.value === email2)[0]
|
||||
.status,
|
||||
).to.equal("completed")
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
48
test/e2e/cypress/integration/profiles/email/error/ui.spec.ts
Normal file
48
test/e2e/cypress/integration/profiles/email/error/ui.spec.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
import { appPrefix } from "../../../../helpers"
|
||||
|
||||
// playwright:migrated
|
||||
describe("Handling self-service error flows", () => {
|
||||
;[
|
||||
{
|
||||
route: express.base,
|
||||
app: "express" as "express",
|
||||
profile: "email",
|
||||
},
|
||||
{
|
||||
route: react.base,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
].forEach(({ route, app, profile }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show the error", () => {
|
||||
cy.visit(`${route}/error?id=stub:500`, {
|
||||
failOnStatusCode: false,
|
||||
})
|
||||
|
||||
if (app === "express") {
|
||||
cy.get(`${appPrefix(app)} [data-testid="ui/error/message"]`).should(
|
||||
"contain.text",
|
||||
"This is a stub error.",
|
||||
)
|
||||
} else {
|
||||
cy.get(`${appPrefix(app)}code`).should(
|
||||
"contain.text",
|
||||
"This is a stub error.",
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
108
test/e2e/cypress/integration/profiles/email/login/error.spec.ts
Normal file
108
test/e2e/cypress/integration/profiles/email/login/error.spec.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
// 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"
|
||||
|
||||
// playwright:migrated
|
||||
describe("Basic email profile with failing login flows", () => {
|
||||
;[
|
||||
{
|
||||
route: express.login,
|
||||
app: "express" as "express",
|
||||
profile: "email",
|
||||
},
|
||||
{
|
||||
route: react.login,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
].forEach(({ route, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("fails when CSRF cookies are missing", () => {
|
||||
cy.get(`${appPrefix(app)}input[name="identifier"]`).type(
|
||||
"i-do-not-exist",
|
||||
)
|
||||
cy.get('input[name="password"]').type("invalid-password")
|
||||
|
||||
cy.shouldHaveCsrfError({ app })
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("fails when a disallowed return_to url is requested", () => {
|
||||
cy.shouldErrorOnDisallowedReturnTo(
|
||||
route + "?return_to=https://not-allowed",
|
||||
{ app },
|
||||
)
|
||||
})
|
||||
|
||||
// playwright:migrated - partially
|
||||
describe("shows validation errors when invalid signup data is used", () => {
|
||||
// playwright:migrated
|
||||
it("should show an error when the identifier is missing", () => {
|
||||
// the browser will prevent the form from submitting if the fields are empty since they are required
|
||||
// here we just remove the required attribute to make the form submit
|
||||
cy.removeAttribute(
|
||||
['input[name="identifier"]', 'input[name="password"]'],
|
||||
"required",
|
||||
)
|
||||
cy.submitPasswordForm()
|
||||
cy.get('*[data-testid="ui/message/4000002"]').should(
|
||||
"contain.text",
|
||||
"Property identifier is missing",
|
||||
)
|
||||
cy.get('*[data-testid="ui/message/4000002"]').should(
|
||||
"contain.text",
|
||||
"Property password is missing",
|
||||
)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show an error when the password is missing", () => {
|
||||
const identity = gen.email()
|
||||
cy.get('input[name="identifier"]')
|
||||
.type(identity)
|
||||
.should("have.value", identity)
|
||||
|
||||
// the browser will prevent the form from submitting if the fields are empty since they are required
|
||||
// here we just remove the required attribute to make the form submit
|
||||
cy.removeAttribute(['input[name="password"]'], "required")
|
||||
|
||||
cy.submitPasswordForm()
|
||||
cy.get('*[data-testid^="ui/message/"]')
|
||||
.invoke("text")
|
||||
.then((text) => {
|
||||
expect(text.trim()).to.be.oneOf([
|
||||
"length must be >= 1, but got 0",
|
||||
"Property password is missing.",
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show fail to sign in", () => {
|
||||
cy.get('input[name="identifier"]').type("i-do-not-exist")
|
||||
cy.get('input[name="password"]').type("invalid-password")
|
||||
|
||||
cy.submitPasswordForm()
|
||||
cy.get('*[data-testid="ui/message/4000006"]').should(
|
||||
"contain.text",
|
||||
"credentials are invalid",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,131 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { APP_URL, appPrefix, gen, website } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
// playwright:migrated
|
||||
describe("Basic email profile with succeeding login flows", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
before(() => {
|
||||
cy.registerApi({ email, password, fields: { "traits.website": website } })
|
||||
})
|
||||
;[
|
||||
{
|
||||
route: express.login,
|
||||
app: "express" as "express",
|
||||
profile: "email",
|
||||
},
|
||||
{
|
||||
route: react.login,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
].forEach(({ route, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.clearAllCookies()
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should sign in and be logged in", () => {
|
||||
cy.get(`${appPrefix(app)}input[name="identifier"]`).type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.submitPasswordForm()
|
||||
cy.location("pathname").should("not.contain", "/login")
|
||||
|
||||
cy.getSession().should((session) => {
|
||||
const { identity } = session
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.schema_id).to.equal("default")
|
||||
expect(identity.schema_url).to.equal(`${APP_URL}/schemas/ZGVmYXVsdA`)
|
||||
expect(identity.traits.website).to.equal(website)
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should sign in with case insensitive identifier surrounded by whitespace", () => {
|
||||
cy.get('input[name="identifier"]').type(
|
||||
" " + email.toUpperCase() + " ",
|
||||
)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.submitPasswordForm()
|
||||
cy.location("pathname").should("not.contain", "/login")
|
||||
|
||||
cy.getSession().should((session) => {
|
||||
const { identity } = session
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.schema_id).to.equal("default")
|
||||
expect(identity.schema_url).to.equal(`${APP_URL}/schemas/ZGVmYXVsdA`)
|
||||
expect(identity.traits.website).to.equal(website)
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should sign in and be redirected", () => {
|
||||
cy.browserReturnUrlOry()
|
||||
cy.visit(route + "?return_to=https://www.example.org/")
|
||||
|
||||
cy.get('input[name="identifier"]').type(email.toUpperCase())
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.submitPasswordForm()
|
||||
|
||||
cy.url().should("eq", "https://www.example.org/")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
describe("for app express handle return_to correctly for expired flows", () => {
|
||||
before(() => {
|
||||
cy.proxy("express")
|
||||
cy.useConfigProfile("email")
|
||||
|
||||
cy.browserReturnUrlOry()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should redirect to return_to when retrying expired flow", () => {
|
||||
cy.shortLoginLifespan()
|
||||
cy.wait(500)
|
||||
|
||||
cy.visit(express.login + "?return_to=https://www.example.org/")
|
||||
|
||||
cy.longLoginLifespan()
|
||||
|
||||
cy.get(appPrefix("express") + 'input[name="identifier"]').type(
|
||||
email.toUpperCase(),
|
||||
)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
|
||||
cy.submitPasswordForm()
|
||||
cy.get('[data-testid="ui/message/4010001"]').should(
|
||||
"contain.text",
|
||||
"The login flow expired",
|
||||
)
|
||||
|
||||
// try again with long lifespan set
|
||||
cy.get('input[name="identifier"]').type(email.toUpperCase())
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.submitPasswordForm()
|
||||
|
||||
// check that redirection has happened
|
||||
cy.url().should("eq", "https://www.example.org/")
|
||||
})
|
||||
})
|
||||
})
|
||||
55
test/e2e/cypress/integration/profiles/email/login/ui.spec.ts
Normal file
55
test/e2e/cypress/integration/profiles/email/login/ui.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
import { appPrefix } from "../../../../helpers"
|
||||
|
||||
// playwright:migrated
|
||||
context("UI tests using the email profile", () => {
|
||||
;[
|
||||
{
|
||||
route: express.login,
|
||||
app: "express" as "express",
|
||||
profile: "email",
|
||||
},
|
||||
{
|
||||
route: react.login,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
].forEach(({ route, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should use the json schema titles", () => {
|
||||
cy.get(`${appPrefix(app)}input[name="identifier"]`)
|
||||
.parent()
|
||||
.should("contain.text", "Your E-Mail")
|
||||
|
||||
cy.get('input[name="password"]')
|
||||
.parentsUntil("label")
|
||||
.should("contain.text", "Password")
|
||||
cy.get('button[value="password"]').should("contain.text", "Sign in")
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("clicks the log in link", () => {
|
||||
cy.get('a[href*="registration"]').click()
|
||||
cy.location("pathname").should("include", "registration")
|
||||
|
||||
if (app === "express") {
|
||||
cy.location("search").should("not.be.empty")
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, gen, website } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
context("Testing logout flows", () => {
|
||||
;[
|
||||
{
|
||||
route: express.login,
|
||||
app: "express" as "express",
|
||||
profile: "email",
|
||||
settings: express.settings,
|
||||
welcome: express.welcome,
|
||||
},
|
||||
{
|
||||
route: react.login,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
settings: react.settings,
|
||||
welcome: "",
|
||||
},
|
||||
].forEach(({ route, profile, app, settings, welcome }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
let email: string
|
||||
let password: string
|
||||
|
||||
before(() => {
|
||||
cy.proxy(app)
|
||||
|
||||
email = gen.email()
|
||||
password = gen.password()
|
||||
|
||||
cy.useConfigProfile(profile)
|
||||
cy.registerApi({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
cy.login({ email, password, cookieUrl: route })
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should sign out and be able to sign in again", () => {
|
||||
cy.getSession()
|
||||
cy.getCookie("ory_kratos_session").should("not.be.null")
|
||||
if (app === "express") {
|
||||
cy.get(
|
||||
`${appPrefix(app)} [data-testid="logout"] a:not(.disabled)`,
|
||||
).click()
|
||||
} else {
|
||||
cy.get(
|
||||
`${appPrefix(app)} [data-testid="logout"]:not(.disabled)`,
|
||||
).click()
|
||||
}
|
||||
cy.getCookie("ory_kratos_session").should("be.null")
|
||||
cy.noSession()
|
||||
cy.url().should("include", "/login")
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should be able to sign out on settings page", () => {
|
||||
if (app === "react") {
|
||||
return
|
||||
}
|
||||
cy.sessionRequiresNo2fa()
|
||||
cy.useLaxAal()
|
||||
|
||||
cy.getSession({ expectAal: "aal1" })
|
||||
cy.getCookie("ory_kratos_session").should("not.be.null")
|
||||
|
||||
cy.visit(settings, {
|
||||
qs: {
|
||||
return_to: "https://www.example.org",
|
||||
},
|
||||
})
|
||||
|
||||
cy.get("a[href*='logout']").click()
|
||||
cy.location("host").should("eq", "www.example.org")
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should be able to sign out on welcome page", () => {
|
||||
if (app === "react") {
|
||||
return
|
||||
}
|
||||
cy.sessionRequiresNo2fa()
|
||||
cy.useLaxAal()
|
||||
|
||||
cy.getSession({ expectAal: "aal1" })
|
||||
|
||||
cy.visit(welcome, {
|
||||
qs: {
|
||||
return_to: "https://www.example.org",
|
||||
},
|
||||
})
|
||||
|
||||
cy.get("a[href*='logout']").click()
|
||||
cy.location("host").should("eq", "www.example.org")
|
||||
})
|
||||
|
||||
it("should be able to sign out at 2fa page", () => {
|
||||
if (app === "react") {
|
||||
return
|
||||
}
|
||||
cy.useLookupSecrets(true)
|
||||
cy.sessionRequires2fa()
|
||||
cy.getSession({ expectAal: "aal1" })
|
||||
cy.getCookie("ory_kratos_session").should("not.be.null")
|
||||
|
||||
// add 2fa to account
|
||||
cy.visit(settings)
|
||||
cy.get(
|
||||
appPrefix(app) + 'button[name="lookup_secret_regenerate"]',
|
||||
).click()
|
||||
cy.get('button[name="lookup_secret_confirm"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
cy.logout()
|
||||
cy.visit(route, {
|
||||
qs: {
|
||||
return_to: "https://www.example.org",
|
||||
},
|
||||
})
|
||||
|
||||
cy.get('[name="identifier"]').clear().type(email)
|
||||
|
||||
cy.reauth({
|
||||
expect: { email, success: false },
|
||||
type: { password: password },
|
||||
})
|
||||
|
||||
cy.get("a[href*='logout']").click()
|
||||
|
||||
cy.location("host").should("eq", "www.example.org")
|
||||
cy.useLookupSecrets(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,268 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, gen, website } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
describe("Registration failures with email profile", () => {
|
||||
;[
|
||||
{
|
||||
route: express.registration,
|
||||
app: "express" as "express",
|
||||
profile: "email",
|
||||
},
|
||||
{
|
||||
route: react.registration,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
].forEach(({ route, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
cy.updateConfigFile((config) => {
|
||||
config.selfservice.flows.registration.login_hints = true
|
||||
return config
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
const identity = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
it("fails when CSRF cookies are missing", () => {
|
||||
cy.get(`${appPrefix(app)} input[name="traits.website"]`).type(
|
||||
"https://www.ory.sh",
|
||||
)
|
||||
cy.get('input[name="traits.email"]')
|
||||
.type(identity)
|
||||
.should("have.value", identity)
|
||||
cy.get('input[name="password"]')
|
||||
.type("12345678")
|
||||
.should("have.value", "12345678")
|
||||
|
||||
cy.shouldHaveCsrfError({ app })
|
||||
})
|
||||
|
||||
it("fails when a disallowed return_to url is requested", () => {
|
||||
cy.shouldErrorOnDisallowedReturnTo(
|
||||
route + "?return_to=https://not-allowed",
|
||||
{ app },
|
||||
)
|
||||
})
|
||||
|
||||
describe("show errors when invalid signup data is used", () => {
|
||||
// playwright:migrated
|
||||
it("should show an error when the password has leaked before", () => {
|
||||
cy.get('input[name="traits.website"]').type("https://www.ory.sh")
|
||||
cy.get('input[name="traits.email"]')
|
||||
.type(identity)
|
||||
.should("have.value", identity)
|
||||
cy.get('input[name="password"]')
|
||||
.type("12345678")
|
||||
.should("have.value", "12345678")
|
||||
|
||||
cy.submitPasswordForm()
|
||||
cy.get('[data-testid="ui/message/4000034"]').should(
|
||||
"contain.text",
|
||||
"data breaches",
|
||||
)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show an error when the password is too similar", () => {
|
||||
cy.get('input[name="traits.website"]').type("https://www.ory.sh")
|
||||
cy.get('input[name="traits.email"]').type(identity)
|
||||
cy.get('input[name="password"]').type(identity)
|
||||
|
||||
cy.submitPasswordForm()
|
||||
cy.get('[data-testid="ui/message/4000031"]').should(
|
||||
"contain.text",
|
||||
"too similar",
|
||||
)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show an error when the password is empty", () => {
|
||||
cy.get('input[name="traits.website"]').type("https://www.ory.sh")
|
||||
cy.get('input[name="traits.email"]').type(identity)
|
||||
|
||||
// the browser will prevent the form from being submitted if the input field is required
|
||||
// we should remove the required attribute to simulate the data not being sent
|
||||
cy.removeAttribute(['input[name="password"]'], "required")
|
||||
|
||||
cy.submitPasswordForm()
|
||||
cy.get('*[data-testid^="ui/message/"]')
|
||||
.invoke("text")
|
||||
.then((text) => {
|
||||
expect(text.trim()).to.be.oneOf([
|
||||
"length must be >= 1, but got 0",
|
||||
"Property password is missing.",
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show an error when the email is empty", () => {
|
||||
cy.get('input[name="traits.website"]').type("https://www.ory.sh")
|
||||
cy.get('input[name="password"]').type(password)
|
||||
|
||||
// the browser will prevent the form from being submitted if the input field is required
|
||||
// we should remove the required attribute to simulate the data not being sent
|
||||
cy.removeAttribute(['input[name="traits.email"]'], "required")
|
||||
|
||||
cy.submitPasswordForm()
|
||||
cy.get('*[data-testid^="ui/message/"]')
|
||||
.invoke("text")
|
||||
.then((text) => {
|
||||
expect(text.trim()).to.be.oneOf([
|
||||
'"" is not valid "email"',
|
||||
"length must be >= 3, but got 0",
|
||||
"Property email is missing.",
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show an error when the email is not an email", () => {
|
||||
cy.get('input[name="traits.website"]').type("https://www.ory.sh")
|
||||
cy.get('input[name="password"]').type(password)
|
||||
|
||||
// the browser will prevent the form from being submitted if the input data doesn't conform to the input field type
|
||||
// in this case an invalid email will prevent the form from being submitted by the browser
|
||||
// we should remove it to ensure kratos is validating the payload
|
||||
cy.get('input[name="traits.email"]').then(($el) =>
|
||||
$el.removeAttr("type"),
|
||||
)
|
||||
cy.get('input[name="traits.email"]').type("not-an-email")
|
||||
|
||||
cy.submitPasswordForm()
|
||||
cy.get(
|
||||
'*[data-testid="ui/message/4000040"], *[data-testid="ui/message/4000002"]',
|
||||
).should("exist")
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show a missing indicator if no fields are set", () => {
|
||||
// the browser will prevent the form from being submitted if the input field is required
|
||||
// we should remove the required attribute to simulate the data not being sent
|
||||
cy.removeAttribute(
|
||||
[
|
||||
'input[name="traits.email"]',
|
||||
'input[name="traits.website"]',
|
||||
'input[name="password"]',
|
||||
],
|
||||
"required",
|
||||
)
|
||||
|
||||
cy.submitPasswordForm()
|
||||
cy.get(
|
||||
'*[data-testid="ui/message/4000001"], *[data-testid="ui/message/4000002"]',
|
||||
).should("exist")
|
||||
})
|
||||
|
||||
it("should show an error when the website is not a valid URI", () => {
|
||||
cy.get('input[name="traits.website"]')
|
||||
.type("1234")
|
||||
.then(($input) => {
|
||||
expect(
|
||||
($input[0] as HTMLInputElement).validationMessage,
|
||||
).to.contain("URL")
|
||||
})
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show an error when the website is too short", () => {
|
||||
// the browser will prevent the form from being submitted if the input field is required
|
||||
// we should remove the required attribute to simulate the data not being sent
|
||||
cy.removeAttribute(
|
||||
['input[name="traits.email"]', 'input[name="password"]'],
|
||||
"required",
|
||||
)
|
||||
|
||||
cy.get('input[name="traits.website"]').type("http://s")
|
||||
|
||||
cy.submitPasswordForm()
|
||||
cy.get('[data-testid="ui/message/4000003"]').should(
|
||||
"contain.text",
|
||||
"length must be >= 10",
|
||||
)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show an error when required params are missing", () => {
|
||||
// the browser will prevent the form from being submitted if the input field is required
|
||||
// we should remove it from the DOM entirely to simulate the data not being sent
|
||||
cy.get('input[name="traits.website"]').then(($el) => $el.remove())
|
||||
cy.get('input[name="traits.email"]').then(($el) => $el.remove())
|
||||
cy.get('input[name="password"]').then(($el) => $el.remove())
|
||||
|
||||
cy.submitPasswordForm()
|
||||
cy.get('[data-testid="ui/message/4000002"]').should(
|
||||
"contain.text",
|
||||
"Property website is missing.",
|
||||
)
|
||||
cy.get('[data-testid="ui/message/4000002"]').should(
|
||||
"contain.text",
|
||||
"Property email is missing.",
|
||||
)
|
||||
cy.get('[data-testid="ui/message/4000002"]').should(
|
||||
"contain.text",
|
||||
"Property password is missing.",
|
||||
)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show an error when the age is too high", () => {
|
||||
// the browser will prevent the form from being submitted if the input field is required
|
||||
// we should remove the required attribute to simulate the data not being sent
|
||||
cy.removeAttribute(
|
||||
[
|
||||
'input[name="traits.email"]',
|
||||
'input[name="traits.website"]',
|
||||
'input[name="password"]',
|
||||
],
|
||||
"required",
|
||||
)
|
||||
|
||||
cy.get('input[name="traits.age"]').type("600")
|
||||
|
||||
cy.submitPasswordForm()
|
||||
cy.get('[data-testid="ui/message/4000020"]').should(
|
||||
"contain.text",
|
||||
"must be <= 300 but found 600",
|
||||
)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show a hint for existing account", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
cy.registerApi({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
|
||||
cy.get('input[name="traits.email"]').type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.get('input[name="traits.website').type(website)
|
||||
cy.get('input[name="traits.age"]').type(`30`)
|
||||
cy.submitPasswordForm()
|
||||
cy.get('[data-testid="ui/message/4000028"]').should(
|
||||
"contain.text",
|
||||
"You tried signing in with " +
|
||||
email +
|
||||
" which is already in use by another account. You can sign in using your password.",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,210 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, APP_URL, gen } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
context("Registration success with email profile", () => {
|
||||
;[
|
||||
{
|
||||
route: express.registration,
|
||||
app: "express" as "express",
|
||||
profile: "email",
|
||||
},
|
||||
{
|
||||
route: react.registration,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
].forEach(({ route, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.deleteMail()
|
||||
cy.clearAllCookies()
|
||||
cy.visit(route)
|
||||
cy.enableVerification()
|
||||
if (app === "express") {
|
||||
cy.enableVerificationUIAfterRegistration("password")
|
||||
}
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should sign up and be logged in", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
const website = "https://www.example.org/"
|
||||
const age = 30
|
||||
|
||||
cy.get(appPrefix(app) + 'input[name="traits"]').should("not.exist")
|
||||
cy.get('input[name="traits.email"]').type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.get('input[name="traits.website').type(website)
|
||||
cy.get('input[name="traits.age"]').type(`${age}`)
|
||||
cy.get('[type="checkbox"][name="traits.tos"]').click({ force: true })
|
||||
|
||||
cy.submitPasswordForm()
|
||||
|
||||
cy.url().should("contain", "verification")
|
||||
cy.getVerificationCodeFromEmail(email).then((code) => {
|
||||
cy.get("input[name=code]").type(code)
|
||||
cy.get("button[name=method][value=code]").click()
|
||||
})
|
||||
|
||||
cy.get('[data-testid="ui/message/1080002"]').should(
|
||||
"have.text",
|
||||
"You successfully verified your email address.",
|
||||
)
|
||||
|
||||
cy.get("[data-testid='node/anchor/continue']").click()
|
||||
|
||||
if (app === "express") {
|
||||
cy.get('a[href*="sessions"').click()
|
||||
}
|
||||
cy.get("pre").should("contain.text", email)
|
||||
|
||||
cy.getSession().should((session) => {
|
||||
const { identity } = session
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.verifiable_addresses).to.have.length(1)
|
||||
expect(identity.schema_id).to.equal("default")
|
||||
expect(identity.schema_url).to.equal(`${APP_URL}/schemas/ZGVmYXVsdA`)
|
||||
expect(identity.traits.website).to.equal(website)
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
expect(identity.traits.age).to.equal(age)
|
||||
expect(identity.traits.tos).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should sign up with advanced form field values be logged in", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
cy.get('input[name="traits"]').should("not.exist")
|
||||
cy.get('input[name="traits.email"]').type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
const website = "https://www.example.org/"
|
||||
cy.get('input[name="traits.website"]').type(website)
|
||||
|
||||
cy.submitPasswordForm()
|
||||
|
||||
cy.url().should("contain", "verification")
|
||||
cy.getVerificationCodeFromEmail(email).then((code) => {
|
||||
cy.get("input[name=code]").type(code)
|
||||
cy.get("button[name=method][value=code]").click()
|
||||
})
|
||||
|
||||
cy.get('[data-testid="ui/message/1080002"]').should(
|
||||
"have.text",
|
||||
"You successfully verified your email address.",
|
||||
)
|
||||
|
||||
cy.get("[data-testid='node/anchor/continue']").click()
|
||||
|
||||
if (app === "express") {
|
||||
cy.get('a[href*="sessions"').click()
|
||||
}
|
||||
cy.get("pre").should("contain.text", email)
|
||||
|
||||
cy.getSession().should((session) => {
|
||||
const { identity } = session
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.verifiable_addresses).to.have.length(1)
|
||||
expect(identity.schema_id).to.equal("default")
|
||||
expect(identity.schema_url).to.equal(`${APP_URL}/schemas/ZGVmYXVsdA`)
|
||||
expect(identity.traits.website).to.equal(website)
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
expect(identity.traits.age).to.be.undefined
|
||||
expect(identity.traits.tos).to.be.oneOf([false, undefined])
|
||||
})
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should sign up and be redirected", () => {
|
||||
cy.disableVerification()
|
||||
cy.browserReturnUrlOry()
|
||||
cy.visit(route + "?return_to=https://www.example.org/")
|
||||
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
const website = "https://www.example.org/"
|
||||
|
||||
cy.get('input[name="traits"]').should("not.exist")
|
||||
cy.get('input[name="traits.email"]').type(email)
|
||||
cy.get('input[name="traits.website').type(website)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.submitPasswordForm()
|
||||
|
||||
cy.url().should("eq", "https://www.example.org/")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("redirect for express app", () => {
|
||||
it("should redirect to return_to after flow expires", () => {
|
||||
// Wait for flow to expire
|
||||
cy.useConfigProfile("email")
|
||||
cy.shortRegisterLifespan()
|
||||
cy.browserReturnUrlOry()
|
||||
cy.proxy("express")
|
||||
cy.visit(express.registration + "?return_to=https://www.example.org/")
|
||||
cy.wait(105)
|
||||
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
const website = "https://www.example.org/"
|
||||
|
||||
cy.get(`${appPrefix("express")} input[name="traits"]`).should("not.exist")
|
||||
cy.get('input[name="traits.email"]').type(email)
|
||||
cy.get('input[name="traits.website').type(website)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
|
||||
cy.longRegisterLifespan()
|
||||
cy.submitPasswordForm()
|
||||
|
||||
cy.get('[data-testid="ui/message/4040001"]').should(
|
||||
"contain.text",
|
||||
"The registration flow expired",
|
||||
)
|
||||
|
||||
// Try again with long lifespan set
|
||||
cy.get('input[name="traits"]').should("not.exist")
|
||||
cy.get('input[name="traits.email"]').type(email)
|
||||
cy.get('input[name="traits.website').type(website)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.submitPasswordForm()
|
||||
|
||||
cy.url().should("eq", "https://www.example.org/")
|
||||
})
|
||||
|
||||
it("should not redirect to verification_flow if not configured", () => {
|
||||
cy.deleteMail()
|
||||
cy.useConfigProfile("email")
|
||||
cy.enableVerification()
|
||||
cy.proxy("express")
|
||||
cy.visit(express.registration + "?return_to=https://www.example.org/")
|
||||
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
const website = "https://www.example.org/"
|
||||
|
||||
cy.get(`${appPrefix("express")} input[name="traits"]`).should("not.exist")
|
||||
cy.get('input[name="traits.email"]').type(email)
|
||||
cy.get('input[name="traits.website').type(website)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
|
||||
cy.submitPasswordForm()
|
||||
|
||||
// Verify that the verification code is still sent
|
||||
cy.getVerificationCodeFromEmail(email).should("not.be.undefined")
|
||||
|
||||
cy.url().should("eq", "https://www.example.org/")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
import { appPrefix } from "../../../../helpers"
|
||||
|
||||
describe("Registration UI for email flows", () => {
|
||||
;[
|
||||
{
|
||||
route: express.registration,
|
||||
app: "express" as "express",
|
||||
profile: "email",
|
||||
},
|
||||
{
|
||||
route: react.registration,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
].forEach(({ route, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
describe("use ui elements", () => {
|
||||
// playwright:migrated
|
||||
it("should use the json schema titles", () => {
|
||||
cy.get(appPrefix(app) + 'input[name="traits.email"]')
|
||||
.parent()
|
||||
.should("contain.text", "Your E-Mail")
|
||||
cy.get('input[name="traits.website"]')
|
||||
.parent()
|
||||
.should("contain.text", "Your website")
|
||||
cy.get('button[value="password"]').should("contain.text", "Sign up")
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("clicks the log in link", () => {
|
||||
cy.get('*[data-testid="cta-link"]').click()
|
||||
cy.location("pathname").should("include", "/login")
|
||||
if (app === "express") {
|
||||
cy.location("search").should("not.be.empty")
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,314 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, gen, website } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
context("Settings failures with email profile", () => {
|
||||
;[
|
||||
{
|
||||
route: express.settings,
|
||||
base: express.base,
|
||||
app: "express" as "express",
|
||||
profile: "email",
|
||||
},
|
||||
{
|
||||
route: react.settings,
|
||||
base: react.base,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
].forEach(({ route, profile, app, base }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
let email = gen.email()
|
||||
let password = gen.password()
|
||||
|
||||
const emailSecond = `second-${gen.email()}`
|
||||
const passwordSecond = gen.password()
|
||||
|
||||
const up = (value) => `not-${value}`
|
||||
|
||||
before(() => {
|
||||
cy.proxy(app)
|
||||
cy.useConfigProfile(profile)
|
||||
cy.registerApi({
|
||||
email: emailSecond,
|
||||
password: passwordSecond,
|
||||
fields: { "traits.website": "https://github.com/ory/kratos" },
|
||||
})
|
||||
cy.registerApi({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.longPrivilegedSessionTime()
|
||||
|
||||
cy.visit(base)
|
||||
cy.clearAllCookies()
|
||||
|
||||
cy.login({ email, password, cookieUrl: base })
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
describe("profile", () => {
|
||||
beforeEach(() => {
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("fails with validation errors", () => {
|
||||
cy.get('input[name="traits.website"]').clear().type("http://s")
|
||||
cy.get('[name="method"][value="profile"]').click()
|
||||
cy.get('[data-testid="ui/message/4000003"]').should(
|
||||
"contain.text",
|
||||
"length must be >= 10",
|
||||
)
|
||||
})
|
||||
|
||||
it("fails because reauth is another person", () => {
|
||||
// Force this because it is hidden
|
||||
cy.get('input[name="traits.email"]').clear().type(up(email))
|
||||
cy.shortPrivilegedSessionTime()
|
||||
|
||||
cy.location().then((loc) => {
|
||||
cy.get('button[value="profile"]').click()
|
||||
|
||||
cy.reauthWithOtherAccount({
|
||||
previousUrl: loc.toString(),
|
||||
expect: { email },
|
||||
type: { email: emailSecond, password: passwordSecond },
|
||||
})
|
||||
|
||||
cy.location("pathname").should("contain", "/settings")
|
||||
})
|
||||
|
||||
// We end up in a new settings flow for the second user
|
||||
cy.get('input[name="traits.email"]').should("have.value", emailSecond)
|
||||
|
||||
// Try to log in with updated credentials -> should fail
|
||||
cy.clearAllCookies()
|
||||
cy.login({
|
||||
email: up(email),
|
||||
password,
|
||||
expectSession: false,
|
||||
cookieUrl: base,
|
||||
})
|
||||
})
|
||||
|
||||
it("does not update data because resumable session was removed", () => {
|
||||
cy.get('input[name="traits.email"]').clear().type(up(email))
|
||||
cy.shortPrivilegedSessionTime()
|
||||
cy.get('button[value="profile"]').click()
|
||||
|
||||
cy.login({ email, password, cookieUrl: base })
|
||||
|
||||
cy.getSession().should((session) => {
|
||||
const { identity } = session
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
|
||||
it("does not update without re-auth", () => {
|
||||
cy.get('input[name="traits.email"]').clear().type(up(email))
|
||||
cy.shortPrivilegedSessionTime() // wait for the privileged session to time out
|
||||
cy.get('button[value="profile"]').click()
|
||||
|
||||
cy.visit(base)
|
||||
|
||||
cy.getSession().should((session) => {
|
||||
const { identity } = session
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
|
||||
it("does not resume another failed request", () => {
|
||||
// checks here that we're checking settingsRequest.id == cookie.stored.id
|
||||
cy.get('input[name="traits.email"]').clear().type(up(email))
|
||||
cy.shortPrivilegedSessionTime() // wait for the privileged session to time out
|
||||
cy.get('button[value="profile"]').click()
|
||||
cy.location("pathname").should("not.contain", "/settings")
|
||||
|
||||
cy.visit(route)
|
||||
cy.get('input[name="traits.website"]')
|
||||
.clear()
|
||||
.type("http://github.com/aeneasr")
|
||||
cy.get('button[value="profile"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
cy.getSession().should((session) => {
|
||||
const { identity } = session
|
||||
expect(identity.traits.email).to.equal(email) // this is NOT up(email)
|
||||
expect(identity.traits.website).to.equal(
|
||||
"http://github.com/aeneasr",
|
||||
) // this is NOT up(email)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("password", () => {
|
||||
beforeEach(() => {
|
||||
cy.longPrivilegedSessionTime()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cy.longPrivilegedSessionTime()
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("fails if password policy is violated", () => {
|
||||
cy.get('input[name="password"]').clear().type("12345678")
|
||||
cy.get('button[value="password"]').click()
|
||||
cy.get('[data-testid="ui/message/4000034"]').should(
|
||||
"contain.text",
|
||||
"data breaches",
|
||||
)
|
||||
})
|
||||
|
||||
it("fails because reauth is another person", () => {
|
||||
cy.shortPrivilegedSessionTime() // wait for the privileged session to time out
|
||||
cy.get('input[name="password"]').clear().type(up(password))
|
||||
|
||||
let firstSession
|
||||
cy.getSession().then((session) => {
|
||||
firstSession = session
|
||||
})
|
||||
|
||||
cy.location().then((loc) => {
|
||||
cy.get('button[value="password"]').click()
|
||||
|
||||
cy.reauthWithOtherAccount({
|
||||
previousUrl: loc.toString(),
|
||||
expect: { email },
|
||||
type: { email: emailSecond, password: passwordSecond },
|
||||
})
|
||||
|
||||
cy.location("pathname").should("contain", "/settings")
|
||||
})
|
||||
|
||||
// We want to ensure that the reauth session is completely different from the one we had in the first place.
|
||||
cy.getSession().then((session) => {
|
||||
expect(session.authentication_methods).to.have.length(1)
|
||||
expect(session.identity.traits.email).to.eq(emailSecond)
|
||||
expect(session.id).to.not.eq(firstSession.id)
|
||||
expect(session.identity.id).to.not.eq(firstSession.identity.id)
|
||||
expect(session.authenticated_at).to.not.eq(
|
||||
firstSession.authenticated_at,
|
||||
)
|
||||
})
|
||||
|
||||
// We end up in a new settings flow for the second user
|
||||
cy.get('input[name="traits.email"]').should("have.value", emailSecond)
|
||||
|
||||
// Try to log in with updated credentials -> should fail
|
||||
cy.clearAllCookies()
|
||||
cy.login({
|
||||
email,
|
||||
password: up(password),
|
||||
expectSession: false,
|
||||
cookieUrl: base,
|
||||
})
|
||||
})
|
||||
|
||||
it("does not update without re-auth", () => {
|
||||
cy.get('input[name="password"]').clear().type(up(password))
|
||||
cy.shortPrivilegedSessionTime() // wait for the privileged session to time out
|
||||
cy.get('button[value="password"]').click()
|
||||
|
||||
cy.visit(base)
|
||||
cy.clearAllCookies()
|
||||
cy.login({
|
||||
email,
|
||||
password: up(password),
|
||||
expectSession: false,
|
||||
cookieUrl: base,
|
||||
})
|
||||
})
|
||||
|
||||
it("does not update data because resumable session was removed", () => {
|
||||
cy.get('input[name="password"]').clear().type(up(password))
|
||||
cy.shortPrivilegedSessionTime() // wait for the privileged session to time out
|
||||
cy.get('button[value="password"]').click()
|
||||
|
||||
cy.clearAllCookies()
|
||||
cy.login({ email, password, cookieUrl: base })
|
||||
cy.clearAllCookies()
|
||||
cy.login({
|
||||
email,
|
||||
password: up(password),
|
||||
expectSession: false,
|
||||
cookieUrl: base,
|
||||
})
|
||||
})
|
||||
|
||||
it("does not resume another queued request", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
cy.clearAllCookies()
|
||||
cy.register({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
cy.visit(route)
|
||||
|
||||
// checks here that we're checking settingsRequest.id == cookie.stored.id
|
||||
const invalidPassword = "invalid-" + gen.password()
|
||||
cy.get('input[name="password"]').clear().type(invalidPassword)
|
||||
cy.shortPrivilegedSessionTime() // wait for the privileged session to time out
|
||||
cy.get('button[value="password"]').click()
|
||||
cy.location("pathname").should("include", "/login")
|
||||
|
||||
const validPassword = "valid-" + gen.password()
|
||||
cy.visit(route)
|
||||
cy.get('input[name="password"]').clear().type(validPassword)
|
||||
cy.get('button[value="password"]').click()
|
||||
|
||||
cy.location("pathname").should("include", "/login")
|
||||
cy.reauth({ expect: { email }, type: { password: password } })
|
||||
|
||||
cy.location("pathname").should("include", "/settings")
|
||||
cy.get('input[name="password"]').should("exist")
|
||||
|
||||
// This should pass because it is the correct password
|
||||
cy.clearAllCookies()
|
||||
cy.login({ email, password: validPassword, cookieUrl: base })
|
||||
|
||||
// This should fail because it is the wrong password
|
||||
cy.clearAllCookies()
|
||||
cy.login({
|
||||
email,
|
||||
password: invalidPassword,
|
||||
expectSession: false,
|
||||
cookieUrl: base,
|
||||
})
|
||||
|
||||
cy.clearAllCookies()
|
||||
cy.login({
|
||||
email,
|
||||
password: password,
|
||||
expectSession: false,
|
||||
cookieUrl: base,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("global errors", () => {
|
||||
it("fails when CSRF is incorrect", () => {
|
||||
cy.get(appPrefix(app) + 'input[name="password"]').type("12345678")
|
||||
cy.shouldHaveCsrfError({ app })
|
||||
})
|
||||
|
||||
it("fails when a disallowed return_to url is requested", () => {
|
||||
cy.shouldErrorOnDisallowedReturnTo(
|
||||
route + "?return_to=https://not-allowed",
|
||||
{ app },
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,194 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, gen, website } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
context("Settings success with email profile", () => {
|
||||
;[
|
||||
{
|
||||
route: express.settings,
|
||||
base: express.base,
|
||||
app: "express" as "express",
|
||||
profile: "email",
|
||||
login: express.login,
|
||||
},
|
||||
{
|
||||
route: react.settings,
|
||||
base: react.base,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
login: react.login,
|
||||
},
|
||||
].forEach(({ route, profile, app, base, login }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
let email = gen.email()
|
||||
const firstPassword = gen.password()
|
||||
const secondPassword = gen.password()
|
||||
const thirdPassword = gen.password()
|
||||
let password = firstPassword
|
||||
|
||||
const up = (value: string) => `not-${value}`
|
||||
const down = (value: string) => value.replace(/not-/, "")
|
||||
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.registerApi({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.deleteMail()
|
||||
cy.login({ email, password, cookieUrl: base })
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("shows all settings forms", () => {
|
||||
cy.get(appPrefix(app) + "h3").should("contain.text", "Profile")
|
||||
cy.get('input[name="traits.email"]').should("contain.value", email)
|
||||
cy.get('input[name="traits.website"]').should("contain.value", website)
|
||||
|
||||
cy.get("h3").should("contain.text", "Password")
|
||||
cy.get('input[name="password"]').should("be.empty")
|
||||
})
|
||||
|
||||
describe("password", () => {
|
||||
// playwright:migrated
|
||||
it("modifies the password with privileged session", () => {
|
||||
// Once input weak password to test which error message is cleared after updating successfully
|
||||
cy.get('input[name="password"]').clear().type("123")
|
||||
cy.get('button[value="password"]').click()
|
||||
cy.get('[data-testid="ui/message/1050001"]').should("not.exist")
|
||||
cy.get('[data-testid="ui/message/4000032"]').should("exist")
|
||||
cy.get('input[name="password"]').should("be.empty")
|
||||
|
||||
password = secondPassword
|
||||
cy.get('input[name="password"]').clear().type(secondPassword)
|
||||
cy.get('button[value="password"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
cy.get('[data-testid="ui/message/4000032"]').should("not.exist")
|
||||
cy.get('[data-testid="ui/message/1050001"]').should("exist")
|
||||
cy.get('input[name="password"]').should("be.empty")
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("is unable to log in with the old password", () => {
|
||||
cy.login({
|
||||
email: email,
|
||||
password: firstPassword,
|
||||
expectSession: false,
|
||||
cookieUrl: base,
|
||||
})
|
||||
})
|
||||
|
||||
it("modifies the password with an unprivileged session", () => {
|
||||
password = thirdPassword
|
||||
cy.get('input[name="password"]').clear().type(password)
|
||||
cy.shortPrivilegedSessionTime() // wait for the privileged session to time out
|
||||
cy.get('button[value="password"]').click()
|
||||
|
||||
cy.reauth({ expect: { email }, type: { password: secondPassword } })
|
||||
|
||||
cy.url().should("include", "/settings")
|
||||
cy.expectSettingsSaved()
|
||||
cy.get('input[name="password"]').should("be.empty")
|
||||
})
|
||||
})
|
||||
|
||||
describe("profile", () => {
|
||||
// playwright:migrated
|
||||
it("modifies an unprotected traits", () => {
|
||||
cy.get('input[name="traits.website"]')
|
||||
.clear()
|
||||
.type("https://github.com/ory")
|
||||
cy.get('input[name="traits.age"]').clear().type("30")
|
||||
cy.get('input[type="checkbox"][name="traits.tos"]').click({
|
||||
force: true,
|
||||
})
|
||||
cy.submitProfileForm()
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
cy.get('input[name="traits.website"]').should(
|
||||
"contain.value",
|
||||
"https://github.com/ory",
|
||||
)
|
||||
cy.get('input[type="checkbox"][name="traits.tos"]')
|
||||
.should("be.checked")
|
||||
.click({ force: true })
|
||||
cy.get('input[name="traits.age"]')
|
||||
.should("have.value", "30")
|
||||
.clear()
|
||||
.type("90")
|
||||
|
||||
cy.submitProfileForm()
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
cy.get('input[type="checkbox"][name="traits.tos"]').should(
|
||||
"not.be.checked",
|
||||
)
|
||||
cy.get('input[name="traits.age"]').should("have.value", "90")
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("modifies a protected trait with privileged session", () => {
|
||||
email = up(email)
|
||||
cy.disableVerification()
|
||||
cy.get('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)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("is unable to log in with the old email", () => {
|
||||
cy.login({
|
||||
email: down(email),
|
||||
password,
|
||||
expectSession: false,
|
||||
cookieUrl: base,
|
||||
})
|
||||
})
|
||||
|
||||
it("modifies a protected trait with unprivileged session", () => {
|
||||
email = up(email)
|
||||
cy.get('input[name="traits.email"]').clear().type(email)
|
||||
cy.shortPrivilegedSessionTime() // wait for the privileged session to time out
|
||||
cy.get('button[value="profile"]').click()
|
||||
|
||||
cy.reauth({ expect: { email: down(email) }, type: { password } })
|
||||
|
||||
cy.url().should("include", "/settings")
|
||||
cy.expectSettingsSaved()
|
||||
cy.get('input[name="traits.email"]').should("contain.value", email)
|
||||
})
|
||||
|
||||
if (app === "react") {
|
||||
it("shows verification screen after email update", () => {
|
||||
cy.deleteMail()
|
||||
cy.enableVerification()
|
||||
email = up(email)
|
||||
cy.get('input[name="traits.email"]').clear().type(email)
|
||||
cy.get('button[value="profile"]').click()
|
||||
|
||||
cy.url().should("contain", "verification")
|
||||
cy.getVerificationCodeFromEmail(email).then((code) => {
|
||||
cy.get("input[name=code]").type(code)
|
||||
cy.get("button[name=method][value=code]").click()
|
||||
})
|
||||
|
||||
cy.get('[data-testid="ui/message/1080002"]').should(
|
||||
"have.text",
|
||||
"You successfully verified your email address.",
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
// 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"
|
||||
|
||||
context("Settings errors with email profile", () => {
|
||||
;[
|
||||
{
|
||||
base: express.base,
|
||||
app: "express" as "express",
|
||||
profile: "email",
|
||||
},
|
||||
{
|
||||
base: react.base,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
].forEach(({ profile, app, base }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
const identity = gen.identity()
|
||||
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.registerApi({
|
||||
...identity,
|
||||
fields: { "traits.website": "https://www.ory.sh/" },
|
||||
})
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.login({ ...identity, cookieUrl: base })
|
||||
cy.visit(base)
|
||||
})
|
||||
|
||||
describe("use ui elements", () => {
|
||||
// playwright:migrated
|
||||
it("should use the json schema titles", () => {
|
||||
const settingsLink = appPrefix(app) + 'a[href*="settings"]'
|
||||
if (app === "express") {
|
||||
cy.get(settingsLink).should("have.attr", "target", "_blank")
|
||||
cy.removeAttribute([settingsLink], "target")
|
||||
}
|
||||
cy.get(settingsLink).click()
|
||||
cy.get('input[name="traits.email"]')
|
||||
.parent()
|
||||
.should("contain.text", "Your E-Mail")
|
||||
cy.get('input[name="traits.website"]')
|
||||
.parent()
|
||||
.should("contain.text", "Your website")
|
||||
|
||||
cy.get('input[name="password"]')
|
||||
.parentsUntil("label")
|
||||
.should("contain.text", "Password")
|
||||
|
||||
cy.get('button[value="profile"]').should("contain.text", "Save")
|
||||
cy.get('button[value="password"]').should("contain.text", "Save")
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("clicks the settings link", () => {
|
||||
const settingsLink = 'a[href*="settings"]'
|
||||
if (app === "express") {
|
||||
cy.get(settingsLink).should("have.attr", "target", "_blank")
|
||||
cy.removeAttribute([settingsLink], "target")
|
||||
}
|
||||
cy.get(settingsLink).click()
|
||||
cy.location("pathname").should("include", "settings")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
133
test/e2e/cypress/integration/profiles/import/import.spec.ts
Normal file
133
test/e2e/cypress/integration/profiles/import/import.spec.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { routes as express } from "../../../helpers/express"
|
||||
import { gen, KRATOS_ADMIN, website } from "../../../helpers"
|
||||
|
||||
context("Import Identities", () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile("oidc")
|
||||
cy.proxy("express")
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
})
|
||||
|
||||
const password = gen.password()
|
||||
for (const tc of [
|
||||
{
|
||||
name: "cleartext",
|
||||
config: {
|
||||
password,
|
||||
},
|
||||
checkPassword: password,
|
||||
},
|
||||
{
|
||||
name: "pbkdf2",
|
||||
config: {
|
||||
hashed_password:
|
||||
"$pbkdf2-sha256$i=1000,l=128$e8/arsEf4cvQihdNgqj0Nw$5xQQKNTyeTHx2Ld5/JDE7A",
|
||||
},
|
||||
checkPassword: "123456",
|
||||
},
|
||||
{
|
||||
name: "bcrypt",
|
||||
config: {
|
||||
hashed_password:
|
||||
"$2a$10$ZsCsoVQ3xfBG/K2z2XpBf.tm90GZmtOqtqWcB5.pYd5Eq8y7RlDyq",
|
||||
},
|
||||
checkPassword: "123456",
|
||||
},
|
||||
{
|
||||
name: "argon2id",
|
||||
config: {
|
||||
hashed_password:
|
||||
"$argon2id$v=19$m=16,t=2,p=1$bVI1aE1SaTV6SGQ3bzdXdw$fnjCcZYmEPOUOjYXsT92Cg",
|
||||
},
|
||||
checkPassword: "123456",
|
||||
},
|
||||
]) {
|
||||
// playwright:migrated
|
||||
it(`should be able to sign in using an imported password (${tc.name})`, () => {
|
||||
const email = gen.email()
|
||||
cy.request("POST", `${KRATOS_ADMIN}/identities`, {
|
||||
schema_id: "default",
|
||||
traits: {
|
||||
email,
|
||||
website,
|
||||
},
|
||||
credentials: {
|
||||
password: {
|
||||
config: tc.config,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cy.visit(express.login)
|
||||
|
||||
// Try to sign in with an incorrect password
|
||||
cy.get('input[name="identifier"]').type(email)
|
||||
cy.get('input[name="password"]').type("invalid-password")
|
||||
cy.submitPasswordForm()
|
||||
cy.get('*[data-testid="ui/message/4000006"]').should(
|
||||
"contain.text",
|
||||
"credentials are invalid",
|
||||
)
|
||||
|
||||
// But with correct password it succeeds
|
||||
cy.get('input[name="password"]').type(tc.checkPassword)
|
||||
cy.submitPasswordForm()
|
||||
|
||||
cy.location("pathname").should("not.contain", "/login")
|
||||
cy.getSession().should((session) => {
|
||||
const { identity } = session
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.traits.website).to.equal(website)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
it(`should be able to sign in using imported oidc credentials`, () => {
|
||||
const email = gen.email()
|
||||
const website = "https://" + gen.password() + ".com"
|
||||
cy.request("POST", `${KRATOS_ADMIN}/identities`, {
|
||||
schema_id: "default",
|
||||
traits: {
|
||||
email,
|
||||
website,
|
||||
},
|
||||
credentials: {
|
||||
oidc: {
|
||||
config: {
|
||||
providers: [
|
||||
{
|
||||
provider: "hydra",
|
||||
subject: email,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cy.visit(express.login)
|
||||
cy.triggerOidc("express")
|
||||
|
||||
cy.get("#username").clear().type(email)
|
||||
cy.get("#remember").click()
|
||||
cy.get("#accept").click()
|
||||
cy.get('[name="scope"]').each(($el) => cy.wrap($el).click())
|
||||
cy.get("#remember").click()
|
||||
cy.get("#accept").click()
|
||||
|
||||
// Wait for the post-OIDC-consent redirect chain to settle so the session
|
||||
// cookie is in Cypress's jar before we hit /sessions/whoami.
|
||||
cy.location("pathname").should("not.contain", "/login")
|
||||
cy.getSession().should((session) => {
|
||||
const { identity } = session
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.traits.website).to.equal(website)
|
||||
})
|
||||
})
|
||||
})
|
||||
131
test/e2e/cypress/integration/profiles/import/import_mfa.spec.ts
Normal file
131
test/e2e/cypress/integration/profiles/import/import_mfa.spec.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { TOTP } from "otpauth"
|
||||
|
||||
import { gen, KRATOS_ADMIN, website } from "../../../helpers"
|
||||
import { routes as express } from "../../../helpers/express"
|
||||
|
||||
// Regression coverage for https://github.com/ory/kratos/issues/4561.
|
||||
//
|
||||
// When TOTP or lookup-secret credentials are imported through the admin
|
||||
// identity API, the user must be able to use them to complete an AAL2
|
||||
// login. Previously the import wrote the credential row but not the
|
||||
// matching identity_credential_identifiers row, so AAL2 login would fail
|
||||
// with "You have no TOTP device set up." or "You have not configured
|
||||
// backup codes yet.".
|
||||
context("Import Identities with MFA credentials", () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile("mfa")
|
||||
cy.proxy("express")
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
cy.useConfig((builder) =>
|
||||
builder.longPrivilegedSessionTime().disableCodeMfa(),
|
||||
)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should be able to complete AAL2 login using imported TOTP credentials", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
// Build the TOTP secret on the test side so we can both ship its
|
||||
// otpauth URL into the import payload and generate matching codes
|
||||
// later when we authenticate.
|
||||
const totp = new TOTP({
|
||||
issuer: "Ory",
|
||||
label: email,
|
||||
algorithm: "SHA1",
|
||||
digits: 6,
|
||||
period: 30,
|
||||
})
|
||||
|
||||
cy.request("POST", `${KRATOS_ADMIN}/identities`, {
|
||||
schema_id: "default",
|
||||
traits: { email, website },
|
||||
credentials: {
|
||||
password: { config: { password } },
|
||||
totp: { config: { totp_url: totp.toString() } },
|
||||
},
|
||||
})
|
||||
|
||||
// Force AAL2 on sign-in so we exercise the TOTP step-up path.
|
||||
cy.requireStrictAal()
|
||||
cy.visit(express.login)
|
||||
|
||||
cy.get('input[name="identifier"]').type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.submitPasswordForm()
|
||||
|
||||
cy.shouldShow2FAScreen()
|
||||
|
||||
cy.get('input[name="totp_code"]').type(totp.generate())
|
||||
cy.get('*[name="method"][value="totp"]').click()
|
||||
|
||||
cy.location("pathname").should("not.contain", "/login")
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["password", "totp"],
|
||||
})
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should be able to complete AAL2 login using imported lookup-secret credentials", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
// Generate a few human-readable codes so failures in the test log
|
||||
// are easy to read; in production these would be opaque strings.
|
||||
const codes = [
|
||||
"imported-recovery-1",
|
||||
"imported-recovery-2",
|
||||
"imported-recovery-3",
|
||||
"imported-recovery-4",
|
||||
]
|
||||
|
||||
cy.request("POST", `${KRATOS_ADMIN}/identities`, {
|
||||
schema_id: "default",
|
||||
traits: { email, website },
|
||||
credentials: {
|
||||
password: { config: { password } },
|
||||
lookup_secret: {
|
||||
config: {
|
||||
codes: codes.map((code) => ({ code })),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cy.requireStrictAal()
|
||||
cy.visit(express.login)
|
||||
|
||||
cy.get('input[name="identifier"]').type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.submitPasswordForm()
|
||||
|
||||
cy.shouldShow2FAScreen()
|
||||
|
||||
// Reject an invalid code first to make sure we are exercising the
|
||||
// lookup-secret branch and not silently bypassing it.
|
||||
cy.get('input[name="lookup_secret"]').type("not-a-real-code")
|
||||
cy.get('*[name="method"][value="lookup_secret"]').click()
|
||||
cy.get('[data-testid="ui/message/4000016"]').should(
|
||||
"contain.text",
|
||||
"The backup recovery code is not valid.",
|
||||
)
|
||||
|
||||
// The form clears between attempts; type one of the imported codes.
|
||||
cy.get('input[name="lookup_secret"]').should("have.value", "")
|
||||
cy.get('input[name="lookup_secret"]').type(codes[0])
|
||||
cy.get('*[name="method"][value="lookup_secret"]').click()
|
||||
|
||||
cy.location("pathname").should("not.contain", "/login")
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["password", "lookup_secret"],
|
||||
})
|
||||
})
|
||||
})
|
||||
157
test/e2e/cypress/integration/profiles/mfa/code.spec.ts
Normal file
157
test/e2e/cypress/integration/profiles/mfa/code.spec.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, gen, website } from "../../../helpers"
|
||||
import { routes as express } from "../../../helpers/express"
|
||||
|
||||
context("2FA code", () => {
|
||||
;[
|
||||
// {
|
||||
// login: react.login,
|
||||
// settings: react.settings,
|
||||
// base: react.base,
|
||||
// app: "react" as "react",
|
||||
// profile: "spa",
|
||||
// },
|
||||
{
|
||||
login: express.login,
|
||||
settings: express.settings,
|
||||
base: express.base,
|
||||
app: "express" as "express",
|
||||
profile: "mfa",
|
||||
},
|
||||
].forEach(({ settings, login, profile, app, base }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
let email: string
|
||||
let password: string
|
||||
|
||||
describe("when using highest_available aal", () => {
|
||||
beforeEach(() => {
|
||||
cy.useConfig((builder) =>
|
||||
builder
|
||||
.longPrivilegedSessionTime()
|
||||
.useHighestAvailable()
|
||||
.enableCodeMFA(),
|
||||
)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show second factor screen on whoami call", () => {
|
||||
email = gen.email()
|
||||
password = gen.password()
|
||||
cy.register({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
cy.deleteMail()
|
||||
|
||||
cy.visit(settings)
|
||||
cy.location("pathname").should("contain", "/login") // we get redirected to login
|
||||
|
||||
cy.getLoginCodeFromEmail(email).then((code) => {
|
||||
cy.get("input[name='code']").type(code)
|
||||
cy.contains("Continue").click()
|
||||
})
|
||||
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["password", "code"],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("when using aal1 required aal", () => {
|
||||
beforeEach(() => {
|
||||
email = gen.email()
|
||||
password = gen.password()
|
||||
cy.useConfig((builder) =>
|
||||
builder
|
||||
.longPrivilegedSessionTime()
|
||||
.useLaxAal()
|
||||
.enableCode()
|
||||
.enableCodeMFA(),
|
||||
)
|
||||
|
||||
cy.register({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
cy.deleteMail()
|
||||
cy.visit(login + "?aal=aal2&via=email")
|
||||
})
|
||||
|
||||
it("should be asked to sign in with 2fa if set up", () => {
|
||||
cy.get("input[name='code']").should("be.visible")
|
||||
cy.getLoginCodeFromEmail(email).then((code) => {
|
||||
cy.get("input[name='code']").type(code)
|
||||
cy.contains("Continue").click()
|
||||
})
|
||||
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["password", "code"],
|
||||
})
|
||||
})
|
||||
|
||||
it("can't use different email in 2fa request", () => {
|
||||
// Setting up another 2fa method to prevent fast-login to happen
|
||||
cy.visit(base)
|
||||
cy.clearAllCookies()
|
||||
cy.useConfig((builder) => builder.disableCodeMfa())
|
||||
cy.login({ email, password, cookieUrl: base })
|
||||
cy.longPrivilegedSessionTime()
|
||||
cy.visit(settings)
|
||||
cy.get(
|
||||
appPrefix(app) + 'button[name="lookup_secret_regenerate"]',
|
||||
).click()
|
||||
cy.get('button[name="lookup_secret_confirm"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
cy.visit(settings)
|
||||
cy.clearAllCookies()
|
||||
cy.useConfig((builder) => builder.enableCodeMFA())
|
||||
cy.login({ email: email, password: password, cookieUrl: base })
|
||||
|
||||
cy.visit(login + "?aal=aal2")
|
||||
cy.get('[name="method"][value="lookup_secret"]').should("exist")
|
||||
cy.get('[name="address"]').should("exist")
|
||||
|
||||
cy.get('[name="address"]').invoke("attr", "value", gen.email())
|
||||
|
||||
cy.get('[name="address"]').click()
|
||||
|
||||
cy.get("*[data-testid='ui/message/4000035']").should("be.visible")
|
||||
cy.get("input[name='code']").should("not.exist")
|
||||
cy.get("[name='address']").should("be.visible")
|
||||
|
||||
// The current session should be unchanged
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["password"],
|
||||
})
|
||||
})
|
||||
|
||||
it("entering wrong code should not invalidate correct codes", () => {
|
||||
cy.get("input[name='code']").should("be.visible").type("123456")
|
||||
|
||||
cy.contains("Continue").click()
|
||||
cy.getLoginCodeFromEmail(email).then((code) => {
|
||||
cy.get("input[name='code']").type(code)
|
||||
cy.contains("Continue").click()
|
||||
})
|
||||
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["password", "code"],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
213
test/e2e/cypress/integration/profiles/mfa/code_optional.spec.ts
Normal file
213
test/e2e/cypress/integration/profiles/mfa/code_optional.spec.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { gen } from "../../../helpers"
|
||||
import { routes as express } from "../../../helpers/express"
|
||||
|
||||
context("2FA code with optional field", () => {
|
||||
;[
|
||||
{
|
||||
login: express.login,
|
||||
settings: express.settings,
|
||||
base: express.base,
|
||||
app: "express" as "express",
|
||||
profile: "mfa-optional",
|
||||
},
|
||||
].forEach(({ settings, login, profile, app, base }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
describe("when using highest_available aal with empty optionalMfaEmail field", () => {
|
||||
let email: string
|
||||
let password: string
|
||||
|
||||
beforeEach(() => {
|
||||
email = gen.email()
|
||||
password = gen.password()
|
||||
|
||||
// Configure system to use highest available AAL
|
||||
cy.useConfig((builder) =>
|
||||
builder.longPrivilegedSessionTime().useHighestAvailable(),
|
||||
)
|
||||
|
||||
// Register a user without setting up code MFA (empty optionalMfaEmail field)
|
||||
cy.register({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.optionalMfaEmail": "" },
|
||||
})
|
||||
cy.deleteMail()
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should not show 2FA page during login when optionalMfaEmail field is empty", () => {
|
||||
// Log out first
|
||||
cy.clearAllCookies()
|
||||
|
||||
// Login with the user
|
||||
cy.visit(login)
|
||||
cy.get('input[name="identifier"]').type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.get('button[name="method"][value="password"]').click()
|
||||
|
||||
// Should be logged in directly without 2FA page
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["password"],
|
||||
})
|
||||
|
||||
// Verify we're not asked for 2FA when visiting settings
|
||||
cy.visit(settings)
|
||||
cy.location("pathname").should("contain", "/settings")
|
||||
cy.get('input[name="traits.email"]').should("contain.value", email)
|
||||
})
|
||||
})
|
||||
|
||||
describe("when using highest_available aal with configured optionalMfaEmail field", () => {
|
||||
let email: string
|
||||
let mfaEmail: string
|
||||
let password: string
|
||||
|
||||
beforeEach(() => {
|
||||
email = gen.email()
|
||||
mfaEmail = gen.email()
|
||||
password = gen.password()
|
||||
|
||||
// Configure system to use highest available AAL
|
||||
cy.useConfig((builder) =>
|
||||
builder.longPrivilegedSessionTime().useHighestAvailable(),
|
||||
)
|
||||
|
||||
// Register a user with optionalMfaEmail field set
|
||||
cy.register({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.optionalMfaEmail": mfaEmail },
|
||||
})
|
||||
cy.deleteMail()
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should show 2FA page during login when optionalMfaEmail field is configured", () => {
|
||||
// Log out first
|
||||
cy.clearAllCookies()
|
||||
|
||||
// Login with the user
|
||||
cy.visit(login)
|
||||
cy.get('input[name="identifier"]').type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.get('button[name="method"][value="password"]').click()
|
||||
|
||||
// Should see the code input field
|
||||
cy.location("pathname").should("contain", "/login")
|
||||
cy.get("input[name='code']").should("be.visible")
|
||||
|
||||
// Get the code from email and enter it
|
||||
cy.getLoginCodeFromEmail(mfaEmail).then((code) => {
|
||||
cy.get("input[name='code']").type(code)
|
||||
cy.contains("Continue").click()
|
||||
})
|
||||
|
||||
// Should be logged in with AAL2
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["password", "code"],
|
||||
})
|
||||
})
|
||||
|
||||
it("should require 2FA for settings flow when optionalMfaEmail field is configured", () => {
|
||||
// Log out first
|
||||
cy.clearAllCookies()
|
||||
|
||||
// Try to access settings directly
|
||||
cy.visit(settings)
|
||||
|
||||
// Should be redirected to login
|
||||
cy.location("pathname").should("contain", "/login")
|
||||
|
||||
// Login with password
|
||||
cy.get('input[name="identifier"]').type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.get('button[name="method"][value="password"]').click()
|
||||
|
||||
// Should see the code input field
|
||||
cy.location("pathname").should("contain", "/login")
|
||||
cy.get("input[name='code']").should("be.visible")
|
||||
|
||||
// Get the code from email and enter it
|
||||
cy.getLoginCodeFromEmail(mfaEmail).then((code) => {
|
||||
cy.get("input[name='code']").type(code)
|
||||
cy.contains("Continue").click()
|
||||
})
|
||||
|
||||
// Go the the settings page again
|
||||
cy.visit(settings)
|
||||
|
||||
// Should now be at settings with AAL2
|
||||
cy.location("pathname").should("contain", "/settings")
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["password", "code"],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("when user with configured optionalMfaEmail logs in with aal1 session", () => {
|
||||
let email: string
|
||||
let mfaEmail: string
|
||||
let password: string
|
||||
|
||||
beforeEach(() => {
|
||||
email = gen.email()
|
||||
mfaEmail = gen.email()
|
||||
password = gen.password()
|
||||
|
||||
// Configure system to allow aal1 login but still require highest_available for settings
|
||||
cy.useConfig((builder) =>
|
||||
builder
|
||||
.longPrivilegedSessionTime()
|
||||
.useLaxSessionAal()
|
||||
.useHighestSettingsFlowAal(),
|
||||
)
|
||||
|
||||
// Register a user with optionalMfaEmail field set
|
||||
cy.register({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.optionalMfaEmail": mfaEmail },
|
||||
})
|
||||
cy.deleteMail()
|
||||
})
|
||||
|
||||
it("should not allow access to settings page with aal1 session", () => {
|
||||
// Log out first
|
||||
cy.clearAllCookies()
|
||||
|
||||
// Login with just password (aal1)
|
||||
cy.visit(login)
|
||||
cy.get('input[name="identifier"]').type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.get('button[name="method"][value="password"]').click()
|
||||
|
||||
// Verify we have an aal1 session
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["password"],
|
||||
})
|
||||
|
||||
// Try to access settings page
|
||||
cy.visit(settings)
|
||||
|
||||
// Should be redirected to login for 2FA
|
||||
cy.location("pathname").should("contain", "/login")
|
||||
|
||||
// Verify we're being asked for 2FA code input
|
||||
cy.get("input[name='code']").should("be.visible")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
305
test/e2e/cypress/integration/profiles/mfa/lookup.spec.ts
Normal file
305
test/e2e/cypress/integration/profiles/mfa/lookup.spec.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, gen, website } from "../../../helpers"
|
||||
import { routes as express } from "../../../helpers/express"
|
||||
import { routes as react } from "../../../helpers/react"
|
||||
|
||||
context("2FA lookup secrets", () => {
|
||||
;[
|
||||
{
|
||||
login: react.login,
|
||||
settings: react.settings,
|
||||
base: react.base,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
{
|
||||
login: express.login,
|
||||
settings: express.settings,
|
||||
base: express.base,
|
||||
app: "express" as "express",
|
||||
profile: "mfa",
|
||||
},
|
||||
].forEach(({ settings, login, profile, app, base }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
let email = gen.email()
|
||||
let password = gen.password()
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit(base)
|
||||
cy.clearAllCookies()
|
||||
cy.useConfig((builder) => builder.disableCodeMfa())
|
||||
email = gen.email()
|
||||
password = gen.password()
|
||||
cy.registerApi({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
cy.login({ email, password, cookieUrl: base })
|
||||
|
||||
cy.longPrivilegedSessionTime()
|
||||
cy.sessionRequiresNo2fa()
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should be able to remove lookup codes", () => {
|
||||
cy.sessionRequires2fa()
|
||||
cy.longPrivilegedSessionTime()
|
||||
cy.visit(settings)
|
||||
cy.get(
|
||||
appPrefix(app) + 'button[name="lookup_secret_regenerate"]',
|
||||
).click()
|
||||
cy.get('button[name="lookup_secret_confirm"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
cy.visit(settings)
|
||||
|
||||
cy.shortPrivilegedSessionTime()
|
||||
cy.get('button[name="lookup_secret_disable"]').click()
|
||||
cy.reauth({
|
||||
expect: { email },
|
||||
type: { email: email, password: password },
|
||||
})
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
cy.clearAllCookies()
|
||||
cy.login({ email: email, password: password, cookieUrl: base })
|
||||
|
||||
cy.visit(login + "?aal=aal2")
|
||||
cy.get("h2")
|
||||
.invoke("text")
|
||||
.should(
|
||||
"match",
|
||||
/Second factor authentication|Two-Factor Authentication/,
|
||||
)
|
||||
cy.get('*[name="method"][value="totp"]').should("not.exist")
|
||||
cy.get('*[name="method"][value="lookup_secret"]').should("not.exist")
|
||||
cy.get('*[name="method"][value="password"]').should("not.exist")
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should go through several lookup secret lifecycles", () => {
|
||||
cy.visit(settings)
|
||||
|
||||
cy.get('[data-testid="node/text/lookup_secret_codes/label"]').should(
|
||||
"not.exist",
|
||||
)
|
||||
cy.get('[data-testid="text-lookup_secret_codes-content"] code').should(
|
||||
"not.exist",
|
||||
)
|
||||
cy.get('button[name="lookup_secret_confirm"]').should("not.exist")
|
||||
cy.get('button[name="lookup_secret_regenerate"]').click()
|
||||
cy.get('[data-testid="node/text/lookup_secret_codes/label"]').should(
|
||||
"contain.text",
|
||||
"These are your back up recovery codes.",
|
||||
)
|
||||
cy.get('[data-testid="text-lookup_secret_codes-content"] code').should(
|
||||
"not.be.empty",
|
||||
)
|
||||
|
||||
let codes: string[]
|
||||
cy.getLookupSecrets().should((c) => {
|
||||
codes = c
|
||||
})
|
||||
|
||||
cy.get('button[name="lookup_secret_confirm"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
cy.get('button[name="lookup_secret_reveal"]').should("exist")
|
||||
cy.get('[data-testid="text-lookup_secret_codes-content"] code').should(
|
||||
"not.exist",
|
||||
)
|
||||
cy.get('button[name="lookup_secret_confirm"]').should("not.exist")
|
||||
cy.get('button[name="lookup_secret_regenerate"]').should("not.exist")
|
||||
|
||||
cy.get('button[name="lookup_secret_reveal"]').click()
|
||||
cy.getLookupSecrets().should((c) => {
|
||||
codes = c
|
||||
})
|
||||
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["password", "lookup_secret"],
|
||||
})
|
||||
|
||||
// Try to log in with a recovery code now
|
||||
cy.visit(login + "?aal=aal2&refresh=true")
|
||||
cy.location("pathname").should("contain", "login")
|
||||
|
||||
cy.get('*[name="method"][value="lookup_secret"]').should("exist")
|
||||
cy.get('*[name="method"][value="password"]').should("not.exist")
|
||||
|
||||
// Type an invalid code
|
||||
cy.get('input[name="lookup_secret"]').should("exist")
|
||||
cy.get('input[name="lookup_secret"]').type("invalid-code")
|
||||
cy.get('*[name="method"][value="lookup_secret"]').click()
|
||||
cy.get('[data-testid="ui/message/4000016"]').should(
|
||||
"contain.text",
|
||||
"The backup recovery code is not valid.",
|
||||
)
|
||||
|
||||
// Type a valid code
|
||||
cy.get('input[name="lookup_secret"]').should("exist")
|
||||
cy.get('input[name="lookup_secret"]').should("have.value", "")
|
||||
cy.get('input[name="lookup_secret"]').then(($e) => {
|
||||
cy.wrap($e).type(codes[0])
|
||||
})
|
||||
cy.get('*[name="method"][value="lookup_secret"]').click()
|
||||
cy.location("pathname").should("not.contain", "login")
|
||||
|
||||
let authenticatedAt
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["password", "lookup_secret", "lookup_secret"],
|
||||
}).then((session) => {
|
||||
authenticatedAt = session.authenticated_at
|
||||
expect(session.authenticator_assurance_level).to.equal("aal2")
|
||||
})
|
||||
|
||||
// Retry auth with the used code
|
||||
cy.visit(login + "?aal=aal2&refresh=true")
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.href).to.include("/login")
|
||||
})
|
||||
cy.get('input[name="lookup_secret"]').then(($e) => {
|
||||
cy.wrap($e).type(codes[0])
|
||||
})
|
||||
cy.get('*[name="method"][value="lookup_secret"]').click()
|
||||
// Use a valid code
|
||||
cy.get('[data-testid="ui/message/4000012"]').should(
|
||||
"contain.text",
|
||||
"This backup recovery code has already been used.",
|
||||
)
|
||||
|
||||
cy.get('input[name="lookup_secret"]').then(($e) => {
|
||||
cy.wrap($e).type(codes[1])
|
||||
})
|
||||
cy.get('*[name="method"][value="lookup_secret"]').click()
|
||||
cy.location("pathname").should("not.contain", "login")
|
||||
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: [
|
||||
"password",
|
||||
"lookup_secret",
|
||||
"lookup_secret",
|
||||
"lookup_secret",
|
||||
],
|
||||
}).then((session) => {
|
||||
expect(session.authenticated_at).to.not.equal(authenticatedAt)
|
||||
})
|
||||
|
||||
// Going back to the settings UI we should see that the codes have been "used"
|
||||
cy.visit(settings)
|
||||
cy.get('button[name="lookup_secret_reveal"]').click()
|
||||
cy.getLookupSecrets().should((c) => {
|
||||
expect(c.slice(2)).to.eql(codes.slice(2))
|
||||
expect(c[0]).to.match(/(Secret was used at )|(Used)/g)
|
||||
expect(c[1]).to.match(/(Secret was used at )|(Used)/g)
|
||||
})
|
||||
|
||||
// Regenerating the codes means the old one become invalid
|
||||
cy.get("*[name=lookup_secret_regenerate]").click()
|
||||
cy.get("*[name=lookup_secret_confirm]").should("exist")
|
||||
let regenCodes
|
||||
cy.getLookupSecrets().should((c) => {
|
||||
regenCodes = c
|
||||
})
|
||||
|
||||
// Confirm it
|
||||
cy.get("*[name=lookup_secret_confirm]").click()
|
||||
cy.get('*[name="lookup_secret_reveal"]').click()
|
||||
cy.getLookupSecrets().should((c) => {
|
||||
expect(c).to.eql(regenCodes)
|
||||
})
|
||||
|
||||
// Log in and see if we can use the old / new keys
|
||||
cy.visit(login + "?aal=aal2&refresh=true")
|
||||
cy.location("pathname").should("contain", "login")
|
||||
|
||||
// Using an old code fails
|
||||
cy.get('input[name="lookup_secret"]').then(($e) => {
|
||||
cy.wrap($e).type(codes[3])
|
||||
})
|
||||
cy.get('*[name="method"][value="lookup_secret"]').click()
|
||||
cy.get('[data-testid="ui/message/4000016"]').should("exist")
|
||||
|
||||
// Using a new code succeeds
|
||||
cy.get('input[name="lookup_secret"]').then(($e) => {
|
||||
cy.wrap($e).type(regenCodes[0])
|
||||
})
|
||||
cy.get('*[name="method"][value="lookup_secret"]').click()
|
||||
cy.location("pathname").should("not.contain", "login")
|
||||
|
||||
// Going back to the settings UI we should see that the codes have been "used"
|
||||
cy.visit(settings)
|
||||
cy.get('button[name="lookup_secret_reveal"]').click()
|
||||
cy.getLookupSecrets().should((c) => {
|
||||
expect(c.slice(1)).to.eql(regenCodes.slice(1))
|
||||
expect(c[0]).to.match(/(Secret was used at )|(Used)/g)
|
||||
})
|
||||
})
|
||||
|
||||
it("should end up at login screen if trying to reveal without privileged session", () => {
|
||||
cy.shortPrivilegedSessionTime()
|
||||
cy.visit(settings)
|
||||
cy.get('button[name="lookup_secret_regenerate"]').click()
|
||||
cy.reauth({
|
||||
expect: { email },
|
||||
type: { email: email, password: password },
|
||||
})
|
||||
|
||||
let codes
|
||||
cy.getLookupSecrets().should((c) => {
|
||||
codes = c
|
||||
})
|
||||
|
||||
cy.shortPrivilegedSessionTime()
|
||||
cy.get('button[name="lookup_secret_confirm"]').click()
|
||||
cy.reauth({
|
||||
expect: { email },
|
||||
type: { email: email, password: password },
|
||||
})
|
||||
if (app === "react") {
|
||||
cy.get('button[value="profile"]').click()
|
||||
}
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
if (app !== "react") {
|
||||
cy.shortPrivilegedSessionTime()
|
||||
cy.get('button[name="lookup_secret_reveal"]').click()
|
||||
cy.reauth({
|
||||
expect: { email },
|
||||
type: { email: email, password: password },
|
||||
})
|
||||
cy.getLookupSecrets().should((c) => {
|
||||
expect(c).to.not.be.empty
|
||||
})
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should not show lookup as an option if not configured", () => {
|
||||
cy.visit(login + "?aal=aal2")
|
||||
cy.get('*[name="method"][value="totp"]').should("not.exist")
|
||||
cy.get('*[name="method"][value="lookup_secret"]').should("not.exist")
|
||||
cy.get('*[name="method"][value="password"]').should("not.exist")
|
||||
cy.get("h2")
|
||||
.invoke("text")
|
||||
.should(
|
||||
"match",
|
||||
/Second factor authentication|Two-Factor Authentication/,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
188
test/e2e/cypress/integration/profiles/mfa/mix.spec.ts
Normal file
188
test/e2e/cypress/integration/profiles/mfa/mix.spec.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, gen, website } from "../../../helpers"
|
||||
import { routes as react } from "../../../helpers/react"
|
||||
import { routes as express } from "../../../helpers/express"
|
||||
import { TOTP } from "otpauth"
|
||||
|
||||
context("2FA with various methods", () => {
|
||||
beforeEach(() => {
|
||||
cy.task("resetCRI", {})
|
||||
})
|
||||
;[
|
||||
{
|
||||
login: react.login,
|
||||
settings: react.settings,
|
||||
base: react.base,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
{
|
||||
login: express.login,
|
||||
settings: express.settings,
|
||||
base: express.base,
|
||||
app: "express" as "express",
|
||||
profile: "mfa",
|
||||
},
|
||||
].forEach(({ settings, login, profile, app, base }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
let email = gen.email()
|
||||
let password = gen.password()
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
email = gen.email()
|
||||
password = gen.password()
|
||||
cy.registerApi({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
cy.clearAllCookies()
|
||||
cy.login({ email, password, cookieUrl: base })
|
||||
cy.longPrivilegedSessionTime()
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.disable",
|
||||
opts: {},
|
||||
})
|
||||
})
|
||||
|
||||
it("should set up an use all mfa combinations", () => {
|
||||
cy.visit(settings)
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.enable",
|
||||
opts: {},
|
||||
}).then(() => {
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.addVirtualAuthenticator",
|
||||
opts: {
|
||||
options: {
|
||||
protocol: "ctap2",
|
||||
transport: "usb",
|
||||
hasResidentKey: true,
|
||||
hasUserVerification: true,
|
||||
isUserVerified: true,
|
||||
},
|
||||
},
|
||||
}).then(() => {
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["password"],
|
||||
})
|
||||
|
||||
cy.visit(settings)
|
||||
// Set up TOTP
|
||||
let secret: string
|
||||
cy.get(
|
||||
appPrefix(app) + '[data-testid="node/text/totp_secret_key/text"]',
|
||||
).then(($e) => {
|
||||
secret = $e.text().trim()
|
||||
})
|
||||
cy.get('[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"],
|
||||
})
|
||||
|
||||
// Set up lookup secrets
|
||||
cy.visit(settings)
|
||||
cy.get('[name="lookup_secret_regenerate"]').click()
|
||||
let codes: string[]
|
||||
cy.getLookupSecrets().then((c) => {
|
||||
codes = c
|
||||
})
|
||||
cy.get('[name="lookup_secret_confirm"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["password", "totp", "lookup_secret"],
|
||||
})
|
||||
|
||||
// Set up WebAuthn
|
||||
cy.visit(settings)
|
||||
cy.get('[name="webauthn_register_displayname"]').type("my-key")
|
||||
// We need a workaround here. So first we click, then we submit
|
||||
cy.clickWebAuthButton("register")
|
||||
cy.expectSettingsSaved()
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["password", "totp", "webauthn", "lookup_secret"],
|
||||
})
|
||||
|
||||
cy.visit(login + "?aal=aal2&refresh=true")
|
||||
cy.get('[name="totp_code"]').then(($e) => {
|
||||
cy.wrap($e).type(
|
||||
new TOTP({
|
||||
secret,
|
||||
}).generate(),
|
||||
)
|
||||
})
|
||||
|
||||
cy.get('[name="method"][value="totp"]').click()
|
||||
cy.location("pathname").should("not.include", "/login")
|
||||
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: [
|
||||
"password",
|
||||
"totp",
|
||||
"webauthn",
|
||||
"lookup_secret",
|
||||
"totp",
|
||||
],
|
||||
})
|
||||
|
||||
// Use TOTP
|
||||
cy.visit(login + "?aal=aal2&refresh=true")
|
||||
cy.clickWebAuthButton("login")
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: [
|
||||
"password",
|
||||
"totp",
|
||||
"webauthn",
|
||||
"lookup_secret",
|
||||
"totp",
|
||||
"webauthn",
|
||||
],
|
||||
})
|
||||
|
||||
// Use lookup
|
||||
cy.visit(login + "?aal=aal2&refresh=true")
|
||||
cy.get('[name="lookup_secret"]').then(($e) => {
|
||||
cy.wrap($e).type(codes[1])
|
||||
})
|
||||
cy.get('[name="method"][value="lookup_secret"]').click()
|
||||
cy.location("pathname").should("not.include", "/login")
|
||||
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: [
|
||||
"password",
|
||||
"totp",
|
||||
"webauthn",
|
||||
"lookup_secret",
|
||||
"totp",
|
||||
"webauthn",
|
||||
"lookup_secret",
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
68
test/e2e/cypress/integration/profiles/mfa/settings.spec.ts
Normal file
68
test/e2e/cypress/integration/profiles/mfa/settings.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, gen, website } from "../../../helpers"
|
||||
import { routes as express } from "../../../helpers/express"
|
||||
import { routes as react } from "../../../helpers/react"
|
||||
|
||||
context("2FA UI settings tests", () => {
|
||||
;[
|
||||
{
|
||||
settings: react.settings,
|
||||
base: react.base,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
{
|
||||
settings: express.settings,
|
||||
base: express.base,
|
||||
app: "express" as "express",
|
||||
profile: "mfa",
|
||||
},
|
||||
].forEach(({ settings, profile, base, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
before(() => {
|
||||
cy.registerApi({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
cy.useConfig((builder) => builder.disableCodeMfa())
|
||||
cy.login({ email, password, cookieUrl: base })
|
||||
cy.visit(settings)
|
||||
})
|
||||
|
||||
it("shows all settings forms", () => {
|
||||
cy.get(appPrefix(app) + "h3").should("contain.text", "Profile Settings")
|
||||
cy.get("h3").should("contain.text", "Change Password")
|
||||
cy.get("h3").should("contain.text", "Manage 2FA Backup Recovery Codes")
|
||||
cy.get("h3").should("contain.text", "Manage 2FA TOTP Authenticator App")
|
||||
cy.get("h3").should("contain.text", "Manage Hardware Tokens")
|
||||
cy.get('input[name="traits.email"]').should("contain.value", email)
|
||||
cy.get('input[name="traits.website"]').should("contain.value", website)
|
||||
|
||||
cy.get('[data-testid="node/text/totp_secret_key/label"]').should(
|
||||
"contain.text",
|
||||
"This is your authenticator app secret",
|
||||
)
|
||||
cy.get("button").should(
|
||||
"contain.text",
|
||||
"Generate new backup recovery codes",
|
||||
)
|
||||
cy.get("button").should("contain.text", "Add security key")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
364
test/e2e/cypress/integration/profiles/mfa/totp.spec.ts
Normal file
364
test/e2e/cypress/integration/profiles/mfa/totp.spec.ts
Normal file
@@ -0,0 +1,364 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { gen, website } from "../../../helpers"
|
||||
import { routes as express } from "../../../helpers/express"
|
||||
import { routes as react } from "../../../helpers/react"
|
||||
import { TOTP } from "otpauth"
|
||||
|
||||
context("2FA TOTP", () => {
|
||||
;[
|
||||
{
|
||||
login: react.login,
|
||||
settings: react.settings,
|
||||
base: react.base,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
{
|
||||
login: express.login,
|
||||
settings: express.settings,
|
||||
base: express.base,
|
||||
app: "express" as "express",
|
||||
profile: "mfa",
|
||||
},
|
||||
].forEach(({ settings, login, profile, app, base }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
let email = gen.email()
|
||||
let password = gen.password()
|
||||
|
||||
beforeEach(() => {
|
||||
cy.useConfig((builder) =>
|
||||
builder.longPrivilegedSessionTime().useLaxAal().disableCodeMfa(),
|
||||
)
|
||||
email = gen.email()
|
||||
password = gen.password()
|
||||
|
||||
cy.register({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should be be asked to sign in with 2fa if set up", () => {
|
||||
cy.visit(settings)
|
||||
cy.requireStrictAal()
|
||||
|
||||
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.clearAllCookies()
|
||||
cy.visit(login)
|
||||
|
||||
cy.get('input[name="identifier"]').type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.submitPasswordForm()
|
||||
|
||||
// MFA is now requested
|
||||
cy.shouldShow2FAScreen()
|
||||
|
||||
// If we visit settings page we still end up at 2fa screen
|
||||
cy.visit(settings)
|
||||
cy.location("pathname").should((loc) => {
|
||||
expect(loc).to.include("/login")
|
||||
})
|
||||
|
||||
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()
|
||||
cy.location("pathname").should((loc) => {
|
||||
expect(loc).to.oneOf(["/welcome", "/", "/sessions", "/settings"])
|
||||
})
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["password", "totp"],
|
||||
})
|
||||
})
|
||||
|
||||
it("signin with 2fa and be redirected", () => {
|
||||
if (app !== "express") {
|
||||
return
|
||||
}
|
||||
|
||||
cy.visit(settings)
|
||||
cy.requireStrictAal()
|
||||
|
||||
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.clearAllCookies()
|
||||
cy.visit(`${login}?return_to=https://www.example.org/`)
|
||||
|
||||
cy.get('input[name="identifier"]').type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.submitPasswordForm()
|
||||
|
||||
// MFA is now requested
|
||||
cy.location("pathname").should((loc) => {
|
||||
expect(loc).to.include("/login")
|
||||
})
|
||||
cy.shouldShow2FAScreen()
|
||||
|
||||
cy.location("pathname").should((loc) => {
|
||||
expect(loc).to.include("/login")
|
||||
})
|
||||
|
||||
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()
|
||||
cy.url().should("eq", "https://www.example.org/")
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should go through several totp lifecycles", () => {
|
||||
cy.visit(settings)
|
||||
|
||||
cy.get('[data-testid="node/text/totp_secret_key/text"]').should("exist")
|
||||
cy.get('img[data-testid="node/image/totp_qr"]').should("exist")
|
||||
|
||||
// Set up TOTP
|
||||
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.get('[data-testid="node/text/totp_secret_key/text"]').should(
|
||||
"not.exist",
|
||||
)
|
||||
cy.get('img[data-testid="node/image/totp_qr"]').should("not.exist")
|
||||
cy.get('*[name="method"][value="totp"]').should("not.exist")
|
||||
cy.get('*[name="totp_unlink"]').should("exist")
|
||||
|
||||
// Let's try to do 2FA
|
||||
cy.visit(login + "?aal=aal2&refresh=true")
|
||||
cy.location("pathname").should((loc) => {
|
||||
expect(loc).to.include("/login")
|
||||
})
|
||||
cy.get('*[name="method"][value="password"]').should("not.exist")
|
||||
|
||||
// Typing a wrong code leaves us with an error message
|
||||
cy.get('*[name="totp_code"]').type("111111")
|
||||
cy.get('*[name="method"][value="totp"]').click()
|
||||
|
||||
cy.get('[data-testid="ui/message/4000008"]').should(
|
||||
"contain.text",
|
||||
"The provided authentication code is invalid, please try again.",
|
||||
)
|
||||
cy.get('input[name="totp_code"]').then(($e) => {
|
||||
cy.wrap($e).type(
|
||||
new TOTP({
|
||||
secret,
|
||||
}).generate(),
|
||||
)
|
||||
})
|
||||
cy.get('*[name="method"][value="totp"]').click()
|
||||
cy.location("pathname").should("not.contain", "/login")
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["password", "totp", "totp"],
|
||||
})
|
||||
|
||||
// Going to settings and unlinking the device
|
||||
cy.visit(settings)
|
||||
cy.get('*[name="totp_unlink"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
cy.get('[data-testid="node/text/totp_secret_key/text"]').should("exist")
|
||||
cy.get('img[data-testid="node/image/totp_qr"]').should("exist")
|
||||
cy.get('*[name="method"][value="totp"]').should("exist")
|
||||
cy.get('*[name="totp_unlink"]').should("not.exist")
|
||||
|
||||
// 2FA should be gone
|
||||
cy.visit(login + "?aal=aal2&refresh=true")
|
||||
cy.location("pathname").should((loc) => {
|
||||
expect(loc).to.include("/login")
|
||||
})
|
||||
cy.get('*[name="method"][value="totp"]').should("not.exist")
|
||||
|
||||
// Linking a new device works
|
||||
cy.visit(settings)
|
||||
let newSecret: string
|
||||
cy.get('[data-testid="node/text/totp_secret_key/text"]').then(($e) => {
|
||||
newSecret = $e.text().trim()
|
||||
})
|
||||
cy.get('input[name="totp_code"]').then(($e) => {
|
||||
cy.wrap($e).type(
|
||||
new TOTP({
|
||||
secret: newSecret,
|
||||
}).generate(),
|
||||
)
|
||||
})
|
||||
cy.get('*[name="method"][value="totp"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
// Old secret no longer works in login
|
||||
cy.visit(login + "?aal=aal2&refresh=true")
|
||||
cy.location("pathname").should((loc) => {
|
||||
expect(loc).to.include("/login")
|
||||
})
|
||||
cy.get('input[name="totp_code"]').then(($e) => {
|
||||
cy.wrap($e).type(
|
||||
new TOTP({
|
||||
secret,
|
||||
}).generate(),
|
||||
)
|
||||
})
|
||||
cy.get('*[name="method"][value="totp"]').click()
|
||||
cy.get('[data-testid="ui/message/4000008"]').should(
|
||||
"contain.text",
|
||||
"The provided authentication code is invalid, please try again.",
|
||||
)
|
||||
|
||||
// But new one does!
|
||||
cy.get('input[name="totp_code"]').then(($e) => {
|
||||
cy.wrap($e).type(
|
||||
new TOTP({
|
||||
secret: newSecret,
|
||||
}).generate(),
|
||||
)
|
||||
})
|
||||
cy.get('*[name="method"][value="totp"]').click()
|
||||
cy.location("pathname").should((loc) => {
|
||||
expect(loc).to.not.include("/login")
|
||||
})
|
||||
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["password", "totp", "totp", "totp", "totp"],
|
||||
})
|
||||
|
||||
// The React app keeps using the same flow. The following scenario used to be broken,
|
||||
// because the internal context wasn't populated properly in the flow after settings were saved.
|
||||
cy.visit(settings)
|
||||
cy.get('*[name="totp_unlink"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should not show totp as an option if not configured", () => {
|
||||
cy.visit(login + "?aal=aal2")
|
||||
cy.location("pathname").should((loc) => {
|
||||
expect(loc).to.include("/login")
|
||||
})
|
||||
|
||||
cy.get('*[name="method"][value="totp"]').should("not.exist")
|
||||
cy.get('*[name="method"][value="password"]').should("not.exist")
|
||||
cy.shouldShow2FAScreen()
|
||||
|
||||
cy.get('[data-testid="logout-link"]').click()
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.href).to.include("/login")
|
||||
expect(loc.search).to.not.include("aal")
|
||||
expect(loc.search).to.not.include("refresh")
|
||||
})
|
||||
cy.noSession()
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should fail to set up totp if verify code is wrong", () => {
|
||||
cy.visit(settings)
|
||||
cy.get('input[name="totp_code"]').type("12345678")
|
||||
cy.get('*[name="method"][value="totp"]').click()
|
||||
cy.get('[data-testid="ui/message/4000008"]').should(
|
||||
"contain.text",
|
||||
"The provided authentication code is invalid, please try again.",
|
||||
)
|
||||
})
|
||||
|
||||
// The React app keeps using the same flow. The following scenario used to be broken,
|
||||
// because the internal context wasn't populated properly in the flow after settings were saved.
|
||||
// playwright:migrated
|
||||
it("should allow changing other settings and then setting up totp", () => {
|
||||
cy.visit(settings)
|
||||
cy.get('input[name="traits.website"]')
|
||||
.clear()
|
||||
.type("https://some-website.com")
|
||||
cy.get('*[name="method"][value="profile"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
331
test/e2e/cypress/integration/profiles/mfa/webauthn.spec.ts
Normal file
331
test/e2e/cypress/integration/profiles/mfa/webauthn.spec.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, gen, website } from "../../../helpers"
|
||||
import { routes as react } from "../../../helpers/react"
|
||||
import { routes as express } from "../../../helpers/express"
|
||||
|
||||
context("2FA WebAuthn", () => {
|
||||
beforeEach(() => {
|
||||
cy.task("resetCRI", {})
|
||||
})
|
||||
;[
|
||||
{
|
||||
login: express.login,
|
||||
settings: express.settings,
|
||||
base: express.base,
|
||||
app: "express" as "express",
|
||||
profile: "mfa",
|
||||
},
|
||||
{
|
||||
login: react.login,
|
||||
settings: react.settings,
|
||||
base: react.base,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
].forEach(({ settings, login, profile, app, base }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
let email = gen.email()
|
||||
let password = gen.password()
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
|
||||
cy.useConfig((builder) => builder.disableCodeMfa())
|
||||
email = gen.email()
|
||||
password = gen.password()
|
||||
cy.registerApi({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
|
||||
cy.login({ email, password })
|
||||
|
||||
cy.longPrivilegedSessionTime()
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.disable",
|
||||
opts: {},
|
||||
})
|
||||
})
|
||||
|
||||
it("should be able to identify if the authenticator is wrong", () => {
|
||||
cy.visit(settings)
|
||||
|
||||
// Set up virtual authenticator
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.enable",
|
||||
opts: {},
|
||||
}).then(() => {
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.addVirtualAuthenticator",
|
||||
opts: {
|
||||
options: {
|
||||
protocol: "ctap2",
|
||||
transport: "usb",
|
||||
hasResidentKey: true,
|
||||
hasUserVerification: true,
|
||||
isUserVerified: true,
|
||||
},
|
||||
},
|
||||
}).then((addResult) => {
|
||||
cy.get(
|
||||
appPrefix(app) + '[name="webauthn_register_displayname"]',
|
||||
).type("key1")
|
||||
|
||||
cy.clickWebAuthButton("register")
|
||||
|
||||
cy.get('*[name="webauthn_remove"]').should("have.length", 1)
|
||||
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.removeVirtualAuthenticator",
|
||||
opts: addResult,
|
||||
}).then(() => {
|
||||
cy.visit(login + "?aal=aal2&refresh=true")
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.href).to.include("/login")
|
||||
})
|
||||
cy.clickWebAuthButton("login")
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.href).to.include("/login")
|
||||
})
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["password", "webauthn"],
|
||||
})
|
||||
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.addVirtualAuthenticator",
|
||||
opts: {
|
||||
options: {
|
||||
protocol: "ctap2",
|
||||
transport: "usb",
|
||||
hasResidentKey: true,
|
||||
hasUserVerification: true,
|
||||
isUserVerified: true,
|
||||
},
|
||||
},
|
||||
}).then((addResult) => {
|
||||
cy.visit(login + "?aal=aal2&refresh=true")
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.href).to.include("/login")
|
||||
})
|
||||
cy.clickWebAuthButton("login")
|
||||
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.href).to.include("/login")
|
||||
})
|
||||
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["password", "webauthn"],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should be able to link multiple authenticators", () => {
|
||||
cy.visit(settings)
|
||||
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.enable",
|
||||
opts: {},
|
||||
}).then(() => {
|
||||
// Register the first credential with the first virtual authenticator.
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.addVirtualAuthenticator",
|
||||
opts: {
|
||||
options: {
|
||||
protocol: "ctap2",
|
||||
transport: "usb",
|
||||
hasResidentKey: true,
|
||||
hasUserVerification: true,
|
||||
isUserVerified: true,
|
||||
},
|
||||
},
|
||||
}).then((firstAuthenticator) => {
|
||||
cy.get('*[name="webauthn_register_displayname"]').type("key1")
|
||||
cy.clickWebAuthButton("register")
|
||||
cy.get('*[name="webauthn_remove"]').should("have.length", 1)
|
||||
|
||||
// Swap in a second authenticator for the next credential. The
|
||||
// settings flow lists the first credential in excludeCredentials;
|
||||
// with both authenticators attached, create() would race the
|
||||
// excluded authenticator's InvalidStateError against the new one, so
|
||||
// remove the first before registering on the second. The identity
|
||||
// keeps both credentials server-side either way.
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.removeVirtualAuthenticator",
|
||||
opts: firstAuthenticator,
|
||||
})
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.addVirtualAuthenticator",
|
||||
opts: {
|
||||
options: {
|
||||
protocol: "ctap2",
|
||||
transport: "usb",
|
||||
hasResidentKey: true,
|
||||
hasUserVerification: true,
|
||||
isUserVerified: true,
|
||||
},
|
||||
},
|
||||
}).then(() => {
|
||||
cy.get('*[name="webauthn_register_displayname"]').type("key2")
|
||||
cy.clickWebAuthButton("register")
|
||||
|
||||
cy.get('*[name="webauthn_remove"]').should("have.length", 2)
|
||||
|
||||
cy.visit(login + "?aal=aal2&refresh=true")
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.href).to.include("/login")
|
||||
})
|
||||
cy.get('*[name="webauthn_login_trigger"]').should(
|
||||
"have.length",
|
||||
1,
|
||||
)
|
||||
cy.clickWebAuthButton("login")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should not register the same authenticator twice", () => {
|
||||
cy.visit(settings)
|
||||
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.enable",
|
||||
opts: {},
|
||||
}).then(() => {
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.addVirtualAuthenticator",
|
||||
opts: {
|
||||
options: {
|
||||
protocol: "ctap2",
|
||||
transport: "usb",
|
||||
hasResidentKey: true,
|
||||
hasUserVerification: true,
|
||||
isUserVerified: true,
|
||||
},
|
||||
},
|
||||
}).then(() => {
|
||||
cy.get('*[name="webauthn_register_displayname"]').type("key1")
|
||||
cy.clickWebAuthButton("register")
|
||||
cy.get('*[name="webauthn_remove"]').should("have.length", 1)
|
||||
|
||||
// Registering again with the same authenticator must be rejected:
|
||||
// the settings flow lists the existing credential in
|
||||
// excludeCredentials, so the browser refuses to create a duplicate
|
||||
// and the credential count stays at one.
|
||||
cy.get('*[name="webauthn_register_displayname"]').type("key2")
|
||||
cy.clickWebAuthButton("register")
|
||||
|
||||
cy.get('*[name="webauthn_remove"]').should("have.length", 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should be not be able to link provider if webauth is not enabled", () => {
|
||||
cy.visit(settings)
|
||||
cy.get('*[name="webauthn_register_displayname"]').type("my-key")
|
||||
cy.clickWebAuthButton("register")
|
||||
cy.get('*[name="webauthn_remove"]').should("not.exist")
|
||||
})
|
||||
|
||||
it("should be able to link a webauthn provider", () => {
|
||||
cy.visit(settings)
|
||||
|
||||
// Set up virtual authenticator
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.enable",
|
||||
opts: {},
|
||||
}).then(() => {
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.addVirtualAuthenticator",
|
||||
opts: {
|
||||
options: {
|
||||
protocol: "ctap2",
|
||||
transport: "usb",
|
||||
hasResidentKey: true,
|
||||
hasUserVerification: true,
|
||||
isUserVerified: true,
|
||||
},
|
||||
},
|
||||
}).then((addResult) => {
|
||||
// Signing up without a display name causes an error
|
||||
cy.get('*[name="webauthn_remove"]').should("not.exist")
|
||||
|
||||
cy.clickWebAuthButton("register")
|
||||
|
||||
cy.get('[data-testid="ui/message/4000002"]').should(
|
||||
"contain.text",
|
||||
"Property webauthn_register_displayname is missing.",
|
||||
)
|
||||
|
||||
// Setting up with key works
|
||||
cy.get('*[name="webauthn_register_displayname"]').type("my-key")
|
||||
|
||||
// We need a workaround here. So first we click, then we submit
|
||||
cy.clickWebAuthButton("register")
|
||||
|
||||
cy.expectSettingsSaved()
|
||||
cy.get('*[name="webauthn_remove"]').should("exist")
|
||||
|
||||
// Login without refresh
|
||||
cy.login({ email, password })
|
||||
cy.visit(login + "?aal=aal2")
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.href).to.include("/login")
|
||||
})
|
||||
|
||||
cy.get('*[name="webauthn_login_trigger"]').should("have.length", 1)
|
||||
cy.clickWebAuthButton("login")
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.href).to.not.include("/login")
|
||||
})
|
||||
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["password", "webauthn"],
|
||||
})
|
||||
|
||||
// Login with refresh
|
||||
cy.visit(login + "?aal=aal2&refresh=true")
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.href).to.include("/login")
|
||||
})
|
||||
|
||||
cy.get('*[name="webauthn_login_trigger"]').should("have.length", 1)
|
||||
cy.clickWebAuthButton("login")
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.href).to.not.include("/login")
|
||||
})
|
||||
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["password", "webauthn", "webauthn"],
|
||||
})
|
||||
cy.visit(settings)
|
||||
cy.get('*[name="webauthn_remove"]').click()
|
||||
cy.get('*[name="webauthn_remove"]').should("not.exist")
|
||||
|
||||
cy.visit(login + "?aal=aal2&refresh=true")
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.href).to.include("/login")
|
||||
})
|
||||
|
||||
cy.get('button[name="webauthn_login_trigger"]').should("not.exist")
|
||||
cy.get('[data-testid="ui/message/1010003"]').should("exist")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { gen, MOBILE_URL } from "../../../../helpers"
|
||||
|
||||
context("Mobile Profile", () => {
|
||||
describe("Login Flow Errors", () => {
|
||||
before(() => {
|
||||
cy.clearAllCookies()
|
||||
cy.useConfigProfile("mobile")
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit(MOBILE_URL + "/Login")
|
||||
})
|
||||
|
||||
describe("shows validation errors when invalid signup data is used", () => {
|
||||
it("should show an error when the identifier is missing", () => {
|
||||
cy.get('input[data-testid="password"]').type(gen.password())
|
||||
|
||||
cy.get(
|
||||
'div[data-testid="field/method/password"] div[data-testid="submit-form"]',
|
||||
).click()
|
||||
|
||||
cy.get('*[data-testid="field/identifier"]').should(
|
||||
"contain.text",
|
||||
"Property identifier is missing.",
|
||||
)
|
||||
|
||||
cy.get('*[data-testid="field/password"]').should(
|
||||
"not.contain.text",
|
||||
"Property password is missing.",
|
||||
)
|
||||
})
|
||||
|
||||
it("should show an error when the password is missing", () => {
|
||||
const email = gen.email()
|
||||
cy.get('input[data-testid="identifier"]')
|
||||
.type(email)
|
||||
.should("have.value", email)
|
||||
|
||||
cy.get(
|
||||
'div[data-testid="field/method/password"] div[data-testid="submit-form"]',
|
||||
).click()
|
||||
|
||||
cy.get('*[data-testid="field/password"]').should(
|
||||
"contain.text",
|
||||
"Property password is missing.",
|
||||
)
|
||||
})
|
||||
|
||||
it("should show fail to sign in", () => {
|
||||
cy.get('input[data-testid="identifier"]').type(gen.email())
|
||||
cy.get('input[data-testid="password"]').type(gen.password())
|
||||
cy.get(
|
||||
'*[data-testid="field/method/password"] *[data-testid="submit-form"]',
|
||||
).click()
|
||||
cy.get('*[data-testid="form-messages"]').should(
|
||||
"contain.text",
|
||||
"credentials are invalid",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { gen, MOBILE_URL, website } from "../../../../helpers"
|
||||
|
||||
context("Mobile Profile", () => {
|
||||
describe("Login Flow Success", () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile("mobile")
|
||||
})
|
||||
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
before(() => {
|
||||
cy.registerApi({ email, password, fields: { "traits.website": website } })
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
cy.visit(MOBILE_URL + "/Login")
|
||||
})
|
||||
|
||||
it("should sign up and be logged in", () => {
|
||||
cy.get('input[data-testid="identifier"]').type(email)
|
||||
cy.get('input[data-testid="password"]').type(password)
|
||||
cy.get(
|
||||
'div[data-testid="field/method/password"] div[data-testid="submit-form"]',
|
||||
).click()
|
||||
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
})
|
||||
})
|
||||
})
|
||||
123
test/e2e/cypress/integration/profiles/mobile/mfa/backup.spec.ts
Normal file
123
test/e2e/cypress/integration/profiles/mobile/mfa/backup.spec.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { gen, MOBILE_URL, website } from "../../../../helpers"
|
||||
|
||||
context("Mobile Profile", () => {
|
||||
describe("TOTP 2FA Flow", () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile("mobile")
|
||||
})
|
||||
|
||||
describe("password", () => {
|
||||
let email = gen.email()
|
||||
let password = gen.password()
|
||||
|
||||
before(() => {
|
||||
cy.clearAllCookies()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
email = gen.email()
|
||||
password = gen.password()
|
||||
|
||||
cy.registerApi({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
|
||||
cy.loginMobile({ email, password })
|
||||
cy.visit(MOBILE_URL + "/Settings")
|
||||
})
|
||||
|
||||
it("should be able to lifecycle through lookup_secret flows", () => {
|
||||
cy.get('[data-testid="field/lookup_secret_codes"]').should("not.exist")
|
||||
cy.get('[data-testid="field/lookup_secret_confirm/true"]').should(
|
||||
"not.exist",
|
||||
)
|
||||
cy.get('[data-testid="field/lookup_secret_reveal/true"]').should(
|
||||
"not.exist",
|
||||
)
|
||||
cy.get('[data-testid="field/lookup_secret_regenerate/true"]').click()
|
||||
|
||||
cy.get('[data-testid="field/lookup_secret_reveal/true"]').should(
|
||||
"not.exist",
|
||||
)
|
||||
cy.get('[data-testid="field/lookup_secret_codes"]').should("exist")
|
||||
let codes
|
||||
cy.get('[data-testid="field/lookup_secret_codes/text"]').then(($e) => {
|
||||
codes = $e.text().trim().split(", ")
|
||||
})
|
||||
cy.get('[data-testid="field/lookup_secret_confirm/true"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
cy.get('[data-testid="field/lookup_secret_confirm/true"]').should(
|
||||
"not.exist",
|
||||
)
|
||||
cy.get('[data-testid="field/lookup_secret_regenerate/true"]').should(
|
||||
"not.exist",
|
||||
)
|
||||
cy.get('[data-testid="field/lookup_secret_codes/true"]').should(
|
||||
"not.exist",
|
||||
)
|
||||
|
||||
cy.get('[data-testid="field/lookup_secret_reveal/true"]').click()
|
||||
cy.get('[data-testid="field/lookup_secret_regenerate/true"]').should(
|
||||
"exist",
|
||||
)
|
||||
cy.get('[data-testid="field/lookup_secret_codes/text"]').then(($e) => {
|
||||
const actualCodes = $e.text().trim().split(", ")
|
||||
expect(actualCodes.join(", ")).to.eq(codes.join(", "))
|
||||
})
|
||||
|
||||
let newCodes
|
||||
cy.get('[data-testid="field/lookup_secret_regenerate/true"]').click()
|
||||
cy.get(
|
||||
'[data-testid="field/lookup_secret_regenerate/true"]:disabled',
|
||||
).should("not.exist")
|
||||
|
||||
cy.get('[data-testid="field/lookup_secret_codes/text"]')
|
||||
.and(($e) => {
|
||||
expect($e.text().trim().split(", ").join(", ")).to.not.eq(
|
||||
codes.join(", "),
|
||||
)
|
||||
})
|
||||
.then(($e) => {
|
||||
newCodes = $e.text().trim().split(", ")
|
||||
})
|
||||
|
||||
cy.get('[data-testid="field/lookup_secret_confirm/true"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
cy.get('[data-testid="field/lookup_secret_reveal/true"]').click()
|
||||
cy.get('[data-testid="field/lookup_secret_codes/text"]').and(($e) => {
|
||||
const actualCodes = $e.text().trim().split(", ")
|
||||
expect(actualCodes.join(", ")).to.eq(newCodes.join(", "))
|
||||
})
|
||||
|
||||
cy.visit(MOBILE_URL + "/Login?aal=aal2&refresh=true")
|
||||
|
||||
// First use a wrong code
|
||||
cy.get("[data-testid=lookup_secret]").then(($e) => {
|
||||
console.log(codes)
|
||||
cy.wrap($e).type("1234")
|
||||
})
|
||||
cy.get('[data-testid="field/method/lookup_secret"]').click()
|
||||
cy.get('[data-testid="form-messages"]').should(
|
||||
"contain.text",
|
||||
"The backup recovery code is not valid.",
|
||||
)
|
||||
cy.get("[data-testid=lookup_secret]").then(($e) => {
|
||||
cy.wrap($e).type(newCodes[0])
|
||||
})
|
||||
cy.get('[data-testid="field/method/lookup_secret"]').click()
|
||||
cy.get('[data-testid="session-content"]').should("contain", "aal2")
|
||||
cy.get('[data-testid="session-content"]').should(
|
||||
"contain",
|
||||
"lookup_secret",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
91
test/e2e/cypress/integration/profiles/mobile/mfa/mix.spec.ts
Normal file
91
test/e2e/cypress/integration/profiles/mobile/mfa/mix.spec.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { gen, MOBILE_URL, website } from "../../../../helpers"
|
||||
import { TOTP } from "otpauth"
|
||||
|
||||
context("Mobile Profile", () => {
|
||||
describe("TOTP 2FA Flow", () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile("mobile")
|
||||
})
|
||||
|
||||
describe("password", () => {
|
||||
let email = gen.email()
|
||||
let password = gen.password()
|
||||
|
||||
before(() => {
|
||||
cy.clearAllCookies()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
email = gen.email()
|
||||
password = gen.password()
|
||||
|
||||
cy.registerApi({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
cy.loginMobile({ email, password })
|
||||
cy.visit(MOBILE_URL + "/Settings")
|
||||
})
|
||||
|
||||
it("should be able to use both TOTP and lookup", () => {
|
||||
// set up totp
|
||||
let totpSecret
|
||||
cy.get('*[data-testid="field/totp_secret_key/text"]').then(($e) => {
|
||||
totpSecret = $e.text().trim()
|
||||
})
|
||||
cy.get('*[data-testid="field/totp_code"]').then(($e) => {
|
||||
cy.wrap($e).type(
|
||||
new TOTP({
|
||||
secret: totpSecret,
|
||||
}).generate(),
|
||||
)
|
||||
})
|
||||
cy.get('*[data-testid="field/method/totp"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
// Set up backup code
|
||||
cy.get('*[data-testid="field/lookup_secret_regenerate/true"]').click()
|
||||
let recoveryCodes
|
||||
cy.get('*[data-testid="field/lookup_secret_codes/text"]').then(($e) => {
|
||||
recoveryCodes = $e.text().trim().split(", ")
|
||||
})
|
||||
cy.get('*[data-testid="field/lookup_secret_confirm/true"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
// Lets sign in with TOTP
|
||||
cy.visit(MOBILE_URL + "/Login?aal=aal2&refresh=true")
|
||||
cy.get('*[data-testid="field/totp_code"]').then(($e) => {
|
||||
cy.wrap($e).type(
|
||||
new TOTP({
|
||||
secret: totpSecret,
|
||||
}).generate(),
|
||||
)
|
||||
})
|
||||
cy.get('*[data-testid="field/method/totp"]').click()
|
||||
|
||||
// We have AAL now
|
||||
cy.get('[data-testid="session-content"]').should("contain", "aal2")
|
||||
cy.get('[data-testid="session-content"]').should("contain", "totp")
|
||||
|
||||
// Lets sign in with lookup secret
|
||||
cy.visit(MOBILE_URL + "/Login?aal=aal2&refresh=true")
|
||||
cy.get('*[data-testid="field/lookup_secret"]').then(($e) => {
|
||||
cy.wrap($e).type(recoveryCodes[0])
|
||||
})
|
||||
cy.get('*[data-testid="field/method/lookup_secret"]').click()
|
||||
|
||||
// We have AAL now
|
||||
cy.get('[data-testid="session-content"]').should("contain", "aal2")
|
||||
cy.get('[data-testid="session-content"]').should("contain", "totp")
|
||||
cy.get('[data-testid="session-content"]').should(
|
||||
"contain",
|
||||
"lookup_secret",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
103
test/e2e/cypress/integration/profiles/mobile/mfa/totp.spec.ts
Normal file
103
test/e2e/cypress/integration/profiles/mobile/mfa/totp.spec.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { gen, MOBILE_URL, website } from "../../../../helpers"
|
||||
import { TOTP } from "otpauth"
|
||||
|
||||
context("Mobile Profile", () => {
|
||||
describe("TOTP 2FA Flow", () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile("mobile")
|
||||
})
|
||||
|
||||
describe("password", () => {
|
||||
let email = gen.email()
|
||||
let password = gen.password()
|
||||
|
||||
before(() => {
|
||||
cy.clearAllCookies()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
email = gen.email()
|
||||
password = gen.password()
|
||||
|
||||
cy.registerApi({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
cy.loginMobile({ email, password })
|
||||
cy.visit(MOBILE_URL + "/Settings")
|
||||
})
|
||||
|
||||
it("should be able to lifecycle through TOTP flows", () => {
|
||||
cy.get('*[data-testid="field/totp_qr"]').should("exist")
|
||||
cy.get('*[data-testid="field/totp_code"]').should("exist")
|
||||
|
||||
// Set up TOTP with invalid key
|
||||
cy.get('*[data-testid="field/totp_code"]').type("111111")
|
||||
cy.get('*[data-testid="field/method/totp"]').click()
|
||||
cy.get('*[data-testid="field/totp_code"]').should(
|
||||
"contain.text",
|
||||
"The provided authentication code is invalid, please try again.",
|
||||
)
|
||||
|
||||
// Set up TOTP with valid key
|
||||
let secret
|
||||
cy.get('*[data-testid="field/totp_secret_key/text"]').then(($e) => {
|
||||
secret = $e.text().trim()
|
||||
})
|
||||
cy.get('*[data-testid="field/totp_code"]').then(($e) => {
|
||||
cy.wrap($e).type(
|
||||
new TOTP({
|
||||
secret,
|
||||
}).generate(),
|
||||
)
|
||||
})
|
||||
cy.get('*[data-testid="field/method/totp"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
// Form should look different now
|
||||
cy.get('*[data-testid="field/totp_secret_key/text"]').should(
|
||||
"not.exist",
|
||||
)
|
||||
cy.get('*[data-testid="field/totp_code"]').should("not.exist")
|
||||
cy.get('*[data-testid="field/totp_qr"]').should("not.exist")
|
||||
cy.get('*[data-testid="field/totp_unlink/true"]').should("exist")
|
||||
|
||||
// Lets sign in
|
||||
cy.visit(MOBILE_URL + "/Login?aal=aal2&refresh=true")
|
||||
|
||||
// First use a wrong code
|
||||
cy.get('*[data-testid="field/totp_code"]').type("111111")
|
||||
cy.get('*[data-testid="field/method/totp"]').click()
|
||||
cy.get('*[data-testid="form-messages"]').should(
|
||||
"contain.text",
|
||||
"The provided authentication code is invalid, please try again.",
|
||||
)
|
||||
|
||||
// Use the correct code
|
||||
cy.get('*[data-testid="field/totp_code"]').then(($e) => {
|
||||
cy.wrap($e).type(
|
||||
new TOTP({
|
||||
secret,
|
||||
}).generate(),
|
||||
)
|
||||
})
|
||||
cy.get('*[data-testid="field/method/totp"]').click()
|
||||
|
||||
// We have AAL now
|
||||
cy.get('[data-testid="session-content"]').should("contain", "aal2")
|
||||
cy.get('[data-testid="session-content"]').should("contain", "totp")
|
||||
|
||||
// Go back to settings and unlink
|
||||
cy.visit(MOBILE_URL + "/Settings")
|
||||
cy.get('*[data-testid="field/totp_unlink/true"]').click()
|
||||
cy.get('*[data-testid="field/totp_unlink/true"]').should("not.exist")
|
||||
cy.get('*[data-testid="field/totp_qr"]').should("exist")
|
||||
cy.get('*[data-testid="field/totp_code"]').should("exist")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,172 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { gen, MOBILE_URL, website } from "../../../../helpers"
|
||||
|
||||
const signupMobilePasskey = (email = gen.email()) => {
|
||||
cy.visit(MOBILE_URL + "/Registration")
|
||||
cy.get('input[data-testid="traits.email"]').type(email)
|
||||
cy.get('input[data-testid="traits.website"]').type(website)
|
||||
cy.get('[data-testid="passkey-button"]').click()
|
||||
|
||||
cy.wait(1000)
|
||||
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
return cy.wrap(email)
|
||||
}
|
||||
|
||||
// The RN app stores the session token in localStorage (AsyncStorage on web).
|
||||
// cy.clearAllCookies() alone does not clear it, so login flows fail with a
|
||||
// stale token. This helper clears both cookies and localStorage.
|
||||
const clearMobileSession = () => {
|
||||
cy.clearAllCookies()
|
||||
cy.window().then((win) => win.localStorage.clear())
|
||||
}
|
||||
|
||||
// Logout via the RN app's logout button which clears the stored session.
|
||||
const logoutMobile = () => {
|
||||
cy.get('*[data-testid="logout"]').click()
|
||||
cy.get('input[data-testid="identifier"]').should("exist")
|
||||
}
|
||||
|
||||
context("Mobile Profile", () => {
|
||||
describe("Passkey Flows", () => {
|
||||
let authenticator: any
|
||||
|
||||
before(() => {
|
||||
cy.task("resetCRI", {})
|
||||
cy.useConfigProfile("passkey")
|
||||
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.enable",
|
||||
opts: {},
|
||||
})
|
||||
.then(() => {
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.addVirtualAuthenticator",
|
||||
opts: {
|
||||
options: {
|
||||
protocol: "ctap2",
|
||||
transport: "internal",
|
||||
hasResidentKey: true,
|
||||
hasUserVerification: true,
|
||||
isUserVerified: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
.then((result) => {
|
||||
authenticator = result
|
||||
cy.log("authenticator ID:", authenticator)
|
||||
})
|
||||
|
||||
cy.longPrivilegedSessionTime()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
clearMobileSession()
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.clearCredentials",
|
||||
opts: authenticator,
|
||||
})
|
||||
})
|
||||
|
||||
after(() => {
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.removeVirtualAuthenticator",
|
||||
opts: authenticator,
|
||||
}).then(() => {
|
||||
cy.task("resetCRI", {})
|
||||
})
|
||||
})
|
||||
|
||||
it("should register with passkey", () => {
|
||||
const email = gen.email()
|
||||
signupMobilePasskey(email)
|
||||
})
|
||||
|
||||
it("should login with passkey after registration", () => {
|
||||
const email = gen.email()
|
||||
signupMobilePasskey(email).then((registeredEmail) => {
|
||||
logoutMobile()
|
||||
cy.visit(MOBILE_URL + "/Login")
|
||||
|
||||
cy.get('[data-testid="passkey-button"]').click()
|
||||
cy.wait(1000)
|
||||
|
||||
cy.get('[data-testid="session-content"]').should(
|
||||
"contain",
|
||||
registeredEmail,
|
||||
)
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
})
|
||||
})
|
||||
|
||||
it("should add passkey in settings after password registration", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
cy.registerApi({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
cy.loginMobile({ email, password })
|
||||
cy.visit(MOBILE_URL + "/Settings")
|
||||
|
||||
cy.get(
|
||||
'*[data-testid="settings-passkey"] [data-testid="passkey-button"]',
|
||||
).click()
|
||||
cy.wait(1000)
|
||||
cy.expectSettingsSaved()
|
||||
})
|
||||
|
||||
it("should not be able to unlink last passkey", () => {
|
||||
signupMobilePasskey().then(() => {
|
||||
cy.visit(MOBILE_URL + "/Settings")
|
||||
// React Native Web renders disabled as aria-disabled on TouchableOpacity
|
||||
cy.get('[data-testid^="passkey-remove-"]').should(
|
||||
"have.attr",
|
||||
"aria-disabled",
|
||||
"true",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it("should be able to link password then remove passkey", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
signupMobilePasskey(email).then(() => {
|
||||
cy.visit(MOBILE_URL + "/Settings")
|
||||
|
||||
// Add a password
|
||||
cy.get(
|
||||
'*[data-testid="settings-password"] input[data-testid="password"]',
|
||||
)
|
||||
.clear()
|
||||
.type(password)
|
||||
cy.get(
|
||||
'*[data-testid="settings-password"] div[data-testid="submit-form"]',
|
||||
).click()
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
// Now passkey remove should be enabled
|
||||
cy.get('[data-testid^="passkey-remove-"]').should(
|
||||
"not.have.attr",
|
||||
"aria-disabled",
|
||||
"true",
|
||||
)
|
||||
cy.get('[data-testid^="passkey-remove-"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
// Logout and login with password
|
||||
logoutMobile()
|
||||
cy.loginMobile({ email, password })
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,112 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { gen, MOBILE_URL, website } from "../../../../helpers"
|
||||
|
||||
context("Mobile Profile", () => {
|
||||
describe("Registration Flow Errors", () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile("mobile")
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit(MOBILE_URL + "/Registration")
|
||||
})
|
||||
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
describe("show errors when invalid signup data is used", () => {
|
||||
it("should show an error when the password has leaked before", () => {
|
||||
cy.get('input[data-testid="traits.email"]').type(email)
|
||||
cy.get('input[data-testid="password"]').type("12345678")
|
||||
cy.get('input[data-testid="traits.website"]').type(website)
|
||||
cy.get(
|
||||
'div[data-testid="field/method/password"] div[data-testid="submit-form"]',
|
||||
).click()
|
||||
|
||||
cy.get('*[data-testid="field/password"]').should(
|
||||
"contain.text",
|
||||
"data breaches",
|
||||
)
|
||||
})
|
||||
|
||||
it("should show an error when the password is too similar", () => {
|
||||
cy.get('input[data-testid="traits.email"]').type(email)
|
||||
cy.get('input[data-testid="password"]').type(email)
|
||||
cy.get('input[data-testid="traits.website"]').type(website)
|
||||
cy.get(
|
||||
'div[data-testid="field/method/password"] div[data-testid="submit-form"]',
|
||||
).click()
|
||||
|
||||
cy.get('*[data-testid="field/password"]').should(
|
||||
"contain.text",
|
||||
"too similar",
|
||||
)
|
||||
})
|
||||
|
||||
it("should show an error when the password is empty", () => {
|
||||
cy.get('input[data-testid="traits.website"]').type(website)
|
||||
cy.get('input[data-testid="traits.email"]').type(email)
|
||||
|
||||
cy.get(
|
||||
'div[data-testid="field/method/password"] div[data-testid="submit-form"]',
|
||||
).click()
|
||||
cy.get('*[data-testid="field/password"]').should(
|
||||
"contain.text",
|
||||
"Property password is missing",
|
||||
)
|
||||
})
|
||||
|
||||
it("should show an error when the email is empty", () => {
|
||||
cy.get('input[data-testid="traits.website"]').type("https://www.ory.sh")
|
||||
cy.get('input[data-testid="password"]').type(password)
|
||||
|
||||
cy.get(
|
||||
'div[data-testid="field/method/password"] div[data-testid="submit-form"]',
|
||||
).click()
|
||||
cy.get('*[data-testid="field/traits.email"]').should(
|
||||
"contain.text",
|
||||
"Property email is missing",
|
||||
)
|
||||
})
|
||||
|
||||
it("should show an error when the email is not an email", () => {
|
||||
cy.get('input[data-testid="traits.website"]').type("https://www.ory.sh")
|
||||
cy.get('input[data-testid="traits.email"]').type("not-an-email")
|
||||
cy.get('input[data-testid="password"]').type(password)
|
||||
|
||||
cy.get(
|
||||
'div[data-testid="field/method/password"] div[data-testid="submit-form"]',
|
||||
).click()
|
||||
cy.get('*[data-testid="field/traits.email"]').should(
|
||||
"contain.text",
|
||||
"Enter a valid email address",
|
||||
)
|
||||
})
|
||||
|
||||
it("should show a missing indicator if no fields are set", () => {
|
||||
cy.get(
|
||||
'div[data-testid="field/method/password"] div[data-testid="submit-form"]',
|
||||
).click()
|
||||
cy.get('*[data-testid="field/password"]').should(
|
||||
"contain.text",
|
||||
"Property password is missing",
|
||||
)
|
||||
})
|
||||
|
||||
it("should show an error when the website is too short", () => {
|
||||
cy.get('input[data-testid="traits.website"]').type("http://s")
|
||||
cy.get('input[data-testid="traits.email"]').type(email)
|
||||
|
||||
cy.get(
|
||||
'div[data-testid="field/method/password"] div[data-testid="submit-form"]',
|
||||
).click()
|
||||
cy.get('*[data-testid="field/traits.website"]').should(
|
||||
"contain.text",
|
||||
"length must be >= 10",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { gen, MOBILE_URL, website } from "../../../../helpers"
|
||||
import { testFlowWebhook } from "../../../../helpers/webhook"
|
||||
|
||||
context("Mobile Profile", () => {
|
||||
describe("Login Flow Success", () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile("mobile")
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.deleteMail()
|
||||
cy.visit(MOBILE_URL + "/Registration")
|
||||
})
|
||||
|
||||
it("should sign up and be logged in", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
cy.get('input[data-testid="traits.email"]').type(email)
|
||||
cy.get('input[data-testid="password"]').type(password)
|
||||
cy.get('input[data-testid="traits.website"]').type(website)
|
||||
cy.get(
|
||||
'div[data-testid="field/method/password"] div[data-testid="submit-form"]',
|
||||
).click()
|
||||
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
})
|
||||
|
||||
it("should pass transient_payload to webhook", () => {
|
||||
testFlowWebhook(
|
||||
(hooks) =>
|
||||
cy.setupHooks("registration", "after", "password", [
|
||||
...hooks,
|
||||
{ hook: "session" },
|
||||
]),
|
||||
() => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
cy.get('input[data-testid="traits.email"]').type(email)
|
||||
cy.get('input[data-testid="password"]').type(password)
|
||||
cy.get('input[data-testid="traits.website"]').type(website)
|
||||
cy.get(
|
||||
'div[data-testid="field/method/password"] div[data-testid="submit-form"]',
|
||||
).click()
|
||||
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it("should sign up and show verification form", () => {
|
||||
cy.enableVerification()
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
cy.get('input[data-testid="traits.email"]').type(email)
|
||||
cy.get('input[data-testid="password"]').type(password)
|
||||
cy.get('input[data-testid="traits.website"]').type(website)
|
||||
cy.get(
|
||||
'div[data-testid="field/method/password"] div[data-testid="submit-form"]',
|
||||
).click()
|
||||
|
||||
cy.get('div[data-testid="field/code"] input').should("be.visible")
|
||||
|
||||
cy.getVerificationCodeFromEmail(email).then((code) => {
|
||||
cy.get('div[data-testid="field/code"] input').type(code)
|
||||
cy.get(
|
||||
'div[data-testid="field/method/code"] div[data-testid=submit-form]',
|
||||
).click()
|
||||
})
|
||||
|
||||
cy.get('[data-testid="ui/message/1080002"]').should(
|
||||
"have.text",
|
||||
"You successfully verified your email address.",
|
||||
)
|
||||
|
||||
cy.get("div[data-testid=continue-button]").click()
|
||||
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { gen, MOBILE_URL, website } from "../../../../helpers"
|
||||
|
||||
context("Mobile Profile", () => {
|
||||
describe("Settings Flow Errors", () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile("mobile")
|
||||
})
|
||||
|
||||
let email, password
|
||||
|
||||
before(() => {
|
||||
email = gen.email()
|
||||
password = gen.password()
|
||||
cy.registerApi({ email, password, fields: { "traits.website": website } })
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.loginMobile({ email, password })
|
||||
cy.visit(MOBILE_URL + "/Settings")
|
||||
})
|
||||
|
||||
describe("profile", () => {
|
||||
it("fails with validation errors", () => {
|
||||
cy.get(
|
||||
'*[data-testid="settings-profile"] input[data-testid="traits.website"]',
|
||||
)
|
||||
.clear()
|
||||
.type("http://s")
|
||||
cy.get(
|
||||
'*[data-testid="settings-profile"] div[data-testid="submit-form"]',
|
||||
).click()
|
||||
|
||||
cy.get(
|
||||
'*[data-testid="settings-profile"] div[data-testid="submit-form"]',
|
||||
).should("not.have.attr", "data-focusable", "false")
|
||||
|
||||
cy.get('*[data-testid="field/traits.website"]').should(
|
||||
"contain.text",
|
||||
"length must be >= 10",
|
||||
)
|
||||
|
||||
cy.get('*[data-testid="settings-password"]').should("exist")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { gen, KRATOS_ADMIN, MOBILE_URL, website } from "../../../../helpers"
|
||||
|
||||
context("Mobile OIDC Settings", () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile("mobile")
|
||||
})
|
||||
|
||||
describe("link buttons", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
before(() => {
|
||||
cy.registerApi({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
cy.loginMobile({ email, password })
|
||||
cy.visit(MOBILE_URL + "/Settings")
|
||||
})
|
||||
|
||||
it("should show link buttons for unlinked providers", () => {
|
||||
cy.get('[data-testid="settings-oidc"]').should("exist")
|
||||
cy.get('[data-testid="settings-oidc"]').within(() => {
|
||||
cy.get('[data-testid="field/link/hydra"]').should("exist")
|
||||
cy.get('[data-testid="field/link/google"]').should("exist")
|
||||
cy.get('[data-testid="field/link/github"]').should("exist")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("unlink", () => {
|
||||
let email: string
|
||||
let password: string
|
||||
|
||||
beforeEach(() => {
|
||||
email = gen.email()
|
||||
password = gen.password()
|
||||
|
||||
// Create an identity with both password and OIDC credentials via Admin API
|
||||
cy.request({
|
||||
method: "POST",
|
||||
url: KRATOS_ADMIN + "/admin/identities",
|
||||
body: {
|
||||
schema_id: "default",
|
||||
traits: { email, website },
|
||||
credentials: {
|
||||
password: { config: { password } },
|
||||
oidc: {
|
||||
config: {
|
||||
providers: [
|
||||
{
|
||||
provider: "hydra",
|
||||
subject: email,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cy.clearAllCookies()
|
||||
cy.loginMobile({ email, password })
|
||||
cy.visit(MOBILE_URL + "/Settings")
|
||||
})
|
||||
|
||||
it("should show unlink button for linked provider", () => {
|
||||
cy.get('[data-testid="settings-oidc"]').should("exist")
|
||||
cy.get('[data-testid="settings-oidc"]').within(() => {
|
||||
cy.get('[data-testid="field/unlink/hydra"]').should("exist")
|
||||
cy.get('[data-testid="field/link/google"]').should("exist")
|
||||
cy.get('[data-testid="field/link/github"]').should("exist")
|
||||
})
|
||||
})
|
||||
|
||||
it("should unlink a provider", () => {
|
||||
cy.get('[data-testid="settings-oidc"]').within(() => {
|
||||
cy.get('[data-testid="field/unlink/hydra"]').should("exist")
|
||||
cy.get('[data-testid="field/unlink/hydra"]').click()
|
||||
})
|
||||
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
// After unlinking, the button should change to link
|
||||
cy.get('[data-testid="settings-oidc"]').within(() => {
|
||||
cy.get('[data-testid="field/link/hydra"]').should("exist")
|
||||
cy.get('[data-testid="field/unlink/hydra"]').should("not.exist")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { gen, MOBILE_URL, website } from "../../../../helpers"
|
||||
|
||||
context("Mobile Profile", () => {
|
||||
describe("Login Flow Success", () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile("mobile")
|
||||
})
|
||||
|
||||
const up = (value) => `not-${value}`
|
||||
|
||||
describe("password", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
before(() => {
|
||||
cy.registerApi({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
cy.loginMobile({ email, password })
|
||||
cy.visit(MOBILE_URL + "/Settings")
|
||||
})
|
||||
|
||||
it("modifies the password", () => {
|
||||
const newPassword = up(password)
|
||||
cy.get(
|
||||
'*[data-testid="settings-password"] input[data-testid="password"]',
|
||||
)
|
||||
.clear()
|
||||
.type(newPassword)
|
||||
cy.get(
|
||||
'*[data-testid="settings-password"] div[data-testid="submit-form"]',
|
||||
).click()
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
cy.get(
|
||||
'*[data-testid="settings-password"] div[data-testid="submit-form"]',
|
||||
).should("not.have.attr", "data-focusable", "false")
|
||||
cy.get('*[data-testid="logout"]').click()
|
||||
cy.get('input[data-testid="identifier"]').should("exist")
|
||||
|
||||
cy.loginMobile({ email, password })
|
||||
cy.get('[data-testid="session-token"]').should("not.exist")
|
||||
cy.loginMobile({ email, password: newPassword })
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
})
|
||||
})
|
||||
|
||||
describe("profile", () => {
|
||||
let email: string
|
||||
let password: string
|
||||
|
||||
beforeEach(() => {
|
||||
cy.deleteMail()
|
||||
email = gen.email()
|
||||
password = gen.password()
|
||||
cy.registerApi({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
cy.loginMobile({ email, password })
|
||||
cy.location("pathname").should("not.contain", "/Login")
|
||||
cy.visit(MOBILE_URL + "/Settings")
|
||||
})
|
||||
|
||||
it("modifies an unprotected trait", () => {
|
||||
cy.get(
|
||||
'*[data-testid="settings-profile"] input[data-testid="traits.website"]',
|
||||
)
|
||||
.clear()
|
||||
.type("https://github.com/ory")
|
||||
cy.get(
|
||||
'*[data-testid="settings-profile"] div[data-testid="submit-form"]',
|
||||
).click()
|
||||
cy.get(
|
||||
'*[data-testid="settings-profile"] div[data-testid="submit-form"]',
|
||||
).should("not.have.attr", "data-focusable", "false")
|
||||
|
||||
cy.visit(MOBILE_URL + "/Home")
|
||||
cy.get('[data-testid="session-content"]').should(
|
||||
"contain",
|
||||
"https://github.com/ory",
|
||||
)
|
||||
})
|
||||
|
||||
it("modifies a protected trait", () => {
|
||||
const newEmail = up(email)
|
||||
cy.get(
|
||||
'*[data-testid="settings-profile"] input[data-testid="traits.email"]',
|
||||
)
|
||||
.clear()
|
||||
.type(newEmail)
|
||||
cy.get(
|
||||
'*[data-testid="settings-profile"] div[data-testid="submit-form"]',
|
||||
).click()
|
||||
cy.get(
|
||||
'*[data-testid="settings-profile"] div[data-testid="submit-form"]',
|
||||
).should("not.have.attr", "data-focusable", "false")
|
||||
|
||||
cy.visit(MOBILE_URL + "/Home")
|
||||
cy.get('[data-testid="session-content"]').should("contain", newEmail)
|
||||
})
|
||||
|
||||
it("shows verification screen after email update", () => {
|
||||
cy.enableVerification()
|
||||
const newEmail = up(email)
|
||||
cy.get(
|
||||
'*[data-testid="settings-profile"] input[data-testid="traits.email"]',
|
||||
)
|
||||
.clear()
|
||||
.type(newEmail)
|
||||
cy.get(
|
||||
'*[data-testid="settings-profile"] div[data-testid="submit-form"]',
|
||||
).click()
|
||||
cy.get(
|
||||
'*[data-testid="settings-profile"] div[data-testid="submit-form"]',
|
||||
).should("not.have.attr", "data-focusable", "false")
|
||||
|
||||
cy.get('div[data-testid="field/code"] input').should("be.visible")
|
||||
|
||||
cy.getVerificationCodeFromEmail(newEmail).then((code) => {
|
||||
cy.get('div[data-testid="field/code"] input').type(code)
|
||||
cy.get(
|
||||
'div[data-testid="field/method/code"] div[data-testid=submit-form]',
|
||||
).click()
|
||||
})
|
||||
|
||||
cy.get('[data-testid="ui/message/1080002"]').should(
|
||||
"have.text",
|
||||
"You successfully verified your email address.",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
52
test/e2e/cypress/integration/profiles/network/errors.spec.ts
Normal file
52
test/e2e/cypress/integration/profiles/network/errors.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { routes as express } from "../../../helpers/express"
|
||||
import { gen } from "../../../helpers"
|
||||
|
||||
describe("Registration failures with email profile", () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile("network")
|
||||
cy.proxy("express")
|
||||
})
|
||||
|
||||
it("should not be able to register if we need a localhost schema", () => {
|
||||
cy.setDefaultIdentitySchema("localhost")
|
||||
cy.visit(express.registration, { failOnStatusCode: false })
|
||||
cy.get('[data-testid="code-box"]').should(
|
||||
"contain.text",
|
||||
"no such host", // could be ::1 or 127.0.0.1
|
||||
)
|
||||
})
|
||||
|
||||
it("should not be able to register if we schema has a local ref", () => {
|
||||
cy.setDefaultIdentitySchema("ref")
|
||||
cy.visit(express.registration, { failOnStatusCode: false })
|
||||
cy.get('[data-testid="code-box"]').should(
|
||||
"contain.text",
|
||||
"no route to host",
|
||||
)
|
||||
})
|
||||
|
||||
it("should not be able to login because pre webhook uses local url", () => {
|
||||
cy.setDefaultIdentitySchema("working")
|
||||
cy.visit(express.login, { failOnStatusCode: false })
|
||||
cy.get('[data-testid="code-box"]').should(
|
||||
"contain.text",
|
||||
"no route to host",
|
||||
)
|
||||
})
|
||||
|
||||
it("should not be able to verify because post webhook uses local jsonnet", () => {
|
||||
cy.setDefaultIdentitySchema("working")
|
||||
cy.visit(express.registration, { failOnStatusCode: false })
|
||||
cy.get('input[name="traits.email"]').type(gen.email())
|
||||
cy.get('input[name="traits.website"]').type("https://google.com/")
|
||||
cy.get('input[name="password"]').type(gen.password())
|
||||
cy.get('[type="submit"]').click()
|
||||
cy.get('[data-testid="code-box"]').should(
|
||||
"contain.text",
|
||||
"no route to host",
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,349 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { gen } from "../../../helpers"
|
||||
import * as httpbin from "../../../helpers/httpbin"
|
||||
import * as oauth2 from "../../../helpers/oauth2"
|
||||
|
||||
context("OpenID Provider", () => {
|
||||
const client = {
|
||||
auth_endpoint: "http://localhost:4744/oauth2/auth",
|
||||
token_endpoint: "http://localhost:4744/oauth2/token",
|
||||
id: Cypress.env("OIDC_DUMMY_CLIENT_ID"),
|
||||
secret: Cypress.env("OIDC_DUMMY_CLIENT_SECRET"),
|
||||
token_endpoint_auth_method: "client_secret_basic",
|
||||
grant_types: ["authorization_code", "refresh_token"],
|
||||
response_types: ["code", "id_token"],
|
||||
scopes: ["openid", "offline", "email", "website"],
|
||||
callbacks: [
|
||||
"http://localhost:5555/callback",
|
||||
"https://ory-network-httpbin-ijakee5waq-ez.a.run.app/anything",
|
||||
],
|
||||
}
|
||||
|
||||
before(() => {
|
||||
cy.deleteMail()
|
||||
cy.useConfigProfile("oidc-provider")
|
||||
cy.proxy("express")
|
||||
})
|
||||
|
||||
it("login", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
cy.registerApi({
|
||||
email: email,
|
||||
password: password,
|
||||
fields: { "traits.website": "http://t1.local" },
|
||||
})
|
||||
|
||||
const url = oauth2.getDefaultAuthorizeURL(client)
|
||||
|
||||
cy.visit(url)
|
||||
|
||||
// kratos login ui
|
||||
cy.get("[name=identifier]").type(email)
|
||||
cy.get("[name=password]").type(password)
|
||||
cy.get("[type='submit'][value='password']").click()
|
||||
|
||||
// consent ui
|
||||
cy.get("#openid").click()
|
||||
cy.get("#offline").click()
|
||||
cy.get("#accept").click()
|
||||
|
||||
const scope = ["offline", "openid"]
|
||||
httpbin.checkToken(client, scope, (token: any) => {
|
||||
expect(token).to.have.property("access_token")
|
||||
expect(token).to.have.property("id_token")
|
||||
expect(token).to.have.property("refresh_token")
|
||||
expect(token).to.have.property("token_type")
|
||||
expect(token).to.have.property("expires_in")
|
||||
expect(token.scope).to.equal("offline openid")
|
||||
let idToken = JSON.parse(
|
||||
decodeURIComponent(escape(window.atob(token.id_token.split(".")[1]))),
|
||||
)
|
||||
expect(idToken).to.have.property("amr")
|
||||
expect(idToken.amr).to.deep.equal(["password"])
|
||||
})
|
||||
})
|
||||
|
||||
it("login-without-scopes", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
cy.registerApi({
|
||||
email: email,
|
||||
password: password,
|
||||
fields: { "traits.website": "http://t1.local" },
|
||||
})
|
||||
|
||||
const url = oauth2.getDefaultAuthorizeURL(client)
|
||||
cy.visit(url)
|
||||
|
||||
// kratos login ui
|
||||
cy.get("[name=identifier]").type(email)
|
||||
cy.get("[name=password]").type(password)
|
||||
cy.get("[type='submit'][value='password']").click()
|
||||
|
||||
// consent ui
|
||||
cy.get("#accept").click()
|
||||
|
||||
const scope = ["offline", "openid"]
|
||||
httpbin.checkToken(client, scope, (token: any) => {
|
||||
expect(token).to.have.property("access_token")
|
||||
expect(token).not.to.have.property("id_token")
|
||||
expect(token).not.to.have.property("refresh_token")
|
||||
expect(token).to.have.property("token_type")
|
||||
expect(token).to.have.property("expires_in")
|
||||
expect(token.scope).to.equal("")
|
||||
})
|
||||
})
|
||||
|
||||
it("respects-login-remember-config", () => {
|
||||
let odicLogin = () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
cy.registerApi({
|
||||
email: email,
|
||||
password: password,
|
||||
fields: { "traits.website": "http://t1.local" },
|
||||
})
|
||||
|
||||
let url = oauth2.getDefaultAuthorizeURL(client)
|
||||
cy.visit(url)
|
||||
|
||||
// kratos login ui
|
||||
cy.get("[name=identifier]").type(email)
|
||||
cy.get("[name=password]").type(password)
|
||||
cy.get("[type='submit'][value='password']").click()
|
||||
// consent ui
|
||||
cy.get("#accept").click()
|
||||
}
|
||||
|
||||
cy.clearAllCookies()
|
||||
cy.updateConfigFile((config) => {
|
||||
config.session.cookie = config.session.cookie || {}
|
||||
config.session.cookie.persistent = true
|
||||
config.session.lifespan = "1234s"
|
||||
return config
|
||||
})
|
||||
|
||||
odicLogin()
|
||||
cy.getCookies({ domain: "localhost" })
|
||||
cy.getCookie("ory_hydra_session_dev", { domain: "localhost" }).should(
|
||||
"not.be.null",
|
||||
)
|
||||
cy.getCookie("ory_hydra_session_dev", { domain: "localhost" }).then(
|
||||
(cookie: Cypress.Cookie) => {
|
||||
let expected = Date.now() / 1000 + 1234
|
||||
let precision = 10
|
||||
expect(cookie.expiry).to.be.lessThan(expected + precision)
|
||||
expect(cookie.expiry).to.be.greaterThan(expected - precision)
|
||||
},
|
||||
)
|
||||
|
||||
cy.clearAllCookies()
|
||||
cy.updateConfigFile((config) => {
|
||||
config.session.cookie = config.session.cookie || {}
|
||||
config.session.cookie.persistent = false
|
||||
return config
|
||||
})
|
||||
|
||||
odicLogin()
|
||||
cy.getCookie("ory_hydra_session_dev", { domain: "localhost" }).should(
|
||||
"be.null",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
context("OpenID Provider - change between flows", () => {
|
||||
const client = {
|
||||
auth_endpoint: "http://localhost:4744/oauth2/auth",
|
||||
token_endpoint: "http://localhost:4744/oauth2/token",
|
||||
id: Cypress.env("OIDC_DUMMY_CLIENT_ID"),
|
||||
secret: Cypress.env("OIDC_DUMMY_CLIENT_SECRET"),
|
||||
token_endpoint_auth_method: "client_secret_basic",
|
||||
grant_types: ["authorization_code", "refresh_token"],
|
||||
response_types: ["code", "id_token"],
|
||||
scopes: ["openid", "offline", "email", "website"],
|
||||
callbacks: [
|
||||
"http://localhost:5555/callback",
|
||||
"https://ory-network-httpbin-ijakee5waq-ez.a.run.app/anything",
|
||||
],
|
||||
}
|
||||
|
||||
function doConsent(amrs?: string[]) {
|
||||
cy.url().should("contain", "/consent")
|
||||
|
||||
// consent ui
|
||||
cy.get("#openid").click()
|
||||
cy.get("#offline").click()
|
||||
cy.get("#accept").click()
|
||||
|
||||
const scope = ["offline", "openid"]
|
||||
httpbin.checkToken(client, scope, (token: any) => {
|
||||
expect(token).to.have.property("access_token")
|
||||
expect(token).to.have.property("id_token")
|
||||
expect(token).to.have.property("refresh_token")
|
||||
expect(token).to.have.property("token_type")
|
||||
expect(token).to.have.property("expires_in")
|
||||
expect(token.scope).to.equal("offline openid")
|
||||
let idToken = JSON.parse(
|
||||
decodeURIComponent(escape(window.atob(token.id_token.split(".")[1]))),
|
||||
)
|
||||
expect(idToken).to.have.property("amr")
|
||||
expect(idToken.amr).to.deep.equal(amrs || ["password"])
|
||||
})
|
||||
}
|
||||
|
||||
before(() => {
|
||||
cy.useConfigProfile("oidc-provider")
|
||||
cy.updateConfigFile((config) => {
|
||||
config.selfservice.allowed_return_urls = [
|
||||
oauth2.getDefaultAuthorizeURL(client),
|
||||
]
|
||||
config.oauth2_provider.override_return_to = true
|
||||
return config
|
||||
})
|
||||
cy.proxy("express")
|
||||
})
|
||||
|
||||
it("switch to registration flow", () => {
|
||||
const identity = gen.identityWithWebsite()
|
||||
|
||||
const url = oauth2.getDefaultAuthorizeURL(client)
|
||||
|
||||
cy.visit(url)
|
||||
cy.get("[href*='/registration']").click()
|
||||
cy.url().should("contain", "/registration")
|
||||
|
||||
cy.get("[name='traits.email']").type(identity.email)
|
||||
cy.get("[name='password']").type(identity.password)
|
||||
cy.get("[name='traits.website']").type(identity.fields["traits.website"])
|
||||
cy.get("[type='submit'][value='password']").click()
|
||||
|
||||
doConsent()
|
||||
})
|
||||
|
||||
it("switch to recovery flow with password reset", () => {
|
||||
cy.updateConfigFile((config) => {
|
||||
config.selfservice.flows.recovery.lifespan = "1m"
|
||||
config.selfservice.methods.link.config.lifespan = "1m"
|
||||
config.selfservice.flows.verification.enabled = false
|
||||
if (!config.selfservice.flows.recovery) {
|
||||
config.selfservice.flows.recovery = {}
|
||||
}
|
||||
config.selfservice.flows.recovery.enabled = true
|
||||
config.selfservice.flows.settings.privileged_session_max_age = "5m"
|
||||
return config
|
||||
})
|
||||
cy.deleteMail()
|
||||
cy.useRecoveryStrategy("code")
|
||||
cy.notifyUnknownRecipients("recovery", false)
|
||||
|
||||
const identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
|
||||
const url = oauth2.getDefaultAuthorizeURL(client)
|
||||
cy.visit(url)
|
||||
cy.get("[href*='/recovery']").click()
|
||||
|
||||
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.get('[data-testid="ui/message/1060001"]', { timeout: 30000 }).should(
|
||||
"contain.text",
|
||||
"You successfully recovered your account. ",
|
||||
)
|
||||
|
||||
cy.getSession()
|
||||
cy.location("pathname").should("eq", "/settings")
|
||||
// do a password change
|
||||
const newPassword = gen.password()
|
||||
cy.get('input[name="password"]').clear().type(newPassword)
|
||||
cy.get('button[value="password"]').click()
|
||||
|
||||
// since we aren't skipping the consent, Kratos will ask us to login again
|
||||
cy.get('input[name="password"]').clear().type(newPassword)
|
||||
cy.get('button[value="password"]').click()
|
||||
|
||||
// we should now end up on the consent screen
|
||||
doConsent(["code_recovery", "password"])
|
||||
})
|
||||
|
||||
it("switch to recovery flow with oidc link", () => {
|
||||
cy.deleteMail()
|
||||
cy.longRecoveryLifespan()
|
||||
cy.longLinkLifespan()
|
||||
cy.enableRecovery()
|
||||
cy.useRecoveryStrategy("code")
|
||||
cy.notifyUnknownRecipients("recovery", false)
|
||||
cy.longPrivilegedSessionTime()
|
||||
|
||||
const fakeOidcFlow = (
|
||||
identity: ReturnType<typeof gen.identityWithWebsite>,
|
||||
) => {
|
||||
cy.get("input[name='username']").type(identity.email)
|
||||
cy.get("button[name='action'][value='accept']").click()
|
||||
// consent screen for the 'fake' oidc provider
|
||||
cy.url().should("contain", "/consent")
|
||||
// consent ui for 3rd party integration to Kratos (sign in with Google)
|
||||
// it will look the same as Hydra consent ui since we are reusing it as
|
||||
// a 'fake' oidc provider
|
||||
cy.get("#openid").click()
|
||||
cy.get("#offline").click()
|
||||
cy.get("#accept").click()
|
||||
}
|
||||
|
||||
const identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
|
||||
const url = oauth2.getDefaultAuthorizeURL(client)
|
||||
cy.visit(url)
|
||||
|
||||
cy.get("[href*='/recovery']").click()
|
||||
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.get('[data-testid="ui/message/1060001"]', { timeout: 30000 }).should(
|
||||
"contain.text",
|
||||
"You successfully recovered your account. ",
|
||||
)
|
||||
|
||||
cy.getSession()
|
||||
cy.location("pathname").should("eq", "/settings")
|
||||
|
||||
// (OAauth2+Kratos provider '/settings' -> Fake OAuth2 '/login')
|
||||
|
||||
// we are reusing Hydra as an OIDC provider for Kratos and doing an OIDC account
|
||||
// linking process here
|
||||
// this is to check if the user can recover their account through OIDC linking
|
||||
// and still be redirected back to the original request
|
||||
cy.get("button[value='google']").click()
|
||||
cy.location("pathname").should("eq", "/login")
|
||||
fakeOidcFlow(identity)
|
||||
|
||||
// it should return us back to the initial OAauth2 login flow
|
||||
// sign in through the 'fake' oidc provider
|
||||
cy.get("button[type='submit'][value='google']").click()
|
||||
|
||||
// login again at the 'fake' oidc provider
|
||||
fakeOidcFlow(identity)
|
||||
|
||||
// complete the consent screen
|
||||
doConsent(["oidc"])
|
||||
})
|
||||
})
|
||||
166
test/e2e/cypress/integration/profiles/oidc-provider/mfa.spec.ts
Normal file
166
test/e2e/cypress/integration/profiles/oidc-provider/mfa.spec.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { gen } from "../../../helpers"
|
||||
import { routes as express } from "../../../helpers/express"
|
||||
import * as oauth2 from "../../../helpers/oauth2"
|
||||
import * as httpbin from "../../../helpers/httpbin"
|
||||
import { TOTP } from "otpauth"
|
||||
|
||||
context("OIDC Provider 2FA", () => {
|
||||
const client = {
|
||||
auth_endpoint: "http://localhost:4744/oauth2/auth",
|
||||
token_endpoint: "http://localhost:4744/oauth2/token",
|
||||
id: Cypress.env("OIDC_DUMMY_CLIENT_ID"),
|
||||
secret: Cypress.env("OIDC_DUMMY_CLIENT_SECRET"),
|
||||
token_endpoint_auth_method: "client_secret_basic",
|
||||
grant_types: ["authorization_code", "refresh_token"],
|
||||
response_types: ["code", "id_token"],
|
||||
scopes: ["openid", "offline", "email", "website"],
|
||||
callbacks: [
|
||||
"http://localhost:5555/callback",
|
||||
"https://ory-network-httpbin-ijakee5waq-ez.a.run.app/anything",
|
||||
],
|
||||
}
|
||||
|
||||
;[
|
||||
{
|
||||
login: express.login,
|
||||
settings: express.settings,
|
||||
base: express.base,
|
||||
profile: "oidc-provider-mfa",
|
||||
app: "express" as "express",
|
||||
},
|
||||
].forEach(({ settings, login, profile, app, base }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
let email = gen.email()
|
||||
let password = gen.password()
|
||||
let secret
|
||||
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
|
||||
email = gen.email()
|
||||
password = gen.password()
|
||||
|
||||
cy.register({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": "http://t1.local" },
|
||||
})
|
||||
cy.visit(settings)
|
||||
|
||||
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.clearAllCookies()
|
||||
})
|
||||
|
||||
it("should be be asked to sign in with 2fa if set up", () => {
|
||||
let url = oauth2.getDefaultAuthorizeURL(client)
|
||||
|
||||
cy.get("body")
|
||||
.then((body$) => {
|
||||
// Credits https://github.com/suchipi, https://github.com/cypress-io/cypress/issues/944#issuecomment-444312914
|
||||
const appWindow = body$[0].ownerDocument.defaultView
|
||||
const appIframe = appWindow.parent.document.querySelector("iframe")
|
||||
|
||||
return new Promise((resolve) => {
|
||||
appIframe.onload = () => resolve(undefined)
|
||||
appWindow.location.href = url
|
||||
})
|
||||
})
|
||||
.then(() => {
|
||||
// kratos login ui
|
||||
cy.get("[name=identifier]").type(email)
|
||||
cy.get("[name=password]").type(password)
|
||||
cy.get("[type=submit]").click()
|
||||
|
||||
cy.get('input[name="totp_code"]').then(($e) => {
|
||||
cy.wrap($e).type(
|
||||
new TOTP({
|
||||
secret,
|
||||
}).generate(),
|
||||
)
|
||||
})
|
||||
cy.get('*[name="method"][value="totp"]').click()
|
||||
|
||||
// consent ui
|
||||
cy.get("#openid").click()
|
||||
cy.get("#offline").click()
|
||||
cy.get("#accept").click()
|
||||
|
||||
let scope = ["offline", "openid"]
|
||||
httpbin.checkToken(client, scope, (token: any) => {
|
||||
expect(token).to.have.property("access_token")
|
||||
expect(token).to.have.property("id_token")
|
||||
expect(token).to.have.property("refresh_token")
|
||||
expect(token).to.have.property("token_type")
|
||||
expect(token).to.have.property("expires_in")
|
||||
expect(token.scope).to.equal("offline openid")
|
||||
let idToken = JSON.parse(
|
||||
decodeURIComponent(
|
||||
escape(window.atob(token.id_token.split(".")[1])),
|
||||
),
|
||||
)
|
||||
expect(idToken).to.have.property("amr")
|
||||
expect(idToken.amr).to.deep.equal(["password", "totp"])
|
||||
})
|
||||
|
||||
// We shouldn't need to authenticate again
|
||||
url = oauth2.getDefaultAuthorizeURL(client)
|
||||
|
||||
cy.get("body")
|
||||
.then((body$) => {
|
||||
// Credits https://github.com/suchipi, https://github.com/cypress-io/cypress/issues/944#issuecomment-444312914
|
||||
const appWindow = body$[0].ownerDocument.defaultView
|
||||
const appIframe =
|
||||
appWindow.parent.document.querySelector("iframe")
|
||||
|
||||
return new Promise((resolve) => {
|
||||
appIframe.onload = () => resolve(undefined)
|
||||
appWindow.location.href = url
|
||||
})
|
||||
})
|
||||
.then(() => {
|
||||
// We get the consent screen instead of login
|
||||
cy.get("#openid").click()
|
||||
cy.get("#offline").click()
|
||||
cy.get("#accept").click()
|
||||
|
||||
httpbin.checkToken(client, scope, (token: any) => {
|
||||
expect(token).to.have.property("access_token")
|
||||
expect(token).to.have.property("id_token")
|
||||
expect(token).to.have.property("refresh_token")
|
||||
expect(token).to.have.property("token_type")
|
||||
expect(token).to.have.property("expires_in")
|
||||
expect(token.scope).to.equal("offline openid")
|
||||
let idToken = JSON.parse(
|
||||
decodeURIComponent(
|
||||
escape(window.atob(token.id_token.split(".")[1])),
|
||||
),
|
||||
)
|
||||
expect(idToken).to.have.property("amr")
|
||||
expect(idToken.amr).to.deep.equal(["password", "totp"])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,183 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { APP_URL, gen } from "../../../helpers"
|
||||
import * as oauth2 from "../../../helpers/oauth2"
|
||||
import * as httpbin from "../../../helpers/httpbin"
|
||||
|
||||
context("OpenID Provider", () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile("oidc-provider")
|
||||
cy.proxy("express")
|
||||
})
|
||||
const client = {
|
||||
auth_endpoint: "http://localhost:4744/oauth2/auth",
|
||||
token_endpoint: "http://localhost:4744/oauth2/token",
|
||||
id: Cypress.env("OIDC_DUMMY_CLIENT_ID"),
|
||||
secret: Cypress.env("OIDC_DUMMY_CLIENT_SECRET"),
|
||||
token_endpoint_auth_method: "client_secret_basic",
|
||||
grant_types: ["authorization_code", "refresh_token"],
|
||||
response_types: ["code", "id_token"],
|
||||
scopes: ["openid", "offline", "email", "website"],
|
||||
callbacks: [
|
||||
"http://localhost:5555/callback",
|
||||
"https://ory-network-httpbin-ijakee5waq-ez.a.run.app/anything",
|
||||
],
|
||||
}
|
||||
|
||||
it("registration", () => {
|
||||
const url = oauth2.getDefaultAuthorizeURL(client)
|
||||
|
||||
cy.visit(url)
|
||||
cy.get("[data-testid=signup-link]").click()
|
||||
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
cy.get('[name="traits.email"]').type(email)
|
||||
cy.get("[name=password]").type(password)
|
||||
cy.get('[name="traits.website"]').type("http://example.com")
|
||||
cy.get('input[type=checkbox][name="traits.tos"]').click({ force: true })
|
||||
cy.get('[name="traits.age"]').type("199")
|
||||
cy.get('input[type=checkbox][name="traits.consent"]').click({ force: true })
|
||||
cy.get('input[type=checkbox][name="traits.newsletter"]').click({
|
||||
force: true,
|
||||
})
|
||||
|
||||
cy.get("[type='submit'][value='password']").click()
|
||||
|
||||
cy.get("#openid").click()
|
||||
cy.get("#offline").click()
|
||||
cy.get("#accept").click()
|
||||
|
||||
const scope = ["offline", "openid"]
|
||||
httpbin.checkToken(client, scope, (token: any) => {
|
||||
expect(token).to.have.property("access_token")
|
||||
expect(token).to.have.property("id_token")
|
||||
expect(token).to.have.property("refresh_token")
|
||||
expect(token).to.have.property("token_type")
|
||||
expect(token).to.have.property("expires_in")
|
||||
expect(token.scope).to.equal("offline openid")
|
||||
let idToken = JSON.parse(
|
||||
decodeURIComponent(escape(window.atob(token.id_token.split(".")[1]))),
|
||||
)
|
||||
expect(idToken).to.have.property("amr")
|
||||
expect(idToken.amr).to.deep.equal(["password"])
|
||||
})
|
||||
})
|
||||
|
||||
it("registration with session, skip=false and skip=true", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
cy.register({
|
||||
email,
|
||||
password,
|
||||
fields: {
|
||||
"traits.website": "https://www.ory.sh",
|
||||
"traits.tos": "1",
|
||||
"traits.age": 22,
|
||||
},
|
||||
})
|
||||
|
||||
const url = oauth2.getDefaultAuthorizeURL(client)
|
||||
|
||||
cy.request(url).then((res) => {
|
||||
const lastResp = res.allRequestResponses[1]["Request URL"]
|
||||
const login_challenge = new URL(lastResp).searchParams.get(
|
||||
"login_challenge",
|
||||
)
|
||||
expect(login_challenge).to.not.be.null
|
||||
cy.visit(
|
||||
APP_URL +
|
||||
"/self-service/registration/browser?login_challenge=" +
|
||||
login_challenge,
|
||||
)
|
||||
})
|
||||
|
||||
cy.url().should("contain", "/login")
|
||||
cy.get("[data-testid='login-flow']").should("exist")
|
||||
cy.get("[data-testid='login-flow'] [name='password']").type(password)
|
||||
cy.get(
|
||||
"[data-testid='login-flow'] button[name='method'][value='password']",
|
||||
).click()
|
||||
|
||||
// we want to skip the consent flow here
|
||||
// so we ask to remember the user
|
||||
cy.get("[name='remember']").click()
|
||||
cy.get("#openid").click()
|
||||
cy.get("#offline").click()
|
||||
cy.get("#accept").click()
|
||||
|
||||
const scope = ["offline", "openid"]
|
||||
httpbin.checkToken(client, scope, (token: any) => {
|
||||
expect(token).to.have.property("access_token")
|
||||
expect(token).to.have.property("id_token")
|
||||
expect(token).to.have.property("refresh_token")
|
||||
expect(token).to.have.property("token_type")
|
||||
expect(token).to.have.property("expires_in")
|
||||
expect(token.scope).to.equal("offline openid")
|
||||
let idToken = JSON.parse(
|
||||
decodeURIComponent(escape(window.atob(token.id_token.split(".")[1]))),
|
||||
)
|
||||
expect(idToken).to.have.property("amr")
|
||||
expect(idToken.amr).to.deep.equal(["password", "password"])
|
||||
})
|
||||
|
||||
// use the hydra origin to make a new OAuth request from it
|
||||
cy.get("body")
|
||||
.then((body$) => {
|
||||
// Credits https://github.com/suchipi, https://github.com/cypress-io/cypress/issues/944#issuecomment-444312914
|
||||
const appWindow = body$[0].ownerDocument.defaultView
|
||||
const appIframe = appWindow.parent.document.querySelector("iframe")
|
||||
|
||||
return new Promise((resolve) => {
|
||||
appIframe.onload = () => resolve(undefined)
|
||||
appWindow.location.href = "http://localhost:4744/health/ready"
|
||||
})
|
||||
})
|
||||
.then(() => {
|
||||
// we don't want to redirect here since we only want the login challenge from hydra
|
||||
// we reusing the challenge to navigate to the registration page
|
||||
cy.request({
|
||||
url: oauth2.getDefaultAuthorizeURL(client),
|
||||
followRedirect: false,
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res.redirectedToUrl).to.include("login_challenge")
|
||||
return new URL(res.redirectedToUrl).searchParams.get(
|
||||
"login_challenge",
|
||||
)
|
||||
})
|
||||
.then((login_challenge) => {
|
||||
cy.get("body").then((body$) => {
|
||||
const appWindow = body$[0].ownerDocument.defaultView
|
||||
const appIframe =
|
||||
appWindow.parent.document.querySelector("iframe")
|
||||
|
||||
return new Promise((resolve) => {
|
||||
appIframe.onload = () => resolve(undefined)
|
||||
appWindow.location.href =
|
||||
APP_URL +
|
||||
"/self-service/registration/browser?login_challenge=" +
|
||||
login_challenge
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
httpbin.checkToken(client, scope, (token: any) => {
|
||||
expect(token).to.have.property("access_token")
|
||||
expect(token).to.have.property("id_token")
|
||||
expect(token).to.have.property("refresh_token")
|
||||
expect(token).to.have.property("token_type")
|
||||
expect(token).to.have.property("expires_in")
|
||||
expect(token.scope).to.equal("offline openid")
|
||||
let idToken = JSON.parse(
|
||||
decodeURIComponent(escape(window.atob(token.id_token.split(".")[1]))),
|
||||
)
|
||||
expect(idToken).to.have.property("amr")
|
||||
expect(idToken.amr).to.deep.equal(["password", "password"])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, gen, website } from "../../../../helpers"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
|
||||
context("Social Sign In Errors", () => {
|
||||
;[
|
||||
{
|
||||
login: react.login,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
{
|
||||
login: express.login,
|
||||
app: "express" as "express",
|
||||
profile: "oidc",
|
||||
},
|
||||
].forEach(({ login, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
cy.visit(login)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should fail when the login request is rejected", () => {
|
||||
cy.triggerOidc(app)
|
||||
cy.get("#reject").click()
|
||||
cy.location("pathname").should("equal", "/login")
|
||||
cy.get(appPrefix(app) + '[data-testid="ui/message/4000001"]').should(
|
||||
"contain.text",
|
||||
"login rejected request",
|
||||
)
|
||||
cy.noSession()
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should fail when the consent request is rejected", () => {
|
||||
const email = gen.email()
|
||||
cy.triggerOidc(app)
|
||||
cy.get("#username").type(email)
|
||||
cy.get("#accept").click()
|
||||
cy.get("#reject").click()
|
||||
cy.location("pathname").should("equal", "/login")
|
||||
cy.get('[data-testid="ui/message/4000001"]').should(
|
||||
"contain.text",
|
||||
"consent rejected request",
|
||||
)
|
||||
cy.noSession()
|
||||
})
|
||||
|
||||
it("should fail when the id_token is missing", () => {
|
||||
const email = gen.email()
|
||||
cy.triggerOidc(app)
|
||||
cy.get("#username").type(email)
|
||||
cy.get("#accept").click()
|
||||
cy.get("#website").type(website)
|
||||
cy.get("#accept").click()
|
||||
cy.location("pathname").should("equal", "/login")
|
||||
cy.get('[data-testid="ui/message/4000001"]').should(
|
||||
"contain.text",
|
||||
"no id_token",
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail to convert a sign in flow to a sign up flow when registration is disabled", () => {
|
||||
cy.disableRegistration()
|
||||
|
||||
const email = gen.email()
|
||||
cy.visit(login)
|
||||
cy.triggerOidc(app)
|
||||
|
||||
cy.get("#username").clear().type(email)
|
||||
cy.get("#remember").click()
|
||||
cy.get("#accept").click()
|
||||
cy.get('[name="scope"]').each(($el) => cy.wrap($el).click())
|
||||
cy.get("#remember").click()
|
||||
cy.get("#accept").click()
|
||||
|
||||
cy.get('[data-testid="ui/message/4000001"]').should(
|
||||
"contain.text",
|
||||
"Registration is not allowed because it was disabled",
|
||||
)
|
||||
|
||||
cy.noSession()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
128
test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts
Normal file
128
test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
import { appPrefix, gen, website } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
context("Social Sign In Successes", () => {
|
||||
;[
|
||||
{
|
||||
login: react.login,
|
||||
registration: react.registration,
|
||||
settings: react.settings,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
{
|
||||
login: express.login,
|
||||
registration: express.registration,
|
||||
settings: express.settings,
|
||||
app: "express" as "express",
|
||||
profile: "oidc",
|
||||
},
|
||||
].forEach(({ login, registration, profile, app, settings }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should be able to sign up, sign out, and then sign in", () => {
|
||||
const email = gen.email()
|
||||
cy.registerOidc({ app, email, website, route: registration })
|
||||
cy.logout()
|
||||
cy.noSession()
|
||||
cy.loginOidc({ app, url: login })
|
||||
})
|
||||
|
||||
it("should be able to sign up and link existing account", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
// Create a new account
|
||||
cy.registerApi({
|
||||
email,
|
||||
password,
|
||||
fields: { "traits.website": website },
|
||||
})
|
||||
|
||||
// Try to log in with the same identifier through OIDC. This should fail and create a new login flow.
|
||||
cy.registerOidc({
|
||||
app,
|
||||
email,
|
||||
website,
|
||||
expectSession: false,
|
||||
})
|
||||
cy.noSession()
|
||||
|
||||
// Log in with the same identifier through the login flow. This should link the accounts.
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.submitPasswordForm()
|
||||
cy.location("pathname").should("not.contain", "/login")
|
||||
cy.getSession()
|
||||
|
||||
// Hydra OIDC should now be linked
|
||||
cy.visit(settings)
|
||||
cy.get('[value="hydra"]')
|
||||
.should("have.attr", "name", "unlink")
|
||||
.should("contain.text", "Unlink Ory")
|
||||
})
|
||||
|
||||
it("should be able to sign up with redirects", () => {
|
||||
const email = gen.email()
|
||||
cy.registerOidc({
|
||||
app,
|
||||
email,
|
||||
website,
|
||||
route: registration + "?return_to=https://www.example.org/",
|
||||
})
|
||||
cy.location("href").should("eq", "https://www.example.org/")
|
||||
cy.logout()
|
||||
cy.noSession()
|
||||
cy.loginOidc({
|
||||
app,
|
||||
url: login + "?return_to=https://www.example.org/",
|
||||
})
|
||||
cy.location("href").should("eq", "https://www.example.org/")
|
||||
})
|
||||
|
||||
it("should be able to log in with upstream parameters", () => {
|
||||
const email = gen.email()
|
||||
cy.registerOidc({
|
||||
app,
|
||||
email,
|
||||
website,
|
||||
route: registration + "?return_to=https://www.example.org/",
|
||||
})
|
||||
|
||||
cy.location("href").should("eq", "https://www.example.org/")
|
||||
cy.logout()
|
||||
cy.noSession()
|
||||
cy.intercept("GET", "**/oauth2/auth*").as("getHydraLogin")
|
||||
cy.loginOidc({
|
||||
app,
|
||||
url: login + "?return_to=https://www.example.org/",
|
||||
preTriggerHook: () => {
|
||||
// Inject hd and login_hint into upstream_parameters. hd is
|
||||
// forwarded; login_hint is never forwarded because a caller-supplied
|
||||
// value cannot be trusted (HackerOne #3239672, ory-corp/cloud#8955).
|
||||
cy.addInputElement("form", "upstream_parameters.hd", "ory.sh")
|
||||
cy.addInputElement("form", "upstream_parameters.login_hint", email)
|
||||
},
|
||||
})
|
||||
// once a request to getHydraLogin responds, 'cy.wait' will resolve
|
||||
cy.wait("@getHydraLogin")
|
||||
.its("request.url")
|
||||
.should("include", "hd=ory.sh")
|
||||
.and("not.include", "login_hint")
|
||||
|
||||
cy.location("href").should("eq", "https://www.example.org/")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, gen, website } from "../../../../helpers"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
|
||||
context("Social Sign Out Successes", () => {
|
||||
;[
|
||||
{
|
||||
base: react.base,
|
||||
registration: react.registration,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
{
|
||||
base: express.base,
|
||||
registration: express.registration,
|
||||
app: "express" as "express",
|
||||
profile: "oidc",
|
||||
},
|
||||
].forEach(({ base, registration, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
before(() => {
|
||||
cy.clearAllCookies()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit(base)
|
||||
const email = gen.email()
|
||||
cy.registerOidc({ app, email, website, route: registration })
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should sign out and be able to sign in again", () => {
|
||||
if (app === "express") {
|
||||
cy.get(
|
||||
`${appPrefix(app)} [data-testid="logout"] a:not(disabled)`,
|
||||
).click()
|
||||
} else {
|
||||
cy.get(
|
||||
`${appPrefix(app)} [data-testid="logout"]:not(disabled)`,
|
||||
).click()
|
||||
}
|
||||
cy.noSession()
|
||||
cy.url().should("include", "/login")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, gen, website } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
context("Social Sign Up Errors", () => {
|
||||
;[
|
||||
{
|
||||
registration: react.registration,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
{
|
||||
registration: express.registration,
|
||||
app: "express" as "express",
|
||||
profile: "oidc",
|
||||
},
|
||||
].forEach(({ registration, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
cy.visit(registration)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should fail when the login request is rejected", () => {
|
||||
cy.triggerOidc(app)
|
||||
cy.get("#reject").click()
|
||||
cy.location("pathname").should("equal", "/registration")
|
||||
cy.get(appPrefix(app) + '[data-testid="ui/message/4000001"]').should(
|
||||
"contain.text",
|
||||
"login rejected request",
|
||||
)
|
||||
cy.noSession()
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should fail when the consent request is rejected", () => {
|
||||
const email = gen.email()
|
||||
cy.triggerOidc(app)
|
||||
cy.get("#username").type(email)
|
||||
cy.get("#accept").click()
|
||||
cy.get("#reject").click()
|
||||
cy.location("pathname").should("equal", "/registration")
|
||||
cy.get('[data-testid="ui/message/4000001"]').should(
|
||||
"contain.text",
|
||||
"consent rejected request",
|
||||
)
|
||||
cy.noSession()
|
||||
})
|
||||
|
||||
it("should fail when the id_token is missing", () => {
|
||||
const email = gen.email()
|
||||
cy.triggerOidc(app)
|
||||
cy.get("#username").type(email)
|
||||
cy.get("#accept").click()
|
||||
cy.get("#website").type(website)
|
||||
cy.get("#accept").click()
|
||||
cy.location("pathname").should("equal", "/registration")
|
||||
cy.get('[data-testid="ui/message/4000001"]').should(
|
||||
"contain.text",
|
||||
"no id_token",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,274 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, gen, website } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
import { testFlowWebhook } from "../../../../helpers/webhook"
|
||||
|
||||
context("Social Sign Up Successes", () => {
|
||||
;[
|
||||
{
|
||||
login: react.login,
|
||||
registration: react.registration,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
{
|
||||
login: express.login,
|
||||
registration: express.registration,
|
||||
app: "express" as "express",
|
||||
profile: "oidc",
|
||||
},
|
||||
].forEach(({ registration, login, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
cy.visit(registration)
|
||||
cy.setIdentitySchema(
|
||||
"file://test/e2e/profiles/oidc/identity.traits.schema.json",
|
||||
)
|
||||
})
|
||||
|
||||
const shouldSession = (email) => (session) => {
|
||||
const { identity } = session
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.traits.website).to.equal(website)
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
}
|
||||
|
||||
it("should be able to sign up with incomplete data and finally be signed in", () => {
|
||||
const email = gen.email()
|
||||
|
||||
cy.registerOidc({
|
||||
app,
|
||||
email,
|
||||
expectSession: false,
|
||||
route: registration,
|
||||
})
|
||||
|
||||
cy.get("#registration-password").should("not.exist")
|
||||
cy.get(appPrefix(app) + '[name="traits.email"]').should(
|
||||
"have.value",
|
||||
email,
|
||||
)
|
||||
cy.get('[data-testid="ui/message/4000002"]').should(
|
||||
"contain.text",
|
||||
"Property website is missing",
|
||||
)
|
||||
|
||||
cy.get('[name="traits.consent"][type="checkbox"]')
|
||||
.siblings("label")
|
||||
.click()
|
||||
cy.get('[name="traits.newsletter"][type="checkbox"]')
|
||||
.siblings("label")
|
||||
.click()
|
||||
cy.get('[name="traits.website"]').type("http://s")
|
||||
|
||||
cy.get('[name="provider"]')
|
||||
.should("have.length", 1)
|
||||
.should("have.value", "hydra")
|
||||
.should("contain.text", "Continue")
|
||||
.click()
|
||||
|
||||
cy.get("#registration-password").should("not.exist")
|
||||
cy.get('[name="traits.email"]').should("have.value", email)
|
||||
cy.get('[name="traits.website"]').should("have.value", "http://s")
|
||||
cy.get('[data-testid="ui/message/4000003"]').should(
|
||||
"contain.text",
|
||||
"length must be >= 10",
|
||||
)
|
||||
cy.get('[name="traits.website"]')
|
||||
.should("have.value", "http://s")
|
||||
.clear()
|
||||
.type(website)
|
||||
|
||||
cy.get('[name="traits.consent"]').should("be.checked")
|
||||
cy.get('[name="traits.newsletter"]').should("be.checked")
|
||||
|
||||
cy.triggerOidc(app)
|
||||
|
||||
cy.location("pathname").should((loc) => {
|
||||
expect(loc).to.be.oneOf(["/welcome", "/", "/sessions"])
|
||||
})
|
||||
|
||||
cy.getSession().should((session) => {
|
||||
shouldSession(email)(session)
|
||||
expect(session.identity.traits.consent).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
it("should pass transient_payload to webhook", () => {
|
||||
testFlowWebhook(
|
||||
(hooks) =>
|
||||
cy.setupHooks("registration", "after", "oidc", [
|
||||
...hooks,
|
||||
{ hook: "session" },
|
||||
]),
|
||||
() => {
|
||||
const email = gen.email()
|
||||
cy.registerOidc({
|
||||
app,
|
||||
email,
|
||||
website,
|
||||
route: registration,
|
||||
})
|
||||
cy.getSession().should(shouldSession(email))
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should be able to sign up with complete data", () => {
|
||||
const email = gen.email()
|
||||
|
||||
cy.registerOidc({ app, email, website, route: registration })
|
||||
cy.getSession().should(shouldSession(email))
|
||||
})
|
||||
|
||||
it("should be able to convert a sign up flow to a sign in flow", () => {
|
||||
const email = gen.email()
|
||||
|
||||
cy.registerOidc({ app, email, website, route: registration })
|
||||
cy.logout()
|
||||
cy.noSession()
|
||||
cy.visit(registration)
|
||||
cy.triggerOidc(app)
|
||||
|
||||
cy.location("pathname").should((path) => {
|
||||
expect(path).to.oneOf(["/", "/welcome", "/sessions"])
|
||||
})
|
||||
|
||||
cy.getSession().should(shouldSession(email))
|
||||
})
|
||||
|
||||
it("should be able to convert a sign in flow to a sign up flow", () => {
|
||||
cy.setIdentitySchema(
|
||||
"file://test/e2e/profiles/oidc/identity-required.traits.schema.json",
|
||||
)
|
||||
|
||||
const email = gen.email()
|
||||
cy.visit(login)
|
||||
cy.triggerOidc(app)
|
||||
|
||||
cy.get("#username").clear().type(email)
|
||||
cy.get("#remember").click()
|
||||
cy.get("#accept").click()
|
||||
cy.get('[name="scope"]').each(($el) => cy.wrap($el).click())
|
||||
cy.get("#remember").click()
|
||||
cy.get("#accept").click()
|
||||
|
||||
cy.get('[data-testid="ui/message/4000002"]').should(
|
||||
"contain.text",
|
||||
"Property website is missing",
|
||||
)
|
||||
cy.get('[name="traits.website"]').type("http://s")
|
||||
|
||||
cy.triggerOidc(app)
|
||||
|
||||
cy.get('[data-testid="ui/message/4000003"]').should(
|
||||
"contain.text",
|
||||
"length must be >= 10",
|
||||
)
|
||||
cy.get('[name="traits.requirednested"]').should("not.exist")
|
||||
cy.get('[name="traits.requirednested.a"]').siblings("label").click()
|
||||
cy.get('[name="traits.consent"]').siblings("label").click()
|
||||
cy.get('[name="traits.website"]')
|
||||
.should("have.value", "http://s")
|
||||
.clear()
|
||||
.type(website)
|
||||
cy.triggerOidc(app)
|
||||
|
||||
cy.location("pathname").should("not.contain", "/registration")
|
||||
|
||||
cy.getSession().should(shouldSession(email))
|
||||
})
|
||||
|
||||
it("should be able to sign up with redirects", () => {
|
||||
const email = gen.email()
|
||||
cy.registerOidc({
|
||||
app,
|
||||
email,
|
||||
website,
|
||||
route: registration + "?return_to=https://www.example.org/",
|
||||
})
|
||||
cy.location("href").should("eq", "https://www.example.org/")
|
||||
cy.logout()
|
||||
})
|
||||
|
||||
it("should be able to register with upstream parameters", () => {
|
||||
const email = gen.email()
|
||||
cy.intercept("GET", "**/oauth2/auth*").as("getHydraRegistration")
|
||||
|
||||
cy.visit(registration + "?return_to=https://www.example.org/")
|
||||
|
||||
// hd is forwarded; login_hint is never forwarded because a
|
||||
// caller-supplied value cannot be trusted (HackerOne #3239672,
|
||||
// ory-corp/cloud#8955).
|
||||
cy.addInputElement("form", "upstream_parameters.hd", "ory.sh")
|
||||
cy.addInputElement("form", "upstream_parameters.login_hint", email)
|
||||
|
||||
cy.triggerOidc(app)
|
||||
|
||||
// once a request to getHydraRegistration responds, 'cy.wait' will resolve
|
||||
cy.wait("@getHydraRegistration")
|
||||
.its("request.url")
|
||||
.should("include", "hd=ory.sh")
|
||||
.and("not.include", "login_hint")
|
||||
})
|
||||
|
||||
it("oidc registration with duplicate identifier should return new login flow with duplicate error", () => {
|
||||
cy.visit(registration)
|
||||
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
cy.get('input[name="traits.email"]').type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.get('input[name="traits.website"]').type(website)
|
||||
cy.get('[name="traits.consent"]').siblings("label").click()
|
||||
cy.get('[name="traits.newsletter"]').siblings("label").click()
|
||||
|
||||
cy.submitPasswordForm()
|
||||
|
||||
cy.location("pathname").should("not.contain", "/registration")
|
||||
|
||||
cy.getSession().should(shouldSession(email))
|
||||
|
||||
cy.logout()
|
||||
cy.noSession()
|
||||
|
||||
// register the account through the OIDC provider
|
||||
cy.registerOidc({
|
||||
app,
|
||||
acceptConsent: true,
|
||||
acceptLogin: true,
|
||||
expectSession: false,
|
||||
email,
|
||||
website,
|
||||
route: registration,
|
||||
})
|
||||
|
||||
cy.get('[data-testid="ui/message/1010016"]').should("be.visible")
|
||||
|
||||
cy.location("href").should("contain", "/login")
|
||||
|
||||
cy.get("[name='provider'][value='google']").should("be.visible")
|
||||
cy.get("[name='provider'][value='github']").should("be.visible")
|
||||
|
||||
if (app === "express") {
|
||||
cy.get("[data-testid='forgot-password-link']").should("be.visible")
|
||||
}
|
||||
|
||||
cy.get("input[name='password']").type(password)
|
||||
cy.submitPasswordForm()
|
||||
cy.getSession()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { APP_URL, appPrefix, gen, website } from "../../../../helpers"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
|
||||
context("Social Sign In Settings Errors", () => {
|
||||
;[
|
||||
{
|
||||
registration: react.registration,
|
||||
settings: react.settings,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
{
|
||||
registration: express.registration,
|
||||
settings: express.settings,
|
||||
app: "express" as "express",
|
||||
profile: "oidc",
|
||||
},
|
||||
].forEach(({ registration, profile, app, settings }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
let email
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
email = gen.email()
|
||||
|
||||
cy.registerOidc({
|
||||
app,
|
||||
email,
|
||||
expectSession: true,
|
||||
website,
|
||||
route: registration,
|
||||
})
|
||||
cy.visit(settings)
|
||||
})
|
||||
|
||||
describe("oidc", () => {
|
||||
it("should fail to link google because id token is missing", () => {
|
||||
cy.get(appPrefix(app) + 'button[value="google"]').click()
|
||||
cy.get("#remember").click()
|
||||
cy.get("#accept").click()
|
||||
|
||||
cy.get('[data-testid="ui/message/4000001"]').should(
|
||||
"contain.text",
|
||||
'Authentication failed because no id_token was returned. Please accept the "openid" permission and try again.',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,252 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, gen, website } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
import { util } from "prettier"
|
||||
import skip = util.skip
|
||||
|
||||
context("Social Sign In Settings Success", () => {
|
||||
;[
|
||||
{
|
||||
registration: react.registration,
|
||||
settings: react.settings,
|
||||
login: react.login,
|
||||
app: "react" as "react",
|
||||
profile: "spa",
|
||||
},
|
||||
{
|
||||
registration: express.registration,
|
||||
settings: express.settings,
|
||||
login: express.login,
|
||||
app: "express" as "express",
|
||||
profile: "oidc",
|
||||
},
|
||||
].forEach(({ registration, login, profile, app, settings }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
let email
|
||||
|
||||
const hydraReauthFails = () => {
|
||||
cy.clearAllCookies()
|
||||
cy.visit(login)
|
||||
cy.get(appPrefix(app) + '[value="hydra"]').click()
|
||||
|
||||
cy.get("#username").type(email)
|
||||
cy.get("#remember").click()
|
||||
cy.get("#accept").click()
|
||||
|
||||
cy.get('input[name="traits.website"]').clear().type(website)
|
||||
cy.triggerOidc(app, "hydra")
|
||||
|
||||
cy.get('[data-testid="ui/message/1010016"]').should(
|
||||
"contain.text",
|
||||
"as another way to sign in.",
|
||||
)
|
||||
|
||||
cy.noSession()
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
email = gen.email()
|
||||
|
||||
cy.registerOidc({
|
||||
app,
|
||||
email,
|
||||
expectSession: true,
|
||||
website,
|
||||
route: registration,
|
||||
})
|
||||
cy.visit(settings)
|
||||
})
|
||||
|
||||
describe("oidc", () => {
|
||||
beforeEach(() => {
|
||||
cy.useConfig((builder) =>
|
||||
builder
|
||||
.longRecoveryLifespan()
|
||||
.longVerificationLifespan()
|
||||
.longPrivilegedSessionTime(),
|
||||
)
|
||||
})
|
||||
|
||||
it("should show the correct options", () => {
|
||||
cy.get('[value="hydra"]').should("not.exist")
|
||||
|
||||
cy.get('[value="google"]')
|
||||
.should("have.attr", "name", "link")
|
||||
.should("contain.text", "Link google")
|
||||
|
||||
cy.get('[value="github"]')
|
||||
.should("have.attr", "name", "link")
|
||||
.should("contain.text", "Link github")
|
||||
})
|
||||
|
||||
it("should show the unlink once password is set", () => {
|
||||
cy.get('[value="hydra"]').should("not.exist")
|
||||
|
||||
cy.get('input[name="password"]').type(gen.password())
|
||||
cy.get('button[value="password"]').click()
|
||||
|
||||
cy.get('[value="hydra"]')
|
||||
.should("have.attr", "name", "unlink")
|
||||
.should("contain.text", "Unlink Ory")
|
||||
})
|
||||
|
||||
it("should link google", () => {
|
||||
cy.get('[value="google"]').click()
|
||||
|
||||
cy.get('input[name="scope"]').each(($el) => cy.wrap($el).click())
|
||||
cy.get("#remember").click()
|
||||
cy.get("#accept").click()
|
||||
|
||||
cy.visit(settings)
|
||||
|
||||
cy.get('[value="google"]')
|
||||
.should("have.attr", "name", "unlink")
|
||||
.should("contain.text", "Unlink google")
|
||||
|
||||
cy.logout()
|
||||
|
||||
cy.visit(login)
|
||||
|
||||
// we create an intercept for login so we make sure the login endpoint is called
|
||||
cy.intercept("GET", "**/oauth2/auth*").as("login")
|
||||
|
||||
cy.get('[value="google"]').click()
|
||||
|
||||
// wait for the login endpoint to be called
|
||||
cy.wait("@login")
|
||||
|
||||
// check the session
|
||||
cy.getSession()
|
||||
})
|
||||
|
||||
it("should link google after re-auth", () => {
|
||||
cy.shortPrivilegedSessionTime()
|
||||
cy.get('[value="google"]').click()
|
||||
cy.location("pathname").should("equal", "/login")
|
||||
|
||||
cy.longPrivilegedSessionTime()
|
||||
cy.get('[value="hydra"]').click()
|
||||
|
||||
// prompt=login means that we need to re-auth!
|
||||
cy.get("#username").type(email)
|
||||
cy.get("#accept").click()
|
||||
|
||||
// we re-authed, now we do the google oauth2 dance
|
||||
cy.get("#username").type(gen.email())
|
||||
cy.get("#accept").click()
|
||||
cy.get('input[name="scope"]').each(($el) => cy.wrap($el).click())
|
||||
cy.get("#accept").click()
|
||||
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
cy.get('[value="google"]')
|
||||
.should("have.attr", "name", "unlink")
|
||||
.should("contain.text", "Unlink google")
|
||||
|
||||
cy.visit(settings)
|
||||
|
||||
cy.get('[value="google"]')
|
||||
.should("have.attr", "name", "unlink")
|
||||
.should("contain.text", "Unlink google")
|
||||
})
|
||||
|
||||
it("should unlink hydra and no longer be able to sign in", () => {
|
||||
if (app === "react") {
|
||||
// This test is flaky on React, so we skip it for now.
|
||||
return
|
||||
}
|
||||
cy.get('[value="hydra"]').should("not.exist")
|
||||
cy.get('input[name="password"]').type(gen.password())
|
||||
cy.get('[value="password"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
cy.visit(settings)
|
||||
|
||||
cy.get('[value="hydra"]').click()
|
||||
|
||||
// It will no longer be possible to sign up with this provider because of a UNIQUE key violation
|
||||
// because of the email verification table.
|
||||
//
|
||||
// Basically what needs to happen is for the user to use the LINK feature to link this account.
|
||||
hydraReauthFails()
|
||||
})
|
||||
|
||||
it("should unlink hydra after reauth", () => {
|
||||
cy.get('[value="hydra"]').should("not.exist")
|
||||
|
||||
cy.get('input[name="password"]').type(gen.password())
|
||||
cy.get('[value="password"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
cy.visit(settings)
|
||||
|
||||
cy.shortPrivilegedSessionTime()
|
||||
cy.get('[value="hydra"]').click()
|
||||
|
||||
cy.longPrivilegedSessionTime()
|
||||
cy.location("pathname").should("equal", "/login")
|
||||
cy.get('[value="hydra"]').click()
|
||||
|
||||
// prompt=login means that we need to re-auth!
|
||||
cy.get("#username").type(email)
|
||||
cy.get("#accept").click()
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
hydraReauthFails()
|
||||
})
|
||||
|
||||
it("should show only linked providers during reauth", () => {
|
||||
cy.shortPrivilegedSessionTime()
|
||||
|
||||
cy.get('input[name="password"]').type(gen.password())
|
||||
cy.get('[value="password"]').click()
|
||||
|
||||
cy.location("pathname").should("equal", "/login")
|
||||
|
||||
cy.get('[value="hydra"]').should("exist")
|
||||
cy.get('[value="google"]').should("not.exist")
|
||||
cy.get('[value="github"]').should("not.exist")
|
||||
})
|
||||
|
||||
it("settings screen stays intact when the original sign up method gets removed", () => {
|
||||
const expectSettingsOk = () => {
|
||||
cy.get('[value="google"]', { timeout: 1000 })
|
||||
.should("have.attr", "name", "link")
|
||||
.should("contain.text", "Link google")
|
||||
|
||||
cy.get('[value="github"]', { timeout: 1000 })
|
||||
.should("have.attr", "name", "link")
|
||||
.should("contain.text", "Link github")
|
||||
}
|
||||
|
||||
cy.visit(settings)
|
||||
expectSettingsOk()
|
||||
|
||||
// set password
|
||||
cy.get('input[name="password"]').type(gen.password())
|
||||
cy.get('button[value="password"]').click()
|
||||
|
||||
// remove the provider used to log in
|
||||
cy.updateConfigFile((config) => {
|
||||
config.selfservice.methods.oidc.config.providers =
|
||||
config.selfservice.methods.oidc.config.providers.filter(
|
||||
({ id }) => id !== "hydra",
|
||||
)
|
||||
return config
|
||||
})
|
||||
|
||||
// visit settings and everything should still be fine
|
||||
cy.visit(settings)
|
||||
expectSettingsOk()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
302
test/e2e/cypress/integration/profiles/passkey/flows.spec.ts
Normal file
302
test/e2e/cypress/integration/profiles/passkey/flows.spec.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { gen } from "../../../helpers"
|
||||
import { routes as express } from "../../../helpers/express"
|
||||
import { routes as react } from "../../../helpers/react"
|
||||
import { testFlowWebhook } from "../../../helpers/webhook"
|
||||
|
||||
const signup = (registration: string, app: string, email = gen.email()) => {
|
||||
cy.visit(registration)
|
||||
|
||||
const websiteTrait = `${
|
||||
app === "express" ? `form[data-testid="passkey-flow"]` : ""
|
||||
} input[name="traits.website"]`
|
||||
const emailTrait = `${
|
||||
app === "express" ? `form[data-testid="passkey-flow"]` : ""
|
||||
} input[name="traits.email"]`
|
||||
|
||||
cy.get(emailTrait).type(email)
|
||||
cy.get(websiteTrait).type("https://www.ory.sh")
|
||||
cy.get('[name="passkey_register_trigger"]').click()
|
||||
|
||||
cy.wait(1000)
|
||||
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["passkey"],
|
||||
}).then((session) => {
|
||||
expect(session.identity.traits.email).to.equal(email)
|
||||
expect(session.identity.traits.website).to.equal("https://www.ory.sh")
|
||||
})
|
||||
}
|
||||
|
||||
context("Passkey registration", () => {
|
||||
before(() => {
|
||||
cy.task("resetCRI", {})
|
||||
})
|
||||
after(() => {
|
||||
cy.task("resetCRI", {})
|
||||
})
|
||||
;[
|
||||
{
|
||||
login: react.login,
|
||||
registration: express.registration,
|
||||
settings: react.settings,
|
||||
base: react.base,
|
||||
app: "react" as "react",
|
||||
profile: "passkey",
|
||||
},
|
||||
{
|
||||
login: express.login,
|
||||
registration: express.registration,
|
||||
settings: express.settings,
|
||||
base: express.base,
|
||||
app: "express" as "express",
|
||||
profile: "passkey",
|
||||
},
|
||||
].forEach(({ registration, login, profile, app, base, settings }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
let authenticator: any
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.enable",
|
||||
opts: {},
|
||||
})
|
||||
.then(() => {
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.addVirtualAuthenticator",
|
||||
opts: {
|
||||
options: {
|
||||
protocol: "ctap2",
|
||||
transport: "internal",
|
||||
hasResidentKey: true,
|
||||
hasUserVerification: true,
|
||||
isUserVerified: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
.then((result) => {
|
||||
authenticator = result
|
||||
cy.log("authenticator ID:", authenticator)
|
||||
})
|
||||
|
||||
cy.longPrivilegedSessionTime()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.clearCredentials",
|
||||
opts: authenticator,
|
||||
})
|
||||
})
|
||||
|
||||
after(() => {
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.removeVirtualAuthenticator",
|
||||
opts: authenticator,
|
||||
})
|
||||
})
|
||||
|
||||
it("should register after validation errors", () => {
|
||||
cy.visit(registration)
|
||||
|
||||
// the browser will prevent the form from being submitted if the input field is required
|
||||
// we should remove the required attribute to simulate the data not being sent
|
||||
cy.removeAttribute(
|
||||
['input[name="traits.email"]', 'input[name="traits.website"]'],
|
||||
"required",
|
||||
)
|
||||
|
||||
cy.get(`input[name="traits.website"]`).then(($el) => {
|
||||
$el.removeAttr("type")
|
||||
})
|
||||
|
||||
const websiteTrait = `${
|
||||
app === "express" ? `form[data-testid="passkey-flow"]` : ""
|
||||
} input[name="traits.website"]`
|
||||
|
||||
const emailTrait = `${
|
||||
app === "express" ? `form[data-testid="passkey-flow"]` : ""
|
||||
} input[name="traits.email"]`
|
||||
|
||||
cy.get(websiteTrait).type("b")
|
||||
cy.get('[name="passkey_register_trigger"]').click()
|
||||
|
||||
cy.get('[data-testid="ui/message/4000002"]').should("to.exist")
|
||||
cy.get('[data-testid="ui/message/4000001"]').should("to.exist")
|
||||
cy.get(websiteTrait).should("have.value", "b")
|
||||
|
||||
const email = gen.email()
|
||||
cy.get(emailTrait).type(email)
|
||||
cy.get('[name="passkey_register_trigger"]').click()
|
||||
|
||||
cy.wait(1000)
|
||||
|
||||
cy.get('[data-testid="ui/message/4000001"]').should("to.exist")
|
||||
cy.get(websiteTrait).should("have.value", "b")
|
||||
cy.get(emailTrait).should("have.value", email)
|
||||
cy.get(websiteTrait).clear()
|
||||
cy.get(websiteTrait).type("https://www.ory.sh")
|
||||
|
||||
cy.get('[name="passkey_register_trigger"]').click()
|
||||
|
||||
cy.wait(1000)
|
||||
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["passkey"],
|
||||
}).then((session) => {
|
||||
expect(session.identity.traits.email).to.equal(email)
|
||||
expect(session.identity.traits.website).to.equal("https://www.ory.sh")
|
||||
})
|
||||
})
|
||||
|
||||
it("should pass transient_payload to webhook", () => {
|
||||
testFlowWebhook(
|
||||
(hooks) =>
|
||||
cy.setupHooks("registration", "after", "passkey", [
|
||||
...hooks,
|
||||
{ hook: "session" },
|
||||
]),
|
||||
() => {
|
||||
signup(registration, app)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it("should be able to login with registered account", () => {
|
||||
const email = gen.email()
|
||||
|
||||
signup(registration, app, email)
|
||||
cy.logout()
|
||||
cy.visit(login)
|
||||
|
||||
cy.get('[name="passkey_login_trigger"]').click()
|
||||
cy.wait(1000)
|
||||
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["passkey"],
|
||||
}).then((session) => {
|
||||
expect(session.identity.traits.email).to.equal(email)
|
||||
expect(session.identity.traits.website).to.equal("https://www.ory.sh")
|
||||
})
|
||||
})
|
||||
|
||||
it("should not be able to unlink last passkey", () => {
|
||||
const email = gen.email()
|
||||
signup(registration, app, email)
|
||||
cy.visit(settings)
|
||||
cy.get('[name="passkey_remove"]').should("have.attr", "disabled")
|
||||
})
|
||||
|
||||
it("should be able to link password and use both methods for sign in", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
signup(registration, app, email)
|
||||
cy.visit(settings)
|
||||
cy.get('[name="passkey_remove"]').should("have.attr", "disabled")
|
||||
cy.get('[name="password"]').type(password)
|
||||
cy.get('[value="password"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
cy.get('[name="passkey_remove"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
cy.logout()
|
||||
cy.visit(login)
|
||||
|
||||
cy.get('[name="identifier"]').type(email)
|
||||
cy.get('[name="password"]').type(password)
|
||||
cy.get('[name="method"][value="password"]').click()
|
||||
})
|
||||
|
||||
it("should be able to refresh", () => {
|
||||
const email = gen.email()
|
||||
signup(registration, app, email)
|
||||
cy.visit(login + "?refresh=true")
|
||||
cy.get('[name="identifier"][type="hidden"]').should("exist")
|
||||
cy.get('[name="identifier"][type="input"]').should("not.exist")
|
||||
cy.get('[name="password"]').should("not.exist")
|
||||
cy.get('[value="password"]').should("not.exist")
|
||||
cy.get('[name="passkey_login_trigger"]').click()
|
||||
cy.wait(1000)
|
||||
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["passkey", "passkey"],
|
||||
}).then((session) => {
|
||||
expect(session.identity.traits.email).to.equal(email)
|
||||
expect(session.identity.traits.website).to.equal("https://www.ory.sh")
|
||||
})
|
||||
})
|
||||
|
||||
it("should not be able to use for MFA", () => {
|
||||
const email = gen.email()
|
||||
signup(registration, app, email)
|
||||
cy.visit(login + "?aal=aal2")
|
||||
cy.get('[value="passkey"]').should("not.exist")
|
||||
cy.get('[name="passkey_login_trigger"]').should("not.exist")
|
||||
})
|
||||
|
||||
it("should be able to add method later and try a variety of refresh flows", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
cy.visit(registration)
|
||||
|
||||
const emailTrait = `${
|
||||
app === "express" ? `[data-testid="registration-flow"]` : ""
|
||||
} [name="traits.email"]`
|
||||
const websiteTrait = `${
|
||||
app === "express" ? `[data-testid="registration-flow"]` : ""
|
||||
} [name="traits.website"]`
|
||||
|
||||
cy.get(emailTrait).type(email)
|
||||
cy.get('[name="password"]').type(password)
|
||||
cy.get(websiteTrait).type("https://www.ory.sh")
|
||||
cy.get('[value="password"]').click()
|
||||
cy.location("pathname").should("not.contain", "/registration")
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["password"],
|
||||
})
|
||||
|
||||
cy.visit(settings)
|
||||
cy.get('[name="passkey_register_trigger"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
cy.visit(login + "?refresh=true")
|
||||
cy.get('[name="password"]').should("exist")
|
||||
cy.get('[name="passkey_login_trigger"]').click()
|
||||
cy.wait(1000)
|
||||
cy.location("pathname").should("not.contain", "/login")
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["password", "passkey", "passkey"],
|
||||
})
|
||||
|
||||
cy.visit(login + "?refresh=true")
|
||||
cy.get('[name="password"]').type(password)
|
||||
cy.get('[value="password"]').click()
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["password", "passkey", "passkey", "password"],
|
||||
})
|
||||
|
||||
cy.logout()
|
||||
cy.visit(login)
|
||||
|
||||
cy.get('[name="passkey_login_trigger"]').click()
|
||||
cy.wait(1000)
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["passkey"],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
359
test/e2e/cypress/integration/profiles/passwordless/flows.spec.ts
Normal file
359
test/e2e/cypress/integration/profiles/passwordless/flows.spec.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
// 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 { testFlowWebhook } from "../../../helpers/webhook"
|
||||
|
||||
const signup = (registration: string, app: string, email = gen.email()) => {
|
||||
cy.visit(registration)
|
||||
|
||||
const emailTrait = `${
|
||||
app === "express" ? '[data-testid="passwordless-flow"]' : ""
|
||||
} [name="traits.email"]`
|
||||
const websiteTrait = `${
|
||||
app === "express" ? '[data-testid="passwordless-flow"]' : ""
|
||||
} [name="traits.website"]`
|
||||
|
||||
cy.get('[name="webauthn_register_displayname"]').type("key1")
|
||||
cy.get(emailTrait).type(email)
|
||||
cy.get(websiteTrait).type("https://www.ory.sh")
|
||||
cy.clickWebAuthButton("register")
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["webauthn"],
|
||||
}).then((session) => {
|
||||
expect(session.identity.traits.email).to.equal(email)
|
||||
expect(session.identity.traits.website).to.equal("https://www.ory.sh")
|
||||
})
|
||||
}
|
||||
|
||||
context("Passwordless registration", () => {
|
||||
before(() => {
|
||||
cy.task("resetCRI", {})
|
||||
})
|
||||
after(() => {
|
||||
cy.task("resetCRI", {})
|
||||
})
|
||||
;[
|
||||
{
|
||||
login: react.login,
|
||||
registration: express.registration,
|
||||
settings: react.settings,
|
||||
base: react.base,
|
||||
app: "react" as "react",
|
||||
profile: "passwordless",
|
||||
},
|
||||
{
|
||||
login: express.login,
|
||||
registration: express.registration,
|
||||
settings: express.settings,
|
||||
base: express.base,
|
||||
app: "express" as "express",
|
||||
profile: "passwordless",
|
||||
},
|
||||
].forEach(({ registration, login, profile, app, base, settings }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
let authenticator
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
cy.addVirtualAuthenticator().then((result) => {
|
||||
authenticator = result
|
||||
})
|
||||
cy.longPrivilegedSessionTime()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
})
|
||||
|
||||
after(() => {
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.removeVirtualAuthenticator",
|
||||
opts: authenticator,
|
||||
})
|
||||
})
|
||||
|
||||
it("should register after validation errors", () => {
|
||||
cy.visit(registration)
|
||||
|
||||
// the browser will prevent the form from being submitted if the input field is required
|
||||
// we should remove the required attribute to simulate the data not being sent
|
||||
cy.removeAttribute(
|
||||
['input[name="traits.email"]', 'input[name="traits.website"]'],
|
||||
"required",
|
||||
)
|
||||
|
||||
cy.get(`input[name="traits.website"]`).then(($el) => {
|
||||
$el.removeAttr("type")
|
||||
})
|
||||
|
||||
const websiteTrait = `${
|
||||
app === "express" ? `[data-testid="passwordless-flow"]` : ""
|
||||
} [name="traits.website"]`
|
||||
|
||||
const emailTrait = `${
|
||||
app === "express" ? `[data-testid="passwordless-flow"]` : ""
|
||||
} [name="traits.email"]`
|
||||
|
||||
cy.get(appPrefix(app) + '[name="webauthn_register_displayname"]').type(
|
||||
"key1",
|
||||
)
|
||||
cy.get(websiteTrait).type("b")
|
||||
cy.clickWebAuthButton("register")
|
||||
|
||||
cy.get('[data-testid="ui/message/4000002"]').should("to.exist")
|
||||
cy.get('[data-testid="ui/message/4000001"]').should("to.exist")
|
||||
cy.get(websiteTrait).should("have.value", "b")
|
||||
|
||||
const email = gen.email()
|
||||
cy.get(emailTrait).type(email)
|
||||
cy.clickWebAuthButton("register")
|
||||
|
||||
cy.get('[data-testid="ui/message/4000001"]').should("to.exist")
|
||||
cy.get(websiteTrait).should("have.value", "b")
|
||||
cy.get(emailTrait).should("have.value", email)
|
||||
cy.get(websiteTrait).clear()
|
||||
cy.get(websiteTrait).type("https://www.ory.sh")
|
||||
cy.clickWebAuthButton("register")
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["webauthn"],
|
||||
}).then((session) => {
|
||||
expect(session.identity.traits.email).to.equal(email)
|
||||
expect(session.identity.traits.website).to.equal("https://www.ory.sh")
|
||||
})
|
||||
})
|
||||
|
||||
it("should pass transient_payload to webhook", () => {
|
||||
testFlowWebhook(
|
||||
(hooks) =>
|
||||
cy.setupHooks("registration", "after", "webauthn", [
|
||||
...hooks,
|
||||
{ hook: "session" },
|
||||
]),
|
||||
() => {
|
||||
signup(registration, app)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
// I have no idea why this does not work in an E2E test. It works just fine manually.
|
||||
xit("should use webauthn credential as passkey", () => {
|
||||
const email = gen.email()
|
||||
|
||||
signup(registration, app, email)
|
||||
cy.logout()
|
||||
cy.visit(login)
|
||||
|
||||
cy.get('[name="passkey_login_trigger"]').click()
|
||||
cy.wait(1000)
|
||||
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["passkey"],
|
||||
}).then((session) => {
|
||||
expect(session.identity.traits.email).to.equal(email)
|
||||
expect(session.identity.traits.website).to.equal("https://www.ory.sh")
|
||||
})
|
||||
})
|
||||
|
||||
it("should be able to login with registered account", () => {
|
||||
const email = gen.email()
|
||||
|
||||
signup(registration, app, email)
|
||||
cy.logout()
|
||||
cy.visit(login)
|
||||
|
||||
const identifierTrait = `${
|
||||
app === "express" ? `[data-testid="passwordless-flow"]` : ""
|
||||
} [name="identifier"]`
|
||||
|
||||
cy.get(identifierTrait).type(email)
|
||||
cy.get('[value="webauthn"]').click()
|
||||
cy.get('[data-testid="ui/message/1010012"]').should("to.exist")
|
||||
cy.get('[name="password"]').should("to.not.exist")
|
||||
cy.clickWebAuthButton("login")
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["webauthn"],
|
||||
}).then((session) => {
|
||||
expect(session.identity.traits.email).to.equal(email)
|
||||
expect(session.identity.traits.website).to.equal("https://www.ory.sh")
|
||||
})
|
||||
})
|
||||
|
||||
it("should not be able to unlink last security key", () => {
|
||||
const email = gen.email()
|
||||
signup(registration, app, email)
|
||||
cy.visit(settings)
|
||||
cy.get('[name="webauthn_remove"]').should("be.disabled")
|
||||
})
|
||||
|
||||
it("should be able to link password and use both methods for sign in", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
signup(registration, app, email)
|
||||
cy.visit(settings)
|
||||
cy.get('[name="webauthn_remove"]').should("be.disabled")
|
||||
cy.get('[name="password"]').type(password)
|
||||
cy.get('[value="password"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
cy.get('[name="webauthn_remove"]').click()
|
||||
cy.expectSettingsSaved()
|
||||
cy.logout()
|
||||
cy.visit(login)
|
||||
|
||||
const identifierTrait = `${
|
||||
app === "express" ? `[data-testid="passwordless-flow"]` : ""
|
||||
} [name="identifier"]`
|
||||
|
||||
cy.get(identifierTrait).type(email)
|
||||
cy.get('[value="webauthn"]').click()
|
||||
cy.get('[data-testid="ui/message/4000015"]').should("to.exist")
|
||||
cy.get(identifierTrait).should("exist")
|
||||
cy.get('[name="password"]').should("exist")
|
||||
cy.get('[value="password"]').should("exist")
|
||||
})
|
||||
|
||||
it("should be able to refresh", () => {
|
||||
const email = gen.email()
|
||||
signup(registration, app, email)
|
||||
cy.visit(login + "?refresh=true")
|
||||
cy.get('[name="identifier"][type="hidden"]').should("exist")
|
||||
cy.get('[name="identifier"][type="input"]').should("not.exist")
|
||||
cy.get('[name="password"]').should("not.exist")
|
||||
cy.get('[value="password"]').should("not.exist")
|
||||
cy.clickWebAuthButton("login")
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["webauthn", "webauthn"],
|
||||
}).then((session) => {
|
||||
expect(session.identity.traits.email).to.equal(email)
|
||||
expect(session.identity.traits.website).to.equal("https://www.ory.sh")
|
||||
})
|
||||
})
|
||||
|
||||
it("should not be able to use for MFA", () => {
|
||||
const email = gen.email()
|
||||
signup(registration, app, email)
|
||||
cy.visit(login + "?aal=aal2")
|
||||
cy.get('[value="webauthn"]').should("not.exist")
|
||||
cy.get('[name="webauthn_login_trigger"]').should("not.exist")
|
||||
})
|
||||
|
||||
it("should be able to add method later and try a variety of refresh flows", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
cy.visit(registration)
|
||||
|
||||
const emailTrait = `${
|
||||
app === "express" ? `[data-testid="registration-flow"]` : ""
|
||||
} [name="traits.email"]`
|
||||
const websiteTrait = `${
|
||||
app === "express" ? `[data-testid="registration-flow"]` : ""
|
||||
} [name="traits.website"]`
|
||||
|
||||
cy.get(emailTrait).type(email)
|
||||
cy.get('[name="password"]').type(password)
|
||||
cy.get(websiteTrait).type("https://www.ory.sh")
|
||||
cy.get('[value="password"]').click()
|
||||
cy.location("pathname").should("not.contain", "/registration")
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["password"],
|
||||
})
|
||||
|
||||
cy.visit(settings)
|
||||
cy.get('[name="webauthn_register_displayname"]').type("key2")
|
||||
cy.clickWebAuthButton("register")
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
cy.visit(login + "?refresh=true")
|
||||
cy.get('[name="password"]').should("exist")
|
||||
cy.clickWebAuthButton("login")
|
||||
cy.location("pathname").should("not.contain", "/login")
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["password", "webauthn", "webauthn"],
|
||||
})
|
||||
|
||||
cy.visit(login + "?refresh=true")
|
||||
cy.get('[name="password"]').type(password)
|
||||
cy.get('[value="password"]').click()
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["password", "webauthn", "webauthn", "password"],
|
||||
})
|
||||
|
||||
cy.logout()
|
||||
cy.visit(login)
|
||||
|
||||
const identifierTrait = `${
|
||||
app === "express" ? `[data-testid="passwordless-flow"]` : ""
|
||||
} [name="identifier"]`
|
||||
|
||||
cy.get(identifierTrait).type(email)
|
||||
cy.get('[value="webauthn"]').click()
|
||||
cy.clickWebAuthButton("login")
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["webauthn"],
|
||||
})
|
||||
})
|
||||
|
||||
it("should not be able to use for MFA even when passwordless is false", () => {
|
||||
const email = gen.email()
|
||||
// Ensure passwordless is enabled for the initial sign-up, even if a
|
||||
// previous (retried) attempt left it disabled below.
|
||||
cy.updateConfigFile((config) => {
|
||||
config.selfservice.methods.webauthn.config.passwordless = true
|
||||
return config
|
||||
})
|
||||
signup(registration, app, email)
|
||||
cy.updateConfigFile((config) => {
|
||||
config.selfservice.methods.webauthn.config.passwordless = false
|
||||
return config
|
||||
})
|
||||
cy.visit(login + "?aal=aal2")
|
||||
cy.get('[value="webauthn"]').should("not.exist")
|
||||
cy.get('[name="webauthn_login_trigger"]').should("not.exist")
|
||||
|
||||
cy.visit(settings)
|
||||
cy.get('[name="webauthn_remove"]').should("be.disabled")
|
||||
|
||||
// The settings flow excludes every already-registered credential
|
||||
// (including the passwordless one), so the new MFA credential must be
|
||||
// created on a different authenticator. Swap the passwordless
|
||||
// authenticator out entirely so only the new one is attached: with both
|
||||
// present, create() races the excluded authenticator's
|
||||
// InvalidStateError against the new authenticator, and the second-factor
|
||||
// login (whose allowCredentials omits the passwordless credential) can
|
||||
// resolve against the wrong device.
|
||||
cy.task("sendCRI", {
|
||||
query: "WebAuthn.removeVirtualAuthenticator",
|
||||
opts: authenticator,
|
||||
})
|
||||
cy.addVirtualAuthenticator().then((secondAuthenticator) => {
|
||||
// Keep the handle valid for the after() cleanup hook.
|
||||
cy.then(() => {
|
||||
authenticator = secondAuthenticator
|
||||
})
|
||||
|
||||
cy.get('[name="webauthn_register_displayname"]').type("key2")
|
||||
cy.clickWebAuthButton("register")
|
||||
cy.expectSettingsSaved()
|
||||
|
||||
cy.visit(login + "?aal=aal2&refresh=true")
|
||||
cy.clickWebAuthButton("login")
|
||||
cy.getSession({
|
||||
expectAal: "aal2",
|
||||
expectMethods: ["webauthn", "webauthn", "webauthn"],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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.
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,382 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Session } from "@ory/kratos-client"
|
||||
import { gen, MOBILE_URL } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
const Selectors = {
|
||||
mobile: {
|
||||
identifier: "[data-testid='field/identifier']",
|
||||
recoveryEmail: "[data-testid='field/email']",
|
||||
email: "[data-testid='traits.email']",
|
||||
email2: "[data-testid='traits.email2']",
|
||||
website: "[data-testid='traits.website']",
|
||||
username: "[data-testid='traits.username']",
|
||||
code: "[data-testid='field/code'] input",
|
||||
recoveryCode: "[data-testid='code']",
|
||||
submitCode: "[data-testid='field/method/code']",
|
||||
resendCode: "[data-testid='field/resend/code']",
|
||||
credentialSelection: "[data-testid='field/screen/credential-selection']",
|
||||
submitRecovery: "[data-testid='field/method/code']",
|
||||
codeHiddenMethod: "[data-testid='field/method/code']",
|
||||
},
|
||||
express: {
|
||||
identifier: "[data-testid='login-flow-code'] input[name='identifier']",
|
||||
recoveryEmail: "input[name=email]",
|
||||
email: "[data-testid='node/input/traits.email'] input[name='traits.email']",
|
||||
email2:
|
||||
"[data-testid='node/input/traits.email2'] input[name='traits.email2']",
|
||||
website:
|
||||
"[data-testid='node/input/traits.website'] [name='traits.website']",
|
||||
username:
|
||||
"[data-testid='node/input/traits.username'] input[name='traits.username']",
|
||||
code: "input[name='code']",
|
||||
recoveryCode: "input[name=code]",
|
||||
submitRecovery: "button[name=method][value=code]",
|
||||
submitCode: "button[name='method'][value='code']",
|
||||
resendCode: "button[name='resend'][value='code']",
|
||||
codeHiddenMethod: "input[name='method'][value='code'][type='hidden']",
|
||||
credentialSelection: "[name='screen'][value='credential-selection']",
|
||||
},
|
||||
react: {
|
||||
identifier: "input[name='identifier']",
|
||||
recoveryEmail: "input[name=email]",
|
||||
email: "input[name='traits.email']",
|
||||
email2: "input[name='traits.email2']",
|
||||
website: "[name='traits.website']",
|
||||
username: "input[name='traits.username']",
|
||||
code: "input[name='code']",
|
||||
recoveryCode: "input[name=code]",
|
||||
submitRecovery: "button[name=method][value=code]",
|
||||
submitCode: "button[name='method'][value='code']",
|
||||
resendCode: "button[name='resend'][value='code']",
|
||||
codeHiddenMethod: "input[name='method'][value='code'][type='hidden']",
|
||||
credentialSelection: "[name='screen'][value='credential-selection']",
|
||||
},
|
||||
}
|
||||
|
||||
context("Registration success with code method", () => {
|
||||
;[
|
||||
{
|
||||
route: express.registration,
|
||||
login: express.login,
|
||||
recovery: express.recovery,
|
||||
app: "express" as "express",
|
||||
profile: "two-steps",
|
||||
},
|
||||
{
|
||||
route: react.registration,
|
||||
login: react.login,
|
||||
recovery: react.recovery,
|
||||
app: "react" as "react",
|
||||
profile: "two-steps",
|
||||
},
|
||||
{
|
||||
route: MOBILE_URL + "/Registration",
|
||||
login: MOBILE_URL + "/Login",
|
||||
recovery: MOBILE_URL + "/Recovery",
|
||||
app: "mobile" as "mobile",
|
||||
profile: "two-steps",
|
||||
},
|
||||
].forEach(({ route, login, recovery, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
if (app !== "mobile") {
|
||||
cy.proxy(app)
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.deleteMail({ atLeast: 0 })
|
||||
cy.clearAllCookies()
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
it("should be able to resend the registration code", () => {
|
||||
const email = gen.email()
|
||||
const website = "https://www.example.org/"
|
||||
|
||||
cy.get(Selectors[app]["email"]).type(email)
|
||||
cy.get(Selectors[app]["website"]).type(website)
|
||||
|
||||
cy.submitProfileForm(app)
|
||||
cy.submitCodeForm(app)
|
||||
cy.get('[data-testid="ui/message/1040005"]').should("be.visible")
|
||||
|
||||
cy.getRegistrationCodeFromEmail(email).then((code) =>
|
||||
cy.wrap(code).as("code1"),
|
||||
)
|
||||
|
||||
// cy.get(Selectors[app]["email"]).should("have.value", email)
|
||||
cy.get(Selectors[app]["codeHiddenMethod"]).should("exist")
|
||||
cy.get(Selectors[app]["resendCode"]).click()
|
||||
|
||||
cy.getRegistrationCodeFromEmail(email).then((code) => {
|
||||
cy.wrap(code).as("code2")
|
||||
})
|
||||
|
||||
cy.get("@code1").then((code1) => {
|
||||
// previous code should not work
|
||||
cy.get(Selectors[app]["code"]).clear()
|
||||
cy.get(Selectors[app]["code"]).type(code1.toString())
|
||||
|
||||
cy.submitCodeForm(app)
|
||||
cy.get('[data-testid="ui/message/4040003"]').should(
|
||||
"contain.text",
|
||||
"The registration code is invalid or has already been used. Please try again.",
|
||||
)
|
||||
})
|
||||
|
||||
// Navigate back and forth again
|
||||
cy.get(Selectors[app]["credentialSelection"]).click()
|
||||
cy.submitCodeForm(app)
|
||||
|
||||
cy.getRegistrationCodeFromEmail(email).then((code) => {
|
||||
cy.wrap(code).as("code2")
|
||||
})
|
||||
|
||||
cy.get("@code2").then((code2) => {
|
||||
cy.get(Selectors[app]["code"]).clear()
|
||||
cy.get(Selectors[app]["code"]).type(code2.toString())
|
||||
cy.submitCodeForm(app)
|
||||
})
|
||||
|
||||
if (app === "express") {
|
||||
cy.url().should("match", /\/welcome/)
|
||||
} else {
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
}
|
||||
|
||||
if (app === "mobile") {
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
cy.get('[data-testid="session-token"]').then((token) => {
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["code"],
|
||||
token: token.text(),
|
||||
}).then((session) => {
|
||||
cy.wrap(session).as("session")
|
||||
})
|
||||
})
|
||||
} else {
|
||||
cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then(
|
||||
(session) => {
|
||||
cy.wrap(session).as("session")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
cy.get<Session>("@session").then(({ identity }) => {
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should sign up and be logged in with session hook", () => {
|
||||
if (app === "react") {
|
||||
// This test is flaky on React, so we skip it for now.
|
||||
return
|
||||
}
|
||||
|
||||
const email = gen.email()
|
||||
const website = "https://www.example.org/"
|
||||
|
||||
cy.get(Selectors[app]["email"]).type(email)
|
||||
cy.get(Selectors[app]["website"]).type(website)
|
||||
cy.submitProfileForm(app)
|
||||
|
||||
cy.submitCodeForm(app)
|
||||
cy.get('[data-testid="ui/message/1040005"]').should("be.visible")
|
||||
|
||||
cy.getRegistrationCodeFromEmail(email).then((code) => {
|
||||
cy.get(Selectors[app]["code"]).type(code)
|
||||
cy.get(Selectors[app]["submitCode"]).click()
|
||||
})
|
||||
|
||||
if (app === "mobile") {
|
||||
cy.get('[data-testid="session-token"]').then((token) => {
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["code"],
|
||||
token: token.text(),
|
||||
}).then((session) => {
|
||||
cy.wrap(session).as("session")
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
} else {
|
||||
cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then(
|
||||
(session) => {
|
||||
cy.wrap(session).as("session")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
cy.get<Session>("@session").then(({ identity }) => {
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should be able to sign up without session hook", () => {
|
||||
if (app === "react") {
|
||||
// This test is flaky on React, so we skip it for now.
|
||||
return
|
||||
}
|
||||
|
||||
cy.setPostCodeRegistrationHooks([])
|
||||
const email = gen.email()
|
||||
const website = "https://www.example.org/"
|
||||
|
||||
cy.get(Selectors[app]["email"]).type(email)
|
||||
cy.get(Selectors[app]["website"]).type(website)
|
||||
cy.submitProfileForm(app)
|
||||
|
||||
cy.submitCodeForm(app)
|
||||
cy.get('[data-testid="ui/message/1040005"]').should("be.visible")
|
||||
|
||||
cy.getRegistrationCodeFromEmail(email).then((code) => {
|
||||
cy.get(Selectors[app]["code"]).type(code)
|
||||
cy.get(Selectors[app]["submitCode"]).click()
|
||||
})
|
||||
|
||||
cy.visit(login)
|
||||
cy.get(Selectors[app]["identifier"]).type(email)
|
||||
cy.get(Selectors[app]["submitCode"]).click()
|
||||
|
||||
cy.getLoginCodeFromEmail(email).then((code) => {
|
||||
cy.get(Selectors[app]["code"]).type(code)
|
||||
cy.get(Selectors[app]["submitCode"]).click()
|
||||
})
|
||||
|
||||
if (app === "mobile") {
|
||||
cy.get('[data-testid="session-token"]').then((token) => {
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["code"],
|
||||
token: token.text(),
|
||||
}).then((session) => {
|
||||
cy.wrap(session).as("session")
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
} else {
|
||||
cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then(
|
||||
(session) => {
|
||||
cy.wrap(session).as("session")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
cy.get<Session>("@session").then(({ identity }) => {
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
|
||||
// Try keep this test as the last one, as it updates the identity schema.
|
||||
it("should be able to use multiple identifiers to signup with and sign in to", () => {
|
||||
cy.setPostCodeRegistrationHooks([
|
||||
{
|
||||
hook: "session",
|
||||
},
|
||||
])
|
||||
|
||||
// Setup complex schema
|
||||
cy.setIdentitySchema(
|
||||
"file://test/e2e/profiles/code/identity.complex.traits.schema.json",
|
||||
)
|
||||
|
||||
cy.visit(route)
|
||||
|
||||
cy.get(Selectors[app]["username"]).type(Math.random().toString(36))
|
||||
|
||||
const email = gen.email()
|
||||
cy.get(Selectors[app]["email"]).type(email)
|
||||
|
||||
const email2 = gen.email()
|
||||
cy.get(Selectors[app]["email2"]).type(email2)
|
||||
|
||||
cy.submitProfileForm(app)
|
||||
cy.submitCodeForm(app)
|
||||
cy.get('[data-testid="ui/message/1040005"]').should("be.visible")
|
||||
|
||||
// intentionally use email 1 to sign up for the account
|
||||
cy.getRegistrationCodeFromEmail(email, { expectedCount: 1 }).then(
|
||||
(code) => {
|
||||
cy.get(Selectors[app]["code"]).type(code)
|
||||
cy.get(Selectors[app]["submitCode"]).click()
|
||||
},
|
||||
)
|
||||
|
||||
if (app === "mobile") {
|
||||
cy.visit(MOBILE_URL + "/Home")
|
||||
cy.get('*[data-testid="logout"]').click()
|
||||
} else {
|
||||
cy.logout()
|
||||
}
|
||||
|
||||
// There are verification emails from the registration process in the inbox that we need to deleted
|
||||
// for the assertions below to pass.
|
||||
cy.deleteMail({ atLeast: 1 })
|
||||
|
||||
// Attempt to sign in with email 2 (should fail)
|
||||
cy.visit(login)
|
||||
cy.get(Selectors[app]["identifier"]).type(email2)
|
||||
|
||||
cy.get(Selectors[app]["submitCode"]).click()
|
||||
|
||||
cy.getLoginCodeFromEmail(email2, {
|
||||
expectedCount: 1,
|
||||
}).then((code) => {
|
||||
cy.get(Selectors[app]["code"]).type(code)
|
||||
cy.get(Selectors[app]["submitCode"]).click()
|
||||
})
|
||||
|
||||
if (app === "mobile") {
|
||||
cy.get('[data-testid="session-token"]').then((token) => {
|
||||
cy.getSession({
|
||||
expectAal: "aal1",
|
||||
expectMethods: ["code"],
|
||||
token: token.text(),
|
||||
}).then((session) => {
|
||||
cy.wrap(session).as("session")
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('[data-testid="session-content"]').should("contain", email)
|
||||
cy.get('[data-testid="session-token"]').should("not.be.empty")
|
||||
} else {
|
||||
cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then(
|
||||
(session) => {
|
||||
cy.wrap(session).as("session")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
cy.get<Session>("@session").then(({ identity }) => {
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.verifiable_addresses).to.have.length(2)
|
||||
expect(
|
||||
identity.verifiable_addresses.filter((v) => v.value === email)[0]
|
||||
.status,
|
||||
).to.equal("completed")
|
||||
expect(
|
||||
identity.verifiable_addresses.filter((v) => v.value === email2)[0]
|
||||
.status,
|
||||
).to.equal("completed")
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,225 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, gen, website } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
import { testFlowWebhook } from "../../../../helpers/webhook"
|
||||
|
||||
context("Social Sign Up Successes", () => {
|
||||
;[
|
||||
{
|
||||
login: react.login,
|
||||
registration: react.registration,
|
||||
app: "react" as "react",
|
||||
profile: "two-steps",
|
||||
},
|
||||
{
|
||||
login: express.login,
|
||||
registration: express.registration,
|
||||
app: "express" as "express",
|
||||
profile: "two-steps",
|
||||
},
|
||||
].forEach(({ registration, login, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
cy.visit(registration)
|
||||
cy.setIdentitySchema(
|
||||
"file://test/e2e/profiles/oidc/identity.traits.schema.json",
|
||||
)
|
||||
})
|
||||
|
||||
const shouldSession = (email) => (session) => {
|
||||
const { identity } = session
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.traits.website).to.equal(website)
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
}
|
||||
|
||||
it("should be able to sign up with incomplete data and finally be signed in", () => {
|
||||
const email = gen.email()
|
||||
|
||||
cy.registerOidc({
|
||||
app,
|
||||
email,
|
||||
expectSession: false,
|
||||
route: registration,
|
||||
})
|
||||
|
||||
cy.get("#registration-password").should("not.exist")
|
||||
cy.get(appPrefix(app) + '[name="traits.email"]').should(
|
||||
"have.value",
|
||||
email,
|
||||
)
|
||||
cy.get('[data-testid="ui/message/4000002"]').should(
|
||||
"contain.text",
|
||||
"Property website is missing",
|
||||
)
|
||||
|
||||
cy.get('[name="traits.consent"][type="checkbox"]')
|
||||
.siblings("label")
|
||||
.click()
|
||||
cy.get('[name="traits.newsletter"][type="checkbox"]')
|
||||
.siblings("label")
|
||||
.click()
|
||||
cy.get('[name="traits.website"]').type("http://s")
|
||||
|
||||
cy.get('[name="provider"]')
|
||||
.should("have.length", 1)
|
||||
.should("have.value", "hydra")
|
||||
.should("contain.text", "Continue")
|
||||
.click()
|
||||
|
||||
cy.get("#registration-password").should("not.exist")
|
||||
cy.get('[name="traits.email"]').should("have.value", email)
|
||||
cy.get('[name="traits.website"]').should("have.value", "http://s")
|
||||
cy.get('[data-testid="ui/message/4000003"]').should(
|
||||
"contain.text",
|
||||
"length must be >= 10",
|
||||
)
|
||||
cy.get('[name="traits.website"]')
|
||||
.should("have.value", "http://s")
|
||||
.clear()
|
||||
.type(website)
|
||||
|
||||
cy.get('[name="traits.consent"]').should("be.checked")
|
||||
cy.get('[name="traits.newsletter"]').should("be.checked")
|
||||
|
||||
cy.triggerOidc(app)
|
||||
|
||||
cy.location("pathname").should((loc) => {
|
||||
expect(loc).to.be.oneOf(["/welcome", "/", "/sessions"])
|
||||
})
|
||||
|
||||
cy.getSession().should((session) => {
|
||||
shouldSession(email)(session)
|
||||
expect(session.identity.traits.consent).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
it("should pass transient_payload to webhook", () => {
|
||||
testFlowWebhook(
|
||||
(hooks) =>
|
||||
cy.setupHooks("registration", "after", "oidc", [
|
||||
...hooks,
|
||||
{ hook: "session" },
|
||||
]),
|
||||
() => {
|
||||
const email = gen.email()
|
||||
cy.registerOidc({
|
||||
app,
|
||||
email,
|
||||
website,
|
||||
route: registration,
|
||||
})
|
||||
cy.getSession().should(shouldSession(email))
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it("should be able to sign up with complete data", () => {
|
||||
const email = gen.email()
|
||||
|
||||
cy.registerOidc({ app, email, website, route: registration })
|
||||
cy.getSession().should(shouldSession(email))
|
||||
})
|
||||
|
||||
it("should be able to convert a sign up flow to a sign in flow", () => {
|
||||
const email = gen.email()
|
||||
|
||||
cy.registerOidc({ app, email, website, route: registration })
|
||||
cy.logout()
|
||||
cy.noSession()
|
||||
cy.visit(registration)
|
||||
cy.triggerOidc(app)
|
||||
|
||||
cy.location("pathname").should((path) => {
|
||||
expect(path).to.oneOf(["/", "/welcome", "/sessions"])
|
||||
})
|
||||
|
||||
cy.getSession().should(shouldSession(email))
|
||||
})
|
||||
|
||||
it("should be able to convert a sign in flow to a sign up flow", () => {
|
||||
cy.setIdentitySchema(
|
||||
"file://test/e2e/profiles/oidc/identity-required.traits.schema.json",
|
||||
)
|
||||
|
||||
const email = gen.email()
|
||||
cy.visit(login)
|
||||
cy.triggerOidc(app)
|
||||
|
||||
cy.get("#username").clear().type(email)
|
||||
cy.get("#remember").click()
|
||||
cy.get("#accept").click()
|
||||
cy.get('[name="scope"]').each(($el) => cy.wrap($el).click())
|
||||
cy.get("#remember").click()
|
||||
cy.get("#accept").click()
|
||||
|
||||
cy.get('[data-testid="ui/message/4000002"]').should(
|
||||
"contain.text",
|
||||
"Property website is missing",
|
||||
)
|
||||
cy.get('[name="traits.website"]').type("http://s")
|
||||
|
||||
cy.triggerOidc(app)
|
||||
|
||||
cy.get('[data-testid="ui/message/4000003"]').should(
|
||||
"contain.text",
|
||||
"length must be >= 10",
|
||||
)
|
||||
cy.get('[name="traits.requirednested"]').should("not.exist")
|
||||
cy.get('[name="traits.requirednested.a"]').siblings("label").click()
|
||||
cy.get('[name="traits.consent"]').siblings("label").click()
|
||||
cy.get('[name="traits.website"]')
|
||||
.should("have.value", "http://s")
|
||||
.clear()
|
||||
.type(website)
|
||||
cy.triggerOidc(app)
|
||||
|
||||
cy.location("pathname").should("not.contain", "/registration")
|
||||
|
||||
cy.getSession().should(shouldSession(email))
|
||||
})
|
||||
|
||||
it("should be able to sign up with redirects", () => {
|
||||
const email = gen.email()
|
||||
cy.registerOidc({
|
||||
app,
|
||||
email,
|
||||
website,
|
||||
route: registration + "?return_to=https://www.example.org/",
|
||||
})
|
||||
cy.location("href").should("eq", "https://www.example.org/")
|
||||
cy.logout()
|
||||
})
|
||||
|
||||
it("should be able to register with upstream parameters", () => {
|
||||
const email = gen.email()
|
||||
cy.intercept("GET", "**/oauth2/auth*").as("getHydraRegistration")
|
||||
|
||||
cy.visit(registration + "?return_to=https://www.example.org/")
|
||||
|
||||
// hd is forwarded; login_hint is never forwarded because a
|
||||
// caller-supplied value cannot be trusted (HackerOne #3239672,
|
||||
// ory-corp/cloud#8955).
|
||||
cy.addInputElement("form", "upstream_parameters.hd", "ory.sh")
|
||||
cy.addInputElement("form", "upstream_parameters.login_hint", email)
|
||||
|
||||
cy.triggerOidc(app)
|
||||
|
||||
// once a request to getHydraRegistration responds, 'cy.wait' will resolve
|
||||
cy.wait("@getHydraRegistration")
|
||||
.its("request.url")
|
||||
.should("include", "hd=ory.sh")
|
||||
.and("not.include", "login_hint")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, APP_URL, gen } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
context("Registration success with two-step signup", () => {
|
||||
;[
|
||||
{
|
||||
route: express.registration,
|
||||
app: "express" as "express",
|
||||
profile: "two-steps",
|
||||
},
|
||||
{
|
||||
route: react.registration,
|
||||
app: "react" as "react",
|
||||
profile: "two-steps",
|
||||
},
|
||||
].forEach(({ route, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.deleteMail()
|
||||
cy.clearAllCookies()
|
||||
cy.visit(route)
|
||||
cy.enableVerification()
|
||||
if (app === "express") {
|
||||
cy.enableVerificationUIAfterRegistration("password")
|
||||
}
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should sign up and be logged in", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
const website = "https://www.example.org/"
|
||||
|
||||
// Fill out step one forms
|
||||
cy.get(appPrefix(app) + 'input[name="traits"]').should("not.exist")
|
||||
cy.get('input[name="traits.email"]').type("someone@foo")
|
||||
cy.get('input[name="traits.website"]').type(website)
|
||||
cy.get('[name="method"][value="profile"]').click()
|
||||
|
||||
// navigate back, fill traits again
|
||||
cy.get('[name="screen"][value="previous"]').click()
|
||||
cy.get('input[name="traits.email"]').type(
|
||||
"{selectall}{backspace}" + email,
|
||||
)
|
||||
cy.get('[name="method"][value="profile"]').click()
|
||||
|
||||
// Fill out step two forms
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.get('[name="method"][value="password"]').click()
|
||||
|
||||
if (app === "express") {
|
||||
cy.get('a[href*="sessions"]').click()
|
||||
}
|
||||
cy.get("pre").should("contain.text", email)
|
||||
|
||||
cy.getSession().should((session) => {
|
||||
const { identity } = session
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.schema_id).to.equal("default")
|
||||
expect(identity.schema_url).to.equal(`${APP_URL}/schemas/ZGVmYXVsdA`)
|
||||
expect(identity.traits.website).to.equal(website)
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
|
||||
// playwright:migrated
|
||||
it("should handle form errors", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
const websiteTooShort = "a://b"
|
||||
const website = "https://www.example.com"
|
||||
|
||||
// Fill out step one forms
|
||||
cy.get(appPrefix(app) + 'input[name="traits"]').should("not.exist")
|
||||
cy.get('input[name="traits.email"]').type(email)
|
||||
cy.get('input[name="traits.website"]').type(websiteTooShort)
|
||||
cy.get('[name="method"][value="profile"]').click()
|
||||
|
||||
// Assert form errors
|
||||
cy.get('[data-testid="ui/message/4000003"]').should("to.exist")
|
||||
|
||||
// Enter correct value
|
||||
cy.get('input[name="traits.website"]').type(
|
||||
"{selectall}{backspace}" + website,
|
||||
)
|
||||
cy.get('[name="method"][value="profile"]').click()
|
||||
|
||||
// Fill out step two forms
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.get('[name="method"][value="password"]').click()
|
||||
|
||||
if (app === "express") {
|
||||
cy.get('a[href*="sessions"]').click()
|
||||
}
|
||||
cy.get("pre").should("contain.text", email)
|
||||
|
||||
cy.getSession().should((session) => {
|
||||
const { identity } = session
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.schema_id).to.equal("default")
|
||||
expect(identity.schema_url).to.equal(`${APP_URL}/schemas/ZGVmYXVsdA`)
|
||||
expect(identity.traits.website).to.equal(website)
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
// 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"
|
||||
|
||||
context("Account Verification Login Errors", () => {
|
||||
;[
|
||||
{
|
||||
login: react.login,
|
||||
app: "react" as "react",
|
||||
profile: "verification",
|
||||
},
|
||||
{
|
||||
login: express.login,
|
||||
app: "express" as "express",
|
||||
profile: "verification",
|
||||
},
|
||||
].forEach(({ profile, login, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.deleteMail()
|
||||
cy.useConfigProfile(profile)
|
||||
cy.useConfig((builder) => builder.enableLoginForVerifiedAddressOnly())
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
it("is unable to login as long as the email is not verified", () => {
|
||||
cy.deleteMail()
|
||||
|
||||
const identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
cy.visit(login)
|
||||
|
||||
cy.get(appPrefix(app) + '[name="identifier"]').type(identity.email)
|
||||
cy.get('[name="password"]').type(identity.password)
|
||||
cy.get('[value="password"]').click()
|
||||
|
||||
if (app === "express") {
|
||||
cy.url().should("contain", "/verification")
|
||||
}
|
||||
|
||||
cy.get('[data-testid="ui/message/4000010"]').contains(
|
||||
"Account not active yet",
|
||||
)
|
||||
|
||||
cy.noSession()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, gen } from "../../../../helpers"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
|
||||
context("Account Verification Login Success", () => {
|
||||
;[
|
||||
{
|
||||
login: react.login,
|
||||
app: "react" as "react",
|
||||
profile: "verification",
|
||||
},
|
||||
{
|
||||
login: express.login,
|
||||
app: "express" as "express",
|
||||
profile: "verification",
|
||||
},
|
||||
].forEach(({ profile, login, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.deleteMail()
|
||||
cy.useConfigProfile(profile)
|
||||
cy.useConfig((builder) => builder.enableLoginForVerifiedAddressOnly())
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
it("is able to login after successful email verification", () => {
|
||||
cy.deleteMail()
|
||||
|
||||
const identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
cy.performEmailVerification({ expect: { email: identity.email } })
|
||||
|
||||
cy.visit(login)
|
||||
|
||||
cy.get(appPrefix(app) + 'input[name="identifier"]').type(identity.email)
|
||||
cy.get('input[name="password"]').type(identity.password)
|
||||
cy.get('button[value="password"]').click()
|
||||
|
||||
cy.location("pathname").should("not.contain", "/login")
|
||||
|
||||
cy.getSession()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import {
|
||||
appPrefix,
|
||||
assertVerifiableAddress,
|
||||
gen,
|
||||
parseHtml,
|
||||
verifyHrefPattern,
|
||||
} from "../../../../helpers"
|
||||
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
context("Account Verification Registration Errors", () => {
|
||||
;[
|
||||
{
|
||||
login: react.login,
|
||||
verification: react.verification,
|
||||
app: "react" as "react",
|
||||
profile: "verification",
|
||||
},
|
||||
{
|
||||
login: express.login,
|
||||
verification: express.verification,
|
||||
app: "express" as "express",
|
||||
profile: "verification",
|
||||
},
|
||||
].forEach(({ profile, login, verification, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.deleteMail()
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
let identity
|
||||
beforeEach(() => {
|
||||
cy.useConfig((builder) =>
|
||||
builder
|
||||
.enableVerification()
|
||||
.disableRecovery()
|
||||
.shortCodeLifespan()
|
||||
.longVerificationLifespan(),
|
||||
)
|
||||
cy.deleteMail()
|
||||
|
||||
identity = gen.identityWithWebsite()
|
||||
cy.registerApi(identity)
|
||||
cy.login(identity)
|
||||
})
|
||||
|
||||
it("is unable to verify the email address if the code is no longer valid and resend the code", () => {
|
||||
cy.verifyEmailButExpired({
|
||||
expect: { email: identity.email },
|
||||
})
|
||||
|
||||
cy.longCodeLifespan()
|
||||
|
||||
cy.visit(verification)
|
||||
cy.get(appPrefix(app) + 'input[name="email"]').should("be.empty")
|
||||
cy.get('input[name="email"]').type(identity.email)
|
||||
cy.get('button[value="code"]').click()
|
||||
cy.contains("An email containing a verification")
|
||||
cy.verifyEmail({
|
||||
expect: { email: identity.email, password: identity.password },
|
||||
})
|
||||
})
|
||||
|
||||
it("is unable to verify the email address if the code is incorrect", () => {
|
||||
cy.getMail({
|
||||
body: "Verify your account",
|
||||
email: identity.email,
|
||||
}).then((mail) => {
|
||||
const link = parseHtml(mail.body).querySelector("a")
|
||||
|
||||
expect(verifyHrefPattern.test(link.href)).to.be.true
|
||||
|
||||
cy.visit(link.href + "-not") // add random stuff to the confirm challenge
|
||||
cy.getSession().should((session) =>
|
||||
assertVerifiableAddress({
|
||||
isVerified: false,
|
||||
email: identity.email,
|
||||
})(session),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { assertVerifiableAddress, gen } from "../../../../helpers"
|
||||
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
|
||||
context("Account Verification Registration Success", () => {
|
||||
;[
|
||||
{
|
||||
registration: react.registration,
|
||||
app: "react" as "react",
|
||||
profile: "verification",
|
||||
},
|
||||
{
|
||||
registration: express.registration,
|
||||
app: "express" as "express",
|
||||
profile: "verification",
|
||||
},
|
||||
].forEach(({ profile, registration, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.deleteMail()
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.useConfig((builder) => builder.longVerificationLifespan())
|
||||
cy.deleteMail()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cy.deleteMail()
|
||||
})
|
||||
|
||||
const up = (value) => `up-${value}`
|
||||
const { email, password } = gen.identity()
|
||||
|
||||
it("is able to verify the email address after sign up", () => {
|
||||
const identity = gen.identityWithWebsite()
|
||||
const { email, password } = identity
|
||||
cy.registerApi(identity)
|
||||
cy.login(identity)
|
||||
cy.getSession().should((session) =>
|
||||
assertVerifiableAddress({
|
||||
isVerified: false,
|
||||
email,
|
||||
})(session),
|
||||
)
|
||||
|
||||
cy.verifyEmail({ expect: { email, password } })
|
||||
})
|
||||
|
||||
xit("sends the warning email on double sign up", () => {
|
||||
// FIXME https://github.com/ory/kratos/issues/133
|
||||
cy.clearAllCookies()
|
||||
cy.register({ email, password: up(password) })
|
||||
cy.clearAllCookies()
|
||||
cy.login({ email, password })
|
||||
|
||||
cy.verifyEmail({ expect: { email, password } })
|
||||
})
|
||||
|
||||
it("is redirected to after_verification_return_to after verification", () => {
|
||||
cy.clearAllCookies()
|
||||
const { email, password } = gen.identity()
|
||||
cy.register({
|
||||
email,
|
||||
password,
|
||||
query: {
|
||||
return_to: "http://localhost:4455/verification_return_to_callback",
|
||||
after_verification_return_to:
|
||||
"http://localhost:4455/verification_callback",
|
||||
},
|
||||
})
|
||||
cy.login({ email, password })
|
||||
cy.verifyEmail({
|
||||
expect: {
|
||||
email,
|
||||
password,
|
||||
redirectTo: "http://localhost:4455/verification_callback",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("is redirected to `return_to` after verification", () => {
|
||||
cy.clearAllCookies()
|
||||
const { email, password } = gen.identity()
|
||||
cy.register({
|
||||
email,
|
||||
password,
|
||||
query: {
|
||||
return_to: "http://localhost:4455/verification_return_to_callback",
|
||||
},
|
||||
})
|
||||
cy.login({ email, password })
|
||||
cy.verifyEmail({
|
||||
expect: {
|
||||
email,
|
||||
password,
|
||||
redirectTo: "http://localhost:4455/verification_return_to_callback",
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import {
|
||||
appPrefix,
|
||||
assertVerifiableAddress,
|
||||
gen,
|
||||
parseHtml,
|
||||
verifyHrefPattern,
|
||||
} from "../../../../helpers"
|
||||
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
|
||||
context("Account Verification Settings Error", () => {
|
||||
;[
|
||||
{
|
||||
settings: react.settings,
|
||||
base: react.base,
|
||||
app: "react" as "react",
|
||||
profile: "verification",
|
||||
},
|
||||
{
|
||||
settings: express.settings,
|
||||
base: express.base,
|
||||
app: "express" as "express",
|
||||
profile: "verification",
|
||||
},
|
||||
].forEach(({ profile, settings, app, base }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.deleteMail()
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
describe("error flow", () => {
|
||||
let identity
|
||||
before(() => {
|
||||
cy.deleteMail()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.useConfig((builder) => builder.longCodeLifespan())
|
||||
identity = gen.identityWithWebsite()
|
||||
cy.clearAllCookies()
|
||||
cy.registerApi(identity)
|
||||
cy.deleteMail({ atLeast: 1 }) // clean up registration email
|
||||
|
||||
cy.login({ ...identity, cookieUrl: base })
|
||||
cy.visit(settings)
|
||||
})
|
||||
|
||||
it("is unable to verify the email address if the code is no longer valid", () => {
|
||||
cy.shortCodeLifespan()
|
||||
cy.visit(settings)
|
||||
|
||||
const email = `not-${identity.email}`
|
||||
cy.get(appPrefix(app) + 'input[name="traits.email"]')
|
||||
.clear()
|
||||
.type(email)
|
||||
cy.get('button[value="profile"]').click()
|
||||
|
||||
cy.verifyEmailButExpired({
|
||||
expect: { email },
|
||||
})
|
||||
})
|
||||
|
||||
it("is unable to verify the email address if the code is incorrect", () => {
|
||||
const email = `not-${identity.email}`
|
||||
cy.get('input[name="traits.email"]').clear().type(email)
|
||||
cy.get('button[value="profile"]').click()
|
||||
|
||||
cy.getMail({
|
||||
body: "Verify your account",
|
||||
email,
|
||||
}).then((mail) => {
|
||||
const link = parseHtml(mail.body).querySelector("a")
|
||||
|
||||
expect(verifyHrefPattern.test(link.href)).to.be.true
|
||||
|
||||
cy.visit(link.href + "-not") // add random stuff to the confirm challenge
|
||||
cy.log(link.href)
|
||||
cy.getSession().then(
|
||||
assertVerifiableAddress({ isVerified: false, email }),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
xit("should not update the traits until the email has been verified and the old email has accepted the change", () => {
|
||||
// FIXME https://github.com/ory/kratos/issues/292
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import {
|
||||
APP_URL,
|
||||
appPrefix,
|
||||
assertVerifiableAddress,
|
||||
gen,
|
||||
} from "../../../../helpers"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
|
||||
context("Account Verification Settings Success", () => {
|
||||
;[
|
||||
{
|
||||
settings: react.settings,
|
||||
app: "react" as "react",
|
||||
profile: "verification",
|
||||
},
|
||||
{
|
||||
settings: express.settings,
|
||||
app: "express" as "express",
|
||||
profile: "verification",
|
||||
},
|
||||
].forEach(({ profile, settings, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.deleteMail()
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
let identity
|
||||
|
||||
before(() => {
|
||||
cy.deleteMail()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
identity = gen.identity()
|
||||
cy.register(identity)
|
||||
cy.deleteMail({ atLeast: 1 }) // clean up registration email
|
||||
|
||||
cy.login(identity)
|
||||
cy.visit(settings)
|
||||
})
|
||||
|
||||
it("should update the verify address and request a verification email", () => {
|
||||
const email = `not-${identity.email}`
|
||||
cy.get(appPrefix(app) + 'input[name="traits.email"]')
|
||||
.clear()
|
||||
.type(email)
|
||||
cy.get('[value="profile"]').click()
|
||||
|
||||
if (app == "express") {
|
||||
cy.expectSettingsSaved()
|
||||
cy.get('input[name="traits.email"]').should("contain.value", email)
|
||||
cy.getSession().then(
|
||||
assertVerifiableAddress({ isVerified: false, email }),
|
||||
)
|
||||
}
|
||||
cy.verifyEmail({ expect: { email } })
|
||||
})
|
||||
|
||||
xit("should should be able to allow or deny (and revert?) the address change", () => {
|
||||
// FIXME https://github.com/ory/kratos/issues/292
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,204 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import {
|
||||
assertVerifiableAddress,
|
||||
gen,
|
||||
parseHtml,
|
||||
verifyHrefPattern,
|
||||
} from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
import { Strategy } from "../../../../support"
|
||||
|
||||
context("Account Verification Error", () => {
|
||||
;[
|
||||
{
|
||||
verification: react.verification,
|
||||
base: react.base,
|
||||
app: "react" as "react",
|
||||
profile: "verification",
|
||||
},
|
||||
{
|
||||
verification: express.verification,
|
||||
base: express.base,
|
||||
app: "express" as "express",
|
||||
profile: "verification",
|
||||
},
|
||||
].forEach(({ profile, verification, app, base }) => {
|
||||
for (let s of ["code", "link"] as Strategy[]) {
|
||||
describe(`for strategy ${s}`, () => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.deleteMail()
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
let identity
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
|
||||
cy.useConfig((builder) =>
|
||||
builder
|
||||
.longVerificationLifespan()
|
||||
.longLifespan(s)
|
||||
.useVerificationStrategy(s)
|
||||
.notifyUnknownRecipients("verification", false),
|
||||
)
|
||||
|
||||
identity = gen.identity()
|
||||
cy.registerApi(identity)
|
||||
cy.deleteMail({ atLeast: 1 }) // clean up registration email
|
||||
cy.login(identity)
|
||||
cy.visit(verification)
|
||||
})
|
||||
|
||||
it("responds with a HTML response on link click of an API flow if the flow is expired", () => {
|
||||
cy.updateConfigFile((config) => {
|
||||
config.selfservice.flows.verification.lifespan = "1s"
|
||||
return config
|
||||
})
|
||||
|
||||
cy.verificationApi({
|
||||
email: identity.email,
|
||||
strategy: s,
|
||||
})
|
||||
|
||||
cy.wait(1000)
|
||||
cy.shortVerificationLifespan()
|
||||
|
||||
cy.getMail({
|
||||
removeMail: true,
|
||||
body: "Verify your account",
|
||||
email: identity.email,
|
||||
}).then((message) => {
|
||||
expect(message.toAddresses[0].trim()).to.equal(identity.email)
|
||||
|
||||
const link = parseHtml(message.body).querySelector("a")
|
||||
|
||||
cy.longVerificationLifespan()
|
||||
cy.visit(link.href)
|
||||
cy.get('[data-testid="ui/message/4070005"]').should(
|
||||
"contain.text",
|
||||
"verification flow expired",
|
||||
)
|
||||
|
||||
cy.getSession().should((session) => {
|
||||
assertVerifiableAddress({
|
||||
isVerified: false,
|
||||
email: identity.email,
|
||||
})(session)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("responds with a HTML response on link click of an API flow if the code is expired", () => {
|
||||
cy.shortLifespan(s)
|
||||
|
||||
// Init expired flow
|
||||
cy.verificationApi({
|
||||
email: identity.email,
|
||||
strategy: s,
|
||||
})
|
||||
|
||||
cy.wait(1000)
|
||||
|
||||
cy.verifyEmailButExpired({
|
||||
expect: { email: identity.email },
|
||||
strategy: s,
|
||||
})
|
||||
})
|
||||
|
||||
it("is unable to verify the email address if the code is expired", () => {
|
||||
cy.shortLifespan(s)
|
||||
|
||||
cy.visit(verification)
|
||||
cy.get('input[name="email"]').type(identity.email)
|
||||
cy.get(`button[value="${s}"]`).click()
|
||||
|
||||
cy.contains("An email containing a verification")
|
||||
cy.get(`[name="method"][value="${s}"]`).should("exist")
|
||||
cy.verifyEmailButExpired({
|
||||
expect: { email: identity.email },
|
||||
strategy: s,
|
||||
})
|
||||
})
|
||||
|
||||
it("is unable to verify the email address if the code is incorrect", () => {
|
||||
cy.get('input[name="email"]').type(identity.email)
|
||||
cy.get(`button[value="${s}"]`).click()
|
||||
|
||||
cy.contains("An email containing a verification")
|
||||
|
||||
cy.getMail({
|
||||
email: identity.email,
|
||||
body: "Verify your account",
|
||||
}).then((mail) => {
|
||||
const link = parseHtml(mail.body).querySelector("a")
|
||||
|
||||
expect(verifyHrefPattern.test(link.href)).to.be.true
|
||||
|
||||
cy.visit(link.href + "-not") // add random stuff to the confirm challenge
|
||||
cy.getSession().then(
|
||||
assertVerifiableAddress({
|
||||
isVerified: false,
|
||||
email: identity.email,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it("unable to verify non-existent account", () => {
|
||||
cy.notifyUnknownRecipients("verification")
|
||||
const email = gen.identity().email
|
||||
cy.get('input[name="email"]').type(email)
|
||||
cy.get(`button[value="${s}"]`).click()
|
||||
cy.getMail({
|
||||
subject: "Someone tried to verify this email address",
|
||||
email,
|
||||
removeMail: true,
|
||||
}).then((mail) => {
|
||||
expect(mail.toAddresses).includes(email)
|
||||
expect(mail.subject).eq(
|
||||
"Someone tried to verify this email address",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
if (s === "code") {
|
||||
it("is unable to brute force the code", () => {
|
||||
cy.visit(verification)
|
||||
cy.get('input[name="email"]').type(identity.email)
|
||||
cy.get(`button[value="${s}"]`).click()
|
||||
|
||||
cy.contains("An email containing a verification")
|
||||
|
||||
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/4070006"]').should(
|
||||
"have.text",
|
||||
"The verification code is invalid or has already been used. Please try again.",
|
||||
)
|
||||
}
|
||||
|
||||
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.getSession().then(
|
||||
assertVerifiableAddress({
|
||||
isVerified: false,
|
||||
email: identity.email,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,173 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { APP_URL, assertVerifiableAddress, gen } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { routes as react } from "../../../../helpers/react"
|
||||
import { Strategy } from "../../../../support"
|
||||
|
||||
context("Account Verification Settings Success", () => {
|
||||
;[
|
||||
{
|
||||
verification: react.verification,
|
||||
app: "react" as "react",
|
||||
profile: "verification",
|
||||
},
|
||||
{
|
||||
verification: express.verification,
|
||||
app: "express" as "express",
|
||||
profile: "verification",
|
||||
},
|
||||
].forEach(({ profile, verification, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
for (let s of ["code", "link"] as Strategy[]) {
|
||||
describe(`for strategy ${s}`, () => {
|
||||
beforeEach(() => {
|
||||
cy.useConfig((builder) =>
|
||||
builder
|
||||
.useVerificationStrategy(s)
|
||||
.notifyUnknownRecipients("verification", false),
|
||||
)
|
||||
})
|
||||
|
||||
it("should request verification and receive an email and verify it", () => {
|
||||
const identity = gen.identity()
|
||||
cy.register(identity)
|
||||
cy.deleteMail({ atLeast: 1 }) // clean up registration email
|
||||
|
||||
cy.login(identity)
|
||||
cy.visit(verification)
|
||||
cy.get('input[name="email"]').type(identity.email)
|
||||
cy.get(`button[value="${s}"]`).click()
|
||||
|
||||
cy.contains("An email containing a verification")
|
||||
|
||||
cy.get(`[name="method"][value="${s}"]`).should("exist")
|
||||
|
||||
cy.verifyEmail({ expect: { email: identity.email }, strategy: s })
|
||||
})
|
||||
|
||||
it("should request verification for an email that does not exist yet", () => {
|
||||
cy.notifyUnknownRecipients("verification", true)
|
||||
const identity = gen.identity()
|
||||
cy.register(identity)
|
||||
cy.deleteMail({ atLeast: 1 }) // clean up registration email
|
||||
|
||||
cy.login(identity)
|
||||
cy.visit(verification)
|
||||
const email = `not-${identity.email}`
|
||||
cy.get('input[name="email"]').type(email)
|
||||
cy.get(`button[value="${s}"]`).click()
|
||||
|
||||
cy.contains("An email containing a verification")
|
||||
|
||||
cy.getMail({
|
||||
subject: "Someone tried to verify this email address",
|
||||
email,
|
||||
}).should((message) => {
|
||||
expect(message.subject.trim()).to.equal(
|
||||
"Someone tried to verify this email address",
|
||||
)
|
||||
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)
|
||||
})
|
||||
|
||||
cy.getSession().then(
|
||||
assertVerifiableAddress({
|
||||
isVerified: false,
|
||||
email: identity.email,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("should not verify email when clicking on link received on different address", () => {
|
||||
const identity = gen.identity()
|
||||
cy.register(identity)
|
||||
cy.deleteMail({ atLeast: 1 }) // clean up registration email
|
||||
|
||||
cy.login(identity)
|
||||
cy.visit(verification)
|
||||
cy.get('input[name="email"]').type(identity.email)
|
||||
cy.get(`button[value="${s}"]`).click()
|
||||
|
||||
cy.verifyEmail({ expect: { email: identity.email }, strategy: s })
|
||||
|
||||
// identity is verified
|
||||
cy.logout()
|
||||
|
||||
// registered with other email address
|
||||
const identity2 = gen.identity()
|
||||
cy.register(identity2)
|
||||
cy.deleteMail({ atLeast: 1 }) // clean up registration email
|
||||
|
||||
cy.login(identity2)
|
||||
|
||||
cy.visit(APP_URL + "/verification")
|
||||
|
||||
// request verification link for identity
|
||||
cy.get('input[name="email"]').type(identity.email)
|
||||
cy.get(`button[value="${s}"]`).click()
|
||||
|
||||
cy.performEmailVerification({
|
||||
expect: { email: identity.email },
|
||||
strategy: s,
|
||||
})
|
||||
|
||||
// expect current session to still not have a verified email address
|
||||
cy.getSession().should(
|
||||
assertVerifiableAddress({
|
||||
email: identity2.email,
|
||||
isVerified: false,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("should redirect to return_to after completing verification", () => {
|
||||
cy.clearAllCookies()
|
||||
// registered with other email address
|
||||
const identity2 = gen.identity()
|
||||
cy.register(identity2)
|
||||
cy.deleteMail({ atLeast: 1 }) // clean up registration email
|
||||
|
||||
cy.login(identity2)
|
||||
|
||||
cy.visit(APP_URL + "/self-service/verification/browser", {
|
||||
qs: { return_to: "http://localhost:4455/verification_callback" },
|
||||
})
|
||||
// request verification link for identity
|
||||
cy.get('input[name="email"]').type(identity2.email)
|
||||
cy.get(`[name="method"][value="${s}"]`).click()
|
||||
cy.verifyEmail({
|
||||
expect: {
|
||||
email: identity2.email,
|
||||
redirectTo: "http://localhost:4455/verification_callback",
|
||||
},
|
||||
strategy: s,
|
||||
})
|
||||
})
|
||||
|
||||
it("should not notify an unknown recipient", () => {
|
||||
const recipient = gen.email()
|
||||
|
||||
cy.visit(APP_URL + "/self-service/verification/browser")
|
||||
cy.get('input[name="email"]').type(recipient)
|
||||
cy.get(`[name="method"][value="${s}"]`).click()
|
||||
|
||||
cy.getCourierMessages().then((messages) => {
|
||||
expect(messages.map((msg) => msg.recipient)).to.not.include(
|
||||
recipient,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { gen } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
|
||||
describe("Basic email profile with failing login flows with webhooks", () => {
|
||||
;[
|
||||
{
|
||||
route: express.login,
|
||||
app: "express" as "express",
|
||||
profile: "webhooks",
|
||||
},
|
||||
].forEach(({ route, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
it("should show fail to sign in when webhooks rejects login", () => {
|
||||
const email = gen.blockedEmail()
|
||||
const password = gen.password()
|
||||
|
||||
cy.registerApi({ email, password, fields: {} })
|
||||
cy.get('input[name="identifier"]').type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
|
||||
cy.submitPasswordForm()
|
||||
cy.get('*[data-testid="ui/message/1234"]').should(
|
||||
"contain.text",
|
||||
"email could not be validated",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { APP_URL, appPrefix, gen, website } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { testFlowWebhook } from "../../../../helpers/webhook"
|
||||
|
||||
describe("Basic email profile with succeeding login flows with webhooks", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
before(() => {
|
||||
cy.registerApi({ email, password, fields: { "traits.website": website } })
|
||||
})
|
||||
;[
|
||||
{
|
||||
route: express.login,
|
||||
app: "express" as "express",
|
||||
profile: "webhooks",
|
||||
},
|
||||
].forEach(({ route, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.clearAllCookies()
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
it("should sign in and be logged in", () => {
|
||||
cy.get(`${appPrefix(app)}input[name="identifier"]`).type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.submitPasswordForm()
|
||||
cy.location("pathname").should("not.contain", "/login")
|
||||
|
||||
cy.getSession().should((session) => {
|
||||
const { identity } = session
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.schema_id).to.equal("default")
|
||||
expect(identity.schema_url).to.equal(`${APP_URL}/schemas/ZGVmYXVsdA`)
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
|
||||
it("should pass transient_payload to webhook", () => {
|
||||
testFlowWebhook(
|
||||
(h) => cy.setupHooks("login", "after", "password", h),
|
||||
() => {
|
||||
cy.get(`${appPrefix(app)}input[name="identifier"]`).type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.submitPasswordForm()
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { gen } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
|
||||
describe("Registration failures with email profile with webhooks", () => {
|
||||
;[
|
||||
{
|
||||
route: express.registration,
|
||||
app: "express" as "express",
|
||||
profile: "webhooks",
|
||||
},
|
||||
].forEach(({ route, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
const blockedIdentity = gen.blockedEmail()
|
||||
const password = gen.password()
|
||||
|
||||
it("should show an error when the webhook is blocking registration", () => {
|
||||
cy.get('input[name="traits.email"]').type(blockedIdentity)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
|
||||
cy.submitPasswordForm()
|
||||
cy.get('input[name="traits.email"]').should(
|
||||
"have.value",
|
||||
blockedIdentity,
|
||||
)
|
||||
cy.get('*[data-testid="ui/message/1234"]').should(
|
||||
"contain.text",
|
||||
"email could not be validated",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,172 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { appPrefix, APP_URL, gen } from "../../../../helpers"
|
||||
import { routes as express } from "../../../../helpers/express"
|
||||
import { testFlowWebhook } from "../../../../helpers/webhook"
|
||||
|
||||
context("Registration success with email profile with webhooks", () => {
|
||||
;[
|
||||
{
|
||||
route: express.registration,
|
||||
app: "express" as "express",
|
||||
profile: "webhooks",
|
||||
},
|
||||
].forEach(({ route, profile, app }) => {
|
||||
describe(`for app ${app}`, () => {
|
||||
before(() => {
|
||||
cy.useConfigProfile(profile)
|
||||
cy.proxy(app)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearAllCookies()
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
it("should sign up and be logged in", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
cy.get(appPrefix(app) + 'input[name="traits"]').should("not.exist")
|
||||
cy.get('input[name="traits.email"]').type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
|
||||
cy.submitPasswordForm()
|
||||
if (app === "express") {
|
||||
cy.get("a[href*='sessions']").click()
|
||||
}
|
||||
cy.get("pre").should("contain.text", email)
|
||||
|
||||
cy.getSession().should((session) => {
|
||||
const { identity } = session
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.schema_id).to.equal("default")
|
||||
expect(identity.schema_url).to.equal(`${APP_URL}/schemas/ZGVmYXVsdA`)
|
||||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
|
||||
it("should work without transient_payload", () => {
|
||||
const WEBHOOK_TARGET = "https://webhook-target-gsmwn5ab4a-uc.a.run.app"
|
||||
const documentUrl = (key: string) =>
|
||||
`${WEBHOOK_TARGET}/documents/${key}`
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
const documentID = gen.password()
|
||||
const jsonnet = "function(ctx) ctx"
|
||||
cy.setupHooks("registration", "after", "password", [
|
||||
{
|
||||
hook: "web_hook",
|
||||
config: {
|
||||
body: `base64://${Buffer.from(jsonnet).toString("base64")}`,
|
||||
url: documentUrl(documentID),
|
||||
method: "PUT",
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
cy.get('input[name="traits.email"]').type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
|
||||
cy.submitPasswordForm()
|
||||
|
||||
cy.request({
|
||||
url: documentUrl(documentID),
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
}).then(({ body, status }) => {
|
||||
const b = JSON.parse(body)
|
||||
expect(status).to.equal(200)
|
||||
expect(b.identity).is.not.undefined
|
||||
expect(b.flow.transient_payload).is.empty
|
||||
})
|
||||
})
|
||||
|
||||
it("should pass transient_payload to webhook", () => {
|
||||
testFlowWebhook(cy.setPostPasswordRegistrationHooks.bind(cy), () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
cy.get('input[name="traits.email"]').type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
|
||||
cy.submitPasswordForm()
|
||||
})
|
||||
})
|
||||
|
||||
it("should sign up and modify the identity", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
const updatedEmail = {
|
||||
identity: {
|
||||
traits: { email: "updated-" + email },
|
||||
verifiable_addresses: [
|
||||
{ via: "email", value: "updated-" + email, verified: true },
|
||||
{ via: "email", value: "this-email-should-be-ignored" },
|
||||
{ via: "email", value: "" },
|
||||
],
|
||||
recovery_addresses: [
|
||||
{ via: "email", value: "updated-" + email },
|
||||
{ via: "email", value: "this-email-should-be-ignored" },
|
||||
{ via: "email", value: "" },
|
||||
],
|
||||
metadata_public: { some: "public fields" },
|
||||
},
|
||||
}
|
||||
cy.setPostPasswordRegistrationHooks([
|
||||
{ hook: "session" },
|
||||
{
|
||||
hook: "web_hook",
|
||||
config: {
|
||||
url:
|
||||
"http://127.0.0.1:4459/webhook/write?response=" +
|
||||
encodeURIComponent(JSON.stringify(updatedEmail)),
|
||||
method: "POST",
|
||||
body: "file://test/e2e/profiles/webhooks/webhook_body.jsonnet",
|
||||
response: { parse: true },
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
cy.get(appPrefix(app) + 'input[name="traits"]').should("not.exist")
|
||||
cy.get('input[name="traits.email"]').type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
|
||||
cy.submitPasswordForm()
|
||||
|
||||
// Wait for the post-registration landing page (express welcome) so the
|
||||
// session cookie is in Cypress's jar before we hit /sessions/whoami.
|
||||
if (app === "express") {
|
||||
cy.get("a[href*='sessions']").click()
|
||||
}
|
||||
cy.get("pre").should("contain.text", "updated-" + email)
|
||||
|
||||
cy.getSession().should((session) => {
|
||||
const { identity } = session
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.schema_id).to.equal("default")
|
||||
expect(identity.schema_url).to.equal(`${APP_URL}/schemas/ZGVmYXVsdA`)
|
||||
expect(identity.traits.email).to.equal("updated-" + email)
|
||||
expect((identity as any).metadata_public.some).to.equal(
|
||||
"public fields",
|
||||
)
|
||||
expect(identity.verifiable_addresses[0].verified).to.equal(true)
|
||||
expect(identity.verifiable_addresses[0].verified_at).not.to.be.empty
|
||||
expect(identity.verifiable_addresses[0].via).to.eq("email")
|
||||
expect(identity.verifiable_addresses[0].value).to.eq(
|
||||
"updated-" + email,
|
||||
)
|
||||
expect(identity.verifiable_addresses).to.have.length(1)
|
||||
|
||||
expect(identity.recovery_addresses).to.have.length(1)
|
||||
expect(identity.recovery_addresses[0].via).to.eq("email")
|
||||
expect(identity.recovery_addresses[0].value).to.eq("updated-" + email)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user