Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: consent-verbose code example #1456

Merged
merged 2 commits into from
Jul 20, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
323 changes: 255 additions & 68 deletions code-examples/sdk/typescript/src/oauth2/consent-verbose.ts
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
}
}