2021-01-16 22:00:17 +00:00
|
|
|
package api
|
2021-01-10 00:44:02 +00:00
|
|
|
|
|
|
|
import (
|
2021-01-18 21:16:52 +00:00
|
|
|
"fmt"
|
2021-01-12 04:48:32 +00:00
|
|
|
"time"
|
2021-01-18 21:16:52 +00:00
|
|
|
"strings"
|
2021-01-10 00:44:02 +00:00
|
|
|
"net/http"
|
2021-01-18 21:16:52 +00:00
|
|
|
"encoding/json"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
log "github.com/sirupsen/logrus"
|
2021-01-18 21:24:28 +00:00
|
|
|
"github.com/lestrrat-go/jwx/jwt"
|
2021-01-18 21:16:52 +00:00
|
|
|
|
2021-01-12 04:48:32 +00:00
|
|
|
"reichard.io/imagini/internal/models"
|
2021-01-10 00:44:02 +00:00
|
|
|
)
|
|
|
|
|
2021-01-16 22:00:17 +00:00
|
|
|
func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) {
|
2021-01-12 04:48:32 +00:00
|
|
|
if r.Method != http.MethodPost {
|
2021-01-16 22:00:17 +00:00
|
|
|
errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed)
|
2021-01-12 04:48:32 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Decode into Struct
|
|
|
|
var creds models.APICredentials
|
|
|
|
err := json.NewDecoder(r.Body).Decode(&creds)
|
|
|
|
if err != nil {
|
2021-01-16 22:00:17 +00:00
|
|
|
errorJSON(w, "Invalid parameters.", http.StatusBadRequest)
|
2021-01-12 04:48:32 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate
|
|
|
|
if creds.User == "" || creds.Password == "" {
|
2021-01-16 22:00:17 +00:00
|
|
|
errorJSON(w, "Invalid parameters.", http.StatusBadRequest)
|
2021-01-12 04:48:32 +00:00
|
|
|
return
|
|
|
|
}
|
2021-01-10 00:44:02 +00:00
|
|
|
|
2021-01-12 04:48:32 +00:00
|
|
|
// Do login
|
2021-01-18 21:16:52 +00:00
|
|
|
resp, user := api.Auth.AuthenticateUser(creds)
|
2021-01-18 04:56:56 +00:00
|
|
|
if !resp {
|
2021-01-16 22:00:17 +00:00
|
|
|
errorJSON(w, "Invalid credentials.", http.StatusUnauthorized)
|
2021-01-18 04:56:56 +00:00
|
|
|
return
|
2021-01-12 04:48:32 +00:00
|
|
|
}
|
2021-01-18 04:56:56 +00:00
|
|
|
|
2021-01-19 20:50:48 +00:00
|
|
|
// 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
|
|
|
|
}
|
2021-01-18 21:16:52 +00:00
|
|
|
|
|
|
|
// Create Tokens
|
|
|
|
accessToken, err := api.Auth.CreateJWTAccessToken(user, device)
|
|
|
|
refreshToken, err := api.Auth.CreateJWTRefreshToken(user, device)
|
2021-01-18 04:56:56 +00:00
|
|
|
|
|
|
|
// Set appropriate cookies
|
2021-01-18 21:16:52 +00:00
|
|
|
accessCookie := http.Cookie{Name: "AccessToken", Value: accessToken, HttpOnly: true}
|
|
|
|
refreshCookie := http.Cookie{Name: "RefreshToken", Value: refreshToken, HttpOnly: true}
|
2021-01-18 04:56:56 +00:00
|
|
|
http.SetCookie(w, &accessCookie)
|
|
|
|
http.SetCookie(w, &refreshCookie)
|
|
|
|
|
|
|
|
// Response success
|
|
|
|
successJSON(w, "Login success.", http.StatusOK)
|
2021-01-10 00:44:02 +00:00
|
|
|
}
|
|
|
|
|
2021-01-16 22:00:17 +00:00
|
|
|
func (api *API) logoutHandler(w http.ResponseWriter, r *http.Request) {
|
2021-01-12 04:48:32 +00:00
|
|
|
if r.Method != http.MethodPost {
|
|
|
|
http.Error(w, "Method is not supported.", http.StatusMethodNotAllowed)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-01-19 20:50:48 +00:00
|
|
|
// TODO: Reset Refresh Key
|
2021-01-12 04:48:32 +00:00
|
|
|
|
2021-01-19 20:50:48 +00:00
|
|
|
// 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)})
|
2021-01-10 00:44:02 +00:00
|
|
|
|
2021-01-19 20:50:48 +00:00
|
|
|
successJSON(w, "Logout success.", http.StatusOK)
|
2021-01-10 00:44:02 +00:00
|
|
|
}
|
2021-01-18 04:56:56 +00:00
|
|
|
|
|
|
|
func (api *API) refreshLoginHandler(w http.ResponseWriter, r *http.Request) {
|
2021-01-18 21:16:52 +00:00
|
|
|
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)
|
2021-01-18 04:56:56 +00:00
|
|
|
if !ok {
|
|
|
|
errorJSON(w, "Invalid credentials.", http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
}
|
2021-01-18 21:16:52 +00:00
|
|
|
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}}
|
2021-01-18 04:56:56 +00:00
|
|
|
|
|
|
|
// Update token
|
2021-01-18 21:16:52 +00:00
|
|
|
accessToken, err := api.Auth.CreateJWTAccessToken(user, device)
|
2021-01-18 04:56:56 +00:00
|
|
|
accessCookie := http.Cookie{Name: "AccessToken", Value: accessToken}
|
|
|
|
http.SetCookie(w, &accessCookie)
|
|
|
|
|
|
|
|
// Response success
|
|
|
|
successJSON(w, "Refresh success.", http.StatusOK)
|
|
|
|
}
|
2021-01-19 20:50:48 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
2021-01-19 21:26:10 +00:00
|
|
|
val := trimQuotes(strings.TrimSpace(splitItem[1]))
|
2021-01-19 20:50:48 +00:00
|
|
|
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
|
|
|
|
}
|