auth.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746
  1. package foodgroup
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/xml"
  6. "errors"
  7. "fmt"
  8. "log/slog"
  9. "strconv"
  10. "strings"
  11. "time"
  12. "github.com/mk6i/open-oscar-server/config"
  13. "github.com/mk6i/open-oscar-server/state"
  14. "github.com/mk6i/open-oscar-server/wire"
  15. "github.com/google/uuid"
  16. )
  17. // MaxConcurrentLoginsPerUser is the maximum number of concurrent logins allowed
  18. // for a single user.
  19. const MaxConcurrentLoginsPerUser = 5
  20. // NewAuthService creates a new instance of AuthService.
  21. func NewAuthService(
  22. cfg config.Config,
  23. sessionManager SessionRegistry,
  24. sessionRetriever SessionRetriever,
  25. chatSessionRegistry ChatSessionRegistry,
  26. userManager UserManager,
  27. cookieBaker CookieBaker,
  28. chatMessageRelayer ChatMessageRelayer,
  29. accountManager AccountManager,
  30. bartItemManager BARTItemManager,
  31. feedbagManager FeedbagManager,
  32. classes wire.RateLimitClasses,
  33. createAccount state.CreateAccountFunc,
  34. logger *slog.Logger,
  35. ) *AuthService {
  36. return &AuthService{
  37. chatSessionRegistry: chatSessionRegistry,
  38. config: cfg,
  39. cookieBaker: cookieBaker,
  40. sessionManager: sessionManager,
  41. sessionRetriever: sessionRetriever,
  42. userManager: userManager,
  43. chatMessageRelayer: chatMessageRelayer,
  44. accountManager: accountManager,
  45. bartItemManager: bartItemManager,
  46. feedbagManager: feedbagManager,
  47. rateLimitClasses: classes,
  48. timeNow: time.Now,
  49. maxConcurrentLoginsPerUser: MaxConcurrentLoginsPerUser,
  50. createAccount: createAccount,
  51. logger: logger,
  52. }
  53. }
  54. // AuthService provides client login and session management services. It
  55. // supports both FLAP (AIM v1.0-v3.0) and BUCP (AIM v3.5-v5.9) authentication
  56. // modes.
  57. type AuthService struct {
  58. chatMessageRelayer ChatMessageRelayer
  59. chatSessionRegistry ChatSessionRegistry
  60. config config.Config
  61. cookieBaker CookieBaker
  62. logger *slog.Logger
  63. sessionManager SessionRegistry
  64. sessionRetriever SessionRetriever
  65. userManager UserManager
  66. accountManager AccountManager
  67. bartItemManager BARTItemManager
  68. feedbagManager FeedbagManager
  69. rateLimitClasses wire.RateLimitClasses
  70. timeNow func() time.Time
  71. maxConcurrentLoginsPerUser int
  72. createAccount state.CreateAccountFunc
  73. }
  74. // RegisterChatSession adds a user to a chat room. The authCookie param is an
  75. // opaque token returned by {{OServiceService.ServiceRequest}} that identifies
  76. // the user and chat room. It returns the session object registered in the
  77. // ChatSessionRegistry.
  78. // This method does not verify that the user and chat room exist because it
  79. // implicitly trusts the contents of the token signed by
  80. // {{OServiceService.ServiceRequest}}.
  81. func (s AuthService) RegisterChatSession(ctx context.Context, authCookie state.ServerCookie, sessCfg func(sess *state.Session)) (*state.SessionInstance, error) {
  82. sess, err := s.chatSessionRegistry.AddSession(ctx, authCookie.ChatCookie, authCookie.ScreenName, sessCfg)
  83. if err != nil {
  84. return nil, fmt.Errorf("AddSession: %w", err)
  85. }
  86. sess.Session().SetRateClasses(time.Now(), s.rateLimitClasses)
  87. return sess, err
  88. }
  89. func (s AuthService) CrackCookie(authCookie []byte) (state.ServerCookie, error) {
  90. c := state.ServerCookie{}
  91. buf, err := s.cookieBaker.Crack(authCookie)
  92. if err != nil {
  93. return c, err
  94. }
  95. if err := wire.UnmarshalBE(&c, bytes.NewBuffer(buf)); err != nil {
  96. return c, err
  97. }
  98. return c, nil
  99. }
  100. // RegisterBOSSession adds a new session to the session registry.
  101. func (s AuthService) RegisterBOSSession(ctx context.Context, authCookie state.ServerCookie, sessCfg func(sess *state.Session)) (*state.SessionInstance, error) {
  102. u, err := s.userManager.User(ctx, authCookie.ScreenName.IdentScreenName())
  103. if err != nil {
  104. return nil, fmt.Errorf("failed to retrieve user: %w", err)
  105. }
  106. if u == nil {
  107. return nil, fmt.Errorf("user not found")
  108. }
  109. ctx, cancel := context.WithTimeout(ctx, time.Second*5)
  110. defer cancel()
  111. flag := wire.MultiConnFlag(authCookie.MultiConnFlag)
  112. doMultiSess := flag == wire.MultiConnFlagsRecentClient
  113. cfg := func(sess *state.Session) {
  114. sess.SetSignonTime(time.Now())
  115. sess.SetRateClasses(time.Now(), s.rateLimitClasses)
  116. sess.SetMemberSince(time.Now())
  117. }
  118. sess, err := s.sessionManager.AddSession(ctx, u.DisplayScreenName, doMultiSess, sessCfg, cfg)
  119. if err != nil {
  120. return nil, fmt.Errorf("AddSession: %w", err)
  121. }
  122. // set the unconfirmed user info flag if this account is unconfirmed
  123. if confirmed, err := s.accountManager.ConfirmStatus(ctx, sess.IdentScreenName()); err != nil {
  124. return nil, fmt.Errorf("error setting unconfirmed user flag: %w", err)
  125. } else if !confirmed {
  126. sess.SetUserInfoFlag(wire.OServiceUserFlagUnconfirmed)
  127. }
  128. if u.IsBot {
  129. sess.SetUserInfoFlag(wire.OServiceUserFlagBot)
  130. }
  131. sess.SetKerberosAuth(authCookie.KerberosAuth == 1)
  132. // set string containing OSCAR client name and version
  133. sess.SetClientID(authCookie.ClientID)
  134. sess.Session().SetOfflineMsgCount(u.OfflineMsgCount)
  135. if _, alreadySet := sess.Session().BuddyIcon(); !alreadySet {
  136. bartID, err := s.bartItemManager.BuddyIconMetadata(ctx, sess.IdentScreenName())
  137. if err != nil {
  138. return nil, fmt.Errorf("BuddyIconMetadata: %w", err)
  139. }
  140. if bartID != nil {
  141. sess.Session().SetBuddyIcon(*bartID)
  142. }
  143. }
  144. // indicate whether the client supports/wants multiple concurrent sessions
  145. sess.SetMultiConnFlag(flag)
  146. if u.DisplayScreenName.IsUIN() {
  147. sess.SetUserInfoFlag(wire.OServiceUserFlagICQ)
  148. uin, err := strconv.Atoi(u.IdentScreenName.String())
  149. if err != nil {
  150. return nil, fmt.Errorf("error converting username to UIN: %w", err)
  151. }
  152. sess.Session().SetUIN(uint32(uin))
  153. }
  154. return sess, nil
  155. }
  156. // RetrieveBOSSession returns a user's existing session instance
  157. func (s AuthService) RetrieveBOSSession(ctx context.Context, serverCookie state.ServerCookie) (*state.SessionInstance, error) {
  158. u, err := s.userManager.User(ctx, serverCookie.ScreenName.IdentScreenName())
  159. if err != nil {
  160. return nil, fmt.Errorf("failed to retrieve user: %w", err)
  161. }
  162. if u == nil {
  163. return nil, fmt.Errorf("user not found")
  164. }
  165. sess := s.sessionRetriever.RetrieveSession(u.IdentScreenName)
  166. if sess == nil {
  167. return nil, nil
  168. }
  169. return sess.Instance(serverCookie.SessionNum), nil
  170. }
  171. // Signout removes this user's session.
  172. func (s AuthService) Signout(ctx context.Context, session *state.Session) {
  173. s.sessionManager.RemoveSession(session)
  174. }
  175. // SignoutChat removes user from chat room and notifies remaining participants
  176. // of their departure.
  177. func (s AuthService) SignoutChat(ctx context.Context, sess *state.Session) {
  178. alertUserLeft(ctx, sess, s.chatMessageRelayer)
  179. s.chatSessionRegistry.RemoveSession(sess)
  180. }
  181. // BUCPChallenge processes a BUCP authentication challenge request. It
  182. // retrieves the user's auth key based on the screen name provided in the
  183. // request. The client uses the auth key to salt the MD5 password hash provided
  184. // in the subsequent login request. If the account is valid, return
  185. // SNAC(0x17,0x07), otherwise return SNAC(0x17,0x03).
  186. func (s AuthService) BUCPChallenge(ctx context.Context, inBody wire.SNAC_0x17_0x06_BUCPChallengeRequest, newUUID func() uuid.UUID) (wire.SNACMessage, error) {
  187. screenName, exists := inBody.String(wire.LoginTLVTagsScreenName)
  188. if !exists {
  189. s.logger.Debug("BUCPChallenge: screen name TLV not found in request")
  190. return wire.SNACMessage{}, errors.New("screen name doesn't exist in tlv")
  191. }
  192. s.logger.Debug("BUCPChallenge: received challenge request",
  193. "screen_name", screenName,
  194. "is_uin", state.DisplayScreenName(screenName).IsUIN())
  195. var authKey string
  196. user, err := s.userManager.User(ctx, state.NewIdentScreenName(screenName))
  197. if err != nil {
  198. s.logger.Error("BUCPChallenge: user lookup failed", "screen_name", screenName, "err", err.Error())
  199. return wire.SNACMessage{}, err
  200. }
  201. switch {
  202. case user != nil:
  203. // user lookup succeeded
  204. authKey = user.AuthKey
  205. s.logger.Debug("BUCPChallenge: user found, returning auth key",
  206. "screen_name", screenName,
  207. "auth_key_len", len(authKey))
  208. case s.config.DisableAuth:
  209. // can't find user, generate stub auth key
  210. authKey = newUUID().String()
  211. s.logger.Debug("BUCPChallenge: user not found, auth disabled, generating stub auth key",
  212. "screen_name", screenName)
  213. default:
  214. // can't find user, return login error
  215. s.logger.Debug("BUCPChallenge: user not found, returning error",
  216. "screen_name", screenName,
  217. "error_code", wire.LoginErrInvalidUsernameOrPassword)
  218. return wire.SNACMessage{
  219. Frame: wire.SNACFrame{
  220. FoodGroup: wire.BUCP,
  221. SubGroup: wire.BUCPLoginResponse,
  222. },
  223. Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{
  224. TLVRestBlock: wire.TLVRestBlock{
  225. TLVList: []wire.TLV{
  226. wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, wire.LoginErrInvalidUsernameOrPassword),
  227. },
  228. },
  229. },
  230. }, nil
  231. }
  232. return wire.SNACMessage{
  233. Frame: wire.SNACFrame{
  234. FoodGroup: wire.BUCP,
  235. SubGroup: wire.BUCPChallengeResponse,
  236. },
  237. Body: wire.SNAC_0x17_0x07_BUCPChallengeResponse{
  238. AuthKey: authKey,
  239. },
  240. }, nil
  241. }
  242. // BUCPLogin processes a BUCP authentication request for AIM v3.5-v5.9. Upon
  243. // successful login, a session is created.
  244. // If login credentials are invalid and app config DisableAuth is true, a stub
  245. // user is created and login continues as normal. DisableAuth allows you to
  246. // skip the account creation procedure, which simplifies the login flow during
  247. // development.
  248. // If login is successful, the SNAC TLV list contains the BOS server address
  249. // (wire.LoginTLVTagsReconnectHere) and an authorization cookie
  250. // (wire.LoginTLVTagsAuthorizationCookie). Else, an error code is set
  251. // (wire.LoginTLVTagsErrorSubcode).
  252. func (s AuthService) BUCPLogin(ctx context.Context, inBody wire.SNAC_0x17_0x02_BUCPLoginRequest, advertisedHost string) (wire.SNACMessage, error) {
  253. block, err := s.login(ctx, inBody.TLVList, advertisedHost)
  254. if err != nil {
  255. return wire.SNACMessage{}, err
  256. }
  257. return wire.SNACMessage{
  258. Frame: wire.SNACFrame{
  259. FoodGroup: wire.BUCP,
  260. SubGroup: wire.BUCPLoginResponse,
  261. },
  262. Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{
  263. TLVRestBlock: block,
  264. },
  265. }, nil
  266. }
  267. // FLAPLogin processes a FLAP authentication request for AIM v1.0-v3.0. Upon
  268. // successful login, a session is created.
  269. // If login credentials are invalid and app config DisableAuth is true, a stub
  270. // user is created and login continues as normal. DisableAuth allows you to
  271. // skip the account creation procedure, which simplifies the login flow during
  272. // development.
  273. // If login is successful, the SNAC TLV list contains the BOS server address
  274. // (wire.LoginTLVTagsReconnectHere) and an authorization cookie
  275. // (wire.LoginTLVTagsAuthorizationCookie). Else, an error code is set
  276. // (wire.LoginTLVTagsErrorSubcode).
  277. func (s AuthService) FLAPLogin(ctx context.Context, inFrame wire.FLAPSignonFrame, advertisedHost string) (wire.TLVRestBlock, error) {
  278. return s.login(ctx, inFrame.TLVList, advertisedHost)
  279. }
  280. // KerberosLogin handles AIM-style Kerberos authentication for AIM 6.0+.
  281. // Credit for understanding the SNAC structure and values goes to this mailing
  282. // list attachment from 2007:
  283. //
  284. // https://web.archive.org/web/20100619063015/http://pidgin.im/pipermail/devel/attachments/20070906/e0069ff5/attachment-0001.txt
  285. //
  286. // Several values in the response are poorly understood but necessary for proper
  287. // processing on the client side.
  288. func (s AuthService) KerberosLogin(ctx context.Context, inBody wire.SNAC_0x050C_0x0002_KerberosLoginRequest, advertisedHost string) (wire.SNACMessage, error) {
  289. b, ok := inBody.TicketRequestMetadata.Bytes(wire.KerberosTLVTicketRequest)
  290. if !ok {
  291. return wire.SNACMessage{}, fmt.Errorf("ticket request metadata bytes is missing")
  292. }
  293. var info wire.KerberosLoginRequestTicket
  294. if err := wire.UnmarshalBE(&info, bytes.NewReader(b)); err != nil {
  295. return wire.SNACMessage{}, fmt.Errorf("ticket request metadata unmarshal: %w", err)
  296. }
  297. list := wire.TLVList{
  298. wire.NewTLVBE(wire.LoginTLVTagsScreenName, inBody.ClientPrincipal),
  299. wire.NewTLVBE(wire.LoginTLVTagsMultiConnFlags, wire.MultiConnFlagsRecentClient),
  300. }
  301. if info.Version >= 4 {
  302. list = append(list, wire.NewTLVBE(wire.LoginTLVTagsRoastedKerberosPassword, info.Password))
  303. } else {
  304. list = append(list, wire.NewTLVBE(wire.LoginTLVTagsPlaintextKerberosPassword, info.Password))
  305. }
  306. result, err := s.login(ctx, list, advertisedHost)
  307. if err != nil {
  308. return wire.SNACMessage{}, fmt.Errorf("login: %w", err)
  309. }
  310. cookie, loginOK := result.Bytes(wire.LoginTLVTagsAuthorizationCookie)
  311. if !loginOK {
  312. return wire.SNACMessage{
  313. Frame: wire.SNACFrame{
  314. FoodGroup: wire.Kerberos,
  315. SubGroup: wire.KerberosKerberosLoginErrResponse,
  316. },
  317. Body: wire.SNAC_0x050C_0x0004_KerberosLoginErrResponse{
  318. KerbRequestID: inBody.RequestID,
  319. ScreenName: inBody.ClientPrincipal,
  320. ErrCode: wire.KerberosErrAuthFailure,
  321. Message: "Auth failure",
  322. },
  323. }, nil
  324. }
  325. return wire.SNACMessage{
  326. Frame: wire.SNACFrame{
  327. FoodGroup: wire.Kerberos,
  328. SubGroup: wire.KerberosLoginSuccessResponse,
  329. },
  330. Body: wire.SNAC_0x050C_0x0003_KerberosLoginSuccessResponse{
  331. RequestID: inBody.RequestID,
  332. Epoch: uint32(s.timeNow().Unix()),
  333. ClientPrincipal: inBody.ClientPrincipal,
  334. ClientRealm: "AOL",
  335. Tickets: []wire.KerberosTicket{
  336. {
  337. PVNO: 5,
  338. EncTicket: []byte{},
  339. TicketRealm: "AOL",
  340. ServicePrincipal: "im/boss",
  341. ClientRealm: "AOL",
  342. ClientPrincipal: inBody.ClientPrincipal,
  343. AuthTime: uint32(s.timeNow().Unix()),
  344. StartTime: uint32(s.timeNow().Unix()),
  345. EndTime: uint32(s.timeNow().Add(24 * time.Hour).Unix()),
  346. Unknown4: 1610612736,
  347. Unknown5: 1073741824,
  348. ConnectionMetadata: wire.TLVBlock{
  349. TLVList: wire.TLVList{
  350. wire.NewTLVBE(wire.KerberosTLVBOSServerInfo, wire.KerberosBOSServerInfo{
  351. Unknown: 1,
  352. ConnectionInfo: wire.TLVBlock{
  353. TLVList: wire.TLVList{
  354. wire.NewTLVBE(wire.KerberosTLVHostname, advertisedHost),
  355. wire.NewTLVBE(wire.KerberosTLVCookie, cookie),
  356. wire.NewTLVBE(wire.KerberosTLVConnSettings, wire.KerberosConnUseSSL),
  357. },
  358. },
  359. }),
  360. },
  361. },
  362. },
  363. },
  364. },
  365. }, nil
  366. }
  367. // loginProperties represents the properties sent by the client at login.
  368. type loginProperties struct {
  369. clientID string
  370. isBUCPAuth bool
  371. isFLAPAuth bool
  372. isFLAPJavaAuth bool
  373. isKerberosPlaintextAuth bool
  374. isKerberosRoastedAuth bool
  375. isPlaintextAuth bool
  376. isTOCAuth bool
  377. multiConnFlag uint8
  378. passwordHash []byte
  379. plaintextPassword []byte
  380. roastedPass []byte
  381. screenName state.DisplayScreenName
  382. }
  383. // fromTLV creates an instance of loginProperties from a TLV list.
  384. func (l *loginProperties) fromTLV(list wire.TLVList) error {
  385. // extract screen name
  386. if screenName, found := list.String(wire.LoginTLVTagsScreenName); found {
  387. l.screenName = state.DisplayScreenName(screenName)
  388. } else {
  389. return errors.New("screen name doesn't exist in tlv")
  390. }
  391. // extract client name and version
  392. if clientID, found := list.String(wire.LoginTLVTagsClientIdentity); found {
  393. l.clientID = clientID
  394. }
  395. // get the password from the appropriate TLV. older clients have a
  396. // roasted password, newer clients have a hashed password. ICQ may omit
  397. // the password TLV when logging in without saved password.
  398. switch {
  399. case list.HasTag(wire.LoginTLVTagsPasswordHash):
  400. // extract password hash for BUCP login
  401. l.passwordHash, _ = list.Bytes(wire.LoginTLVTagsPasswordHash)
  402. l.isBUCPAuth = true
  403. case list.HasTag(wire.LoginTLVTagsRoastedPassword):
  404. // extract roasted password for FLAP login
  405. l.roastedPass, _ = list.Bytes(wire.LoginTLVTagsRoastedPassword)
  406. if strings.HasPrefix(l.clientID, "AOL Instant Messenger (TM) version") &&
  407. strings.Contains(l.clientID, "for Java") {
  408. l.isFLAPJavaAuth = true
  409. } else {
  410. l.isFLAPAuth = true
  411. }
  412. case list.HasTag(wire.LoginTLVTagsRoastedTOCPassword):
  413. // extract roasted password for TOC FLAP login
  414. l.roastedPass, _ = list.Bytes(wire.LoginTLVTagsRoastedTOCPassword)
  415. l.isTOCAuth = true
  416. case list.HasTag(wire.LoginTLVTagsPlaintextKerberosPassword):
  417. l.plaintextPassword, _ = list.Bytes(wire.LoginTLVTagsPlaintextKerberosPassword)
  418. l.isKerberosPlaintextAuth = true
  419. case list.HasTag(wire.LoginTLVTagsPlaintextPassword):
  420. l.plaintextPassword, _ = list.Bytes(wire.LoginTLVTagsPlaintextPassword)
  421. l.isPlaintextAuth = true
  422. case list.HasTag(wire.LoginTLVTagsRoastedKerberosPassword):
  423. l.roastedPass, _ = list.Bytes(wire.LoginTLVTagsRoastedKerberosPassword)
  424. l.isKerberosRoastedAuth = true
  425. default:
  426. l.isFLAPAuth = true
  427. }
  428. // does the client support multiple concurrent sessions?
  429. if multiConnFlags, found := list.Uint8(wire.LoginTLVTagsMultiConnFlags); found {
  430. l.multiConnFlag = multiConnFlags
  431. }
  432. return nil
  433. }
  434. // login validates a user's credentials and creates their session. it returns
  435. // metadata used in both BUCP and FLAP authentication responses.
  436. func (s AuthService) login(ctx context.Context, tlv wire.TLVList, advertisedHost string) (wire.TLVRestBlock, error) {
  437. props := loginProperties{}
  438. if err := props.fromTLV(tlv); err != nil {
  439. s.logger.Debug("login: failed to parse TLVs", "err", err.Error())
  440. return wire.TLVRestBlock{}, err
  441. }
  442. s.logger.Debug("login: parsed login properties",
  443. "screen_name", props.screenName,
  444. "client_id", props.clientID,
  445. "is_bucp", props.isBUCPAuth,
  446. "is_flap", props.isFLAPAuth,
  447. "is_flap_java", props.isFLAPJavaAuth,
  448. "is_toc", props.isTOCAuth,
  449. "is_kerberos_plaintext", props.isKerberosPlaintextAuth,
  450. "is_kerberos_roasted", props.isKerberosRoastedAuth,
  451. "is_plaintext", props.isPlaintextAuth,
  452. "password_hash_len", len(props.passwordHash),
  453. "roasted_pass_len", len(props.roastedPass))
  454. user, err := s.userManager.User(ctx, props.screenName.IdentScreenName())
  455. if err != nil {
  456. s.logger.Error("login: user lookup failed", "screen_name", props.screenName, "err", err.Error())
  457. return wire.TLVRestBlock{}, err
  458. }
  459. if user == nil {
  460. s.logger.Debug("login: user not found", "screen_name", props.screenName)
  461. // user not found
  462. if s.config.DisableAuth {
  463. // auth disabled, create the user
  464. s.logger.Debug("login: auth disabled, creating user", "screen_name", props.screenName)
  465. return s.createUser(ctx, props, advertisedHost)
  466. }
  467. // auth enabled, return separate login errors for ICQ and AIM
  468. loginErr := wire.LoginErrInvalidUsernameOrPassword
  469. if props.screenName.IsUIN() {
  470. loginErr = wire.LoginErrICQUserErr
  471. }
  472. s.logger.Debug("login: returning user not found error",
  473. "screen_name", props.screenName,
  474. "error_code", loginErr)
  475. return loginFailureResponse(props, loginErr), nil
  476. }
  477. s.logger.Debug("login: user found", "screen_name", props.screenName, "is_icq", user.IsICQ)
  478. // check if suspended status should prevent login
  479. if user.SuspendedStatus > 0x0 {
  480. s.logger.Debug("login: user suspended",
  481. "screen_name", props.screenName,
  482. "suspended_status", user.SuspendedStatus)
  483. return loginFailureResponse(props, user.SuspendedStatus), nil
  484. }
  485. if s.config.DisableAuth {
  486. // user exists, but don't validate
  487. s.logger.Debug("login: auth disabled, skipping password validation", "screen_name", props.screenName)
  488. return s.loginSuccessResponse(ctx, props, advertisedHost)
  489. }
  490. var loginOK bool
  491. var authMethod string
  492. switch {
  493. case props.isBUCPAuth:
  494. authMethod = "BUCP"
  495. loginOK = user.ValidateHash(props.passwordHash)
  496. case props.isFLAPAuth:
  497. authMethod = "FLAP"
  498. loginOK = user.ValidateRoastedPass(props.roastedPass)
  499. case props.isFLAPJavaAuth:
  500. authMethod = "FLAP_Java"
  501. loginOK = user.ValidateRoastedJavaPass(props.roastedPass)
  502. case props.isTOCAuth:
  503. authMethod = "TOC"
  504. loginOK = user.ValidateRoastedTOCPass(props.roastedPass)
  505. case props.isKerberosPlaintextAuth:
  506. authMethod = "Kerberos_Plaintext"
  507. loginOK = user.ValidatePlaintextPass(props.plaintextPassword)
  508. case props.isKerberosRoastedAuth:
  509. authMethod = "Kerberos_Roasted"
  510. loginOK = user.ValidateRoastedKerberosPass(props.roastedPass)
  511. case props.isPlaintextAuth:
  512. authMethod = "Plaintext"
  513. loginOK = user.ValidatePlaintextPass(props.plaintextPassword)
  514. }
  515. s.logger.Debug("login: password validation result",
  516. "screen_name", props.screenName,
  517. "auth_method", authMethod,
  518. "login_ok", loginOK)
  519. if !loginOK {
  520. s.logger.Debug("login: password validation failed",
  521. "screen_name", props.screenName,
  522. "auth_method", authMethod)
  523. return loginFailureResponse(props, wire.LoginErrInvalidPassword), nil
  524. }
  525. // limit concurrent logins per user
  526. if props.multiConnFlag == uint8(wire.MultiConnFlagsRecentClient) {
  527. sess := s.sessionRetriever.RetrieveSession(props.screenName.IdentScreenName())
  528. if sess != nil && sess.InstanceCount() >= s.maxConcurrentLoginsPerUser {
  529. s.logger.Debug("login: too many concurrent sessions",
  530. "screen_name", props.screenName,
  531. "instance_count", sess.InstanceCount())
  532. return loginFailureResponse(props, wire.LoginErrRateLimitExceeded), nil
  533. }
  534. }
  535. s.logger.Debug("login: login successful", "screen_name", props.screenName)
  536. return s.loginSuccessResponse(ctx, props, advertisedHost)
  537. }
  538. func (s AuthService) createUser(ctx context.Context, props loginProperties, advertisedHost string) (wire.TLVRestBlock, error) {
  539. err := s.createAccount(ctx, props.screenName, "welcome1")
  540. if err != nil {
  541. switch {
  542. case errors.Is(err, state.ErrAIMHandleInvalidFormat) || errors.Is(err, state.ErrAIMHandleLength):
  543. return loginFailureResponse(props, wire.LoginErrInvalidUsernameOrPassword), nil
  544. case errors.Is(err, state.ErrICQUINInvalidFormat):
  545. return loginFailureResponse(props, wire.LoginErrICQUserErr), nil
  546. default:
  547. return wire.TLVRestBlock{}, err
  548. }
  549. }
  550. return s.loginSuccessResponse(ctx, props, advertisedHost)
  551. }
  552. func (s AuthService) loginSuccessResponse(ctx context.Context, props loginProperties, advertisedHost string) (wire.TLVRestBlock, error) {
  553. loginCookie := state.ServerCookie{
  554. Service: wire.BOS,
  555. ScreenName: props.screenName,
  556. ClientID: props.clientID,
  557. MultiConnFlag: props.multiConnFlag,
  558. }
  559. if props.isKerberosPlaintextAuth || props.isKerberosRoastedAuth {
  560. loginCookie.KerberosAuth = 1
  561. }
  562. buf := &bytes.Buffer{}
  563. if err := wire.MarshalBE(loginCookie, buf); err != nil {
  564. return wire.TLVRestBlock{}, err
  565. }
  566. cookie, err := s.cookieBaker.Issue(buf.Bytes())
  567. if err != nil {
  568. return wire.TLVRestBlock{}, fmt.Errorf("failed to issue auth cookie: %w", err)
  569. }
  570. reconnectHost := advertisedHost
  571. sslState := wire.OServiceServiceResponseSSLStateNotUsed
  572. s.logger.Debug("loginSuccessResponse: returning login response",
  573. "screen_name", props.screenName,
  574. "reconnect_host", reconnectHost,
  575. "ssl_state", sslState)
  576. loginTLVTags := wire.TLVList{
  577. wire.NewTLVBE(wire.LoginTLVTagsScreenName, props.screenName),
  578. wire.NewTLVBE(wire.LoginTLVTagsReconnectHere, reconnectHost),
  579. wire.NewTLVBE(wire.LoginTLVTagsAuthorizationCookie, cookie),
  580. wire.NewTLVBE(wire.OServiceTLVTagsSSLState, sslState),
  581. }
  582. if err := s.addLinkedAccountsTLV(ctx, props.screenName, &loginTLVTags); err != nil {
  583. return wire.TLVRestBlock{}, err
  584. }
  585. return wire.TLVRestBlock{TLVList: loginTLVTags}, nil
  586. }
  587. // addLinkedAccountsTLV builds the linked accounts XML and appends the
  588. // corresponding TLV to tlvs. If linkedNames is empty, tlvs is not modified.
  589. func (s AuthService) addLinkedAccountsTLV(ctx context.Context, screenName state.DisplayScreenName, tlvs *wire.TLVList) error {
  590. items, err := s.feedbagManager.Feedbag(ctx, screenName.IdentScreenName())
  591. if err != nil {
  592. return fmt.Errorf("failed to get linked accounts: %w", err)
  593. }
  594. linkedNames := state.NewFeedbagList(items, nil).LinkedScreenNames()
  595. if len(linkedNames) == 0 {
  596. return nil
  597. }
  598. acctXML, err := buildLinkedAccountsXML(screenName.IdentScreenName(), linkedNames)
  599. if err != nil {
  600. return fmt.Errorf("failed to build linked accounts xml: %w", err)
  601. }
  602. *tlvs = append(*tlvs, wire.NewTLVBE(wire.OServiceTLVTagsLinkedAccounts, acctXML))
  603. return nil
  604. }
  605. func loginFailureResponse(props loginProperties, errCode uint16) wire.TLVRestBlock {
  606. return wire.TLVRestBlock{
  607. TLVList: []wire.TLV{
  608. wire.NewTLVBE(wire.LoginTLVTagsScreenName, props.screenName),
  609. wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, errCode),
  610. },
  611. }
  612. }
  613. // buildLinkedAccountsXML will return the XML doc expected for wire.OServiceTLVTagsLinkedAccounts
  614. // Example:
  615. // <SET SETID="1">
  616. //
  617. // <RESREC TYPE="PRIMARY-ACCOUNT" ID="1"><n>PrimaryName</n></RESREC>
  618. // <RESREC TYPE="LINKED-ACCOUNT" ID="2"><n>LinkedName1</n></RESREC>
  619. // <RESREC TYPE="LINKED-ACCOUNT" ID="3"><n>LinkedName2</n></RESREC>
  620. // <RESREC TYPE="LINKED-ACCOUNT" ID="4"><n>LinkedName3</n></RESREC>
  621. // <RESREC TYPE="LINKED-ACCOUNT" ID="5"><n>LinkedName4</n></RESREC>
  622. //
  623. // </SET>
  624. func buildLinkedAccountsXML(screenName state.IdentScreenName, linkedNames []state.IdentScreenName) (string, error) {
  625. // ResRec represents the <RESREC> element with its attributes and nested <n> tag
  626. type ResRec struct {
  627. Type string `xml:"TYPE,attr"`
  628. ID string `xml:"ID,attr"`
  629. Name string `xml:"n"`
  630. }
  631. // Set represents the root <SET> element
  632. type Set struct {
  633. XMLName xml.Name `xml:"SET"`
  634. SetID string `xml:"SETID,attr"`
  635. Records []ResRec `xml:"RESREC"`
  636. }
  637. data := Set{
  638. SetID: "1",
  639. Records: []ResRec{
  640. {Type: "PRIMARY-ACCOUNT", ID: "1", Name: screenName.String()},
  641. },
  642. }
  643. for i, name := range linkedNames {
  644. newRec := ResRec{
  645. Type: "LINKED-ACCOUNT",
  646. // Incrementing ID starting from 2 (since Primary is 1)
  647. ID: strconv.Itoa(i + 2),
  648. Name: name.String(),
  649. }
  650. data.Records = append(data.Records, newRec)
  651. }
  652. output, err := xml.Marshal(data)
  653. if err != nil {
  654. return "", err
  655. }
  656. return string(output), nil
  657. }