auth.go 27 KB

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