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: signin and demo app #13

Merged
merged 5 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions apps/vc-api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,10 @@ TELEGRAM_BOT_USERNAME=<YOUR_TELEGRAM_BOT_USERNAME>

DISCORD_CLIENT_ID=<YOUR_DISCORD_CLIENT_ID>
DISCORD_CLIENT_SECRET=<YOUR_DISCORD_CLIENT_SECRET>

JWT_SECRET=<YOUR_JWT_SECRET>
SIWE_DOMAIN=<YOUR_SIWE_DOMAIN>
SIWE_ORIGIN=<YOUR_SIWE_ORIGIN>

ENCRYPT_KEY`=<YOUR_ENCRYPTION_PASSWORD>
ENCRYPT_SALT=<YOUR_ENCRYPTION_SALT>
9 changes: 6 additions & 3 deletions apps/vc-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,22 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"did-resolver": "4.1.0",
"ens-did-resolver": "^1.0.4",
"ethers": "^6.13.2",
"ethr-did-resolver": "^10.1.10",
"express": "4.20.0",
"moment": "^2.30.1",
"web-did-resolver": "^2.0.27",
"@nestjs/swagger": "^7.4.0",
"@veramo/did-manager": "^6.0.0",
"@veramo/key-manager": "^6.0.0",
"@veramo/kms-local": "^6.0.0",
"@veramo/did-provider-ethr": "^6.0.0",
"@veramo/did-resolver": "^6.0.0",
"@veramo/credential-w3c": "^6.0.0",
"@veramo/credential-ld": "^6.0.0"
"@veramo/credential-ld": "^6.0.0",
"@justaname.id/sdk": "^0.2.76",
"@nestjs/jwt": "^10.2.0",
"uuid": "^10.0.0",
"rxjs": "^7.8.0",
"vitest": "^1.3.1"
}
}
113 changes: 76 additions & 37 deletions apps/vc-api/src/api/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,90 @@
import { Controller, Get, Inject, Param, Query, Res } from '@nestjs/common';
import {
CREDENTIAL_CREATOR_FACADE,
ICredentialCreatorFacade
} from '../../core/applications/credentials/facade/icredential.facade';
import { Response } from 'express';
import { AuthCallbackApiResponse } from './auth.callback.response.api';
import { AUTH_CONTROLLER_MAPPER, IAuthControllerMapper } from './mapper/iauth.controller.mapper';
import { AuthGetAuthUrlRequestApiRequestParam } from './auth.get-auth-url.request.api';
import { Body, Controller, Get, Inject, Post, Req, Res, UseGuards } from '@nestjs/common';
import { JustaName } from '@justaname.id/sdk';
import { ENVIRONMENT_GETTER, IEnvironmentGetter } from '../../core/applications/environment/ienvironment.getter';
import { AuthSigninApiRequest } from './requests/auth.signin.api.request';
import { Response, Request} from 'express';
import { JwtService } from '@nestjs/jwt';
import moment from 'moment';
import { JwtGuard } from '../../guards/jwt.guard';

type Siwj = { address: string, subname: string };

@Controller('auth')
export class AuthController {

justaname: JustaName
constructor(
@Inject(CREDENTIAL_CREATOR_FACADE)
private readonly credentialCreatorFacade: ICredentialCreatorFacade,
@Inject(ENVIRONMENT_GETTER) private readonly environmentGetter: IEnvironmentGetter,

private readonly jwtService: JwtService
) {
this.justaname = JustaName.init({
config: {
chainId: this.environmentGetter.getChainId(),
domain: this.environmentGetter.getSiweDomain(),
origin:this.environmentGetter.getSiweOrigin(),
},
ensDomain: this.environmentGetter.getEnsDomain(),
providerUrl: (this.environmentGetter.getChainId() === 1 ? 'https://mainnet.infura.io/v3/' :'https://sepolia.infura.io/v3/') +this.environmentGetter.getInfuraProjectId()
})
}

@Get('nonce')
async getNonce() {
return this.justaname.signIn.generateNonce()
}

@Post('signin')
async signInChallenge(
@Body() body: AuthSigninApiRequest,
@Res() res: Response,
@Req() req: Request
) {
const { data: message, subname } = await this.justaname.signIn.signIn(body.message, body.signature)

if (!message) {
res.status(500).json({ message: 'No message returned.' });
return;
}

if (!message.expirationTime) {
res.status(500).json({ message: 'No expirationTime returned.' });
return;
}


@Inject(AUTH_CONTROLLER_MAPPER)
private readonly authControllerMapper: IAuthControllerMapper
) {}
const token = this.jwtService.sign({ subname, address: message.address }, {
expiresIn: moment(message.expirationTime).diff(moment(), 'seconds')
});


res.cookie('justanidtoken', token, { httpOnly: true, secure: true, sameSite: 'none' });

return res.status(200).send({ subname, address: message.address });

@Get('')
async welcomeToJustaNameVerifications(): Promise<string[]> {
return ['Welcome to JustaName Verifications! Please use the /auth/:authName endpoint to get started.']
}

@Get(':authName')
async getAuthUrl(
@Param() authGetAuthUrlRequestApi: AuthGetAuthUrlRequestApiRequestParam,
@UseGuards(JwtGuard)
@Get('session')
async getSession(
@Req() req: Request & { user: Siwj },
@Res() res: Response
): Promise<void> {
const redirect = await this.credentialCreatorFacade.getAuthUrl(authGetAuthUrlRequestApi.authName)
if(redirect.startsWith('http')) {
res.redirect(redirect)
return
) {
if (!req.user) {
res.status(401).json({ message: 'You have to first sign_in' });
return;
}
res.send(redirect)
res.setHeader('Content-Type', 'text/plain');
res.status(200).send({ subname: req.user?.subname, address: req.user?.address });
}

@Get(':authName/callback')
async callback(
@Param() authGetAuthUrlRequestApiParam: AuthGetAuthUrlRequestApiRequestParam,
@Query() authGetAuthUrlRequestApiQuery: any,
): Promise<AuthCallbackApiResponse> {
const verifiedEthereumEip712Signature2021 = await this.credentialCreatorFacade.callback(
this.authControllerMapper.mapAuthCallbackApiRequestToCredentialCallbackRequest(
authGetAuthUrlRequestApiQuery,
authGetAuthUrlRequestApiParam)
)
return this.authControllerMapper.mapCredentialCallbackResponseToAuthCallbackApiResponse(verifiedEthereumEip712Signature2021)
@UseGuards(JwtGuard)
@Get('signout')
async signOut(
@Req() req: Request,
@Res() res: Response
) {
res.clearCookie('justanidtoken');
res.status(200).send({ message: 'You have been signed out' });
}
}
12 changes: 12 additions & 0 deletions apps/vc-api/src/api/auth/requests/auth.signin.api.request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class AuthSigninApiRequest {
@ApiProperty()
@IsString()
message: string;

@ApiProperty()
@IsString()
signature: string;
}
115 changes: 115 additions & 0 deletions apps/vc-api/src/api/credentials/credentials.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { Controller, Get, Inject, Param, Query, Req, Res, Session, UseGuards } from '@nestjs/common';
import {
CREDENTIAL_CREATOR_FACADE,
ICredentialCreatorFacade
} from '../../core/applications/credentials/facade/icredential.facade';
import { Response } from 'express';
import { AUTH_CONTROLLER_MAPPER, IcredentialsControllerMapper } from './mapper/icredentials.controller.mapper';
import { CredentialsGetAuthUrlRequestApiRequestParam } from './credentials.get-auth-url.request.api';
import { v4 as uuidv4 } from 'uuid';
import { filter, Subject, take } from 'rxjs';
import { SubjectData } from './isubject.data';
import { JwtGuard } from '../../guards/jwt.guard';

type Siwj = { address: string, subname: string };

@Controller('credentials')
export class CredentialsController {

private authSubjects: Map<string, Subject<SubjectData>> = new Map();

constructor(
@Inject(CREDENTIAL_CREATOR_FACADE)
private readonly credentialCreatorFacade: ICredentialCreatorFacade,

@Inject(AUTH_CONTROLLER_MAPPER)
private readonly authControllerMapper: IcredentialsControllerMapper
) {}

@Get('')
async welcomeToJustaNameVerifications(): Promise<string[]> {
return ['Welcome to JustaName Verifications! Please use the /auth/:authName endpoint to get started.']
}

@UseGuards(JwtGuard)
@Get(':authName')
async getAuthUrl(
@Param() authGetAuthUrlRequestApi: CredentialsGetAuthUrlRequestApiRequestParam,
@Res() res: Response,
@Req() req: Request & { user: Siwj }
): Promise<any> {

const authId = uuidv4();
const subject = new Subject<SubjectData>();
this.authSubjects.set(authId, subject);

const redirectUrl = await this.credentialCreatorFacade.getAuthUrl(
authGetAuthUrlRequestApi.authName,
req.user?.subname,
authId
)


res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});

res.write(`data: ${JSON.stringify({ redirectUrl })}\n\n`);

subject.pipe(
filter(data => data.authId === authId),
take(1)
).subscribe(
(data) => {
res.write(`data: ${JSON.stringify(data.result)}\n\n`);
res.end();
this.authSubjects.delete(authId);
},
(error) => {
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
res.end();
this.authSubjects.delete(authId);
}
);
}

@Get(':authName/callback')
async callback(
@Param() authGetAuthUrlRequestApiParam: CredentialsGetAuthUrlRequestApiRequestParam,
@Query() authGetAuthUrlRequestApiQuery: any,
@Res() res: Response
): Promise<void> {
const verifiedEthereumEip712Signature2021 = await this.credentialCreatorFacade.callback(
this.authControllerMapper.mapAuthCallbackApiRequestToCredentialCallbackRequest(
authGetAuthUrlRequestApiQuery,
authGetAuthUrlRequestApiParam)
)

const { authId, dataKey, verifiableCredential} = verifiedEthereumEip712Signature2021

const subject = this.authSubjects.get(authId);
subject?.next({
authId,
result: {
verifiableCredential,
dataKey
}
});


res.send(`
<html>
<body>
<script>
window.close();
// In case window.close() doesn't work (e.g., in some browsers where the window wasn't opened by script)
document.body.innerHTML = "Authentication successful. You can close this tab.";
</script>
</body>
</html>
`);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ enum AuthName {
Telegram = 'telegram',
}

export class AuthGetAuthUrlRequestApiRequestParam
export class CredentialsGetAuthUrlRequestApiRequestParam
{
@IsNotEmpty()
@IsEnum(AuthName)
authName: AuthName
}

export class AuthGetAuthUrlApiRequestQuery
export class CredentialsGetAuthUrlApiRequestQuery
{
@IsOptional()
@IsString()
Expand Down
9 changes: 9 additions & 0 deletions apps/vc-api/src/api/credentials/isubject.data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { VerifiedEthereumEip712Signature2021 } from '../../core/domain/entities/eip712';

export interface SubjectData {
authId: string;
result: {
verifiableCredential: VerifiedEthereumEip712Signature2021;
dataKey: string;
};
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Injectable } from '@nestjs/common';
import { IAuthControllerMapper } from './iauth.controller.mapper';
import { AuthCallbackApiResponse } from '../auth.callback.response.api';
import { IcredentialsControllerMapper } from './icredentials.controller.mapper';
import { AuthCallbackApiResponse } from '../credentials.callback.response.api';
import { CredentialCallbackResponse } from '../../../core/applications/credentials/facade/credential.callback.response';
import { AuthGetAuthUrlApiRequestQuery, AuthGetAuthUrlRequestApiRequestParam } from '../auth.get-auth-url.request.api';
import { CredentialsGetAuthUrlApiRequestQuery, CredentialsGetAuthUrlRequestApiRequestParam } from '../credentials.get-auth-url.request.api';
import { CredentialCallbackRequest } from '../../../core/applications/credentials/facade/credential.callback.request';

@Injectable()
export class AuthControllerMapper implements IAuthControllerMapper {
export class CredentialsControllerMapper implements IcredentialsControllerMapper {

constructor() {}

Expand All @@ -28,8 +28,8 @@ export class AuthControllerMapper implements IAuthControllerMapper {
}

mapAuthCallbackApiRequestToCredentialCallbackRequest(
authCallbackApiRequestQuery: AuthGetAuthUrlApiRequestQuery,
authCallbackApiRequestParams: AuthGetAuthUrlRequestApiRequestParam
authCallbackApiRequestQuery: CredentialsGetAuthUrlApiRequestQuery,
authCallbackApiRequestParams: CredentialsGetAuthUrlRequestApiRequestParam
): CredentialCallbackRequest {
return {
credentialName: authCallbackApiRequestParams.authName,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { AuthCallbackApiResponse } from '../auth.callback.response.api';
import { AuthCallbackApiResponse } from '../credentials.callback.response.api';
import { CredentialCallbackResponse } from '../../../core/applications/credentials/facade/credential.callback.response';
import { AuthGetAuthUrlApiRequestQuery, AuthGetAuthUrlRequestApiRequestParam } from '../auth.get-auth-url.request.api';
import { CredentialsGetAuthUrlApiRequestQuery, CredentialsGetAuthUrlRequestApiRequestParam } from '../credentials.get-auth-url.request.api';
import { CredentialCallbackRequest } from '../../../core/applications/credentials/facade/credential.callback.request';

export const AUTH_CONTROLLER_MAPPER = 'AUTH_CONTROLLER_MAPPER';

export interface IAuthControllerMapper {
export interface IcredentialsControllerMapper {
mapCredentialCallbackResponseToAuthCallbackApiResponse(
credentialCallbackResponse: CredentialCallbackResponse,
): AuthCallbackApiResponse;

mapAuthCallbackApiRequestToCredentialCallbackRequest(
authCallbackApiRequestQuery: AuthGetAuthUrlApiRequestQuery,
authCallbackApiRequestParams: AuthGetAuthUrlRequestApiRequestParam
authCallbackApiRequestQuery: CredentialsGetAuthUrlApiRequestQuery,
authCallbackApiRequestParams: CredentialsGetAuthUrlRequestApiRequestParam
): CredentialCallbackRequest;
}
Loading
Loading