auth.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  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/open-oscar-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. } else {
  60. // Parse form-encoded or URL parameters
  61. if err := r.ParseForm(); err != nil {
  62. h.Logger.Error("failed to parse form data", "error", err)
  63. SendError(w, http.StatusBadRequest, "invalid form data")
  64. return
  65. }
  66. // Try form values first, then fall back to query parameters
  67. username = r.FormValue("s")
  68. if username == "" {
  69. username = r.FormValue("username")
  70. }
  71. password = r.FormValue("pwd")
  72. if password == "" {
  73. password = r.FormValue("password")
  74. }
  75. devID = r.FormValue("devId")
  76. h.Logger.Debug("form-encoded login attempt",
  77. "username", username,
  78. "has_password", password != "",
  79. "devId", devID,
  80. "form", r.Form)
  81. }
  82. // Validate required fields
  83. if username == "" || password == "" {
  84. SendError(w, http.StatusBadRequest, "username and password required")
  85. return
  86. }
  87. // Authenticate user
  88. user, err := h.UserManager.AuthenticateUser(r.Context(), username, password)
  89. if err != nil {
  90. // If DISABLE_AUTH is enabled and user doesn't exist, create the user
  91. if h.DisableAuth && err.Error() == "user not found" {
  92. h.Logger.Info("DISABLE_AUTH: Creating new user",
  93. "username", username)
  94. // Create new user with the provided username
  95. newUser := state.User{
  96. IdentScreenName: state.NewIdentScreenName(username),
  97. DisplayScreenName: state.DisplayScreenName(username),
  98. }
  99. // Insert the new user
  100. if err := h.UserManager.InsertUser(r.Context(), newUser); err != nil {
  101. h.Logger.Error("failed to create user",
  102. "username", username,
  103. "error", err)
  104. SendError(w, http.StatusInternalServerError, "failed to create user")
  105. return
  106. }
  107. // Try to authenticate again after creating the user
  108. user, err = h.UserManager.AuthenticateUser(r.Context(), username, password)
  109. if err != nil {
  110. h.Logger.Error("failed to authenticate after creating user",
  111. "username", username,
  112. "error", err)
  113. SendError(w, http.StatusInternalServerError, "internal server error")
  114. return
  115. }
  116. } else {
  117. h.Logger.Warn("authentication failed",
  118. "username", username,
  119. "error", err)
  120. SendError(w, http.StatusUnauthorized, "authentication failed")
  121. return
  122. }
  123. }
  124. // Generate authentication token
  125. token, err := h.generateToken()
  126. if err != nil {
  127. h.Logger.Error("failed to generate token", "error", err)
  128. SendError(w, http.StatusInternalServerError, "internal server error")
  129. return
  130. }
  131. // Store token with 24 hour expiry
  132. expiresAt := time.Now().Add(24 * time.Hour)
  133. if err := h.TokenStore.StoreToken(r.Context(), token, user.IdentScreenName, expiresAt); err != nil {
  134. h.Logger.Error("failed to store token", "error", err)
  135. SendError(w, http.StatusInternalServerError, "internal server error")
  136. return
  137. }
  138. // Generate session secret (for signing subsequent requests)
  139. sessionSecret, err := h.generateToken()
  140. if err != nil {
  141. h.Logger.Error("failed to generate session secret", "error", err)
  142. SendError(w, http.StatusInternalServerError, "internal server error")
  143. return
  144. }
  145. // Build response
  146. resp := BaseResponse{}
  147. resp.Response.StatusCode = 200
  148. resp.Response.StatusText = "OK"
  149. resp.Response.Data = map[string]interface{}{
  150. "token": map[string]interface{}{
  151. "a": token,
  152. "expiresIn": 86400, // 24 hours in seconds
  153. },
  154. "loginId": string(user.DisplayScreenName),
  155. "screenName": string(user.DisplayScreenName),
  156. "sessionSecret": sessionSecret,
  157. "hostTime": time.Now().Unix(),
  158. "tokenExpiresIn": 86400, // 24 hours in seconds
  159. }
  160. // Send response in requested format (JSON, JSONP, XML, or AMF)
  161. SendResponse(w, r, resp, h.Logger)
  162. h.Logger.Info("user authenticated successfully",
  163. "username", username,
  164. "screenName", user.DisplayScreenName)
  165. }
  166. // generateToken generates a secure random token.
  167. func (h *AuthHandler) generateToken() (string, error) {
  168. b := make([]byte, 32)
  169. if _, err := rand.Read(b); err != nil {
  170. return "", err
  171. }
  172. return base64.URLEncoding.EncodeToString(b), nil
  173. }