Skip to content

Commit

Permalink
fix(AuthTokenVerifier): restore recovery of address from sig (#133)
Browse files Browse the repository at this point in the history
Try to login by recovering address and by considering the publicKeys in the DID Document.
In other words, even if the DID Document does not explicitly allow an address "recovery", attempt it anyways.
Tests are also refactored to cover this scenario.
Jira item: PDA-14

Co-authored-by: Dmitry Fesenko <[email protected]>
  • Loading branch information
jrhender and JGiter authored Oct 3, 2021
1 parent 42a58fb commit 3c40401
Show file tree
Hide file tree
Showing 6 changed files with 318 additions and 271 deletions.
2 changes: 1 addition & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export default {
// unmockedModulePathPatterns: undefined,

// Indicates whether each individual test should be reported during the run
// verbose: undefined,
verbose: true,

// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
Expand Down
194 changes: 111 additions & 83 deletions lib/AuthTokenVerifier.ts
Original file line number Diff line number Diff line change
@@ -1,104 +1,132 @@
import { JWT } from "@ew-did-registry/jwt";
import { Keys } from "@ew-did-registry/keys";
import {
IAuthentication,
IDIDDocument,
IPublicKey,
PubKeyType,
} from "@ew-did-registry/did-resolver-interface";
import { utils } from "ethers";
import base64url from "base64url";

import { JWT } from '@ew-did-registry/jwt';
import { Keys } from '@ew-did-registry/keys';
import { IAuthentication, IDIDDocument, IPublicKey } from "@ew-did-registry/did-resolver-interface";
const { arrayify, recoverAddress, keccak256, hashMessage } = utils;

export class AuthTokenVerifier {
private _jwt = new JWT(new Keys());
constructor(private readonly _didDocument: IDIDDocument) {}

private readonly privateKey: string
private readonly didDocument: IDIDDocument

constructor(
private readonly _privateKey: string,
private readonly _didDocument: IDIDDocument
) {
this.didDocument = _didDocument,
this.privateKey = _privateKey
}

/**
/**
* @description checks a token was signed by the issuer DID or a valid authentication delegate of the issuer
*
* @param token
* @param issuerDID
*
* @returns {string} issuer DID or null
* @returns {string} Authenticated identity DID
*/
public async verify(token: string, issuerDID: string): Promise<string | null> {
if (await this.isAuthorized(token))
return issuerDID
return null
public async verify(token: string): Promise<string | null> {
if ((await this.isIdentity(token)) || (await this.isDelegate(token))) {
return this._didDocument.id;
}
return null;
}

private isAuthorized = async (claimedToken: string): Promise<boolean> => {
//read publickey field in DID document
const didPubKeys = this.didDocument.publicKey
if (didPubKeys.length === 0) {
return false
}

const keys = new Keys({ privateKey: this.privateKey })
const jwtSigner = new JWT(keys)

//get all authentication public keys
const authenticationPubkeys = didPubKeys.filter(pubkey => {
return this.isAuthenticationKey(pubkey, this.didDocument.authentication)
})
/**
* @description Determines if a token was signed with an Ethereum signature by the address in the id of the DID Document
* Not that JWT-compliant signatures can't be used to recover an ethereum
*/
private async isIdentity(token: string) {
const [encodedHeader, encodedPayload, encodedSignature] = token.split(".");
const msg = `0x${Buffer.from(`${encodedHeader}.${encodedPayload}`).toString(
"hex"
)}`;
const signature = base64url.decode(encodedSignature);
const hash = arrayify(keccak256(msg));
const claimedAddress = this._didDocument.id.split(":")[2];
try {
if (claimedAddress === recoverAddress(hash, signature)) {
return true;
}
} catch (_) {}
const digest = arrayify(hashMessage(hash));
try {
if (claimedAddress === recoverAddress(digest, signature)) {
return true;
}
} catch (_) {}
return false;
}

const validKeys = await this.filterValidKeys(authenticationPubkeys, async (pubKeyField) => {
try {
console.log(pubKeyField)
const parts = pubKeyField["publicKeyHex"]?.split('x')
const publickey = parts.length == 2 ? parts[1] : parts[0]
console.log(publickey)
const decodedClaim = await jwtSigner.verify(claimedToken, publickey);
return decodedClaim !== undefined;
}
catch (error) {
return false
}
})
return validKeys.length !== 0
private async isDelegate(token: string) {
const didPubKeys = this._didDocument.publicKey;
if (didPubKeys.length === 0) {
return false;
}

private filterValidKeys = async (authenticatedKey: IPublicKey[], verifSignature) => {
const results = await Promise.all(authenticatedKey.map(verifSignature));

return authenticatedKey.filter((_key, index) => results[index]);
}
const authenticationPubkeys = didPubKeys.filter((pubkey) => {
return this.isAuthenticationKey(pubkey, this._didDocument.authentication);
});
const validKeys = await this.filterValidKeys(
authenticationPubkeys,
async (pubKeyField) => {
try {
const parts = pubKeyField["publicKeyHex"]?.split("x");
const publickey = parts.length == 2 ? parts[1] : parts[0];
const decodedClaim = await this._jwt.verify(token, publickey);

/**
* The authentication token should be signed by an "authentication" key of the DID (https://www.w3.org/TR/did-core/#authentication)
* There are two ways to determine an authentication key.
* 1. The publicKey's type is "sigAuth"
* 2. The publicKey's id is in the authentication array of the DID document
* @param publicKey The publicKey to test
* @param documentAuthField The authentication array of the DID document
* @returns whether or not the key is an authentication key
*/
private isAuthenticationKey = (publicKey: IPublicKey, documentAuthField: (string | IAuthentication)[]) => {
if (documentAuthField.length === 0 && publicKey !== undefined) {
return this.isSigAuth(publicKey["type"])
return decodedClaim !== undefined;
} catch (error) {
return false;
}
const authenticationKeys = documentAuthField.map(auth => {
return (this.areLinked(auth["publicKey"], publicKey["id"]))
})
return authenticationKeys.includes(true);
}
}
);
return validKeys.length !== 0;
}

//used to check if publicKey field in authentication refers to the publicKey ID in publicKey field
private areLinked = (authId: string, pubKeyID: string) => {
if (authId === pubKeyID) {
return true
}
if (authId.includes("#")) {
return pubKeyID.split("#")[0] == authId.split("#")[0]
}
return false
private filterValidKeys = async (
authenticatedKey: IPublicKey[],
verifSignature
) => {
const results = await Promise.all(authenticatedKey.map(verifSignature));

return authenticatedKey.filter((_key, index) => results[index]);
};

/**
* The authentication token should be signed by an "authentication" key of the DID (https://www.w3.org/TR/did-core/#authentication)
* There are two ways to determine an authentication key.
* 1. The publicKey's type is "sigAuth"
* 2. The publicKey's id is in the authentication array of the DID document
* @param publicKey The publicKey to test
* @param documentAuthField The authentication array of the DID document
* @returns whether or not the key is an authentication key
*/
private isAuthenticationKey = (
publicKey: IPublicKey,
documentAuthField: (string | IAuthentication)[]
) => {
if (this.isSigAuth(publicKey.type)) {
return true;
}
if (documentAuthField.length === 0) {
return false;
}
const authenticationKeys = documentAuthField.map((auth) => {
return this.areLinked(auth["publicKey"], publicKey["id"]);
});
return authenticationKeys.includes(true);
};

private isSigAuth = (pubKeyType: string) => {
const extractedType = pubKeyType.substring(pubKeyType.length - 7);
return extractedType === "sigAuth"
//used to check if publicKey field in authentication refers to the publicKey ID in publicKey field
private areLinked = (authId: string, pubKeyID: string) => {
if (authId === pubKeyID) {
return true;
}
if (authId.includes("#")) {
return pubKeyID.split("#")[0] == authId.split("#")[0];
}
return false;
};

private isSigAuth = (pubKeyType: string) => {
return pubKeyType.endsWith(PubKeyType.SignatureAuthentication2018);
};
}
4 changes: 2 additions & 2 deletions lib/LoginStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ export class LoginStrategy extends BaseStrategy {
const didDocument = this.cacheServerClient?.isAvailable
? await this.cacheServerClient.getDidDocument(payload.iss)
: await this.didResolver.read(payload.iss)
const authenticationClaimVerifier = new AuthTokenVerifier(this.privateKey, didDocument)
const did = await authenticationClaimVerifier.verify(token, payload.iss)
const authenticationClaimVerifier = new AuthTokenVerifier(didDocument)
const did = await authenticationClaimVerifier.verify(token)

if (!did) {
console.log('Not Verified')
Expand Down
18 changes: 13 additions & 5 deletions package-lock.json

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

Loading

0 comments on commit 3c40401

Please sign in to comment.