-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(AuthTokenVerifier): restore recovery of address from sig (#133)
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
Showing
6 changed files
with
318 additions
and
271 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.