This repository has been archived on 2023-11-13. You can view files and clone it, but cannot push or open issues or pull requests.
imagini/internal/api/auth.go

165 lines
5.2 KiB
Go
Raw Normal View History

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"
"github.com/lestrrat-go/jwx/jwt"
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
// https://www.calhoun.io/pitfalls-of-context-values-and-how-to-avoid-or-mitigate-them/
// https://pace.dev/blog/2018/05/09/how-I-write-http-services-after-eight-years.html
// https://medium.com/@benbjohnson/standard-package-layout-7cdbc8391fc1#333c
// https://www.alexedwards.net/blog/organising-database-access <---- best
// - TLDR: Do what you're doing, but use closeures for the handlers
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-18 21:16:52 +00:00
// Verify Device Name Exists
deviceHeader := r.Header.Get("X-Imagini-DeviceName")
if deviceHeader == "" {
errorJSON(w, "Missing 'X-Imagini-DeviceName' header.", http.StatusBadRequest)
return
}
// Derive Device Type
var deviceType string
userAgent := strings.ToLower(r.Header.Get("User-Agent"))
if strings.HasPrefix(userAgent, "ios-imagini"){
deviceType = "iOS"
} else if strings.HasPrefix(userAgent, "android-imagini"){
deviceType = "Android"
} else if strings.HasPrefix(userAgent, "chrome"){
deviceType = "Chrome"
} else if strings.HasPrefix(userAgent, "firefox"){
deviceType = "Firefox"
} else if strings.HasPrefix(userAgent, "msie"){
deviceType = "Internet Explorer"
} else if strings.HasPrefix(userAgent, "edge"){
deviceType = "Edge"
} else if strings.HasPrefix(userAgent, "safari"){
deviceType = "Safari"
}else {
deviceType = "Unknown"
}
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-18 21:16:52 +00:00
// Create New Device
device, err := api.DB.CreateDevice(models.Device{Name: deviceHeader, Type: deviceType})
// 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
}
// Do logout
// TODO: Clear Session Server Side
2021-01-10 00:44:02 +00:00
2021-01-12 04:48:32 +00:00
// Tell Client to Expire Token
cookie := &http.Cookie{
Name: "Token",
Value: "",
Path: "/",
Expires: time.Unix(0, 0),
HttpOnly: true,
}
http.SetCookie(w, cookie)
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)
}