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" graphql "reichard.io/imagini/graph/model" ) func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") 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, Path: "/", HttpOnly: true} refreshCookie := http.Cookie{Name: "RefreshToken", Value: refreshToken, Path: "/", 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) } /** * This will find or create the requested device based on ID and User. **/ func (api *API) upsertRequestedDevice(user graphql.User, r *http.Request) (graphql.Device, error) { requestedDevice := deriveRequestedDevice(r) requestedDevice.Type = deriveDeviceType(r) requestedDevice.User.ID = user.ID if *requestedDevice.ID == "" { err := api.DB.CreateDevice(&requestedDevice) createdDevice, err := api.DB.Device(&requestedDevice) return createdDevice, err } foundDevice, err := api.DB.Device(&graphql.Device{ ID: requestedDevice.ID, User: &user, }) return foundDevice, err } func deriveDeviceType(r *http.Request) graphql.DeviceType { userAgent := strings.ToLower(r.Header.Get("User-Agent")) if strings.HasPrefix(userAgent, "ios-imagini"){ return graphql.DeviceTypeIOs } else if strings.HasPrefix(userAgent, "android-imagini"){ return graphql.DeviceTypeAndroid } else if strings.HasPrefix(userAgent, "chrome"){ return graphql.DeviceTypeChrome } else if strings.HasPrefix(userAgent, "firefox"){ return graphql.DeviceTypeFirefox } else if strings.HasPrefix(userAgent, "msie"){ return graphql.DeviceTypeInternetExplorer } else if strings.HasPrefix(userAgent, "edge"){ return graphql.DeviceTypeEdge } else if strings.HasPrefix(userAgent, "safari"){ return graphql.DeviceTypeSafari } return graphql.DeviceTypeUnknown } func deriveRequestedDevice(r *http.Request) graphql.Device { deviceSkeleton := graphql.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 != "deviceid" && key != "devicename" { continue } // Derive Value val := trimQuotes(strings.TrimSpace(splitItem[1])) if key == "deviceid" { parsedDeviceUUID, err := uuid.Parse(val) if err != nil { log.Warn("[auth] deriveRequestedDevice - Unable to parse requested DeviceUUID: ", val) continue } stringDeviceUUID := parsedDeviceUUID.String() deviceSkeleton.ID = &stringDeviceUUID } else if key == "devicename" { deviceSkeleton.Name = val } } // If name not set, set to type if deviceSkeleton.Name == "" { deviceSkeleton.Name = deviceSkeleton.Type.String() } return deviceSkeleton } func (api *API) refreshAccessToken(w http.ResponseWriter, r *http.Request) (jwt.Token, error) { refreshCookie, err := r.Cookie("RefreshToken") if err != nil { log.Warn("[middleware] RefreshToken not found") return nil, err } // Validate Refresh Token refreshToken, err := api.Auth.ValidateJWTRefreshToken(refreshCookie.Value) if err != nil { http.SetCookie(w, &http.Cookie{Name: "AccessToken", Expires: time.Unix(0, 0)}) http.SetCookie(w, &http.Cookie{Name: "RefreshToken", Expires: time.Unix(0, 0)}) return nil, err } // Acquire User & Device (Trusted) did, ok := refreshToken.Get("did") if !ok { return nil, err } uid, ok := refreshToken.Get(jwt.SubjectKey) if !ok { return nil, err } deviceUUID, err := uuid.Parse(fmt.Sprintf("%v", did)) if err != nil { return nil, err } userUUID, err := uuid.Parse(fmt.Sprintf("%v", uid)) if err != nil { return nil, err } stringUserUUID := userUUID.String() stringDeviceUUID := deviceUUID.String() // Device & User Skeleton user := graphql.User{ID: &stringUserUUID} device := graphql.Device{ID: &stringDeviceUUID} // Update token accessTokenString, err := api.Auth.CreateJWTAccessToken(user, device) if err != nil { return nil, err } accessCookie := http.Cookie{Name: "AccessToken", Value: accessTokenString} http.SetCookie(w, &accessCookie) // TODO: Update Refresh Key & Token // Convert to jwt.Token accessTokenBytes := []byte(accessTokenString) accessToken, err := jwt.ParseBytes(accessTokenBytes) return accessToken, err } func trimQuotes(s string) string { if len(s) >= 2 { if s[0] == '"' && s[len(s)-1] == '"' { return s[1 : len(s)-1] } } return s }