Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* @forgerock/javascript-sdk
*
* autoscript.ts
*
* Copyright (c) 2020 ForgeRock. All rights reserved.
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
// @ts-nocheck
import * as forgerock from '@forgerock/javascript-sdk';
import { delay as rxDelay, map, mergeMap } from 'rxjs/operators';
import { fromEvent } from 'rxjs';

function autoscript() {
const delay = 0;

const url = new URL(window.location.href);
const amUrl = url.searchParams.get('amUrl') || 'https://auth.example.com:9443/am';
const realmPath = url.searchParams.get('realmPath') || 'root';
const un = url.searchParams.get('un') || 'sdkuser';
const pw = url.searchParams.get('pw') || 'password';
const tree = url.searchParams.get('tree') || 'PasswordlessWebAuthn';
console.log('Configure the SDK');
forgerock.Config.set({
realmPath,
tree,
serverConfig: {
baseUrl: amUrl,
},
});

try {
forgerock.SessionManager.logout();
} catch (err) {
// Do nothing
}
/***
* Test Device Registration (Not Automated)
* must create cert because you cannot have a TLS error with webauthn to test manually
*/
/***
* Test Device Registration (Not Automated)
* must create cert because you cannot have a TLS error with webauthn to test manually
*/
console.log('Click the device registration button!');
const deviceRegistration = document.querySelector('.device-registration');
fromEvent(deviceRegistration, 'click')
.pipe(
mergeMap(() => {
console.log('Initiate first step with `undefined`');
return forgerock.FRAuth.next();
}),
rxDelay(delay),
mergeMap((step) => {
console.log('Set username on auth tree callback');
console.log(step);
step.getCallbackOfType('NameCallback').setName(un);
return forgerock.FRAuth.next(step);
}),
rxDelay(delay),
mergeMap((step) => {
console.log('Set password on auth tree callback');
step.getCallbackOfType('PasswordCallback').setPassword(pw);
return forgerock.FRAuth.next(step);
}),
mergeMap(async (step) => {
const webAuthnStep = forgerock.FRWebAuthn.getWebAuthnStepType(step);
if (webAuthnStep === 2) {
console.log('WebAuthn step is registration');
} else {
throw new Error('WebAuthn step is incorrectly identified');
}
console.log('Handle WebAuthn Registration');
try {
step = await forgerock.FRWebAuthn.register<'mydevice'>(step, 'mydevice');
// ensure the step here has the 'mydevice' name at the end of the value. (outcome)
const deviceValue = step.getCallbacksOfType('HiddenValueCallback')[0].getInputValue();
if (!deviceValue.includes('mydevice')) {
throw new Error('Device name is not correct');
}
} catch (err) {
console.log(err);
}
return forgerock.FRAuth.next(step);
}),
map((step) => {
console.log('step', step);
if (step.payload.status === 401) {
throw new Error('Auth_Error');
} else if (step.payload.tokenId) {
console.log('Basic login successful.');
document.body.innerHTML = '<p class="Logged_In">Login successful</p>';
return step;
} else {
throw new Error('Something went wrong.');
}
}),
)
.subscribe({
error: (err) => {
console.log(`Error: ${err.message}`);
document.body.innerHTML = `<p class="Test_Complete">${err.message}</p>`;
},
complete: () => {
console.log('Test script complete');
document.body.innerHTML = `<p class="Test_Complete">Test script complete</p>`;
},
});
}

autoscript();
export default autoscript;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<title>E2E Test | ForgeRock JavaScript SDK</title>

<!-- Needed only for automation with mock server -->
<meta name="referrer" content="unsafe-url" />

<link rel="shortcut icon" href="/fr-ico.png" type="image/png" />

<style>
@media (prefers-color-scheme: dark) {
html {
background-color: black;
color: white;
}
a {
color: lightblue;
}
a:visited {
color: lavender;
}
a:hover {
color: lightskyblue;
}
}
</style>
</head>

<body>
<!-- script src="/_polyfills/fast-text-encoder.js"></script -->
<button class="device-registration">Device Registration</button>

<script src="autoscript.js"></script>
</body>
</html>
2 changes: 2 additions & 0 deletions e2e/autoscript-apps/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
<a href="./authn-social-login-am/index.html">AuthN: Social Login AM</a><br />
<a href="./authn-social-login-idm/index.html">AuthN: Social Login IDM</a><br />
<a href="./authn-webauthn/index.html">AuthN: WebAuthn</a><br />
<a href="./authn-webauthn-device-registration/index.html">AuthN: WebAuthN Device Registration</a
><br />
<a href="./authz-token/index.html">AuthZ: Token</a><br />
<a href="./authz-tree-basic/index.html">AuthZ: Tree-based with Basic Login</a><br />
<a href="./authz-tree-oauth/index.html">AuthZ: Tree-based with OAuth</a><br />
Expand Down
1 change: 1 addition & 0 deletions e2e/autoscript-apps/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const pages = [
'authn-social-login-am',
'authn-social-login-idm',
'authn-webauthn',
'authn-webauthn-device-registration',
'authz-token',
'authz-tree-basic',
'authz-txn-basic',
Expand Down
5 changes: 4 additions & 1 deletion e2e/mock-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@
},
"keywords": [],
"author": "",
"license": "ISC"
"license": "ISC",
"devDependencies": {
"@types/superagent": "^4.1.16"
}
}
4 changes: 3 additions & 1 deletion e2e/mock-api/src/app/app.certs.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ const __dirname = path.dirname(__filename);

const cert = readFileSync(path.resolve(__dirname, '../../../node_modules/lws/ssl/lws-cert.pem'));
const key = readFileSync(path.resolve(__dirname, '../../../node_modules/lws/ssl/private-key.pem'));

// for local testing
// const cert = readFileSync(path.resolve(__dirname, '../../../cert.pem'));
// const key = readFileSync(path.resolve(__dirname, '../../../key.pem'));
export { cert, key };
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/javascript-sdk/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"build": {
"executor": "@nrwl/rollup:rollup",
"options": {
"compiler": "babel",
"compiler": "tsc",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so typechecking happens on build, was an oversight by me.

"entryFile": "packages/javascript-sdk/src/index.ts",
"outputPath": "dist/packages/javascript-sdk",
"assets": [
Expand Down
65 changes: 50 additions & 15 deletions packages/javascript-sdk/src/fr-webauthn/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
parseRelyingPartyId,
} from './helpers';
import {
AttestationType,
RelyingParty,
WebAuthnAuthenticationMetadata,
WebAuthnCallbacks,
Expand All @@ -29,6 +30,15 @@ import {
import TextOutputCallback from '../fr-auth/callbacks/text-output-callback';
import { parseWebAuthnAuthenticateText, parseWebAuthnRegisterText } from './script-parser';

// <clientdata>::<attestation>::<publickeyCredential>::<DeviceName>
type OutcomeWithName<
ClientId extends string,
Attestation extends AttestationType,
PubKeyCred extends PublicKeyCredential,
Name = '',
> = Name extends infer P extends string
? `${ClientId}::${Attestation}::${PubKeyCred['id']}${P extends '' ? '' : `::${P}`}`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

realistically, i thought a simpler type could be used, i have no idea why i couldn't use a simpler type, it only worked when I went this route.

: never;
// JSON-based WebAuthn
type WebAuthnMetadata = WebAuthnAuthenticationMetadata | WebAuthnRegistrationMetadata;
// Script-based WebAuthn
Expand Down Expand Up @@ -92,7 +102,7 @@ abstract class FRWebAuthn {
public static async authenticate(step: FRStep): Promise<FRStep> {
const { hiddenCallback, metadataCallback, textOutputCallback } = this.getCallbacks(step);
if (hiddenCallback && (metadataCallback || textOutputCallback)) {
let outcome: string;
let outcome: ReturnType<typeof this.getAuthenticationOutcome>;

try {
let publicKey: PublicKeyCredentialRequestOptions;
Expand Down Expand Up @@ -127,17 +137,22 @@ abstract class FRWebAuthn {
throw e;
}
}

/**
* Populates the step with the necessary registration outcome.
*
* @param step The step that contains WebAuthn registration data
* @return The populated step
*/
public static async register(step: FRStep): Promise<FRStep> {
// Can make this generic const in Typescritp 5.0 > and the name itself will
// be inferred from the type so `typeof deviceName` will not just return string
// but the actual name of the deviceName passed in as a generic.
public static async register<T extends string = undefined>(
step: FRStep,
deviceName?: T,
): Promise<FRStep> {
const { hiddenCallback, metadataCallback, textOutputCallback } = this.getCallbacks(step);
if (hiddenCallback && (metadataCallback || textOutputCallback)) {
let outcome: string;
let outcome: OutcomeWithName<string, AttestationType, PublicKeyCredential>;

try {
let publicKey: PublicKeyCredentialRequestOptions;
Expand All @@ -147,10 +162,9 @@ abstract class FRWebAuthn {
} else if (textOutputCallback) {
publicKey = parseWebAuthnRegisterText(textOutputCallback.getMessage());
}
// TypeScript doesn't like `publicKey` being assigned in conditionals above
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const credential = await this.getRegistrationCredential(publicKey);
const credential = await this.getRegistrationCredential(
publicKey as PublicKeyCredentialCreationOptions,
);
outcome = this.getRegistrationOutcome(credential);
} catch (error) {
if (!(error instanceof Error)) throw error;
Expand All @@ -162,8 +176,7 @@ abstract class FRWebAuthn {
hiddenCallback.setInputValue(`${WebAuthnOutcome.Error}::${error.name}:${error.message}`);
throw error;
}

hiddenCallback.setInputValue(outcome);
hiddenCallback.setInputValue(deviceName ? `${outcome}::${deviceName}` : outcome);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hypothetically, we could give hiddenCallback a generic that defaults to unknown and then we can provide that here for more type inference and passing.

return step;
} else {
const e = new Error('Incorrect callbacks for WebAuthn registration');
Expand Down Expand Up @@ -263,7 +276,11 @@ abstract class FRWebAuthn {
* @param credential The credential to convert
* @return The outcome string
*/
public static getAuthenticationOutcome(credential: PublicKeyCredential | null): string {
public static getAuthenticationOutcome(
credential: PublicKeyCredential | null,
):
| OutcomeWithName<string, AttestationType, typeof credential>
| OutcomeWithName<string, AttestationType, typeof credential, string> {
if (credential === null) {
const e = new Error('No credential generated from authentication');
e.name = WebAuthnOutcomeType.UnknownError;
Expand All @@ -273,19 +290,33 @@ abstract class FRWebAuthn {
try {
const clientDataJSON = arrayBufferToString(credential.response.clientDataJSON);
const assertionResponse = credential.response as AuthenticatorAssertionResponse;
const authenticatorData = new Int8Array(assertionResponse.authenticatorData).toString();
const authenticatorData = new Int8Array(
assertionResponse.authenticatorData,
).toString() as AttestationType;
const signature = new Int8Array(assertionResponse.signature).toString();

// Current native typing for PublicKeyCredential does not include `userHandle`
// eslint-disable-next-line
// @ts-ignore
const userHandle = arrayBufferToString(credential.response.userHandle);

let stringOutput = `${clientDataJSON}::${authenticatorData}::${signature}::${credential.id}`;
let stringOutput =
`${clientDataJSON}::${authenticatorData}::${signature}::${credential.id}` as OutcomeWithName<
string,
AttestationType,
PublicKeyCredential
>;
// Check if Username is stored on device
if (userHandle) {
stringOutput = `${stringOutput}::${userHandle}`;
return stringOutput as OutcomeWithName<
string,
AttestationType,
PublicKeyCredential,
string
>;
}

return stringOutput;
} catch (error) {
const e = new Error('Transforming credential object to string failed');
Expand Down Expand Up @@ -321,7 +352,9 @@ abstract class FRWebAuthn {
* @param credential The credential to convert
* @return The outcome string
*/
public static getRegistrationOutcome(credential: PublicKeyCredential | null): string {
public static getRegistrationOutcome(
credential: PublicKeyCredential | null,
): OutcomeWithName<string, AttestationType, PublicKeyCredential> {
if (credential === null) {
const e = new Error('No credential generated from registration');
e.name = WebAuthnOutcomeType.UnknownError;
Expand All @@ -331,7 +364,9 @@ abstract class FRWebAuthn {
try {
const clientDataJSON = arrayBufferToString(credential.response.clientDataJSON);
const attestationResponse = credential.response as AuthenticatorAttestationResponse;
const attestationObject = new Int8Array(attestationResponse.attestationObject).toString();
const attestationObject = new Int8Array(
attestationResponse.attestationObject,
).toString() as AttestationType.Direct;
return `${clientDataJSON}::${attestationObject}::${credential.id}`;
} catch (error) {
const e = new Error('Transforming credential object to string failed');
Expand Down