Skip to content

Commit

Permalink
fix: Add DID deactivation check for credential issuing [DEV-3136] (#345)
Browse files Browse the repository at this point in the history
* Add parameter allowDeactivated for allowing issue
credential with deactivatedDID in /credential/issue endpoint.

* Update package-lock.json.

* Remove the custom implementation of JSON.stringify
because this custom method is written incorrectly and doesn't allow to
show swagger.json in SwaggerUI.

* Fix login/logout dynamic custom button panel.

* Revert code.

* Update custom-button.ts.

* Refactor code.

* Remove an unnecessary use of boolean literals in
conditional expression.

* Add allowDeactivatedDid query parameter to
/credential/verify endpoint.

* Remove an unnecessary use of boolean literals in
conditional expression.

* Add implementation for blocking issue credential
based on a deactivated DID.

* Add allowDeactivatedDid query parameter for
/presentation/verify endpoint.

* Update package-lock.json.

* Update package-lock.json

* Add more wide error code when result is empty and
etc.

* Add more wide error code for presentation/verify.

* Update package-lock.json.

* Move a common logic of decode JWT to helper.ts.

* Remove duplicate import packages.

* Update package-lock.json.

* Add static validation for JWT.

* Update .eslintrc.json

* Check credential status before suspension/reinstate

* Update package-lock.json

* npm run format

* PresentationVerifyRequest

* Add presentation validator JWT check

---------

Co-authored-by: Ankur Banerjee <ankurdotb@users.noreply.github.com>
  • Loading branch information
abdulla-ashurov and ankurdotb committed Sep 8, 2023
1 parent 95385f9 commit 023bba1
Show file tree
Hide file tree
Showing 10 changed files with 314 additions and 211 deletions.
2 changes: 1 addition & 1 deletion .github/linters/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018,
"ecmaVersion": "ESNext",
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "jest"],
Expand Down
292 changes: 146 additions & 146 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ class App {
app.post(`/credential/issue`, CredentialController.issueValidator, new CredentialController().issue);
app.post(`/credential/verify`, CredentialController.credentialValidator, new CredentialController().verify);
app.post(`/credential/revoke`, CredentialController.credentialValidator, new CredentialController().revoke);
app.post('/credential/suspend', new CredentialController().suspend);
app.post('/credential/reinstate', new CredentialController().reinstate);
app.post('/credential/suspend', CredentialController.credentialValidator, new CredentialController().suspend);
app.post('/credential/reinstate', CredentialController.credentialValidator, new CredentialController().reinstate);

// presentation
app.post(
Expand Down
109 changes: 103 additions & 6 deletions src/controllers/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { check, query, validationResult } from 'express-validator';

import { Credentials } from '../services/credentials.js';
import { IdentityServiceStrategySetup } from '../services/identity/index.js';
import jwt_decode from 'jwt-decode';

export class CredentialController {
public static issueValidator = [
Expand Down Expand Up @@ -35,7 +36,18 @@ export class CredentialController {
}
return false;
})
.withMessage('Entry must be a jwt string or an credential'),
.withMessage('Entry must be a JWT or a credential body with JWT proof')
.custom((value) => {
if (typeof value === 'string') {
try {
jwt_decode(value);
} catch (e) {
return false;
}
}
return true;
})
.withMessage('An invalid JWT string'),
check('policies').optional().isObject().withMessage('Verification policies should be an object'),
query('verifyStatus').optional().isBoolean().withMessage('verifyStatus should be a boolean value'),
query('publish').optional().isBoolean().withMessage('publish should be a boolean value'),
Expand All @@ -51,7 +63,18 @@ export class CredentialController {
}
return false;
})
.withMessage('Entry must be a jwt string or a presentation'),
.withMessage('Entry must be a JWT or a presentation body with JWT proof')
.custom((value) => {
if (typeof value === 'string') {
try {
jwt_decode(value);
} catch (e) {
return false;
}
}
return true;
})
.withMessage('An invalid JWT string'),
check('verifierDid').optional().isString().withMessage('Invalid verifier DID'),
check('policies').optional().isObject().withMessage('Verification policies should be an object'),
query('verifyStatus').optional().isBoolean().withMessage('verifyStatus should be a boolean value'),
Expand Down Expand Up @@ -101,6 +124,20 @@ export class CredentialController {
request.body['@context'] = [request.body['@context']];
}

const resolvedResult = await new IdentityServiceStrategySetup(response.locals.customerId).agent.resolve(
request.body.issuerDid
);
const body = await resolvedResult.json();
if (!body?.didDocument) {
return response.status(resolvedResult.status).send({ body });
}

if (body.didDocumentMetadata.deactivated) {
return response.status(StatusCodes.BAD_REQUEST).send({
error: `${request.body.issuerDid} is deactivated`,
});
}

try {
const credential: VerifiableCredential = await Credentials.instance.issue_credential(
request.body,
Expand Down Expand Up @@ -136,6 +173,12 @@ export class CredentialController {
* schema:
* type: boolean
* default: false
* - in: query
* name: allowDeactivatedDid
* description: If set to `true` allow to verify credential which based on deactivated DID.
* schema:
* type: boolean
* default: false
* requestBody:
* content:
* application/x-www-form-urlencoded:
Expand Down Expand Up @@ -165,7 +208,31 @@ export class CredentialController {
}

const { credential, policies } = request.body;
const verifyStatus = request.query.verifyStatus === 'true' ? true : false;
const verifyStatus = request.query.verifyStatus === 'true';
const allowDeactivatedDid = request.query.allowDeactivatedDid === 'true';

let issuerDid = '';
if (typeof credential === 'object' && credential?.issuer?.id) {
issuerDid = credential.issuer.id;
} else {
const decoded: any = jwt_decode(credential);
issuerDid = decoded.iss;
}

if (!allowDeactivatedDid) {
const result = await new IdentityServiceStrategySetup(response.locals.customerId).agent.resolve(issuerDid);
const body = await result.json();
if (!body?.didDocument) {
return response.status(result.status).send({ body });
}

if (body.didDocumentMetadata.deactivated) {
return response.status(StatusCodes.BAD_REQUEST).send({
error: `${issuerDid} is deactivated`,
});
}
}

try {
const result = await new IdentityServiceStrategySetup(response.locals.customerId).agent.verifyCredential(
credential,
Expand Down Expand Up @@ -394,14 +461,20 @@ export class CredentialController {
* schema:
* type: boolean
* default: false
* - in: query
* name: allowDeactivatedDid
* description: If set to `true` allow to verify credential which based on deactivated DID.
* schema:
* type: boolean
* default: false
* requestBody:
* content:
* application/x-www-form-urlencoded:
* schema:
* $ref: '#/components/schemas/PresentationRequest'
* $ref: '#/components/schemas/PresentationVerifyRequest'
* application/json:
* schema:
* $ref: '#/components/schemas/PresentationRequest'
* $ref: '#/components/schemas/PresentationVerifyRequest'
* responses:
* 200:
* description: The request was successful.
Expand All @@ -423,7 +496,31 @@ export class CredentialController {
}

const { presentation, verifierDid, policies } = request.body;
const verifyStatus = request.query.verifyStatus === 'true' ? true : false;
const verifyStatus = request.query.verifyStatus === 'true';
const allowDeactivatedDid = request.query.allowDeactivatedDid === 'true';

let issuerDid = '';
if (typeof presentation === 'object' && presentation?.issuer?.id) {
issuerDid = presentation.issuer.id;
} else {
const decoded: any = jwt_decode(presentation);
issuerDid = decoded.iss;
}

if (!allowDeactivatedDid) {
const result = await new IdentityServiceStrategySetup(response.locals.customerId).agent.resolve(issuerDid);
const body = await result.json();
if (!body?.didDocument) {
return response.status(result.status).send({ body });
}

if (body.didDocumentMetadata.deactivated) {
return response.status(StatusCodes.BAD_REQUEST).send({
error: `${issuerDid} is deactivated`,
});
}
}

try {
const result = await new IdentityServiceStrategySetup(response.locals.customerId).agent.verifyPresentation(
presentation,
Expand Down
8 changes: 4 additions & 4 deletions src/controllers/issuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,13 +414,13 @@ export class IssuerController {
);

if (!deactivated) {
return response.status(StatusCodes.BAD_REQUEST).json({deactivated: false});
return response.status(StatusCodes.BAD_REQUEST).json({ deactivated: false });
}

const result = await new IdentityServiceStrategySetup(response.locals.customerId).agent.resolveDid(
request.params.did,
response.locals.customerId
)
);

return response.status(StatusCodes.OK).json(result);
} catch (error) {
Expand Down Expand Up @@ -506,8 +506,8 @@ export class IssuerController {
);
if (result) {
const url = new URL(
`${process.env.RESOLVER_URL || DefaultResolverUrl}${did}?` +
`resourceId=${resourcePayload.id}&resourceMetadata=true`,
`${process.env.RESOLVER_URL || DefaultResolverUrl}${did}?` +
`resourceId=${resourcePayload.id}&resourceMetadata=true`
);
const didDereferencing = (await (await fetch(url)).json()) as DIDMetadataDereferencingResult;

Expand Down
12 changes: 10 additions & 2 deletions src/helpers/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import type { DIDDocument } from 'did-resolver';
import { MethodSpecificIdAlgo, CheqdNetwork, TVerificationKey, TVerificationKeyPrefix } from '@cheqd/sdk';
import { VerificationMethods, createVerificationKeys, createDidVerificationMethod, createDidPayload } from '@cheqd/sdk';
import {
MethodSpecificIdAlgo,
CheqdNetwork,
TVerificationKey,
TVerificationKeyPrefix,
VerificationMethods,
createVerificationKeys,
createDidVerificationMethod,
createDidPayload,
} from '@cheqd/sdk';
import { createHmac } from 'node:crypto';
import type { ParsedQs } from 'qs';
import type { SpecValidationResult } from '../types/shared.js';
Expand Down
5 changes: 3 additions & 2 deletions src/middleware/auth/base-auth.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { Request, Response } from 'express';
import InvalidTokenError from "jwt-decode";
import jwt_decode from 'jwt-decode';
import * as dotenv from 'dotenv';
import { StatusCodes } from 'http-status-codes';
import stringify from 'json-stringify-safe';
import { DefaultNetworkPattern } from '../../types/shared.js';
import { MethodToScope, IAuthResourceHandler, Namespaces, IAuthResponse } from '../../types/authentication.js';
import { LogToHelper } from './logto.js';
import InvalidTokenError from "jwt-decode";
import jwt_decode from 'jwt-decode';


dotenv.config();

Expand Down
68 changes: 35 additions & 33 deletions src/static/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@
"type": "boolean",
"default": false
}
},
{
"in": "query",
"name": "allowDeactivatedDid",
"description": "If set to `true` allow to verify credential which based on deactivated DID.",
"schema": {
"type": "boolean",
"default": false
}
}
],
"requestBody": {
Expand Down Expand Up @@ -344,18 +353,27 @@
"type": "boolean",
"default": false
}
},
{
"in": "query",
"name": "allowDeactivatedDid",
"description": "If set to `true` allow to verify credential which based on deactivated DID.",
"schema": {
"type": "boolean",
"default": false
}
}
],
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/PresentationRequest"
"$ref": "#/components/schemas/PresentationVerifyRequest"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/PresentationRequest"
"$ref": "#/components/schemas/PresentationVerifyRequest"
}
}
}
Expand Down Expand Up @@ -1678,34 +1696,26 @@
"properties": {
"credential": {
"description": "Verifiable Credential to be verified as a VC-JWT string or a JSON object.",
"allOf": [
{
"type": "object"
},
{
"type": "string"
}
]
"type": "object"
},
"policies": {
"description": "Custom verification policies to execute when verifying credential.",
"type": "object",
"properties": {
"now": {
"description": "Policy to verify current time during the verification check (provided as Unix/epoch time).",
"type": "number"
},
"issuanceDate": {
"description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.",
"type": "boolean"
"type": "boolean",
"default": true
},
"expirationDate": {
"description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.",
"type": "boolean"
"type": "boolean",
"default": true
},
"audience": {
"description": "Policy to skip the audience check when set to `false`.",
"type": "boolean"
"type": "boolean",
"default": false
}
}
}
Expand Down Expand Up @@ -1742,22 +1752,15 @@
}
}
},
"PresentationRequest": {
"PresentationVerifyRequest": {
"type": "object",
"required": [
"presentation"
],
"properties": {
"presentation": {
"description": "Verifiable Presentation to be verified as a VP-JWT string or a JSON object.",
"allOf": [
{
"type": "string"
},
{
"type": "object"
}
]
"type": "object"
},
"verifiedDid": {
"description": "Provide an optional verifier DID (also known as 'domain' parameter), if the verifier DID in the presentation is not managed in the wallet.",
Expand All @@ -1767,21 +1770,20 @@
"description": "Custom verification policies to execute when verifying presentation.",
"type": "object",
"properties": {
"now": {
"description": "Policy to verify current time during the verification check (provided as Unix/epoch time).",
"type": "number"
},
"issuanceDate": {
"description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.",
"type": "boolean"
"type": "boolean",
"default": true
},
"expirationDate": {
"description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.",
"type": "boolean"
"type": "boolean",
"default": true
},
"audience": {
"description": "Policy to skip the audience check when set to `false`.",
"type": "boolean"
"type": "boolean",
"default": false
}
}
}
Expand Down
Loading

0 comments on commit 023bba1

Please sign in to comment.