Skip to content

Commit

Permalink
Merge pull request #18 from cre8/cre8/issue15
Browse files Browse the repository at this point in the history
Improve issuer and verifier
  • Loading branch information
cre8 authored May 1, 2024
2 parents ae77e0f + 2de95aa commit a67cde4
Show file tree
Hide file tree
Showing 15 changed files with 645 additions and 32 deletions.
4 changes: 1 addition & 3 deletions apps/holder/projects/pwa/src/app/login/login.component.html
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
<button mat-raised-button color="primary" (click)="authService.login()">
Login
</button>
<button mat-raised-button color="primary" (click)="login()">Login</button>
4 changes: 4 additions & 0 deletions apps/holder/projects/pwa/src/app/login/login.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ import { MatButtonModule } from '@angular/material/button';
})
export class LoginComponent {
constructor(public authService: AuthService) {}

login() {
this.authService.login('credentials');
}
}
1 change: 1 addition & 0 deletions apps/issuer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-list-routes": "^1.2.1",
"joi": "^17.13.0",
"jose": "^5.2.4",
"passport-azure-ad": "^4.3.5",
"passport-http-bearer": "^1.0.1",
Expand Down
22 changes: 22 additions & 0 deletions apps/issuer/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Joi from 'joi';
import 'dotenv/config';

/**
* Define the environment variables schema
*/
const envVarsSchema = Joi.object()
.keys({
PORT: Joi.number().default(3000),
CONFIG_RELOAD: Joi.boolean().default(false),
ISSUER_BASE_URL: Joi.string().required(),
NODE_ENVIRONMENT: Joi.string()
.valid('development', 'production')
.default('development'),
})
.unknown();

const { error, value: envVars } = envVarsSchema.validate(process.env);

if (error) {
throw new Error(`Config validation error: ${error.message}`);
}
16 changes: 15 additions & 1 deletion apps/issuer/src/issuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { CredentialSchema } from './types.js';

/**
* The issuer class is responsible for managing the credentials and the metadata of the issuer.
* In case the CONFIG_REALOD environment variable is set, the issuer will reload the configuration every time a method is called.
*/
export class Issuer {
/**
* The metadata of the issuer.
*/
private metadata: CredentialIssuerMetadataOpts;
private metadata!: CredentialIssuerMetadataOpts;

/**
* The credentials supported by the issuer.
Expand All @@ -21,6 +22,10 @@ export class Issuer {
* Creates a new instance of the issuer.
*/
constructor() {
this.loadConfig();
}

private loadConfig() {
//instead of reading at the beginning, we could implement a read on demand.
this.metadata = JSON.parse(
readFileSync(join('templates', 'metadata.json'), 'utf-8')
Expand Down Expand Up @@ -54,6 +59,9 @@ export class Issuer {
* @returns
*/
getCredential(id: string) {
if (process.env.CONFIG_RELOAD) {
this.loadConfig();
}
const credential = this.credentials.get(id);
if (!credential) {
throw new Error(`The credential with the id ${id} is not supported.`);
Expand All @@ -67,6 +75,9 @@ export class Issuer {
* @returns
*/
getDisclosureFrame(id: string) {
if (process.env.CONFIG_RELOAD) {
this.loadConfig();
}
const credential = this.credentials.get(id);
if (!credential) {
throw new Error(`The credential with the id ${id} is not supported.`);
Expand All @@ -78,6 +89,9 @@ export class Issuer {
* Returns the metadata of the issuer.
*/
getMetadata() {
if (process.env.CONFIG_RELOAD) {
this.loadConfig();
}
return this.metadata;
}
}
3 changes: 2 additions & 1 deletion apps/issuer/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import './config.js';
import { ES256, digest, generateSalt } from '@sd-jwt/crypto-nodejs';
import { SDJwtVcInstance } from '@sd-jwt/sd-jwt-vc';
import {
Expand All @@ -18,7 +19,7 @@ import {
import { OID4VCIServer } from '@sphereon/oid4vci-issuer-server';
import { SdJwtDecodedVerifiableCredentialPayload } from '@sphereon/ssi-types';
import { DIDDocument } from 'did-resolver';
import 'dotenv/config';

import expressListRoutes from 'express-list-routes';
import {
JWK,
Expand Down
2 changes: 1 addition & 1 deletion apps/issuer/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": ["src/**/*"]
"include": ["src/**/*", "src/config.ts"]
}
1 change: 1 addition & 0 deletions apps/verifier/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-list-routes": "^1.2.1",
"joi": "^17.13.0",
"jose": "^5.2.4",
"passport-azure-ad": "^4.3.5",
"passport-http-bearer": "^1.0.1",
Expand Down
45 changes: 40 additions & 5 deletions apps/verifier/src/RPManager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ES256, digest } from '@sd-jwt/crypto-nodejs';
import {
EcdsaSignature,
InMemoryRPSessionManager,
JWK,
JWTPayload,
PassBy,
Expand All @@ -19,7 +18,7 @@ import {
} from '@sphereon/did-auth-siop';
import { JWkResolver, encodeDidJWK } from './did.js';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { join, normalize, sep } from 'node:path';
import { VerifierRP } from './types.js';
import { SDJwtVcInstance } from '@sd-jwt/sd-jwt-vc';
import { KbVerifier, Verifier } from '@sd-jwt/types';
Expand All @@ -29,6 +28,7 @@ import { importJWK, jwtVerify } from 'jose';
import { getKeys, getPublicKey } from './keys.js';
import { EventEmitter } from 'node:events';
import { RPInstance } from './types.js';
import { InMemoryRPSessionManager } from './session-manager.js';

// load the keys
const { privateKey, publicKey } = await getKeys();
Expand All @@ -40,7 +40,9 @@ export const kid = did;
// create the event emitter to listen to events.
export const eventEmitter = new EventEmitter();
//TODO: implement a persistant session manager so reloads don't lose state
export const sessionManager = new InMemoryRPSessionManager(eventEmitter);
export const sessionManager = new InMemoryRPSessionManager(eventEmitter, {
// maxAgeInSeconds: 10,
});

/**
* The RPManager is responsible for managing the relying parties.
Expand All @@ -58,14 +60,47 @@ export class RPManager {
let rp = this.rp.get(id);
if (!rp) {
rp = this.buildRP(id);
if (process.env.CONFIG_RELOAD) {
// checks every minute if the rp has active sessions. If there is none, the rp is removed. We want to do this so we can update the rp with new input without losing state. This approach could be improved since we are waiting around 4 minutes for the last finished request until the entries are removed.
setInterval(async () => {
this.remove(id);
}, 1000 * 60);
}
this.rp.set(id, rp);
}
return rp;
}

/**
* Removes a relying party. This is useful when the instance should be restarted with a new definition.
* @param id
*/
async remove(id: string, force = false) {
const rp = this.rp.get(id);
if (!rp) {
return;
}
if (
!force &&
//the limit for a session is 5 minutes, so after this a session becomes idle an can be removed.
!(await (rp.rp.sessionManager as InMemoryRPSessionManager).isIdle())
) {
// we have active sessions, we don't want to remove the rp. But at this point we do not know if they have already finished it. We just know they are not over the maximum defined limit (default 5 minutes).
return;
}
this.rp.delete(id);
console.log('Removed the rp');
}

// create the relying party
private buildRP(id: string) {
// create the relying party
const verifierFile = readFileSync(join('templates', `${id}.json`), 'utf-8');
// escape potential path traversal attacks
const safeId = normalize(id).split(sep).pop();
// instead of reading a file, we could pass a storage reference. Then the storage can be implemented in different ways, like using a database or a file system.
const verifierFile = readFileSync(
join('templates', `${safeId}.json`),
'utf-8'
);
if (!verifierFile) {
throw new Error(`The verifier with the id ${id} is not supported.`);
}
Expand Down
22 changes: 22 additions & 0 deletions apps/verifier/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Joi from 'joi';
import 'dotenv/config';

/**
* Define the environment variables schema
*/
const envVarsSchema = Joi.object()
.keys({
PORT: Joi.number().default(3000),
CONFIG_RELOAD: Joi.boolean().default(false),
VERIFIER_BASE_URL: Joi.string().required(),
NODE_ENVIRONMENT: Joi.string()
.valid('development', 'production')
.default('development'),
})
.unknown();

const { error, value: envVars } = envVarsSchema.validate(process.env);

if (error) {
throw new Error(`Config validation error: ${error.message}`);
}
14 changes: 13 additions & 1 deletion apps/verifier/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import './config.js';
import {
PresentationDefinitionLocation,
SupportedVersion,
} from '@sphereon/did-auth-siop';
import 'dotenv/config';
import expressListRoutes from 'express-list-routes';
import { v4 } from 'uuid';
import { expressSupport } from './server.js';
Expand Down Expand Up @@ -87,6 +87,18 @@ expressSupport.express.post(
}
);

// only set this when reload is activated
if (process.env.CONFIG_RELOAD) {
/**
* This will remove a rp so it can be reloaded with new values
*/
expressSupport.express.delete('/siop/:rp', async (req, res) => {
const rpId = req.params.rp;
await rpManager.remove(rpId, true);
res.send();
});
}

expressSupport.express.get('/health', async (req, res) => {
res.send('ok');
});
Expand Down
Loading

0 comments on commit a67cde4

Please sign in to comment.