Skip to content
This repository has been archived by the owner on Jan 13, 2025. It is now read-only.

Commit

Permalink
Feat/webauthn (#71)
Browse files Browse the repository at this point in the history
* fix: add webauthn for testing

Signed-off-by: Mirko Mollik <mirko.mollik@fit.fraunhofer.de>

* fix: add webauthn keys for verification

Signed-off-by: Mirko Mollik <mirkomollik@gmail.com>

* fix: set correct config

Signed-off-by: Mirko Mollik <mirko.mollik@fit.fraunhofer.de>

---------

Signed-off-by: Mirko Mollik <mirko.mollik@fit.fraunhofer.de>
Signed-off-by: Mirko Mollik <mirkomollik@gmail.com>
  • Loading branch information
cre8 authored Jun 26, 2024
1 parent f7e63c6 commit 3c75342
Show file tree
Hide file tree
Showing 28 changed files with 1,389 additions and 249 deletions.
2 changes: 1 addition & 1 deletion apps/demo/public/assets/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"issuerUrl": "http://localhost:3001",
"verifierUrl": "http://localhost:3002",
"credentialId": "Identity",
"tokenEndpoint": "http://localhost:8080/realms/wallet/protocol/openid-connect/token",
"tokenEndpoint": "http://host.docker.internal:8080/realms/wallet/protocol/openid-connect/token",
"clientId": "relying-party",
"clientSecret": "hA0mbfpKl8wdMrUxr2EjKtL5SGsKFW5D"
}
1 change: 1 addition & 0 deletions apps/demo/src/app/eid/eid.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export class EidComponent implements OnDestroy {
{ pin: false }
);
this.qrCodeIssueField.setValue(res.uri);
this.issuanceUrl = res.uri;
this.qrCodeIssueImage = await qrcode.toDataURL(res.uri);
//TODO: show pin
this.issuerService.statusEvent.subscribe((status) => {
Expand Down
3 changes: 3 additions & 0 deletions apps/holder-backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ OIDC_ADMIN_CLIENT_SECRET=secret

DB_TYPE=sqlite
DB_NAME=tmp/holder-db.sqlite

WEBAUTHN_RP_NAME=Cloud wallet
WEBAUTHN_RP_ID=localhost
2 changes: 2 additions & 0 deletions apps/holder-backend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import { AppController } from './app.controller';
import { SettingsModule } from './settings/settings.module';
import { ScheduleModule } from '@nestjs/schedule';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { WEBAUTHN_VALIDATION_SCHEMA } from './auth/webauthn/webauthn.service';

@Module({
imports: [
ConfigModule.forRoot({
validationSchema: Joi.object({
...OIDC_VALIDATION_SCHEMA,
...WEBAUTHN_VALIDATION_SCHEMA,
...KEY_VALIDATION_SCHEMA,
...DB_VALIDATION_SCHEMA,
}),
Expand Down
11 changes: 9 additions & 2 deletions apps/holder-backend/src/app/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import {
OIDC_CLIENT_SCHEMA,
OidcClientModule,
} from './oidc-client/oidcclient.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Passkey } from './webauthn/entities/passkey.entity';
import { WebAuthnController } from './webauthn/webauthn.controller';
import { WebauthnService } from './webauthn/webauthn.service';

export const OIDC_VALIDATION_SCHEMA = {
OIDC_AUTH_URL: Joi.string().required(),
Expand Down Expand Up @@ -44,6 +48,8 @@ export const OIDC_VALIDATION_SCHEMA = {
} as KeycloakConnectOptions),
}),
OidcClientModule.forRoot(),
TypeOrmModule.forFeature([Passkey]),
ConfigModule,
],
providers: [
{
Expand All @@ -59,8 +65,9 @@ export const OIDC_VALIDATION_SCHEMA = {
useClass: RoleGuard,
},
AuthService,
WebauthnService,
],
exports: [KeycloakConnectModule],
controllers: [AuthController],
exports: [KeycloakConnectModule, WebauthnService],
controllers: [AuthController, WebAuthnController],
})
export class AuthModule {}
1 change: 1 addition & 0 deletions apps/holder-backend/src/app/auth/user.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export class KeycloakUser {
sub: string;
email: string;
[key: string]: unknown;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {
AuthenticationExtensionsClientOutputs,
AuthenticationResponseJSON,
AuthenticatorAssertionResponseJSON,
AuthenticatorAttachment,
} from '@simplewebauthn/types';

export class AuthenticationResponse implements AuthenticationResponseJSON {
id: string;
rawId: string;
response: AuthenticatorAssertionResponseJSON;
authenticatorAttachment?: AuthenticatorAttachment;
clientExtensionResults: AuthenticationExtensionsClientOutputs;
type: 'public-key';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {
AuthenticationExtensionsClientOutputs,
AuthenticatorAttachment,
AuthenticatorAttestationResponseJSON,
RegistrationResponseJSON,
} from '@simplewebauthn/types';

export class RegistrationResponse implements RegistrationResponseJSON {
id: string;
rawId: string;
response: AuthenticatorAttestationResponseJSON;
authenticatorAttachment?: AuthenticatorAttachment;
clientExtensionResults: AuthenticationExtensionsClientOutputs;
type: 'public-key';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
BeforeInsert,
BeforeUpdate,
Column,
Entity,
PrimaryColumn,
} from 'typeorm';
import type {
AuthenticatorTransportFuture,
CredentialDeviceType,
Base64URLString,
} from '@simplewebauthn/types';

@Entity()
export class Passkey {
// SQL: Store as `TEXT`. Index this column
@PrimaryColumn()
id: Base64URLString;
// SQL: Store raw bytes as `BYTEA`/`BLOB`/etc...
// Caution: Node ORM's may map this to a Buffer on retrieval,
// convert to Uint8Array as necessary
@Column()
publicKey: string;

// SQL: Foreign Key to an instance of your internal user model
@Column()
user: string;
// SQL: Store as `TEXT`. Index this column. A UNIQUE constraint on
// (webAuthnUserID + user) also achieves maximum user privacy
@Column()
webauthnUserID: Base64URLString;
// SQL: Consider `BIGINT` since some authenticators return atomic timestamps as counters
@Column({ type: 'bigint' })
counter: number;
// SQL: `VARCHAR(32)` or similar, longest possible value is currently 12 characters
// Ex: 'singleDevice' | 'multiDevice'
@Column()
deviceType: CredentialDeviceType;
// SQL: `BOOL` or whatever similar type is supported
@Column({ type: 'boolean' })
backedUp: boolean;
// SQL: `VARCHAR(255)` and store string array as a CSV string
// Ex: ['ble' | 'cable' | 'hybrid' | 'internal' | 'nfc' | 'smart-card' | 'usb']
@Column({ nullable: true, type: 'json' })
transports?: AuthenticatorTransportFuture[];
}
66 changes: 66 additions & 0 deletions apps/holder-backend/src/app/auth/webauthn/webauthn.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Req,
UseGuards,
} from '@nestjs/common';
import { ApiOAuth2, ApiOperation, ApiTags } from '@nestjs/swagger';
import { AuthGuard, AuthenticatedUser } from 'nest-keycloak-connect';
import { WebauthnService } from './webauthn.service';
import { KeycloakUser } from '../user';
import { RegistrationResponse } from './dto/registration-response.dto';
import { Request } from 'express';

@UseGuards(AuthGuard)
@ApiOAuth2([])
@ApiTags('auth')
@Controller('webauthn')
export class WebAuthnController {
constructor(private webAuthnService: WebauthnService) {}

@ApiOperation({ summary: 'get registration options' })
@Get('registration')
getRegistrationOptions(@AuthenticatedUser() user: KeycloakUser) {
return this.webAuthnService.generateRegistrationOptions(
user.sub,
user.email
);
}

@ApiOperation({ summary: 'complete registration' })
@Post('registration')
verifyRegistration(
@AuthenticatedUser() user: KeycloakUser,
@Body() body: RegistrationResponse,
@Req() req: Request
) {
const expectedOrigin = req.headers.origin;
return this.webAuthnService.startRegistration(
user.sub,
body,
expectedOrigin
);
}

@ApiOperation({ summary: 'get keys' })
@Get('keys')
getKeys(@AuthenticatedUser() user: KeycloakUser) {
return this.webAuthnService.getKeys(user.sub);
}

@ApiOperation({ summary: 'delete key' })
@Delete('keys/:id')
deleteKey(@AuthenticatedUser() user: KeycloakUser, @Param('id') id: string) {
return this.webAuthnService.deleteKey(user.sub, id);
}

@ApiOperation({ summary: 'get authentication options' })
@Get('authentication')
getAuthenticationOptions(@AuthenticatedUser() user: KeycloakUser) {
return this.webAuthnService.generateAuthenticationOptions(user.sub);
}
}
Loading

0 comments on commit 3c75342

Please sign in to comment.