-
-
Notifications
You must be signed in to change notification settings - Fork 374
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: consent-verbose code example (#1456)
- Loading branch information
Showing
1 changed file
with
255 additions
and
68 deletions.
There are no files selected for viewing
323 changes: 255 additions & 68 deletions
323
code-examples/sdk/typescript/src/oauth2/consent-verbose.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,85 +1,272 @@ | ||
// Copyright © 2022 Ory Corp | ||
// Copyright © 2023 Ory Corp | ||
// SPDX-License-Identifier: Apache-2.0 | ||
import { | ||
AcceptOAuth2ConsentRequestSession, | ||
IdentityApi, | ||
OAuth2ConsentRequest, | ||
} from "@ory/client" | ||
import { UserConsentCard } from "@ory/elements-markup" | ||
import bodyParser from "body-parser" | ||
import csrf from "csurf" | ||
import { defaultConfig, RouteCreator, RouteRegistrator } from "../pkg" | ||
import { register404Route } from "./404" | ||
import { oidcConformityMaybeFakeSession } from "./stub/oidc-cert" | ||
|
||
import { Configuration, OAuth2Api } from "@ory/client" | ||
import { Request, Response } from "express" | ||
async function createOAuth2ConsentRequestSession( | ||
grantScopes: string[], | ||
consentRequest: OAuth2ConsentRequest, | ||
identityApi: IdentityApi, | ||
): Promise<AcceptOAuth2ConsentRequestSession> { | ||
// The session allows us to set session data for id and access tokens | ||
|
||
const ory = new OAuth2Api( | ||
new Configuration({ | ||
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`, | ||
accessToken: process.env.ORY_API_KEY, | ||
}), | ||
) | ||
const id_token: { [key: string]: any } = {} | ||
|
||
function authenticateUserCredentials(email: string, password: string): any { | ||
// Example method to authenticate users and fetch them from the DB. | ||
if (consentRequest.subject && grantScopes.length > 0) { | ||
const identity = ( | ||
await identityApi.getIdentity({ id: consentRequest.subject }) | ||
).data | ||
|
||
if (grantScopes.indexOf("email") > -1) { | ||
// Client may check email of user | ||
id_token.email = identity.traits["email"] || "" | ||
} | ||
if (grantScopes.indexOf("phone") > -1) { | ||
// Client may check phone number of user | ||
id_token.phone = identity.traits["phone"] || "" | ||
} | ||
} | ||
|
||
return { | ||
// This data will be available when introspecting the token. Try to avoid sensitive information here, | ||
// unless you limit who can introspect tokens. | ||
access_token: { | ||
// foo: 'bar' | ||
}, | ||
|
||
// This data will be available in the ID token. | ||
id_token, | ||
} | ||
} | ||
|
||
// Please note that this is an example implementation. | ||
// In a production app, please add proper error handling. | ||
export async function handleLogin(request: Request, response: Response) { | ||
const challenge = request.query.login_challenge.toString() | ||
const { data: loginRequest } = await ory.getOAuth2LoginRequest({ | ||
loginChallenge: challenge.toString(), | ||
}) | ||
|
||
if (loginRequest.skip) { | ||
// User is already authenticated, don't show the login form and simply accept the login request. | ||
await ory | ||
.acceptOAuth2LoginRequest({ | ||
loginChallenge: challenge, | ||
acceptOAuth2LoginRequest: { | ||
subject: loginRequest.subject, | ||
}, | ||
// A simple express handler that shows the Hydra consent screen. | ||
export const createConsentRoute: RouteCreator = | ||
(createHelpers) => (req, res, next) => { | ||
console.log("createConsentRoute") | ||
res.locals.projectName = "An application requests access to your data!" | ||
|
||
const { oauth2, identity } = createHelpers(req, res) | ||
const { consent_challenge } = req.query | ||
|
||
// The challenge is used to fetch information about the consent request from ORY hydraAdmin. | ||
const challenge = String(consent_challenge) | ||
if (!challenge) { | ||
next( | ||
new Error("Expected a consent challenge to be set but received none."), | ||
) | ||
return | ||
} | ||
|
||
let trustedClients: string[] = [] | ||
if (process.env.TRUSTED_CLIENT_IDS) { | ||
trustedClients = String(process.env.TRUSTED_CLIENT_IDS).split(",") | ||
} | ||
|
||
console.log("getOAuth2ConsentRequest", challenge) | ||
// 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 | ||
oauth2 | ||
.getOAuth2ConsentRequest({ consentChallenge: challenge }) | ||
// This will be called if the HTTP request was successful | ||
.then(async ({ data: body }) => { | ||
// If a user has granted this application the requested scope, hydra will tell us to not show the UI. | ||
if ( | ||
body.skip || | ||
body.client?.skip_consent || | ||
(body.client?.client_id && | ||
trustedClients.indexOf(body.client?.client_id) > -1) | ||
) { | ||
// You can apply logic here, for example grant another scope, or do whatever... | ||
// ... | ||
|
||
let grantScope: string[] = body.requested_scope || [] | ||
if (!Array.isArray(grantScope)) { | ||
grantScope = [grantScope] | ||
} | ||
const session = await createOAuth2ConsentRequestSession( | ||
grantScope, | ||
body, | ||
identity, | ||
) | ||
|
||
// Now it's time to grant the consent request. You could also deny the request if something went terribly wrong | ||
return oauth2 | ||
.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, | ||
|
||
// 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, | ||
}, | ||
}) | ||
.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", { | ||
card: UserConsentCard({ | ||
consent: body, | ||
csrfToken: req.csrfToken(), | ||
cardImage: body.client?.logo_uri || "/ory-logo.svg", | ||
client_name: body.client?.client_name || "unknown client", | ||
requested_scope: body.requested_scope, | ||
client: body.client, | ||
action: (process.env.BASE_URL || "") + "/consent", | ||
}), | ||
}) | ||
}) | ||
.then(({ data }) => response.redirect(data.redirect_to)) | ||
return | ||
// 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. | ||
} | ||
|
||
// Show the login form if the form was not submitted. | ||
if (request.method === "GET") { | ||
response.render("login", { | ||
loginRequest, | ||
}) | ||
return | ||
} | ||
export const createConsentPostRoute: RouteCreator = | ||
(createHelpers) => (req, res, next) => { | ||
// The challenge is a hidden input field, so we have to retrieve it from the request body | ||
const challenge = req.body.consent_challenge | ||
const { oauth2, identity } = createHelpers(req, res) | ||
|
||
// 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 ( | ||
oauth2 | ||
.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) | ||
) | ||
} | ||
|
||
let grantScope = req.body.grant_scope | ||
if (!Array.isArray(grantScope)) { | ||
grantScope = [grantScope] | ||
} | ||
|
||
// 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's fetch the consent request again to be able to set `grantAccessTokenAudience` properly. | ||
oauth2 | ||
.getOAuth2ConsentRequest({ consentChallenge: challenge }) | ||
// This will be called if the HTTP request was successful | ||
.then(async ({ data: body }) => { | ||
const session = await createOAuth2ConsentRequestSession( | ||
grantScope, | ||
body, | ||
identity, | ||
) | ||
return oauth2 | ||
.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, | ||
|
||
// The user did not want to sign in with the given app. | ||
if (request.body.submit === "Deny access") { | ||
await ory | ||
.rejectOAuth2LoginRequest({ | ||
loginChallenge: challenge, | ||
rejectOAuth2Request: { | ||
error: "access_denied", | ||
error_description: "The resource owner denied the request", | ||
}, | ||
// 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" sesion expires, in seconds. Set this to 0 so it will never expire. | ||
remember_for: process.env.REMEMBER_CONSENT_FOR_SECONDS | ||
? Number(process.env.REMEMBER_CONSENT_SESSION_FOR_SECONDS) | ||
: 3600, | ||
}, | ||
}) | ||
.then(({ data: body }) => { | ||
// All we need to do now is to redirect the user back! | ||
res.redirect(String(body.redirect_to)) | ||
}) | ||
}) | ||
.then(({ data }) => response.redirect(data.redirect_to)) | ||
.catch(next) | ||
} | ||
|
||
const user = authenticateUserCredentials( | ||
request.body.email, | ||
request.body.password, | ||
) | ||
// Sets up csrf protection | ||
const csrfProtection = csrf({ | ||
cookie: { | ||
sameSite: "lax", | ||
}, | ||
}) | ||
|
||
// Check login credentials (e.g. email + password) in your user database. | ||
if (user!) { | ||
response.render("login", { error: "invalid credentials", loginRequest }) | ||
return | ||
var parseForm = bodyParser.urlencoded({ extended: false }) | ||
|
||
export const registerConsentRoute: RouteRegistrator = function ( | ||
app, | ||
createHelpers = defaultConfig, | ||
) { | ||
if (process.env.HYDRA_ADMIN_URL) { | ||
console.log("found HYDRA_ADMIN_URL") | ||
return app.get( | ||
"/consent", | ||
csrfProtection, | ||
createConsentRoute(createHelpers), | ||
) | ||
} else { | ||
return register404Route | ||
} | ||
} | ||
|
||
// User was authenticated successfully, | ||
return await ory | ||
.acceptOAuth2LoginRequest({ | ||
loginChallenge: challenge, | ||
acceptOAuth2LoginRequest: { | ||
subject: user.id, | ||
remember: Boolean(request.body.remember), | ||
remember_for: 3600, | ||
context: { | ||
// You can add any context that you want to be available to the consent endpoint. | ||
}, | ||
}, | ||
}) | ||
.then(({ data }) => response.redirect(data.redirect_to)) | ||
export const registerConsentPostRoute: RouteRegistrator = function ( | ||
app, | ||
createHelpers = defaultConfig, | ||
) { | ||
if (process.env.HYDRA_ADMIN_URL) { | ||
return app.post( | ||
"/consent", | ||
parseForm, | ||
csrfProtection, | ||
createConsentPostRoute(createHelpers), | ||
) | ||
} else { | ||
return register404Route | ||
} | ||
} |