Skip to content

Commit

Permalink
feat(auth): allow issuing single record specific tokens (#7728)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
naftis authored Oct 25, 2024
1 parent 113f9eb commit a7fd62a
Show file tree
Hide file tree
Showing 24 changed files with 316 additions and 100 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
49 changes: 25 additions & 24 deletions packages/auth/src/features/authenticate/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -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[],
Expand Down Expand Up @@ -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
}
})
}
2 changes: 1 addition & 1 deletion packages/auth/src/features/invalidateToken/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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')
Expand All @@ -118,5 +67,5 @@ export async function tokenHandler(
true
)

return success(h, token)
return oauthResponse.success(h, token)
}
31 changes: 31 additions & 0 deletions packages/auth/src/features/oauthToken/handler.ts
Original file line number Diff line number Diff line change
@@ -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)
}
64 changes: 64 additions & 0 deletions packages/auth/src/features/oauthToken/responses.ts
Original file line number Diff line number Diff line change
@@ -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)
61 changes: 61 additions & 0 deletions packages/auth/src/features/oauthToken/token-exchange.ts
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/auth/src/features/verifyCode/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit a7fd62a

Please sign in to comment.