Skip to content

Commit

Permalink
Fix EmailAlreadyInUse issues (#56)
Browse files Browse the repository at this point in the history
* proto: add more error reasons

* rpc: return more information in ErrEmailAlreadyInUse

* rpc: allow for more than one guest account per project (duh)

* rpc: more typed errors

* proto: use 7xxx error codes
  • Loading branch information
patrislav authored Jul 12, 2024
1 parent 4307e72 commit 40a23d4
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 77 deletions.
14 changes: 9 additions & 5 deletions proto/authenticator.gen.go

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

6 changes: 5 additions & 1 deletion proto/authenticator.ridl
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,11 @@ struct VerificationContext
error 1000 Unauthorized "Unauthorized access" HTTP 401
error 1001 TenantNotFound "Tenant not found" HTTP 404

error 2000 EmailAlreadyInUse "Could not create account as the email is already in use" HTTP 409
error 7000 EmailAlreadyInUse "Could not create account as the email is already in use" HTTP 409
error 7001 AccountAlreadyLinked "Could not link account as it is linked to another wallet" HTTP 409
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


##
Expand Down
14 changes: 9 additions & 5 deletions proto/clients/authenticator.gen.go

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

68 changes: 64 additions & 4 deletions proto/clients/authenticator.gen.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable */
// sequence-waas-authenticator v0.1.0 0162a6fb35dd49d6f4d924ebce4bceb847479f3a
// sequence-waas-authenticator v0.1.0 93374d947ac55a97a4d3a9821262e850d787f549
// --
// Code generated by [email protected] with typescript generator. DO NOT EDIT.
//
Expand All @@ -12,7 +12,7 @@ export const WebRPCVersion = "v1"
export const WebRPCSchemaVersion = "v0.1.0"

// Schema hash generated from your RIDL schema
export const WebRPCSchemaHash = "0162a6fb35dd49d6f4d924ebce4bceb847479f3a"
export const WebRPCSchemaHash = "93374d947ac55a97a4d3a9821262e850d787f549"

//
// Types
Expand Down Expand Up @@ -699,7 +699,7 @@ export class TenantNotFoundError extends WebrpcError {
export class EmailAlreadyInUseError extends WebrpcError {
constructor(
name: string = 'EmailAlreadyInUse',
code: number = 2000,
code: number = 7000,
message: string = 'Could not create account as the email is already in use',
status: number = 0,
cause?: string
Expand All @@ -709,6 +709,58 @@ export class EmailAlreadyInUseError extends WebrpcError {
}
}

export class AccountAlreadyLinkedError extends WebrpcError {
constructor(
name: string = 'AccountAlreadyLinked',
code: number = 7001,
message: string = 'Could not link account as it is linked to another wallet',
status: number = 0,
cause?: string
) {
super(name, code, message, status, cause)
Object.setPrototypeOf(this, AccountAlreadyLinkedError.prototype)
}
}

export class ProofVerificationFailedError extends WebrpcError {
constructor(
name: string = 'ProofVerificationFailed',
code: number = 7002,
message: string = 'The authentication proof could not be verified',
status: number = 0,
cause?: string
) {
super(name, code, message, status, cause)
Object.setPrototypeOf(this, ProofVerificationFailedError.prototype)
}
}

export class AnswerIncorrectError extends WebrpcError {
constructor(
name: string = 'AnswerIncorrect',
code: number = 7003,
message: string = 'The provided answer is incorrect',
status: number = 0,
cause?: string
) {
super(name, code, message, status, cause)
Object.setPrototypeOf(this, AnswerIncorrectError.prototype)
}
}

export class ChallengeExpiredError extends WebrpcError {
constructor(
name: string = 'ChallengeExpired',
code: number = 7004,
message: string = 'The challenge has expired',
status: number = 0,
cause?: string
) {
super(name, code, message, status, cause)
Object.setPrototypeOf(this, ChallengeExpiredError.prototype)
}
}


export enum errors {
WebrpcEndpoint = 'WebrpcEndpoint',
Expand All @@ -725,6 +777,10 @@ export enum errors {
Unauthorized = 'Unauthorized',
TenantNotFound = 'TenantNotFound',
EmailAlreadyInUse = 'EmailAlreadyInUse',
AccountAlreadyLinked = 'AccountAlreadyLinked',
ProofVerificationFailed = 'ProofVerificationFailed',
AnswerIncorrect = 'AnswerIncorrect',
ChallengeExpired = 'ChallengeExpired',
}

const webrpcErrorByCode: { [code: number]: any } = {
Expand All @@ -741,7 +797,11 @@ const webrpcErrorByCode: { [code: number]: any } = {
[-10]: WebrpcStreamFinishedError,
[1000]: UnauthorizedError,
[1001]: TenantNotFoundError,
[2000]: EmailAlreadyInUseError,
[7000]: EmailAlreadyInUseError,
[7001]: AccountAlreadyLinkedError,
[7002]: ProofVerificationFailedError,
[7003]: AnswerIncorrectError,
[7004]: ChallengeExpiredError,
}

export type Fetch = (input: RequestInfo, init?: RequestInit) => Promise<Response>
Expand Down
45 changes: 32 additions & 13 deletions rpc/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package rpc

import (
"context"
"errors"
"fmt"
"time"

Expand Down Expand Up @@ -55,16 +56,16 @@ func (s *RPC) federateAccount(
tntData := tenant.FromContext(ctx)

if intent.Data.SessionID != sess.ID {
return nil, fmt.Errorf("sessionId mismatch")
return nil, proto.ErrWebrpcBadRequest.WithCausef("sessionId mismatch")
}

authProvider, err := s.getAuthProvider(intent.Data.IdentityType)
if err != nil {
return nil, fmt.Errorf("get auth provider: %w", err)
return nil, proto.ErrWebrpcBadRequest.WithCausef("get auth provider: %w", err)
}

if intent.Data.IdentityType == intents.IdentityType_Guest {
return nil, fmt.Errorf("cannot federate a guest account")
return nil, proto.ErrWebrpcBadRequest.WithCausef("cannot federate a guest account")
}

var verifCtx *proto.VerificationContext
Expand All @@ -75,34 +76,52 @@ func (s *RPC) federateAccount(
}
dbVerifCtx, found, err := s.VerificationContexts.Get(ctx, authID)
if err != nil {
return nil, fmt.Errorf("getting verification context: %w", err)
return nil, proto.ErrWebrpcInternalError.WithCausef("getting verification context: %w", err)
}
if found && dbVerifCtx != nil {
verifCtx, _, err = crypto.DecryptData[*proto.VerificationContext](ctx, dbVerifCtx.EncryptedKey, dbVerifCtx.Ciphertext, tntData.KMSKeys)
if err != nil {
return nil, fmt.Errorf("decrypting verification context data: %w", err)
return nil, proto.ErrWebrpcInternalError.WithCausef("decrypting verification context data: %w", err)
}

if time.Now().After(verifCtx.ExpiresAt) {
return nil, fmt.Errorf("auth session expired")
return nil, proto.ErrChallengeExpired
}

if !dbVerifCtx.CorrespondsTo(verifCtx) {
return nil, fmt.Errorf("malformed verification context data")
return nil, proto.ErrWebrpcInternalError.WithCausef("malformed verification context data")
}
}

ident, err := authProvider.Verify(ctx, verifCtx, sess.ID, intent.Data.Answer)
if err != nil {
return nil, fmt.Errorf("verifying identity: %w", err)
if verifCtx != nil {
now := time.Now()
verifCtx.Attempts += 1
verifCtx.LastAttemptAt = &now

encryptedKey, algorithm, ciphertext, err := crypto.EncryptData(ctx, att, tntData.KMSKeys[0], verifCtx)
if err != nil {
return nil, proto.ErrWebrpcInternalError.WithCausef("encrypt data: %w", err)
}
if err := s.VerificationContexts.UpdateData(ctx, dbVerifCtx, encryptedKey, algorithm, ciphertext); err != nil {
return nil, proto.ErrWebrpcInternalError.WithCausef("update verification context: %w", err)
}
}

var wErr proto.WebRPCError
if errors.As(err, &wErr) {
return nil, wErr
}
return nil, proto.ErrAnswerIncorrect.WithCausef("verifying answer: %w", err)
}

_, found, err = s.Accounts.Get(ctx, tntData.ProjectID, ident)
if err != nil {
return nil, fmt.Errorf("retrieving account: %w", err)
return nil, proto.ErrWebrpcInternalError.WithCausef("retrieving account: %w", err)
}
if found {
return nil, fmt.Errorf("account already exists")
return nil, proto.ErrAccountAlreadyLinked
}

accData := &proto.AccountData{
Expand All @@ -114,7 +133,7 @@ func (s *RPC) federateAccount(

encryptedKey, algorithm, ciphertext, err := crypto.EncryptData(ctx, att, tntData.KMSKeys[0], accData)
if err != nil {
return nil, fmt.Errorf("encrypting account data: %w", err)
return nil, proto.ErrWebrpcInternalError.WithCausef("encrypting account data: %w", err)
}

account := &data.Account{
Expand All @@ -130,11 +149,11 @@ func (s *RPC) federateAccount(
}

if _, err := s.Wallets.FederateAccount(waasapi.Context(ctx), account.UserID, waasapi.ConvertToAPIIntent(intent.ToIntent())); err != nil {
return nil, fmt.Errorf("creating account with WaaS API: %w", err)
return nil, proto.ErrWebrpcInternalError.WithCausef("creating account with WaaS API: %w", err)
}

if err := s.Accounts.Put(ctx, account); err != nil {
return nil, fmt.Errorf("save account: %w", err)
return nil, proto.ErrWebrpcInternalError.WithCausef("save account: %w", err)
}

outAcc := &intents.Account{
Expand Down
4 changes: 2 additions & 2 deletions rpc/auth/email/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,10 @@ func extractVerifier(verifier string) (emailAddress string, sessionID string, er
if !found {
return "", "", fmt.Errorf("invalid verifier")
}
return normalizeEmail(emailAddress), sessionID, nil
return Normalize(emailAddress), sessionID, nil
}

func normalizeEmail(email string) string {
func Normalize(email string) string {
return strings.ToLower(strings.TrimSpace(email))
}

Expand Down
54 changes: 28 additions & 26 deletions rpc/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,35 +185,37 @@ func TestGuestAuth(t *testing.T) {
ctx, err := proto.WithHTTPRequestHeaders(context.Background(), header)
require.NoError(t, err)

sessWallet, err := ethwallet.NewWalletFromRandomEntropy()
require.NoError(t, err)
signingSession := intents.NewSessionP256K1(sessWallet)
for i := 0; i < 10; i++ {
sessWallet, err := ethwallet.NewWalletFromRandomEntropy()
require.NoError(t, err)
signingSession := intents.NewSessionP256K1(sessWallet)

initiateAuth := generateSignedIntent(t, intents.IntentName_initiateAuth, intents.IntentDataInitiateAuth{
SessionID: signingSession.SessionID(),
IdentityType: intents.IdentityType_Guest,
Verifier: signingSession.SessionID(),
}, signingSession)
initRes, err := c.SendIntent(ctx, initiateAuth)
require.NoError(t, err)
assert.Equal(t, proto.IntentResponseCode_authInitiated, initRes.Code)
initiateAuth := generateSignedIntent(t, intents.IntentName_initiateAuth, intents.IntentDataInitiateAuth{
SessionID: signingSession.SessionID(),
IdentityType: intents.IdentityType_Guest,
Verifier: signingSession.SessionID(),
}, signingSession)
initRes, err := c.SendIntent(ctx, initiateAuth)
require.NoError(t, err)
assert.Equal(t, proto.IntentResponseCode_authInitiated, initRes.Code)

b, err := json.Marshal(initRes.Data)
require.NoError(t, err)
var initResData intents.IntentResponseAuthInitiated
require.NoError(t, json.Unmarshal(b, &initResData))
b, err := json.Marshal(initRes.Data)
require.NoError(t, err)
var initResData intents.IntentResponseAuthInitiated
require.NoError(t, json.Unmarshal(b, &initResData))

answer := crypto.Keccak256([]byte(*initResData.Challenge + signingSession.SessionID()))
registerSession := generateSignedIntent(t, intents.IntentName_openSession, intents.IntentDataOpenSession{
SessionID: signingSession.SessionID(),
IdentityType: intents.IdentityType_Guest,
Verifier: signingSession.SessionID(),
Answer: hexutil.Encode(answer),
}, signingSession)
sess, registerRes, err := c.RegisterSession(ctx, registerSession, "Friendly name")
require.NoError(t, err)
assert.Equal(t, "Guest:"+signingSession.SessionID(), sess.Identity.String())
assert.Equal(t, proto.IntentResponseCode_sessionOpened, registerRes.Code)
answer := crypto.Keccak256([]byte(*initResData.Challenge + signingSession.SessionID()))
registerSession := generateSignedIntent(t, intents.IntentName_openSession, intents.IntentDataOpenSession{
SessionID: signingSession.SessionID(),
IdentityType: intents.IdentityType_Guest,
Verifier: signingSession.SessionID(),
Answer: hexutil.Encode(answer),
}, signingSession)
sess, registerRes, err := c.RegisterSession(ctx, registerSession, "Friendly name")
require.NoError(t, err)
assert.Equal(t, "Guest:"+signingSession.SessionID(), sess.Identity.String())
assert.Equal(t, proto.IntentResponseCode_sessionOpened, registerRes.Code)
}
})
}

Expand Down
Loading

0 comments on commit 40a23d4

Please sign in to comment.