package api import ( "fmt" "time" "strings" "net/http" "encoding/json" "github.com/google/uuid" log "github.com/sirupsen/logrus" "github.com/lestrrat-go/jwx/jwt" "reichard.io/imagini/internal/models" ) func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed) return } // Decode into Struct var creds models.APICredentials err := json.NewDecoder(r.Body).Decode(&creds) if err != nil { errorJSON(w, "Invalid parameters.", http.StatusBadRequest) return } // Validate if creds.User == "" || creds.Password == "" { errorJSON(w, "Invalid parameters.", http.StatusBadRequest) return } // Do login resp, user := api.Auth.AuthenticateUser(creds) if !resp { errorJSON(w, "Invalid credentials.", http.StatusUnauthorized) return } // Upsert device device, err := api.upsertRequestedDevice(user, r) if err != nil { log.Error("[api] loginHandler - Failed to upsert device: ", err) errorJSON(w, "DB error. Unable to proceed.", http.StatusUnauthorized) return } // Create Tokens accessToken, err := api.Auth.CreateJWTAccessToken(user, device) refreshToken, err := api.Auth.CreateJWTRefreshToken(user, device) // Set appropriate cookies accessCookie := http.Cookie{Name: "AccessToken", Value: accessToken, HttpOnly: true} refreshCookie := http.Cookie{Name: "RefreshToken", Value: refreshToken, HttpOnly: true} http.SetCookie(w, &accessCookie) http.SetCookie(w, &refreshCookie) // Response success successJSON(w, "Login success.", http.StatusOK) } func (api *API) logoutHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method is not supported.", http.StatusMethodNotAllowed) return } // TODO: Reset Refresh Key // Clear Cookies http.SetCookie(w, &http.Cookie{Name: "AccessToken", Expires: time.Unix(0, 0)}) http.SetCookie(w, &http.Cookie{Name: "RefreshToken", Expires: time.Unix(0, 0)}) successJSON(w, "Logout success.", http.StatusOK) } func (api *API) refreshLoginHandler(w http.ResponseWriter, r *http.Request) { refreshCookie, err := r.Cookie("RefreshToken") if err != nil { log.Warn("[middleware] Cookie not found") w.WriteHeader(http.StatusUnauthorized) return } // Validate Refresh Token refreshToken, ok := api.Auth.ValidateJWTRefreshToken(refreshCookie.Value) if !ok { http.SetCookie(w, &http.Cookie{Name: "AccessToken", Expires: time.Unix(0, 0)}) http.SetCookie(w, &http.Cookie{Name: "RefreshToken", Expires: time.Unix(0, 0)}) errorJSON(w, "Invalid credentials.", http.StatusUnauthorized) return } // Acquire User & Device (Trusted) did, ok := refreshToken.Get("did") if !ok { errorJSON(w, "Invalid credentials.", http.StatusUnauthorized) return } uid, ok := refreshToken.Get(jwt.SubjectKey) if !ok { errorJSON(w, "Invalid credentials.", http.StatusUnauthorized) return } deviceID, err := uuid.Parse(fmt.Sprintf("%v", did)) if err != nil { errorJSON(w, "Invalid credentials.", http.StatusUnauthorized) return } userID, err := uuid.Parse(fmt.Sprintf("%v", uid)) if err != nil { errorJSON(w, "Invalid credentials.", http.StatusUnauthorized) return } // Device Skeleton user := models.User{Base: models.Base{UUID: userID}} device := models.Device{Base: models.Base{UUID: deviceID}} // Update token accessToken, err := api.Auth.CreateJWTAccessToken(user, device) accessCookie := http.Cookie{Name: "AccessToken", Value: accessToken} http.SetCookie(w, &accessCookie) // Response success successJSON(w, "Refresh success.", http.StatusOK) } /** * This will find or create the requested device based on ID and User. **/ func (api *API) upsertRequestedDevice(user models.User, r *http.Request) (models.Device, error) { requestedDevice := deriveRequestedDevice(r) requestedDevice.Type = deriveDeviceType(r) requestedDevice.User = user if requestedDevice.UUID == uuid.Nil { createdDevice, err := api.DB.CreateDevice(requestedDevice) return createdDevice, err } foundDevice, err := api.DB.Device(models.Device{ Base: models.Base{ UUID: requestedDevice.UUID }, User: user, }) return foundDevice, err } func deriveDeviceType(r *http.Request) string { userAgent := strings.ToLower(r.Header.Get("User-Agent")) if strings.HasPrefix(userAgent, "ios-imagini"){ return "iOS" } else if strings.HasPrefix(userAgent, "android-imagini"){ return "Android" } else if strings.HasPrefix(userAgent, "chrome"){ return "Chrome" } else if strings.HasPrefix(userAgent, "firefox"){ return "Firefox" } else if strings.HasPrefix(userAgent, "msie"){ return "Internet Explorer" } else if strings.HasPrefix(userAgent, "edge"){ return "Edge" } else if strings.HasPrefix(userAgent, "safari"){ return "Safari" } return "Unknown" } func deriveRequestedDevice(r *http.Request) models.Device { deviceSkeleton := models.Device{} authHeader := r.Header.Get("X-Imagini-Authorization") splitAuthInfo := strings.Split(authHeader, ",") // For each Key - Value pair for i := range splitAuthInfo { // Split Key - Value item := strings.TrimSpace(splitAuthInfo[i]) splitItem := strings.SplitN(item, "=", 2) if len(splitItem) != 2 { continue } // Derive Key key := strings.ToLower(strings.TrimSpace(splitItem[0])) if key != "deviceuuid" && key != "devicename" { continue } // Derive Value val := trimQuotes(strings.ToLower(strings.TrimSpace(splitItem[1]))) if key == "deviceuuid" { parsedDeviceUUID, err := uuid.Parse(val) if err != nil { log.Warn("[auth] deriveRequestedDevice - Unable to parse requested DeviceUUID: ", val) continue } deviceSkeleton.Base = models.Base{UUID: parsedDeviceUUID} } else if key == "devicename" { deviceSkeleton.Name = val } } // If name not set, set to type if deviceSkeleton.Name == "" { deviceSkeleton.Name = deviceSkeleton.Type } return deviceSkeleton } func trimQuotes(s string) string { if len(s) >= 2 { if s[0] == '"' && s[len(s)-1] == '"' { return s[1 : len(s)-1] } } return s }