Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Switchboard): DID user authentication and registration prototype #2497

Draft
wants to merge 45 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
2ff018f
adjusted IJWTPayload
artursudnik May 17, 2021
17f99eb
auth/login-did endpoint added handled temporarily with 'did' login st…
artursudnik May 20, 2021
cd2b51c
jwt passport strategy populates user.rights if access token contains …
artursudnik May 20, 2021
0c7ddc2
filter out DID token roles not listed in the process.env.ACCEPTED_ROLES
artursudnik May 21, 2021
df2dd5b
synchronize user.rights record to DID access token roles
artursudnik May 24, 2021
7c6e09e
set user.password as nullable
artursudnik May 21, 2021
c9326a0
extend User entity with a nullable did string field
artursudnik May 25, 2021
c9c7fab
added user/register-did endpoint
artursudnik May 25, 2021
73d601b
jwt-basic passport strategy used to protect user/register-did endpoint
artursudnik May 25, 2021
8bd089d
UI: added iam-client-lib dependency
artursudnik May 28, 2021
768b530
iam-client-lib upgraded to 3.0.0-alpha.6
artursudnik May 31, 2021
6e5d461
frontend iam-client-lib integration
artursudnik May 31, 2021
e14a361
"Login with DID and Metamask" button prototype
artursudnik May 31, 2021
0db4845
upgraded iam-client-lib to 3.0.0-alpha.17
artursudnik Jun 8, 2021
1992f36
mapping lowercase chain role names to origin camel-case role names
artursudnik Jun 8, 2021
ffb0713
Merge branch 'master' into feat/sb-integration-prototype-user
artursudnik Jun 8, 2021
de232fa
fix: incorrect characters case handling when checking accepted roles
artursudnik Jun 10, 2021
8493375
Merge remote-tracking branch 'origin/master' into feat/sb-integration…
artursudnik Jun 10, 2021
00ec059
tests: a structure of not-implemented tests
artursudnik Jun 11, 2021
a2afb44
upgraded iam-client-lib to 3.0.0-alpha.20
artursudnik Jun 11, 2021
aeb1ef1
fix: incorrect comment
artursudnik Jun 14, 2021
6a50838
chore: UserService findByDid method
artursudnik Jun 14, 2021
37fb482
chore: JWT strategy validation finds a user record based on did field…
artursudnik Jun 15, 2021
74c14c5
tests: multiple tests implemented by simulating actual access token
artursudnik Jun 15, 2021
6bc5071
tests: fixed expected response status and body
artursudnik Jun 15, 2021
98ba083
gave up the idea of accepted roles
artursudnik Jun 15, 2021
f3a1993
chore: implemented check if DID user already exist before creating a …
artursudnik Jun 15, 2021
f6308db
TODO
artursudnik Jun 15, 2021
6fa974c
fix: security - did value should be taken from an access token not a …
artursudnik Jun 15, 2021
1f13966
tests: added more access token validations
artursudnik Jun 15, 2021
4cb6a39
TODO
artursudnik Jun 16, 2021
d574b5c
tests: implemented test for password field null value for DID users
artursudnik Jun 16, 2021
d5285e0
Merge remote-tracking branch 'origin/master' into feat/sb-integration…
artursudnik Jun 17, 2021
1d0021f
chore: pnpm-lock.yaml updated
artursudnik Jun 17, 2021
258a8fe
chore: upgraded iam-client-lib to the 3.0.0-alpha.21 version
artursudnik Jun 17, 2021
0b300fc
chore: added user/did-roles GET endpoint
artursudnik Jun 17, 2021
c83cfdd
chore: fixed iam-client-lib versions mismatch
artursudnik Jun 18, 2021
d436002
chore: setting DID user role explicitly in user.controller
artursudnik Jun 18, 2021
06c9a32
tests: added more test scenarios, refactoring
artursudnik Jun 18, 2021
adff9f9
Merge remote-tracking branch 'origin/master' into feat/sb-integration…
artursudnik Jun 21, 2021
3aaec3a
fix: incorrect types conversion in tests
artursudnik Jun 21, 2021
9ea4775
docs: SB integration TODO document
artursudnik Jun 21, 2021
ea3a494
chore: added [email protected] to the origin-backend
artursudnik Jun 22, 2021
2d0961d
chore: integrated passport-did-auth login strategy into auth/login-di…
artursudnik Jun 22, 2021
f4ac1a8
tests: cleanup
artursudnik Jun 22, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,077 changes: 2,226 additions & 851 deletions common/config/rush/pnpm-lock.yaml

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions packages/origin-backend-core/src/DidUserRegistrationData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
import { Transform } from 'class-transformer';
import { Role } from './User';

export class DidUserRegistrationData {
@IsNotEmpty()
@IsString()
title: string;

@IsNotEmpty()
@IsString()
firstName: string;

@IsNotEmpty()
@IsString()
lastName: string;

@IsEmail()
@Transform((value: string) => value.toLowerCase())
email: string;

@IsNotEmpty()
@IsString()
did: string;

@IsNotEmpty()
@IsString()
telephone: string;

role: Role;
}
1 change: 1 addition & 0 deletions packages/origin-backend-core/src/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface IUserProperties {
firstName: string;
lastName: string;
email: string;
did?: string;
telephone: string;
notifications: boolean;
rights: number;
Expand Down
1 change: 1 addition & 0 deletions packages/origin-backend-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export * from './OrganizationInvitation';
export * from './OriginConfiguration';
export * from './User';
export * from './UserRegistrationData';
export * from './DidUserRegistrationData';
export * from './queries';
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class UserPasswordNullable1621942514841 implements MigrationInterface {
name = 'UserPasswordNullable1621942514841';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "password" DROP NOT NULL`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "password" SET NOT NULL`);
}
}
19 changes: 19 additions & 0 deletions packages/origin-backend/migrations/1621942557906-UserDidField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class UserDidField1621942557906 implements MigrationInterface {
name = 'UserDidField1621942557906';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" ADD "did" character varying`);
await queryRunner.query(
`ALTER TABLE "user" ADD CONSTRAINT "UQ_7d4ee7205853cfea0f68240b589" UNIQUE ("did")`
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user" DROP CONSTRAINT "UQ_7d4ee7205853cfea0f68240b589"`
);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "did"`);
}
}
7 changes: 5 additions & 2 deletions packages/origin-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
"reflect-metadata": "0.1.13",
"rxjs": "6.6.7",
"typeorm": "0.2.34",
"uuid": "8.3.2"
"uuid": "8.3.2",
"passport-did-auth": "1.0.0-alpha.13"
},
"devDependencies": {
"typescript": "4.3.4",
Expand Down Expand Up @@ -98,7 +99,9 @@
"shx": "0.3.3",
"chai": "4.3.0",
"@types/chai": "4.2.15",
"ts-node": "9.1.1"
"ts-node": "9.1.1",
"iam-client-lib": "3.0.0-alpha.21",
"jsonwebtoken": "8.5.1"
},
"files": [
"dist",
Expand Down
22 changes: 20 additions & 2 deletions packages/origin-backend/src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { Controller, Request, Post, UseGuards, HttpCode, HttpStatus } from '@nestjs/common';
import {
Controller,
Request,
Response,
Post,
UseGuards,
HttpCode,
HttpStatus
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Request as ExpressRequest } from 'express';
import { Request as ExpressRequest, Response as ExpressResponse } from 'express';
import { IUser } from '@energyweb/origin-backend-core';

import { ApiBearerAuth, ApiBody, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthService } from './auth/auth.service';
import { LoginReturnDataDTO } from './auth/login-return-data.dto';
import { LoginDataDTO } from './auth/login-data.dto';
import { LoginDataDidDTO } from './auth/login-data-did.dto';

@ApiTags('auth')
@ApiBearerAuth('access-token')
Expand All @@ -22,4 +31,13 @@ export class AppController {
async login(@Request() req: ExpressRequest): Promise<LoginReturnDataDTO> {
return this.authService.login(req.user as Omit<IUser, 'password'>);
}

@UseGuards(AuthGuard('did'))
@Post('auth/login-did')
@HttpCode(HttpStatus.OK)
@ApiBody({ type: LoginDataDidDTO })
@ApiResponse({ status: HttpStatus.OK, type: LoginReturnDataDTO, description: 'Log in (DID)' })
async loginDid(@Request() req: ExpressRequest, @Response() res: ExpressResponse) {
return res.send({ accessToken: req.user });
}
}
4 changes: 3 additions & 1 deletion packages/origin-backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { ConfigService } from '@nestjs/config';
import { UserModule } from '../pods/user/user.module';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { DidStrategy } from './did.strategy';
import { JwtStrategy } from './jwt.strategy';
import { JwtBasicStrategy } from './jwt-basic.strategy';

@Global()
@Module({
Expand All @@ -21,7 +23,7 @@ import { JwtStrategy } from './jwt.strategy';
inject: [ConfigService]
})
],
providers: [AuthService, LocalStrategy, JwtStrategy],
providers: [AuthService, LocalStrategy, DidStrategy, JwtStrategy, JwtBasicStrategy],
exports: [AuthService, PassportModule, JwtModule]
})
export class AuthModule {}
36 changes: 33 additions & 3 deletions packages/origin-backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import bcrypt from 'bcryptjs';
import { IUser, UserLoginReturnData } from '@energyweb/origin-backend-core';
import { IUser, UserLoginReturnData, Role, LoggedInUser } from '@energyweb/origin-backend-core';

import { UserService } from '../pods/user/user.service';

export interface IJWTPayload {
id: number;
email: string;
id?: number;
did?: string;
email?: string;
verifiedRoles?: { name: string; nameSpace: string }[];
}

@Injectable()
Expand All @@ -34,4 +36,32 @@ export class AuthService {
accessToken: this.jwtService.sign(payload)
};
}

/** TODO: this is a simulation of logging in with the passport-did-auth LoginStrategy
* parked until https://energyweb.atlassian.net/browse/SWTCH-949 is solved
*/

async loginDid(user: Omit<IUser, 'password'>): Promise<UserLoginReturnData> {
const loggedInUser = new LoggedInUser(user);

const rights: { name: string; nameSpace: string }[] = [];

Object.values(Role).forEach((role: Role) => {
if (loggedInUser.hasRole(role)) {
const roleName = Role[role].toString().toLowerCase();
rights.push({ name: roleName, nameSpace: `${roleName}.bogus.iat.ewc` });
}
});

const payload: IJWTPayload = {
email: user.email,
id: user.id,
//TODO: DID - this is going to be provided by passport-did-auth LoginStrategy
verifiedRoles: rights
};

return {
accessToken: this.jwtService.sign(payload)
};
}
}
22 changes: 22 additions & 0 deletions packages/origin-backend/src/auth/did.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { LoginStrategy } from 'passport-did-auth';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';

@Injectable()
export class DidStrategy extends PassportStrategy(LoginStrategy, 'did') {
constructor() {
super({
jwtSecret: process.env.JWT_SECRET,
jwtSignOptions: {
expiresIn: process.env.JWT_EXPIRY_TIME
},
rpcUrl: process.env.RPC_URL || 'https://volta-rpc.energyweb.org/',
cacheServerUrl:
process.env.CACHE_SERVER_URL || 'https://identitycache-dev.energyweb.org/',
acceptedRoles: [],
privateKey:
process.env.DEPLOY_KEY ||
'9945c05be0b1b7b35b7cec937e78c6552ecedca764b53a772547d94a687db929'
});
}
}
28 changes: 28 additions & 0 deletions packages/origin-backend/src/auth/jwt-basic.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, Inject } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

import { UserService } from '../pods/user/user.service';
import { IJWTPayload } from './auth.service';

@Injectable()
export class JwtBasicStrategy extends PassportStrategy(Strategy, 'jwt-basic') {
constructor(
@Inject(ConfigService) configService: ConfigService,
private readonly userService: UserService
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET')
});
}

async validate(payload: IJWTPayload): Promise<IJWTPayload> {
if (!payload.did) {
return null;
}
return payload;
}
}
44 changes: 40 additions & 4 deletions packages/origin-backend/src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,21 @@ import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, Inject } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { IUser } from '@energyweb/origin-backend-core';
import { IUser, Role } from '@energyweb/origin-backend-core';

import { UserService } from '../pods/user/user.service';
import { IJWTPayload } from './auth.service';

const chainToOriginRoleNamesMap: { [index: string]: string } = Object.keys(Role)
.map((key: keyof typeof Role) => Role[key])
.filter((value) => typeof value === 'string')
.reduce((acc: { [index: string]: string }, val) => {
const originRoleName = val.toString();
const chainRoleName = originRoleName.toLowerCase();
acc[chainRoleName] = originRoleName;
return acc;
}, {});

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
Expand All @@ -21,12 +31,38 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
}

async validate(payload: IJWTPayload): Promise<IUser> {
const user = await this.userService.findByEmail(payload.email);
let user;

if (user) {
if (payload.verifiedRoles) {
user = await this.userService.findByDid(payload.did);
} else {
user = await this.userService.findByEmail(payload.email);
return user;
}

return null;
if (!user) {
return null;
}

const roles: Role[] = payload.verifiedRoles
.filter((role) => Role[chainToOriginRoleNamesMap[role.name] as keyof typeof Role]) // only roles that are recognized by the Origin
.map((role) => chainToOriginRoleNamesMap[role.name])
.map((roleName: keyof typeof Role) => Role[roleName]);

// TODO: design and implement functionality onboarding users and organizaitons
// then, based on it implement filtering roles used to build user.rights value

let rights = roles.reduce((acc, role) => {
return acc | role;
}, 0);

if (user.rights !== rights) {
// if DID roles are changed on Switchboard, the Origin DB record needs to be synchronized
await this.userService.changeRole(user.id, ...roles);
}

user.rights = rights;

return user;
}
}
9 changes: 9 additions & 0 deletions packages/origin-backend/src/auth/login-data-did.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';

export class LoginDataDidDTO {
@ApiProperty({ type: String })
@IsString()
@IsNotEmpty()
identityToken: string;
}
10 changes: 10 additions & 0 deletions packages/origin-backend/src/pods/user/dto/register-did-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { PickType } from '@nestjs/swagger';
import { UserDTO } from './user.dto';

export class RegisterDidUserDTO extends PickType(UserDTO, [
'title',
'firstName',
'lastName',
'email',
'telephone'
] as const) {}
Loading