From b18c7824bdbcf4fb4b87c572bb2c21aed19cd875 Mon Sep 17 00:00:00 2001 From: Nicolas Burtey Date: Mon, 25 Sep 2023 20:30:49 +0100 Subject: [PATCH] chore: rewrite app in async/await fashion --- apps/consent/package.json | 2 +- apps/consent/src/routes/consent.ts | 235 ++++++++++++++--------------- apps/consent/src/routes/login.ts | 127 ++++++++-------- apps/consent/src/routes/logout.ts | 66 ++++---- apps/consent/yarn.lock | 8 +- apps/login-consent/tsconfig.json | 14 -- 6 files changed, 217 insertions(+), 235 deletions(-) delete mode 100644 apps/login-consent/tsconfig.json diff --git a/apps/consent/package.json b/apps/consent/package.json index d9153922f8e..84b51f00c82 100644 --- a/apps/consent/package.json +++ b/apps/consent/package.json @@ -17,7 +17,7 @@ }, "prettier": "ory-prettier-styles", "dependencies": { - "@ory/hydra-client": "^2.1.1", + "@ory/hydra-client": "^2.2.0-rc.3", "@types/axios": "^0.14.0", "@types/cookie-parser": "^1.4.2", "@types/csurf": "^1.9.36", diff --git a/apps/consent/src/routes/consent.ts b/apps/consent/src/routes/consent.ts index 59c86e8a2f5..6cbc4f4a60e 100644 --- a/apps/consent/src/routes/consent.ts +++ b/apps/consent/src/routes/consent.ts @@ -16,7 +16,7 @@ const csrfProtection = csrf({ }) const router = express.Router() -router.get("/", csrfProtection, (req, res, next) => { +router.get("/", csrfProtection, async (req, res, next) => { // Parses the URL query const query = url.parse(req.url, true).query @@ -30,90 +30,85 @@ router.get("/", csrfProtection, (req, res, next) => { // This section processes consent requests and either shows the consent UI or // accepts the consent request right away if the user has given consent to this // app before - hydraClient - .getOAuth2ConsentRequest({ consentChallenge: challenge }) + try { + const data = await hydraClient.getOAuth2ConsentRequest({ + consentChallenge: challenge, + }) // This will be called if the HTTP request was successful - .then(({ data: body }) => { - // If a user has granted this application the requested scope, hydra will tell us to not show the UI. - // Any cast needed because the SDK changes are still unreleased. - // TODO: Remove in a later version. - if (body.skip || (body.client as any)?.skip_consent) { - // You can apply logic here, for example grant another scope, or do whatever... - // ... - - // FIXME: doesn't seem to work - // body.requested_scope?.push("offline") - - // Now it's time to grant the consent request. You could also deny the request if something went terribly wrong - return hydraClient - .acceptOAuth2ConsentRequest({ - consentChallenge: challenge, - acceptOAuth2ConsentRequest: { - // We can grant all scopes that have been requested - hydra already checked for us that no additional scopes - // are requested accidentally. - grant_scope: body.requested_scope, - - // ORY Hydra checks if requested audiences are allowed by the client, so we can simply echo this. - grant_access_token_audience: body.requested_access_token_audience, - - // The session allows us to set session data for id and access tokens - session: { - // This data will be available when introspecting the token. Try to avoid sensitive information here, - // unless you limit who can introspect tokens. - access_token: { card: "alice" }, - // This data will be available in the ID token. - id_token: { who: "bob" }, - }, - }, - }) - .then(({ data: body }) => { - // All we need to do now is to redirect the user back to hydra! - res.redirect(String(body.redirect_to)) - }) - } - - // If consent can't be skipped we MUST show the consent UI. - res.render("consent", { - csrfToken: req.csrfToken(), - challenge: challenge, - // We have a bunch of data available from the response, check out the API docs to find what these values mean - // and what additional data you have available. - requested_scope: body.requested_scope, - user: body.subject, - client: body.client, - action: urljoin(process.env.BASE_URL || "", "/consent"), + + const body = data.data + + // If a user has granted this application the requested scope, hydra will tell us to not show the UI. + // Any cast needed because the SDK changes are still unreleased. + if (body.client?.skip_consent) { + // You can apply logic here, for example grant another scope, or do whatever... + // ... + + // FIXME: doesn't seem to work + // body.requested_scope?.push("offline") + + // Now it's time to grant the consent request. You could also deny the request if something went terribly wrong + const response = await hydraClient.acceptOAuth2ConsentRequest({ + consentChallenge: challenge, + acceptOAuth2ConsentRequest: { + // We can grant all scopes that have been requested - hydra already checked for us that no additional scopes + // are requested accidentally. + grant_scope: body.requested_scope, + + // ORY Hydra checks if requested audiences are allowed by the client, so we can simply echo this. + grant_access_token_audience: body.requested_access_token_audience, + + // The session allows us to set session data for id and access tokens + session: { + // This data will be available when introspecting the token. Try to avoid sensitive information here, + // unless you limit who can introspect tokens. + access_token: { card: "alice" }, + // This data will be available in the ID token. + id_token: { who: "bob" }, + }, + }, }) + + res.redirect(String(response.data.redirect_to)) + } + + // If consent can't be skipped we MUST show the consent UI. + res.render("consent", { + csrfToken: req.csrfToken(), + challenge: challenge, + // We have a bunch of data available from the response, check out the API docs to find what these values mean + // and what additional data you have available. + requested_scope: body.requested_scope, + user: body.subject, + client: body.client, + action: urljoin(process.env.BASE_URL || "", "/consent"), }) - // This will handle any error that happens when making HTTP calls to hydra - .catch(next) - // The consent request has now either been accepted automatically or rendered. + } catch (err) { + next(err) + } }) -router.post("/", csrfProtection, (req, res, next) => { +router.post("/", csrfProtection, async (req, res, next) => { // The challenge is now a hidden input field, so let's take it from the request body instead const challenge = req.body.challenge - // Let's see if the user decided to accept or reject the consent request.. if (req.body.submit === "Deny access") { - // Looks like the consent request was denied by the user - return ( - hydraClient - .rejectOAuth2ConsentRequest({ - consentChallenge: challenge, - rejectOAuth2Request: { - error: "access_denied", - error_description: "The resource owner denied the request", - }, - }) - .then(({ data: body }) => { - // All we need to do now is to redirect the browser back to hydra! - res.redirect(String(body.redirect_to)) - }) - // This will handle any error that happens when making HTTP calls to hydra - .catch(next) - ) + try { + const response = await hydraClient.rejectOAuth2ConsentRequest({ + consentChallenge: challenge, + rejectOAuth2Request: { + error: "access_denied", + error_description: "The resource owner denied the request", + }, + }) + + res.redirect(String(response.data.redirect_to)) + } catch (err) { + next(err) + } + + return } - // label:consent-deny-end let grantScope = req.body.grant_scope if (!Array.isArray(grantScope)) { @@ -121,6 +116,14 @@ router.post("/", csrfProtection, (req, res, next) => { } // The session allows us to set session data for id and access tokens + + // Here is also the place to add data to the ID or access token. For example, + // if the scope 'profile' is added, add the family and given name to the ID Token claims: + // if (grantScope.indexOf('profile')) { + // session.id_token.family_name = 'Doe' + // session.id_token.given_name = 'John' + // } + let session = { // This data will be available when introspecting the token. Try to avoid sensitive information here, // unless you limit who can introspect tokens. @@ -129,53 +132,45 @@ router.post("/", csrfProtection, (req, res, next) => { id_token: { card: "bob" }, } - // Here is also the place to add data to the ID or access token. For example, - // if the scope 'profile' is added, add the family and given name to the ID Token claims: - // if (grantScope.indexOf('profile')) { - // session.id_token.family_name = 'Doe' - // session.id_token.given_name = 'John' - // } + try { + // Let's fetch the consent request again to be able to set `grantAccessTokenAudience` properly. + const responseInit = await hydraClient.getOAuth2ConsentRequest({ + consentChallenge: challenge, + }) - // Let's fetch the consent request again to be able to set `grantAccessTokenAudience` properly. - hydraClient - .getOAuth2ConsentRequest({ consentChallenge: challenge }) - // This will be called if the HTTP request was successful - .then(({ data: body }) => { - return hydraClient - .acceptOAuth2ConsentRequest({ - consentChallenge: challenge, - acceptOAuth2ConsentRequest: { - // We can grant all scopes that have been requested - hydra already checked for us that no additional scopes - // are requested accidentally. - grant_scope: grantScope, - - // If the environment variable CONFORMITY_FAKE_CLAIMS is set we are assuming that - // the app is built for the automated OpenID Connect Conformity Test Suite. You - // can peak inside the code for some ideas, but be aware that all data is fake - // and this only exists to fake a login system which works in accordance to OpenID Connect. - // - // If that variable is not set, the session will be used as-is. - session: oidcConformityMaybeFakeSession(grantScope, body, session), - - // ORY Hydra checks if requested audiences are allowed by the client, so we can simply echo this. - grant_access_token_audience: body.requested_access_token_audience, - - // This tells hydra to remember this consent request and allow the same client to request the same - // scopes from the same user, without showing the UI, in the future. - remember: Boolean(req.body.remember), - - // When this "remember" session expires, in seconds. Set this to 0 so it will never expire. - remember_for: 3600, - }, - }) - .then(({ data: body }) => { - // All we need to do now is to redirect the user back to hydra! - res.redirect(String(body.redirect_to)) - }) + const body = responseInit.data + + const responseConfirm = await hydraClient.acceptOAuth2ConsentRequest({ + consentChallenge: challenge, + acceptOAuth2ConsentRequest: { + // We can grant all scopes that have been requested - hydra already checked for us that no additional scopes + // are requested accidentally. + grant_scope: grantScope, + + // If the environment variable CONFORMITY_FAKE_CLAIMS is set we are assuming that + // the app is built for the automated OpenID Connect Conformity Test Suite. You + // can peak inside the code for some ideas, but be aware that all data is fake + // and this only exists to fake a login system which works in accordance to OpenID Connect. + // + // If that variable is not set, the session will be used as-is. + session: oidcConformityMaybeFakeSession(grantScope, body, session), + + // ORY Hydra checks if requested audiences are allowed by the client, so we can simply echo this. + grant_access_token_audience: body.requested_access_token_audience, + + // This tells hydra to remember this consent request and allow the same client to request the same + // scopes from the same user, without showing the UI, in the future. + remember: Boolean(req.body.remember), + + // When this "remember" session expires, in seconds. Set this to 0 so it will never expire. + remember_for: 3600, + }, }) - // This will handle any error that happens when making HTTP calls to hydra - .catch(next) - // label:docs-accept-consent + + res.redirect(String(responseConfirm.data.redirect_to)) + } catch (err) { + next(err) + } }) export default router diff --git a/apps/consent/src/routes/login.ts b/apps/consent/src/routes/login.ts index 5e486edec4b..c6547155315 100644 --- a/apps/consent/src/routes/login.ts +++ b/apps/consent/src/routes/login.ts @@ -43,18 +43,15 @@ router.get("/", csrfProtection, async (req, res, next) => { // Now it's time to grant the login request. You could also deny the request if something went terribly wrong // (e.g. your arch-enemy logging in...) - return hydraClient - .acceptOAuth2LoginRequest({ - loginChallenge: challenge, - acceptOAuth2LoginRequest: { - // All we need to do is to confirm that we indeed want to log in the user. - subject: String(body.subject), - }, - }) - .then(({ data: body }) => { - // All we need to do now is to redirect the user back to hydra! - res.redirect(String(body.redirect_to)) - }) + const response = await hydraClient.acceptOAuth2LoginRequest({ + loginChallenge: challenge, + acceptOAuth2LoginRequest: { + // All we need to do is to confirm that we indeed want to log in the user. + subject: String(body.subject), + }, + }) + + res.redirect(String(response.data.redirect_to)) } // If authentication can't be skipped we MUST show the login UI. @@ -77,22 +74,20 @@ router.post("/", csrfProtection, async (req, res, next) => { // Let's see if the user decided to accept or reject the consent request.. if (req.body.submit === "Deny access") { // Looks like the consent request was denied by the user - return ( - hydraClient - .rejectOAuth2LoginRequest({ - loginChallenge: challenge, - rejectOAuth2Request: { - error: "access_denied", - error_description: "The resource owner denied the request", - }, - }) - .then(({ data: body }) => { - // All we need to do now is to redirect the browser back to hydra! - res.redirect(String(body.redirect_to)) - }) - // This will handle any error that happens when making HTTP calls to hydra - .catch(next) - ) + try { + const response = await hydraClient.rejectOAuth2LoginRequest({ + loginChallenge: challenge, + rejectOAuth2Request: { + error: "access_denied", + error_description: "The resource owner denied the request", + }, + }) + + res.redirect(String(response.data.redirect_to)) + } catch (err) { + // This will handle any error that happens when making HTTP calls to hydra + next(err) + } } const email = req.body.email @@ -158,43 +153,45 @@ router.post("/verification", csrfProtection, async (req, res, next) => { // Seems like the user authenticated! Let's tell hydra... - hydraClient - .getOAuth2LoginRequest({ loginChallenge: challenge }) - .then(({ data: loginRequest }) => - hydraClient - .acceptOAuth2LoginRequest({ - loginChallenge: challenge, - acceptOAuth2LoginRequest: { - // Subject is an alias for user ID. A subject can be a random string, a UUID, an email address, .... - subject: userId, - - // This tells hydra to remember the browser and automatically authenticate the user in future requests. This will - // set the "skip" parameter in the other route to true on subsequent requests! - remember: Boolean(req.body.remember), - - // When the session expires, in seconds. Set this to 0 so it will never expire. - remember_for: 3600, - - // Sets which "level" (e.g. 2-factor authentication) of authentication the user has. The value is really arbitrary - // and optional. In the context of OpenID Connect, a value of 0 indicates the lowest authorization level. - // acr: '0', - // - // If the environment variable CONFORMITY_FAKE_CLAIMS is set we are assuming that - // the app is built for the automated OpenID Connect Conformity Test Suite. You - // can peak inside the code for some ideas, but be aware that all data is fake - // and this only exists to fake a login system which works in accordance to OpenID Connect. - // - // If that variable is not set, the ACR value will be set to the default passed here ('0') - acr: oidcConformityMaybeFakeAcr(loginRequest, "0"), - }, - }) - .then(({ data: body }) => { - // All we need to do now is to redirect the user back to hydra! - res.redirect(String(body.redirect_to)) - }), - ) - // This will handle any error that happens when making HTTP calls to hydra - .catch(next) + const response = await hydraClient.getOAuth2LoginRequest({ + loginChallenge: challenge, + }) + + const loginRequest = response.data + + try { + const response2 = await hydraClient.acceptOAuth2LoginRequest({ + loginChallenge: challenge, + acceptOAuth2LoginRequest: { + // Subject is an alias for user ID. A subject can be a random string, a UUID, an email address, .... + subject: userId, + + // This tells hydra to remember the browser and automatically authenticate the user in future requests. This will + // set the "skip" parameter in the other route to true on subsequent requests! + remember: Boolean(req.body.remember), + + // When the session expires, in seconds. Set this to 0 so it will never expire. + remember_for: 3600, + + // Sets which "level" (e.g. 2-factor authentication) of authentication the user has. The value is really arbitrary + // and optional. In the context of OpenID Connect, a value of 0 indicates the lowest authorization level. + // acr: '0', + // + // If the environment variable CONFORMITY_FAKE_CLAIMS is set we are assuming that + // the app is built for the automated OpenID Connect Conformity Test Suite. You + // can peak inside the code for some ideas, but be aware that all data is fake + // and this only exists to fake a login system which works in accordance to OpenID Connect. + // + // If that variable is not set, the ACR value will be set to the default passed here ('0') + acr: oidcConformityMaybeFakeAcr(loginRequest, "0"), + }, + }) + + // All we need to do now is to redirect the user back to hydra! + res.redirect(String(response2.data.redirect_to)) + } catch (err) { + next(err) + } // You could also deny the login request which tells hydra that no one authenticated! // hydra.rejectLoginRequest(challenge, { diff --git a/apps/consent/src/routes/logout.ts b/apps/consent/src/routes/logout.ts index 9357f142a01..89a6e717bfc 100644 --- a/apps/consent/src/routes/logout.ts +++ b/apps/consent/src/routes/logout.ts @@ -11,7 +11,7 @@ import { hydraClient } from "../config" const csrfProtection = csrf({ cookie: true }) const router = express.Router() -router.get("/", csrfProtection, (req, res, next) => { +router.get("/", csrfProtection, async (req, res, next) => { // Parses the URL query const query = url.parse(req.url, true).query @@ -22,49 +22,53 @@ router.get("/", csrfProtection, (req, res, next) => { return } - hydraClient - .getOAuth2LogoutRequest({ logoutChallenge: challenge }) + try { + const response = await hydraClient.getOAuth2LogoutRequest({ + logoutChallenge: challenge, + }) // This will be called if the HTTP request was successful - .then(() => { - // Here we have access to e.g. response.subject, response.sid, ... - // The most secure way to perform a logout request is by asking the user if he/she really want to log out. - res.render("logout", { - csrfToken: req.csrfToken(), - challenge: challenge, - action: urljoin(process.env.BASE_URL || "", "/logout"), - }) + // Here we have access to e.g. response.subject, response.sid, ... + + // The most secure way to perform a logout request is by asking the user if he/she really want to log out. + res.render("logout", { + csrfToken: req.csrfToken(), + challenge: challenge, + action: urljoin(process.env.BASE_URL || "", "/logout"), }) + } catch (err) { + next(err) // This will handle any error that happens when making HTTP calls to hydra - .catch(next) + } }) -router.post("/", csrfProtection, (req, res, next) => { +router.post("/", csrfProtection, async (req, res, next) => { // The challenge is now a hidden input field, so let's take it from the request body instead const challenge = req.body.challenge if (req.body.submit === "No") { - return ( - hydraClient - .rejectOAuth2LogoutRequest({ logoutChallenge: challenge }) - .then(() => { - // The user did not want to log out. Let's redirect him back somewhere or do something else. - res.redirect("https://www.ory.sh/") - }) - // This will handle any error that happens when making HTTP calls to hydra - .catch(next) - ) + try { + const response = await hydraClient.rejectOAuth2LogoutRequest({ + logoutChallenge: challenge, + }) + // The user did not want to log out. Let's redirect him back somewhere or do something else. + res.redirect("https://www.blink.sh/") + } catch (err) { + next(err) + } } - // The user agreed to log out, let's accept the logout request. - hydraClient - .acceptOAuth2LogoutRequest({ logoutChallenge: challenge }) - .then(({ data: body }) => { - // All we need to do now is to redirect the user back to hydra! - res.redirect(String(body.redirect_to)) + try { + // The user agreed to log out, let's accept the logout request. + const response = await hydraClient.acceptOAuth2LogoutRequest({ + logoutChallenge: challenge, }) - // This will handle any error that happens when making HTTP calls to hydra - .catch(next) + + // All we need to do now is to redirect the user back to hydra! + res.redirect(String(response.data.redirect_to)) + } catch (err) { + next(err) + } }) export default router diff --git a/apps/consent/yarn.lock b/apps/consent/yarn.lock index bc30b48fb32..0bf879a132a 100644 --- a/apps/consent/yarn.lock +++ b/apps/consent/yarn.lock @@ -23,10 +23,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@ory/hydra-client@^2.1.1": - version "2.1.1" - resolved "https://registry.npmjs.org/@ory/hydra-client/-/hydra-client-2.1.1.tgz" - integrity sha512-fL1Jna/wBJh9fAObklj+XZS2n3002+Wm3BkDk/BcwZ9ylsDlkdEZK0ZhxPocMkX2bhDZAeCVNe7CDCumSu9Kqw== +"@ory/hydra-client@^2.2.0-rc.3": + version "2.2.0-rc.3" + resolved "https://registry.npmjs.org/@ory/hydra-client/-/hydra-client-2.2.0-rc.3.tgz#236be3ac395b35ba9ffec021701fac5438d3b24c" + integrity sha512-FEGhnEBmRlnrZyW5WPIH6Jmc00+kfZYRdFbSHBqqOfWJP7/Vj6z7q3MfA4osvQgGUfvdZsb1/HHCe6aoLaYZaQ== dependencies: axios "^0.21.4" diff --git a/apps/login-consent/tsconfig.json b/apps/login-consent/tsconfig.json deleted file mode 100644 index 8b47580716d..00000000000 --- a/apps/login-consent/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "resolveJsonModule": true, - "target": "es5", - "module": "commonjs", - "lib": ["es2019", "dom"], - "outDir": "../consent/lib", - "rootDir": "../consent/src", - "strict": true, - "typeRoots": ["../consent/typings"], - "esModuleInterop": true - }, - "exclude": ["../consent/contrib"] -}