auth.go 26 KB

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