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

Improve editor for issuer and verifier #95

Merged
merged 14 commits into from
Sep 1, 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
8 changes: 6 additions & 2 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,13 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# build the docker image for keycloak
# build the docker image for keycloak and publish it to the GitHub Container Registry
- name: Build Keycloak Docker Image
run: cd deploys/keycloak && docker compose build keycloak && docker compose push keycloak
run: |
cd deploys/keycloak &&
docker compose pull keycloak &&
docker compose build keycloak &&
docker compose push keycloak
# add the release, build the container and release it with the information for sentry
- name: Build and push images
Expand Down
13 changes: 11 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,15 @@ jobs:
run: npx playwright install --with-deps

- name: Build keycloak
run: cd deploys/keycloak && docker compose build keycloak
run: |
cd deploys/keycloak &&
docker compose pull keycloak &&
docker compose build keycloak

- name: Add entry to /etc/hosts
run: echo "127.0.0.1 host.testcontainers.internal" | sudo tee -a /etc/hosts

- name: Lint, test, build, e2e
- name: Lint, test, container, e2e
run: INPUT_GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} pnpm exec nx affected -t lint test container e2e
# comment out since the current e2e tests do not produce any artifacts
# - name: Upload coverage
Expand Down Expand Up @@ -76,6 +79,12 @@ jobs:
path: tmp/logs
retention-days: 30

# validate if the .env.example files in the deploys folder are up to date and that the containers can be health checked and started
- name: Validate deploy environment
run:
cd deploys &&
./test.sh

- name: Check if testcontainer logs exist
id: check_testcontainer_logs
run: echo "exists=$(if [ -d tmp/logs ]; then echo true; else echo false; fi)" >> $GITHUB_ENV
2 changes: 2 additions & 0 deletions .sonarcloud.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Exclusions
sonar.exclusions=libs/verifier-shared/src/lib/api/** libs/issuer-shared/src/lib/api/** libs/holder-shared/src/lib/api/**
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
[![CD](https://github.com/openwallet-foundation-labs/credhub/actions/workflows/cd.yml/badge.svg)](https://github.com/openwallet-foundation-labs/credhub/actions/workflows/cd.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=openwallet-foundation-labs_credhub&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=openwallet-foundation-labs_credhub) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://raw.githubusercontent.com/openwallet-foundation/credo-ts/main/LICENSE) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](https://www.typescriptlang.org/)

# credhub

credhub is comprehensive monorepo including a cloud wallet for natural persons together with a minimal issuer and verifier service. The cloud wallet will host all credentials and key pairs, including the business logic to receive and present credentials.

# Getting Started

Documentation on how to get started with credhub can be found at [https://credhub.eu](https://credhub.eu)

# Virtual meetings

There is a bi weekly virtual meeting to discuss the progress of the project. You can get a calendar invite [here](https://zoom-lfx.platform.linuxfoundation.org/meeting/93045942637?password=2c738e22-bb7b-44a7-aab1-e98fa7fc82f6)

# Contributing

If you would like to contribute to the project, please read our [contributing guide](./CONTRIBUTING.md).

# License

This project is licensed under the [Apache License Version 2.0 (Apache-2.0).](./LICENSE)
4 changes: 4 additions & 0 deletions apps/demo/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
FROM docker.io/nginx:stable-alpine

# Install wget
RUN apk --no-cache add wget

COPY dist/apps/demo/* /usr/share/nginx/html/
RUN echo "server {" > /etc/nginx/conf.d/default.conf && \
echo " listen 80;" >> /etc/nginx/conf.d/default.conf && \
Expand Down
22 changes: 17 additions & 5 deletions apps/holder-app-e2e/src/credentials.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { faker } from '@faker-js/faker';
import { test, expect, Page } from '@playwright/test';
import { getConfig, register } from './helpers';
import axios from 'axios';
import axios, { AxiosError } from 'axios';
import { GlobalConfig } from '../global-setup';

export const username = faker.internet.email();
Expand Down Expand Up @@ -48,17 +48,24 @@ async function getAxiosInstance(port: number) {

async function receiveCredential(pin = false) {
const axios = await getAxiosInstance(config.issuerPort);
const templates = await axios
.get('/templates')
.then((response) => response.data);
const credentialId = templates.find(
(template: any) => template.name === 'Identity'
).id;

const response = await axios
.post(`/sessions`, {
credentialSubject: {
prename: 'Max',
surname: 'Mustermann',
},
credentialId: 'Identity',
credentialId,
pin,
})
.catch((e) => {
console.log(e);
.catch((e: AxiosError) => {
console.log(JSON.stringify(e.response?.data, null, 2));
throw Error('Failed to create session');
});
const uri = response.data.uri;
Expand Down Expand Up @@ -98,8 +105,13 @@ test('issuance with pin', async () => {

test('verify credential', async () => {
await receiveCredential();
const credentialId = 'Identity';
const axios = await getAxiosInstance(config.verifierPort);
const templates = await axios
.get('/templates')
.then((response) => response.data);
const credentialId = templates.find(
(template: any) => template.value.name === 'Identity'
).id;
let uri = '';
try {
const response = await axios.post(`/siop/${credentialId}`);
Expand Down
3 changes: 3 additions & 0 deletions apps/holder-app/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
FROM docker.io/nginx:stable-alpine

# Install wget
RUN apk --no-cache add wget

# Copy application files and the startup script with permissions
COPY dist/apps/holder-app/* /usr/share/nginx/html/
COPY --chmod=755 apps/holder-app/startup.sh /usr/local/bin/startup.sh
Expand Down
28 changes: 26 additions & 2 deletions apps/holder-app/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import {
isDevMode,
ErrorHandler,
} from '@angular/core';
import { Router, provideRouter } from '@angular/router';
import {
RouteReuseStrategy,
Router,
provideRouter,
withRouterConfig,
} from '@angular/router';

import { routes } from './app.routes';
import {
Expand All @@ -27,6 +32,24 @@ import { provideServiceWorker } from '@angular/service-worker';
import * as Sentry from '@sentry/angular';
import { environment } from '../environments/environment';

class MyStrategy implements RouteReuseStrategy {
shouldDetach() {
return false;
}
store() {
return null;
}
shouldAttach() {
return false;
}
retrieve() {
return null;
}
shouldReuseRoute() {
return false;
}
}

Sentry.init({
dsn: environment.sentryDsn,
enabled: !isDevMode(),
Expand All @@ -45,11 +68,12 @@ Sentry.init({

export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideRouter(routes, withRouterConfig({ onSameUrlNavigation: 'reload' })),
provideAnimations(),
provideOAuthClient(),
provideHttpClient(withInterceptorsFromDi()),
importProvidersFrom(ApiModule),
{ provide: RouteReuseStrategy, useClass: MyStrategy },
{ provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: { hasBackdrop: true } },
{
provide: APP_INITIALIZER,
Expand Down
2 changes: 1 addition & 1 deletion apps/holder-backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ OIDC_AUTH_URL=http://host.docker.internal:8080
OIDC_REALM=wallet
OIDC_PUBLIC_CLIENT_ID=wallet
OIDC_ADMIN_CLIENT_ID=wallet-admin
OIDC_ADMIN_CLIENT_SECRET=secret
OIDC_ADMIN_CLIENT_SECRET=kwpCrguxUOn9gump77E0B3vAkiOhW8eL

# DB config
# DB_TYPE=postgres
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
import {
Body,
ConflictException,
Controller,
Delete,
Get,
Param,
Post,
Query,
UseGuards,
} from '@nestjs/common';
import {
ApiBody,
ApiOAuth2,
ApiOperation,
ApiQuery,
ApiTags,
} from '@nestjs/swagger';
import { ApiOAuth2, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
import { AuthGuard, AuthenticatedUser } from 'nest-keycloak-connect';
import { CredentialsService } from './credentials.service';
import { CreateCredentialDto } from './dto/create-credential.dto';
import { CredentialResponse } from './dto/credential-response.dto';
import { KeycloakUser } from '../auth/user';

Expand All @@ -29,16 +20,6 @@ import { KeycloakUser } from '../auth/user';
export class CredentialsController {
constructor(private readonly credentialsService: CredentialsService) {}

@ApiOperation({ summary: 'store a credential' })
@ApiBody({ type: CreateCredentialDto })
@Post()
create(
@Body() createCredentialDto: CreateCredentialDto,
@AuthenticatedUser() user: KeycloakUser
) {
return this.credentialsService.create(createCredentialDto, user.sub);
}

@ApiOperation({ summary: 'get all credentials' })
@ApiQuery({ name: 'archive', required: false, type: Boolean })
@Get()
Expand All @@ -52,7 +33,7 @@ export class CredentialsController {
);
return credentials.map((credential) => ({
id: credential.id,
display: credential.metaData.display[0],
display: credential.metaData.display?.[0],
issuer: credential.issuer,
}));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { firstValueFrom } from 'rxjs';
import { Verifier } from '@sd-jwt/types';
import { JWK, JWTPayload } from '@sphereon/oid4vci-common';
import { CryptoService, ResolverService } from '@credhub/backend';
import { getListFromStatusListJWT } from '@sd-jwt/jwt-status-list';

type DateKey = 'exp' | 'nbf';
@Injectable()
Expand Down Expand Up @@ -119,7 +118,7 @@ export class CredentialsService {
return this.instance
.decode(credential)
.then((vc) =>
vc.jwt.payload[key] ? (vc.jwt.payload[key] as number) : undefined
vc.jwt.payload[key] ? (vc.jwt.payload[key] as number) * 1000 : undefined
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ export class Oid4vciService {
const sdjwtvc = await this.sdjwt.decode(
credentialResponse.credential as string
);
//TODO: also save the reference to the credential metadata. This will allow use to render the credential later. Either save the metadata or save a reference so it can be loaded on demand.
const credentialEntry = await this.credentialsService.create(
{
value: credentialResponse.credential as string,
Expand Down
17 changes: 11 additions & 6 deletions apps/issuer-backend/src/app/issuer/issuer-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class IssuerDataService {
this.metadata.credential_issuer = this.configSerivce.get('ISSUER_BASE_URL');

this.metadata.credential_configurations_supported =
this.templatesService.getSupported(await this.templatesService.listAll());
await this.templatesService.getSupported();
}

/**
Expand All @@ -51,18 +51,23 @@ export class IssuerDataService {

/**
* Returns the disclosure frame of the credential with the given id, throws an error if the credential is not supported.
* @param id
* @param vct
* @returns
*/
async getDisclosureFrame(id: string) {
async getDisclosureFrame(vct: string) {
if (this.configSerivce.get('CONFIG_RELOAD')) {
this.loadConfig();
}
const credential = await this.templatesService.getOne(id);
//becuase the vct is stored in a json field, we will need to fetch all elements and then filter
const credential = await this.templatesService
.listAll()
.then((templates) =>
templates.find((template) => template.value.schema.vct === vct)
);
if (!credential) {
throw new Error(`The credential with the id ${id} is not supported.`);
throw new Error(`The credential with the id ${vct} is not supported.`);
}
return credential.sd;
return credential.value.sd;
}

/**
Expand Down
25 changes: 21 additions & 4 deletions apps/issuer-backend/src/app/issuer/issuer.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@ import {
NotFoundException,
Param,
Post,
Query,
UseGuards,
} from '@nestjs/common';
import { IssuerService } from './issuer.service';
import { SessionRequestDto } from './dto/session-request.dto';
import { ApiOAuth2, ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiOAuth2, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
import { AuthGuard } from 'nest-keycloak-connect';
import { SessionResponseDto } from './dto/session-response.dto';
import { CredentialOfferSession } from './dto/credential-offer-session.dto';
import { DBStates } from '@credhub/relying-party-shared';
import { CredentialsService } from '../credentials/credentials.service';
import { SessionEntryDto } from './dto/session-entry.dto';
import { CredentialOfferPayloadV1_0_13 } from '@sphereon/oid4vci-common';

@UseGuards(AuthGuard)
@ApiOAuth2([])
Expand All @@ -29,12 +31,27 @@ export class IssuerController {
) {}

@ApiOperation({ summary: 'Lists all sessions' })
@ApiQuery({ name: 'configId', required: false })
@Get()
async listAll(): Promise<CredentialOfferSession[]> {
async listAll(
@Query('configId') configId?: string
): Promise<CredentialOfferSession[]> {
return (
this.issuerService.vcIssuer
.credentialOfferSessions as DBStates<CredentialOfferSession>
).all();
)
.all()
.then((entries) => {
if (configId) {
return entries.filter((entry) =>
(
entry.credentialOffer
.credential_offer as CredentialOfferPayloadV1_0_13
).credential_configuration_ids.includes(configId)
);
}
return entries;
});
}

@ApiOperation({ summary: 'Returns the status for a session' })
Expand All @@ -50,7 +67,7 @@ export class IssuerController {
const credentials = await this.credentialsService.getBySessionId(id);
return {
session,
credentials: credentials,
credentials,
};
}

Expand Down
Loading