From d3f870592e0968315bcb3ba1202e0ee3b1be6054 Mon Sep 17 00:00:00 2001 From: DaevMithran Date: Thu, 4 Jan 2024 17:40:24 +0530 Subject: [PATCH 1/8] feat: Add account create api --- src/app.ts | 1 + src/controllers/account.ts | 30 +++++++++++ src/middleware/auth/routes/account-auth.ts | 1 + src/static/swagger.json | 60 ++++++++++++++++++++++ src/types/swagger-types.ts | 10 ++++ 5 files changed, 102 insertions(+) diff --git a/src/app.ts b/src/app.ts index e2a3b90e..1bf1f104 100644 --- a/src/app.ts +++ b/src/app.ts @@ -184,6 +184,7 @@ class App { ); // Account API + app.post('/account/create', new AccountController().bootstrap); app.get('/account', new AccountController().get); app.get('/account/idtoken', new AccountController().getIdToken); diff --git a/src/controllers/account.ts b/src/controllers/account.ts index 1b0e8b95..8ba32c96 100644 --- a/src/controllers/account.ts +++ b/src/controllers/account.ts @@ -132,6 +132,36 @@ export class AccountController { return response.status(StatusCodes.BAD_REQUEST).json({}); } + /** + * @openapi + * + * /account/create: + * post: + * tags: [Account] + * summary: Create an client for an authenticated user. + * description: This endpoint creates a client in the custodian-mode for an authenticated user + * requestBody: + * content: + * application/x-www-form-urlencoded: + * schema: + * $ref: '#/components/schemas/AccountCreateRequest' + * application/json: + * schema: + * $ref: '#/components/schemas/AccountCreateRequest' + * responses: + * 200: + * description: The request was successful. + * content: + * application/json: + * idToken: + * type: string + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + */ public async bootstrap(request: Request, response: Response) { // For now we keep temporary 1-1 relation between user and customer // So the flow is: diff --git a/src/middleware/auth/routes/account-auth.ts b/src/middleware/auth/routes/account-auth.ts index d7ca8ffb..830f32d2 100644 --- a/src/middleware/auth/routes/account-auth.ts +++ b/src/middleware/auth/routes/account-auth.ts @@ -6,6 +6,7 @@ export class AccountAuthHandler extends BaseAuthHandler { constructor() { super(); this.registerRoute('/account', 'GET', 'read:account', { skipNamespace: true }); + this.registerRoute('/account', 'POST', 'create:account', { skipNamespace: true }); } public async handle(request: Request, response: Response): Promise { if (!request.path.includes('/account')) { diff --git a/src/static/swagger.json b/src/static/swagger.json index 08ac3717..327da351 100644 --- a/src/static/swagger.json +++ b/src/static/swagger.json @@ -102,6 +102,50 @@ } } }, + "/account/create": { + "post": { + "tags": [ + "Account" + ], + "summary": "Create an client for an authenticated user.", + "description": "This endpoint creates a client in the custodian-mode for an authenticated user", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/AccountCreateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "idToken": { + "type": "string" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, "/credential-status/create/unencrypted": { "post": { "tags": [ @@ -3305,6 +3349,22 @@ } } }, + "AccountCreateRequest": { + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "primaryEmail": { + "type": "string" + } + } + } + } + }, "InvalidRequest": { "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", "type": "object", diff --git a/src/types/swagger-types.ts b/src/types/swagger-types.ts index 801ddbf9..2e81aec2 100644 --- a/src/types/swagger-types.ts +++ b/src/types/swagger-types.ts @@ -1227,6 +1227,16 @@ * address: * type: string * example: cheqd1wgsvqwlkmdp60f4dek26ak0sjw6au3ytd3pz7f + * AccountCreateRequest: + * type: object + * properties: + * user: + * type: object + * properties: + * id: + * type: string + * primaryEmail: + * type: string * InvalidRequest: * description: A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body. * type: object From 048c55b332d29f44baa5896e6920ba2b1c085511 Mon Sep 17 00:00:00 2001 From: DaevMithran Date: Mon, 8 Jan 2024 18:57:40 +0530 Subject: [PATCH 2/8] feat: Support m2m token in auth --- src/middleware/auth/base-auth-handler.ts | 7 ++- .../auth/user-info-fetcher/m2m-token.ts | 50 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 src/middleware/auth/user-info-fetcher/m2m-token.ts diff --git a/src/middleware/auth/base-auth-handler.ts b/src/middleware/auth/base-auth-handler.ts index 21575616..0b495462 100644 --- a/src/middleware/auth/base-auth-handler.ts +++ b/src/middleware/auth/base-auth-handler.ts @@ -8,6 +8,7 @@ import { APITokenUserInfoFetcher } from './user-info-fetcher/api-token.js'; import type { IUserInfoFetcher } from './user-info-fetcher/base.js'; import { IAuthHandler, RuleRoutine, IAPIGuard } from './routine.js'; import type { IAuthResponse, MethodToScopeRule } from '../../types/authentication.js'; +import { M2MTokenUserInfoFetcher } from './user-info-fetcher/m2m-token.js'; export class BaseAPIGuard extends RuleRoutine implements IAPIGuard { userInfoFetcher: IUserInfoFetcher = {} as IUserInfoFetcher; @@ -127,7 +128,11 @@ export class BaseAuthHandler extends BaseAPIGuard implements IAuthHandler { private chooseUserFetcherStrategy(request: Request): void { const token = this.extractBearerTokenFromHeaders(request.headers) as string; - if (token) { + const userId = request.headers.userId; + if (token && userId && typeof userId === 'string') { + this.setUserInfoStrategy(new M2MTokenUserInfoFetcher(token)); + this.setUserId(userId); + } else if (token) { this.setUserInfoStrategy(new APITokenUserInfoFetcher(token)); } else { this.setUserInfoStrategy(new SwaggerUserInfoFetcher()); diff --git a/src/middleware/auth/user-info-fetcher/m2m-token.ts b/src/middleware/auth/user-info-fetcher/m2m-token.ts new file mode 100644 index 00000000..a46aaf4b --- /dev/null +++ b/src/middleware/auth/user-info-fetcher/m2m-token.ts @@ -0,0 +1,50 @@ +import type { Request } from 'express'; +import { AuthReturn } from '../routine.js'; +import type { IAuthResponse } from '../../../types/authentication.js'; +import { StatusCodes } from 'http-status-codes'; +import type { IUserInfoFetcher } from './base.js'; +import type { IOAuthProvider } from '../oauth/base.js'; +import { createRemoteJWKSet, jwtVerify } from 'jose'; + +import * as dotenv from 'dotenv'; +dotenv.config(); + +export class M2MTokenUserInfoFetcher extends AuthReturn implements IUserInfoFetcher { + token: string; + + constructor(token: string) { + super(); + this.token = token; + } + + async fetchUserInfo(request: Request, oauthProvider: IOAuthProvider): Promise { + return this.verifyJWTToken(this.token as string, oauthProvider); + } + + public async verifyJWTToken(token: string, oauthProvider: IOAuthProvider): Promise { + try { + const { payload } = await jwtVerify( + token, // The raw Bearer Token extracted from the request header + createRemoteJWKSet(new URL(oauthProvider.endpoint_jwks)), // generate a jwks using jwks_uri inquired from Logto server + { + // expected issuer of the token, should be issued by the Logto server + issuer: oauthProvider.endpoint_issuer, + // expected audience token, should be the resource indicator of the current API + audience: process.env.M2M_LOGTO_APP_ID, + } + ); + // Setup the scopes from the token + if (!payload.roles) { + return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No roles found in the token.`); + } + const scopes = await oauthProvider.getScopesForRoles(payload.roles as string[]); + if (!scopes) { + return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No scopes found for the roles.`); + } + this.setScopes(scopes); + return this.returnOk(); + } catch (error) { + return this.returnError(StatusCodes.INTERNAL_SERVER_ERROR, `Unexpected error: ${error}`); + } + } +} From 3c9707d5ff56393dcf53ef9b2ab572d5e7300958 Mon Sep 17 00:00:00 2001 From: DaevMithran Date: Tue, 9 Jan 2024 18:35:54 +0530 Subject: [PATCH 3/8] feat: Add getAppScopes in auth --- src/middleware/auth/base-auth-handler.ts | 9 +++--- src/middleware/auth/logto-helper.ts | 28 +++++++++++++++++++ src/middleware/auth/oauth/base.ts | 4 +++ src/middleware/auth/oauth/logto-provider.ts | 4 +++ .../auth/user-info-fetcher/m2m-token.ts | 20 ++++++------- 5 files changed, 50 insertions(+), 15 deletions(-) diff --git a/src/middleware/auth/base-auth-handler.ts b/src/middleware/auth/base-auth-handler.ts index 0b495462..626f1f03 100644 --- a/src/middleware/auth/base-auth-handler.ts +++ b/src/middleware/auth/base-auth-handler.ts @@ -109,7 +109,7 @@ export class BaseAuthHandler extends BaseAPIGuard implements IAuthHandler { private nextHandler: IAuthHandler; oauthProvider: IOAuthProvider; private bearerTokenIdentifier = 'Bearer'; - private pathSkip = ['/swagger', '/static', '/logto', '/account/bootstrap']; + private pathSkip = ['/swagger', '/static', '/logto', '/account/bootstrap', '/account/create']; constructor() { super(); @@ -128,10 +128,11 @@ export class BaseAuthHandler extends BaseAPIGuard implements IAuthHandler { private chooseUserFetcherStrategy(request: Request): void { const token = this.extractBearerTokenFromHeaders(request.headers) as string; - const userId = request.headers.userId; - if (token && userId && typeof userId === 'string') { + const { tenantId } = request.body; + console.log('REQUEST BODY', request.body); + if (token) { this.setUserInfoStrategy(new M2MTokenUserInfoFetcher(token)); - this.setUserId(userId); + this.setUserId(tenantId); } else if (token) { this.setUserInfoStrategy(new APITokenUserInfoFetcher(token)); } else { diff --git a/src/middleware/auth/logto-helper.ts b/src/middleware/auth/logto-helper.ts index 2f406529..af13f552 100644 --- a/src/middleware/auth/logto-helper.ts +++ b/src/middleware/auth/logto-helper.ts @@ -114,6 +114,23 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { } return this.returnOk(scopes); } + + public async getAppScopes(appId: string): Promise { + const scopes = [] as string[]; + const roles = await this.getRolesForApp(appId); + if (roles.status !== StatusCodes.OK) { + return this.returnError(StatusCodes.BAD_GATEWAY, roles.error); + } + // Check that default role is set + for (const role of roles.data) { + const _s = await this.getScopesForRole(role.id); + if (_s.status === StatusCodes.OK) { + scopes.push(..._s.data); + } + } + return this.returnOk(scopes); + } + private async setDefaultScopes(): Promise { const _r = await this.getAllResources(); if (_r.status !== StatusCodes.OK) { @@ -224,6 +241,17 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { return this.returnError(StatusCodes.BAD_GATEWAY, `getRolesForUser ${err}`); } } + + public async getRolesForApp(appId: string): Promise { + const uri = new URL(`/api/applications/${appId}/roles`, process.env.LOGTO_ENDPOINT); + try { + // Note: By default, the API returns first 20 roles. + // If our roles per user grows to more than 20, we need to implement pagination + return await this.getToLogto(uri, 'GET'); + } catch (err) { + return this.returnError(StatusCodes.BAD_GATEWAY, `getRolesForUser ${err}`); + } + } private async getRoleInfo(roleId: string): Promise { const uri = new URL(`/api/roles/${roleId}`, process.env.LOGTO_ENDPOINT); try { diff --git a/src/middleware/auth/oauth/base.ts b/src/middleware/auth/oauth/base.ts index 88184109..5b637656 100644 --- a/src/middleware/auth/oauth/base.ts +++ b/src/middleware/auth/oauth/base.ts @@ -9,6 +9,7 @@ export interface IOAuthProvider { getDefaultScopes(): string[] | void; getAllResourcesWithNames(): string[] | void; getUserScopes(userId: string): Promise; + getAppScopes(appId: string): Promise; getScopesForRoles(rolesList: string[]): Promise; } @@ -28,6 +29,9 @@ export abstract class OAuthProvider implements IOAuthProvider { getUserScopes(userId: string): Promise { throw new Error('Method not implemented.'); } + getAppScopes(appId: string): Promise { + throw new Error('Method not implemented.'); + } getScopesForRoles(rolesList: string[]): Promise { throw new Error('Method not implemented.'); } diff --git a/src/middleware/auth/oauth/logto-provider.ts b/src/middleware/auth/oauth/logto-provider.ts index 6b920605..8db5e812 100644 --- a/src/middleware/auth/oauth/logto-provider.ts +++ b/src/middleware/auth/oauth/logto-provider.ts @@ -36,6 +36,10 @@ export class LogToProvider extends OAuthProvider implements IOAuthProvider { return await this.logToHelper.getUserScopes(userId); } + public async getAppScopes(appId: string): Promise { + return await this.logToHelper.getAppScopes(appId); + } + public async getScopesForRoles(rolesList: string[]): Promise { if (this.logToHelper) { const scopes = await this.logToHelper.getScopesForRolesList(rolesList); diff --git a/src/middleware/auth/user-info-fetcher/m2m-token.ts b/src/middleware/auth/user-info-fetcher/m2m-token.ts index a46aaf4b..07179229 100644 --- a/src/middleware/auth/user-info-fetcher/m2m-token.ts +++ b/src/middleware/auth/user-info-fetcher/m2m-token.ts @@ -23,27 +23,25 @@ export class M2MTokenUserInfoFetcher extends AuthReturn implements IUserInfoFetc public async verifyJWTToken(token: string, oauthProvider: IOAuthProvider): Promise { try { + console.log(token); const { payload } = await jwtVerify( token, // The raw Bearer Token extracted from the request header - createRemoteJWKSet(new URL(oauthProvider.endpoint_jwks)), // generate a jwks using jwks_uri inquired from Logto server - { - // expected issuer of the token, should be issued by the Logto server - issuer: oauthProvider.endpoint_issuer, - // expected audience token, should be the resource indicator of the current API - audience: process.env.M2M_LOGTO_APP_ID, - } + createRemoteJWKSet(new URL(oauthProvider.endpoint_jwks)) // generate a jwks using jwks_uri inquired from Logto server ); + // console.log(payload); // Setup the scopes from the token - if (!payload.roles) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No roles found in the token.`); + if (!payload.sub) { + return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No sub found in the token.`); } - const scopes = await oauthProvider.getScopesForRoles(payload.roles as string[]); - if (!scopes) { + const { error, data: scopes } = await oauthProvider.getAppScopes(payload.sub); + // console.log('Scopes', scopes); + if (error) { return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No scopes found for the roles.`); } this.setScopes(scopes); return this.returnOk(); } catch (error) { + console.error(error); return this.returnError(StatusCodes.INTERNAL_SERVER_ERROR, `Unexpected error: ${error}`); } } From c8ef96f33ec052ecf0593a2e8c84e96b409b7906 Mon Sep 17 00:00:00 2001 From: DaevMithran Date: Wed, 10 Jan 2024 15:37:58 +0530 Subject: [PATCH 4/8] Update account/create api --- src/app.ts | 2 +- src/controllers/account.ts | 214 +++++++++++++++--- src/middleware/auth/base-auth-handler.ts | 19 +- src/middleware/auth/logto-helper.ts | 30 +++ .../auth/user-info-fetcher/m2m-token.ts | 4 +- 5 files changed, 223 insertions(+), 46 deletions(-) diff --git a/src/app.ts b/src/app.ts index 5406a2cf..75f7dd11 100644 --- a/src/app.ts +++ b/src/app.ts @@ -185,7 +185,7 @@ class App { ); // Account API - app.post('/account/create', new AccountController().bootstrap); + app.post('/account/create', new AccountController().create); app.get('/account', new AccountController().get); app.get('/account/idtoken', new AccountController().getIdToken); diff --git a/src/controllers/account.ts b/src/controllers/account.ts index 8ba32c96..7c57b16b 100644 --- a/src/controllers/account.ts +++ b/src/controllers/account.ts @@ -13,6 +13,8 @@ import type { CustomerEntity } from '../database/entities/customer.entity.js'; import type { UserEntity } from '../database/entities/user.entity.js'; import type { PaymentAccountEntity } from '../database/entities/payment.account.entity.js'; import { IdentityServiceStrategySetup } from '../services/identity/index.js'; +import { decodeJwt } from 'jose'; +import { BaseAuthHandler } from '../middleware/auth/base-auth-handler.js'; export class AccountController { /** @@ -132,36 +134,6 @@ export class AccountController { return response.status(StatusCodes.BAD_REQUEST).json({}); } - /** - * @openapi - * - * /account/create: - * post: - * tags: [Account] - * summary: Create an client for an authenticated user. - * description: This endpoint creates a client in the custodian-mode for an authenticated user - * requestBody: - * content: - * application/x-www-form-urlencoded: - * schema: - * $ref: '#/components/schemas/AccountCreateRequest' - * application/json: - * schema: - * $ref: '#/components/schemas/AccountCreateRequest' - * responses: - * 200: - * description: The request was successful. - * content: - * application/json: - * idToken: - * type: string - * 400: - * $ref: '#/components/schemas/InvalidRequest' - * 401: - * $ref: '#/components/schemas/UnauthorizedError' - * 500: - * $ref: '#/components/schemas/InternalError' - */ public async bootstrap(request: Request, response: Response) { // For now we keep temporary 1-1 relation between user and customer // So the flow is: @@ -195,14 +167,12 @@ export class AccountController { } const logToUserId = request.body.user.id; const logToUserEmail = request.body.user.primaryEmail; - const defaultRole = await RoleService.instance.getDefaultRole(); if (!defaultRole) { return response.status(StatusCodes.BAD_REQUEST).json({ error: 'Default role is not set on Credential Service side', }); } - // 2. Check if such row exists in the DB user = await UserService.instance.get(logToUserId); if (!user) { @@ -243,7 +213,7 @@ export class AccountController { customer = user.customer; } - // 4. Check is paymentAccount exists for the customer + // 4. Check is paymentAccount exists for the customer\ const accounts = await PaymentAccountService.instance.find({ customer }); if (accounts.length === 0) { const key = await new IdentityServiceStrategySetup(customer.customerId).agent.createKey( @@ -333,4 +303,182 @@ export class AccountController { } return response.status(StatusCodes.OK).json({}); } + + /** + * @openapi + * + * /account/create: + * post: + * tags: [Account] + * summary: Create an client for an authenticated user. + * description: This endpoint creates a client in the custodian-mode for an authenticated user + * requestBody: + * content: + * application/x-www-form-urlencoded: + * schema: + * $ref: '#/components/schemas/AccountCreateRequest' + * application/json: + * schema: + * $ref: '#/components/schemas/AccountCreateRequest' + * responses: + * 200: + * description: The request was successful. + * content: + * application/json: + * idToken: + * type: string + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + */ + public async create(request: Request, response: Response) { + // For now we keep temporary 1-1 relation between user and customer + // So the flow is: + // 1. Get LogTo user id from request body + // 2. Check if such row exists in the DB + // 2.1. If no - create it + // 3. If yes - check that there is customer associated with such user + // 3.1. If no: + // 3.1.1. Create customer + // 3.1.2. Assign customer to the user + + // 4. Check is paymentAccount exists for the customer + // 4.1. If no - create it + + // 5.1 Get app's roles + // 5.2 If list of roles is empty and the user is not suspended - assign default role + // 7. Check the token balance for Testnet account + + let customer: CustomerEntity | null; + let user: UserEntity | null; + let paymentAccount: PaymentAccountEntity | null; + + // 1. Get logTo UserId from request body + if (!request.body.user || !request.body.user.primaryEmail) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: 'User id is not specified or primaryEmail is not set', + }); + } + const token = BaseAuthHandler.extractBearerTokenFromHeaders(request.headers) as string; + const payload = decodeJwt(token); + const logToUserId = payload.sub as string; + const logToUserEmail = request.body.user.primaryEmail; + const defaultRole = await RoleService.instance.getDefaultRole(); + if (!defaultRole) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: 'Default role is not set on Credential Service side', + }); + } + // 2. Check if such row exists in the DB + user = await UserService.instance.get(logToUserId); + if (!user) { + // 2.1. If no - create customer first + // Cause for now we assume only 1-1 connection between user and customer + // We think here that if no user row - no customer also, cause customer should be created before user + // Even if customer was created before for such user but the process was interruted somehow - we need to create it again + // Cause we don't know the state of the customer in this case + // 2.1.1. Create customer + customer = (await CustomerService.instance.create(logToUserEmail)) as CustomerEntity; + if (!customer) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: 'User is not found in db: Customer was not created', + }); + } + // 2.2. Create user + user = await UserService.instance.create(logToUserId, customer, defaultRole); + if (!user) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: 'User is not found in db: User was not created', + }); + } + } + // 3. If yes - check that there is customer associated with such user + if (!user.customer) { + // 3.1. If no: + // 3.1.1. Create customer + customer = (await CustomerService.instance.create(logToUserEmail)) as CustomerEntity; + if (!customer) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: 'User exists in db: Customer was not created', + }); + } + // 3.1.2. Assign customer to the user + user.customer = customer; + await UserService.instance.update(user.logToId, customer); + } else { + customer = user.customer; + } + + // 4. Check is paymentAccount exists for the customer\ + const accounts = await PaymentAccountService.instance.find({ customer }); + if (accounts.length === 0) { + const key = await new IdentityServiceStrategySetup(customer.customerId).agent.createKey( + 'Secp256k1', + customer + ); + if (!key) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: 'PaymentAccount is not found in db: Key was not created', + }); + } + paymentAccount = (await PaymentAccountService.instance.create( + CheqdNetwork.Testnet, + true, + customer, + key + )) as PaymentAccountEntity; + if (!paymentAccount) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: 'PaymentAccount is not found in db: Payment account was not created', + }); + } + } else { + paymentAccount = accounts[0]; + } + + const logToHelper = new LogToHelper(); + const _r = await logToHelper.setup(); + if (_r.status !== StatusCodes.OK) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: _r.error, + }); + } + // 5. Assign default role on LogTo + // 5.1 Get user's roles + const roles = await logToHelper.getRolesForApp(user.logToId); + if (roles.status !== StatusCodes.OK) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: roles.error, + }); + } + + // 5.2 If list of roles is empty and the user is not suspended - assign default role + if (roles.data.length === 0) { + const _r = await logToHelper.setDefaultRoleForApp(user.logToId); + if (_r.status !== StatusCodes.OK) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: _r.error, + }); + } + } + + // 7. Check the token balance for Testnet account + if (paymentAccount.address && process.env.ENABLE_ACCOUNT_TOPUP === 'true') { + const balances = await checkBalance(paymentAccount.address, process.env.TESTNET_RPC_URL); + const balance = balances[0]; + if (!balance || +balance.amount < TESTNET_MINIMUM_BALANCE * Math.pow(10, DEFAULT_DENOM_EXPONENT)) { + // 3.1 If it's less then required for DID creation - assign new portion from testnet-faucet + const resp = await FaucetHelper.delegateTokens(paymentAccount.address); + if (resp.status !== StatusCodes.OK) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: resp.error, + }); + } + } + } + return response.status(StatusCodes.OK).json(customer); + } } diff --git a/src/middleware/auth/base-auth-handler.ts b/src/middleware/auth/base-auth-handler.ts index 626f1f03..bea6e478 100644 --- a/src/middleware/auth/base-auth-handler.ts +++ b/src/middleware/auth/base-auth-handler.ts @@ -9,6 +9,7 @@ import type { IUserInfoFetcher } from './user-info-fetcher/base.js'; import { IAuthHandler, RuleRoutine, IAPIGuard } from './routine.js'; import type { IAuthResponse, MethodToScopeRule } from '../../types/authentication.js'; import { M2MTokenUserInfoFetcher } from './user-info-fetcher/m2m-token.js'; +import { decodeJwt } from 'jose'; export class BaseAPIGuard extends RuleRoutine implements IAPIGuard { userInfoFetcher: IUserInfoFetcher = {} as IUserInfoFetcher; @@ -108,7 +109,7 @@ export class BaseAPIGuard extends RuleRoutine implements IAPIGuard { export class BaseAuthHandler extends BaseAPIGuard implements IAuthHandler { private nextHandler: IAuthHandler; oauthProvider: IOAuthProvider; - private bearerTokenIdentifier = 'Bearer'; + private static bearerTokenIdentifier = 'Bearer'; private pathSkip = ['/swagger', '/static', '/logto', '/account/bootstrap', '/account/create']; constructor() { @@ -119,7 +120,7 @@ export class BaseAuthHandler extends BaseAPIGuard implements IAuthHandler { this.nextHandler = {} as IAuthHandler; } - public extractBearerTokenFromHeaders({ authorization }: IncomingHttpHeaders): string | unknown { + public static extractBearerTokenFromHeaders({ authorization }: IncomingHttpHeaders): string | unknown { if (authorization && authorization.startsWith(this.bearerTokenIdentifier)) { return authorization.slice(this.bearerTokenIdentifier.length + 1); } @@ -127,14 +128,14 @@ export class BaseAuthHandler extends BaseAPIGuard implements IAuthHandler { } private chooseUserFetcherStrategy(request: Request): void { - const token = this.extractBearerTokenFromHeaders(request.headers) as string; - const { tenantId } = request.body; - console.log('REQUEST BODY', request.body); + const token = BaseAuthHandler.extractBearerTokenFromHeaders(request.headers) as string; if (token) { - this.setUserInfoStrategy(new M2MTokenUserInfoFetcher(token)); - this.setUserId(tenantId); - } else if (token) { - this.setUserInfoStrategy(new APITokenUserInfoFetcher(token)); + const payload = decodeJwt(token); + if (payload.aud === process.env.LOGTO_APP_ID) { + this.setUserInfoStrategy(new APITokenUserInfoFetcher(token)); + } else { + this.setUserInfoStrategy(new M2MTokenUserInfoFetcher(token)); + } } else { this.setUserInfoStrategy(new SwaggerUserInfoFetcher()); } diff --git a/src/middleware/auth/logto-helper.ts b/src/middleware/auth/logto-helper.ts index af13f552..87bd48d5 100644 --- a/src/middleware/auth/logto-helper.ts +++ b/src/middleware/auth/logto-helper.ts @@ -82,6 +82,22 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { return await this.assignDefaultRoleForUser(userId, process.env.LOGTO_DEFAULT_ROLE_ID); } + public async setDefaultRoleForApp(appId: string): Promise { + const roles = await this.getRolesForUser(appId); + if (roles.status !== StatusCodes.OK) { + return this.returnError(StatusCodes.BAD_GATEWAY, roles.error); + } + // Check that default role is set + for (const role of roles.data) { + if (role.id === process.env.LOGTO_DEFAULT_ROLE_ID) { + return this.returnOk(roles.data); + } + } + + // Assign a default role to a user + return await this.assignDefaultRoleForApp(appId, process.env.LOGTO_DEFAULT_ROLE_ID); + } + private returnOk(data: any): ICommonErrorResponse { return { status: StatusCodes.OK, @@ -292,6 +308,20 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { return this.returnError(StatusCodes.BAD_GATEWAY, `getRolesForUser ${err}`); } } + + private async assignDefaultRoleForApp(appId: string, roleId: string): Promise { + const uri = new URL(`/api/applications/${appId}/roles`, process.env.LOGTO_ENDPOINT); + // Such role exists + try { + const body = { + roleIds: [roleId], + }; + return await this.postToLogto(uri, body, { 'Content-Type': 'application/json' }); + } catch (err) { + return this.returnError(StatusCodes.BAD_GATEWAY, `getRolesForUser ${err}`); + } + } + private async getRoleIdByName(roleName: string): Promise { const uri = new URL(`/api/roles`, process.env.LOGTO_ENDPOINT); try { diff --git a/src/middleware/auth/user-info-fetcher/m2m-token.ts b/src/middleware/auth/user-info-fetcher/m2m-token.ts index 07179229..e985f4a0 100644 --- a/src/middleware/auth/user-info-fetcher/m2m-token.ts +++ b/src/middleware/auth/user-info-fetcher/m2m-token.ts @@ -23,22 +23,20 @@ export class M2MTokenUserInfoFetcher extends AuthReturn implements IUserInfoFetc public async verifyJWTToken(token: string, oauthProvider: IOAuthProvider): Promise { try { - console.log(token); const { payload } = await jwtVerify( token, // The raw Bearer Token extracted from the request header createRemoteJWKSet(new URL(oauthProvider.endpoint_jwks)) // generate a jwks using jwks_uri inquired from Logto server ); - // console.log(payload); // Setup the scopes from the token if (!payload.sub) { return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No sub found in the token.`); } const { error, data: scopes } = await oauthProvider.getAppScopes(payload.sub); - // console.log('Scopes', scopes); if (error) { return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No scopes found for the roles.`); } this.setScopes(scopes); + this.setUserId(payload.sub); return this.returnOk(); } catch (error) { console.error(error); From 96e690c501d499760559d2c623bf5dd91f875a07 Mon Sep 17 00:00:00 2001 From: DaevMithran Date: Wed, 10 Jan 2024 19:01:38 +0530 Subject: [PATCH 5/8] Update create api flow & auth --- src/app.ts | 3 +- src/controllers/account.ts | 189 ++++++------------ src/middleware/auth/base-auth-handler.ts | 5 +- src/middleware/auth/routine.ts | 11 + .../auth/user-info-fetcher/m2m-token.ts | 7 +- src/middleware/authentication.ts | 36 ++-- src/types/authentication.ts | 1 + tests/unit/validation/is-jsonld-proof.test.ts | 1 - 8 files changed, 107 insertions(+), 146 deletions(-) diff --git a/src/app.ts b/src/app.ts index 75f7dd11..011bb28c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -44,7 +44,6 @@ class App { } private middleware() { - const auth = new Authentication(); this.express.use( express.json({ limit: '50mb', @@ -68,8 +67,8 @@ class App { }, }) ); - this.express.use(cookieParser()); + const auth = new Authentication(); if (process.env.ENABLE_AUTHENTICATION === 'true') { this.express.use( session({ diff --git a/src/controllers/account.ts b/src/controllers/account.ts index 7c57b16b..2593bd8f 100644 --- a/src/controllers/account.ts +++ b/src/controllers/account.ts @@ -13,8 +13,6 @@ import type { CustomerEntity } from '../database/entities/customer.entity.js'; import type { UserEntity } from '../database/entities/user.entity.js'; import type { PaymentAccountEntity } from '../database/entities/payment.account.entity.js'; import { IdentityServiceStrategySetup } from '../services/identity/index.js'; -import { decodeJwt } from 'jose'; -import { BaseAuthHandler } from '../middleware/auth/base-auth-handler.js'; export class AccountController { /** @@ -213,7 +211,7 @@ export class AccountController { customer = user.customer; } - // 4. Check is paymentAccount exists for the customer\ + // 4. Check is paymentAccount exists for the customer const accounts = await PaymentAccountService.instance.find({ customer }); if (accounts.length === 0) { const key = await new IdentityServiceStrategySetup(customer.customerId).agent.createKey( @@ -337,23 +335,13 @@ export class AccountController { public async create(request: Request, response: Response) { // For now we keep temporary 1-1 relation between user and customer // So the flow is: - // 1. Get LogTo user id from request body - // 2. Check if such row exists in the DB - // 2.1. If no - create it - // 3. If yes - check that there is customer associated with such user - // 3.1. If no: - // 3.1.1. Create customer - // 3.1.2. Assign customer to the user - - // 4. Check is paymentAccount exists for the customer - // 4.1. If no - create it - - // 5.1 Get app's roles - // 5.2 If list of roles is empty and the user is not suspended - assign default role - // 7. Check the token balance for Testnet account - + // 1. Get LogTo app id from request header + // 2. Check if the customer exists + // 2.1. if no - Create customer + // 3. Check is paymentAccount exists for the customer + // 3.1. If no - create it + // 4. Check the token balance for Testnet account let customer: CustomerEntity | null; - let user: UserEntity | null; let paymentAccount: PaymentAccountEntity | null; // 1. Get logTo UserId from request body @@ -362,123 +350,68 @@ export class AccountController { error: 'User id is not specified or primaryEmail is not set', }); } - const token = BaseAuthHandler.extractBearerTokenFromHeaders(request.headers) as string; - const payload = decodeJwt(token); - const logToUserId = payload.sub as string; const logToUserEmail = request.body.user.primaryEmail; - const defaultRole = await RoleService.instance.getDefaultRole(); - if (!defaultRole) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: 'Default role is not set on Credential Service side', - }); - } - // 2. Check if such row exists in the DB - user = await UserService.instance.get(logToUserId); - if (!user) { - // 2.1. If no - create customer first - // Cause for now we assume only 1-1 connection between user and customer - // We think here that if no user row - no customer also, cause customer should be created before user - // Even if customer was created before for such user but the process was interruted somehow - we need to create it again - // Cause we don't know the state of the customer in this case - // 2.1.1. Create customer - customer = (await CustomerService.instance.create(logToUserEmail)) as CustomerEntity; - if (!customer) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: 'User is not found in db: Customer was not created', - }); - } - // 2.2. Create user - user = await UserService.instance.create(logToUserId, customer, defaultRole); - if (!user) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: 'User is not found in db: User was not created', - }); - } - } - // 3. If yes - check that there is customer associated with such user - if (!user.customer) { - // 3.1. If no: - // 3.1.1. Create customer - customer = (await CustomerService.instance.create(logToUserEmail)) as CustomerEntity; - if (!customer) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: 'User exists in db: Customer was not created', - }); - } - // 3.1.2. Assign customer to the user - user.customer = customer; - await UserService.instance.update(user.logToId, customer); - } else { - customer = user.customer; - } - // 4. Check is paymentAccount exists for the customer\ - const accounts = await PaymentAccountService.instance.find({ customer }); - if (accounts.length === 0) { - const key = await new IdentityServiceStrategySetup(customer.customerId).agent.createKey( - 'Secp256k1', - customer - ); - if (!key) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: 'PaymentAccount is not found in db: Key was not created', - }); - } - paymentAccount = (await PaymentAccountService.instance.create( - CheqdNetwork.Testnet, - true, - customer, - key - )) as PaymentAccountEntity; - if (!paymentAccount) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: 'PaymentAccount is not found in db: Payment account was not created', - }); + try { + // 2. Check if the customer exists + if (response.locals.customer) { + customer = response.locals.customer as CustomerEntity; + } else { + // 2.1 Create customer + customer = (await CustomerService.instance.create(logToUserEmail)) as CustomerEntity; + if (!customer) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: 'Customer creation failed', + }); + } } - } else { - paymentAccount = accounts[0]; - } - const logToHelper = new LogToHelper(); - const _r = await logToHelper.setup(); - if (_r.status !== StatusCodes.OK) { - return response.status(StatusCodes.BAD_GATEWAY).json({ - error: _r.error, - }); - } - // 5. Assign default role on LogTo - // 5.1 Get user's roles - const roles = await logToHelper.getRolesForApp(user.logToId); - if (roles.status !== StatusCodes.OK) { - return response.status(StatusCodes.BAD_GATEWAY).json({ - error: roles.error, - }); - } - - // 5.2 If list of roles is empty and the user is not suspended - assign default role - if (roles.data.length === 0) { - const _r = await logToHelper.setDefaultRoleForApp(user.logToId); - if (_r.status !== StatusCodes.OK) { - return response.status(StatusCodes.BAD_GATEWAY).json({ - error: _r.error, - }); + // 3. Check is paymentAccount exists for the customer + const accounts = await PaymentAccountService.instance.find({ customer }); + if (accounts.length === 0) { + const key = await new IdentityServiceStrategySetup(customer.customerId).agent.createKey( + 'Secp256k1', + customer + ); + if (!key) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: 'PaymentAccount is not found in db: Key was not created', + }); + } + paymentAccount = (await PaymentAccountService.instance.create( + CheqdNetwork.Testnet, + true, + customer, + key + )) as PaymentAccountEntity; + if (!paymentAccount) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: 'PaymentAccount is not found in db: Payment account was not created', + }); + } + } else { + paymentAccount = accounts[0]; } - } - // 7. Check the token balance for Testnet account - if (paymentAccount.address && process.env.ENABLE_ACCOUNT_TOPUP === 'true') { - const balances = await checkBalance(paymentAccount.address, process.env.TESTNET_RPC_URL); - const balance = balances[0]; - if (!balance || +balance.amount < TESTNET_MINIMUM_BALANCE * Math.pow(10, DEFAULT_DENOM_EXPONENT)) { - // 3.1 If it's less then required for DID creation - assign new portion from testnet-faucet - const resp = await FaucetHelper.delegateTokens(paymentAccount.address); - if (resp.status !== StatusCodes.OK) { - return response.status(StatusCodes.BAD_GATEWAY).json({ - error: resp.error, - }); + // 4. Check the token balance for Testnet account + if (paymentAccount.address && process.env.ENABLE_ACCOUNT_TOPUP === 'true') { + const balances = await checkBalance(paymentAccount.address, process.env.TESTNET_RPC_URL); + const balance = balances[0]; + if (!balance || +balance.amount < TESTNET_MINIMUM_BALANCE * Math.pow(10, DEFAULT_DENOM_EXPONENT)) { + // 3.1 If it's less then required for DID creation - assign new portion from testnet-faucet + const resp = await FaucetHelper.delegateTokens(paymentAccount.address); + if (resp.status !== StatusCodes.OK) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: resp.error, + }); + } } } + return response.status(StatusCodes.OK).json(customer); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal Error: ${(error as Error)?.message || error}`, + }); } - return response.status(StatusCodes.OK).json(customer); } } diff --git a/src/middleware/auth/base-auth-handler.ts b/src/middleware/auth/base-auth-handler.ts index bea6e478..629c9e43 100644 --- a/src/middleware/auth/base-auth-handler.ts +++ b/src/middleware/auth/base-auth-handler.ts @@ -48,6 +48,7 @@ export class BaseAPIGuard extends RuleRoutine implements IAPIGuard { } this.setScopes(_res.data.scopes); this.setUserId(_res.data.userId); + this.setCustomerId(_res.data.customerId); // Checks if the list of scopes from user enough to make an action if (!this.areValidScopes(this.getRule(), this.getScopes())) { return this.returnError( @@ -110,7 +111,7 @@ export class BaseAuthHandler extends BaseAPIGuard implements IAuthHandler { private nextHandler: IAuthHandler; oauthProvider: IOAuthProvider; private static bearerTokenIdentifier = 'Bearer'; - private pathSkip = ['/swagger', '/static', '/logto', '/account/bootstrap', '/account/create']; + private pathSkip = ['/swagger', '/static', '/logto', '/account/bootstrap']; constructor() { super(); @@ -135,6 +136,8 @@ export class BaseAuthHandler extends BaseAPIGuard implements IAuthHandler { this.setUserInfoStrategy(new APITokenUserInfoFetcher(token)); } else { this.setUserInfoStrategy(new M2MTokenUserInfoFetcher(token)); + const customerId = request.headers['customer-id']; + if (typeof customerId === 'string') this.setCustomerId(customerId); } } else { this.setUserInfoStrategy(new SwaggerUserInfoFetcher()); diff --git a/src/middleware/auth/routine.ts b/src/middleware/auth/routine.ts index 00ef7343..9f6291bc 100644 --- a/src/middleware/auth/routine.ts +++ b/src/middleware/auth/routine.ts @@ -18,6 +18,7 @@ export interface IAuthParams { reset(): void; // Getters getUserId(): string; + getCustomerId(): string; getScopes(): string[]; getNamespace(): Namespaces; getIsAllowedUnauthorized(): boolean; @@ -34,6 +35,7 @@ export interface IAuthParams { export class AuthParams implements IAuthParams { userId: string; + customerId: string; scopes: string[]; namespace: Namespaces; isAllowedUnauthorized: boolean; @@ -44,6 +46,7 @@ export class AuthParams implements IAuthParams { this.namespace = '' as Namespaces; this.scopes = []; this.userId = '' as string; + this.customerId = '' as string; this.isAllowedUnauthorized = false; this.rule = {} as MethodToScopeRule; this.routeToScoupeList = []; @@ -53,6 +56,9 @@ export class AuthParams implements IAuthParams { public getUserId(): string { return this.userId; } + public getCustomerId(): string { + return this.customerId; + } public getScopes(): string[] { return this.scopes; } @@ -73,6 +79,9 @@ export class AuthParams implements IAuthParams { public setUserId(userId: string): void { this.userId = userId; } + public setCustomerId(customerId: string): void { + this.customerId = customerId; + } public setScopes(scopes: string[]): void { this.scopes = scopes; } @@ -112,6 +121,7 @@ export class AuthReturn extends AuthParams implements IReturn { error: '', data: { userId: this.getUserId(), + customerId: this.getCustomerId(), scopes: this.getScopes() as string[], namespace: this.getNamespace(), isAllowedUnauthorized: this.getIsAllowedUnauthorized(), @@ -125,6 +135,7 @@ export class AuthReturn extends AuthParams implements IReturn { error: error, data: { userId: '', + customerId: '', scopes: [], namespace: this.getNamespace(), isAllowedUnauthorized: this.getIsAllowedUnauthorized(), diff --git a/src/middleware/auth/user-info-fetcher/m2m-token.ts b/src/middleware/auth/user-info-fetcher/m2m-token.ts index e985f4a0..ef66ab07 100644 --- a/src/middleware/auth/user-info-fetcher/m2m-token.ts +++ b/src/middleware/auth/user-info-fetcher/m2m-token.ts @@ -25,7 +25,11 @@ export class M2MTokenUserInfoFetcher extends AuthReturn implements IUserInfoFetc try { const { payload } = await jwtVerify( token, // The raw Bearer Token extracted from the request header - createRemoteJWKSet(new URL(oauthProvider.endpoint_jwks)) // generate a jwks using jwks_uri inquired from Logto server + createRemoteJWKSet(new URL(oauthProvider.endpoint_jwks)), // generate a jwks using jwks_uri inquired from Logto server + { + // expected issuer of the token, should be issued by the Logto server + issuer: oauthProvider.endpoint_issuer, + } ); // Setup the scopes from the token if (!payload.sub) { @@ -36,7 +40,6 @@ export class M2MTokenUserInfoFetcher extends AuthReturn implements IUserInfoFetc return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No scopes found for the roles.`); } this.setScopes(scopes); - this.setUserId(payload.sub); return this.returnOk(); } catch (error) { console.error(error); diff --git a/src/middleware/authentication.ts b/src/middleware/authentication.ts index 065bc880..8981a986 100644 --- a/src/middleware/authentication.ts +++ b/src/middleware/authentication.ts @@ -16,6 +16,7 @@ import { configLogToExpress } from '../types/constants.js'; import { handleAuthRoutes, withLogto } from '@logto/express'; import { LogToProvider } from './auth/oauth/logto-provider.js'; import { AuthInfoHandler } from './auth/routes/auth-user-info.js'; +import { CustomerService } from '../services/customer.js'; dotenv.config(); @@ -136,19 +137,30 @@ export class Authentication { // Only for rules when it's not allowed for unauthorized users // we need to find customer and assign it to the response.locals if (!_resp.data.isAllowedUnauthorized) { - const user = await UserService.instance.get(_resp.data.userId); - if (!user) { - return response.status(StatusCodes.NOT_FOUND).json({ - error: `Looks like user with logToId ${_resp.data.userId} is not found`, - }); + if (_resp.data.userId !== '') { + const user = await UserService.instance.get(_resp.data.userId); + if (!user) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: `Looks like user with logToId ${_resp.data.userId} is not found`, + }); + } + if (user && !user.customer) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: `Looks like user with logToId ${_resp.data.userId} is not assigned to any CredentialService customer`, + }); + } + response.locals.customer = user.customer; + response.locals.user = user; + } else if (!(['/account/create'].includes(request.path) && _resp.data.customerId == '')) { + // allow bootstrap apis to create a customer if there is not customerId + const customer = await CustomerService.instance.get(_resp.data.customerId); + if (!customer) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: `Looks like user with logToId ${_resp.data.customerId} is not found`, + }); + } + response.locals.customer = customer; } - if (user && !user.customer) { - return response.status(StatusCodes.NOT_FOUND).json({ - error: `Looks like user with logToId ${_resp.data.userId} is not assigned to any CredentialService customer`, - }); - } - response.locals.customer = user.customer; - response.locals.user = user; } next(); } catch (err) { diff --git a/src/types/authentication.ts b/src/types/authentication.ts index 6cdf1061..afb8c8f7 100644 --- a/src/types/authentication.ts +++ b/src/types/authentication.ts @@ -72,6 +72,7 @@ export interface IAuthResponse extends ICommonErrorResponse { status: number; data: { userId: string; + customerId: string; scopes: string[]; namespace: Namespaces; isAllowedUnauthorized: boolean; diff --git a/tests/unit/validation/is-jsonld-proof.test.ts b/tests/unit/validation/is-jsonld-proof.test.ts index 6e5d473e..9a8a9ae5 100644 --- a/tests/unit/validation/is-jsonld-proof.test.ts +++ b/tests/unit/validation/is-jsonld-proof.test.ts @@ -15,7 +15,6 @@ const validProof = { describe('isJSONLDProofValidator. Positive', () => { it('should return true for valid JSONLD proof', () => { const result = jsonldProofValdiator.validate(validProof); - console.log(result); expect(result.valid).toBe(true); }); }); From 393afcf79548d827822be92a6c98dd0940bbb77b Mon Sep 17 00:00:00 2001 From: DaevMithran Date: Thu, 11 Jan 2024 13:24:25 +0530 Subject: [PATCH 6/8] Add request validator --- src/app.ts | 2 +- src/controllers/account.ts | 28 +++++++++++++++++++++++----- src/static/swagger.json | 3 --- src/types/swagger-types.ts | 2 -- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/app.ts b/src/app.ts index 011bb28c..882b7cf0 100644 --- a/src/app.ts +++ b/src/app.ts @@ -184,7 +184,7 @@ class App { ); // Account API - app.post('/account/create', new AccountController().create); + app.post('/account/create', AccountController.createValidator, new AccountController().create); app.get('/account', new AccountController().get); app.get('/account/idtoken', new AccountController().getIdToken); diff --git a/src/controllers/account.ts b/src/controllers/account.ts index 2593bd8f..0ea71660 100644 --- a/src/controllers/account.ts +++ b/src/controllers/account.ts @@ -13,8 +13,24 @@ import type { CustomerEntity } from '../database/entities/customer.entity.js'; import type { UserEntity } from '../database/entities/user.entity.js'; import type { PaymentAccountEntity } from '../database/entities/payment.account.entity.js'; import { IdentityServiceStrategySetup } from '../services/identity/index.js'; +import { check, validationResult } from 'express-validator'; export class AccountController { + public static createValidator = [ + check('user') + .exists() + .withMessage('user is required') + .isObject() + .withMessage('user property should be valid object') + .bail(), + check('user.primaryEmail') + .exists() + .withMessage('user.primaryEmail is required') + .trim() + .isEmail() + .withMessage('primaryEmail is not a valid email id') + .bail(), + ]; /** * @openapi * @@ -335,7 +351,7 @@ export class AccountController { public async create(request: Request, response: Response) { // For now we keep temporary 1-1 relation between user and customer // So the flow is: - // 1. Get LogTo app id from request header + // 1. Get LogToPrimaryEmail from request body // 2. Check if the customer exists // 2.1. if no - Create customer // 3. Check is paymentAccount exists for the customer @@ -345,11 +361,13 @@ export class AccountController { let paymentAccount: PaymentAccountEntity | null; // 1. Get logTo UserId from request body - if (!request.body.user || !request.body.user.primaryEmail) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: 'User id is not specified or primaryEmail is not set', - }); + // validate request + const result = validationResult(request); + // handle error + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ error: result.array().pop()?.msg }); } + const logToUserEmail = request.body.user.primaryEmail; try { diff --git a/src/static/swagger.json b/src/static/swagger.json index 362aedee..1127234e 100644 --- a/src/static/swagger.json +++ b/src/static/swagger.json @@ -3438,9 +3438,6 @@ "user": { "type": "object", "properties": { - "id": { - "type": "string" - }, "primaryEmail": { "type": "string" } diff --git a/src/types/swagger-types.ts b/src/types/swagger-types.ts index df5cda1f..5eb086a1 100644 --- a/src/types/swagger-types.ts +++ b/src/types/swagger-types.ts @@ -1247,8 +1247,6 @@ * user: * type: object * properties: - * id: - * type: string * primaryEmail: * type: string * InvalidRequest: From 8104b7b24742edeeafbe11dd3b80624258969b84 Mon Sep 17 00:00:00 2001 From: DaevMithran Date: Thu, 11 Jan 2024 18:06:10 +0530 Subject: [PATCH 7/8] Delegate tokens only to testnet account --- src/controllers/account.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/controllers/account.ts b/src/controllers/account.ts index 0ea71660..e6224183 100644 --- a/src/controllers/account.ts +++ b/src/controllers/account.ts @@ -386,7 +386,8 @@ export class AccountController { // 3. Check is paymentAccount exists for the customer const accounts = await PaymentAccountService.instance.find({ customer }); - if (accounts.length === 0) { + paymentAccount = accounts.find((account) => account.namespace === CheqdNetwork.Testnet) || null; + if (paymentAccount === null) { const key = await new IdentityServiceStrategySetup(customer.customerId).agent.createKey( 'Secp256k1', customer @@ -407,8 +408,6 @@ export class AccountController { error: 'PaymentAccount is not found in db: Payment account was not created', }); } - } else { - paymentAccount = accounts[0]; } // 4. Check the token balance for Testnet account From 019853531c4d3b6734fd3d8d709636f0095fd2eb Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Thu, 11 Jan 2024 16:16:19 +0100 Subject: [PATCH 8/8] chore: Suggestion to simplifying the user or customer getting (#469) * fix: Fix error handling on tracking operations [DEV-3527] (#461) * fix: Fix error handling on tracking operations * Silently log tracking errors * Remove duplicate return statements --------- Co-authored-by: Andrew Nikitin * chore(release): 2.15.1-develop.1 [skip ci] ## [2.15.1-develop.1](https://github.com/cheqd/credential-service/compare/2.15.0...2.15.1-develop.1) (2024-01-11) ### Bug Fixes * Fix error handling on tracking operations [DEV-3527] ([#461](https://github.com/cheqd/credential-service/issues/461)) ([53d7dfd](https://github.com/cheqd/credential-service/commit/53d7dfdcc23d90dba85a681f39ebc724b7fbdcc2)) * Simplyfing setting up user or customer from fetcher * Fix it for account creating --------- Co-authored-by: DaevMithran <61043607+DaevMithran@users.noreply.github.com> Co-authored-by: semantic-release-bot --- CHANGELOG.md | 7 +++ package.json | 2 +- src/controllers/account.ts | 55 +++++++------------ src/controllers/credential-status.ts | 45 ++++++++------- src/controllers/credentials.ts | 30 ++++++---- src/controllers/resource.ts | 10 ++-- src/middleware/auth/base-auth-handler.ts | 2 - .../auth/user-info-fetcher/m2m-token.ts | 4 ++ src/middleware/authentication.ts | 30 +++++++--- 9 files changed, 102 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d25bb36..7d67a8c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [2.15.1-develop.1](https://github.com/cheqd/credential-service/compare/2.15.0...2.15.1-develop.1) (2024-01-11) + + +### Bug Fixes + +* Fix error handling on tracking operations [DEV-3527] ([#461](https://github.com/cheqd/credential-service/issues/461)) ([53d7dfd](https://github.com/cheqd/credential-service/commit/53d7dfdcc23d90dba85a681f39ebc724b7fbdcc2)) + ## [2.15.0](https://github.com/cheqd/credential-service/compare/2.14.0...2.15.0) (2024-01-05) diff --git a/package.json b/package.json index 01d0b575..3d9e6ea3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cheqd/credential-service", - "version": "2.15.0", + "version": "2.15.1-develop.1", "description": "cheqd Credential Service Backend", "source": "src/index.ts", "main": "dist/index.js", diff --git a/src/controllers/account.ts b/src/controllers/account.ts index e6224183..8948f055 100644 --- a/src/controllers/account.ts +++ b/src/controllers/account.ts @@ -111,41 +111,28 @@ export class AccountController { } const identityStrategySetup = new IdentityServiceStrategySetup(response.locals.customer.customerId); - let apiKey = await identityStrategySetup.agent.getAPIKey(response.locals.customer, response.locals.user); - // If there is no API key for the customer - create it - if (!apiKey) { - apiKey = await identityStrategySetup.agent.setAPIKey( - request.session.idToken, - response.locals.customer, - response.locals.user - ); - } else if (apiKey.isExpired()) { - // If API key is expired - update it - apiKey = await identityStrategySetup.agent.updateAPIKey(apiKey, request.session.idToken); - } - return response.status(StatusCodes.OK).json({ - idToken: apiKey?.apiKey, - }); - } - - public async setupDefaultRole(request: Request, response: Response) { - if (request.body) { - const { body } = request; - if (!body.user.isSuspended) { - const logToHelper = new LogToHelper(); - const _r = await logToHelper.setup(); - if (_r.status !== StatusCodes.OK) { - return response.status(StatusCodes.BAD_GATEWAY).json({ - error: _r.error, - }); - } - const resp = await logToHelper.setDefaultRoleForUser(body.user.id as string); - return response.status(resp.status).json({ - error: resp.error, - }); + try { + // Get the API key for the customer + let apiKey = await identityStrategySetup.agent.getAPIKey(response.locals.customer, response.locals.user); + // If there is no API key for the customer - create it + if (!apiKey) { + apiKey = await identityStrategySetup.agent.setAPIKey( + request.session.idToken, + response.locals.customer, + response.locals.user + ); + } else if (apiKey.isExpired()) { + // If API key is expired - update it + apiKey = await identityStrategySetup.agent.updateAPIKey(apiKey, request.session.idToken); } + return response.status(StatusCodes.OK).json({ + idToken: apiKey?.apiKey, + }); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + }); } - return response.status(StatusCodes.BAD_REQUEST).json({}); } public async bootstrap(request: Request, response: Response) { @@ -424,7 +411,7 @@ export class AccountController { } } } - return response.status(StatusCodes.OK).json(customer); + return response.status(StatusCodes.CREATED).json(customer); } catch (error) { return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: `Internal Error: ${(error as Error)?.message || error}`, diff --git a/src/controllers/credential-status.ts b/src/controllers/credential-status.ts index c22c067b..9e3c410f 100644 --- a/src/controllers/credential-status.ts +++ b/src/controllers/credential-status.ts @@ -562,13 +562,14 @@ export class CredentialStatusController { }, } as ITrackOperation; - const trackResult = await identityServiceStrategySetup.agent.trackOperation(trackResourceInfo); + const trackResult = await identityServiceStrategySetup.agent + .trackOperation(trackResourceInfo) + .catch((error) => { + return { error }; + }); if (trackResult.error) { - return response - .status(StatusCodes.INTERNAL_SERVER_ERROR) - .json(trackResult as CreateEncryptedStatusListUnsuccessfulResponseBody); + console.error(`Tracking Error: ${trackResult.error}`); } - // return result return response.status(StatusCodes.OK).json({ ...result, encrypted: undefined }); } catch (error) { @@ -695,14 +696,14 @@ export class CredentialStatusController { feePaymentNetwork: CheqdNetwork.Testnet, }, } as ITrackOperation; - const trackResult = await identityServiceStrategySetup.agent.trackOperation(trackResourceInfo); - + const trackResult = await identityServiceStrategySetup.agent + .trackOperation(trackResourceInfo) + .catch((error) => { + return { error }; + }); if (trackResult.error) { - return response - .status(StatusCodes.INTERNAL_SERVER_ERROR) - .json(trackResult as CreateEncryptedStatusListUnsuccessfulResponseBody); + console.error(`Tracking Error: ${trackResult.error}`); } - // return result return response.status(StatusCodes.OK).json({ ...result, encrypted: undefined }); } catch (error) { @@ -873,12 +874,13 @@ export class CredentialStatusController { symmetricKey: '', }, } as ITrackOperation; - const trackResult = await identityServiceStrategySetup.agent.trackOperation(trackResourceInfo); + const trackResult = await identityServiceStrategySetup.agent + .trackOperation(trackResourceInfo) + .catch((error) => { + return { error }; + }); if (trackResult.error) { - return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - updated: false, - error: trackResult.error, - } as UpdateUnencryptedStatusListUnsuccessfulResponseBody); + console.error(`Tracking Error: ${trackResult.error}`); } } @@ -1072,12 +1074,13 @@ export class CredentialStatusController { feePaymentNetwork: CheqdNetwork.Testnet, }, } as ITrackOperation; - const trackResult = await identityServiceStrategySetup.agent.trackOperation(trackResourceInfo); + const trackResult = await identityServiceStrategySetup.agent + .trackOperation(trackResourceInfo) + .catch((error) => { + return { error }; + }); if (trackResult.error) { - return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - updated: false, - error: trackResult.error, - } as UpdateUnencryptedStatusListUnsuccessfulResponseBody); + console.error(`Tracking Error: ${trackResult.error}`); } } diff --git a/src/controllers/credentials.ts b/src/controllers/credentials.ts index 83a0221e..852879f9 100644 --- a/src/controllers/credentials.ts +++ b/src/controllers/credentials.ts @@ -331,11 +331,13 @@ export class CredentialController { } as ITrackOperation; // Track operation - const trackResult = await identityServiceStrategySetup.agent.trackOperation(trackInfo); - if (trackResult.error) { - return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - error: trackResult.error, + const trackResult = await identityServiceStrategySetup.agent + .trackOperation(trackInfo) + .catch((error) => { + return { error }; }); + if (trackResult.error) { + console.error(`Tracking Error: ${trackResult.error}`); } } // Return Ok response @@ -435,11 +437,13 @@ export class CredentialController { } as ITrackOperation; // Track operation - const trackResult = await identityServiceStrategySetup.agent.trackOperation(trackInfo); - if (trackResult.error) { - return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - error: trackResult.error, + const trackResult = await identityServiceStrategySetup.agent + .trackOperation(trackInfo) + .catch((error) => { + return { error }; }); + if (trackResult.error) { + console.error(`Tracking Error: ${trackResult.error}`); } } @@ -537,11 +541,13 @@ export class CredentialController { } as ITrackOperation; // Track operation - const trackResult = await identityServiceStrategySetup.agent.trackOperation(trackInfo); - if (trackResult.error) { - return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - error: trackResult.error, + const trackResult = await identityServiceStrategySetup.agent + .trackOperation(trackInfo) + .catch((error) => { + return { error }; }); + if (trackResult.error) { + console.error(`Tracking Error: ${trackResult.error}`); } } // Return Ok response diff --git a/src/controllers/resource.ts b/src/controllers/resource.ts index efeb22e1..4f06862a 100644 --- a/src/controllers/resource.ts +++ b/src/controllers/resource.ts @@ -190,11 +190,13 @@ export class ResourceController { }, } as ITrackOperation; - const trackResult = await identityServiceStrategySetup.agent.trackOperation(trackResourceInfo); - if (trackResult.error) { - return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - error: `${trackResult.error}`, + const trackResult = await identityServiceStrategySetup.agent + .trackOperation(trackResourceInfo) + .catch((error) => { + return { error }; }); + if (trackResult.error) { + console.error(`Tracking Error: ${trackResult.error}`); } return response.status(StatusCodes.CREATED).json({ diff --git a/src/middleware/auth/base-auth-handler.ts b/src/middleware/auth/base-auth-handler.ts index 629c9e43..c479bb25 100644 --- a/src/middleware/auth/base-auth-handler.ts +++ b/src/middleware/auth/base-auth-handler.ts @@ -136,8 +136,6 @@ export class BaseAuthHandler extends BaseAPIGuard implements IAuthHandler { this.setUserInfoStrategy(new APITokenUserInfoFetcher(token)); } else { this.setUserInfoStrategy(new M2MTokenUserInfoFetcher(token)); - const customerId = request.headers['customer-id']; - if (typeof customerId === 'string') this.setCustomerId(customerId); } } else { this.setUserInfoStrategy(new SwaggerUserInfoFetcher()); diff --git a/src/middleware/auth/user-info-fetcher/m2m-token.ts b/src/middleware/auth/user-info-fetcher/m2m-token.ts index ef66ab07..4c6df508 100644 --- a/src/middleware/auth/user-info-fetcher/m2m-token.ts +++ b/src/middleware/auth/user-info-fetcher/m2m-token.ts @@ -18,6 +18,10 @@ export class M2MTokenUserInfoFetcher extends AuthReturn implements IUserInfoFetc } async fetchUserInfo(request: Request, oauthProvider: IOAuthProvider): Promise { + // Get customerId from header + const customerId = request.headers['customer-id']; + if (typeof customerId === 'string') this.setCustomerId(customerId); + // Verify M2M token return this.verifyJWTToken(this.token as string, oauthProvider); } diff --git a/src/middleware/authentication.ts b/src/middleware/authentication.ts index 8981a986..d48f62f6 100644 --- a/src/middleware/authentication.ts +++ b/src/middleware/authentication.ts @@ -122,6 +122,11 @@ export class Authentication { } } + // ToDo: refactor it or keep for the moment of setting up the admin panel + private isBootstrapping(request: Request) { + return ['/account/create'].includes(request.path); + } + public async guard(request: Request, response: Response, next: NextFunction) { const { provider } = request.body as { claim: string; provider: string }; if (this.authHandler.skipPath(request.path)) return next(); @@ -135,10 +140,12 @@ export class Authentication { }); } // Only for rules when it's not allowed for unauthorized users - // we need to find customer and assign it to the response.locals + // we need to find customer or user and assign them to the response.locals if (!_resp.data.isAllowedUnauthorized) { + let customer; + let user; if (_resp.data.userId !== '') { - const user = await UserService.instance.get(_resp.data.userId); + user = await UserService.instance.get(_resp.data.userId); if (!user) { return response.status(StatusCodes.NOT_FOUND).json({ error: `Looks like user with logToId ${_resp.data.userId} is not found`, @@ -149,18 +156,23 @@ export class Authentication { error: `Looks like user with logToId ${_resp.data.userId} is not assigned to any CredentialService customer`, }); } - response.locals.customer = user.customer; - response.locals.user = user; - } else if (!(['/account/create'].includes(request.path) && _resp.data.customerId == '')) { - // allow bootstrap apis to create a customer if there is not customerId - const customer = await CustomerService.instance.get(_resp.data.customerId); + customer = user.customer; + } + if (_resp.data.customerId !== '' && !customer) { + customer = await CustomerService.instance.get(_resp.data.customerId); if (!customer) { return response.status(StatusCodes.NOT_FOUND).json({ - error: `Looks like user with logToId ${_resp.data.customerId} is not found`, + error: `Looks like customer with id ${_resp.data.customerId} is not found`, }); } - response.locals.customer = customer; } + if (!customer && !user && !this.isBootstrapping(request)) { + return response.status(StatusCodes.UNAUTHORIZED).json({ + error: `Looks like customer and user are not found in the system or they are not registered yet. Please contact administrator.`, + }) + } + response.locals.customer = customer; + response.locals.user = user; } next(); } catch (err) {