diff --git a/proto/authenticator.gen.go b/proto/authenticator.gen.go index 46adb27a..ba1f2da8 100644 --- a/proto/authenticator.gen.go +++ b/proto/authenticator.gen.go @@ -1,4 +1,4 @@ -// sequence-waas-authenticator v0.1.0 a61eba85f37d76e045a3cf6657005e84edaf5785 +// sequence-waas-authenticator v0.1.0 35f86317a98af91896d1114ad52dd22102d9de9f // -- // Code generated by webrpc-gen@v0.18.8 with golang generator. DO NOT EDIT. // @@ -34,7 +34,7 @@ func WebRPCSchemaVersion() string { // Schema hash generated from your RIDL schema func WebRPCSchemaHash() string { - return "a61eba85f37d76e045a3cf6657005e84edaf5785" + return "35f86317a98af91896d1114ad52dd22102d9de9f" } // @@ -1325,4 +1325,5 @@ var ( ErrProofVerificationFailed = WebRPCError{Code: 7002, Name: "ProofVerificationFailed", Message: "The authentication proof could not be verified", HTTPStatus: 400} ErrAnswerIncorrect = WebRPCError{Code: 7003, Name: "AnswerIncorrect", Message: "The provided answer is incorrect", HTTPStatus: 400} ErrChallengeExpired = WebRPCError{Code: 7004, Name: "ChallengeExpired", Message: "The challenge has expired", HTTPStatus: 400} + ErrTooManyAttempts = WebRPCError{Code: 7005, Name: "TooManyAttempts", Message: "Too many attempts", HTTPStatus: 400} ) diff --git a/proto/authenticator.ridl b/proto/authenticator.ridl index 979d4bb3..f9faf3a1 100644 --- a/proto/authenticator.ridl +++ b/proto/authenticator.ridl @@ -201,6 +201,7 @@ error 7001 AccountAlreadyLinked "Could not link account as it is linked to anoth error 7002 ProofVerificationFailed "The authentication proof could not be verified" HTTP 400 error 7003 AnswerIncorrect "The provided answer is incorrect" HTTP 400 error 7004 ChallengeExpired "The challenge has expired" HTTP 400 +error 7005 TooManyAttempts "Too many attempts" HTTP 400 ## diff --git a/proto/clients/authenticator.gen.go b/proto/clients/authenticator.gen.go index e626d47c..35d535cd 100644 --- a/proto/clients/authenticator.gen.go +++ b/proto/clients/authenticator.gen.go @@ -1,4 +1,4 @@ -// sequence-waas-authenticator v0.1.0 a61eba85f37d76e045a3cf6657005e84edaf5785 +// sequence-waas-authenticator v0.1.0 35f86317a98af91896d1114ad52dd22102d9de9f // -- // Code generated by webrpc-gen@v0.18.8 with golang generator. DO NOT EDIT. // @@ -33,7 +33,7 @@ func WebRPCSchemaVersion() string { // Schema hash generated from your RIDL schema func WebRPCSchemaHash() string { - return "a61eba85f37d76e045a3cf6657005e84edaf5785" + return "35f86317a98af91896d1114ad52dd22102d9de9f" } // @@ -806,4 +806,5 @@ var ( ErrProofVerificationFailed = WebRPCError{Code: 7002, Name: "ProofVerificationFailed", Message: "The authentication proof could not be verified", HTTPStatus: 400} ErrAnswerIncorrect = WebRPCError{Code: 7003, Name: "AnswerIncorrect", Message: "The provided answer is incorrect", HTTPStatus: 400} ErrChallengeExpired = WebRPCError{Code: 7004, Name: "ChallengeExpired", Message: "The challenge has expired", HTTPStatus: 400} + ErrTooManyAttempts = WebRPCError{Code: 7005, Name: "TooManyAttempts", Message: "Too many attempts", HTTPStatus: 400} ) diff --git a/proto/clients/authenticator.gen.ts b/proto/clients/authenticator.gen.ts index d2b4fd50..fba2ae43 100644 --- a/proto/clients/authenticator.gen.ts +++ b/proto/clients/authenticator.gen.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// sequence-waas-authenticator v0.1.0 a61eba85f37d76e045a3cf6657005e84edaf5785 +// sequence-waas-authenticator v0.1.0 35f86317a98af91896d1114ad52dd22102d9de9f // -- // Code generated by webrpc-gen@v0.18.8 with typescript generator. DO NOT EDIT. // @@ -12,7 +12,7 @@ export const WebRPCVersion = "v1" export const WebRPCSchemaVersion = "v0.1.0" // Schema hash generated from your RIDL schema -export const WebRPCSchemaHash = "a61eba85f37d76e045a3cf6657005e84edaf5785" +export const WebRPCSchemaHash = "35f86317a98af91896d1114ad52dd22102d9de9f" // // Types @@ -768,6 +768,19 @@ export class ChallengeExpiredError extends WebrpcError { } } +export class TooManyAttemptsError extends WebrpcError { + constructor( + name: string = 'TooManyAttempts', + code: number = 7005, + message: string = 'Too many attempts', + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, TooManyAttemptsError.prototype) + } +} + export enum errors { WebrpcEndpoint = 'WebrpcEndpoint', @@ -788,6 +801,7 @@ export enum errors { ProofVerificationFailed = 'ProofVerificationFailed', AnswerIncorrect = 'AnswerIncorrect', ChallengeExpired = 'ChallengeExpired', + TooManyAttempts = 'TooManyAttempts', } const webrpcErrorByCode: { [code: number]: any } = { @@ -809,6 +823,7 @@ const webrpcErrorByCode: { [code: number]: any } = { [7002]: ProofVerificationFailedError, [7003]: AnswerIncorrectError, [7004]: ChallengeExpiredError, + [7005]: TooManyAttemptsError, } export type Fetch = (input: RequestInfo, init?: RequestInit) => Promise diff --git a/rpc/auth_test.go b/rpc/auth_test.go index e8949ea0..a3481554 100644 --- a/rpc/auth_test.go +++ b/rpc/auth_test.go @@ -27,11 +27,13 @@ import ( func TestEmailAuth(t *testing.T) { type assertionParams struct { - tenant *data.Tenant - email string + tenant *data.Tenant + email string + attempt int } testCases := map[string]struct { + retryAttempts int emailBuilderFn func(t *testing.T, p assertionParams) string assertInitiateAuthFn func(t *testing.T, res *proto.IntentResponse, err error) bool extractAnswerFn func(t *testing.T, p assertionParams, res *proto.IntentResponse) string @@ -85,6 +87,50 @@ func TestEmailAuth(t *testing.T) { require.ErrorContains(t, err, "incorrect answer") }, }, + "MultipleAttempts": { + retryAttempts: 2, + assertInitiateAuthFn: func(t *testing.T, res *proto.IntentResponse, err error) bool { + return true + }, + extractAnswerFn: func(t *testing.T, p assertionParams, res *proto.IntentResponse) string { + if p.attempt < 2 { + return "Wrong" + } + _, message, found := getSentEmailMessage(t, fmt.Sprintf("user+%d@example.com", p.tenant.ProjectID)) + require.True(t, found) + return strings.TrimPrefix(message, "Your login code: ") + }, + assertRegisterSessionFn: func(t *testing.T, p assertionParams, sess *proto.Session, res *proto.IntentResponse, err error) { + if p.attempt < 2 { + require.ErrorContains(t, err, "incorrect answer") + return + } + expectedIdentity := newEmailIdentity(fmt.Sprintf("user+%d@example.com", p.tenant.ProjectID)) + require.NoError(t, err) + assert.Equal(t, expectedIdentity, sess.Identity) + }, + }, + "TooManyAttempts": { + retryAttempts: 10, + assertInitiateAuthFn: func(t *testing.T, res *proto.IntentResponse, err error) bool { + return true + }, + extractAnswerFn: func(t *testing.T, p assertionParams, res *proto.IntentResponse) string { + if p.attempt < 3 { + return "Wrong" + } + _, message, found := getSentEmailMessage(t, fmt.Sprintf("user+%d@example.com", p.tenant.ProjectID)) + require.True(t, found) + return strings.TrimPrefix(message, "Your login code: ") + }, + assertRegisterSessionFn: func(t *testing.T, p assertionParams, sess *proto.Session, res *proto.IntentResponse, err error) { + if p.attempt < 3 { + require.ErrorContains(t, err, "incorrect answer") + } else { + require.ErrorContains(t, err, "Too many attempts") + } + }, + }, } for name, testCase := range testCases { @@ -143,21 +189,25 @@ func TestEmailAuth(t *testing.T) { } } - code := testCase.extractAnswerFn(t, p, initiateAuthRes) - challenge := initiateAuthRes.Data.(map[string]any)["challenge"].(string) - answer := hexutil.Encode(crypto.Keccak256([]byte(challenge + code))) + for attempt := 0; attempt < testCase.retryAttempts+1; attempt++ { + p.attempt = attempt - openSessionData := intents.IntentDataOpenSession{ - SessionID: signingSession.SessionID(), - IdentityType: intents.IdentityType_Email, - Verifier: p.email + ";" + signingSession.SessionID(), - Answer: answer, - } - openSession := generateSignedIntent(t, intents.IntentName_openSession, openSessionData, signingSession) + code := testCase.extractAnswerFn(t, p, initiateAuthRes) + challenge := initiateAuthRes.Data.(map[string]any)["challenge"].(string) + answer := hexutil.Encode(crypto.Keccak256([]byte(challenge + code))) + + openSessionData := intents.IntentDataOpenSession{ + SessionID: signingSession.SessionID(), + IdentityType: intents.IdentityType_Email, + Verifier: p.email + ";" + signingSession.SessionID(), + Answer: answer, + } + openSession := generateSignedIntent(t, intents.IntentName_openSession, openSessionData, signingSession) - session, openSessionRes, err := c.RegisterSession(ctx, openSession, "friendly name") - if testCase.assertRegisterSessionFn != nil { - testCase.assertRegisterSessionFn(t, p, session, openSessionRes, err) + session, openSessionRes, err := c.RegisterSession(ctx, openSession, "friendly name") + if testCase.assertRegisterSessionFn != nil { + testCase.assertRegisterSessionFn(t, p, session, openSessionRes, err) + } } }) } diff --git a/rpc/sessions.go b/rpc/sessions.go index f2e77a22..6b37fab1 100644 --- a/rpc/sessions.go +++ b/rpc/sessions.go @@ -74,6 +74,10 @@ func (s *RPC) RegisterSession( return nil, nil, proto.ErrWebrpcInternalError.WithCausef("decrypting verification context data: %w", err) } + if verifCtx.Attempts >= 3 { + return nil, nil, proto.ErrTooManyAttempts + } + if time.Now().After(verifCtx.ExpiresAt) { return nil, nil, proto.ErrChallengeExpired }