Skip to content

Commit

Permalink
Merge pull request #4 from ixofoundation/develop
Browse files Browse the repository at this point in the history
Add matrix creds passing to login flows
  • Loading branch information
Michael-Ixo authored Aug 29, 2024
2 parents 65ac639 + 5891075 commit 63f6a46
Show file tree
Hide file tree
Showing 6 changed files with 284 additions and 1 deletion.
119 changes: 118 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@

The Ixo Message Relayer is a server that facilitates a meticulously coordinated sequence of operations ensuring mobile-to-web authentication, transaction signing, and secure data passing on the IXO blockchain. The process kicks off with the Login module, where the SDK generates a random hash and a secureHash (which is a SHA-256 hash of the hash and a secureNonce). A QR code, containing this hash, is then displayed for the mobile app to scan. Once scanned, the mobile app uploads the user data to the server using this hash as an identifier of the login request, which the SDK is polling for. This endpoint is secured with an AUTHORIZATION environment variable, ensuring only the mobile app with the correct authorization can upload this data. Subsequently, the SDK polls the server to fetch the login data, providing a secureNonce in the process. The server validates the request by hashing the provided hash and secureNonce to ensure it matches the secureHash, thereby affirming the authenticity of the user making the request. Upon validation, the server returns the login data to the SDK and purges the data from the server to maintain data cleanliness.

The server also includes a Matrix integration feature, enabling the management of Matrix login credentials. The Matrix flow, similar to the login flow, begins with the SDK generating a random hash and a secureHash (derived from the hash and a secureNonce). A QR code containing this hash is then displayed for the mobile app to scan. Once scanned, the mobile app uploads the Matrix login request data to the server using this hash as the identifier.

The Matrix login flow is conditional: it checks whether the user has a Matrix account and whether they are logged in to that account within the mobile app. If these conditions are met, the mobile app can proceed with the Matrix login. The server securely stores the Matrix login data and allows the SDK to poll for this data, similar to the regular login flow. The server validates the request by comparing the provided hash and secureNonce with the stored secureHash, ensuring that the request is authentic.

Upon successful validation, the server returns the Matrix login data to the SDK. A unique Matrix access token is then generated for each 'site' (or client) during this process. This token, which uses the client/site as the device name during its creation, is unique to that specific client/site and can be revoked or deactivated by the user through the mobile app. This revocation can occur at the user's discretion or when they log out of their Matrix profile or switch profiles. The response format for Matrix endpoints is designed to be flexible, allowing for new fields to be added over time while maintaining backward compatibility with existing fields.

The server also supports a secure data passing feature that allows the SDK to encrypt data, store it on the server, and have it decrypted and processed by the mobile app. This feature ensures that only the mobile app with the correct access token can retrieve and decrypt the data, making it useful for operations such as KYC (Know Your Customer) processes. The data is uploaded with an identifier hash and a type indicating the operation to be performed. The mobile app decrypts the data, performs the required operation, and uploads the success status and response to the server.

Note: The following describes the V1 transaction module, which is now deprecated in favor of the enhanced V2 transactions module. Users are encouraged to transition to V2 for a more efficient and dynamic transaction handling experience.
Expand Down Expand Up @@ -145,8 +151,21 @@ The server is designed to work seamlessly with a complementary SDK which facilit
- [Response Body](#response-body-16)
- [Response Properties](#response-properties-16)
- [Usage](#usage-14)
- [Matrix Login Endpoints](#matrix-login-endpoints)
- [POST `/matrix/login/create`](#post-matrixlogincreate)
- [Parameters](#parameters-14)
- [Request Body](#request-body-17)
- [Response Body](#response-body-17)
- [Response Properties](#response-properties-17)
- [Usage](#usage-15)
- [POST `/matrix/login/fetch`](#post-matrixloginfetch)
- [Parameters](#parameters-15)
- [Request Body](#request-body-18)
- [Response Body](#response-body-18)
- [Response Properties](#response-properties-18)
- [Usage](#usage-16)
- [Types](#types)
- [TransactionV2Dto](#transactionv2dto)
- [TransactionV2Dto](#transactionv2dto)
- [📃 License](#-license)

## Environment Variables
Expand Down Expand Up @@ -1118,6 +1137,104 @@ curl -X POST https://[server-address]/data/update \
-d '{"hash": "uniqueHash", "secureHash": "secureHashValue", "success": true, "response": "responseMessage"}'
```

## Matrix Login Endpoints

### POST `/matrix/login/create`

This endpoint, similar to the login create endpoint, is utilized by the mobile app to store matrix login request data on the server. Upon scanning a QR code generated by the SDK, the mobile app initiates a matrix login request by sending the relevant data to this endpoint. The matrix login data is stored on the server under a unique hash identifier generated by the SDK, which facilitates subsequent polling by the SDK to retrieve this data for matrix login. The endpoint is protected by an authorization mechanism to ensure that only the mobile app can upload matrix login data.

#### Parameters

- `hash`: A unique identifier for the matrix login request.
- `secureHash`: A secure hash generated by hashing the `hash` and a `secureNonce`.
- `data`: The matrix login request data.
- `success`: A boolean indicating the success status of the matrix login request.

#### Request Body

```json
{
"hash": "string",
"secureHash": "string",
"data": "object",
"success": "boolean"
}
```

#### Response Body

```json
{
"success": "boolean",
"data": {
"message": "string"
}
}
```

#### Response Properties

- **success**: Indicates whether the request to server was successful.
- **data**:
- **message**: A message explaining the success or failure of the request.

#### Usage

```bash
curl -X POST https://[server-address]/matrix/login/create \
-H "Content-Type: application/json" \
-d '{"hash": "uniqueHash", "secureHash": "secureHashValue", "data": { ... }, "success": true}'
```

### POST `/matrix/login/fetch`

This endpoint, similar to the login fetch endpoint, facilitates the retrieval of matrix login request data that was previously stored on the server by the mobile app. The SDK polls this endpoint to fetch the matrix login data for a user based on a unique hash identifier. The server validates the request by hashing the provided hash and a secureNonce to ensure it matches the stored secureHash, thereby affirming the authenticity of the user making the request. Upon validation, the server returns the matrix login data to the SDK and deletes the data from the server to maintain data cleanliness.

#### Parameters

- `hash`: A unique identifier for the matrix login request.
- `secureNonce`: A secure nonce generated by the SDK.

#### Request Body

```json
{
"hash": "string",
"secureNonce": "string"
}
```

#### Response Body

```json
{
"success": "boolean",
"data": {
"message": "string",
"data": "object",
"success": "boolean"
},
"code": "number"
}
```

#### Response Properties

- **success**: Indicates whether the request to server was successful.
- **code**: A code indicating whether the SDK should continue polling (418 if it should continue).
- **data**:
- **message**: A message explaining the success or failure of the request.
- **data**: The matrix login data
- **success**: Whether the matrix login was a success or fail due to rejection on mobile for example

#### Usage

```bash
curl -X POST https://[server-address]/matrix/login/fetch \
-H "Content-Type: application/json" \
-d '{"hash": "uniqueHash", "secureNonce": "secureNonceValue"}'
```

## Types

#### TransactionV2Dto
Expand Down
3 changes: 3 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { TransactionModule } from './transaction/transaction.module';
import { ScheduleModule } from '@nestjs/schedule';
import { ConfigModule } from '@nestjs/config';
import { DataModule } from './data/data.module';
import { MatrixModule } from './matrix/matrix.module';

@Module({
imports: [
Expand All @@ -17,6 +18,7 @@ import { DataModule } from './data/data.module';
LoginModule,
TransactionModule,
DataModule,
MatrixModule,
],
controllers: [AppController],
providers: [AppService],
Expand All @@ -34,6 +36,7 @@ export class AppModule implements NestModule {
'/transaction/v2/update',
'/data/fetch',
'/data/update',
'/matrix/login/create',
);
}
}
53 changes: 53 additions & 0 deletions src/matrix/matrix.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Body, Controller, HttpException, Post, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { MatrixService } from './matrix.service';
import { MatrixLoginFetchDto, MatrixLoginCreateDto } from './matrix.dto';
import { Response } from 'express';

@Controller('matrix')
@ApiTags('Matrix')
export class MatrixController {
constructor(private readonly matrixService: MatrixService) {}

@Post('/login/create') // for mobile
createMatrixLoginRequest(@Body() dto: MatrixLoginCreateDto) {
try {
return this.matrixService.createMatrixLogin(dto);
} catch (error) {
throw new HttpException(error.message, 400);
}
}

private fetchLoginTimeout = 10000; // 12 seconds timeout
private fetchLoginPollInterval = 1500; // check every 1.5 second
@Post('/login/fetch') // for client
fetchMatrixLoginRequest(
@Body() dto: MatrixLoginFetchDto,
@Res() res: Response,
) {
const startTime = Date.now();
const poll = async (): Promise<any> => {
try {
// Check if the client disconnected before making the next call
if (res.destroyed) return;

const result = await this.matrixService.fetchMatrixLogin(dto);
if (
result.success || // if success result
result.code !== 418 || // or if failed result but code is not 418(polling code)
Date.now() - startTime > this.fetchLoginTimeout // or if timeout
) {
return res.send(result);
}

await new Promise((resolve) =>
setTimeout(resolve, this.fetchLoginPollInterval),
);
return poll();
} catch (error) {
throw new HttpException(error.message, 400);
}
};
return poll();
}
}
11 changes: 11 additions & 0 deletions src/matrix/matrix.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export class MatrixLoginCreateDto {
hash: string;
secureHash: string;
data: string;
success: boolean;
}

export class MatrixLoginFetchDto {
hash: string;
secureNonce: string;
}
9 changes: 9 additions & 0 deletions src/matrix/matrix.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { MatrixController } from './matrix.controller';

@Module({
controllers: [MatrixController],
providers: [MatrixService],
})
export class MatrixModule {}
90 changes: 90 additions & 0 deletions src/matrix/matrix.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Injectable } from '@nestjs/common';
import { MatrixLoginFetchDto, MatrixLoginCreateDto } from './matrix.dto';
import { PrismaService } from 'nestjs-prisma';
import { Cron, CronExpression } from '@nestjs/schedule';
import { generateSecureHash } from '@ixo/signx-sdk';
import { returnError, returnSuccess } from 'src/utils';

@Injectable()
export class MatrixService {
constructor(private prisma: PrismaService) {}

async createMatrixLogin(dto: MatrixLoginCreateDto) {
// validate request
if (
!dto.hash ||
!dto.secureHash ||
!dto.data ||
typeof dto.success !== 'boolean'
) {
return returnError('Invalid request, missing parameters');
}

const validUntil = new Date(Date.now() + 1000 * 60 * 2); // 2 minutes

await this.prisma.login.upsert({
where: { hash: dto.hash },
create: {
hash: dto.hash,
secureHash: dto.secureHash,
data: dto.data,
validUntil,
success: dto.success,
},
update: {
secureHash: dto.secureHash,
data: dto.data,
validUntil,
success: dto.success,
},
});

return returnSuccess({
message: 'Matrix login request created successfully',
});
}

async fetchMatrixLogin(dto: MatrixLoginFetchDto): Promise<any> {
// validate request
if (!dto.hash || !dto.secureNonce) {
return returnError('Invalid request, missing parameters');
}

const login = await this.prisma.login.findUnique({
where: { hash: dto.hash },
});
if (!login) {
return returnError('Matrix login request not found', 418); // 418 I'm a teapot, for sdk to know to keep polling
}

// validate request
const secureHash = generateSecureHash(dto.hash, dto.secureNonce);
if (login.secureHash !== secureHash) {
return returnError('Invalid request, hash mismatch');
}
if (login.validUntil < new Date()) {
return returnError('Matrix login request expired');
}

// remove login request after fetching
await this.prisma.login.delete({ where: { hash: dto.hash } });

return returnSuccess({
message: 'Matrix login request fetched successfully',
data: login.data,
success: login.success,
});
}

// clear expired login requests every minute
@Cron(CronExpression.EVERY_5_MINUTES)
async clearExpiredLogins() {
await this.prisma.login.deleteMany({
where: {
validUntil: {
lte: new Date(),
},
},
});
}
}

0 comments on commit 63f6a46

Please sign in to comment.