From 0286723ce21156d28d36eeabbb153da8e79ab992 Mon Sep 17 00:00:00 2001 From: ATHULKNAIR Date: Sun, 26 Dec 2021 16:04:59 +0530 Subject: [PATCH] url with launch compaitable --- package.json | 2 +- src/index.ts | 19 +- src/routes/hydraConsent.ts | 348 +++++++++++++++++++++--------------- src/routes/hydraLogin.ts | 47 +++-- src/routes/patientPicker.ts | 126 +++++++++++++ views/login.hbs | 4 +- views/patientpicker.hbs | 27 +++ 7 files changed, 405 insertions(+), 168 deletions(-) create mode 100644 src/routes/patientPicker.ts create mode 100644 views/patientpicker.hbs diff --git a/package.json b/package.json index 23d08e3f..f50eadf4 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "tsc", "serve": "node lib/index.js", - "start": "set BASE_URL=http://127.0.0.1:4455&& set HYDRA_ADMIN_URL=http://localhost:4445&& set KRATOS_PUBLIC_URL=http://localhost:4433/&& set KRATOS_ADMIN_URL=http://localhost:4434/&& set PORT=4455 && set KRATOS_BROWSER_URL=http://127.0.0.1:4433/&& ts-node-dev --watch public,views --respawn src/index.ts", + "start": "set BASE_URL=http://localhost:4455&& set HYDRA_ADMIN_URL=http://localhost/hydra/admin&& set KRATOS_PUBLIC_URL=http://localhost/kratos/public&& set KRATOS_ADMIN_URL=http://localhost/kratos/admin&& set PORT=4455 && set KRATOS_BROWSER_URL=http://localhost/kratos/admin&& ts-node-dev --watch public,views --respawn src/index.ts", "test": "npm-run-all build", "format": "prettier --write src/**/*.ts", "check-format": "prettier --check src/**/*.ts", diff --git a/src/index.ts b/src/index.ts index 79294d6a..9ee6b3dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ import hydraLogin, { postLogin } from './routes/hydraLogin' import hydraLogout from './routes/hydraLogout' import session from 'express-session' import { hydraGetConsent, hydraPostConsent } from './routes/hydraConsent' +import { getPatientPicker, postPatientPicker } from './routes/patientPicker' import bodyParser from 'body-parser' import cors from 'cors' @@ -34,7 +35,6 @@ const app = express() app.locals.logoUrl = config.logoUrl - app.use(morgan('tiny')) app.use(cookieParser()) app.use( @@ -106,7 +106,7 @@ if (process.env.NODE_ENV === 'stub') { app.get('/recovery', recoveryHandler) app.get('/postLogin', csrfProtection, postLogin) app.get('/hydralogin', csrfProtection, hydraLogin) - app.get('/hydralogout', csrfProtection, hydraLogout) + app.get('/hydralogout', cors(), csrfProtection, hydraLogout) app.get( '/hydraconsent', csrfProtection, @@ -119,6 +119,19 @@ if (process.env.NODE_ENV === 'stub') { csrfProtection, hydraPostConsent ) + app.get( + '/patientpicker/:challenge/:launch', + protect, + csrfProtection, + getPatientPicker + ) + app.post( + '/patientpicker/:challenge/:launch', + protect, + bodyParser.urlencoded({ extended: true }), + csrfProtection, + postPatientPicker + ) app.get('/postlogout', cors(), (req, res, next) => { res.send('You have successfully logged out') }) @@ -138,8 +151,6 @@ app.use((err: Error, req: Request, res: Response, next: NextFunction) => { }) }) - - const port = Number(process.env.PORT) || 3000 let listener = () => { diff --git a/src/routes/hydraConsent.ts b/src/routes/hydraConsent.ts index 07fc6271..782bbe48 100644 --- a/src/routes/hydraConsent.ts +++ b/src/routes/hydraConsent.ts @@ -1,167 +1,219 @@ -import { Request, Response, NextFunction } from "express" -import { AdminApi, Configuration, ConsentRequestSession } from '@oryd/hydra-client' +import { Request, Response, NextFunction } from 'express' +import { + AdminApi, + Configuration, + ConsentRequestSession, +} from '@oryd/hydra-client' import config from '../config' +import urljoin from 'url-join' const hydraAdmin = new AdminApi( - new Configuration({ - basePath: config.hydra.admin - }) + new Configuration({ + basePath: config.hydra.admin, + }) ) +const getLaunch = (url) => { + const launch = new URL(url).searchParams.get('launch') + if (launch) { + const decodedLaunch = Buffer.from(launch, 'base64') + return JSON.parse(decodedLaunch.toString()) + } +} + const createHydraSession = ( - requestedScope: string[] = [], - context + requestedScope: string[] = [], + context, + launch ): ConsentRequestSession => { - return { - id_token: { - userdata: context, - fhirUser: "http://fhirserver.com/Patient/123" - }, - access_token: { - scope: requestedScope, - organization_id: "medblocks", - patient_id: "sidharth", - realm_access: { - roles: requestedScope - } - } - } + return { + id_token: { + userdata: context, + launch, + }, + access_token: { + scope: requestedScope, + organization_id: 'medblocks', + patient_id: 'sidharth', + realm_access: { + roles: requestedScope, + }, + }, + } } export const hydraGetConsent = ( - req: Request, - res: Response, - next: NextFunction + req: Request, + res: Response, + next: NextFunction ) => { - // Parses the URL query - // The challenge is used to fetch information about the consent request from ORY Hydra. - const challenge = req.query.consent_challenge as string - - if (!challenge) { - console.error("Expected consent_challenge to be set") - next(new Error('Expected consent_challenge to be set.')) - return - } - - hydraAdmin - .getConsentRequest(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. - if (body.skip) { - // You can apply logic here, for example grant another scope, or do whatever... - console.log("skipping consent request") - console.log({ body }) - // Now it's time to grant the consent request. You could also deny the request if something went terribly wrong - - const acceptConsentRequest = { - grant_scope: body.requested_scope, - grant_access_token_audience: body.requested_access_token_audience, - session: createHydraSession( - body.requested_scope, - body.context - ) - } - - // We can grant all scopes that have been requested - hydra already checked for us that no additional scopes - // are requested accidentally. - - // ORY Hydra checks if requested audiences are allowed by the client, so we can simply echo this. - - - // The session allows us to set session data for id and access tokens. Let's add the email if it is included. - - return hydraAdmin - .acceptConsentRequest(challenge, acceptConsentRequest) - .then(({ data: body }) => { - // All we need to do now is to redirect the user back to hydra! - res.redirect(String(body.redirect_to)) - }) + // Parses the URL query + // The challenge is used to fetch information about the consent request from ORY Hydra. + const challenge = req.query.consent_challenge as string + + if (!challenge) { + console.error('Expected consent_challenge to be set') + next(new Error('Expected consent_challenge to be set.')) + return + } + + hydraAdmin + .getConsentRequest(challenge) + // This will be called if the HTTP request was successful + .then(({ data: body }) => { + console.log('consent', body.request_url) + + // If a user has granted this application the requested scope, hydra will tell us to not show the UI. + if (body.skip) { + // You can apply logic here, for example grant another scope, or do whatever... + console.log('skipping consent request') + console.log({ body }) + // Now it's time to grant the consent request. You could also deny the request if something went terribly wrong + + const prompt = new URL(body.request_url).searchParams.get('prompt') + const launch = new URL(body.request_url).searchParams.get('launch') + const scopeLaunchPatient = + body.requested_scope.includes('launch/patient') + const decodedLaunch = + launch && JSON.parse(Buffer.from(launch, 'base64').toString()) + + if (scopeLaunchPatient) { + if (decodedLaunch && !decodedLaunch.patient) { + if (prompt === 'none') { + console.log('client has launch/patient scope ') + console.log( + 'client has launch/patient scope , but contains no patient' + ) + return hydraAdmin + .rejectConsentRequest(challenge, { + error: 'login_required', + error_description: 'client has launch/patient scope but contains no patient. Patient selection cannot have prompt=none', + }) + .then(({ data: body }) => { + // All we need to do now is to redirect the user back to hydra! + console.log('consent redirectURL', body.redirect_to) + res.redirect(String(body.redirect_to)) + }) } - console.log("not skipping consent request") - console.log({ body }) - // If consent can't be skipped we MUST show the consent UI. - const context = body.context as any - const name = context?.traits?.name?.first - console.log(body.context) - console.log({ body }) - res.render('consent', { - csrfToken: req.csrfToken(), - challenge: challenge, - action: config.baseUrl + "/hydraconsent", - // 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: name, - client: body.client?.client_name || body.client?.client_id, - }) - }) - // This will handle any error that happens when making HTTP calls to hydra - .catch(next) -} + else{ + console.log('Hello',{challenge,launch}) + res.redirect(`/patientpicker/${challenge}/${launch}`) + } + } + } + const acceptConsentRequest = { + grant_scope: body.requested_scope, + grant_access_token_audience: body.requested_access_token_audience, + session: createHydraSession( + body.requested_scope, + body.context, + getLaunch(body.request_url) + ), + } -export const hydraPostConsent = ( - req: Request, - res: Response, - next: NextFunction -) => { - // The challenge is now a hidden input field, so let's take it from the request body instead - const challenge = req.body.challenge + // We can grant all scopes that have been requested - hydra already checked for us that no additional scopes + // are requested accidentally. - // Let's see if the user decided to accept or reject the consent request.. - if (req.body.submit !== 'Allow access') { - // Looks like the consent request was denied by the user - const rejectConsentRequest = { error: 'access_denied', error_description: 'The resource owner denied the request' } as any + // ORY Hydra checks if requested audiences are allowed by the client, so we can simply echo this. - return ( - hydraAdmin - .rejectConsentRequest(challenge, rejectConsentRequest) - .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] - } - - // Seems like the user authenticated! Let's tell hydra... - hydraAdmin - .getConsentRequest(challenge) - // This will be called if the HTTP request was successful - .then(({ data: body }) => { - const acceptConsentRequest = {} as any - // We can grant all scopes that have been requested - hydra already checked for us that no additional scopes - // are requested accidentally. - acceptConsentRequest.grant_scope = grantScope - - // ORY Hydra checks if requested audiences are allowed by the client, so we can simply echo this. - acceptConsentRequest.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. - acceptConsentRequest.remember = true - - // When this "remember" sesion expires, in seconds. Set this to 0 so it will never expire. - acceptConsentRequest.remember_for = 3600 - - // The session allows us to set session data for id and access tokens. Let's add the email if it is included. - acceptConsentRequest.session = createHydraSession( - body.requested_scope, - body.context - ) - console.log({ acceptConsentRequest }) - return hydraAdmin.acceptConsentRequest(challenge, acceptConsentRequest) - }) - .then(({ data: body }) => { + // The session allows us to set session data for id and access tokens. Let's add the email if it is included. + + return hydraAdmin + .acceptConsentRequest(challenge, acceptConsentRequest) + .then(({ data: body }) => { // All we need to do now is to redirect the user back to hydra! res.redirect(String(body.redirect_to)) + }) + } + console.log('not skipping consent request') + console.log({ body }) + // If consent can't be skipped we MUST show the consent UI. + const context = body.context as any + const name = context?.traits?.name?.first + console.log('context', body.context) + console.log({ body }) + res.render('consent', { + csrfToken: req.csrfToken(), + challenge: challenge, + action: config.baseUrl + '/hydraconsent', + // 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: name, + client: body.client?.client_name || body.client?.client_id, + }) + }) + // This will handle any error that happens when making HTTP calls to hydra + .catch(next) +} + +export const hydraPostConsent = ( + req: Request, + res: Response, + next: NextFunction +) => { + // 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 !== 'Allow access') { + // Looks like the consent request was denied by the user + const rejectConsentRequest = { + error: 'access_denied', + error_description: 'The resource owner denied the request', + } as any + + return ( + hydraAdmin + .rejectConsentRequest(challenge, rejectConsentRequest) + .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) -} \ No newline at end of file + ) + } + + let grantScope = req.body.grant_scope + if (!Array.isArray(grantScope)) { + grantScope = [grantScope] + } + + // Seems like the user authenticated! Let's tell hydra... + hydraAdmin + .getConsentRequest(challenge) + // This will be called if the HTTP request was successful + .then(({ data: body }) => { + // console.log('launch',JSON.parse(decodedLaunch.toString())) + const acceptConsentRequest = {} as any + // We can grant all scopes that have been requested - hydra already checked for us that no additional scopes + // are requested accidentally. + acceptConsentRequest.grant_scope = grantScope + + // ORY Hydra checks if requested audiences are allowed by the client, so we can simply echo this. + acceptConsentRequest.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. + acceptConsentRequest.remember = true + + // When this "remember" sesion expires, in seconds. Set this to 0 so it will never expire. + acceptConsentRequest.remember_for = 3600 + + // The session allows us to set session data for id and access tokens. Let's add the email if it is included. + acceptConsentRequest.session = createHydraSession( + body.requested_scope, + body.context, + getLaunch(body.request_url) + ) + console.log({ acceptConsentRequest }) + return hydraAdmin.acceptConsentRequest(challenge, acceptConsentRequest) + }) + .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) +} diff --git a/src/routes/hydraLogin.ts b/src/routes/hydraLogin.ts index 7a704b82..65fbd4c9 100644 --- a/src/routes/hydraLogin.ts +++ b/src/routes/hydraLogin.ts @@ -12,8 +12,10 @@ const hydraAdmin = new AdminApi( ) const kratos = new V0alpha2Api( - new KratosConfig({ basePath: config.kratos.public }) + new KratosConfig({ basePath: config.kratos.admin }) ) +console.log({config}) + const redirectToLogin = (req: Request, res: Response, next: NextFunction) => { if (!req.session) { @@ -34,7 +36,7 @@ const redirectToLogin = (req: Request, res: Response, next: NextFunction) => { const state = crypto.randomBytes(48).toString('hex') req.session.hydraLoginState = state - req.session.save(error => { + req.session.save((error) => { if (error) { console.error(error) next(error) @@ -66,7 +68,11 @@ const redirectToLogin = (req: Request, res: Response, next: NextFunction) => { }) } -export const postLogin = async (req: Request, res: Response, next: NextFunction) => { +export const postLogin = async ( + req: Request, + res: Response, + next: NextFunction +) => { const query = url.parse(req.url, true).query // The challenge is used to fetch information about the login request from ORY Hydra. @@ -76,33 +82,36 @@ export const postLogin = async (req: Request, res: Response, next: NextFunction) const challenge = String(query.login_challenge) const kratosSessionCookie = req.cookies.ory_kratos_session if (!kratosSessionCookie) { - console.log("No Kratos Cookie found") + console.log('No Kratos Cookie found') return redirectToLogin(req, res, next) } const hydraLoginState = req.query.hydra_login_state req.headers['host'] = config.kratos.public.split('/')[2] if (hydraLoginState !== req.session.hydraLoginState) { - console.log("Login state not equal to one previously set") + console.log('Login state not equal to one previously set') console.log(req.session.hydraLoginState) return redirectToLogin(req, res, next) } try { const session = await kratos.toSession(undefined, req.header('Cookie')) const { data } = session - const hydraResponse = await hydraAdmin.acceptLoginRequest(challenge, { subject: data.identity.id, context: data.identity,remember:true}) + console.log({ data }) + const hydraResponse = await hydraAdmin.acceptLoginRequest(challenge, { + subject: data.identity.id, + context: data.identity, + remember: true, + }) return res.redirect(hydraResponse.data.redirect_to) - } - catch (e) { + } catch (e) { next(e) } - } export default (req: Request, res: Response, next: NextFunction) => { const query = url.parse(req.url, true).query // The challenge is used to fetch information about the login request from ORY Hydra. const challenge = String(query.login_challenge) if (!challenge) { - console.error("Expected consent_challenge to be set") + console.error('Expected consent_challenge to be set') next(new Error('Expected a login challenge to be set but received none.')) return } @@ -110,26 +119,38 @@ export default (req: Request, res: Response, next: NextFunction) => { hydraAdmin .getLoginRequest(challenge) .then(async ({ data: body }) => { + console.log('login', body.request_url) // If hydra was already able to authenticate the user, skip will be true and we do not need to re-authenticate // the user. if (body.skip) { // You can apply logic here, for example update the number of times the user logged in. // ... - console.log("skipping login request") + + console.log('skipping login request') console.log({ body }) // 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...) + + + + const session = await kratos.adminGetIdentity(body.subject) + const { data } = session + console.log({ data }) + + return hydraAdmin .acceptLoginRequest(challenge, { // All we need to do is to confirm that we indeed want to log in the user. - subject: String(body.subject) + subject: String(body.subject), + context:data }) .then(({ data: body }) => { // All we need to do now is to redirect the user back to hydra! + console.log('login redirectURL',body.redirect_to) res.redirect(String(body.redirect_to)) }) } else { - console.log("not skipping login request") + console.log('not skipping login request') console.log({ body }) return redirectToLogin(req, res, next) } diff --git a/src/routes/patientPicker.ts b/src/routes/patientPicker.ts new file mode 100644 index 00000000..26eff9ee --- /dev/null +++ b/src/routes/patientPicker.ts @@ -0,0 +1,126 @@ +import { Request, Response, NextFunction } from 'express' +import { + AdminApi, + Configuration, + ConsentRequestSession, +} from '@oryd/hydra-client' +import config from '../config' +const hydraAdmin = new AdminApi( + new Configuration({ + basePath: config.hydra.admin, + }) +) + + +const createHydraSession = ( + requestedScope: string[] = [], + context, + launch +): ConsentRequestSession => { + return { + id_token: { + userdata: context, + launch, + }, + access_token: { + scope: requestedScope, + organization_id: 'medblocks', + patient_id: 'sidharth', + realm_access: { + roles: requestedScope, + }, + }, + } +} + +export const getPatientPicker = ( + req: Request, + res: Response, + next: NextFunction +) => { + console.log({req}) + // Parses the URL query + // The challenge is used to fetch information about the consent request from ORY Hydra. + const challenge = req.params.challenge as string + const launch = req.params.launch as string + + console.log('patientpicker',{challenge,launch},req.csrfToken()) //F1nO5k1S-iiosONWU2A_iLqzNmcj_FR24ACE + + if (!challenge) { + console.error('Expected challenge to be set') + next(new Error('Expected challenge to be set.')) + return + } + + hydraAdmin + .getConsentRequest(challenge) + // This will be called if the HTTP request was successful + .then(({ data: body }) => { + + console.log('showing patients') + + console.log('get patients',{ body }) + res.render('patientpicker', { + csrfToken: req.csrfToken(), + challenge: challenge, + launch:launch, + action:` ${config.baseUrl}/patientpicker/${challenge}/${launch}`, + + }) + }) + // This will handle any error that happens when making HTTP calls to hydra + .catch(next) +} + +export const postPatientPicker = ( + req: Request, + res: Response, + next: NextFunction +) => { + + console.log('entered the post patientpicker') + // The challenge is now a hidden input field, so let's take it from the request body instead + console.log(req.body) + const challenge = req.body.challenge + const launch = req.body.launch + const csrf = req.body.csrfToken + + console.log('post patientpicker',{challenge,launch,csrf}) + + let patient = req.body.patient + const decodedLaunch = Buffer.from(launch, 'base64') + + + // Seems like the user authenticated! Let's tell hydra... + hydraAdmin + .getConsentRequest(challenge) + // This will be called if the HTTP request was successful + .then(({ data: body }) => { + // console.log('launch',JSON.parse(decodedLaunch.toString())) + const acceptConsentRequest = { + grant_scope: body.requested_scope, + grant_access_token_audience: body.requested_access_token_audience, + session: createHydraSession( + body.requested_scope, + body.context, + {...JSON.parse(decodedLaunch.toString()),patient} + ), + } + + // We can grant all scopes that have been requested - hydra already checked for us that no additional scopes + // are requested accidentally. + + // ORY Hydra checks if requested audiences are allowed by the client, so we can simply echo this. + + // The session allows us to set session data for id and access tokens. Let's add the email if it is included. + + return hydraAdmin + .acceptConsentRequest(challenge, acceptConsentRequest) + }) + .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) +} diff --git a/views/login.hbs b/views/login.hbs index 9385b085..12e8879c 100644 --- a/views/login.hbs +++ b/views/login.hbs @@ -13,8 +13,8 @@
- Register new account - Reset password + Register new account + Reset password
diff --git a/views/patientpicker.hbs b/views/patientpicker.hbs new file mode 100644 index 00000000..681a7a4c --- /dev/null +++ b/views/patientpicker.hbs @@ -0,0 +1,27 @@ +
+ +
\ No newline at end of file