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

Re-implementing E-Signet flow to mosip repository #23

Merged
merged 22 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
693c91f
feat: initial commit of re-implementing E-Signet flow to mosip reposi…
Dec 11, 2024
2aa2ea2
feat: set up the get-oidp-user-info endpoint and replicated GraphQL q…
PathumN99 Dec 12, 2024
4b67f2f
feat: E-Signet mock server initial commit
PathumN99 Dec 12, 2024
695007b
refactor: moved e-signet related methods to esignet-api.ts
PathumN99 Dec 13, 2024
aed6020
refactor: refactored get-oidp-user-info endpoint
PathumN99 Dec 13, 2024
4f4ddb7
feat: Form configuration changes
PathumN99 Dec 17, 2024
f9a5720
Some minor amends
euanmillar Dec 17, 2024
fe72e50
aligned fetch token to e-signet requirements and changed port
euanmillar Dec 17, 2024
8e9c98b
Add search params to authorise and comments for todos
euanmillar Dec 17, 2024
ef95c8f
Add note around search params
euanmillar Dec 17, 2024
fd610fd
feat: oidc/userinfo endpoint in esignet-mock server
PathumN99 Dec 18, 2024
ad5cb20
/authorize endpoint in esignet-mock server
PathumN99 Dec 19, 2024
7e0e4b1
Added @fastify/formbody plugin to accept x-www-form-urlencoded conten…
PathumN99 Dec 19, 2024
823cdc5
/esignet/get-oidp-user-info endpoint minor changes
PathumN99 Dec 20, 2024
993c8b2
Minor changes in fetch location from FHIR URL
PathumN99 Dec 30, 2024
720bbb1
generateSignedJwt issue fixed in get-oidp-user-info API
PathumN99 Dec 30, 2024
1175621
Refactor JWT and set up monorepo
euanmillar Jan 3, 2025
1825d50
Remove completed todos
euanmillar Jan 6, 2025
54ae37e
Fix conflicts
euanmillar Jan 6, 2025
9f0f475
remove .DS_Store and update gitignore
euanmillar Jan 7, 2025
9f68810
rename webhooks in a future PR
euanmillar Jan 7, 2025
8781067
Bump version number
euanmillar Jan 7, 2025
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
5 changes: 5 additions & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
"envalid": "^8.0.0",
"fastify": "^5.0.0",
"fastify-type-provider-zod": "^4.0.2",
"jsonwebtoken": "^9.0.2",
"node-fetch": "2",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we need node-fetch with a recent Node version? The country-config package might need it (as @types/node-fetch) as it's exported into the country-configuration but otherwise we could be fine with the default one

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that we can test the server as standalone before it is merged into opencrvs-countryconfig-mosio, I think we should leave it in and remove later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it a devDependency. Is that OK?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When running locally standalone, we can use the Node's native fetch? And when the country-config uses the Docker image of packages/server, the contained image also can just use the native fetch? I think the country-config itself isn't running this code as is, if I understand this correctly

"pino": "^9.5.0",
euanmillar marked this conversation as resolved.
Show resolved Hide resolved
"tsx": "^4.19.2",
"typescript": "^5.6.3",
"zod": "^3.23.8"
Expand All @@ -21,6 +24,8 @@
"@eslint/js": "^9.13.0",
"@types/eslint__js": "^8.42.3",
"@types/fhir": "^0.0.37",
"@types/jsonwebtoken": "^9.0.7",
"@types/node-fetch": "^2.6.12",
"eslint": "^9.13.0",
"pino-pretty": "^11.3.0",
"typescript": "^5.6.3",
Expand Down
4 changes: 3 additions & 1 deletion packages/server/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cleanEnv, str, port } from "envalid";
import { cleanEnv, str, port, url } from "envalid";

export const env = cleanEnv(process.env, {
PORT: port({ default: 2024 }),
Expand All @@ -15,4 +15,6 @@ export const env = cleanEnv(process.env, {
devDefault: "http://localhost:7070/graphql",
desc: "The URL of the OpenCRVS GraphQL Gateway",
}),
HEARTH_URL: url({ default: 'http://localhost:3447/fhir' }),
NATIONAL_ID_OIDP_REST_URL: url({ default: '' }),
});
131 changes: 131 additions & 0 deletions packages/server/src/esignet-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* OpenCRVS is also distributed under the terms of the Civil Registration
* & Healthcare Disclaimer located at http://opencrvs.org/license.
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/

import * as jwt from "jsonwebtoken";
import { env } from "./constants";
import fetch from 'node-fetch'
import { logger } from "./logger";

type OIDPUserAddress = {
formatted: string;
street_address: string;
locality: string;
region: string;
postal_code: string;
city: string;
country: string;
};

type OIDPUserInfo = {
sub: string;
name?: string;
given_name?: string;
family_name?: string;
middle_name?: string;
nickname?: string;
preferred_username?: string;
profile?: string;
picture?: string;
website?: string;
email?: string;
email_verified?: boolean;
gender?: "female" | "male";
birthdate?: string;
zoneinfo?: string;
locale?: string;
phone_number?: string;
phone_number_verified?: boolean;
address?: Partial<OIDPUserAddress>;
updated_at?: number;
};

const OIDP_USERINFO_ENDPOINT =
env.NATIONAL_ID_OIDP_REST_URL && new URL('oidc/userinfo', env.NATIONAL_ID_OIDP_REST_URL).toString()

const decodeUserInfoResponse = (response: string) => {
return jwt.decode(response) as OIDPUserInfo;
};

export const fetchFromHearth = <T = any>(
euanmillar marked this conversation as resolved.
Show resolved Hide resolved
suffix: string,
method = "GET",
body: string | undefined = undefined
): Promise<T> => {
return fetch(`${env.HEARTH_URL}${suffix}`, {
method,
headers: {
"Content-Type": "application/fhir+json",
},
body,
})
.then((response) => {
return response.json();
})
.catch((error) => {
return Promise.reject(
new Error(`FHIR with Hearth request failed: ${error.message}`)
);
});
};

const searchLocationFromHearth = (name: string) =>
euanmillar marked this conversation as resolved.
Show resolved Hide resolved
fetchFromHearth<fhir2.Bundle>(
`/Location?${new URLSearchParams({ name, type: "ADMIN_STRUCTURE" })}`
);

const findAdminStructureLocationWithName = async (name: string) => {
const fhirBundleLocations = await searchLocationFromHearth(name);

if ((fhirBundleLocations.entry?.length ?? 0) > 1) {
throw new Error(
"Multiple admin structure locations found with the same name"
);
}

if ((fhirBundleLocations.entry?.length ?? 0) === 0) {
logger.warn("No admin structure location found with the name: " + name);
return null;
}

return fhirBundleLocations.entry?.[0].resource?.id;
};

const pickUserInfo = async (userInfo: OIDPUserInfo) => {
const stateFhirId =
userInfo.address?.country &&
(await findAdminStructureLocationWithName(userInfo.address.country));

return {
oidpUserInfo: userInfo,
stateFhirId,
districtFhirId:
userInfo.address?.region &&
(await findAdminStructureLocationWithName(userInfo.address.region)),
locationLevel3FhirId:
userInfo.address?.locality &&
(await findAdminStructureLocationWithName(userInfo.address.locality)),
};
};

export const fetchUserInfo = async (accessToken: string) => {
const request = await fetch(OIDP_USERINFO_ENDPOINT!, {
headers: {
Authorization: "Bearer " + accessToken,
},
});

const response = await request.text();
const decodedResponse = decodeUserInfoResponse(response);

logger.info(`OIDP user info response: ${JSON.stringify(decodedResponse)}`);

return pickUserInfo(decodedResponse);
};
10 changes: 9 additions & 1 deletion packages/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
validatorCompiler,
ZodTypeProvider,
} from "fastify-type-provider-zod";
import { mosipHandler, mosipNidSchema } from "./webhooks/mosip";
import { getOIDPUserInfo, mosipHandler, mosipNidSchema, OIDPUserInfoSchema } from "./webhooks/mosip";
euanmillar marked this conversation as resolved.
Show resolved Hide resolved
import { opencrvsHandler, opencrvsRecordSchema } from "./webhooks/opencrvs";
import { env } from "./constants";
import * as openapi from "./openapi-documentation";
Expand Down Expand Up @@ -50,6 +50,14 @@ app.after(() => {
body: mosipNidSchema,
},
});
app.withTypeProvider<ZodTypeProvider>().route({
url: "/esignet/get-oidp-user-info",
method: "POST",
handler: getOIDPUserInfo,
schema: {
body: OIDPUserInfoSchema,
},
});
});

async function run() {
Expand Down
17 changes: 17 additions & 0 deletions packages/server/src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* OpenCRVS is also distributed under the terms of the Civil Registration
* & Healthcare Disclaimer located at http://opencrvs.org/license.
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/
import pino from "pino"
export const logger = pino()

const level = process.env.NODE_ENV === 'test' ? 'silent' : process.env.LOG_LEVEL
if (level) {
logger.level = level
}
73 changes: 73 additions & 0 deletions packages/server/src/opencrvs-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,76 @@ export const upsertRegistrationIdentifier = (
},
headers,
});

export const getOIDPUserInfo = (
euanmillar marked this conversation as resolved.
Show resolved Hide resolved
{
code,
clientId,
redirectUri,
grantType,
}: {
code: String;
clientId: String;
redirectUri: String;
grantType: String;
},
{ headers }: { headers: Record<string, any> }
) =>
post({
query: /* GraphQL */ `
mutation getOIDPUserInfo(
$code: String!
$clientId: String!
$redirectUri: String!
$grantType: String
) {
getOIDPUserInfo(
code: $code
clientId: $clientId
redirectUri: $redirectUri
grantType: $grantType
) {
oidpUserInfo {
sub
name
given_name
family_name
middle_name
nickname
preferred_username
profile
picture
website
email
email_verified
gender
birthdate
zoneinfo
locale
phone_number
phone_number_verified
address {
formatted
street_address
locality
region
postal_code
city
country
}
updated_at
}
districtFhirId
stateFhirId
locationLevel3FhirId
}
}
`,
variables: {
code,
clientId,
redirectUri,
grantType,
},
headers,
});
31 changes: 31 additions & 0 deletions packages/server/src/webhooks/mosip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,34 @@ export const mosipHandler = async (

reply.code(200);
};

export const OIDPUserInfoSchema = z.object({
euanmillar marked this conversation as resolved.
Show resolved Hide resolved
code: z.string(),
clientId: z.string(),
redirectUri: z.string(),
grantType: z.string(),
});

type OIDPUserInfoRequest = FastifyRequest<{
euanmillar marked this conversation as resolved.
Show resolved Hide resolved
Body: z.infer<typeof OIDPUserInfoSchema>;
}>;

export const getOIDPUserInfo = async (
euanmillar marked this conversation as resolved.
Show resolved Hide resolved
request: OIDPUserInfoRequest,
reply: FastifyReply
) => {
const { token } = request.headers;
const { code, clientId, redirectUri, grantType } = request.body;

await opencrvs.getOIDPUserInfo(
{
code,
clientId,
redirectUri,
grantType,
},
{ headers: { Authorization: `Bearer ${token}` } }
);

reply.code(200);
};
Loading
Loading