Skip to content

Commit d55caa4

Browse files
committed
chore: add cypress tests
1 parent ebfad8a commit d55caa4

File tree

4 files changed

+285
-0
lines changed

4 files changed

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

cypress/support/commands.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,81 @@ 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 = "foo@bar.com",
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+
}
287+
288+
if (doCreateClient) {
289+
createClient(client).should((client) => {
290+
run(client)
291+
})
292+
return
293+
}
294+
run(client)
295+
},
296+
)

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/code
42+
export URLS_DEVICE_SUCCESS=http://127.0.0.1:5003/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)