Basic Auth Context
This commit is contained in:
@@ -4,14 +4,16 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
"strings"
|
||||
"context"
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/lestrrat-go/jwx/jwt"
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
|
||||
"reichard.io/imagini/internal/models"
|
||||
graphql "reichard.io/imagini/graph/model"
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -22,7 +24,7 @@ func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Decode into Struct
|
||||
var creds models.APICredentials
|
||||
var creds APICredentials
|
||||
err := json.NewDecoder(r.Body).Decode(&creds)
|
||||
if err != nil {
|
||||
errorJSON(w, "Invalid parameters.", http.StatusBadRequest)
|
||||
@@ -36,7 +38,7 @@ func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Do login
|
||||
resp, user := api.Auth.AuthenticateUser(creds)
|
||||
resp, user := api.Auth.AuthenticateUser(creds.User, creds.Password)
|
||||
if !resp {
|
||||
errorJSON(w, "Invalid credentials.", http.StatusUnauthorized)
|
||||
return
|
||||
@@ -82,7 +84,7 @@ func (api *API) logoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
/**
|
||||
* 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) {
|
||||
func (api *API) upsertRequestedDevice(user model.User, r *http.Request) (model.Device, error) {
|
||||
requestedDevice := deriveRequestedDevice(r)
|
||||
requestedDevice.Type = deriveDeviceType(r)
|
||||
requestedDevice.User.ID = user.ID
|
||||
@@ -93,7 +95,7 @@ func (api *API) upsertRequestedDevice(user graphql.User, r *http.Request) (graph
|
||||
return createdDevice, err
|
||||
}
|
||||
|
||||
foundDevice, err := api.DB.Device(&graphql.Device{
|
||||
foundDevice, err := api.DB.Device(&model.Device{
|
||||
ID: requestedDevice.ID,
|
||||
User: &user,
|
||||
})
|
||||
@@ -101,28 +103,28 @@ func (api *API) upsertRequestedDevice(user graphql.User, r *http.Request) (graph
|
||||
return foundDevice, err
|
||||
}
|
||||
|
||||
func deriveDeviceType(r *http.Request) graphql.DeviceType {
|
||||
func deriveDeviceType(r *http.Request) model.DeviceType {
|
||||
userAgent := strings.ToLower(r.Header.Get("User-Agent"))
|
||||
if strings.HasPrefix(userAgent, "ios-imagini"){
|
||||
return graphql.DeviceTypeIOs
|
||||
return model.DeviceTypeIOs
|
||||
} else if strings.HasPrefix(userAgent, "android-imagini"){
|
||||
return graphql.DeviceTypeAndroid
|
||||
return model.DeviceTypeAndroid
|
||||
} else if strings.HasPrefix(userAgent, "chrome"){
|
||||
return graphql.DeviceTypeChrome
|
||||
return model.DeviceTypeChrome
|
||||
} else if strings.HasPrefix(userAgent, "firefox"){
|
||||
return graphql.DeviceTypeFirefox
|
||||
return model.DeviceTypeFirefox
|
||||
} else if strings.HasPrefix(userAgent, "msie"){
|
||||
return graphql.DeviceTypeInternetExplorer
|
||||
return model.DeviceTypeInternetExplorer
|
||||
} else if strings.HasPrefix(userAgent, "edge"){
|
||||
return graphql.DeviceTypeEdge
|
||||
return model.DeviceTypeEdge
|
||||
} else if strings.HasPrefix(userAgent, "safari"){
|
||||
return graphql.DeviceTypeSafari
|
||||
return model.DeviceTypeSafari
|
||||
}
|
||||
return graphql.DeviceTypeUnknown
|
||||
return model.DeviceTypeUnknown
|
||||
}
|
||||
|
||||
func deriveRequestedDevice(r *http.Request) graphql.Device {
|
||||
deviceSkeleton := graphql.Device{}
|
||||
func deriveRequestedDevice(r *http.Request) model.Device {
|
||||
deviceSkeleton := model.Device{}
|
||||
authHeader := r.Header.Get("X-Imagini-Authorization")
|
||||
splitAuthInfo := strings.Split(authHeader, ",")
|
||||
|
||||
@@ -202,8 +204,8 @@ func (api *API) refreshAccessToken(w http.ResponseWriter, r *http.Request) (jwt.
|
||||
stringDeviceUUID := deviceUUID.String()
|
||||
|
||||
// Device & User Skeleton
|
||||
user := graphql.User{ID: &stringUserUUID}
|
||||
device := graphql.Device{ID: &stringDeviceUUID}
|
||||
user := model.User{ID: &stringUserUUID}
|
||||
device := model.Device{ID: &stringDeviceUUID}
|
||||
|
||||
// Update token
|
||||
accessTokenString, err := api.Auth.CreateJWTAccessToken(user, device)
|
||||
@@ -230,3 +232,17 @@ func trimQuotes(s string) string {
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func hasMinRoleDirective(ctx context.Context, obj interface{}, next graphql.Resolver, role model.Role) (res interface{}, err error) {
|
||||
// if !getCurrentUser(ctx).HasRole(role) {
|
||||
// // block calling the next resolver
|
||||
// return nil, fmt.Errorf("Access denied")
|
||||
// }
|
||||
|
||||
// or let it pass through
|
||||
return next(ctx)
|
||||
}
|
||||
|
||||
func metaDirective(ctx context.Context, obj interface{}, next graphql.Resolver, gorm *string) (res interface{}, err error){
|
||||
return next(ctx)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"os"
|
||||
"context"
|
||||
"net/http"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
type Middleware func(http.Handler) http.HandlerFunc
|
||||
@@ -20,6 +22,35 @@ func multipleMiddleware(h http.HandlerFunc, m ...Middleware) http.HandlerFunc {
|
||||
return wrapped
|
||||
}
|
||||
|
||||
func (api *API) injectContextMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Info("[middleware] Entering testMiddleware...")
|
||||
authContext := &model.AuthContext{
|
||||
AuthResponse: &w,
|
||||
AuthRequest: r,
|
||||
}
|
||||
accessCookie, err := r.Cookie("AccessToken")
|
||||
if err != nil {
|
||||
log.Warn("[middleware] AccessToken not found")
|
||||
} else {
|
||||
authContext.AccessToken = accessCookie.Value
|
||||
}
|
||||
refreshCookie, err := r.Cookie("RefreshToken")
|
||||
if err != nil {
|
||||
log.Warn("[middleware] RefreshToken not found")
|
||||
} else {
|
||||
authContext.RefreshToken = refreshCookie.Value
|
||||
}
|
||||
|
||||
// Add context
|
||||
ctx := context.WithValue(r.Context(), "auth", authContext)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
log.Info("[middleware] Exiting testMiddleware...")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) authMiddleware(next http.Handler) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package models
|
||||
package api
|
||||
|
||||
type APICredentials struct {
|
||||
User string `json:"user"`
|
||||
@@ -1,33 +1,38 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
|
||||
"reichard.io/imagini/internal/models"
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
"github.com/99designs/gqlgen/graphql/playground"
|
||||
|
||||
"reichard.io/imagini/graph"
|
||||
"reichard.io/imagini/graph/generated"
|
||||
)
|
||||
|
||||
func (api *API) registerRoutes() {
|
||||
srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))
|
||||
|
||||
// TODO: Provide Authentication for srv
|
||||
// Set up Directives
|
||||
c := generated.Config{ Resolvers: &graph.Resolver{ DB: api.DB } }
|
||||
c.Directives.HasMinRole = hasMinRoleDirective
|
||||
c.Directives.Meta = metaDirective
|
||||
srv := handler.NewDefaultServer(generated.NewExecutableSchema(c))
|
||||
|
||||
// Handle GraphQL
|
||||
api.Router.Handle("/playground", playground.Handler("GraphQL playground", "/query"))
|
||||
api.Router.Handle("/query", srv)
|
||||
api.Router.Handle("/query", api.injectContextMiddleware(srv))
|
||||
|
||||
// Handle Resource Route
|
||||
api.Router.HandleFunc("/media/", multipleMiddleware(
|
||||
api.mediaHandler,
|
||||
api.authMiddleware,
|
||||
))
|
||||
api.Router.HandleFunc("/logout", api.logoutHandler)
|
||||
api.Router.HandleFunc("/login", api.loginHandler)
|
||||
|
||||
}
|
||||
|
||||
func errorJSON(w http.ResponseWriter, err string, code int) {
|
||||
errStruct := &models.APIResponse{Error: &models.APIError{Message: err, Code: int64(code)}}
|
||||
errStruct := &APIResponse{Error: &APIError{Message: err, Code: int64(code)}}
|
||||
responseJSON(w, errStruct, code)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,9 @@ import (
|
||||
"github.com/lestrrat-go/jwx/jwa"
|
||||
"github.com/lestrrat-go/jwx/jwt"
|
||||
|
||||
"reichard.io/imagini/graph/model"
|
||||
"reichard.io/imagini/internal/db"
|
||||
"reichard.io/imagini/internal/config"
|
||||
|
||||
graphql "reichard.io/imagini/graph/model"
|
||||
"reichard.io/imagini/internal/models"
|
||||
"reichard.io/imagini/internal/session"
|
||||
)
|
||||
|
||||
@@ -35,21 +33,21 @@ func NewMgr(db *db.DBManager, c *config.Config) *AuthManager {
|
||||
}
|
||||
}
|
||||
|
||||
func (auth *AuthManager) AuthenticateUser(creds models.APICredentials) (bool, graphql.User) {
|
||||
func (auth *AuthManager) AuthenticateUser(user, password string) (bool, model.User) {
|
||||
// Search Objects
|
||||
userByName := &graphql.User{}
|
||||
userByName.Username = creds.User
|
||||
userByName := &model.User{}
|
||||
userByName.Username = user
|
||||
|
||||
foundUser, err := auth.DB.User(userByName)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
userByEmail := &graphql.User{}
|
||||
userByEmail.Email = creds.User
|
||||
userByEmail := &model.User{}
|
||||
userByEmail.Email = user
|
||||
foundUser, err = auth.DB.User(userByEmail)
|
||||
}
|
||||
|
||||
// Error Checking
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
log.Warn("[auth] User not found: ", creds.User)
|
||||
log.Warn("[auth] User not found: ", user)
|
||||
return false, foundUser
|
||||
} else if err != nil {
|
||||
log.Error(err)
|
||||
@@ -61,15 +59,15 @@ func (auth *AuthManager) AuthenticateUser(creds models.APICredentials) (bool, gr
|
||||
// Determine Type
|
||||
switch foundUser.AuthType {
|
||||
case "Local":
|
||||
return authenticateLocalUser(foundUser, creds.Password), foundUser
|
||||
return authenticateLocalUser(foundUser, password), foundUser
|
||||
case "LDAP":
|
||||
return authenticateLDAPUser(foundUser, creds.Password), foundUser
|
||||
return authenticateLDAPUser(foundUser, password), foundUser
|
||||
default:
|
||||
return false, foundUser
|
||||
}
|
||||
}
|
||||
|
||||
func (auth *AuthManager) getRole(user graphql.User) string {
|
||||
func (auth *AuthManager) getRole(user model.User) string {
|
||||
// TODO: Lookup role of user
|
||||
return "User"
|
||||
}
|
||||
@@ -88,7 +86,7 @@ func (auth *AuthManager) ValidateJWTRefreshToken(refreshJWT string) (jwt.Token,
|
||||
return nil, errors.New("did does not parse")
|
||||
}
|
||||
stringDeviceID := deviceID.String()
|
||||
device, err := auth.DB.Device(&graphql.Device{ID: &stringDeviceID})
|
||||
device, err := auth.DB.Device(&model.Device{ID: &stringDeviceID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -119,7 +117,7 @@ func (auth *AuthManager) ValidateJWTAccessToken(accessJWT string) (jwt.Token, er
|
||||
return verifiedToken, nil
|
||||
}
|
||||
|
||||
func (auth *AuthManager) CreateJWTRefreshToken(user graphql.User, device graphql.Device) (string, error) {
|
||||
func (auth *AuthManager) CreateJWTRefreshToken(user model.User, device model.Device) (string, error) {
|
||||
// Acquire Refresh Key
|
||||
byteKey := []byte(*device.RefreshKey)
|
||||
|
||||
@@ -154,7 +152,7 @@ func (auth *AuthManager) CreateJWTRefreshToken(user graphql.User, device graphql
|
||||
return string(signed), nil
|
||||
}
|
||||
|
||||
func (auth *AuthManager) CreateJWTAccessToken(user graphql.User, device graphql.Device) (string, error) {
|
||||
func (auth *AuthManager) CreateJWTAccessToken(user model.User, device model.Device) (string, error) {
|
||||
// Create New Token
|
||||
tm := time.Now()
|
||||
t := jwt.New()
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"errors"
|
||||
"path"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
log "github.com/sirupsen/logrus"
|
||||
// "gorm.io/gorm/logger"
|
||||
"gorm.io/driver/sqlite"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"reichard.io/imagini/internal/config"
|
||||
"reichard.io/imagini/graph/model"
|
||||
@@ -59,6 +59,7 @@ func (dbm *DBManager) bootstrapDatabase() {
|
||||
Username: "admin",
|
||||
AuthType: "Local",
|
||||
Password: &password,
|
||||
Role: model.RoleAdmin,
|
||||
}
|
||||
|
||||
err := dbm.CreateUser(user)
|
||||
@@ -68,8 +69,6 @@ func (dbm *DBManager) bootstrapDatabase() {
|
||||
}
|
||||
}
|
||||
|
||||
// func (dmb *DBManager) {}
|
||||
|
||||
func (dbm *DBManager) QueryBuilder(dest interface{}, params []byte) (int64, error) {
|
||||
// TODO:
|
||||
// - Where Filters
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
func (dbm *DBManager) CreateUser(user *model.User) error {
|
||||
func (dbm *DBManager) CreateUser (user *model.User) error {
|
||||
log.Info("[db] Creating user: ", user.Username)
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*user.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
@@ -27,6 +27,13 @@ func (dbm *DBManager) User (user *model.User) (model.User, error) {
|
||||
return foundUser, err
|
||||
}
|
||||
|
||||
func (dbm *DBManager) Users () ([]*model.User, int64, error) {
|
||||
var foundUsers []*model.User
|
||||
var count int64
|
||||
err := dbm.db.Find(&foundUsers).Count(&count).Error
|
||||
return foundUsers, count, err
|
||||
}
|
||||
|
||||
func (dbm *DBManager) DeleteUser (user model.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user