auth.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. package handlers
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "encoding/base64"
  6. "encoding/json"
  7. "log/slog"
  8. "net/http"
  9. "time"
  10. "github.com/mk6i/retro-aim-server/state"
  11. )
  12. // AuthHandler handles Web AIM API authentication endpoints.
  13. type AuthHandler struct {
  14. UserManager UserManager
  15. TokenStore TokenStore
  16. Logger *slog.Logger
  17. DisableAuth bool
  18. }
  19. // UserManager defines methods for user authentication.
  20. type UserManager interface {
  21. // AuthenticateUser verifies username and password
  22. AuthenticateUser(ctx context.Context, username, password string) (*state.User, error)
  23. // FindUserByScreenName finds a user by their screen name
  24. FindUserByScreenName(ctx context.Context, screenName state.IdentScreenName) (*state.User, error)
  25. // InsertUser creates a new user (for DISABLE_AUTH mode)
  26. InsertUser(ctx context.Context, u state.User) error
  27. }
  28. // TokenStore manages authentication tokens.
  29. type TokenStore interface {
  30. // StoreToken saves an authentication token for a user
  31. StoreToken(ctx context.Context, token string, screenName state.IdentScreenName, expiresAt time.Time) error
  32. // ValidateToken checks if a token is valid and returns the associated screen name
  33. ValidateToken(ctx context.Context, token string) (state.IdentScreenName, error)
  34. // DeleteToken removes a token
  35. DeleteToken(ctx context.Context, token string) error
  36. }
  37. // ClientLoginRequest represents the request body for clientLogin.
  38. type ClientLoginRequest struct {
  39. Username string `json:"username"`
  40. Password string `json:"password"`
  41. DevID string `json:"devId"`
  42. }
  43. // ClientLogin handles POST /auth/clientLogin requests.
  44. // This endpoint authenticates users and returns an authentication token.
  45. func (h *AuthHandler) ClientLogin(w http.ResponseWriter, r *http.Request) {
  46. var username, password, devID string
  47. // Check Content-Type to determine how to parse the request
  48. contentType := r.Header.Get("Content-Type")
  49. if contentType == "application/json" {
  50. // Parse JSON body
  51. var req ClientLoginRequest
  52. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  53. h.Logger.Error("failed to parse JSON clientLogin request", "error", err)
  54. SendError(w, http.StatusBadRequest, "invalid JSON format")
  55. return
  56. }
  57. username = req.Username
  58. password = req.Password
  59. devID = req.DevID
  60. } else {
  61. // Parse form-encoded or URL parameters
  62. if err := r.ParseForm(); err != nil {
  63. h.Logger.Error("failed to parse form data", "error", err)
  64. SendError(w, http.StatusBadRequest, "invalid form data")
  65. return
  66. }
  67. // Try form values first, then fall back to query parameters
  68. username = r.FormValue("s")
  69. if username == "" {
  70. username = r.FormValue("username")
  71. }
  72. password = r.FormValue("pwd")
  73. if password == "" {
  74. password = r.FormValue("password")
  75. }
  76. devID = r.FormValue("devId")
  77. h.Logger.Debug("form-encoded login attempt",
  78. "username", username,
  79. "has_password", password != "",
  80. "devId", devID,
  81. "form", r.Form)
  82. }
  83. // Validate required fields
  84. if username == "" || password == "" {
  85. SendError(w, http.StatusBadRequest, "username and password required")
  86. return
  87. }
  88. // Authenticate user
  89. user, err := h.UserManager.AuthenticateUser(r.Context(), username, password)
  90. if err != nil {
  91. // If DISABLE_AUTH is enabled and user doesn't exist, create the user
  92. if h.DisableAuth && err.Error() == "user not found" {
  93. h.Logger.Info("DISABLE_AUTH: Creating new user",
  94. "username", username)
  95. // Create new user with the provided username
  96. newUser := state.User{
  97. IdentScreenName: state.NewIdentScreenName(username),
  98. DisplayScreenName: state.DisplayScreenName(username),
  99. }
  100. // Insert the new user
  101. if err := h.UserManager.InsertUser(r.Context(), newUser); err != nil {
  102. h.Logger.Error("failed to create user",
  103. "username", username,
  104. "error", err)
  105. SendError(w, http.StatusInternalServerError, "failed to create user")
  106. return
  107. }
  108. // Try to authenticate again after creating the user
  109. user, err = h.UserManager.AuthenticateUser(r.Context(), username, password)
  110. if err != nil {
  111. h.Logger.Error("failed to authenticate after creating user",
  112. "username", username,
  113. "error", err)
  114. SendError(w, http.StatusInternalServerError, "internal server error")
  115. return
  116. }
  117. } else {
  118. h.Logger.Warn("authentication failed",
  119. "username", username,
  120. "error", err)
  121. SendError(w, http.StatusUnauthorized, "authentication failed")
  122. return
  123. }
  124. }
  125. // Generate authentication token
  126. token, err := h.generateToken()
  127. if err != nil {
  128. h.Logger.Error("failed to generate token", "error", err)
  129. SendError(w, http.StatusInternalServerError, "internal server error")
  130. return
  131. }
  132. // Store token with 24 hour expiry
  133. expiresAt := time.Now().Add(24 * time.Hour)
  134. if err := h.TokenStore.StoreToken(r.Context(), token, user.IdentScreenName, expiresAt); err != nil {
  135. h.Logger.Error("failed to store token", "error", err)
  136. SendError(w, http.StatusInternalServerError, "internal server error")
  137. return
  138. }
  139. // Generate session secret (for signing subsequent requests)
  140. sessionSecret, err := h.generateToken()
  141. if err != nil {
  142. h.Logger.Error("failed to generate session secret", "error", err)
  143. SendError(w, http.StatusInternalServerError, "internal server error")
  144. return
  145. }
  146. // Build response
  147. resp := BaseResponse{}
  148. resp.Response.StatusCode = 200
  149. resp.Response.StatusText = "OK"
  150. resp.Response.Data = map[string]interface{}{
  151. "token": map[string]interface{}{
  152. "a": token,
  153. "expiresIn": 86400, // 24 hours in seconds
  154. },
  155. "loginId": string(user.DisplayScreenName),
  156. "screenName": string(user.DisplayScreenName),
  157. "sessionSecret": sessionSecret,
  158. "hostTime": time.Now().Unix(),
  159. "tokenExpiresIn": 86400, // 24 hours in seconds
  160. }
  161. // Send response in requested format (JSON, JSONP, XML, or AMF)
  162. SendResponse(w, r, resp, h.Logger)
  163. h.Logger.Info("user authenticated successfully",
  164. "username", username,
  165. "screenName", user.DisplayScreenName)
  166. }
  167. // generateToken generates a secure random token.
  168. func (h *AuthHandler) generateToken() (string, error) {
  169. b := make([]byte, 32)
  170. if _, err := rand.Read(b); err != nil {
  171. return "", err
  172. }
  173. return base64.URLEncoding.EncodeToString(b), nil
  174. }