Skip to content

Commit

Permalink
Full flow working with ethereum signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
r-marques committed Apr 11, 2022
1 parent 440e94b commit 83995ac
Show file tree
Hide file tree
Showing 13 changed files with 27,959 additions and 14,205 deletions.
41,873 changes: 27,726 additions & 14,147 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@nestjs/platform-express": "^8.4.1",
"@nestjs/swagger": "^5.2.0",
"@nestjs/typeorm": "^8.0.3",
"@nevermined-io/nevermined-sdk-js": "^0.19.12",
"@sideway/address": "^4.1.3",
"@sideway/formula": "^3.0.0",
"@sideway/pinpoint": "^2.0.0",
Expand All @@ -43,6 +44,7 @@
"cli-color": "^2.0.1",
"dotenv": "^16.0.0",
"joi": "^17.6.0",
"jose": "^4.6.0",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21",
"moment": "^2.24.0",
Expand All @@ -57,12 +59,14 @@
"ts-node": "^10.7.0",
"tsconfig-paths": "^3.13.0",
"typeorm": "^0.2.45",
"typescript": "^4.6.2"
"typescript": "^4.6.2",
"web3": "^1.7.1"
},
"devDependencies": {
"@faker-js/faker": "^6.0.0-beta.0",
"@nestjs/cli": "^8.2.2",
"@nestjs/testing": "^8.4.0",
"@truffle/hdwallet-provider": "^2.0.5",
"@types/jest": "^27.4.1",
"@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.179",
Expand Down Expand Up @@ -127,4 +131,4 @@
},
"author": "Keyko",
"license": "ISC"
}
}
9 changes: 4 additions & 5 deletions src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { jwtConstants } from './constants';
import { JwtStrategy } from './jwt.strategy';
import { LocalStrategy } from './local.strategy';

describe('AuthController', () => {
let controller: AuthController;
let authController: AuthController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
Expand All @@ -21,14 +20,14 @@ describe('AuthController', () => {
signOptions: { expiresIn: '60m' }
})
],
providers: [AuthService, LocalStrategy, JwtStrategy],
providers: [AuthService, JwtStrategy],
controllers: [AuthController]
}).compile();

controller = module.get<AuthController>(AuthController);
authController = module.get<AuthController>(AuthController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
expect(authController).toBeDefined();
});
});
13 changes: 8 additions & 5 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
import { Controller, Post, UseGuards, Request, Get } from '@nestjs/common';
import { Controller, Post, UseGuards, Request, Get, Param, Body } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Public } from './auth.decorator';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { JwtAuthGuard } from './jwt-auth.guard';
import { LocalAuthGuard } from './local-auth.guard';

@ApiTags('Auth')
@Controller()
export class AuthController {
constructor(private authService: AuthService) { }
// eslint-disable-next-line @typescript-eslint/require-await
@Public()
@UseGuards(LocalAuthGuard)
@Post('login')
async login(@Request() req) {
async login(
@Body('client_assertion_type') clientAssertionType: string,
@Body('client_assertion') clientAssertion: string,
): Promise<LoginDto> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return this.authService.login(req.user);
return this.authService.validateClaim(clientAssertionType, clientAssertion);
}

// Test endpoint
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
console.log(req.user);
return req.user;
}
}
3 changes: 1 addition & 2 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from '../users/users.module';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { AuthController } from './auth.controller';
import { jwtConstants } from './constants';
import { JwtModule } from '@nestjs/jwt';
Expand All @@ -17,7 +16,7 @@ import { JwtStrategy } from './jwt.strategy';
signOptions: { expiresIn: '60m' }
})
],
providers: [AuthService, LocalStrategy, JwtStrategy],
providers: [AuthService, JwtStrategy],
controllers: [AuthController]
})
export class AuthModule { }
33 changes: 31 additions & 2 deletions src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { Test, TestingModule } from '@nestjs/testing';
import { decodeJwt } from 'jose';
import { UsersModule } from '../users/users.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { jwtConstants } from './constants';
import { JwtStrategy } from './jwt.strategy';
import { LocalStrategy } from './local.strategy';
import HDWalletProvider from '@truffle/hdwallet-provider';
import { Account } from '@nevermined-io/nevermined-sdk-js';
import { EthSignJWT } from './jwt.utils';
import Web3 from 'web3';

describe('AuthService', () => {
let service: AuthService;
Expand All @@ -21,7 +25,7 @@ describe('AuthService', () => {
signOptions: { expiresIn: '60m' }
})
],
providers: [AuthService, LocalStrategy, JwtStrategy],
providers: [AuthService, JwtStrategy],
controllers: [AuthController]
}).compile();

Expand All @@ -31,4 +35,29 @@ describe('AuthService', () => {
it('should be defined', () => {
expect(service).toBeDefined();
});

it('should get a token with web3 signing', async () => {
const seedphrase = 'taxi music thumb unique chat sand crew more leg another off lamp';
const provider = new HDWalletProvider(seedphrase, 'http://localhost:8545', 0, 10);
const address: string = provider.getAddresses()[0];
const account = new Account(address);
const web3 = new Web3(provider);
const clientAssertion = await new EthSignJWT({
iss: web3.utils.toChecksumAddress(account.getId())
})
.setProtectedHeader({ alg: 'ES256K' })
.setIssuedAt()
.setExpirationTime('60m')
.ethSign(account, web3);

const result = await service.validateClaim(
'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
clientAssertion
);
expect(result).toHaveProperty('access_token');

const payload = decodeJwt(result.access_token);
expect(payload.iss).toEqual(web3.utils.toChecksumAddress(account.getId()));
});

});
37 changes: 23 additions & 14 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
import { Injectable } from '@nestjs/common';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import { JWTPayload } from 'jose';
import { CLIENT_ASSERTION_TYPE } from './constants';
import { LoginDto } from './dto/login.dto';
import { jwtEthVerify } from './jwt.utils';

@Injectable()
export class AuthService {
constructor(private usersService: UsersService, private jwtService: JwtService) { }
constructor(private jwtService: JwtService) { }

async validateUser(username: string, pass: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user && user.password === pass) {
const { password, ...result } = user;
return result;
// RFC-7523 Client Authentication https://datatracker.ietf.org/doc/html/rfc7523#section-2.2
// RFC-8812 ECDSA Signature with secp256k1 Curve (ES256K)
// https://www.rfc-editor.org/rfc/rfc8812#name-ecdsa-signature-with-secp25
// This implementation is different from the standard in:
// - the size of the signature. ethereum adds an extra byte to the signature to help
// with recovering the public key that create the signature
// - the hash function used. ES256K uses sha-256 while ethereum uses keccak
async validateClaim(clientAssertionType: string, clientAssertion: string): Promise<LoginDto> {
if (clientAssertionType !== CLIENT_ASSERTION_TYPE) {
throw new UnauthorizedException('Invalid "client_assertion_type"');
}

return null;
}

async login(user: any) {
const payload = { username: user.username, sub: user.userId };
let payload: JWTPayload;
try {
payload = await jwtEthVerify(clientAssertion);
} catch (error) {
throw new UnauthorizedException(`The 'client_assertion' is invalid: ${error.message}`);
}
return {
access_token: this.jwtService.sign(payload)
access_token: this.jwtService.sign({ iss: payload.iss })
};
}
}
4 changes: 3 additions & 1 deletion src/auth/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const jwtConstants = {
secret: 'secretKey'
};
};

export const CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
11 changes: 11 additions & 0 deletions src/auth/dto/login.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsString } from "class-validator";

export class LoginDto {
@ApiProperty({
description: `The Authorization Bearer token`,
example: '{"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjIyIn0.eyJpc3Mi[...omitted for brevity...]"}'
})
@IsString()
access_token: string;
}
2 changes: 1 addition & 1 deletion src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
// eslint-disable-next-line @typescript-eslint/require-await
async validate(payload: any) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
return { userId: payload.sub, username: payload.username };
return { address: payload.iss };
}
}

0 comments on commit 83995ac

Please sign in to comment.