Skip to content

Commit

Permalink
feat: complete account linking workflow (#82)
Browse files Browse the repository at this point in the history
* feat: complete account linking workflow

* chore: fix file

* chore: fix lint

* chore: change wait4

* chore: change back host

* chore: change up host

* chore: bump wait time

* chore: address comments
  • Loading branch information
kleyow committed Aug 2, 2021
1 parent a7594a2 commit da22b5e
Show file tree
Hide file tree
Showing 10 changed files with 476 additions and 286 deletions.
1 change: 1 addition & 0 deletions ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ declare module '@mojaloop/central-services-metrics'
declare module '@hapi/good'
declare module 'hapi-openapi'
declare module 'blipp'
declare module 'string-to-arraybuffer'

// version 2.4 -> https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/javascript-state-machine/index.d.ts
// we are using ^3.1.0
Expand Down
2 changes: 1 addition & 1 deletion config/integration.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"client": "mysql",
"version": "5.5",
"connection": {
"host": "localhost",
"host": "172.17.0.1",
"port": 3306,
"user": "auth-service",
"password": "password",
Expand Down
36 changes: 36 additions & 0 deletions src/domain/buffer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*****
License
--------------
Copyright © 2020 Mojaloop Foundation
The Mojaloop files are made available by the Mojaloop Foundation under the
Apache License, Version 2.0 (the 'License') and you may not use these files
except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, the Mojaloop
files are distributed onan 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
Contributors
--------------
This is the official list of the Mojaloop project contributors for this file.
Names of the original copyright holders (individuals or organizations)
should be listed with a '*' in the first column. People who have
contributed from an organization can be listed under the organization
that actually holds the copyright for their contributions (see the
Gates Foundation organization for an example). Those individuals should have
their names indented and be marked with a '-'. Email address can be added
optionally within square brackets <email>.
- Kevin Leyow <kevin.leyow@modusbox.com>
--------------
******/

export function decodeBase64String (str: string): string {
const base64Buffer = Buffer.from(str, 'base64')
return base64Buffer.toString('utf-8')
}

export function encodeBase64String (str: string, encoding: BufferEncoding = 'utf-8'): string {
const buff = Buffer.from(str, encoding)
return buff.toString('base64')
}
13 changes: 13 additions & 0 deletions src/domain/challenge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
import util from 'util'
import crypto from 'crypto'
import { logger } from '~/shared/logger'
import { thirdparty as tpAPI } from '@mojaloop/api-snippets'
import { canonicalize } from 'json-canonicalize'
import sha256 from 'crypto-js/sha256'

// Async promisified randomBytes function
const randomBytesAsync = util.promisify(crypto.randomBytes)
Expand Down Expand Up @@ -82,3 +85,13 @@ export function verifySignature (
throw error
}
}

export function deriveChallenge (consentsPostRequest: tpAPI.Schemas.ConsentsPostRequestAUTH): string {
const rawChallenge = {
consentId: consentsPostRequest.consentId,
scopes: consentsPostRequest.scopes
}

const RFC8785String = canonicalize(rawChallenge)
return sha256(RFC8785String).toString()
}
6 changes: 1 addition & 5 deletions src/domain/consents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,23 +54,19 @@ import {
import { verifySignature } from '~/domain/challenge'
import { CredentialStatusEnum } from '~/model/consent/consent'
import { thirdparty as tpAPI } from '@mojaloop/api-snippets'
import * as fido from '~/domain/fido-credential'
/**
* Builds internal Consent and Scope objects from request payload
* Stores the objects in the database
* @param request request received from switch
*/
export async function createAndStoreConsent (
consentId: string,
// initiatorId is deprecated
initiatorId: string,
participantId: string,
thirdpartyScopes: tpAPI.Schemas.Scope[],
credential: tpAPI.Schemas.SignedCredential
): Promise<void> {
// validate FIDO credential attestation
if (!fido.validate(credential.payload)) {
throw new SignatureVerificationError(consentId)
}
// TODO: store properly whole credential or only credential.payload.id ?
const consent: Consent = {
id: consentId,
Expand Down
2 changes: 2 additions & 0 deletions src/model/registerConsent.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export enum RegisterConsentPhase {
export interface RegisterConsentStateMachine extends ControlledStateMachine {
verifyConsent: Method
onVerifyConsent: Method
storeConsent: Method
onStoreConsent: Method
registerAuthoritativeSourceWithALS: Method
onRegisterAuthoritativeSourceWithALS: Method
sendConsentCallbackToDFSP: Method
Expand Down
90 changes: 79 additions & 11 deletions src/model/registerConsent.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,15 @@ import deferredJob from '~/shared/deferred-job'

import { reformatError } from '~/shared/api-error'
import axios from 'axios'
import { deriveChallenge } from '~/domain/challenge'
import { encodeBase64String, decodeBase64String } from '../domain/buffer'
import {
v1_1 as fspiopAPI,
thirdparty as tpAPI
} from '@mojaloop/api-snippets'
import { AttestationResult, ExpectedAttestationResult, Fido2Lib } from 'fido2-lib'
import str2ab from 'string-to-arraybuffer'
import { createAndStoreConsent } from '~/domain/consents'

export class RegisterConsentModel
extends PersistentModel<RegisterConsentStateMachine, RegisterConsentData> {
Expand All @@ -58,12 +63,14 @@ export class RegisterConsentModel
init: 'start',
transitions: [
{ name: 'verifyConsent', from: 'start', to: 'consentVerified' },
{ name: 'registerAuthoritativeSourceWithALS', from: 'consentVerified', to: 'registeredAsAuthoritativeSource' },
{ name: 'storeConsent', from: 'consentVerified', to: 'consentStoredAndVerified' },
{ name: 'registerAuthoritativeSourceWithALS', from: 'consentStoredAndVerified', to: 'registeredAsAuthoritativeSource' },
{ name: 'sendConsentCallbackToDFSP', from: 'registeredAsAuthoritativeSource', to: 'callbackSent' }
],
methods: {
// specific transitions handlers methods
onVerifyConsent: () => this.onVerifyConsent(),
onStoreConsent: () => this.onStoreConsent(),
onRegisterAuthoritativeSourceWithALS: () => this.onRegisterAuthoritativeSourceWithALS(),
onSendConsentCallbackToDFSP: () => this.onSendConsentCallbackToDFSP()
}
Expand Down Expand Up @@ -112,17 +119,75 @@ export class RegisterConsentModel
}

async onVerifyConsent (): Promise<void> {
// not sure what functions to use or if they are ready
// for now we are just going do nothing here.
// todo: update transition to
// - verify consent
// - store consent
// - throw errors if there are errors in verifying the consent
/*
const { consentsPostRequestAUTH, participantDFSPId } = this.data
try {
const derivedChallenge = deriveChallenge(consentsPostRequestAUTH)
const encodedDerivedChallenge = encodeBase64String(derivedChallenge)

const decodedJsonString = decodeBase64String(consentsPostRequestAUTH.credential.payload.response.clientDataJSON)
const parsedClientData = JSON.parse(decodedJsonString)

const attestationExpectations: ExpectedAttestationResult = {
challenge: encodedDerivedChallenge,
// not sure what origin should be here
// decoding and copying the origin for now
origin: parsedClientData.origin,
factor: 'either'
}

const f2l = new Fido2Lib()
const clientAttestationResponse: AttestationResult = {
id: str2ab(consentsPostRequestAUTH.credential.payload.id),
rawId: str2ab(consentsPostRequestAUTH.credential.payload.rawId),
response: {
clientDataJSON: consentsPostRequestAUTH.credential.payload.response.clientDataJSON,
attestationObject: consentsPostRequestAUTH.credential.payload.response.attestationObject
}
}

await f2l.attestationResult(
clientAttestationResponse,
attestationExpectations
)
} catch (error) {
this.logger.push({ error }).error('start -> requestIsValid')
this.logger.push({ error }).error('start -> consentVerified')

let mojaloopError
// if error is planned and is a MojaloopApiErrorCode we send back that code
if ((error as Errors.MojaloopApiErrorCode).code) {
mojaloopError = reformatError(error, this.logger)
} else {
// if error is not planned send back a generalized error
mojaloopError = reformatError(
Errors.MojaloopApiErrorCodes.TP_ACCOUNT_LINKING_ERROR,
this.logger
)
}

await this.thirdpartyRequests.putConsentsError(
consentsPostRequestAUTH.consentId,
mojaloopError as unknown as fspiopAPI.Schemas.ErrorInformationObject,
participantDFSPId
)

// throw error to stop state machine
throw error
}
}

async onStoreConsent (): Promise<void> {
const { consentsPostRequestAUTH, participantDFSPId } = this.data
try {
await createAndStoreConsent(
consentsPostRequestAUTH.consentId,
// initiatorId is deprecated so just using empty string for now
'',
participantDFSPId,
consentsPostRequestAUTH.scopes,
consentsPostRequestAUTH.credential
)
} catch (error) {
this.logger.push({ error }).error('consentVerified -> consentStoredAndVerified')

let mojaloopError
// if error is planned and is a MojaloopApiErrorCode we send back that code
Expand All @@ -145,7 +210,6 @@ export class RegisterConsentModel
// throw error to stop state machine
throw error
}
*/
}

async onRegisterAuthoritativeSourceWithALS (): Promise<void> {
Expand Down Expand Up @@ -209,7 +273,7 @@ export class RegisterConsentModel
})
.wait(this.config.requestProcessingTimeoutSeconds * 1000)
} catch (error) {
this.logger.push({ error }).error('consentVerified -> registeredAsAuthoritativeSource')
this.logger.push({ error }).error('consentStoredAndVerified -> registeredAsAuthoritativeSource')
// we send back an account linking error despite the actual error
const mojaloopError = reformatError(
Errors.MojaloopApiErrorCodes.TP_ACCOUNT_LINKING_ERROR,
Expand Down Expand Up @@ -278,6 +342,10 @@ export class RegisterConsentModel
return this.run()

case 'consentVerified':
await this.fsm.storeConsent()
return this.run()

case 'consentStoredAndVerified':
await this.fsm.registerAuthoritativeSourceWithALS()
// check if the ALS sent back an error
await this.checkModelDataForErrorInformation()
Expand Down
Loading

0 comments on commit da22b5e

Please sign in to comment.