| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- package handlers
- import (
- "context"
- "crypto/rand"
- "encoding/base64"
- "encoding/json"
- "log/slog"
- "net/http"
- "time"
- "github.com/mk6i/open-oscar-server/state"
- )
- // AuthHandler handles Web AIM API authentication endpoints.
- type AuthHandler struct {
- UserManager UserManager
- TokenStore TokenStore
- Logger *slog.Logger
- DisableAuth bool
- }
- // UserManager defines methods for user authentication.
- type UserManager interface {
- // AuthenticateUser verifies username and password
- AuthenticateUser(ctx context.Context, username, password string) (*state.User, error)
- // FindUserByScreenName finds a user by their screen name
- FindUserByScreenName(ctx context.Context, screenName state.IdentScreenName) (*state.User, error)
- // InsertUser creates a new user (for DISABLE_AUTH mode)
- InsertUser(ctx context.Context, u state.User) error
- }
- // TokenStore manages authentication tokens.
- type TokenStore interface {
- // StoreToken saves an authentication token for a user
- StoreToken(ctx context.Context, token string, screenName state.IdentScreenName, expiresAt time.Time) error
- // ValidateToken checks if a token is valid and returns the associated screen name
- ValidateToken(ctx context.Context, token string) (state.IdentScreenName, error)
- // DeleteToken removes a token
- DeleteToken(ctx context.Context, token string) error
- }
- // ClientLoginRequest represents the request body for clientLogin.
- type ClientLoginRequest struct {
- Username string `json:"username"`
- Password string `json:"password"`
- DevID string `json:"devId"`
- }
- // ClientLogin handles POST /auth/clientLogin requests.
- // This endpoint authenticates users and returns an authentication token.
- func (h *AuthHandler) ClientLogin(w http.ResponseWriter, r *http.Request) {
- var username, password, devID string
- // Check Content-Type to determine how to parse the request
- contentType := r.Header.Get("Content-Type")
- if contentType == "application/json" {
- // Parse JSON body
- var req ClientLoginRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- h.Logger.Error("failed to parse JSON clientLogin request", "error", err)
- SendError(w, http.StatusBadRequest, "invalid JSON format")
- return
- }
- username = req.Username
- password = req.Password
- devID = req.DevID
- } else {
- // Parse form-encoded or URL parameters
- if err := r.ParseForm(); err != nil {
- h.Logger.Error("failed to parse form data", "error", err)
- SendError(w, http.StatusBadRequest, "invalid form data")
- return
- }
- // Try form values first, then fall back to query parameters
- username = r.FormValue("s")
- if username == "" {
- username = r.FormValue("username")
- }
- password = r.FormValue("pwd")
- if password == "" {
- password = r.FormValue("password")
- }
- devID = r.FormValue("devId")
- h.Logger.Debug("form-encoded login attempt",
- "username", username,
- "has_password", password != "",
- "devId", devID,
- "form", r.Form)
- }
- // Validate required fields
- if username == "" || password == "" {
- SendError(w, http.StatusBadRequest, "username and password required")
- return
- }
- // Authenticate user
- user, err := h.UserManager.AuthenticateUser(r.Context(), username, password)
- if err != nil {
- // If DISABLE_AUTH is enabled and user doesn't exist, create the user
- if h.DisableAuth && err.Error() == "user not found" {
- h.Logger.Info("DISABLE_AUTH: Creating new user",
- "username", username)
- // Create new user with the provided username
- newUser := state.User{
- IdentScreenName: state.NewIdentScreenName(username),
- DisplayScreenName: state.DisplayScreenName(username),
- }
- // Insert the new user
- if err := h.UserManager.InsertUser(r.Context(), newUser); err != nil {
- h.Logger.Error("failed to create user",
- "username", username,
- "error", err)
- SendError(w, http.StatusInternalServerError, "failed to create user")
- return
- }
- // Try to authenticate again after creating the user
- user, err = h.UserManager.AuthenticateUser(r.Context(), username, password)
- if err != nil {
- h.Logger.Error("failed to authenticate after creating user",
- "username", username,
- "error", err)
- SendError(w, http.StatusInternalServerError, "internal server error")
- return
- }
- } else {
- h.Logger.Warn("authentication failed",
- "username", username,
- "error", err)
- SendError(w, http.StatusUnauthorized, "authentication failed")
- return
- }
- }
- // Generate authentication token
- token, err := h.generateToken()
- if err != nil {
- h.Logger.Error("failed to generate token", "error", err)
- SendError(w, http.StatusInternalServerError, "internal server error")
- return
- }
- // Store token with 24 hour expiry
- expiresAt := time.Now().Add(24 * time.Hour)
- if err := h.TokenStore.StoreToken(r.Context(), token, user.IdentScreenName, expiresAt); err != nil {
- h.Logger.Error("failed to store token", "error", err)
- SendError(w, http.StatusInternalServerError, "internal server error")
- return
- }
- // Generate session secret (for signing subsequent requests)
- sessionSecret, err := h.generateToken()
- if err != nil {
- h.Logger.Error("failed to generate session secret", "error", err)
- SendError(w, http.StatusInternalServerError, "internal server error")
- return
- }
- // Build response
- resp := BaseResponse{}
- resp.Response.StatusCode = 200
- resp.Response.StatusText = "OK"
- resp.Response.Data = map[string]interface{}{
- "token": map[string]interface{}{
- "a": token,
- "expiresIn": 86400, // 24 hours in seconds
- },
- "loginId": string(user.DisplayScreenName),
- "screenName": string(user.DisplayScreenName),
- "sessionSecret": sessionSecret,
- "hostTime": time.Now().Unix(),
- "tokenExpiresIn": 86400, // 24 hours in seconds
- }
- // Send response in requested format (JSON, JSONP, XML, or AMF)
- SendResponse(w, r, resp, h.Logger)
- h.Logger.Info("user authenticated successfully",
- "username", username,
- "screenName", user.DisplayScreenName)
- }
- // generateToken generates a secure random token.
- func (h *AuthHandler) generateToken() (string, error) {
- b := make([]byte, 32)
- if _, err := rand.Read(b); err != nil {
- return "", err
- }
- return base64.URLEncoding.EncodeToString(b), nil
- }
|