(WIP) Refresh & Access

This commit is contained in:
2021-01-17 23:56:56 -05:00
parent cd97b6262f
commit 377903f7a1
14 changed files with 366 additions and 136 deletions

View File

@@ -15,9 +15,9 @@ type API struct {
func NewApi(db *db.DBManager, auth *auth.AuthManager) *API {
api := &API{
Router: http.NewServeMux(),
DB: db,
Auth: auth,
Router: http.NewServeMux(),
Auth: auth,
DB: db,
}
api.registerRoutes()
return api

View File

@@ -35,22 +35,25 @@ func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) {
return
}
// TODO: Is user already logged in? If so refresh token, if different user, kill session and log in new user?
// Do login
resp := api.Auth.AuthenticateUser(creds)
if resp == true {
// Return Success
cookie := http.Cookie{
Name: "Token",
Value: "testToken",
}
http.SetCookie(w, &cookie)
successJSON(w, "Login success.", http.StatusOK)
}else {
// Return Failure
if !resp {
errorJSON(w, "Invalid credentials.", http.StatusUnauthorized)
return
}
// Create tokens
accessToken := api.Auth.CreateJWTAccessToken()
refreshToken := api.Auth.CreateRefreshToken()
// Set appropriate cookies
accessCookie := http.Cookie{Name: "AccessToken", Value: accessToken}
refreshCookie := http.Cookie{Name: "RefreshToken", Value: refreshToken}
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) {
@@ -73,3 +76,20 @@ func (api *API) logoutHandler(w http.ResponseWriter, r *http.Request) {
}
http.SetCookie(w, cookie)
}
func (api *API) refreshLoginHandler(w http.ResponseWriter, r *http.Request) {
ok := api.Auth.ValidateRefreshToken()
if !ok {
// TODO: Clear Access & Refresh Cookies
errorJSON(w, "Invalid credentials.", http.StatusUnauthorized)
return
}
// Update token
accessToken := api.Auth.CreateJWTAccessToken()
accessCookie := http.Cookie{Name: "AccessToken", Value: accessToken}
http.SetCookie(w, &accessCookie)
// Response success
successJSON(w, "Refresh success.", http.StatusOK)
}

View File

@@ -2,13 +2,14 @@ package api
import (
"net/http"
"log"
"os"
log "github.com/sirupsen/logrus"
)
type Middleware func(http.Handler) http.Handler
type Middleware func(http.Handler) http.HandlerFunc
func MultipleMiddleware(h http.Handler, m ...Middleware) http.Handler {
func multipleMiddleware(h http.HandlerFunc, m ...Middleware) http.HandlerFunc {
if len(m) < 1 {
return h
}
@@ -19,19 +20,33 @@ func MultipleMiddleware(h http.Handler, m ...Middleware) http.Handler {
return wrapped
}
// func authMiddleware(h http.Handler) http.Handler {
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// _, ok := ValidateUserToken(r)
//
// if ok {
// next.ServeHTTP(w, r)
// } else {
// w.WriteHeader(http.StatusUnauthorized)
// }
// })
// }
func (api *API) authMiddleware(next http.Handler) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("Token")
if err != nil {
log.Warn("[middleware] Cookie not found")
w.WriteHeader(http.StatusUnauthorized)
return
}
func logMiddleware(h http.Handler) http.Handler {
// Validate cookie.Value JWT with
api.Auth.ValidateJWTToken(cookie.Value)
log.Info("[middleware] Cookie Name: ", cookie.Name)
log.Info("[middleware] Cookie Value: ", cookie.Value)
next.ServeHTTP(w, r)
// if true {
// next.ServeHTTP(w, r)
// } else {
// w.WriteHeader(http.StatusUnauthorized)
// }
})
}
func (api *API) logMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.SetOutput(os.Stdout)
log.Println(r.Method, r.URL)

View File

@@ -6,15 +6,38 @@ import (
)
func (api *API) registerRoutes() {
api.Router.HandleFunc("/MediaItems", api.mediaItemsHandler)
api.Router.HandleFunc("/Upload", api.uploadHandler)
api.Router.HandleFunc("/Albums", api.albumsHandler)
api.Router.HandleFunc("/Logout", api.logoutHandler)
api.Router.HandleFunc("/Login", api.loginHandler)
api.Router.HandleFunc("/Users", api.usersHandler)
api.Router.HandleFunc("/Tags", api.tagsHandler)
api.Router.HandleFunc("/Info", api.infoHandler)
api.Router.HandleFunc("/Me", api.meHandler)
api.Router.HandleFunc("/MediaItems", multipleMiddleware(
api.mediaItemsHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/Upload", multipleMiddleware(
api.uploadHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/Albums", multipleMiddleware(
api.albumsHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/Users", multipleMiddleware(
api.usersHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/Tags", multipleMiddleware(
api.tagsHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/Info", multipleMiddleware(
api.infoHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/Me", multipleMiddleware(
api.meHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/Logout", api.logoutHandler)
api.Router.HandleFunc("/Login", api.loginHandler)
api.Router.HandleFunc("/RefreshLogin", api.refreshLoginHandler)
}
// https://stackoverflow.com/a/59764037

View File

@@ -31,9 +31,9 @@ func (api *API) meHandler(w http.ResponseWriter, r *http.Request) {
// Get Authenticated User & Return Object
authCookie, err := r.Cookie("Token")
if err != nil {
log.Error("[routes] ", err)
log.Error("[api] ", err)
return
}
log.Info("[routes] INFO: ", authCookie)
log.Info("[api] Auth Cookie: ", authCookie)
}

View File

@@ -1,20 +1,37 @@
package auth
import (
"errors"
"errors"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
"reichard.io/imagini/internal/db"
"reichard.io/imagini/internal/models"
log "github.com/sirupsen/logrus"
"reichard.io/imagini/internal/db"
"reichard.io/imagini/internal/config"
"reichard.io/imagini/internal/models"
"reichard.io/imagini/internal/session"
"encoding/json"
"fmt"
"time"
"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/jwt"
)
type AuthManager struct {
DB *db.DBManager
DB *db.DBManager
Config *config.Config
Session *session.SessionManager
}
func NewMgr(db *db.DBManager) *AuthManager {
func NewMgr(db *db.DBManager, c *config.Config) *AuthManager {
session := session.NewMgr()
return &AuthManager{
DB: db,
Config: c,
Session: session,
}
}
@@ -46,3 +63,123 @@ func (auth *AuthManager) AuthenticateUser(creds models.APICredentials) bool {
return false
}
}
func (auth *AuthManager) ValidateJWTToken(userJWT string) bool {
byteUserJWT := []byte(userJWT)
serverToken, err := jwt.ParseBytes(byteUserJWT, jwt.WithVerify(jwa.HS256, auth.Config.JWTSecret))
if err != nil {
fmt.Println("failed to parse payload: ", err)
}
uid, ok := serverToken.Get("uid");
if !ok {
fmt.Println("failed to acquire uid")
}
userID := fmt.Sprintf("%v", uid)
userKey := auth.Session.Get(userID)
userToken, err := jwt.ParseBytes(byteUserJWT, jwt.WithVerify(jwa.HS256, userKey))
if err != nil {
fmt.Println("failed to parse payload: ", err)
}
_ = userToken
// TODO:
// - Get User ID from UNVALIDATED token
// - Lookup user key, concat with server key
// - Validate with concatted user & server key
// validatedToken, err := jwt.ParseBytes(byteUserJWT, jwt.WithVerify(jwa.HS256, concatKey))
// if err != nil {
// fmt.Printf("failed to parse payload: %s\n", err)
// }
// userToken := auth.Session.Get(userID)
// log.Info("[auth] DEBUG: ", userToken)
return false
}
func (auth *AuthManager) RevokeRefreshToken() {
}
func (auth *AuthManager) ValidateRefreshToken(refreshToken, deviceID string) bool {
// Acquire Device
deviceUUID, err := uuid.Parse(deviceID)
device := models.Device{Base: models.Base{UUID: deviceUUID}}
foundDevice, err := auth.DB.Device(device)
// Validate Expiration
expTime, err := time.Parse(time.RFC3339, foundDevice.RefreshExp)
if expTime.Before(time.Now()) {
return false
}
// Validate Token
bRefreshToken :=[]byte(refreshToken)
err = bcrypt.CompareHashAndPassword([]byte(foundDevice.RefreshToken), bRefreshToken)
if err == nil {
log.Info("[auth] Refresh Token validation succeeded: ", foundDevice.UUID)
return true
}
log.Warn("[auth] Refresh Token validation failed: ", foundDevice.UUID)
return false
}
func (auth *AuthManager) UpdateRefreshToken(deviceID string) error {
// TODO:
// - Remove Refresh token from Session AND DB
// - Call CreateRefreshToken
return nil
}
func (auth *AuthManager) CreateRefreshToken(deviceID string) (string, error) {
// TODO:
// - Create regular bcrypt password
// - Create Expiration (Depends on Device Type)
// - Store in DB: DeviceID, ValidUntil
generatedToken := uuid.New().String()
hashedRefreshToken, err := bcrypt.GenerateFromPassword([]byte(generatedToken), bcrypt.DefaultCost)
if err != nil {
log.Error(err)
return "", err
}
_ = string(hashedRefreshToken)
return "", nil
}
func (auth *AuthManager) CreateJWTAccessToken(user, role, deviceID string) (string, error) {
// Create New Token
tm := time.Now()
t := jwt.New()
t.Set(`did`, deviceID) // Device ID
t.Set(`role`, role) // User Role (Admin / User)
t.Set(jwt.SubjectKey, user) // User ID
t.Set(jwt.AudienceKey, `imagini`) // App ID
t.Set(jwt.IssuedAtKey, tm) // Issued At
t.Set(jwt.ExpirationKey, tm.Add(time.Minute * 30)) // 30 Minute Access Key
// Validate Token Creation
_, err := json.MarshalIndent(t, "", " ")
if err != nil {
fmt.Printf("failed to generate JSON: %s\n", err)
return "", err
}
// Use Server Key
byteKey := []byte(auth.Config.JWTSecret)
// Sign Token
signed, err := jwt.Sign(t, jwa.HS256, byteKey)
if err != nil {
log.Printf("failed to sign token: %s", err)
return "", err
}
// Return Token
return string(signed), nil
}

View File

@@ -34,6 +34,7 @@ func NewMgr(c *config.Config) *DBManager {
// Initialize database
dbm.db.AutoMigrate(&models.ServerSetting{})
dbm.db.AutoMigrate(&models.Device{})
dbm.db.AutoMigrate(&models.User{})
dbm.db.AutoMigrate(&models.MediaItem{})
dbm.db.AutoMigrate(&models.Tag{})
@@ -83,71 +84,3 @@ func (dbm *DBManager) ItemsFromAlbum(user models.User, album models.Album) []mod
// INNER JOIN MediaItems ON MediaAlbums.mediaID = MediaItems.mediaID
// WHERE MediaAlbums.albumID = ? AND MediaItems.userID = ?`, albumID, userID)
}
// func ItemsFromTags(userID int, tagID int) []MediaItem {
// return nil
// }
//
// func IndexMediaItems(newItems []MediaItem) {
// }
// func PopulateTestData() {
// user1 := User{Username: "Evan", Email: "evan@reichard.io", FirstName: "Evan", LastName: "Reichard", AuthType: "LDAP", Salt: "1234", HashedPWSalt: "1234"}
// user2 := User{Username: "Ryan", Email: "ryan@example.com", FirstName: "Ryan", LastName: "Dunfrey", AuthType: "Local", Salt: "2345", HashedPWSalt: "2345"}
// user3 := User{Username: "Bill", Email: "bill@example.com", FirstName: "Bill", LastName: "Smith", AuthType: "LDAP", Salt: "3456", HashedPWSalt: "3456"}
//
// mi1 := MediaItem{
// User: user1,
// EXIFDate: time.Now(),
// Latitude: "1234",
// Longitude: "1234",
// RelPath: "./1234.jpg",
// Tags: []Tag{
// {Name: "Tag1"},
// {Name: "Tag2"},
// },
// Albums: []Album{
// {Name: "Album1"},
// {Name: "Album2"},
// },
// }
//
// mi2 := MediaItem{
// User: user2,
// EXIFDate: time.Now(),
// Latitude: "1234",
// Longitude: "1234",
// RelPath: "./1234.jpg",
// Tags: []Tag{
// {Name: "Tag3"},
// {Name: "Tag4"},
// },
// Albums: []Album{
// {Name: "Album3"},
// {Name: "Album4"},
// },
// }
//
// mi3 := MediaItem{
// User: user3,
// EXIFDate: time.Now(),
// Latitude: "1234",
// Longitude: "1234",
// RelPath: "./1234.jpg",
// Tags: []Tag{
// {Name: "Tag4"},
// {Name: "Tag5"},
// },
// Albums: []Album{
// {Name: "Album1"},
// {Name: "Album7"},
// },
// }
//
// // db.Create(&user1)
// // db.Create(&user2)
// // db.Create(&user3)
// db.Create(&mi1)
// db.Create(&mi2)
// db.Create(&mi3)
// }

44
internal/db/devices.go Normal file
View File

@@ -0,0 +1,44 @@
package db
import (
"errors"
"gorm.io/gorm"
"golang.org/x/crypto/bcrypt"
log "github.com/sirupsen/logrus"
"reichard.io/imagini/internal/models"
)
func (dbm *DBManager) CreateDevice(device models.Device) error {
log.Info("[query] Creating device: ", device.Name)
_, err := dbm.Device(device)
if !errors.Is(err, gorm.ErrRecordNotFound) {
log.Warn("[query] Device already exists: ", device.Name)
return errors.New("Device already exists")
}
// Generate random password
refreshToken := "asd123"
hashedToken, err := bcrypt.GenerateFromPassword([]byte(refreshToken), bcrypt.DefaultCost)
if err != nil {
log.Error(err)
return err
}
device.RefreshToken = string(hashedToken)
return dbm.db.Create(&device).Error
}
func (dbm *DBManager) Device (device models.Device) (models.Device, error) {
var foundDevice models.Device
var count int64
err := dbm.db.Where(&device).First(&foundDevice).Count(&count).Error
return foundDevice, err
}
func (dbm *DBManager) DeleteDevice (user models.Device) error {
return nil
}
func (dbm *DBManager) UpdateRefreshToken (device models.Device, refreshToken string) error {
return nil
}

View File

@@ -1,40 +1,53 @@
package models
import (
"github.com/google/uuid"
"gorm.io/gorm"
"time"
)
// Might not even need this
// Base contains common columns for all tables.
type Base struct {
UUID uuid.UUID `gorm:"type:uuid;default:default:uuid_generate_v4();primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
func (base *Base) BeforeCreate(tx *gorm.DB) (err error) {
base.UUID = uuid.New()
return
}
type ServerSetting struct {
gorm.Model
Base
Name string `json:"name" gorm:"not null"`
Description string `json:"description" gorm:"not null"`
Value string `json:"value" gorm:"not null"`
}
type Device struct {
gorm.Model
User User `json:"user" gorm:"ForeignKey:ID"`
DeviceName string `json:"name"`
Type string `json:"type"` // Android, iOS, Chrome, FireFox, Edge, etc
Base
User User `json:"user" gorm:"ForeignKey:UUID"`
Name string `json:"name"`
Type string `json:"type"` // Android, iOS, Chrome, FireFox, Edge, etc
RefreshExp string `json:"refresh_exp"`
RefreshToken string `json:"-"`
}
// TODO: ID -> UUID?
type User struct {
gorm.Model
Base
Email string `json:"email" gorm:"unique"`
Username string `json:"username" gorm:"unique"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
AuthType string `json:"auth_type" gorm:"default:Local;not null"`
Password string `json:"-"`
JWTSecret string `json:"-" gorm:"unique;not null"` // TODO: Auto Generate UUID
}
type MediaItem struct {
gorm.Model
User User `json:"user" gorm:"ForeignKey:ID;not null"`
Base
User User `json:"user" gorm:"ForeignKey:UUID;not null"`
EXIFDate time.Time `json:"exif_date"`
Latitude string `json:"latitude"`
Longitude string `json:"longitude"`
@@ -45,11 +58,11 @@ type MediaItem struct {
}
type Tag struct {
gorm.Model
Base
Name string `json:"name" gorm:"not null"`
}
type Album struct {
gorm.Model
Base
Name string `json:"name" gorm:"not null"`
}

View File

@@ -4,8 +4,11 @@ import (
"sync"
)
// Used to maintain a cache of user specific jwt secrets
// This will prevent DB lookups on every request
type SessionManager struct {
Mutex sync.Mutex
mutex sync.Mutex
values map[string]string
}
func NewMgr() *SessionManager {
@@ -13,13 +16,23 @@ func NewMgr() *SessionManager {
}
func (sm *SessionManager) Set(key, value string) {
sm.mutex.Lock()
sm.values[key] = value
sm.mutex.Unlock()
}
func (sm *SessionManager) Get(key string) string {
return ""
sm.mutex.Lock()
defer sm.mutex.Unlock()
return sm.values[key]
}
func (sm *SessionManager) Delete(key string) {
sm.mutex.Lock()
defer sm.mutex.Unlock()
_, exists := sm.values[key]
if !exists {
return
}
delete(sm.values, key)
}