Skip to content

Commit 1e55e05

Browse files
committed
chore: add cypress tests
1 parent df22c9e commit 1e55e05

File tree

5 files changed

+289
-1
lines changed

5 files changed

+289
-1
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright © 2022 Ory Corp
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { prng } from "../../helpers"
5+
6+
const accessTokenStrategies = ["opaque", "jwt"]
7+
8+
describe("The OAuth 2.0 Device Authorization Grant", function () {
9+
accessTokenStrategies.forEach((accessTokenStrategy) => {
10+
describe("access_token_strategy=" + accessTokenStrategy, function () {
11+
const nc = (extradata) => ({
12+
client_secret: prng(),
13+
scope: "offline_access openid",
14+
subject_type: "public",
15+
token_endpoint_auth_method: "client_secret_basic",
16+
grant_types: [
17+
"urn:ietf:params:oauth:grant-type:device_code",
18+
"refresh_token",
19+
],
20+
access_token_strategy: accessTokenStrategy,
21+
...extradata,
22+
})
23+
24+
it("should return an Access, Refresh, and ID Token when scope offline_access and openid are granted", function () {
25+
const client = nc()
26+
cy.deviceAuthFlow(client, {
27+
consent: { scope: ["offline_access", "openid"] },
28+
})
29+
30+
cy.postDeviceAuthFlow().then((resp) => {
31+
const {
32+
result,
33+
token: { access_token, id_token, refresh_token },
34+
} = resp.body
35+
36+
expect(result).to.equal("success")
37+
expect(access_token).to.not.be.empty
38+
expect(id_token).to.not.be.empty
39+
expect(refresh_token).to.not.be.empty
40+
})
41+
})
42+
43+
it("should return an Access and Refresh Token when scope offline_access is granted", function () {
44+
const client = nc()
45+
cy.deviceAuthFlow(client, { consent: { scope: ["offline_access"] } })
46+
47+
cy.postDeviceAuthFlow().then((resp) => {
48+
console.log(resp)
49+
const {
50+
result,
51+
token: { access_token, id_token, refresh_token },
52+
} = resp.body
53+
54+
expect(result).to.equal("success")
55+
expect(access_token).to.not.be.empty
56+
expect(id_token).to.be.undefined
57+
expect(refresh_token).to.not.be.empty
58+
})
59+
})
60+
61+
it("should return an Access and ID Token when scope offline_access is granted", function () {
62+
const client = nc()
63+
cy.deviceAuthFlow(client, { consent: { scope: ["openid"] } })
64+
65+
cy.postDeviceAuthFlow().then((resp) => {
66+
console.log(resp)
67+
const {
68+
result,
69+
token: { access_token, id_token, refresh_token },
70+
} = resp.body
71+
72+
expect(result).to.equal("success")
73+
expect(access_token).to.not.be.empty
74+
expect(id_token).to.not.be.empty
75+
expect(refresh_token).to.be.undefined
76+
})
77+
})
78+
79+
it("should return an Access Token when no scope is granted", function () {
80+
const client = nc()
81+
cy.deviceAuthFlow(client, { consent: { scope: [] } })
82+
83+
cy.postDeviceAuthFlow().then((resp) => {
84+
console.log(resp)
85+
const {
86+
result,
87+
token: { access_token, id_token, refresh_token },
88+
} = resp.body
89+
90+
expect(result).to.equal("success")
91+
expect(access_token).to.not.be.empty
92+
expect(id_token).to.be.undefined
93+
expect(refresh_token).to.be.undefined
94+
})
95+
})
96+
97+
it("should skip consent if the client is confgured thus", function () {
98+
const client = nc({ skip_consent: true })
99+
cy.deviceAuthFlow(client, {
100+
consent: { scope: ["offline_access", "openid"], skip: true },
101+
})
102+
103+
cy.postDeviceAuthFlow().then((resp) => {
104+
console.log(resp)
105+
const {
106+
result,
107+
token: { access_token, id_token, refresh_token },
108+
} = resp.body
109+
110+
expect(result).to.equal("success")
111+
expect(access_token).to.not.be.empty
112+
expect(id_token).to.not.be.empty
113+
expect(refresh_token).to.not.be.empty
114+
})
115+
})
116+
})
117+
})
118+
})

cypress/support/commands.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,90 @@ Cypress.Commands.add("refreshTokenBrowser", (client, token) =>
216216
failOnStatusCode: false,
217217
}),
218218
)
219+
220+
Cypress.Commands.add(
221+
"deviceAuthFlow",
222+
(
223+
client,
224+
{
225+
override: { scope, client_id, client_secret } = {},
226+
consent: {
227+
accept: acceptConsent = true,
228+
skip: skipConsent = false,
229+
remember: rememberConsent = false,
230+
scope: acceptScope = [],
231+
} = {},
232+
login: {
233+
accept: acceptLogin = true,
234+
skip: skipLogin = false,
235+
remember: rememberLogin = false,
236+
username = "[email protected]",
237+
password = "foobar",
238+
} = {},
239+
prompt = "",
240+
createClient: doCreateClient = true,
241+
} = {},
242+
path = "oauth2",
243+
) => {
244+
const run = (client) => {
245+
cy.visit(
246+
`${Cypress.env("client_url")}/${path}/device?client_id=${
247+
client_id || client.client_id
248+
}&client_secret=${client_secret || client.client_secret}&scope=${
249+
scope || client.scope
250+
}`,
251+
{ failOnStatusCode: false },
252+
)
253+
254+
cy.get("#verify").click()
255+
256+
if (!skipLogin) {
257+
cy.get("#email").type(username, { delay: 1 })
258+
cy.get("#password").type(password, { delay: 1 })
259+
260+
if (rememberLogin) {
261+
cy.get("#remember").click()
262+
}
263+
264+
if (acceptLogin) {
265+
cy.get("#accept").click()
266+
} else {
267+
cy.get("#reject").click()
268+
}
269+
}
270+
271+
if (!skipConsent) {
272+
acceptScope.forEach((s) => {
273+
cy.get(`#${s}`).click()
274+
})
275+
276+
if (rememberConsent) {
277+
cy.get("#remember").click()
278+
}
279+
280+
if (acceptConsent) {
281+
cy.get("#accept").click()
282+
} else {
283+
cy.get("#reject").click()
284+
}
285+
286+
cy.location().should((loc) => {
287+
expect(loc.origin).to.eq(Cypress.env("consent_url"))
288+
expect(loc.pathname).to.eq("/oauth2/device/success")
289+
})
290+
}
291+
}
292+
293+
if (doCreateClient) {
294+
createClient(client).should((client) => {
295+
run(client)
296+
})
297+
return
298+
}
299+
run(client)
300+
},
301+
)
302+
303+
Cypress.Commands.add("postDeviceAuthFlow", (path = "oauth2") =>
304+
cy.request(`${Cypress.env("client_url")}/${path}/device/success`),
305+
)

test/e2e/circle-ci.bash

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ fi
3838
(cd oauth2-client; PORT=5002 HYDRA_ADMIN_URL=http://127.0.0.1:5001 npm run consent > ../login-consent-logout.e2e.log 2>&1 &)
3939

4040
export URLS_SELF_ISSUER=http://127.0.0.1:5004/
41+
export URLS_DEVICE_VERIFICATION=http://127.0.0.1:5002/device/verify
42+
export URLS_DEVICE_SUCCESS=http://127.0.0.1:5002/oauth2/device/success
4143
export URLS_CONSENT=http://127.0.0.1:5002/consent
4244
export URLS_LOGIN=http://127.0.0.1:5002/login
4345
export URLS_LOGOUT=http://127.0.0.1:5002/logout

test/e2e/oauth2-client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"express": "^4.21.2",
1515
"express-session": "^1.17.0",
1616
"express-winston": "^3.4.0",
17-
"hydra-login-consent-logout": "2.0.4-pre.2",
17+
"hydra-login-consent-logout": "2.4.0-pre.3",
1818
"jsonwebtoken": "^8.5.1",
1919
"jwks-rsa": "^2.1.4",
2020
"node-fetch": "^2.6.0",

test/e2e/oauth2-client/src/index.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,87 @@ app.get("/oauth2/callback", async (req, res) => {
152152
})
153153
})
154154

155+
app.get("/oauth2/device", async (req, res) => {
156+
const client = {
157+
id: req.query.client_id,
158+
secret: req.query.client_secret,
159+
}
160+
161+
const state = uuid.v4()
162+
const scope = req.query.scope || ""
163+
164+
req.session.client = client
165+
req.session.scope = scope.split(" ")
166+
167+
const params = new URLSearchParams()
168+
params.append("client_id", req.query.client_id)
169+
params.append("scope", scope)
170+
171+
let headers = new Headers()
172+
headers.set(
173+
"Authorization",
174+
"Basic " +
175+
Buffer.from(req.query.client_id + ":" + req.query.client_secret).toString(
176+
"base64",
177+
),
178+
)
179+
180+
fetch(new URL("/oauth2/device/auth", config.public).toString(), {
181+
method: "POST",
182+
body: params,
183+
headers: headers,
184+
})
185+
.then(isStatusOk)
186+
.then((res) => res.json())
187+
.then((body) => {
188+
// Store the device_code to use after authentication to get the tokens
189+
req.session.device_code = body?.device_code
190+
res.redirect(body?.verification_uri_complete)
191+
})
192+
.catch((err) => {
193+
res.send(JSON.stringify({ error: err.toString() }))
194+
})
195+
})
196+
197+
app.get("/oauth2/device/success", async (req, res) => {
198+
const clientId = req.session?.client?.id
199+
const clientSecret = req.session?.client?.secret
200+
201+
if (clientId === undefined || clientSecret === undefined) {
202+
res.send(
203+
JSON.stringify({
204+
result: "error",
205+
error: "no client credentials in session",
206+
}),
207+
)
208+
return
209+
}
210+
211+
const params = new URLSearchParams()
212+
params.append("client_id", clientId)
213+
params.append("device_code", req.session?.device_code)
214+
params.append("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
215+
let headers = new Headers()
216+
headers.set(
217+
"Authorization",
218+
"Basic " + Buffer.from(clientId + ":" + clientSecret).toString("base64"),
219+
)
220+
221+
fetch(new URL("/oauth2/token", config.public).toString(), {
222+
method: "POST",
223+
body: params,
224+
headers: headers,
225+
})
226+
.then(isStatusOk)
227+
.then((resp) => resp.json())
228+
.then((data) => {
229+
res.send({ result: "success", token: data })
230+
})
231+
.catch((err) => {
232+
res.send(JSON.stringify({ error: err.toString() }))
233+
})
234+
})
235+
155236
app.get("/oauth2/refresh", function (req, res) {
156237
oauth2
157238
.create(req.session.credentials)

0 commit comments

Comments
 (0)