Added header-based authentication #30

This commit is contained in:
Marc Ole Bulling
2021-12-12 20:02:07 +01:00
parent 094c51aa85
commit 8e10f3710c
7 changed files with 184 additions and 120 deletions

View File

@@ -43,28 +43,30 @@ var mutex sync.RWMutex
// Configuration is a struct that contains the global configuration
type Configuration struct {
Port string `json:"Port"`
AdminName string `json:"AdminName"`
AdminPassword string `json:"AdminPassword"`
ServerUrl string `json:"ServerUrl"`
DefaultDownloads int `json:"DefaultDownloads"`
DefaultExpiry int `json:"DefaultExpiry"`
DefaultPassword string `json:"DefaultPassword"`
RedirectUrl string `json:"RedirectUrl"`
Sessions map[string]models.Session `json:"Sessions"`
Files map[string]models.File `json:"Files"`
Hotlinks map[string]models.Hotlink `json:"Hotlinks"`
DownloadStatus map[string]models.DownloadStatus `json:"DownloadStatus"`
ApiKeys map[string]models.ApiKey `json:"ApiKeys"`
ConfigVersion int `json:"ConfigVersion"`
SaltAdmin string `json:"SaltAdmin"`
SaltFiles string `json:"SaltFiles"`
LengthId int `json:"LengthId"`
DataDir string `json:"DataDir"`
MaxMemory int `json:"MaxMemory"`
UseSsl bool `json:"UseSsl"`
MaxFileSizeMB int `json:"MaxFileSizeMB"`
DisableLogin bool `json:"DisableLogin"`
Port string `json:"Port"`
AdminName string `json:"AdminName"`
AdminPassword string `json:"AdminPassword"`
ServerUrl string `json:"ServerUrl"`
DefaultDownloads int `json:"DefaultDownloads"`
DefaultExpiry int `json:"DefaultExpiry"`
DefaultPassword string `json:"DefaultPassword"`
RedirectUrl string `json:"RedirectUrl"`
Sessions map[string]models.Session `json:"Sessions"`
Files map[string]models.File `json:"Files"`
Hotlinks map[string]models.Hotlink `json:"Hotlinks"`
DownloadStatus map[string]models.DownloadStatus `json:"DownloadStatus"`
ApiKeys map[string]models.ApiKey `json:"ApiKeys"`
ConfigVersion int `json:"ConfigVersion"`
SaltAdmin string `json:"SaltAdmin"`
SaltFiles string `json:"SaltFiles"`
LengthId int `json:"LengthId"`
DataDir string `json:"DataDir"`
MaxMemory int `json:"MaxMemory"`
UseSsl bool `json:"UseSsl"`
MaxFileSizeMB int `json:"MaxFileSizeMB"`
DisableLogin bool `json:"DisableLogin"`
LoginHeaderKey string `json:"LoginHeaderKey"`
LoginHeaderForceUsername bool `json:"LoginHeaderForceUsername"`
}
// Load loads the configuration or creates the folder structure and a default configuration
@@ -161,6 +163,8 @@ func updateConfig() {
// < v1.3.2
if serverSettings.ConfigVersion < 9 {
serverSettings.DisableLogin = environment.ToBool(Environment.DisableLogin)
serverSettings.LoginHeaderForceUsername = environment.ToBool(Environment.LoginHeaderForceUser)
serverSettings.LoginHeaderKey = Environment.LoginHeaderKey
}
if serverSettings.ConfigVersion < currentConfigVersion {
@@ -444,4 +448,8 @@ func IsLoginDisabled() bool {
return serverSettings.DisableLogin
}
func IsLogoutAvailable() bool {
return !serverSettings.DisableLogin && serverSettings.LoginHeaderKey == ""
}
var osExit = os.Exit

View File

@@ -15,28 +15,30 @@ const IsFalse = "no"
// Environment is a struct containing available env variables
type Environment struct {
ConfigDir string
ConfigFile string
ConfigPath string
DataDir string
AdminName string
AdminPassword string
WebserverPort string
WebserverLocalhost string
ExternalUrl string
RedirectUrl string
SaltAdmin string
SaltFiles string
UseSsl string
AwsBucket string
AwsRegion string
AwsKeyId string
AwsKeySecret string
AwsEndpoint string
DisableLogin string
LengthId int
MaxMemory int
MaxFileSize int
ConfigDir string
ConfigFile string
ConfigPath string
DataDir string
AdminName string
AdminPassword string
WebserverPort string
WebserverLocalhost string
ExternalUrl string
RedirectUrl string
SaltAdmin string
SaltFiles string
UseSsl string
AwsBucket string
AwsRegion string
AwsKeyId string
AwsKeySecret string
AwsEndpoint string
DisableLogin string
LoginHeaderKey string
LoginHeaderForceUser string
LengthId int
MaxMemory int
MaxFileSize int
}
// ToBool checks if a string output by the environment package is equal true or false
@@ -68,28 +70,30 @@ func New() Environment {
configPath := configDir + "/" + configFile
return Environment{
ConfigDir: configDir,
ConfigFile: configFile,
ConfigPath: configPath,
DataDir: envString("DATA_DIR"),
AdminName: envString("USERNAME"),
AdminPassword: envString("PASSWORD"),
WebserverPort: envString("PORT"),
ExternalUrl: envString("EXTERNAL_URL"),
RedirectUrl: envString("REDIRECT_URL"),
SaltAdmin: envString("SALT_ADMIN"),
SaltFiles: envString("SALT_FILES"),
WebserverLocalhost: envBool("LOCALHOST"),
LengthId: envInt("LENGTH_ID", 5),
MaxMemory: envInt("MAX_MEMORY_UPLOAD_MB", 5),
UseSsl: envBool("USE_SSL"),
AwsBucket: envString("AWS_BUCKET"),
AwsRegion: envString("AWS_REGION"),
AwsKeyId: envString("AWS_KEY"),
AwsKeySecret: envString("AWS_KEY_SECRET"),
AwsEndpoint: envString("AWS_ENDPOINT"),
MaxFileSize: envInt("MAX_FILESIZE", 1),
DisableLogin: envBool("DISABLE_LOGIN"),
ConfigDir: configDir,
ConfigFile: configFile,
ConfigPath: configPath,
DataDir: envString("DATA_DIR"),
AdminName: envString("USERNAME"),
AdminPassword: envString("PASSWORD"),
WebserverPort: envString("PORT"),
ExternalUrl: envString("EXTERNAL_URL"),
RedirectUrl: envString("REDIRECT_URL"),
SaltAdmin: envString("SALT_ADMIN"),
SaltFiles: envString("SALT_FILES"),
WebserverLocalhost: envBool("LOCALHOST"),
LengthId: envInt("LENGTH_ID", 5),
MaxMemory: envInt("MAX_MEMORY_UPLOAD_MB", 5),
UseSsl: envBool("USE_SSL"),
AwsBucket: envString("AWS_BUCKET"),
AwsRegion: envString("AWS_REGION"),
AwsKeyId: envString("AWS_KEY"),
AwsKeySecret: envString("AWS_KEY_SECRET"),
AwsEndpoint: envString("AWS_ENDPOINT"),
MaxFileSize: envInt("MAX_FILESIZE", 1),
DisableLogin: envBool("DISABLE_LOGIN"),
LoginHeaderKey: envString("LOGIN_HEADER_KEY"),
LoginHeaderForceUser: envBool("LOGIN_HEADER_FORCE_USER"),
}
}

View File

@@ -10,6 +10,7 @@ import (
"Gokapi/internal/models"
"Gokapi/internal/storage"
"Gokapi/internal/webserver/api"
"Gokapi/internal/webserver/authentication"
"Gokapi/internal/webserver/fileupload"
"Gokapi/internal/webserver/sessionmanager"
"Gokapi/internal/webserver/ssl"
@@ -48,13 +49,11 @@ var imageExpiredPicture []byte
const expiredFile = "static/expired.png"
var (
webserverPort string
webserverExtUrl string
webserverRedirectUrl string
webserverAdminName string
webserverAdminPassword string
webserverMaxMemory int
webserverUseSsl bool
webserverPort string
webserverExtUrl string
webserverRedirectUrl string
webserverMaxMemory int
webserverUseSsl bool
)
// Start the webserver on the port set in the config
@@ -117,8 +116,6 @@ func initLocalVariables() {
webserverPort = settings.Port
webserverExtUrl = settings.ServerUrl
webserverRedirectUrl = settings.RedirectUrl
webserverAdminName = settings.AdminName
webserverAdminPassword = settings.AdminPassword
webserverMaxMemory = settings.MaxMemory
webserverUseSsl = settings.UseSsl
configuration.ReleaseReadOnly()
@@ -211,7 +208,7 @@ func processApi(w http.ResponseWriter, r *http.Request) {
// Shows a login form. If username / pw combo is incorrect, client needs to wait for three seconds.
// If correct, a new session is created and the user is redirected to the admin menu
func showLogin(w http.ResponseWriter, r *http.Request) {
if configuration.IsLoginDisabled() {
if authentication.IsAuthenticated(w, r) {
redirect(w, "admin")
return
}
@@ -221,7 +218,7 @@ func showLogin(w http.ResponseWriter, r *http.Request) {
pw := r.Form.Get("password")
failedLogin := false
if pw != "" && user != "" {
if strings.ToLower(user) == strings.ToLower(webserverAdminName) && configuration.HashPassword(pw, false) == webserverAdminPassword {
if authentication.IsCorrectUsernameAndPassword(user, pw) {
sessionmanager.CreateSession(w, nil)
redirect(w, "admin")
return
@@ -350,19 +347,19 @@ type DownloadView struct {
// UploadView contains parameters for the admin menu template
type UploadView struct {
Items []models.File
ApiKeys []models.ApiKey
Url string
HotlinkUrl string
TimeNow int64
DefaultDownloads int
DefaultExpiry int
DefaultPassword string
IsAdminView bool
IsMainView bool
IsApiView bool
MaxFileSize int
IsLoginDisabled bool
Items []models.File
ApiKeys []models.ApiKey
Url string
HotlinkUrl string
TimeNow int64
DefaultDownloads int
DefaultExpiry int
DefaultPassword string
IsAdminView bool
IsMainView bool
IsApiView bool
MaxFileSize int
IsLogoutAvailable bool
}
// Converts the globalConfig variable to an UploadView struct to pass the infos to
@@ -408,7 +405,7 @@ func (u *UploadView) convertGlobalConfig(isMainView bool) *UploadView {
u.IsAdminView = true
u.IsMainView = isMainView
u.MaxFileSize = settings.MaxFileSizeMB
u.IsLoginDisabled = settings.DisableLogin
u.IsLogoutAvailable = configuration.IsLogoutAvailable()
configuration.ReleaseReadOnly()
return u
}
@@ -454,10 +451,7 @@ func downloadFile(w http.ResponseWriter, r *http.Request) {
// Checks if the user is logged in as an admin. Redirects to login page if not authenticated
func isAuthenticatedOrRedirect(w http.ResponseWriter, r *http.Request, isUpload bool) bool {
if configuration.IsLoginDisabled() {
return true
}
if sessionmanager.IsValidSession(w, r) {
if authentication.IsAuthenticated(w, r) {
return true
}
if isUpload {

View File

@@ -40,7 +40,7 @@ func Process(w http.ResponseWriter, r *http.Request, maxMemory int) {
// DeleteKey deletes the selected API key
func DeleteKey(id string) bool {
if !isValidKey(id, false) {
if !IsValidApiKey(id, false) {
return false
}
settings := configuration.GetServerSettings()
@@ -63,7 +63,7 @@ func NewKey() string {
}
func changeFriendlyName(w http.ResponseWriter, request apiRequest) {
if !isValidKey(request.apiKeyToModify, false) {
if !IsValidApiKey(request.apiKeyToModify, false) {
sendError(w, http.StatusBadRequest, "Invalid api key provided.")
return
}
@@ -115,25 +115,8 @@ func upload(w http.ResponseWriter, request apiRequest, maxMemory int) {
sendOk(w)
}
func isValidKey(key string, modifyTime bool) bool {
if key == "" {
return false
}
settings := configuration.GetServerSettings()
defer configuration.Release()
savedKey, ok := settings.ApiKeys[key]
if ok && savedKey.Id != "" {
if modifyTime {
savedKey.LastUsed = time.Now().Unix()
settings.ApiKeys[key] = savedKey
}
return true
}
return false
}
func isAuthorisedForApi(w http.ResponseWriter, request apiRequest) bool {
if isValidKey(request.apiKey, true) || sessionmanager.IsValidSession(w, request.request) {
if IsValidApiKey(request.apiKey, true) || sessionmanager.IsValidSession(w, request.request) {
return true
}
sendError(w, http.StatusUnauthorized, "Unauthorized")
@@ -168,3 +151,20 @@ func parseRequest(r *http.Request) apiRequest {
request: r,
}
}
func IsValidApiKey(key string, modifyTime bool) bool {
if key == "" {
return false
}
settings := configuration.GetServerSettings()
defer configuration.Release()
savedKey, ok := settings.ApiKeys[key]
if ok && savedKey.Id != "" {
if modifyTime {
savedKey.LastUsed = time.Now().Unix()
settings.ApiKeys[key] = savedKey
}
return true
}
return false
}

View File

@@ -51,11 +51,11 @@ func TestDeleteKey(t *testing.T) {
func TestIsValidApiKey(t *testing.T) {
settings := configuration.GetServerSettings()
configuration.Release()
test.IsEqualBool(t, isValidKey("", false), false)
test.IsEqualBool(t, isValidKey("invalid", false), false)
test.IsEqualBool(t, isValidKey("validkey", false), true)
test.IsEqualBool(t, IsValidApiKey("", false), false)
test.IsEqualBool(t, IsValidApiKey("invalid", false), false)
test.IsEqualBool(t, IsValidApiKey("validkey", false), true)
test.IsEqualBool(t, settings.ApiKeys["validkey"].LastUsed == 0, true)
test.IsEqualBool(t, isValidKey("validkey", true), true)
test.IsEqualBool(t, IsValidApiKey("validkey", true), true)
test.IsEqualBool(t, settings.ApiKeys["validkey"].LastUsed == 0, false)
}

View File

@@ -0,0 +1,58 @@
package authentication
import (
"Gokapi/internal/configuration"
"Gokapi/internal/webserver/sessionmanager"
"net/http"
"strings"
)
func IsAuthenticated(w http.ResponseWriter, r *http.Request) bool {
if byDisabledLogin() {
return true
}
if byHeader(r) {
return true
}
if byInternalSession(w, r) {
return true
}
return false
}
// byHeader returns true if the user was authenticated by a proxy header if enabled
func byHeader(r *http.Request) bool {
settings := configuration.GetServerSettingsReadOnly()
defer configuration.ReleaseReadOnly()
if settings.LoginHeaderKey == "" {
return false
}
value := r.Header.Get(settings.LoginHeaderKey)
if value == "" {
return false
}
if settings.LoginHeaderForceUsername {
return strings.ToLower(value) == strings.ToLower(settings.AdminName)
} else {
return true
}
}
// byDisabledLogin returns true if login has been disabled
func byDisabledLogin() bool {
return configuration.IsLoginDisabled()
}
// byInternalSession returns true if the user holds a valid internal session cookie
func byInternalSession(w http.ResponseWriter, r *http.Request) bool {
return sessionmanager.IsValidSession(w, r)
}
// IsCorrectUsernameAndPassword checks if a provided username and password is correct
func IsCorrectUsernameAndPassword(username, password string) bool {
settings := configuration.GetServerSettingsReadOnly()
configuration.ReleaseReadOnly()
return strings.ToLower(username) == strings.ToLower(settings.AdminName) && configuration.HashPassword(password, false) == settings.AdminPassword
}

View File

@@ -45,7 +45,7 @@
<nav class="nav nav-masthead justify-content-center">
<a class="nav-link {{ if .IsMainView }}active{{ end }}" href="./admin">Upload</a>
<a class="nav-link {{ if not .IsMainView }}active{{ end }}" href="./apiKeys">API</a>
{{ if not .IsLoginDisabled }}<a class="nav-link" href="./logout">Logout</a>{{ end }}
{{ if .IsLogoutAvailable }}<a class="nav-link" href="./logout">Logout</a>{{ end }}
</nav>
</div>
</header>