Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WaaS adoption intent confirmation #628

Draft
wants to merge 2 commits into
base: waas-child-wallet-adoption
Choose a base branch
from
Draft
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
59 changes: 59 additions & 0 deletions packages/waas/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ import {
AdoptChildWalletArgs
} from './intents'
import {
ConfirmationRequiredResponse,
FeeOptionsResponse,
isChildWalletAdoptedResponse,
isCloseSessionResponse,
isConfirmationRequiredResponse,
isFeeOptionsResponse,
isFinishValidateSessionResponse,
isGetAdopterResponse,
Expand Down Expand Up @@ -168,7 +170,9 @@ export class SequenceWaaS {
private validationRequiredCallback: (() => void)[] = []
private emailConflictCallback: ((info: EmailConflictInfo, forceCreate: () => Promise<void>) => Promise<void>)[] = []
private emailAuthCodeRequiredCallback: ((respondWithCode: (code: string) => Promise<void>) => Promise<void>)[] = []
private confirmationRequiredCallback: ((respondWithCode: (code: string) => Promise<void>) => Promise<void>)[] = []
private validationRequiredSalt: string
private lastConfirmationAttemptAt: Date | undefined

public readonly config: Required<SequenceConfig> & Required<WaaSConfigKey> & ExtendedSequenceConfig

Expand Down Expand Up @@ -313,6 +317,13 @@ export class SequenceWaaS {
}
}

onConfirmationRequired(callback: (respondWithCode: (code: string) => Promise<void>) => Promise<void>) {
this.confirmationRequiredCallback.push(callback)
return () => {
this.confirmationRequiredCallback = this.confirmationRequiredCallback.filter(c => c !== callback)
}
}

private async handleValidationRequired({ onValidationRequired }: ValidationArgs = {}): Promise<boolean> {
const proceed = onValidationRequired ? onValidationRequired() : true
if (!proceed) {
Expand All @@ -333,6 +344,45 @@ export class SequenceWaaS {
return this.waitForSessionValid()
}

private async handleConfirmationRequired(response: ConfirmationRequiredResponse) {
if (this.confirmationRequiredCallback.length === 0) {
throw new Error('Missing confirmationRequired callback')
}

return new Promise((resolve, reject) => {
const respondToChallenge = async (answer: string) => {
if (this.lastConfirmationAttemptAt) {
const timeSinceLastAttempt = new Date().getTime() - this.lastConfirmationAttemptAt.getTime()
if (timeSinceLastAttempt < 32000) {
console.info(`Waiting ${Math.ceil((32000 - timeSinceLastAttempt) / 1000)}s before retrying confirmation attempt`)
await new Promise(resolve => setTimeout(resolve, 32000 - timeSinceLastAttempt))
}
}

this.lastConfirmationAttemptAt = new Date()

const intent = await this.waas.confirmIntent(response.data.salt, answer)

try {
const response2 = await this.sendIntent(intent)
resolve(response2)
} catch (e) {
if (e instanceof AnswerIncorrectError) {
// This will NOT resolve NOR reject the top-level promise returned from signIn, it'll keep being pending
// It allows the caller to retry calling the respondToChallenge callback
throw e
} else {
reject(e)
}
}
}

for (const callback of this.confirmationRequiredCallback) {
callback(respondToChallenge)
}
})
}

private headers() {
return {
'X-Access-Key': this.config.projectAccessKey
Expand Down Expand Up @@ -775,6 +825,15 @@ export class SequenceWaaS {
return response
}

if (isConfirmationRequiredResponse(response)) {
const response2 = await this.handleConfirmationRequired(response)
if (isExpectedResponse(response2)) {
return response2
} else {
throw new Error(JSON.stringify(response2))
}
}

if (isValidationRequiredResponse(response)) {
const proceed = await this.handleValidationRequired(args.validation)

Expand Down
15 changes: 14 additions & 1 deletion packages/waas/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
changeIntentTime,
closeSession,
combineTransactionIntents,
confirmIntent,
feeOptions,
finishValidateSession,
getAdopter,
Expand Down Expand Up @@ -50,7 +51,8 @@ import {
IntentDataOpenSession,
IntentDataSendTransaction,
IntentDataSignMessage,
IntentDataValidateSession
IntentDataValidateSession,
IntentDataConfirmIntent,
} from './clients/intent.gen'
import { getDefaultSubtleCryptoBackend, SubtleCryptoBackend } from './subtle-crypto'
import { getDefaultSecureStoreBackend, SecureStoreBackend } from './secure-store'
Expand Down Expand Up @@ -513,6 +515,17 @@ export class SequenceWaaSBase {
return this.signIntent(intent)
}

async confirmIntent(salt: string, secretCode: string): Promise<SignedIntent<IntentDataConfirmIntent>> {
const intent = confirmIntent({
lifespan: DEFAULT_LIFESPAN,
wallet: await this.getWalletAddress(),
confirmationID: salt,
challengeAnswer: ethers.id(salt + secretCode),
})

return this.signIntent(intent)
}

async getSession(): Promise<SignedIntent<IntentDataGetSession>> {
const sessionId = await this.sessionId.get()
if (!sessionId) {
Expand Down
13 changes: 12 additions & 1 deletion packages/waas/src/intents/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
IntentResponseGetSession,
IntentResponseIdToken,
IntentResponseValidationFinished,
IntentResponseValidationStarted
IntentResponseValidationStarted,
IntentResponseConfirmationRequired,
} from '../clients/intent.gen'
import { WebrpcEndpointError, WebrpcError } from '../clients/authenticator.gen'

Expand Down Expand Up @@ -127,6 +128,7 @@ export interface Response<Code, Data> {

export type InitiateAuthResponse = Response<IntentResponseCode.authInitiated, IntentResponseAuthInitiated>
export type ValidateSessionResponse = Response<IntentResponseCode.validationStarted, IntentResponseValidationStarted>
export type ConfirmationRequiredResponse = Response<IntentResponseCode.confirmationRequired, IntentResponseConfirmationRequired>
export type FinishValidateSessionResponse = Response<IntentResponseCode.validationFinished, IntentResponseValidationFinished>
export type GetSessionResponse = Response<IntentResponseCode.getSessionResponse, IntentResponseGetSession>
export type LinkAccountResponse = Response<IntentResponseCode.accountFederated, IntentResponseAccountFederated>
Expand Down Expand Up @@ -249,6 +251,15 @@ export function isFinishValidateSessionResponse(receipt: any): receipt is Finish
return typeof receipt === 'object' && receipt.code === IntentResponseCode.validationFinished && typeof receipt.data === 'object'
}

export function isConfirmationRequiredResponse(receipt: any): receipt is ConfirmationRequiredResponse {
return (
typeof receipt === 'object' &&
receipt.code === IntentResponseCode.confirmationRequired &&
typeof receipt.data === 'object' &&
typeof receipt.data.salt === 'string'
)
}

export function isCloseSessionResponse(receipt: any): receipt is CloseSessionResponse {
return typeof receipt === 'object' && typeof receipt.code === 'string' && receipt.code === 'sessionClosed'
}
Expand Down
9 changes: 8 additions & 1 deletion packages/waas/src/intents/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
IntentDataGetIdToken,
IntentName,
IntentDataAdoptChildWallet,
IntentDataGetAdopter
IntentDataGetAdopter,
IntentDataConfirmIntent,
} from '../clients/intent.gen'

interface BaseArgs {
Expand Down Expand Up @@ -83,3 +84,9 @@ export type GetAdopterArgs = BaseArgs & IntentDataGetAdopter
export function getAdopter({ lifespan, ...data }: GetAdopterArgs): Intent<IntentDataGetAdopter> {
return makeIntent(IntentName.getAdopter, lifespan, data)
}

export type ConfirmIntentArgs = BaseArgs & IntentDataConfirmIntent

export function confirmIntent({ lifespan, ...data }: ConfirmIntentArgs): Intent<IntentDataConfirmIntent> {
return makeIntent(IntentName.confirmIntent, lifespan, data)
}
Loading