Skip to content

Commit

Permalink
fix: Fix namespace lookup [DEV-3150] (#360)
Browse files Browse the repository at this point in the history
* 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.

* Fix namespace lookup

* Additional thing from develop

* Change auth rules due to credential-status endpoint changes

* Update swagger (OpenAPI).

---------

Co-authored-by: abdulla-ashurov <[email protected]>
  • Loading branch information
Andrew Nikitin and abdulla-ashurov authored Aug 31, 2023
1 parent acee992 commit 53cc021
Show file tree
Hide file tree
Showing 13 changed files with 139 additions and 45 deletions.
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"http-status-codes": "^2.2.0",
"json-stringify-safe": "^5.0.1",
"jsonwebtoken": "^9.0.1",
"jwt-decode": "^3.1.2",
"node-cache": "^5.1.2",
"pg": "^8.11.3",
"pg-connection-string": "^2.6.2",
Expand Down
18 changes: 9 additions & 9 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { RevocationController } from './controllers/revocation.js';
import { CORS_ALLOWED_ORIGINS, CORS_ERROR_MSG, configLogToExpress } from './types/constants.js';
import { LogToWebHook } from './middleware/hook.js';
import { Middleware } from './middleware/middleware.js';
import { JSONStringify } from './monkey-patch.js';
// import { JSONStringify } from './monkey-patch.js';

import * as dotenv from 'dotenv';
dotenv.config();
Expand All @@ -28,14 +28,14 @@ dotenv.config();
// TODO: with JSON.sortify = require('json.sortify')
// see: https://github.com/verida/verida-js/blob/c94b95de687c64cc776652602665bb45a327dfb6/packages/encryption-utils/src/index.ts#L10
// eslint-disable-next-line @typescript-eslint/no-unused-vars
JSON.stringify = function (value, _replacer, _space) {
return (
JSONStringify(value) ||
(function () {
throw new Error('JSON.stringify failed');
})()
);
};
// JSON.stringify = function (value, _replacer, _space) {
// return (
// JSONStringify(value) ||
// (function () {
// throw new Error('JSON.stringify failed');
// })()
// );
// };

// Define Swagger file
import swaggerDocument from './static/swagger.json' assert { type: 'json' };
Expand Down
101 changes: 87 additions & 14 deletions src/middleware/auth/base-auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
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 { DefaultDidUrlPattern } from '../../types/shared.js';
import { DefaultNetworkPattern } from '../../types/shared.js';
import { MethodToScope, IAuthResourceHandler, Namespaces, IAuthResponse } from '../../types/authentication.js';
import { LogToHelper } from './logto.js';

Expand Down Expand Up @@ -39,8 +41,15 @@ export abstract class AbstractAuthHandler implements IAuthResourceHandler {
public async commonPermissionCheck(request: Request): Promise<IAuthResponse> {
// Reset all variables
this.reset();
// setting up namespace. It should be testnet or mainnet
this.namespace = AbstractAuthHandler.getNamespaceFromRequest(request);

// Setup the namespace
// Here we just trying to get the network value from the request
// The validation depends on the rule for the request
const namespace = this.getNamespaceFromRequest(request);
if (namespace) {
this.namespace = namespace;
}

// Firstly - try to find the rule for the request
const rule = this.findRule(request.path, request.method, this.getNamespace());

Expand All @@ -55,6 +64,14 @@ export abstract class AbstractAuthHandler implements IAuthResourceHandler {
`Internal error: Issue with finding the rule for the path ${request.path}`
);
} else {
// Namespace should be testnet or mainnet or '' if isSkipNamespace is true
// Otherwise - raise an error.
if (!this.namespace && !rule?.isSkipNamespace()) {
return this.returnError(
StatusCodes.INTERNAL_SERVER_ERROR,
'Seems like there is no information about the network in the request.'
);
}
// If the user is not authenticated - return error
if (!request.user.isAuthenticated) {
return this.returnError(
Expand All @@ -81,7 +98,7 @@ export abstract class AbstractAuthHandler implements IAuthResourceHandler {
}
// Checks if the list of scopes from user enough to make an action
if (!this.areValidScopes(rule, this.getScopes())) {
this.returnError(
return this.returnError(
StatusCodes.FORBIDDEN,
`Unauthorized error: Current LogTo account does not have the required scopes. You need ${this.getScopeForRoute(
request.path,
Expand Down Expand Up @@ -146,21 +163,77 @@ export abstract class AbstractAuthHandler implements IAuthResourceHandler {
this.logToHelper = logToHelper;
}

private static doesMatchMainnet(matches: string[] | null): boolean {
private findNetworkInBody(body: string): string | null {
const matches = body.match(DefaultNetworkPattern);
if (matches && matches.length > 0) {
return Namespaces.Mainnet === matches[0];
return matches[1];
}
return false;
return null;
}

public static getNamespaceFromRequest(req: Request): Namespaces {
if (AbstractAuthHandler.doesMatchMainnet(stringify(req.body).match(DefaultDidUrlPattern))) {
return Namespaces.Mainnet;
private switchNetwork(network: string): Namespaces | null {
switch (network) {
case 'testnet': {
return Namespaces.Testnet;
}
case 'mainnet': {
return Namespaces.Mainnet;
}
default: {
return null;
}
}
if (AbstractAuthHandler.doesMatchMainnet(req.path.match(DefaultDidUrlPattern))) {
return Namespaces.Mainnet;
}

public getNamespaceFromRequest(req: Request): Namespaces | null {
let network: string | null = '';

if (req && req.body && req.body.credential) {
const { credential } = req.body;
let decoded = '';
let issuerDid = "";
// Try to get issuer DID
if (credential && credential.issuer) {
issuerDid = credential.issuer.id;
}
network = this.findNetworkInBody(issuerDid);
if (network) {
return this.switchNetwork(network);
}
try {
decoded = jwt_decode(req.body.credential);
} catch (e) {
// If it's not a JWT - just skip it
if (!(e instanceof InvalidTokenError)) {
throw e;
}
}
// if not - try to search for decoded credential
network = this.findNetworkInBody(stringify(decoded));
if (network) {
return this.switchNetwork(network);
}
}
return Namespaces.Testnet;
// Try to search in request body
if (req && req.body) {
network = this.findNetworkInBody(stringify(req.body));
if (network) {
return this.switchNetwork(network);
}
}
// Try to search in request path
if (req && req.path) {
network = this.findNetworkInBody(decodeURIComponent(req.path));
if (network) {
return this.switchNetwork(network);
}
}
// For DID create we specify it as a separate parameter in body
if (req.body && req.body.network ) {
return this.switchNetwork(req.body.network);
}

return null;
}

// Getters
Expand Down Expand Up @@ -205,7 +278,7 @@ export abstract class AbstractAuthHandler implements IAuthResourceHandler {

public findRule(route: string, method: string, namespace = Namespaces.Testnet): MethodToScope | null {
for (const rule of this.routeToScoupe) {
if (rule.isRuleMatches(route, method, namespace)) {
if (rule.doesRuleMatches(route, method, namespace)) {
return rule;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/middleware/auth/credential-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class CredentialAuthHandler extends AbstractAuthHandler {
this.registerRoute('/credential/suspend', 'POST', 'suspend:credential:mainnet');
this.registerRoute('/credential/reinstate', 'POST', 'reinstate:credential:testnet');
this.registerRoute('/credential/reinstate', 'POST', 'reinstate:credential:mainnet');
// true means allowUnauthorized
// Unauthorized routes
this.registerRoute('/credential/verify', 'POST', '', { allowUnauthorized: true, skipNamespace: true });
}

Expand Down
14 changes: 9 additions & 5 deletions src/middleware/auth/credential-status-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import type { IAuthResponse } from '../../types/authentication.js';
export class CredentialStatusAuthHandler extends AbstractAuthHandler {
constructor() {
super();
this.registerRoute('/credential-status/create', 'POST', 'create:credential-status:testnet');
this.registerRoute('/credential-status/create', 'POST', 'create:credential-status:mainnet');
this.registerRoute('/credential-status/create/encrypted', 'POST', 'create-encrypted:credential-status:testnet');
this.registerRoute('/credential-status/create/encrypted', 'POST', 'create-encrypted:credential-status:mainnet');
this.registerRoute('/credential-status/create/unencrypted', 'POST', 'create-unencrypted:credential-status:testnet');
this.registerRoute('/credential-status/create/unencrypted', 'POST', 'create-unencrypted:credential-status:mainnet');
this.registerRoute('/credential-status/publish', 'POST', 'publish:credential-status:testnet');
this.registerRoute('/credential-status/publish', 'POST', 'publish:credential-status:mainnet');
this.registerRoute('/credential-status/update', 'POST', 'update:credential-status:testnet');
this.registerRoute('/credential-status/update', 'POST', 'update:credential-status:mainnet');
// true means allowUnauthorized
this.registerRoute('/credential-status/update/encrypted', 'POST', 'update-encrypted:credential-status:testnet');
this.registerRoute('/credential-status/update/encrypted', 'POST', 'update-encrypted:credential-status:mainnet');
this.registerRoute('/credential-status/update/unencrypted', 'POST', 'update-unencrypted:credential-status:testnet');
this.registerRoute('/credential-status/update/unencrypted', 'POST', 'update-unencrypted:credential-status:mainnet');
// Unauthorized routes
this.registerRoute('/credential-status/search', 'GET', '', { allowUnauthorized: true, skipNamespace: true });
this.registerRoute('/credential-status/check', 'POST', '', { allowUnauthorized: true, skipNamespace: true });
}
Expand Down
6 changes: 3 additions & 3 deletions src/middleware/auth/did-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ export class DidAuthHandler extends AbstractAuthHandler {
super();
this.registerRoute('/did/create', 'POST', 'create:did:testnet');
this.registerRoute('/did/create', 'POST', 'create:did:mainnet');
this.registerRoute('/did/list', 'GET', 'list:did:testnet');
this.registerRoute('/did/list', 'GET', 'list:did:mainnet');
this.registerRoute('/did/list', 'GET', 'list:did:testnet', {skipNamespace: true});
this.registerRoute('/did/list', 'GET', 'list:did:mainnet', {skipNamespace: true});
this.registerRoute('/did/update', 'POST', 'update:did:testnet');
this.registerRoute('/did/update', 'POST', 'update:did:mainnet');
this.registerRoute('/did/deactivate', 'POST', 'deactivate:did:testnet');
this.registerRoute('/did/deactivate', 'POST', 'deactivate:did:mainnet');
// true means allowUnauthorized
// Unauthorized routes
this.registerRoute('/did/search/(.*)', 'GET', '', { allowUnauthorized: true, skipNamespace: true });
}

Expand Down
2 changes: 1 addition & 1 deletion src/middleware/auth/presentation-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { IAuthResponse } from '../../types/authentication.js';
export class PresentationAuthHandler extends AbstractAuthHandler {
constructor() {
super();
// true means allowUnauthorized
// Unauthorized routes
this.registerRoute('/presentation/verify', 'POST', '', { allowUnauthorized: true, skipNamespace: true });
}
public async handle(request: Request, response: Response): Promise<IAuthResponse> {
Expand Down
2 changes: 1 addition & 1 deletion src/middleware/auth/resource-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class ResourceAuthHandler extends AbstractAuthHandler {
super();
this.registerRoute('/resource/create', 'POST', 'create:resource:testnet');
this.registerRoute('/resource/create', 'POST', 'create:resource:mainnet');
// true means allowUnauthorized
// Unauthorized routes
this.registerRoute('/resource/search/(.*)', 'GET', '', { allowUnauthorized: true, skipNamespace: true });
}
public async handle(request: Request, response: Response): Promise<IAuthResponse> {
Expand Down
3 changes: 3 additions & 0 deletions src/services/identity/postgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ export class PostgresIdentityService extends DefaultIdentityService {
throw new Error('Customer not found');
}
const customer = (await CustomerService.instance.get(agentId)) as CustomerEntity;
if (!customer) {
throw new Error('Customer not found');
}
const dbConnection = Connection.instance.dbConnection;

const privateKey = (await this.getPrivateKey(customer.account))?.privateKeyHex;
Expand Down
2 changes: 1 addition & 1 deletion src/static/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -1910,7 +1910,7 @@
"example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg"
},
"feePaymentAmount": {
"description": "Amount in CHEQ tokens to unlocked the encrypted StatusList2021 DID-Linked Resource.",
"description": "Amount in CHEQ tokens to unlock the encrypted StatusList2021 DID-Linked Resource.",
"type": "number",
"minimum": 0,
"exclusiveMinimum": true,
Expand Down
25 changes: 15 additions & 10 deletions src/types/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,30 @@ export class MethodToScope {
return this.scope === scope;
}

public isRuleMatches(route: string, method: string, namespace = Namespaces.Testnet): boolean {
// If route is exactly the same - check method and scope
public doesRuleMatches(route: string, method: string, namespace = Namespaces.Testnet): boolean {
// If route and method are exactly the same - check scope
if (this.route === route && this.method === method) {
if (this.scope === '' || this.isSkipNamespace()) {
return true;
}
return this.scope.includes(namespace);
return this.checkScope(namespace);
}
// If route is not exactly the same - check if it matches as an regexp
const matches = route.match(this.route);
if (matches && matches.length > 0 && this.method === method) {
if (this.scope === '' || this.isSkipNamespace()) {
return true;
}
return this.scope.includes(namespace);
return this.checkScope(namespace);
}
return false;
}

private checkScope(namespace: string): boolean {
if (this.scope === '' || this.isSkipNamespace()) {
return true;
}
// If namespace is required and it's not provided - return false
if (!namespace) {
return false;
}
return this.scope.includes(namespace);
}

public getScope(): string {
return this.scope;
}
Expand Down
2 changes: 2 additions & 0 deletions src/types/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const DefaultNamespacePattern = `(${CheqdNetwork.Mainnet}|${CheqdNetwork.Testnet
export const DefaultDidUrlPattern = new RegExp(
`^did:cheqd:${DefaultNamespacePattern}:${DefaultMethodSpecificIdPattern}$`
);
export const DefaultNetworkPattern = new RegExp(
`did:cheqd:${DefaultNamespacePattern}:.*`);

export const DefaultStatusActions = {
revoke: 'revoke',
Expand Down

0 comments on commit 53cc021

Please sign in to comment.