From a7fd62a51a98e39361ac86cd9b0c2e0811554d2a Mon Sep 17 00:00:00 2001 From: Pyry Rouvila Date: Fri, 25 Oct 2024 15:30:39 +0300 Subject: [PATCH] feat(auth): allow issuing single record specific tokens (#7728) * refactor: move metrics out of authenticate * feat: initial token exchange endpoint * chore: update comment to be more specific * fix: issue with calling metrics environment * chore: amend changelog * refactor: improve clarity * fix: unused import * chore: add missing schema inputs * revert: revert mosip code removal to keep PR contained * chore: add comment about more fine-grained control * chore: fix test on gateway * feat: actually check if the record id matches to confirm record * revert: the confirm registration changes - lets do inanother pr * refactor: update error messages * fix: make the no-op simpler * fix: the query params not passing properly via gateway * refactor: remove unnecessary gql inputs * fix: update audiences to include minimum * fix: update the todo comment --- CHANGELOG.md | 3 +- .../auth/src/features/authenticate/service.ts | 49 +++++++------- .../src/features/invalidateToken/handler.ts | 2 +- .../client-credentials.ts} | 67 +++---------------- .../{system => oauthToken}/handler.test.ts | 0 .../auth/src/features/oauthToken/handler.ts | 31 +++++++++ .../auth/src/features/oauthToken/responses.ts | 64 ++++++++++++++++++ .../src/features/oauthToken/token-exchange.ts | 61 +++++++++++++++++ .../retrievalSteps/sendUserName/handler.ts | 2 +- .../auth/src/features/verifyCode/handler.ts | 4 +- packages/auth/src/metrics.ts | 35 ++++++++++ packages/auth/src/server.ts | 2 +- packages/commons/src/authentication.ts | 3 +- packages/config/src/config/routes.ts | 4 +- packages/gateway/src/config/proxies.ts | 18 +++++ packages/gateway/src/config/routes.ts | 3 +- .../features/registration/root-resolvers.ts | 21 +++++- .../src/features/registration/schema.graphql | 1 + .../gateway/src/features/user/utils/index.ts | 9 ++- packages/gateway/src/graphql/config.test.ts | 4 +- packages/gateway/src/graphql/config.ts | 11 +-- packages/gateway/src/graphql/schema.d.ts | 17 +++++ packages/gateway/src/graphql/schema.graphql | 1 + packages/user-mgnt/src/config/routes.ts | 4 +- 24 files changed, 316 insertions(+), 100 deletions(-) rename packages/auth/src/features/{system/handler.ts => oauthToken/client-credentials.ts} (51%) rename packages/auth/src/features/{system => oauthToken}/handler.test.ts (100%) create mode 100644 packages/auth/src/features/oauthToken/handler.ts create mode 100644 packages/auth/src/features/oauthToken/responses.ts create mode 100644 packages/auth/src/features/oauthToken/token-exchange.ts create mode 100644 packages/auth/src/metrics.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a22ee34320..00745c19a35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - A new field: `Time Period` is added to advanced search [#6365](https://github.com/opencrvs/opencrvs-core/issues/6365) - Deploy UI-Kit Storybook to [opencrvs.pages.dev](https://opencrvs.pages.dev) to allow extending OpenCRVS using the component library - Reoder the sytem user add/edit field for surname to be first, also change labels from `Last name` to `User's surname` and lastly remove the NID question from the form [#6830](https://github.com/opencrvs/opencrvs-core/issues/6830) +- Auth now allows exchanging user's token for a new record-specific token [#7728](https://github.com/opencrvs/opencrvs-core/issues/7728) ## Bug fixes @@ -53,7 +54,7 @@ - Fix layout issue that was causing the edit button on the AdvancedSearch's date range picker to not show on mobile view. [#7417](https://github.com/opencrvs/opencrvs-core/issues/7417) - Fix hardcoded placeholder copy of input when saving a query in advanced search - Handle label params used in form inputs when rendering in action details modal -- **Staged files getting reset on precommit hook failure** We were running lint-staged separately on each package using lerna which potentially created a race condition causing staged changes to get lost on failure. Now we are running lint-staged directly without depending on lerna. ***This is purely a DX improvement without affecting any functionality of the system*** +- **Staged files getting reset on precommit hook failure** We were running lint-staged separately on each package using lerna which potentially created a race condition causing staged changes to get lost on failure. Now we are running lint-staged directly without depending on lerna. **_This is purely a DX improvement without affecting any functionality of the system_** ### Breaking changes diff --git a/packages/auth/src/features/authenticate/service.ts b/packages/auth/src/features/authenticate/service.ts index 4bdad651dd4..3521566e3e7 100644 --- a/packages/auth/src/features/authenticate/service.ts +++ b/packages/auth/src/features/authenticate/service.ts @@ -9,6 +9,7 @@ * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ import fetch from 'node-fetch' +import { JWT_ISSUER } from '@auth/constants' import { resolve } from 'url' import { readFileSync } from 'fs' import { promisify } from 'util' @@ -21,7 +22,7 @@ import { sendVerificationCode, storeVerificationCode } from '@auth/features/verifyCode/service' -import { logger } from '@opencrvs/commons' +import { logger, UUID } from '@opencrvs/commons' import { unauthorized } from '@hapi/boom' import { chainW, tryCatch } from 'fp-ts/Either' import { pipe } from 'fp-ts/function' @@ -133,6 +134,29 @@ export async function createToken( }) } +export async function createTokenForRecordValidation( + userId: UUID, + recordId: UUID +) { + return sign( + { + scope: ['record.confirm-registration', 'record.reject-registration'], + recordId + }, + cert, + { + subject: userId, + algorithm: 'RS256', + expiresIn: '7 days', + audience: [ + 'opencrvs:gateway-user', // to get to the gateway + 'opencrvs:user-mgnt-user' // to allow the gateway to connect the 'sub' to an actual user + ], + issuer: JWT_ISSUER + } + ) +} + export async function storeUserInformation( nonce: string, userFullName: IUserName[], @@ -230,26 +254,3 @@ export function verifyToken(token: string) { export function getPublicKey() { return publicCert } - -export async function postUserActionToMetrics( - action: string, - token: string, - remoteAddress: string, - userAgent: string, - practitionerId?: string -) { - const url = resolve(env.METRICS_URL, '/audit/events') - const body = { action: action, practitionerId } - const authentication = 'Bearer ' + token - - await fetch(url, { - method: 'POST', - body: JSON.stringify(body), - headers: { - 'Content-Type': 'application/json', - Authorization: authentication, - 'x-real-ip': remoteAddress, - 'x-real-user-agent': userAgent - } - }) -} diff --git a/packages/auth/src/features/invalidateToken/handler.ts b/packages/auth/src/features/invalidateToken/handler.ts index 986bf8cce3e..9d9cbf12364 100644 --- a/packages/auth/src/features/invalidateToken/handler.ts +++ b/packages/auth/src/features/invalidateToken/handler.ts @@ -12,7 +12,7 @@ import * as Hapi from '@hapi/hapi' import * as Joi from 'joi' import { internal } from '@hapi/boom' import { invalidateToken } from '@auth/features/invalidateToken/service' -import { postUserActionToMetrics } from '@auth/features/authenticate/service' +import { postUserActionToMetrics } from '@auth/metrics' import { logger } from '@opencrvs/commons' interface IInvalidateTokenPayload { diff --git a/packages/auth/src/features/system/handler.ts b/packages/auth/src/features/oauthToken/client-credentials.ts similarity index 51% rename from packages/auth/src/features/system/handler.ts rename to packages/auth/src/features/oauthToken/client-credentials.ts index 18b180c2044..894871d085a 100644 --- a/packages/auth/src/features/system/handler.ts +++ b/packages/auth/src/features/oauthToken/client-credentials.ts @@ -8,6 +8,7 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ + import * as Hapi from '@hapi/hapi' import { authenticateSystem, @@ -21,80 +22,28 @@ import { AGE_VERIFICATION_USER_AUDIENCE, NATIONAL_ID_USER_AUDIENCE } from '@auth/constants' +import * as oauthResponse from './responses' -// Based on the example responses in 2023-09-14 -// https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/ - -const invalidRequest = (h: Hapi.ResponseToolkit) => - h - .response({ - error: 'invalid_request', - error_description: - 'Invalid request. Check that all required parameters (client_id, client_secret and grant_type) have been provided.', - error_uri: - 'Refer to https://documentation.opencrvs.org/technology/interoperability/authenticate-a-client' - }) - .header('Cache-Control', 'no-store') - .code(400) - -const invalidGrantType = (h: Hapi.ResponseToolkit) => - h - .response({ - error: 'unsupported_grant_type', - error_description: - 'Invalid grant type. Only client_credentials is supported.', - error_uri: - 'Refer to https://documentation.opencrvs.org/technology/interoperability/authenticate-a-client' - }) - - .header('Cache-Control', 'no-store') - .code(400) - -const invalidClient = (h: Hapi.ResponseToolkit) => - h - .response({ - error: 'invalid_client', - error_description: 'Invalid client id or secret', - error_uri: - 'Refer to https://documentation.opencrvs.org/technology/interoperability/authenticate-a-client' - }) - .header('Cache-Control', 'no-store') - .code(401) - -const success = (h: Hapi.ResponseToolkit, token: string) => - h - .response({ - access_token: token, - token_type: 'Bearer' - }) - .header('Cache-Control', 'no-store') - .code(200) - -export async function tokenHandler( +export async function clientCredentialsHandler( request: Hapi.Request, h: Hapi.ResponseToolkit ) { const clientId = request.query.client_id const clientSecret = request.query.client_secret - const grantType = request.query.grant_type - - if (!clientId || !clientSecret || !grantType) { - return invalidRequest(h) - } - if (grantType !== 'client_credentials') { - return invalidGrantType(h) + if (!clientId || !clientSecret) { + return oauthResponse.invalidRequest(h) } let result try { result = await authenticateSystem(clientId, clientSecret) } catch (err) { - return invalidClient(h) + return oauthResponse.invalidClient(h) } if (result.status !== 'active') { - return invalidClient(h) + return oauthResponse.invalidClient(h) } const isNotificationAPIUser = result.scope.includes('notification-api') @@ -118,5 +67,5 @@ export async function tokenHandler( true ) - return success(h, token) + return oauthResponse.success(h, token) } diff --git a/packages/auth/src/features/system/handler.test.ts b/packages/auth/src/features/oauthToken/handler.test.ts similarity index 100% rename from packages/auth/src/features/system/handler.test.ts rename to packages/auth/src/features/oauthToken/handler.test.ts diff --git a/packages/auth/src/features/oauthToken/handler.ts b/packages/auth/src/features/oauthToken/handler.ts new file mode 100644 index 00000000000..787f97708b5 --- /dev/null +++ b/packages/auth/src/features/oauthToken/handler.ts @@ -0,0 +1,31 @@ +/* + * 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 Hapi from '@hapi/hapi' +import { clientCredentialsHandler } from './client-credentials' +import * as oauthResponse from './responses' +import { tokenExchangeHandler } from './token-exchange' + +export async function tokenHandler( + request: Hapi.Request, + h: Hapi.ResponseToolkit +) { + const grantType = request.query.grant_type + + if (grantType === 'client_credentials') { + return clientCredentialsHandler(request, h) + } + + if (grantType === 'urn:opencrvs:oauth:grant-type:token-exchange') { + return tokenExchangeHandler(request, h) + } + + return oauthResponse.invalidGrantType(h) +} diff --git a/packages/auth/src/features/oauthToken/responses.ts b/packages/auth/src/features/oauthToken/responses.ts new file mode 100644 index 00000000000..7fac3f7f346 --- /dev/null +++ b/packages/auth/src/features/oauthToken/responses.ts @@ -0,0 +1,64 @@ +/* + * 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 Hapi from '@hapi/hapi' + +export const invalidRequest = (h: Hapi.ResponseToolkit) => + h + .response({ + error: 'invalid_request', + error_description: + 'Invalid request. Check that all required parameters have been provided.', + error_uri: + 'Refer to https://documentation.opencrvs.org/technology/interoperability/authenticate-a-client' + }) + .header('Cache-Control', 'no-store') + .code(400) + +export const invalidGrantType = (h: Hapi.ResponseToolkit) => + h + .response({ + error: 'unsupported_grant_type', + error_description: `Invalid or undefined grant type. Only "client_credentials" or "urn:opencrvs:oauth:grant-type:token-exchange" are supported.`, + error_uri: + 'Refer to https://documentation.opencrvs.org/technology/interoperability/authenticate-a-client' + }) + + .header('Cache-Control', 'no-store') + .code(400) + +export const invalidClient = (h: Hapi.ResponseToolkit) => + h + .response({ + error: 'invalid_client', + error_description: 'Invalid client id or secret', + error_uri: + 'Refer to https://documentation.opencrvs.org/technology/interoperability/authenticate-a-client' + }) + .header('Cache-Control', 'no-store') + .code(401) + +export const invalidSubjectToken = (h: Hapi.ResponseToolkit) => + h + .response({ + error: 'unauthorized_client', + error_description: 'Invalid subject token' + }) + .header('Cache-Control', 'no-store') + .code(401) + +export const success = (h: Hapi.ResponseToolkit, token: string) => + h + .response({ + access_token: token, + token_type: 'Bearer' + }) + .header('Cache-Control', 'no-store') + .code(200) diff --git a/packages/auth/src/features/oauthToken/token-exchange.ts b/packages/auth/src/features/oauthToken/token-exchange.ts new file mode 100644 index 00000000000..a1a2b843409 --- /dev/null +++ b/packages/auth/src/features/oauthToken/token-exchange.ts @@ -0,0 +1,61 @@ +/* + * 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 oauthResponse from './responses' +import * as Hapi from '@hapi/hapi' +import { + createTokenForRecordValidation, + verifyToken +} from '@auth/features/authenticate/service' +import { pipe } from 'fp-ts/lib/function' +import { UUID } from '@opencrvs/commons' + +export const SUBJECT_TOKEN_TYPE = + 'urn:ietf:params:oauth:token-type:access_token' +export const RECORD_TOKEN_TYPE = + 'urn:opencrvs:oauth:token-type:single_record_token' + +/** + * Allows creating record-specific tokens for when a 3rd party system needs to confirm a registration + * + * https://datatracker.ietf.org/doc/html/rfc8693#section-2.1 + */ +export async function tokenExchangeHandler( + request: Hapi.Request, + h: Hapi.ResponseToolkit +) { + const subjectToken = request.query.subject_token + const subjectTokenType = request.query.subject_token_type + const requestedTokenType = request.query.requested_token_type + const recordId = request.query.record_id + + if ( + !recordId || + !subjectToken || + subjectTokenType !== SUBJECT_TOKEN_TYPE || + requestedTokenType !== RECORD_TOKEN_TYPE + ) { + return oauthResponse.invalidRequest(h) + } + + const decodedOrError = pipe(subjectToken, verifyToken) + if (decodedOrError._tag === 'Left') { + return oauthResponse.invalidSubjectToken(h) + } + const { sub } = decodedOrError.right + + // @TODO: If in the future we have a fine grained access control for records, check here that the subject actually has access to the record requested + const recordToken = await createTokenForRecordValidation( + sub as UUID, + recordId + ) + + return oauthResponse.success(h, recordToken) +} diff --git a/packages/auth/src/features/retrievalSteps/sendUserName/handler.ts b/packages/auth/src/features/retrievalSteps/sendUserName/handler.ts index 2e4eef210ac..e971215c42b 100644 --- a/packages/auth/src/features/retrievalSteps/sendUserName/handler.ts +++ b/packages/auth/src/features/retrievalSteps/sendUserName/handler.ts @@ -20,8 +20,8 @@ import { deleteRetrievalStepInformation } from '@auth/features/retrievalSteps/verifyUser/service' import { logger } from '@opencrvs/commons' +import { postUserActionToMetrics } from '@auth/metrics' import { env } from '@auth/environment' -import { postUserActionToMetrics } from '@auth/features/authenticate/service' interface IPayload { nonce: string diff --git a/packages/auth/src/features/verifyCode/handler.ts b/packages/auth/src/features/verifyCode/handler.ts index 2d913920494..4843f62707b 100644 --- a/packages/auth/src/features/verifyCode/handler.ts +++ b/packages/auth/src/features/verifyCode/handler.ts @@ -17,9 +17,9 @@ import { } from '@auth/features/verifyCode/service' import { getStoredUserInformation, - createToken, - postUserActionToMetrics + createToken } from '@auth/features/authenticate/service' +import { postUserActionToMetrics } from '@auth/metrics' import { logger } from '@opencrvs/commons' import { WEB_USER_JWT_AUDIENCES, JWT_ISSUER } from '@auth/constants' interface IVerifyPayload { diff --git a/packages/auth/src/metrics.ts b/packages/auth/src/metrics.ts new file mode 100644 index 00000000000..a608074df4c --- /dev/null +++ b/packages/auth/src/metrics.ts @@ -0,0 +1,35 @@ +/* + * 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 { env } from './environment' +import { resolve } from 'url' + +export async function postUserActionToMetrics( + action: string, + token: string, + remoteAddress: string, + userAgent: string, + practitionerId?: string +) { + const url = resolve(env.METRICS_URL, '/audit/events') + const body = { action: action, practitionerId } + const authentication = 'Bearer ' + token + + await fetch(url, { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + Authorization: authentication, + 'x-real-ip': remoteAddress, + 'x-real-user-agent': userAgent + } + }) +} diff --git a/packages/auth/src/server.ts b/packages/auth/src/server.ts index 15e33e8063c..09ae775ede5 100644 --- a/packages/auth/src/server.ts +++ b/packages/auth/src/server.ts @@ -59,7 +59,7 @@ import changePasswordHandler, { import sendUserNameHandler, { requestSchema as reqSendUserNameSchema } from '@auth/features/retrievalSteps/sendUserName/handler' -import { tokenHandler } from '@auth/features/system/handler' +import { tokenHandler } from '@auth/features/oauthToken/handler' import { logger } from '@opencrvs/commons' import { getPublicKey } from '@auth/features/authenticate/service' import anonymousTokenHandler, { diff --git a/packages/commons/src/authentication.ts b/packages/commons/src/authentication.ts index 8b0f09afa8f..2300a607644 100644 --- a/packages/commons/src/authentication.ts +++ b/packages/commons/src/authentication.ts @@ -25,7 +25,8 @@ export const userScopes = { /** Bypasses the rate limiting in gateway. Useful for data seeder. */ bypassRateLimit: 'bypassratelimit', teams: 'teams', - config: 'config' + config: 'config', + confirmRegistration: 'record.confirm-registration' } as const export const userRoleScopes = { diff --git a/packages/config/src/config/routes.ts b/packages/config/src/config/routes.ts index 2fb26a97195..50adc608592 100644 --- a/packages/config/src/config/routes.ts +++ b/packages/config/src/config/routes.ts @@ -79,7 +79,9 @@ export default function getRoutes(): ServerRoute[] { RouteScope.CERTIFY, RouteScope.PERFORMANCE, RouteScope.SYSADMIN, - RouteScope.VALIDATE + RouteScope.VALIDATE, + // @TODO: Refer to an enum / constant + 'record.confirm-registration' ] }, tags: ['api'], diff --git a/packages/gateway/src/config/proxies.ts b/packages/gateway/src/config/proxies.ts index 8f03e331736..b8e8b249c72 100644 --- a/packages/gateway/src/config/proxies.ts +++ b/packages/gateway/src/config/proxies.ts @@ -98,6 +98,24 @@ export const catchAllProxy = { } } satisfies Record +export const authProxy = { + token: { + method: 'POST', + path: '/auth/token', + handler: (req, h) => + h.proxy({ + uri: AUTH_URL + `/token${req.url.search}` + }), + options: { + auth: false, + payload: { + output: 'data', + parse: false + } + } + } +} satisfies Record + export const rateLimitedAuthProxy = { authenticate: { method: 'POST', diff --git a/packages/gateway/src/config/routes.ts b/packages/gateway/src/config/routes.ts index 10d756ed65b..6b065169171 100644 --- a/packages/gateway/src/config/routes.ts +++ b/packages/gateway/src/config/routes.ts @@ -15,7 +15,7 @@ import { validationFailedAction } from '@gateway/features/eventNotification/eventNotificationHandler' import { ServerRoute } from '@hapi/hapi' -import { catchAllProxy, rateLimitedAuthProxy } from './proxies' +import { authProxy, catchAllProxy, rateLimitedAuthProxy } from './proxies' import sendVerifyCodeHandler, { requestSchema, responseSchema @@ -84,6 +84,7 @@ export const getRoutes = () => { catchAllProxy.locationId, catchAllProxy.auth, + authProxy.token, rateLimitedAuthProxy.authenticate, rateLimitedAuthProxy.authenticateSuperUser, rateLimitedAuthProxy.verifyUser diff --git a/packages/gateway/src/features/registration/root-resolvers.ts b/packages/gateway/src/features/registration/root-resolvers.ts index ca6adfd8f2e..bad69765683 100644 --- a/packages/gateway/src/features/registration/root-resolvers.ts +++ b/packages/gateway/src/features/registration/root-resolvers.ts @@ -10,7 +10,11 @@ */ import { AUTH_URL, COUNTRY_CONFIG_URL, SEARCH_URL } from '@gateway/constants' import { fetchFHIR } from '@gateway/features/fhir/service' -import { hasScope, inScope } from '@gateway/features/user/utils' +import { + hasRecordAccess, + hasScope, + inScope +} from '@gateway/features/user/utils' import fetch from '@gateway/fetch' import { IAuthHeader } from '@opencrvs/commons' import { @@ -631,6 +635,21 @@ export const resolvers: GQLResolver = { ) return taskEntry.resource.id + }, + async confirmRegistration(_, { id }, { headers: authHeader }) { + if (!inScope(authHeader, ['record.confirm-registration'])) { + throw new Error( + 'User does not have a "record.confirm-registration" scope' + ) + } + + if (!hasRecordAccess(authHeader, id)) { + throw new Error('User does not have access to the record') + } + + // @TODO this is a no-op, only to test the token exchange actually works + // An upcoming pull request will implement this and a `rejectRegistration` mutations + return id } } } diff --git a/packages/gateway/src/features/registration/schema.graphql b/packages/gateway/src/features/registration/schema.graphql index 191c438008a..eab611f0358 100644 --- a/packages/gateway/src/features/registration/schema.graphql +++ b/packages/gateway/src/features/registration/schema.graphql @@ -643,4 +643,5 @@ type Mutation { comment: String duplicateTrackingId: String ): ID! + confirmRegistration(id: ID!): ID! } diff --git a/packages/gateway/src/features/user/utils/index.ts b/packages/gateway/src/features/user/utils/index.ts index 9618fe3c29a..dc9fd5c513b 100644 --- a/packages/gateway/src/features/user/utils/index.ts +++ b/packages/gateway/src/features/user/utils/index.ts @@ -8,7 +8,7 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import { IAuthHeader, logger } from '@opencrvs/commons' +import { IAuthHeader, logger, UUID } from '@opencrvs/commons' import { USER_MANAGEMENT_URL } from '@gateway/constants' import { ISystemModelData, @@ -23,6 +23,8 @@ export interface ITokenPayload { exp: string algorithm: string scope: string[] + /** The record ID that the token has access to */ + recordId?: UUID } export type scopeType = @@ -115,6 +117,11 @@ export const getTokenPayload = (token: string): ITokenPayload => { return decoded } +export const hasRecordAccess = (authHeader: IAuthHeader, recordId: string) => { + const tokenPayload = getTokenPayload(authHeader.Authorization.split(' ')[1]) + return tokenPayload.recordId === recordId +} + export const getUserId = (authHeader: IAuthHeader): string => { if (!authHeader || !authHeader.Authorization) { throw new Error(`getUserId: Error occurred during token decode`) diff --git a/packages/gateway/src/graphql/config.test.ts b/packages/gateway/src/graphql/config.test.ts index 5a5671b4865..5aa4694317a 100644 --- a/packages/gateway/src/graphql/config.test.ts +++ b/packages/gateway/src/graphql/config.test.ts @@ -189,6 +189,8 @@ describe('Test apollo server config', () => { request: request } ) - expect(response.errors![0].message).toBe('Authentication failed') + expect(response.errors![0].message).toBe( + 'User does not have a register or validate scope' + ) }) }) diff --git a/packages/gateway/src/graphql/config.ts b/packages/gateway/src/graphql/config.ts index d103a77326d..fa554e2e352 100644 --- a/packages/gateway/src/graphql/config.ts +++ b/packages/gateway/src/graphql/config.ts @@ -47,7 +47,7 @@ import { import { AuthenticationError, Config, gql } from 'apollo-server-hapi' import { readFileSync } from 'fs' import { IResolvers } from 'graphql-tools' -import { merge, isEqual } from 'lodash' +import { merge } from 'lodash' import LocationsAPI from '@gateway/features/fhir/locationsAPI' import DocumentsAPI from '@gateway/features/fhir/documentsAPI' import PaymentsAPI from '@gateway/features/fhir/paymentsAPI' @@ -226,9 +226,12 @@ export function authSchemaTransformer(schema: GraphQLSchema) { throw new AuthenticationError('Authentication failed') } - if (credentials && !isEqual(credentials.scope, user.scope)) { - throw new AuthenticationError('Authentication failed') - } + // @TODO: When scope work is done, this check should stay. + // For now, the registrar might not have 'record.confirm-registration' token, but the per-record issued token will have it + + // if (credentials && !isEqual(credentials.scope, user.scope)) { + // throw new AuthenticationError('Authentication failed') + // } } catch (err) { throw new AuthenticationError(err) } diff --git a/packages/gateway/src/graphql/schema.d.ts b/packages/gateway/src/graphql/schema.d.ts index df923dcf8a9..a02daf1f502 100644 --- a/packages/gateway/src/graphql/schema.d.ts +++ b/packages/gateway/src/graphql/schema.d.ts @@ -86,6 +86,7 @@ export interface GQLMutation { markMarriageAsCertified: string markMarriageAsIssued: string markEventAsDuplicate: string + confirmRegistration: string createOrUpdateUser: GQLUser activateUser?: string changePassword?: string @@ -2507,6 +2508,7 @@ export interface GQLMutationTypeResolver { markMarriageAsCertified?: MutationToMarkMarriageAsCertifiedResolver markMarriageAsIssued?: MutationToMarkMarriageAsIssuedResolver markEventAsDuplicate?: MutationToMarkEventAsDuplicateResolver + confirmRegistration?: MutationToConfirmRegistrationResolver createOrUpdateUser?: MutationToCreateOrUpdateUserResolver activateUser?: MutationToActivateUserResolver changePassword?: MutationToChangePasswordResolver @@ -3039,6 +3041,21 @@ export interface MutationToMarkEventAsDuplicateResolver< ): TResult } +export interface MutationToConfirmRegistrationArgs { + id: string +} +export interface MutationToConfirmRegistrationResolver< + TParent = any, + TResult = any +> { + ( + parent: TParent, + args: MutationToConfirmRegistrationArgs, + context: Context, + info: GraphQLResolveInfo + ): TResult +} + export interface MutationToCreateOrUpdateUserArgs { user: GQLUserInput } diff --git a/packages/gateway/src/graphql/schema.graphql b/packages/gateway/src/graphql/schema.graphql index 9ccd12481a5..ec49396ea5a 100644 --- a/packages/gateway/src/graphql/schema.graphql +++ b/packages/gateway/src/graphql/schema.graphql @@ -222,6 +222,7 @@ type Mutation { comment: String duplicateTrackingId: String ): ID! + confirmRegistration(id: ID!): ID! createOrUpdateUser(user: UserInput!): User! activateUser( userId: String! diff --git a/packages/user-mgnt/src/config/routes.ts b/packages/user-mgnt/src/config/routes.ts index f83656e289b..923aa0f1c92 100644 --- a/packages/user-mgnt/src/config/routes.ts +++ b/packages/user-mgnt/src/config/routes.ts @@ -374,7 +374,9 @@ export const getRoutes: () => Hapi.ServerRoute[] = () => { RouteScope.SYSADMIN, RouteScope.VALIDATE, RouteScope.VERIFY, - RouteScope.RECORDSEARCH + RouteScope.RECORDSEARCH, + // @TODO: Refer to an enum / constant + 'record.confirm-registration' ] }, validate: {