Skip to content

Commit 3277d14

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

File tree

4 files changed

+299
-0
lines changed

4 files changed

+299
-0
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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(
31+
(resp) => {
32+
console.log(resp)
33+
const {
34+
result,
35+
token: { access_token, id_token, refresh_token },
36+
} = resp.body
37+
38+
expect(result).to.equal("success")
39+
expect(access_token).to.not.be.empty
40+
expect(id_token).to.not.be.empty
41+
expect(refresh_token).to.not.be.empty
42+
}
43+
)
44+
})
45+
46+
it("should return an Access and Refresh Token when scope offline_access is granted", function () {
47+
const client = nc()
48+
cy.deviceAuthFlow(client, { consent: { scope: ["offline_access"] } })
49+
50+
cy.postDeviceAuthFlow().then(
51+
(resp) => {
52+
console.log(resp)
53+
const {
54+
result,
55+
token: { access_token, id_token, refresh_token },
56+
} = resp.body
57+
58+
expect(result).to.equal("success")
59+
expect(access_token).to.not.be.empty
60+
expect(id_token).to.be.undefined
61+
expect(refresh_token).to.not.be.empty
62+
})
63+
})
64+
65+
it("should return an Access and ID Token when scope offline_access is granted", function () {
66+
const client = nc()
67+
cy.deviceAuthFlow(client, { consent: { scope: ["openid"] } })
68+
69+
cy.postDeviceAuthFlow().then(
70+
(resp) => {
71+
console.log(resp)
72+
const {
73+
result,
74+
token: { access_token, id_token, refresh_token },
75+
} = resp.body
76+
77+
expect(result).to.equal("success")
78+
expect(access_token).to.not.be.empty
79+
expect(id_token).to.not.be.empty
80+
expect(refresh_token).to.be.undefined
81+
})
82+
})
83+
84+
it("should return an Access Token when no scope is granted", function () {
85+
const client = nc()
86+
cy.deviceAuthFlow(client, { consent: { scope: [] } })
87+
88+
cy.postDeviceAuthFlow().then(
89+
(resp) => {
90+
console.log(resp)
91+
const {
92+
result,
93+
token: { access_token, id_token, refresh_token },
94+
} = resp.body
95+
96+
expect(result).to.equal("success")
97+
expect(access_token).to.not.be.empty
98+
expect(id_token).to.be.undefined
99+
expect(refresh_token).to.be.undefined
100+
})
101+
})
102+
103+
it("should skip consent if the client is confgured thus", function () {
104+
const client = nc({ skip_consent: true })
105+
cy.deviceAuthFlow(client, {
106+
consent: { scope: ["offline_access", "openid"], skip: true },
107+
})
108+
109+
cy.postDeviceAuthFlow().then(
110+
(resp) => {
111+
console.log(resp)
112+
const {
113+
result,
114+
token: { access_token, id_token, refresh_token },
115+
} = resp.body
116+
117+
expect(result).to.equal("success")
118+
expect(access_token).to.not.be.empty
119+
expect(id_token).to.not.be.empty
120+
expect(refresh_token).to.not.be.empty
121+
})
122+
})
123+
})
124+
})
125+
})

cypress/support/commands.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,94 @@ 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(
304+
"postDeviceAuthFlow",
305+
(
306+
path = "oauth2",
307+
) =>
308+
cy.request(`${Cypress.env("client_url")}/${path}/device/success`)
309+
)

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/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)