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

233 lines
6.9 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"
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-02-02 20:34:10 +00:00
graphql "reichard.io/imagini/graph/model"
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-02-01 23:24:09 +00:00
w.Header().Set("Access-Control-Allow-Origin", "*")
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-22 05:00:55 +00:00
accessCookie := http.Cookie{Name: "AccessToken", Value: accessToken, Path: "/", HttpOnly: true}
refreshCookie := http.Cookie{Name: "RefreshToken", Value: refreshToken, Path: "/", 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
2021-01-19 20:50:48 +00:00
/**
* This will find or create the requested device based on ID and User.
**/
2021-02-02 20:34:10 +00:00
func (api *API) upsertRequestedDevice(user graphql.User, r *http.Request) (graphql.Device, error) {
2021-01-19 20:50:48 +00:00
requestedDevice := deriveRequestedDevice(r)
requestedDevice.Type = deriveDeviceType(r)
2021-02-02 20:34:10 +00:00
requestedDevice.User.ID = user.ID
2021-01-19 20:50:48 +00:00
2021-02-02 20:34:10 +00:00
if *requestedDevice.ID == "" {
2021-02-01 23:24:09 +00:00
err := api.DB.CreateDevice(&requestedDevice)
createdDevice, err := api.DB.Device(&requestedDevice)
2021-01-19 20:50:48 +00:00
return createdDevice, err
}
2021-02-02 20:34:10 +00:00
foundDevice, err := api.DB.Device(&graphql.Device{
ID: requestedDevice.ID,
User: &user,
2021-01-19 20:50:48 +00:00
})
return foundDevice, err
}
2021-02-02 20:34:10 +00:00
func deriveDeviceType(r *http.Request) graphql.DeviceType {
2021-01-19 20:50:48 +00:00
userAgent := strings.ToLower(r.Header.Get("User-Agent"))
if strings.HasPrefix(userAgent, "ios-imagini"){
2021-02-02 20:34:10 +00:00
return graphql.DeviceTypeIOs
2021-01-19 20:50:48 +00:00
} else if strings.HasPrefix(userAgent, "android-imagini"){
2021-02-02 20:34:10 +00:00
return graphql.DeviceTypeAndroid
2021-01-19 20:50:48 +00:00
} else if strings.HasPrefix(userAgent, "chrome"){
2021-02-02 20:34:10 +00:00
return graphql.DeviceTypeChrome
2021-01-19 20:50:48 +00:00
} else if strings.HasPrefix(userAgent, "firefox"){
2021-02-02 20:34:10 +00:00
return graphql.DeviceTypeFirefox
2021-01-19 20:50:48 +00:00
} else if strings.HasPrefix(userAgent, "msie"){
2021-02-02 20:34:10 +00:00
return graphql.DeviceTypeInternetExplorer
2021-01-19 20:50:48 +00:00
} else if strings.HasPrefix(userAgent, "edge"){
2021-02-02 20:34:10 +00:00
return graphql.DeviceTypeEdge
2021-01-19 20:50:48 +00:00
} else if strings.HasPrefix(userAgent, "safari"){
2021-02-02 20:34:10 +00:00
return graphql.DeviceTypeSafari
2021-01-19 20:50:48 +00:00
}
2021-02-02 20:34:10 +00:00
return graphql.DeviceTypeUnknown
2021-01-19 20:50:48 +00:00
}
2021-02-02 20:34:10 +00:00
func deriveRequestedDevice(r *http.Request) graphql.Device {
deviceSkeleton := graphql.Device{}
2021-01-19 20:50:48 +00:00
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]))
2021-02-02 20:34:10 +00:00
if key != "deviceid" && key != "devicename" {
2021-01-19 20:50:48 +00:00
continue
}
// Derive Value
2021-01-19 21:26:10 +00:00
val := trimQuotes(strings.TrimSpace(splitItem[1]))
2021-02-02 20:34:10 +00:00
if key == "deviceid" {
2021-01-19 20:50:48 +00:00
parsedDeviceUUID, err := uuid.Parse(val)
if err != nil {
log.Warn("[auth] deriveRequestedDevice - Unable to parse requested DeviceUUID: ", val)
continue
}
2021-02-02 20:34:10 +00:00
stringDeviceUUID := parsedDeviceUUID.String()
deviceSkeleton.ID = &stringDeviceUUID
2021-01-19 20:50:48 +00:00
} else if key == "devicename" {
deviceSkeleton.Name = val
}
}
// If name not set, set to type
if deviceSkeleton.Name == "" {
2021-02-02 20:34:10 +00:00
deviceSkeleton.Name = deviceSkeleton.Type.String()
2021-01-19 20:50:48 +00:00
}
return deviceSkeleton
}
2021-02-01 23:24:09 +00:00
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
}
2021-02-02 20:34:10 +00:00
stringUserUUID := userUUID.String()
stringDeviceUUID := deviceUUID.String()
2021-02-01 23:24:09 +00:00
// Device & User Skeleton
2021-02-02 20:34:10 +00:00
user := graphql.User{ID: &stringUserUUID}
device := graphql.Device{ID: &stringDeviceUUID}
2021-02-01 23:24:09 +00:00
// 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
}
2021-01-19 20:50:48 +00:00
func trimQuotes(s string) string {
if len(s) >= 2 {
if s[0] == '"' && s[len(s)-1] == '"' {
return s[1 : len(s)-1]
}
}
return s
}