auth.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. package handler
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "github.com/google/uuid"
  7. "github.com/mkaminski/goaim/oscar"
  8. "github.com/mkaminski/goaim/server"
  9. "github.com/mkaminski/goaim/state"
  10. )
  11. // NewAuthService creates a new instance of AuthService.
  12. func NewAuthService(cfg server.Config, sessionManager SessionManager, messageRelayer MessageRelayer, feedbagManager FeedbagManager, userManager UserManager, chatRegistry ChatRegistry) *AuthService {
  13. return &AuthService{
  14. chatRegistry: chatRegistry,
  15. config: cfg,
  16. feedbagManager: feedbagManager,
  17. messageRelayer: messageRelayer,
  18. sessionManager: sessionManager,
  19. userManager: userManager,
  20. }
  21. }
  22. // AuthService provides user BUCP login and session management services.
  23. type AuthService struct {
  24. chatRegistry ChatRegistry
  25. config server.Config
  26. feedbagManager FeedbagManager
  27. messageRelayer MessageRelayer
  28. sessionManager SessionManager
  29. userManager UserManager
  30. }
  31. // RetrieveChatSession returns a chat room session. Return nil if the session
  32. // does not exist.
  33. func (s AuthService) RetrieveChatSession(chatID string, sessionID string) (*state.Session, error) {
  34. _, chatSessMgr, err := s.chatRegistry.Retrieve(chatID)
  35. if err != nil {
  36. return nil, err
  37. }
  38. return chatSessMgr.(SessionManager).RetrieveSession(sessionID), nil
  39. }
  40. // RetrieveBOSSession returns a user's session. Return nil if the session does
  41. // not exist.
  42. func (s AuthService) RetrieveBOSSession(sessionID string) (*state.Session, error) {
  43. return s.sessionManager.RetrieveSession(sessionID), nil
  44. }
  45. // Signout removes user from the BOS server and notifies adjacent users (those
  46. // who have this user's screen name on their buddy list) of their departure.
  47. func (s AuthService) Signout(ctx context.Context, sess *state.Session) error {
  48. if err := broadcastDeparture(ctx, sess, s.messageRelayer, s.feedbagManager); err != nil {
  49. return err
  50. }
  51. s.sessionManager.RemoveSession(sess)
  52. return nil
  53. }
  54. // SignoutChat removes user from chat room and notifies remaining participants
  55. // of their departure. If user is the last to leave, the chat room is deleted.
  56. func (s AuthService) SignoutChat(ctx context.Context, sess *state.Session, chatID string) error {
  57. chatRoom, chatSessMgr, err := s.chatRegistry.Retrieve(chatID)
  58. if err != nil {
  59. return err
  60. }
  61. alertUserLeft(ctx, sess, chatSessMgr.(ChatMessageRelayer))
  62. chatSessMgr.(SessionManager).RemoveSession(sess)
  63. if chatSessMgr.(SessionManager).Empty() {
  64. s.chatRegistry.Remove(chatRoom.Cookie)
  65. }
  66. return nil
  67. }
  68. // BUCPChallengeRequestHandler satisfies the client request for a random auth
  69. // key. It returns SNAC oscar.BUCPChallengeResponse. If the screen name in
  70. // TLV oscar.TLVScreenName in bodyIn is recognized as a valid user, the
  71. // response contains the account's auth key, which salt's the user's MD5
  72. // password hash. If the account is invalid, an error code is set in TLV
  73. // oscar.TLVErrorSubcode. If login credentials are invalid and app config
  74. // DisableAuth is true, a stub auth key is generated and a successful challenge
  75. // response is returned.
  76. func (s AuthService) BUCPChallengeRequestHandler(bodyIn oscar.SNAC_0x17_0x06_BUCPChallengeRequest, newUUIDFn func() uuid.UUID) (oscar.SNACMessage, error) {
  77. screenName, exists := bodyIn.GetString(oscar.TLVScreenName)
  78. if !exists {
  79. return oscar.SNACMessage{}, errors.New("screen name doesn't exist in tlv")
  80. }
  81. var authKey string
  82. u, err := s.userManager.User(screenName)
  83. switch {
  84. case err != nil:
  85. return oscar.SNACMessage{}, err
  86. case u != nil:
  87. // user lookup succeeded
  88. authKey = u.AuthKey
  89. case s.config.DisableAuth:
  90. // can't find user, generate stub auth key
  91. authKey = newUUIDFn().String()
  92. default:
  93. // can't find user, return login error
  94. snacFrameOut := oscar.SNACFrame{
  95. FoodGroup: oscar.BUCP,
  96. SubGroup: oscar.BUCPLoginResponse,
  97. }
  98. snacPayloadOut := oscar.SNAC_0x17_0x03_BUCPLoginResponse{}
  99. snacPayloadOut.AddTLV(oscar.NewTLV(oscar.TLVErrorSubcode, uint16(0x01)))
  100. return oscar.SNACMessage{
  101. Frame: snacFrameOut,
  102. Body: snacPayloadOut,
  103. }, nil
  104. }
  105. return oscar.SNACMessage{
  106. Frame: oscar.SNACFrame{
  107. FoodGroup: oscar.BUCP,
  108. SubGroup: oscar.BUCPChallengeResponse,
  109. },
  110. Body: oscar.SNAC_0x17_0x07_BUCPChallengeResponse{
  111. AuthKey: authKey,
  112. },
  113. }, nil
  114. }
  115. // BUCPLoginRequestHandler verifies user credentials. Upon successful login, a
  116. // session is created.
  117. // If login credentials are invalid and app config DisableAuth is true, a stub
  118. // user is created and login continues as normal. DisableAuth allows you to
  119. // skip the account creation procedure, which simplifies the login flow during
  120. // development.
  121. // If login is successful, the SNAC TLV list contains the BOS server address
  122. // (oscar.TLVReconnectHere) and an authorization cookie
  123. // (oscar.TLVAuthorizationCookie). Else, an error code is set
  124. // (oscar.TLVErrorSubcode).
  125. func (s AuthService) BUCPLoginRequestHandler(bodyIn oscar.SNAC_0x17_0x02_BUCPLoginRequest, newUUIDFn func() uuid.UUID, newUserFn func(screenName string) (state.User, error)) (oscar.SNACMessage, error) {
  126. screenName, found := bodyIn.GetString(oscar.TLVScreenName)
  127. if !found {
  128. return oscar.SNACMessage{}, errors.New("screen name doesn't exist in tlv")
  129. }
  130. md5Hash, found := bodyIn.GetSlice(oscar.TLVPasswordHash)
  131. if !found {
  132. return oscar.SNACMessage{}, errors.New("password hash doesn't exist in tlv")
  133. }
  134. loginOK := false
  135. u, err := s.userManager.User(screenName)
  136. switch {
  137. case err != nil:
  138. return oscar.SNACMessage{}, err
  139. case u != nil && bytes.Equal(u.PassHash, md5Hash):
  140. // password check succeeded
  141. loginOK = true
  142. case s.config.DisableAuth:
  143. // login failed but let them in anyway
  144. user, err := newUserFn(screenName)
  145. if err != nil {
  146. return oscar.SNACMessage{}, err
  147. }
  148. if err := s.userManager.InsertUser(user); err != nil {
  149. return oscar.SNACMessage{}, err
  150. }
  151. loginOK = true
  152. }
  153. snacPayloadOut := oscar.SNAC_0x17_0x03_BUCPLoginResponse{}
  154. snacPayloadOut.AddTLV(oscar.NewTLV(oscar.TLVScreenName, screenName))
  155. if loginOK {
  156. sess := s.sessionManager.AddSession(newUUIDFn().String(), screenName)
  157. snacPayloadOut.AddTLVList([]oscar.TLV{
  158. oscar.NewTLV(oscar.TLVReconnectHere, server.Address(s.config.OSCARHost, s.config.BOSPort)),
  159. oscar.NewTLV(oscar.TLVAuthorizationCookie, sess.ID()),
  160. })
  161. } else {
  162. snacPayloadOut.AddTLVList([]oscar.TLV{
  163. oscar.NewTLV(oscar.TLVErrorSubcode, uint16(0x01)),
  164. })
  165. }
  166. return oscar.SNACMessage{
  167. Frame: oscar.SNACFrame{
  168. FoodGroup: oscar.BUCP,
  169. SubGroup: oscar.BUCPLoginResponse,
  170. },
  171. Body: snacPayloadOut,
  172. }, nil
  173. }