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:
5
test/e2e/cypress/fixtures/example.json
Normal file
5
test/e2e/cypress/fixtures/example.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
14
test/e2e/cypress/helpers/express.ts
Normal file
14
test/e2e/cypress/helpers/express.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { APP_URL } from "./index"
|
||||
|
||||
export const routes = {
|
||||
base: APP_URL,
|
||||
login: APP_URL + "/login",
|
||||
registration: APP_URL + "/registration",
|
||||
settings: APP_URL + "/settings",
|
||||
recovery: APP_URL + "/recovery",
|
||||
verification: APP_URL + "/verification",
|
||||
welcome: APP_URL + "/welcome",
|
||||
}
|
||||
45
test/e2e/cypress/helpers/httpbin.ts
Normal file
45
test/e2e/cypress/helpers/httpbin.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as oauth2 from "./oauth2"
|
||||
|
||||
export function checkToken(
|
||||
client: oauth2.oAuth2Client,
|
||||
scope: string[],
|
||||
check: (token: any) => void,
|
||||
) {
|
||||
cy.location("href")
|
||||
.should(
|
||||
"match",
|
||||
new RegExp(
|
||||
"https://ory-network-httpbin-ijakee5waq-ez.a.run.app/anything[?]code=.*",
|
||||
),
|
||||
)
|
||||
.then(() => {
|
||||
cy.get("body")
|
||||
.invoke("text")
|
||||
.then((text) => {
|
||||
const result = JSON.parse(text)
|
||||
const tokenParams = {
|
||||
code: result.args.code,
|
||||
redirect_uri:
|
||||
"https://ory-network-httpbin-ijakee5waq-ez.a.run.app/anything",
|
||||
scope: scope.join(" "),
|
||||
}
|
||||
oauth2
|
||||
.getToken(
|
||||
client.token_endpoint,
|
||||
client.id,
|
||||
client.secret,
|
||||
"authorization_code",
|
||||
tokenParams.code,
|
||||
tokenParams.redirect_uri,
|
||||
tokenParams.scope,
|
||||
)
|
||||
.then((res) => {
|
||||
const token = res.body
|
||||
check(token)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
104
test/e2e/cypress/helpers/index.ts
Normal file
104
test/e2e/cypress/helpers/index.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export const email = () => Math.random().toString(36) + "@ory.sh"
|
||||
export const blockedEmail = () =>
|
||||
Math.random().toString(36) + "_blocked" + "@ory.sh"
|
||||
|
||||
export const password = () => Math.random().toString(36)
|
||||
|
||||
export const assertVerifiableAddress =
|
||||
({ isVerified, email }) =>
|
||||
(session) => {
|
||||
const { identity } = session
|
||||
expect(identity).to.have.property("verifiable_addresses")
|
||||
expect(identity.verifiable_addresses).to.have.length(1)
|
||||
|
||||
const address = identity.verifiable_addresses[0]
|
||||
expect(address.id).to.not.be.empty
|
||||
expect(address.verified).to.equal(isVerified)
|
||||
expect(address.value).to.equal(email)
|
||||
|
||||
if (isVerified) {
|
||||
expect(address.verified_at).to.not.be.null
|
||||
} else {
|
||||
expect(address).to.not.have.property("verified_at")
|
||||
}
|
||||
}
|
||||
|
||||
export const assertRecoveryAddress =
|
||||
({ email }) =>
|
||||
({ identity }) => {
|
||||
expect(identity).to.have.property("recovery_addresses")
|
||||
expect(identity.recovery_addresses).to.have.length(1)
|
||||
|
||||
const address = identity.recovery_addresses[0]
|
||||
expect(address.id).to.not.be.empty
|
||||
expect(address.value).to.equal(email)
|
||||
}
|
||||
|
||||
export const parseHtml = (html: string) =>
|
||||
new DOMParser().parseFromString(html, "text/html")
|
||||
|
||||
export const APP_URL = (
|
||||
Cypress.env("app_url") || "http://localhost:4455"
|
||||
).replace(/\/$/, "")
|
||||
|
||||
export const MOBILE_URL = (
|
||||
Cypress.env("mobile_url") || "http://localhost:19006"
|
||||
).replace(/\/$/, "")
|
||||
export const SPA_URL = (
|
||||
Cypress.env("react_url") || "http://localhost:4455"
|
||||
).replace(/\/$/, "")
|
||||
export const KRATOS_ADMIN = (
|
||||
Cypress.env("kratos_admin") || "http://localhost:4434"
|
||||
)
|
||||
.replace()
|
||||
.replace(/\/$/, "")
|
||||
|
||||
export const KRATOS_PUBLIC = (
|
||||
Cypress.env("kratos_public") || "http://localhost:4433"
|
||||
)
|
||||
.replace()
|
||||
.replace(/\/$/, "")
|
||||
|
||||
export const MAIL_API = (
|
||||
Cypress.env("mail_url") || "http://localhost:4437"
|
||||
).replace(/\/$/, "")
|
||||
|
||||
export const website = "https://www.ory.sh/"
|
||||
|
||||
export const gen = {
|
||||
email,
|
||||
blockedEmail,
|
||||
password,
|
||||
identity: () => ({ email: email(), password: password() }),
|
||||
identityWithWebsite: () => ({
|
||||
email: email(),
|
||||
password: password(),
|
||||
fields: { "traits.website": "https://www.ory.sh" },
|
||||
}),
|
||||
}
|
||||
|
||||
// Format is
|
||||
export const verifyHrefPattern =
|
||||
/^http:.*\/self-service\/verification\?(((&|)code|(&|)token|(&|)flow)=([\-a-zA-Z0-9]+)){2}$/
|
||||
|
||||
// intervals define how long to wait for something,
|
||||
export const pollInterval = 250 // how long to wait before retry
|
||||
|
||||
// Adding 1+ second on top because MySQL doesn't do millisecs.
|
||||
export const verifyLifespan = 5000 + 1000
|
||||
export const privilegedLifespan = 5000 + 1000
|
||||
|
||||
export const appPrefix = (app) => `[data-testid="app-${app}"] `
|
||||
|
||||
export const codeRegex = /(\d{6})/
|
||||
|
||||
export function extractOTPCode(body: string): string | null {
|
||||
const result = codeRegex.exec(body)
|
||||
if (result != null && result.length > 0) {
|
||||
return result[0]
|
||||
}
|
||||
return null
|
||||
}
|
||||
106
test/e2e/cypress/helpers/oauth2.ts
Normal file
106
test/e2e/cypress/helpers/oauth2.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as uuid from "uuid"
|
||||
|
||||
export type oAuth2Client = {
|
||||
auth_endpoint: string
|
||||
token_endpoint: string
|
||||
id: string
|
||||
secret: string
|
||||
token_endpoint_auth_method: string
|
||||
grant_types: string[]
|
||||
response_types: string[]
|
||||
scopes: string[]
|
||||
callbacks: string[]
|
||||
}
|
||||
|
||||
export function getDefaultAuthorizeURL(client: oAuth2Client) {
|
||||
const state = uuid.v4()
|
||||
const nonce = uuid.v4()
|
||||
return getAuthorizeURL(
|
||||
client.auth_endpoint,
|
||||
"",
|
||||
client.id,
|
||||
undefined,
|
||||
nonce,
|
||||
"https://ory-network-httpbin-ijakee5waq-ez.a.run.app/anything",
|
||||
"code",
|
||||
["offline", "openid"],
|
||||
state,
|
||||
undefined,
|
||||
)
|
||||
}
|
||||
|
||||
export function getAuthorizeURL(
|
||||
auth_endpoint: string,
|
||||
audience: string,
|
||||
client_id: string,
|
||||
max_age: string | undefined,
|
||||
nonce: string,
|
||||
redirect_uri: string,
|
||||
response_type:
|
||||
| "code"
|
||||
| "id_token"
|
||||
| "id_token token"
|
||||
| "code id_token"
|
||||
| "code token"
|
||||
| "code id_token token",
|
||||
scopes: string[],
|
||||
state: string,
|
||||
code_challenge?: string,
|
||||
): string {
|
||||
const r = new URL(auth_endpoint)
|
||||
r.searchParams.append("audience", audience)
|
||||
r.searchParams.append("client_id", client_id)
|
||||
if (max_age !== undefined) {
|
||||
r.searchParams.append("max_age", max_age)
|
||||
}
|
||||
r.searchParams.append("nonce", nonce)
|
||||
r.searchParams.append("prompt", "")
|
||||
r.searchParams.append("redirect_uri", redirect_uri)
|
||||
r.searchParams.append("response_type", response_type)
|
||||
r.searchParams.append("scope", scopes.join(" "))
|
||||
r.searchParams.append("state", state)
|
||||
|
||||
code_challenge && r.searchParams.append("code_challenge", code_challenge)
|
||||
return r.toString()
|
||||
}
|
||||
|
||||
export function getToken(
|
||||
token_endpoint: string,
|
||||
client_id: string,
|
||||
client_secret: string,
|
||||
grant_type: "authorization_code",
|
||||
code: string,
|
||||
redirect_uri: string,
|
||||
scope: string,
|
||||
) {
|
||||
let urlEncodedData = ""
|
||||
const urlEncodedDataPairs = []
|
||||
urlEncodedDataPairs.push(
|
||||
encodeURIComponent("grant_type") + "=" + encodeURIComponent(grant_type),
|
||||
)
|
||||
urlEncodedDataPairs.push(
|
||||
encodeURIComponent("code") + "=" + encodeURIComponent(code),
|
||||
)
|
||||
urlEncodedDataPairs.push(
|
||||
encodeURIComponent("redirect_uri") + "=" + encodeURIComponent(redirect_uri),
|
||||
)
|
||||
urlEncodedDataPairs.push(
|
||||
encodeURIComponent("scope") + "=" + encodeURIComponent(scope),
|
||||
)
|
||||
|
||||
urlEncodedData = urlEncodedDataPairs.join("&").replace(/%20/g, "+")
|
||||
|
||||
return cy.request({
|
||||
method: "POST",
|
||||
url: token_endpoint,
|
||||
form: true,
|
||||
body: urlEncodedData,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: "Basic " + btoa(client_id + ":" + client_secret),
|
||||
},
|
||||
})
|
||||
}
|
||||
13
test/e2e/cypress/helpers/react.ts
Normal file
13
test/e2e/cypress/helpers/react.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { SPA_URL } from "./index"
|
||||
|
||||
export const routes = {
|
||||
base: SPA_URL,
|
||||
login: SPA_URL + "/login",
|
||||
registration: SPA_URL + "/registration",
|
||||
settings: SPA_URL + "/settings",
|
||||
recovery: SPA_URL + "/recovery",
|
||||
verification: SPA_URL + "/verification",
|
||||
}
|
||||
70
test/e2e/cypress/helpers/webhook.ts
Normal file
70
test/e2e/cypress/helpers/webhook.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { fail } from "assert"
|
||||
import { gen } from "."
|
||||
|
||||
const WEBHOOK_TARGET = "https://webhook-target-gsmwn5ab4a-uc.a.run.app"
|
||||
const documentUrl = (key: string) => `${WEBHOOK_TARGET}/documents/${key}`
|
||||
const jsonnet = Buffer.from("function(ctx) ctx").toString("base64")
|
||||
|
||||
export const testFlowWebhook = (
|
||||
configSetup: (
|
||||
hooks: Array<{ hook: string; config?: any }>,
|
||||
) => Cypress.Chainable<void>,
|
||||
act: () => Cypress.Chainable<void> | void,
|
||||
) => {
|
||||
const documentID = gen.password()
|
||||
configSetup([
|
||||
{
|
||||
hook: "web_hook",
|
||||
config: {
|
||||
body: "base64://" + jsonnet,
|
||||
url: documentUrl(documentID),
|
||||
method: "PUT",
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const transient_payload = {
|
||||
stuff: {
|
||||
blub: [42, 3.14152],
|
||||
fu: "bar",
|
||||
},
|
||||
consent: true,
|
||||
}
|
||||
cy.intercept(
|
||||
"POST",
|
||||
/.*\/self-service\/(registration|login|recovery|verification|settings).*/,
|
||||
(req) => {
|
||||
switch (typeof req.body) {
|
||||
case "string":
|
||||
req.body =
|
||||
req.body +
|
||||
"&transient_payload=" +
|
||||
encodeURIComponent(JSON.stringify(transient_payload))
|
||||
break
|
||||
case "object":
|
||||
req.body = {
|
||||
...req.body,
|
||||
transient_payload,
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
fail()
|
||||
break
|
||||
}
|
||||
req.continue()
|
||||
},
|
||||
)
|
||||
|
||||
act()
|
||||
|
||||
cy.request(documentUrl(documentID)).then(({ body, status }) => {
|
||||
const b = JSON.parse(body)
|
||||
expect(status).to.equal(200)
|
||||
expect(b.identity).is.not.undefined
|
||||
expect(b.flow.transient_payload).to.deep.equal(transient_payload)
|
||||
})
|
||||
}
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
31
test/e2e/cypress/plugins/index.js
Normal file
31
test/e2e/cypress/plugins/index.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
const got = require("got")
|
||||
const CRI = require("chrome-remote-interface")
|
||||
let criPort = 0,
|
||||
criClient = null
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
module.exports = (on) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
|
||||
function ensureRdpPort(args) {
|
||||
const existing = args.find(
|
||||
(arg) => arg.slice(0, 23) === "--remote-debugging-port",
|
||||
)
|
||||
|
||||
if (existing) {
|
||||
return Number(existing.split("=")[1])
|
||||
}
|
||||
|
||||
const port = 40000 + Math.round(Math.random() * 25000)
|
||||
args.push(`--remote-debugging-port=${port}`)
|
||||
return port
|
||||
}
|
||||
1618
test/e2e/cypress/support/commands.ts
Normal file
1618
test/e2e/cypress/support/commands.ts
Normal file
File diff suppressed because it is too large
Load Diff
172
test/e2e/cypress/support/configHelpers.ts
Normal file
172
test/e2e/cypress/support/configHelpers.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { OryKratosConfiguration } from "../../shared/config"
|
||||
|
||||
export class ConfigBuilder {
|
||||
constructor(readonly config: OryKratosConfiguration) {}
|
||||
|
||||
public build() {
|
||||
return this.config
|
||||
}
|
||||
|
||||
public longRecoveryLifespan() {
|
||||
this.config.selfservice.flows.recovery.lifespan = "1h"
|
||||
return this
|
||||
}
|
||||
|
||||
public longLinkLifespan() {
|
||||
this.config.selfservice.methods.link.config.lifespan = "1m"
|
||||
return this
|
||||
}
|
||||
|
||||
public disableVerification() {
|
||||
this.config.selfservice.flows.verification.enabled = false
|
||||
return this
|
||||
}
|
||||
|
||||
public disableCodeMfa() {
|
||||
this.config.selfservice.methods.code.mfa_enabled = false
|
||||
return this
|
||||
}
|
||||
|
||||
public enableRecovery() {
|
||||
if (!this.config.selfservice.flows.recovery) {
|
||||
this.config.selfservice.flows.recovery = {}
|
||||
}
|
||||
this.config.selfservice.flows.recovery.enabled = true
|
||||
return this
|
||||
}
|
||||
|
||||
public disableRecovery() {
|
||||
this.config.selfservice.flows.recovery.enabled = false
|
||||
return this
|
||||
}
|
||||
|
||||
public enableVerification() {
|
||||
this.config.selfservice.flows.verification.enabled = true
|
||||
return this
|
||||
}
|
||||
|
||||
public useRecoveryStrategy(strategy: "link" | "code") {
|
||||
if (!this.config.selfservice.flows.recovery) {
|
||||
this.config.selfservice.flows.recovery = {}
|
||||
}
|
||||
this.config.selfservice.flows.recovery.use = strategy
|
||||
if (!this.config.selfservice.methods[strategy]) {
|
||||
this.config.selfservice.methods[strategy] = {}
|
||||
}
|
||||
this.config.selfservice.methods[strategy].enabled = true
|
||||
return this
|
||||
}
|
||||
|
||||
public disableRecoveryStrategy(strategy: "link" | "code") {
|
||||
this.config.selfservice.methods[strategy].enabled = false
|
||||
return this
|
||||
}
|
||||
|
||||
public notifyUnknownRecipients(
|
||||
flow: "recovery" | "verification",
|
||||
value: boolean,
|
||||
) {
|
||||
this.config.selfservice.flows[flow].notify_unknown_recipients = value
|
||||
return this
|
||||
}
|
||||
|
||||
public longCodeLifespan() {
|
||||
this.config.selfservice.methods.code.config.lifespan = "1m"
|
||||
return this
|
||||
}
|
||||
|
||||
public shortCodeLifespan() {
|
||||
this.config.selfservice.methods.code.config.lifespan = "1ms"
|
||||
return this
|
||||
}
|
||||
|
||||
public longPrivilegedSessionTime() {
|
||||
this.config.selfservice.flows.settings.privileged_session_max_age = "5m"
|
||||
return this
|
||||
}
|
||||
|
||||
public requireStrictAal() {
|
||||
this.config.selfservice.flows.settings.required_aal = "highest_available"
|
||||
this.config.session.whoami.required_aal = "highest_available"
|
||||
return this
|
||||
}
|
||||
|
||||
public enableLoginForVerifiedAddressOnly() {
|
||||
this.config.selfservice.flows.login["after"] = {
|
||||
password: { hooks: [{ hook: "require_verified_address" }] },
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
public longVerificationLifespan() {
|
||||
this.config.selfservice.flows.verification.lifespan = "1m"
|
||||
return this
|
||||
}
|
||||
|
||||
public longLifespan(strategy: "link" | "code") {
|
||||
this.config.selfservice.methods[strategy].config.lifespan = "1m"
|
||||
return this
|
||||
}
|
||||
|
||||
public useVerificationStrategy(strategy: "link" | "code") {
|
||||
this.config.selfservice.flows.verification.use = strategy
|
||||
if (!this.config.selfservice.methods[strategy]) {
|
||||
this.config.selfservice.methods[strategy] = {}
|
||||
}
|
||||
this.config.selfservice.methods[strategy].enabled = true
|
||||
return this
|
||||
}
|
||||
|
||||
public resetCourierTemplates(
|
||||
type: "recovery" | "verification" | "recovery_code" | "verification_code",
|
||||
) {
|
||||
if (
|
||||
this.config.courier?.templates &&
|
||||
type in this.config.courier.templates
|
||||
) {
|
||||
delete this.config.courier.templates[type]
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
public useLaxSessionAal() {
|
||||
this.config.session.whoami.required_aal = "aal1"
|
||||
return this
|
||||
}
|
||||
|
||||
public useLaxSettingsFlowAal() {
|
||||
this.config.selfservice.flows.settings.required_aal = "aal1"
|
||||
return this
|
||||
}
|
||||
|
||||
public useLaxAal() {
|
||||
return this.useLaxSessionAal().useLaxSettingsFlowAal()
|
||||
}
|
||||
|
||||
public useHighestSessionAal() {
|
||||
this.config.session.whoami.required_aal = "highest_available"
|
||||
return this
|
||||
}
|
||||
|
||||
public useHighestSettingsFlowAal() {
|
||||
this.config.selfservice.flows.settings.required_aal = "highest_available"
|
||||
return this
|
||||
}
|
||||
|
||||
public useHighestAvailable() {
|
||||
return this.useHighestSessionAal().useHighestSettingsFlowAal()
|
||||
}
|
||||
|
||||
public enableCode() {
|
||||
this.config.selfservice.methods.code.enabled = true
|
||||
return this
|
||||
}
|
||||
|
||||
public enableCodeMFA() {
|
||||
this.config.selfservice.methods.code.mfa_enabled = true
|
||||
return this
|
||||
}
|
||||
}
|
||||
740
test/e2e/cypress/support/index.d.ts
vendored
Normal file
740
test/e2e/cypress/support/index.d.ts
vendored
Normal file
@@ -0,0 +1,740 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Session as KratosSession } from "@ory/kratos-client"
|
||||
import { OryKratosConfiguration } from "../../shared/config"
|
||||
import { ConfigBuilder } from "./configHelpers"
|
||||
|
||||
export interface MailMessage {
|
||||
fromAddress: string
|
||||
toAddresses: Array<string>
|
||||
body: string
|
||||
subject: string
|
||||
}
|
||||
|
||||
export type Strategy = "code" | "link"
|
||||
type app = "express" | "react"
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Delete mails from the email server.
|
||||
*
|
||||
* @param options
|
||||
*/
|
||||
deleteMail(options: { atLeast?: boolean }): Chainable<void>
|
||||
|
||||
/**
|
||||
* Adds end enables a WebAuth authenticator key.
|
||||
*/
|
||||
addVirtualAuthenticator(): Chainable<any>
|
||||
|
||||
/**
|
||||
* Fetch the browser's Ory Session.
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
getSession(opts?: {
|
||||
expectAal?: "aal2" | "aal1"
|
||||
expectMethods?: Array<
|
||||
| "password"
|
||||
| "webauthn"
|
||||
| "lookup_secret"
|
||||
| "totp"
|
||||
| "code"
|
||||
| "passkey"
|
||||
>
|
||||
token?: string
|
||||
}): Chainable<KratosSession>
|
||||
|
||||
/**
|
||||
* Expect that the browser has no valid Ory Kratos Cookie Session.
|
||||
*/
|
||||
noSession(): Chainable<Response<any>>
|
||||
|
||||
/**
|
||||
* Log a user in
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
login(opts: {
|
||||
email: string
|
||||
password: string
|
||||
expectSession?: boolean
|
||||
cookieUrl?: string
|
||||
}): Chainable<Response<KratosSession | undefined>>
|
||||
|
||||
/**
|
||||
* Sign up a user
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
register(opts: {
|
||||
email: string
|
||||
password: string
|
||||
query?: { [key: string]: string }
|
||||
fields?: { [key: string]: any }
|
||||
}): Chainable<Response<void>>
|
||||
|
||||
/**
|
||||
* Register a user with a code
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
registerWithCode(opts: {
|
||||
email: string
|
||||
code?: string
|
||||
traits?: { [key: string]: any }
|
||||
query?: { [key: string]: string }
|
||||
expectedMailCount?: number
|
||||
}): Chainable<Response<void>>
|
||||
|
||||
/**
|
||||
* Updates a user's settings using an API flow
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
settingsApi(opts: {
|
||||
fields?: { [key: string]: any }
|
||||
}): Chainable<Response<void>>
|
||||
|
||||
/**
|
||||
* Set the "privileged session lifespan" to a large value.
|
||||
*/
|
||||
longPrivilegedSessionTime(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Return and optionally delete an email from the fake email server.
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
getMail(opts: {
|
||||
removeMail?: boolean
|
||||
expectedCount?: number
|
||||
email?: string
|
||||
subject?: string
|
||||
body?: string
|
||||
}): Chainable<MailMessage>
|
||||
|
||||
performEmailVerification(opts?: {
|
||||
expect?: { email?: string; redirectTo?: string }
|
||||
strategy?: Strategy
|
||||
useLinkFromEmail?: boolean
|
||||
}): Chainable<void>
|
||||
|
||||
/**
|
||||
* Sets the Ory Kratos configuration profile.
|
||||
*
|
||||
* @param profile
|
||||
*/
|
||||
useConfigProfile(profile: string): Chainable<void>
|
||||
|
||||
/**
|
||||
* Register a new user with email + password via the API.
|
||||
*
|
||||
* Recommended to use if you need an Ory Session Token or alternatively the user created.
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
registerApi(opts?: {
|
||||
email: string
|
||||
password: string
|
||||
fields: { [key: string]: string }
|
||||
}): Chainable<KratosSession>
|
||||
|
||||
/**
|
||||
* Submits a recovery flow via the API
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
recoverApi(opts: { email: string; returnTo?: string }): Chainable<void>
|
||||
|
||||
/**
|
||||
* Submits a verification flow via the API
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
verificationApi(opts: {
|
||||
email: string
|
||||
returnTo?: string
|
||||
strategy?: Strategy
|
||||
}): Chainable<void>
|
||||
|
||||
/**
|
||||
* Update the config file
|
||||
*
|
||||
* @param cb
|
||||
*/
|
||||
updateConfigFile(cb: (arg: OryKratosConfiguration) => any): Chainable<any>
|
||||
|
||||
/**
|
||||
* Submits a verification flow via the API
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
verificationApiExpired(opts: {
|
||||
email: string
|
||||
strategy?: Strategy
|
||||
returnTo?: string
|
||||
}): Chainable<void>
|
||||
|
||||
/**
|
||||
* Sets the hook.
|
||||
*
|
||||
* @param hooks
|
||||
*/
|
||||
setupHooks(
|
||||
flow:
|
||||
| "registration"
|
||||
| "login"
|
||||
| "recovery"
|
||||
| "verification"
|
||||
| "settings",
|
||||
phase: "before" | "after",
|
||||
kind: "password" | "webauthn" | "oidc" | "code" | "passkey",
|
||||
hooks: Array<{ hook: string; config?: any }>,
|
||||
): Chainable<void>
|
||||
|
||||
/**
|
||||
* Sets the post registration hook.
|
||||
*
|
||||
* @param hooks
|
||||
*/
|
||||
setPostPasswordRegistrationHooks(
|
||||
hooks: Array<{ hook: string; config?: any }>,
|
||||
): Chainable<void>
|
||||
|
||||
/**
|
||||
* Sets the post code registration hook.
|
||||
*
|
||||
* @param hooks
|
||||
*/
|
||||
setPostCodeRegistrationHooks(
|
||||
hooks: Array<{ hook: string; config?: any }>,
|
||||
): Chainable<void>
|
||||
|
||||
/**
|
||||
* Submits a verification flow via the Browser
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
verificationBrowser(opts: {
|
||||
email: string
|
||||
returnTo?: string
|
||||
}): Chainable<void>
|
||||
|
||||
/**
|
||||
* Changes the config so that the login flow lifespan is very short.
|
||||
*
|
||||
*
|
||||
* Useful when testing expiry of login flows.
|
||||
* @see longLoginLifespan()
|
||||
*/
|
||||
shortLoginLifespan(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Changes the config so that the login flow lifespan is very long.
|
||||
*
|
||||
* Useful when testing expiry of login flows.
|
||||
*
|
||||
* @see shortLoginLifespan()
|
||||
*/
|
||||
longLoginLifespan(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Change the config so that `https://www.ory.com/` is a allowed return to URL.
|
||||
*/
|
||||
browserReturnUrlOry(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Change the courier recovery invalid and valid templates to remote base64 strings
|
||||
*/
|
||||
remoteCourierRecoveryTemplates(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Resets the remote courier templates for the given template type to their default values
|
||||
*/
|
||||
resetCourierTemplates(
|
||||
type: "recovery_code" | "recovery" | "verification",
|
||||
): Chainable<void>
|
||||
|
||||
/**
|
||||
* Change the courier recovery code invalid and valid templates to remote base64 strings
|
||||
*/
|
||||
remoteCourierRecoveryCodeTemplates(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Changes the config so that the registration flow lifespan is very short.
|
||||
*
|
||||
* Useful when testing expiry of registration flows.
|
||||
*
|
||||
* @see longRegisterLifespan()
|
||||
*/
|
||||
shortRegisterLifespan(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Changes the config so that the registration flow lifespan is very long.
|
||||
*
|
||||
* Useful when testing expiry of registration flows.
|
||||
*
|
||||
* @see shortRegisterLifespan()
|
||||
*/
|
||||
longRegisterLifespan(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Changes the config so that the settings privileged lifespan is very long.
|
||||
*
|
||||
* Useful when testing privileged settings flows.
|
||||
*
|
||||
* @see longPrivilegedSessionTime()
|
||||
*/
|
||||
shortPrivilegedSessionTime(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Re-authenticates a user.
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
reauth(opts: {
|
||||
expect: { email: string; success?: boolean }
|
||||
type: { email?: string; password?: string }
|
||||
}): Chainable<void>
|
||||
|
||||
/**
|
||||
* Change the config file to support lookup secrets
|
||||
* @param value
|
||||
*/
|
||||
useLookupSecrets(value: boolean): Chainable<void>
|
||||
|
||||
/**
|
||||
* Re-authenticates a user.
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
reauthWithOtherAccount(opts: {
|
||||
previousUrl: string
|
||||
expect: { email: string; success?: boolean }
|
||||
type: { email?: string; password?: string }
|
||||
}): Chainable<void>
|
||||
|
||||
/**
|
||||
* Do not require 2fa for /session/whoami
|
||||
*/
|
||||
sessionRequiresNo2fa(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Require 2fa for /session/whoami if available
|
||||
*/
|
||||
sessionRequires2fa(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Like sessionRequires2fa but sets this also for settings
|
||||
*/
|
||||
requireStrictAal(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Like sessionRequiresNo2fa but sets this also for settings
|
||||
*/
|
||||
useLaxAal(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Gets the lookup codes from the settings page
|
||||
*/
|
||||
getLookupSecrets(): Chainable<Array<string>>
|
||||
|
||||
/**
|
||||
* Expect the settings to be saved.
|
||||
*/
|
||||
expectSettingsSaved(): Chainable<void>
|
||||
|
||||
clearCookies(
|
||||
options?: Partial<Loggable & Timeoutable & { domain: null | string }>,
|
||||
): Chainable<null>
|
||||
|
||||
/**
|
||||
* Submits a password form by clicking the button with method=password
|
||||
*/
|
||||
submitPasswordForm(): Chainable<null>
|
||||
|
||||
/**
|
||||
* Submits a profile form by clicking the button with method=profile
|
||||
*/
|
||||
submitProfileForm(app?: "mobile" | "express" | "react"): Chainable<null>
|
||||
|
||||
/**
|
||||
* Submits a code form by clicking the button with method=code
|
||||
*/
|
||||
submitCodeForm(app: "mobile" | "express" | "react"): Chainable<void>
|
||||
|
||||
/**
|
||||
* Expect a CSRF error to occur
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
shouldHaveCsrfError(opts: { app: string }): Chainable<void>
|
||||
|
||||
/**
|
||||
* Expects the app to error if a return_to is used which isn't allowed.
|
||||
*
|
||||
* @param init
|
||||
* @param opts
|
||||
*/
|
||||
shouldErrorOnDisallowedReturnTo(
|
||||
init: string,
|
||||
opts: { app: string },
|
||||
): Chainable<void>
|
||||
|
||||
/**
|
||||
* Expect that the second factor login screen is shown
|
||||
*/
|
||||
shouldShow2FAScreen(): Chainable<void>
|
||||
|
||||
/** Click a webauthn button
|
||||
*
|
||||
* @param type
|
||||
*/
|
||||
clickWebAuthButton(type: "login" | "register"): Chainable<void>
|
||||
|
||||
/**
|
||||
* Sign up a user using Social Sign In
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
registerOidc(opts: {
|
||||
app: app
|
||||
email?: string
|
||||
website?: string
|
||||
scopes?: Array<string>
|
||||
rememberLogin?: boolean
|
||||
rememberConsent?: boolean
|
||||
acceptLogin?: boolean
|
||||
acceptConsent?: boolean
|
||||
expectSession?: boolean
|
||||
route?: string
|
||||
}): Chainable<void>
|
||||
|
||||
/**
|
||||
* Sign in a user using Social Sign In
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
loginOidc(opts: {
|
||||
app: app
|
||||
expectSession?: boolean
|
||||
url?: string
|
||||
preTriggerHook?: () => void
|
||||
}): Chainable<void>
|
||||
|
||||
/**
|
||||
* Triggers a Social Sign In flow for the given provider
|
||||
*
|
||||
* @param app
|
||||
* @param provider
|
||||
*/
|
||||
triggerOidc(app: "react" | "express", provider?: string): Chainable<void>
|
||||
|
||||
/**
|
||||
* Changes the config so that the recovery privileged lifespan is very long.
|
||||
*
|
||||
* Useful when testing privileged recovery flows.
|
||||
*
|
||||
* @see shortPrivilegedRecoveryTime()
|
||||
*/
|
||||
longRecoveryLifespan(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Changes the config so that the recovery privileged lifespan is very short.
|
||||
*
|
||||
* Useful when testing privileged recovery flows.
|
||||
*
|
||||
* @see shortPrivilegedRecoveryTime()
|
||||
*/
|
||||
shortRecoveryLifespan(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Changes the config so that the verification privileged lifespan is very long.
|
||||
*
|
||||
* Useful when testing recovery/verification flows.
|
||||
*
|
||||
* @see shortLinkLifespan()
|
||||
*/
|
||||
longVerificationLifespan(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Changes the config so that the verification privileged lifespan is very short.
|
||||
*
|
||||
* Useful when testing privileged verification flows.
|
||||
*
|
||||
* @see shortPrivilegedVerificationTime()
|
||||
*/
|
||||
shortVerificationLifespan(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Log a user out
|
||||
*/
|
||||
logout(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Deletes all mail in the mail mock server.
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
deleteMail(opts?: { atLeast?: number }): Chainable<void>
|
||||
|
||||
/**
|
||||
* Changes the config so that the link lifespan is very long.
|
||||
*
|
||||
* Useful when testing recovery/verification flows.
|
||||
*
|
||||
* @see shortLinkLifespan()
|
||||
*/
|
||||
longLinkLifespan(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Changes the config so that the link lifespan is very short.
|
||||
*
|
||||
* Useful when testing recovery/verification flows.
|
||||
*
|
||||
* @see longLinkLifespan()
|
||||
*/
|
||||
shortLinkLifespan(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Changes the config so that the code lifespan is very short.
|
||||
*
|
||||
* Useful when testing recovery/verification flows.
|
||||
*
|
||||
* @see longCodeLifespan()
|
||||
*/
|
||||
shortCodeLifespan(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Sets the `lifespan` of a strategy to 1ms (a short value)
|
||||
*
|
||||
* Useful to test the behavior if the subject of the strategy expired
|
||||
*
|
||||
* @param s the strategy
|
||||
*/
|
||||
shortLifespan(s: Strategy): Chainable<void>
|
||||
|
||||
/**
|
||||
* Sets the `lifespan` of a strategy to 1m
|
||||
*
|
||||
* @param s the strategy
|
||||
*/
|
||||
longLifespan(s: Strategy): Chainable<void>
|
||||
|
||||
/**
|
||||
* Changes the config so that the code lifespan is very long.
|
||||
*
|
||||
* Useful when testing recovery/verification flows.
|
||||
*
|
||||
* @see shortCodeLifespan()
|
||||
*/
|
||||
longCodeLifespan(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Expect a recovery email which is expired.
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
recoverEmailButExpired(opts?: {
|
||||
expect: { email: string }
|
||||
}): Chainable<void>
|
||||
|
||||
/**
|
||||
* Expect a recovery email with a recovery code.
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
recoveryEmailWithCode(opts?: {
|
||||
expect: { email: string; enterCode?: boolean }
|
||||
}): Chainable<void>
|
||||
|
||||
/**
|
||||
* Expect a verification email which is expired.
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
verifyEmailButExpired(opts?: {
|
||||
expect: { email: string }
|
||||
strategy?: Strategy
|
||||
}): Chainable<void>
|
||||
|
||||
/**
|
||||
* Disables verification
|
||||
*/
|
||||
disableVerification(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Enables verification
|
||||
*/
|
||||
enableVerification(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Enables recovery
|
||||
*/
|
||||
enableRecovery(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Sets the recovery strategy to use
|
||||
*/
|
||||
useRecoveryStrategy(strategy: Strategy): Chainable<void>
|
||||
|
||||
/**
|
||||
* Disables a specific recovery strategy
|
||||
*
|
||||
* @param strategy the recovery strategy to disable
|
||||
*/
|
||||
disableRecoveryStrategy(strategy: Strategy): Chainable<void>
|
||||
|
||||
/**
|
||||
* Disabled recovery
|
||||
*/
|
||||
disableRecovery(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Disables registration
|
||||
*/
|
||||
disableRegistration(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Enables registration
|
||||
*/
|
||||
enableRegistration(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Expect a recovery email which is valid.
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
recoverEmail(opts: {
|
||||
expect: { email: string }
|
||||
shouldVisit?: boolean
|
||||
}): Chainable<MailMessage>
|
||||
|
||||
/**
|
||||
* Expect a verification email which is valid.
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
verifyEmail(opts: {
|
||||
expect: { email: string; password?: string; redirectTo?: string }
|
||||
strategy?: Strategy
|
||||
shouldVisit?: boolean
|
||||
}): Chainable<void>
|
||||
|
||||
/**
|
||||
* Configures a hook which only allows verified email addresses to sign in.
|
||||
*/
|
||||
enableLoginForVerifiedAddressOnly(): Chainable<void>
|
||||
|
||||
/**
|
||||
* Sets the value for the `notify_unknown_recipients` key for a flow
|
||||
*
|
||||
* @param flow the flow for which to set the config value
|
||||
* @param value the value, defaults to true
|
||||
*/
|
||||
notifyUnknownRecipients(
|
||||
flow: "recovery" | "verification",
|
||||
value?: boolean,
|
||||
): Chainable<void>
|
||||
|
||||
/**
|
||||
* Sign a user in via the API and return the session.
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
loginApi(opts: {
|
||||
email: string
|
||||
password: string
|
||||
}): Chainable<{ session: KratosSession }>
|
||||
|
||||
/**
|
||||
* Same as loginApi but uses dark magic to avoid cookie issues.
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
loginApiWithoutCookies(opts: {
|
||||
email: string
|
||||
password: string
|
||||
}): Chainable<{ session: KratosSession }>
|
||||
|
||||
/**
|
||||
* Which app to proxy
|
||||
*/
|
||||
proxy(app: "react" | "express"): Chainable<void>
|
||||
|
||||
/**
|
||||
* Log a user in on mobile
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
loginMobile(opts: { email: string; password: string }): Chainable<void>
|
||||
|
||||
/**
|
||||
* Set the identity schema
|
||||
* @param schema
|
||||
*/
|
||||
setIdentitySchema(schema: string): Chainable<void>
|
||||
|
||||
/**
|
||||
* Set the default schema
|
||||
* @param id
|
||||
*/
|
||||
setDefaultIdentitySchema(id: string): Chainable<void>
|
||||
|
||||
/**
|
||||
* Remove the specified attribute from the given HTML elements
|
||||
*/
|
||||
removeAttribute(selectors: string[], attribute: string): Chainable<void>
|
||||
|
||||
/**
|
||||
* Add an input element to the DOM as a child of the given parent
|
||||
*/
|
||||
addInputElement(
|
||||
parent: string,
|
||||
attribute: string,
|
||||
value: string,
|
||||
): Chainable<void>
|
||||
|
||||
/**
|
||||
* Fetches the courier messages from the admin API
|
||||
*/
|
||||
getCourierMessages(): Chainable<
|
||||
{ recipient: string; template_type: string }[]
|
||||
>
|
||||
|
||||
/**
|
||||
* Enable the verification UI after registration hook
|
||||
*/
|
||||
enableVerificationUIAfterRegistration(
|
||||
strategy: "password" | "oidc" | "webauthn",
|
||||
): Chainable<void>
|
||||
|
||||
/**
|
||||
* Extracts a verification code from the received email
|
||||
*/
|
||||
getVerificationCodeFromEmail(email: string): Chainable<string>
|
||||
|
||||
/**
|
||||
* Extracts a registration code from the received email
|
||||
*/
|
||||
getRegistrationCodeFromEmail(
|
||||
email: string,
|
||||
opts?: { expectedCount: number; removeMail?: boolean },
|
||||
): Chainable<string>
|
||||
|
||||
/**
|
||||
* Extracts a login code from the received email
|
||||
*/
|
||||
getLoginCodeFromEmail(
|
||||
email: string,
|
||||
opts?: { expectedCount: number },
|
||||
): Chainable<string>
|
||||
|
||||
useConfig(cb: (config: ConfigBuilder) => ConfigBuilder): Chainable<void>
|
||||
}
|
||||
}
|
||||
}
|
||||
6
test/e2e/cypress/support/index.js
Normal file
6
test/e2e/cypress/support/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import "./commands"
|
||||
|
||||
Cypress.Cookies.debug(true)
|
||||
9
test/e2e/cypress/tsconfig.json
Normal file
9
test/e2e/cypress/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["es2015", "dom"],
|
||||
"types": ["cypress", "node"],
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["**/*.ts", "support/index.ts", "../shared/config.d.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user