Skip to content

Commit

Permalink
feat!: Add Microservice (#181)
Browse files Browse the repository at this point in the history
* feat: Init microservice

* feat: Update request schemas

* docs: Update docs

* feat: Remove credential guard
  • Loading branch information
DaevMithran authored Mar 17, 2023
1 parent c9db4eb commit b08d460
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 278 deletions.
9 changes: 0 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,29 +33,20 @@ ARG NODE_ENV=production
ARG NPM_CONFIG_LOGLEVEL=warn
ARG PORT=8787
ARG ISSUER_ID
ARG ISSUER_ID_KID
ARG ISSUER_ID_METHOD
ARG ISSUER_ID_PUBLIC_KEY_HEX
ARG ISSUER_ID_PRIVATE_KEY_HEX
ARG ISSUER_ID_METHOD_SPECIFIC_ID
ARG COSMOS_PAYER_MNEMONIC
ARG NETWORK_RPC_URL
ARG AUTH0_SERVICE_ENDPOINT
ARG RESOLVER_URL="https://resolver.cheqd.net/1.0/identifiers"

# Run-time environment variables
ENV NODE_ENV ${NODE_ENV}
ENV NPM_CONFIG_LOGLEVEL ${NPM_CONFIG_LOGLEVEL}
ENV PORT ${PORT}
ENV ISSUER_ID ISSUER_ID ${ISSUER_ID}
ENV ISSUER_ID_KID ${ISSUER_ID_KID}
ENV ISSUER_ID_METHOD ${ISSUER_ID_METHOD}
ENV ISSUER_ID_PUBLIC_KEY_HEX ${ISSUER_ID_PUBLIC_KEY_HEX}
ENV ISSUER_ID_PRIVATE_KEY_HEX ${ISSUER_ID_PRIVATE_KEY_HEX}
ENV ISSUER_ID_METHOD_SPECIFIC_ID ${ISSUER_ID_METHOD_SPECIFIC_ID}
ENV COSMOS_PAYER_MNEMONIC ${COSMOS_PAYER_MNEMONIC}
ENV NETWORK_RPC_URL ${NETWORK_RPC_URL}
ENV AUTH0_SERVICE_ENDPOINT ${AUTH0_SERVICE_ENDPOINT}
ENV RESOLVER_URL ${RESOLVER_URL}

# We don't have the node_modules directory
Expand Down
20 changes: 9 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ The purpose of this service is to issue and verify credentials. This service by
- **Endpoint** POST `/api/credentials/issue`
- **Accepts**: `application/json`
- **Request Body**: JSON object with following fields
- `claim` - Claim received from the Auth0 Service
- `provider` - Auth0 login provider (eg: Twitter, Discord, Github, etc)
- `subjectId` - ID of the holder of the credential
- `attributes` - A json object with all the credential attributes
- `subjectDid` - DID of the holder of the credential
- `type` - A string representation of the credential type e.g. "PERSON" (optional)
- `@context` - context of the issued credential (optional)
- `expirationDate` - Date of expiration of the JWT (optional)
- **Success Response Code**: 200
- **Error Response Code** - 400

Expand All @@ -28,7 +30,7 @@ The purpose of this service is to issue and verify credentials. This service by
- **Endpoint** POST `/api/credentials/verify`
- **Accepts**: `application/json`
- **Request Body**: JSON object with following fields:
- `credential` - A verifiable credential
- `credential` - A verifiable credential or the JWT string
- **Success Response Code** - 200
- **Error Response Codes**:
- 400: Bad request body
Expand All @@ -55,13 +57,9 @@ The application expects the following environment variables to be defined for th

1. `ISSUER_ID_PRIVATE_KEY_HEX`: Hex-encoded private key to be used by the identity credential issuer
2. `ISSUER_ID_PUBLIC_KEY_HEX`: Hex-encoded public key to be used by the identity credential issuer
3. `ISSUER_ID_KID`: Key ID to match a specific key inside a JWK
4. `ISSUER_ID_METHOD`: `did:cheqd` method along with network namespace (e.g., `did:cheqd:mainnet:` or `did:cheqd:testnet:`)
5. `ISSUER_ID_METHOD_SPECIFIC_ID`: Unique identifier portion of a `did:cheqd` DID, e.g., `zAXwwqZzhCZA1L77ZBa8fhVNjL9MQCHX`
6. `ISSUER_ID`: Fully-qualified DID for the issuer, e.g., `did:cheqd:mainnet:zAXwwqZzhCZA1L77ZBa8fhVNjL9MQCHX`
7. `COSMOS_PAYER_MNEMONIC`: Mnemonic for the issuer's Cosmos account. This currently doesn't require any balances at the moment, but it required for the library to function.
8. `NETWORK_RPC_URL`: RPC URL for a node on cheqd network, e.g., `rpc.cheqd.net`
9. `AUTH0_SERVICE_ENDPOINT`: Auth0 service endpoint, is an instance of [Auth0 Service](https://github.com/cheqd/auth0-service)
3. `ISSUER_ID`: Fully-qualified DID for the issuer, e.g., `did:cheqd:mainnet:zAXwwqZzhCZA1L77ZBa8fhVNjL9MQCHX`
4. `COSMOS_PAYER_MNEMONIC`: Mnemonic for the issuer's Cosmos account. This currently doesn't require any balances at the moment, but it required for the library to function.
5. `NETWORK_RPC_URL`: Optional RPC URL for a node on cheqd network, e.g., `rpc.cheqd.net`

### Run

Expand Down
46 changes: 17 additions & 29 deletions src/controllers/credentials.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import type { Request, Response } from 'express'
import { GuardedCredentials } from '../middleware/guard'
import { applyMixins } from '../middleware/_'

import { Credentials } from '../services/credentials'
import { check, param, validationResult } from 'express-validator'
import { check, validationResult } from 'express-validator'

export class CredentialController {

public static issueValidator = [
check('subjectId').exists().withMessage('subjectId is required').isString().withMessage('subjectId should be a string'),
param('type').optional().isString().withMessage('type should be a string')
check('subjectDid')
.exists().withMessage('subjectDid is required')
.isString().withMessage('subjectDid should be a string')
.contains('did:').withMessage('subjectDid should be a DID'),
check('attributes')
.exists().withMessage('attributes are required')
.isObject().withMessage('attributes should be an object'),
check('type').optional().isArray().withMessage('type should be a string array'),
check('@context').optional().isArray().withMessage('@context should be a string array'),
check('expirationDate').optional().isDate().withMessage('Invalid expiration date')
]

public static verifyValidator = [
Expand All @@ -18,28 +25,10 @@ export class CredentialController {
public async issue(request: Request, response: Response) {
const result = validationResult(request);
if (!result.isEmpty()) {
return response.status(400).json(result.array()[0].msg)
return response.status(400).json({ error: result.array()[0].msg })
}

switch (request.params.type) {
case 'Ticket':
const body = request.body
return await Credentials.instance.issue_ticket_credential(body.data, body.subjectId)
default:
applyMixins(GuardedCredentials, [Credentials])

const credentials = new GuardedCredentials()

const { authenticated, user, subjectId, provider, error } = await credentials.guard(request)

if (!(authenticated)) {
return response.status(400).json({
message: JSON.stringify(error)
})
}
const verifiable_credential = await credentials.issue_person_credential(user, provider, subjectId)
return response.status(200).json(verifiable_credential)
}

response.json(await Credentials.instance.issue_credential(request.body))
}

public async verify(request: Request, response: Response) {
Expand All @@ -49,11 +38,10 @@ export class CredentialController {

const result = validationResult(request);
if (!result.isEmpty()) {
return response.status(400).json(result.array()[0].msg)
return response.status(400).json({ error: result.array()[0].msg })
}

const verificationResult = await Credentials.instance.verify_credentials(request.body.credential)
return response.json(verificationResult)
return response.json(await Credentials.instance.verify_credentials(request.body.credential))
}

}
12 changes: 0 additions & 12 deletions src/middleware/_.ts

This file was deleted.

32 changes: 0 additions & 32 deletions src/middleware/guard.ts

This file was deleted.

15 changes: 0 additions & 15 deletions src/router.ts

This file was deleted.

141 changes: 47 additions & 94 deletions src/services/credentials.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import {
createAgent, IDataStore, IDIDManager, IKeyManager, IResolver, IVerifyResult, TAgent, W3CVerifiableCredential
createAgent,
IDataStore,
IDIDManager,
IKeyManager,
IResolver,
IVerifyResult,
TAgent,
W3CVerifiableCredential
} from '@veramo/core'
import { CredentialPlugin } from '@veramo/credential-w3c'
import { AbstractIdentifierProvider, DIDManager, MemoryDIDStore } from '@veramo/did-manager'
Expand All @@ -8,27 +15,33 @@ import { KeyManager, MemoryKeyStore, MemoryPrivateKeyStore } from '@veramo/key-m
import { KeyManagementSystem } from '@veramo/kms-local'
import { Resolver, ResolverRegistry } from 'did-resolver'
import { CheqdDIDProvider, getResolver as CheqdDidResolver } from '@cheqd/did-provider-cheqd'
import { NetworkType } from '@cheqd/did-provider-cheqd/src/did-manager/cheqd-did-provider'
import { VC_CONTEXT, VC_EVENTRESERVATION_CONTEXT, VC_PERSON_CONTEXT, VC_PROOF_FORMAT, VC_REMOVE_ORIGINAL_FIELDS, VC_TYPE, } from '../types/constants'
import { CredentialPayload, CredentialSubject, GenericAuthUser, VerifiableCredential, WebPage, Credential } from '../types/types'

import { VC_CONTEXT, VC_PROOF_FORMAT, VC_REMOVE_ORIGINAL_FIELDS, VC_TYPE } from '../types/constants'
import { CredentialPayload, CredentialRequest, VerifiableCredential, Credential } from '../types/types'
import { Identity } from './identity'

require('dotenv').config()

const {
ISSUER_ID,
COSMOS_PAYER_MNEMONIC,
NETWORK_RPC_URL,
EVENT_CONTEXT,
PERSON_CONTEXT,
RESOLVER_URL,
DISCORD_RESOURCE_ID,
GITHUB_RESOURCE_ID,
EVENTBRITE_RESOURCE_ID,
TWITTER_RESOURCE_ID,
IIW_LOGO_RESOURCE_ID
ISSUER_ID,
COSMOS_PAYER_MNEMONIC,
NETWORK_RPC_URL,
} = process.env

export enum DefaultRPCUrl {
Mainnet = 'https://rpc.cheqd.net',
Testnet = 'https://rpc.cheqd.network'
}

export enum NetworkType {
Mainnet = "mainnet",
Testnet = "testnet"
}

export enum DefaultResolverUrl {
Cheqd = "https://resolver.cheqd.net"
}

export class Credentials {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
agent: TAgent<any>
Expand Down Expand Up @@ -64,7 +77,7 @@ export class Credentials {
defaultKms: 'local',
cosmosPayerSeed: COSMOS_PAYER_MNEMONIC,
networkType: network,
rpcUrl: NETWORK_RPC_URL,
rpcUrl: NETWORK_RPC_URL || (network === NetworkType.Testnet ? DefaultRPCUrl.Testnet : DefaultRPCUrl.Mainnet),
}
) as AbstractIdentifierProvider
}
Expand All @@ -79,93 +92,33 @@ export class Credentials {
})
}

getThumbnailURL(provider: string): string | undefined {
switch (provider) {
case 'twitter':
return `${RESOLVER_URL}/${ISSUER_ID}/resources/${TWITTER_RESOURCE_ID}`
case 'discord':
return `${RESOLVER_URL}/${ISSUER_ID}/resources/${DISCORD_RESOURCE_ID}`
case 'github':
return `${RESOLVER_URL}/${ISSUER_ID}/resources/${GITHUB_RESOURCE_ID}`
case 'eventbrite':
return `${RESOLVER_URL}/${ISSUER_ID}/resources/${EVENTBRITE_RESOURCE_ID}`
}
}

async issue_person_credential(user: GenericAuthUser, provider: string, subjectId?: string): Promise<Credential> {
provider = provider.toLowerCase()
const credential_subject: CredentialSubject = {
id: subjectId,
type: undefined
}

const webpage: WebPage = {
'@type': 'ProfilePage',
description: provider,
name: `${user?.nickname}` ?? '<unknown>',
identifier: `@${user?.nickname}` ?? '<unknown>',
lastReviewed: user?.updated_at,
thumbnailUrl: this.getThumbnailURL(provider),
}

if (provider == 'github' || provider == 'twitter') {
webpage.URL = `https://${provider.toLowerCase()}.com/${user?.nickname}`
}

const credential = {
'@context': VC_CONTEXT.concat(PERSON_CONTEXT ? [PERSON_CONTEXT] : VC_PERSON_CONTEXT),
type: ['Person', VC_TYPE],
issuanceDate: new Date().toISOString(),
credentialSubject: credential_subject,
'WebPage': [webpage]
}

return await this.issue_credentials(credential)
}

async issue_ticket_credential(reservationId: string, subjectId?: string): Promise<Credential> {
const credential_subject: CredentialSubject = {
id: subjectId,
type: undefined
}

const credential = {
'@context': VC_CONTEXT.concat(EVENT_CONTEXT ? [EVENT_CONTEXT] : VC_EVENTRESERVATION_CONTEXT),
type: ['EventReservation', VC_TYPE],
issuanceDate: new Date().toISOString(),
credentialSubject: credential_subject,
reservationId,
reservationStatus: 'https://schema.org/ReservationConfirmed',
provider: {
brand: 'EventBrite',
image: this.getThumbnailURL('eventbrite')
},
reservationFor: {
'@type': 'Event',
name: 'Internet Identity Workshop IIWXXXV',
startDate: "2022-11-16T16:00:00",
endDate: "2022-11-18T00:00:00",
location: "Computer History Museum, 1401 N Shoreline Blvd, Mountain View, CA 94043",
logo: `${RESOLVER_URL}/${ISSUER_ID}/resources/${IIW_LOGO_RESOURCE_ID}`
}
}

return await this.issue_credentials(credential)
}

async issue_credentials(credential: CredentialPayload): Promise<Credential> {
async issue_credential(request: CredentialRequest): Promise<Credential> {

if (!this.agent) this.init_agent()

const identity_handler = new Identity(
this.agent,
'demo'
)

const issuer_id = await identity_handler.load_issuer_did(
this.agent as TAgent<any>
)
credential.issuer = { id: issuer_id.did }

const credential: CredentialPayload = {
'@context': [ ...request['@context'] || [], ...VC_CONTEXT ],
type: [ ...request.type || [], VC_TYPE ],
issuer: { id: issuer_id.did },
credentialSubject: {
id: request.subjectDid,
type: undefined
},
issuanceDate: new Date().toISOString(),
...request.attributes
}

if(request.expirationDate) {
credential.expirationDate = request.expirationDate
}

this.agent = identity_handler.agent

Expand All @@ -188,7 +141,7 @@ export class Credentials {
return verifiable_credential
}

async verify_credentials(credential: W3CVerifiableCredential): Promise<IVerifyResult> {
async verify_credentials(credential: W3CVerifiableCredential | string): Promise<IVerifyResult> {
const result = await this.agent?.execute(
'verifyCredential',
{
Expand Down
Loading

0 comments on commit b08d460

Please sign in to comment.