Просмотр исходного кода

return correct auth error codes for icq/aim

This commit updates the login handler to return the proper error
codes based on whether the OSCAR client is AIM or ICQ.

Additionally, set an ICQ flag in the BOS cookie.

Also remove EOF error when attempting to login with nonexistent
account.
Mike 2 лет назад
Родитель
Сommit
58f68d7683
4 измененных файлов с 320 добавлено и 218 удалено
  1. 76 37
      foodgroup/auth.go
  2. 234 180
      foodgroup/auth_test.go
  3. 6 0
      server/oscar/auth.go
  4. 4 1
      wire/snacs.go

+ 76 - 37
foodgroup/auth.go

@@ -6,6 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"net"
+	"strings"
 
 	"github.com/mk6i/retro-aim-server/config"
 	"github.com/mk6i/retro-aim-server/state"
@@ -75,14 +76,24 @@ func (s AuthService) RegisterChatSession(authCookie []byte) (*state.Session, err
 	return s.chatSessionRegistry.AddSession(c.ChatCookie, c.ScreenName), nil
 }
 
+type bosCookie struct {
+	ICQ        uint8                   `oscar:"len_prefix=uint8"`
+	ScreenName state.DisplayScreenName `oscar:"len_prefix=uint8"`
+}
+
 // RegisterBOSSession creates and returns a user's session.
 func (s AuthService) RegisterBOSSession(authCookie []byte) (*state.Session, error) {
-	screenName, err := s.cookieBaker.Crack(authCookie)
+	buf, err := s.cookieBaker.Crack(authCookie)
 	if err != nil {
 		return nil, err
 	}
 
-	u, err := s.userManager.User(state.NewIdentScreenName(string(screenName)))
+	c := bosCookie{}
+	if err := wire.UnmarshalBE(&c, bytes.NewBuffer(buf)); err != nil {
+		return nil, err
+	}
+
+	u, err := s.userManager.User(c.ScreenName.IdentScreenName())
 	if err != nil {
 		return nil, fmt.Errorf("failed to retrieve user: %w", err)
 	}
@@ -140,10 +151,8 @@ func (s AuthService) SignoutChat(ctx context.Context, sess *state.Session) {
 // BUCPChallenge processes a BUCP authentication challenge request. It
 // retrieves the user's auth key based on the screen name provided in the
 // request. The client uses the auth key to salt the MD5 password hash provided
-// in the subsequent login request. If the account is invalid, an error code is
-// set in TLV wire.LoginTLVTagsErrorSubcode. If login credentials are invalid and app
-// config DisableAuth is true, a stub auth key is generated and a successful
-// challenge response is returned.
+// in the subsequent login request. If the account is valid, return
+// SNAC(0x17,0x07), otherwise return SNAC(0x17,0x03).
 func (s AuthService) BUCPChallenge(
 	bodyIn wire.SNAC_0x17_0x06_BUCPChallengeRequest,
 	newUUIDFn func() uuid.UUID,
@@ -250,56 +259,86 @@ func (s AuthService) login(
 		return wire.TLVRestBlock{}, errors.New("screen name doesn't exist in tlv")
 	}
 
+	isICQ := false
+	if clientName, hasclientName := TLVList.String(wire.LoginTLVTagsClientIdentity); hasclientName {
+		isICQ = strings.HasPrefix(clientName, "ICQ ")
+	}
+
 	user, err := s.userManager.User(state.NewIdentScreenName(screenName))
 	if err != nil {
 		return wire.TLVRestBlock{}, err
 	}
 
-	var loginOK bool
-	if user != nil {
-		if md5Hash, hasMD5 := TLVList.Slice(wire.LoginTLVTagsPasswordHash); hasMD5 {
-			loginOK = user.ValidateHash(md5Hash)
-		} else if roastedPass, hasRoasted := TLVList.Slice(wire.LoginTLVTagsRoastedPassword); hasRoasted {
-			loginOK = user.ValidateRoastedPass(roastedPass)
-		} else {
-			return wire.TLVRestBlock{}, errors.New("password hash doesn't exist in tlv")
-		}
-	}
-
-	if loginOK || s.config.DisableAuth {
-		if !loginOK {
-			// make login succeed anyway. create new user if the account
-			// doesn't already exist.
+	if user == nil {
+		if s.config.DisableAuth {
 			newUser, err := newUserFn(state.DisplayScreenName(screenName))
 			if err != nil {
 				return wire.TLVRestBlock{}, err
 			}
 			if err := s.userManager.InsertUser(newUser); err != nil {
-				if !errors.Is(err, state.ErrDupUser) {
-					return wire.TLVRestBlock{}, err
-				}
+				return wire.TLVRestBlock{}, err
 			}
+			return s.loginSuccessResponse(screenName, isICQ, err)
 		}
 
-		cookie, err := s.cookieBaker.Issue([]byte(screenName))
-		if err != nil {
-			return wire.TLVRestBlock{}, fmt.Errorf("failed to make auth cookie: %w", err)
+		loginErr := wire.LoginErrInvalidUsernameOrPassword
+		if isICQ {
+			loginErr = wire.LoginErrICQUserErr
 		}
-		// auth success
-		return wire.TLVRestBlock{
-			TLVList: []wire.TLV{
-				wire.NewTLV(wire.LoginTLVTagsScreenName, screenName),
-				wire.NewTLV(wire.LoginTLVTagsReconnectHere, net.JoinHostPort(s.config.OSCARHost, s.config.BOSPort)),
-				wire.NewTLV(wire.LoginTLVTagsAuthorizationCookie, cookie),
-			},
-		}, nil
+		return loginFailureResponse(screenName, loginErr), nil
+	}
+
+	if s.config.DisableAuth {
+		return s.loginSuccessResponse(screenName, isICQ, err)
+	}
+
+	var loginOK bool
+	// get the password from the appropriate TLV. older clients have a
+	// roasted password, newer clients have a hashed password. ICQ may omit
+	// the password TLV when logging in without saved password.
+	if md5Hash, hasMD5 := TLVList.Slice(wire.LoginTLVTagsPasswordHash); hasMD5 {
+		loginOK = user.ValidateHash(md5Hash)
+	} else if roastedPass, hasRoasted := TLVList.Slice(wire.LoginTLVTagsRoastedPassword); hasRoasted {
+		loginOK = user.ValidateRoastedPass(roastedPass)
+	}
+	if !loginOK {
+		return loginFailureResponse(screenName, wire.LoginErrInvalidPassword), nil
+	}
+
+	return s.loginSuccessResponse(screenName, isICQ, err)
+}
+
+func (s AuthService) loginSuccessResponse(screenName string, isICQ bool, err error) (wire.TLVRestBlock, error) {
+	loginCookie := bosCookie{
+		ScreenName: state.DisplayScreenName(screenName),
+	}
+	if isICQ {
+		loginCookie.ICQ = 1
+	}
+
+	buf := &bytes.Buffer{}
+	if err := wire.MarshalBE(loginCookie, buf); err != nil {
+		return wire.TLVRestBlock{}, err
+	}
+	cookie, err := s.cookieBaker.Issue(buf.Bytes())
+	if err != nil {
+		return wire.TLVRestBlock{}, fmt.Errorf("failed to make auth cookie: %w", err)
 	}
 
-	// auth failure
 	return wire.TLVRestBlock{
 		TLVList: []wire.TLV{
 			wire.NewTLV(wire.LoginTLVTagsScreenName, screenName),
-			wire.NewTLV(wire.LoginTLVTagsErrorSubcode, wire.LoginErrInvalidUsernameOrPassword),
+			wire.NewTLV(wire.LoginTLVTagsReconnectHere, net.JoinHostPort(s.config.OSCARHost, s.config.BOSPort)),
+			wire.NewTLV(wire.LoginTLVTagsAuthorizationCookie, cookie),
 		},
 	}, nil
 }
+
+func loginFailureResponse(screenName string, code uint16) wire.TLVRestBlock {
+	return wire.TLVRestBlock{
+		TLVList: []wire.TLV{
+			wire.NewTLV(wire.LoginTLVTagsScreenName, screenName),
+			wire.NewTLV(wire.LoginTLVTagsErrorSubcode, code),
+		},
+	}
+}

+ 234 - 180
foodgroup/auth_test.go

@@ -16,8 +16,9 @@ import (
 
 func TestAuthService_BUCPLoginRequest(t *testing.T) {
 	user := state.User{
-		IdentScreenName: state.NewIdentScreenName("screen_name"),
-		AuthKey:         "auth_key",
+		IdentScreenName:   state.NewIdentScreenName("screen_name"),
+		DisplayScreenName: "screen_name",
+		AuthKey:           "auth_key",
 	}
 	assert.NoError(t, user.HashPassword("the_password"))
 
@@ -39,7 +40,7 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 		wantErr error
 	}{
 		{
-			name: "user provides valid credentials and logs in successfully",
+			name: "AIM account exists, correct password, login OK",
 			cfg: config.Config{
 				OSCARHost: "127.0.0.1",
 				BOSPort:   "1234",
@@ -63,7 +64,14 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 				},
 				cookieIssuerParams: cookieIssuerParams{
 					{
-						data:   []byte(user.IdentScreenName.String()),
+						data: func() []byte {
+							loginCookie := bosCookie{
+								ScreenName: user.DisplayScreenName,
+							}
+							buf := &bytes.Buffer{}
+							assert.NoError(t, wire.MarshalBE(loginCookie, buf))
+							return buf.Bytes()
+						}(),
 						cookie: []byte("the-cookie"),
 					},
 				},
@@ -85,15 +93,15 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 			},
 		},
 		{
-			name: "user logs in with non-existent screen name--account is created and logged in successfully",
+			name: "ICQ account exists, correct password, login OK",
 			cfg: config.Config{
-				OSCARHost:   "127.0.0.1",
-				BOSPort:     "1234",
-				DisableAuth: true,
+				OSCARHost: "127.0.0.1",
+				BOSPort:   "1234",
 			},
 			inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{
 				TLVRestBlock: wire.TLVRestBlock{
 					TLVList: wire.TLVList{
+						wire.NewTLV(wire.LoginTLVTagsClientIdentity, "ICQ 2000b"),
 						wire.NewTLV(wire.LoginTLVTagsPasswordHash, user.StrongMD5Pass),
 						wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
 					},
@@ -104,25 +112,25 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 					getUserParams: getUserParams{
 						{
 							screenName: user.IdentScreenName,
-							result:     nil,
-						},
-					},
-					insertUserParams: insertUserParams{
-						{
-							user: user,
+							result:     &user,
 						},
 					},
 				},
 				cookieIssuerParams: cookieIssuerParams{
 					{
-						data:   []byte(user.IdentScreenName.String()),
+						data: func() []byte {
+							loginCookie := bosCookie{
+								ScreenName: user.DisplayScreenName,
+								ICQ:        1,
+							}
+							buf := &bytes.Buffer{}
+							assert.NoError(t, wire.MarshalBE(loginCookie, buf))
+							return buf.Bytes()
+						}(),
 						cookie: []byte("the-cookie"),
 					},
 				},
 			},
-			newUserFn: func(screenName state.DisplayScreenName) (state.User, error) {
-				return user, nil
-			},
 			expectOutput: wire.SNACMessage{
 				Frame: wire.SNACFrame{
 					FoodGroup: wire.BUCP,
@@ -140,16 +148,15 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 			},
 		},
 		{
-			name: "user logs in with invalid password--account is created and logged in successfully",
+			name: "AIM account exists, incorrect password, login fails",
 			cfg: config.Config{
-				OSCARHost:   "127.0.0.1",
-				BOSPort:     "1234",
-				DisableAuth: true,
+				OSCARHost: "127.0.0.1",
+				BOSPort:   "1234",
 			},
 			inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{
 				TLVRestBlock: wire.TLVRestBlock{
 					TLVList: wire.TLVList{
-						wire.NewTLV(wire.LoginTLVTagsPasswordHash, []byte("bad-password-hash")),
+						wire.NewTLV(wire.LoginTLVTagsPasswordHash, []byte("bad_password")),
 						wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
 					},
 				},
@@ -162,22 +169,8 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 							result:     &user,
 						},
 					},
-					insertUserParams: insertUserParams{
-						{
-							user: user,
-						},
-					},
-				},
-				cookieIssuerParams: cookieIssuerParams{
-					{
-						data:   []byte(user.IdentScreenName.String()),
-						cookie: []byte("the-cookie"),
-					},
 				},
 			},
-			newUserFn: func(screenName state.DisplayScreenName) (state.User, error) {
-				return user, nil
-			},
 			expectOutput: wire.SNACMessage{
 				Frame: wire.SNACFrame{
 					FoodGroup: wire.BUCP,
@@ -185,27 +178,25 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 				},
 				Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{
 					TLVRestBlock: wire.TLVRestBlock{
-						TLVList: wire.TLVList{
+						TLVList: []wire.TLV{
 							wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
-							wire.NewTLV(wire.LoginTLVTagsReconnectHere, "127.0.0.1:1234"),
-							wire.NewTLV(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")),
+							wire.NewTLV(wire.LoginTLVTagsErrorSubcode, wire.LoginErrInvalidPassword),
 						},
 					},
 				},
 			},
 		},
 		{
-			name: "user logs in with invalid password--account already exists and logged in successfully",
+			name: "AIM account doesn't exist, login fails",
 			cfg: config.Config{
-				OSCARHost:   "127.0.0.1",
-				BOSPort:     "1234",
-				DisableAuth: true,
+				OSCARHost: "127.0.0.1",
+				BOSPort:   "1234",
 			},
 			inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{
 				TLVRestBlock: wire.TLVRestBlock{
 					TLVList: wire.TLVList{
-						wire.NewTLV(wire.LoginTLVTagsPasswordHash, []byte("bad-password-hash")),
-						wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
+						wire.NewTLV(wire.LoginTLVTagsPasswordHash, []byte("password")),
+						wire.NewTLV(wire.LoginTLVTagsScreenName, []byte("non_existent_screen_name")),
 					},
 				},
 			},
@@ -213,26 +204,11 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 				userManagerParams: userManagerParams{
 					getUserParams: getUserParams{
 						{
-							screenName: user.IdentScreenName,
-							result:     &user,
-						},
-					},
-					insertUserParams: insertUserParams{
-						{
-							user: user,
-							err:  state.ErrDupUser,
+							screenName: state.NewIdentScreenName("non_existent_screen_name"),
+							result:     nil,
 						},
 					},
 				},
-				cookieIssuerParams: cookieIssuerParams{
-					{
-						data:   []byte(user.IdentScreenName.String()),
-						cookie: []byte("the-cookie"),
-					},
-				},
-			},
-			newUserFn: func(screenName state.DisplayScreenName) (state.User, error) {
-				return user, nil
 			},
 			expectOutput: wire.SNACMessage{
 				Frame: wire.SNACFrame{
@@ -241,25 +217,26 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 				},
 				Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{
 					TLVRestBlock: wire.TLVRestBlock{
-						TLVList: wire.TLVList{
-							wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
-							wire.NewTLV(wire.LoginTLVTagsReconnectHere, "127.0.0.1:1234"),
-							wire.NewTLV(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")),
+						TLVList: []wire.TLV{
+							wire.NewTLV(wire.LoginTLVTagsScreenName, state.NewIdentScreenName("non_existent_screen_name")),
+							wire.NewTLV(wire.LoginTLVTagsErrorSubcode, wire.LoginErrInvalidUsernameOrPassword),
 						},
 					},
 				},
 			},
 		},
 		{
-			name: "user provides invalid password--account creation fails due to user creation runtime error",
+			name: "ICQ account doesn't exist, login fails",
 			cfg: config.Config{
-				DisableAuth: true,
+				OSCARHost: "127.0.0.1",
+				BOSPort:   "1234",
 			},
 			inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{
 				TLVRestBlock: wire.TLVRestBlock{
 					TLVList: wire.TLVList{
-						wire.NewTLV(wire.LoginTLVTagsPasswordHash, []byte("bad-password-hash")),
-						wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
+						wire.NewTLV(wire.LoginTLVTagsClientIdentity, "ICQ 2000b"),
+						wire.NewTLV(wire.LoginTLVTagsPasswordHash, []byte("password")),
+						wire.NewTLV(wire.LoginTLVTagsScreenName, []byte("non_existent_uin")),
 					},
 				},
 			},
@@ -267,26 +244,38 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 				userManagerParams: userManagerParams{
 					getUserParams: getUserParams{
 						{
-							screenName: user.IdentScreenName,
-							result:     &user,
+							screenName: state.NewIdentScreenName("non_existent_uin"),
+							result:     nil,
 						},
 					},
 				},
 			},
-			newUserFn: func(screenName state.DisplayScreenName) (state.User, error) {
-				return user, io.EOF
+			expectOutput: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.BUCP,
+					SubGroup:  wire.BUCPLoginResponse,
+				},
+				Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: []wire.TLV{
+							wire.NewTLV(wire.LoginTLVTagsScreenName, state.NewIdentScreenName("non_existent_uin")),
+							wire.NewTLV(wire.LoginTLVTagsErrorSubcode, wire.LoginErrICQUserErr),
+						},
+					},
+				},
 			},
-			wantErr: io.EOF,
 		},
 		{
-			name: "user provides invalid password--account creation fails due to user upsert runtime error",
+			name: "account doesn't exist, authentication is disabled, account is created, login succeeds",
 			cfg: config.Config{
+				OSCARHost:   "127.0.0.1",
+				BOSPort:     "1234",
 				DisableAuth: true,
 			},
 			inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{
 				TLVRestBlock: wire.TLVRestBlock{
 					TLVList: wire.TLVList{
-						wire.NewTLV(wire.LoginTLVTagsPasswordHash, []byte("bad-password-hash")),
+						wire.NewTLV(wire.LoginTLVTagsPasswordHash, user.StrongMD5Pass),
 						wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
 					},
 				},
@@ -296,32 +285,59 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 					getUserParams: getUserParams{
 						{
 							screenName: user.IdentScreenName,
-							result:     &user,
+							result:     nil,
 						},
 					},
 					insertUserParams: insertUserParams{
 						{
 							user: user,
-							err:  io.EOF,
 						},
 					},
 				},
+				cookieIssuerParams: cookieIssuerParams{
+					{
+						data: func() []byte {
+							loginCookie := bosCookie{
+								ScreenName: user.DisplayScreenName,
+							}
+							buf := &bytes.Buffer{}
+							assert.NoError(t, wire.MarshalBE(loginCookie, buf))
+							return buf.Bytes()
+						}(),
+						cookie: []byte("the-cookie"),
+					},
+				},
 			},
 			newUserFn: func(screenName state.DisplayScreenName) (state.User, error) {
 				return user, nil
 			},
-			wantErr: io.EOF,
+			expectOutput: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.BUCP,
+					SubGroup:  wire.BUCPLoginResponse,
+				},
+				Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
+							wire.NewTLV(wire.LoginTLVTagsReconnectHere, "127.0.0.1:1234"),
+							wire.NewTLV(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")),
+						},
+					},
+				},
+			},
 		},
 		{
-			name: "user provides invalid password and receives invalid login response",
+			name: "account exists, password is invalid, authentication is disabled, login succeeds",
 			cfg: config.Config{
-				OSCARHost: "127.0.0.1",
-				BOSPort:   "1234",
+				OSCARHost:   "127.0.0.1",
+				BOSPort:     "1234",
+				DisableAuth: true,
 			},
 			inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{
 				TLVRestBlock: wire.TLVRestBlock{
 					TLVList: wire.TLVList{
-						wire.NewTLV(wire.LoginTLVTagsPasswordHash, []byte("bad_password")),
+						wire.NewTLV(wire.LoginTLVTagsPasswordHash, []byte("bad-password-hash")),
 						wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
 					},
 				},
@@ -331,10 +347,26 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 					getUserParams: getUserParams{
 						{
 							screenName: user.IdentScreenName,
-							result:     nil,
+							result:     &user,
 						},
 					},
 				},
+				cookieIssuerParams: cookieIssuerParams{
+					{
+						data: func() []byte {
+							loginCookie := bosCookie{
+								ScreenName: user.DisplayScreenName,
+							}
+							buf := &bytes.Buffer{}
+							assert.NoError(t, wire.MarshalBE(loginCookie, buf))
+							return buf.Bytes()
+						}(),
+						cookie: []byte("the-cookie"),
+					},
+				},
+			},
+			newUserFn: func(screenName state.DisplayScreenName) (state.User, error) {
+				return user, nil
 			},
 			expectOutput: wire.SNACMessage{
 				Frame: wire.SNACFrame{
@@ -345,7 +377,8 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 					TLVRestBlock: wire.TLVRestBlock{
 						TLVList: wire.TLVList{
 							wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
-							wire.NewTLV(wire.LoginTLVTagsErrorSubcode, uint16(0x01)),
+							wire.NewTLV(wire.LoginTLVTagsReconnectHere, "127.0.0.1:1234"),
+							wire.NewTLV(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")),
 						},
 					},
 				},
@@ -411,12 +444,13 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 
 func TestAuthService_FLAPLoginResponse(t *testing.T) {
 	user := state.User{
-		IdentScreenName: state.NewIdentScreenName("screen_name"),
-		AuthKey:         "auth_key",
+		AuthKey:           "auth_key",
+		DisplayScreenName: "screen_name",
+		IdentScreenName:   state.NewIdentScreenName("screen_name"),
 	}
 	assert.NoError(t, user.HashPassword("the_password"))
 
-	// obfuscated password value: "the_password"
+	// roastedPassword the roasted form of "the_password"
 	roastedPassword := []byte{0x87, 0x4E, 0xE4, 0x9B, 0x49, 0xE7, 0xA8, 0xE1, 0x06, 0xCC, 0xCB, 0x82}
 
 	cases := []struct {
@@ -437,7 +471,7 @@ func TestAuthService_FLAPLoginResponse(t *testing.T) {
 		wantErr error
 	}{
 		{
-			name: "user provides valid credentials and logs in successfully",
+			name: "AIM account exists, correct password, login OK",
 			cfg: config.Config{
 				OSCARHost: "127.0.0.1",
 				BOSPort:   "1234",
@@ -461,7 +495,14 @@ func TestAuthService_FLAPLoginResponse(t *testing.T) {
 				},
 				cookieIssuerParams: cookieIssuerParams{
 					{
-						data:   []byte(user.IdentScreenName.String()),
+						data: func() []byte {
+							loginCookie := bosCookie{
+								ScreenName: user.DisplayScreenName,
+							}
+							buf := &bytes.Buffer{}
+							assert.NoError(t, wire.MarshalBE(loginCookie, buf))
+							return buf.Bytes()
+						}(),
 						cookie: []byte("the-cookie"),
 					},
 				},
@@ -475,15 +516,15 @@ func TestAuthService_FLAPLoginResponse(t *testing.T) {
 			},
 		},
 		{
-			name: "user logs in with non-existent screen name--account is created and logged in successfully",
+			name: "ICQ account exists, correct password, login OK",
 			cfg: config.Config{
-				OSCARHost:   "127.0.0.1",
-				BOSPort:     "1234",
-				DisableAuth: true,
+				OSCARHost: "127.0.0.1",
+				BOSPort:   "1234",
 			},
 			inputSNAC: wire.FLAPSignonFrame{
 				TLVRestBlock: wire.TLVRestBlock{
 					TLVList: wire.TLVList{
+						wire.NewTLV(wire.LoginTLVTagsClientIdentity, "ICQ 2000b"),
 						wire.NewTLV(wire.LoginTLVTagsRoastedPassword, roastedPassword),
 						wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
 					},
@@ -494,25 +535,25 @@ func TestAuthService_FLAPLoginResponse(t *testing.T) {
 					getUserParams: getUserParams{
 						{
 							screenName: user.IdentScreenName,
-							result:     nil,
-						},
-					},
-					insertUserParams: insertUserParams{
-						{
-							user: user,
+							result:     &user,
 						},
 					},
 				},
 				cookieIssuerParams: cookieIssuerParams{
 					{
-						data:   []byte(user.IdentScreenName.String()),
+						data: func() []byte {
+							loginCookie := bosCookie{
+								ScreenName: user.DisplayScreenName,
+								ICQ:        1,
+							}
+							buf := &bytes.Buffer{}
+							assert.NoError(t, wire.MarshalBE(loginCookie, buf))
+							return buf.Bytes()
+						}(),
 						cookie: []byte("the-cookie"),
 					},
 				},
 			},
-			newUserFn: func(screenName state.DisplayScreenName) (state.User, error) {
-				return user, nil
-			},
 			expectOutput: wire.TLVRestBlock{
 				TLVList: wire.TLVList{
 					wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
@@ -522,16 +563,15 @@ func TestAuthService_FLAPLoginResponse(t *testing.T) {
 			},
 		},
 		{
-			name: "user logs in with invalid password--account is created and logged in successfully",
+			name: "AIM account exists, incorrect password, login fails",
 			cfg: config.Config{
-				OSCARHost:   "127.0.0.1",
-				BOSPort:     "1234",
-				DisableAuth: true,
+				OSCARHost: "127.0.0.1",
+				BOSPort:   "1234",
 			},
 			inputSNAC: wire.FLAPSignonFrame{
 				TLVRestBlock: wire.TLVRestBlock{
 					TLVList: wire.TLVList{
-						wire.NewTLV(wire.LoginTLVTagsPasswordHash, []byte("bad-roasted-password")),
+						wire.NewTLV(wire.LoginTLVTagsRoastedPassword, []byte("bad_roasted_password")),
 						wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
 					},
 				},
@@ -544,42 +584,26 @@ func TestAuthService_FLAPLoginResponse(t *testing.T) {
 							result:     &user,
 						},
 					},
-					insertUserParams: insertUserParams{
-						{
-							user: user,
-						},
-					},
-				},
-				cookieIssuerParams: cookieIssuerParams{
-					{
-						data:   []byte(user.IdentScreenName.String()),
-						cookie: []byte("the-cookie"),
-					},
 				},
 			},
-			newUserFn: func(screenName state.DisplayScreenName) (state.User, error) {
-				return user, nil
-			},
 			expectOutput: wire.TLVRestBlock{
-				TLVList: wire.TLVList{
+				TLVList: []wire.TLV{
 					wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
-					wire.NewTLV(wire.LoginTLVTagsReconnectHere, "127.0.0.1:1234"),
-					wire.NewTLV(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")),
+					wire.NewTLV(wire.LoginTLVTagsErrorSubcode, wire.LoginErrInvalidPassword),
 				},
 			},
 		},
 		{
-			name: "user logs in with invalid password--account already exists and logged in successfully",
+			name: "AIM account doesn't exist, login fails",
 			cfg: config.Config{
-				OSCARHost:   "127.0.0.1",
-				BOSPort:     "1234",
-				DisableAuth: true,
+				OSCARHost: "127.0.0.1",
+				BOSPort:   "1234",
 			},
 			inputSNAC: wire.FLAPSignonFrame{
 				TLVRestBlock: wire.TLVRestBlock{
 					TLVList: wire.TLVList{
-						wire.NewTLV(wire.LoginTLVTagsPasswordHash, []byte("bad-roasted-password")),
-						wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
+						wire.NewTLV(wire.LoginTLVTagsRoastedPassword, roastedPassword),
+						wire.NewTLV(wire.LoginTLVTagsScreenName, []byte("non_existent_screen_name")),
 					},
 				},
 			},
@@ -587,45 +611,31 @@ func TestAuthService_FLAPLoginResponse(t *testing.T) {
 				userManagerParams: userManagerParams{
 					getUserParams: getUserParams{
 						{
-							screenName: user.IdentScreenName,
-							result:     &user,
-						},
-					},
-					insertUserParams: insertUserParams{
-						{
-							user: user,
-							err:  state.ErrDupUser,
+							screenName: state.NewIdentScreenName("non_existent_screen_name"),
+							result:     nil,
 						},
 					},
 				},
-				cookieIssuerParams: cookieIssuerParams{
-					{
-						data:   []byte(user.IdentScreenName.String()),
-						cookie: []byte("the-cookie"),
-					},
-				},
-			},
-			newUserFn: func(screenName state.DisplayScreenName) (state.User, error) {
-				return user, nil
 			},
 			expectOutput: wire.TLVRestBlock{
-				TLVList: wire.TLVList{
-					wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
-					wire.NewTLV(wire.LoginTLVTagsReconnectHere, "127.0.0.1:1234"),
-					wire.NewTLV(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")),
+				TLVList: []wire.TLV{
+					wire.NewTLV(wire.LoginTLVTagsScreenName, state.NewIdentScreenName("non_existent_screen_name")),
+					wire.NewTLV(wire.LoginTLVTagsErrorSubcode, wire.LoginErrInvalidUsernameOrPassword),
 				},
 			},
 		},
 		{
-			name: "user provides invalid password--account creation fails due to user creation runtime error",
+			name: "ICQ account doesn't exist, login fails",
 			cfg: config.Config{
-				DisableAuth: true,
+				OSCARHost: "127.0.0.1",
+				BOSPort:   "1234",
 			},
 			inputSNAC: wire.FLAPSignonFrame{
 				TLVRestBlock: wire.TLVRestBlock{
 					TLVList: wire.TLVList{
-						wire.NewTLV(wire.LoginTLVTagsPasswordHash, []byte("bad-roasted-password")),
-						wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
+						wire.NewTLV(wire.LoginTLVTagsClientIdentity, "ICQ 2000b"),
+						wire.NewTLV(wire.LoginTLVTagsRoastedPassword, roastedPassword),
+						wire.NewTLV(wire.LoginTLVTagsScreenName, []byte("non_existent_uin")),
 					},
 				},
 			},
@@ -633,26 +643,30 @@ func TestAuthService_FLAPLoginResponse(t *testing.T) {
 				userManagerParams: userManagerParams{
 					getUserParams: getUserParams{
 						{
-							screenName: user.IdentScreenName,
-							result:     &user,
+							screenName: state.NewIdentScreenName("non_existent_uin"),
+							result:     nil,
 						},
 					},
 				},
 			},
-			newUserFn: func(screenName state.DisplayScreenName) (state.User, error) {
-				return user, io.EOF
+			expectOutput: wire.TLVRestBlock{
+				TLVList: []wire.TLV{
+					wire.NewTLV(wire.LoginTLVTagsScreenName, state.NewIdentScreenName("non_existent_uin")),
+					wire.NewTLV(wire.LoginTLVTagsErrorSubcode, wire.LoginErrICQUserErr),
+				},
 			},
-			wantErr: io.EOF,
 		},
 		{
-			name: "user provides invalid password--account creation fails due to user upsert runtime error",
+			name: "account doesn't exist, authentication is disabled, account is created, login succeeds",
 			cfg: config.Config{
+				OSCARHost:   "127.0.0.1",
+				BOSPort:     "1234",
 				DisableAuth: true,
 			},
 			inputSNAC: wire.FLAPSignonFrame{
 				TLVRestBlock: wire.TLVRestBlock{
 					TLVList: wire.TLVList{
-						wire.NewTLV(wire.LoginTLVTagsPasswordHash, []byte("bad-roasted-password")),
+						wire.NewTLV(wire.LoginTLVTagsRoastedPassword, roastedPassword),
 						wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
 					},
 				},
@@ -662,32 +676,51 @@ func TestAuthService_FLAPLoginResponse(t *testing.T) {
 					getUserParams: getUserParams{
 						{
 							screenName: user.IdentScreenName,
-							result:     &user,
+							result:     nil,
 						},
 					},
 					insertUserParams: insertUserParams{
 						{
 							user: user,
-							err:  io.EOF,
 						},
 					},
 				},
+				cookieIssuerParams: cookieIssuerParams{
+					{
+						data: func() []byte {
+							loginCookie := bosCookie{
+								ScreenName: user.DisplayScreenName,
+							}
+							buf := &bytes.Buffer{}
+							assert.NoError(t, wire.MarshalBE(loginCookie, buf))
+							return buf.Bytes()
+						}(),
+						cookie: []byte("the-cookie"),
+					},
+				},
 			},
 			newUserFn: func(screenName state.DisplayScreenName) (state.User, error) {
 				return user, nil
 			},
-			wantErr: io.EOF,
+			expectOutput: wire.TLVRestBlock{
+				TLVList: wire.TLVList{
+					wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
+					wire.NewTLV(wire.LoginTLVTagsReconnectHere, "127.0.0.1:1234"),
+					wire.NewTLV(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")),
+				},
+			},
 		},
 		{
-			name: "user provides invalid password and receives invalid login response",
+			name: "account exists, password is invalid, authentication is disabled, login succeeds",
 			cfg: config.Config{
-				OSCARHost: "127.0.0.1",
-				BOSPort:   "1234",
+				OSCARHost:   "127.0.0.1",
+				BOSPort:     "1234",
+				DisableAuth: true,
 			},
 			inputSNAC: wire.FLAPSignonFrame{
 				TLVRestBlock: wire.TLVRestBlock{
 					TLVList: wire.TLVList{
-						wire.NewTLV(wire.LoginTLVTagsPasswordHash, []byte("bad-roasted-password")),
+						wire.NewTLV(wire.LoginTLVTagsRoastedPassword, "bad-roasted-password"),
 						wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
 					},
 				},
@@ -697,15 +730,32 @@ func TestAuthService_FLAPLoginResponse(t *testing.T) {
 					getUserParams: getUserParams{
 						{
 							screenName: user.IdentScreenName,
-							result:     nil,
+							result:     &user,
 						},
 					},
 				},
+				cookieIssuerParams: cookieIssuerParams{
+					{
+						data: func() []byte {
+							loginCookie := bosCookie{
+								ScreenName: user.DisplayScreenName,
+							}
+							buf := &bytes.Buffer{}
+							assert.NoError(t, wire.MarshalBE(loginCookie, buf))
+							return buf.Bytes()
+						}(),
+						cookie: []byte("the-cookie"),
+					},
+				},
+			},
+			newUserFn: func(screenName state.DisplayScreenName) (state.User, error) {
+				return user, nil
 			},
 			expectOutput: wire.TLVRestBlock{
 				TLVList: wire.TLVList{
 					wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
-					wire.NewTLV(wire.LoginTLVTagsErrorSubcode, uint16(0x01)),
+					wire.NewTLV(wire.LoginTLVTagsReconnectHere, "127.0.0.1:1234"),
+					wire.NewTLV(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")),
 				},
 			},
 		},
@@ -714,7 +764,7 @@ func TestAuthService_FLAPLoginResponse(t *testing.T) {
 			inputSNAC: wire.FLAPSignonFrame{
 				TLVRestBlock: wire.TLVRestBlock{
 					TLVList: wire.TLVList{
-						wire.NewTLV(wire.LoginTLVTagsPasswordHash, user.StrongMD5Pass),
+						wire.NewTLV(wire.LoginTLVTagsRoastedPassword, roastedPassword),
 						wire.NewTLV(wire.LoginTLVTagsScreenName, user.IdentScreenName),
 					},
 				},
@@ -972,12 +1022,16 @@ func TestAuthService_RegisterBOSSession_HappyPath(t *testing.T) {
 		AddSession(sess.DisplayScreenName()).
 		Return(sess)
 
-	authCookie := []byte(`the-auth-cookie`)
+	authCookie := bosCookie{
+		ScreenName: sess.DisplayScreenName(),
+	}
+	cookieBuf := &bytes.Buffer{}
+	assert.NoError(t, wire.MarshalBE(authCookie, cookieBuf))
 
 	cookieBaker := newMockCookieBaker(t)
 	cookieBaker.EXPECT().
-		Crack(authCookie).
-		Return([]byte("screen-name"), nil)
+		Crack(cookieBuf.Bytes()).
+		Return(cookieBuf.Bytes(), nil)
 
 	userManager := newMockUserManager(t)
 	userManager.EXPECT().
@@ -991,7 +1045,7 @@ func TestAuthService_RegisterBOSSession_HappyPath(t *testing.T) {
 
 	svc := NewAuthService(config.Config{}, sessionManager, nil, userManager, nil, cookieBaker, nil, nil, nil, accountManager)
 
-	have, err := svc.RegisterBOSSession(authCookie)
+	have, err := svc.RegisterBOSSession(cookieBuf.Bytes())
 	assert.NoError(t, err)
 	assert.Equal(t, sess, have)
 }

+ 6 - 0
server/oscar/auth.go

@@ -101,6 +101,12 @@ func (rt AuthServer) processBUCPAuth(flapc *wire.FlapClient, err error) error {
 		return err
 	}
 
+	if outSNAC.Frame.SubGroup == wire.BUCPLoginResponse {
+		screenName, _ := challengeRequest.String(wire.LoginTLVTagsScreenName)
+		rt.Logger.Debug("failed BUCP challenge: user does not exist", "screen_name", screenName)
+		return nil // account does not exist
+	}
+
 	loginRequest := wire.SNAC_0x17_0x02_BUCPLoginRequest{}
 	if err := flapc.ReceiveSNAC(&wire.SNACFrame{}, &loginRequest); err != nil {
 		return err

+ 4 - 1
wire/snacs.go

@@ -83,6 +83,7 @@ const (
 const (
 	LoginTLVTagsScreenName          uint16 = 0x01
 	LoginTLVTagsRoastedPassword     uint16 = 0x02
+	LoginTLVTagsClientIdentity      uint16 = 0x03
 	LoginTLVTagsReconnectHere       uint16 = 0x05
 	LoginTLVTagsAuthorizationCookie uint16 = 0x06
 	LoginTLVTagsErrorSubcode        uint16 = 0x08
@@ -90,7 +91,9 @@ const (
 )
 
 const (
-	LoginErrInvalidUsernameOrPassword uint16 = 0x01
+	LoginErrInvalidUsernameOrPassword uint16 = 0x0001
+	LoginErrInvalidPassword           uint16 = 0x0005 // invalid password
+	LoginErrICQUserErr                uint16 = 0x0008 // ICQ user doesn't exist
 )
 
 //