added jwt authentication

This commit is contained in:
George Shaw 2017-11-03 17:18:26 -05:00
parent c55a3127c1
commit 3461ffaec2
12 changed files with 316 additions and 247 deletions

View file

@ -1,99 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Private Test</title>
<link rel="icon" type="image/x-icon" href="https://play.golang.org/favicon.ico">
</head>
<body>
<form class="api_form" method="POST" action="user_auth">
<table>
<thead>
<tr>
<th colspan="2">gPanel Login</th>
</tr>
</thead>
<tbody>
<tr>
<td>Username:</td>
<td><input type="text" placeholder="username..." name="user"></td>
</tr>
<tr>
<td>Password:</td>
<td><input type="password" placeholder="password..." name="pass"></td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="Login"></td>
</tr>
</tbody>
</table>
</form>
<form class="api_form" method="POST" action="user_register">
<table>
<thead>
<tr>
<th colspan="2">gPanel Registration</th>
</tr>
</thead>
<tbody>
<tr>
<td>Username:</td>
<td><input type="text" placeholder="username..." name="user"></td>
</tr>
<tr>
<td>Password:</td>
<td><input type="password" placeholder="password..." name="pass"></td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="Register"></td>
</tr>
</tbody>
</table>
</form>
<form class="api_form" method="POST" action="user_logout">
<table>
<thead>
<tr>
<th colspan="2">gPanel Logout</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2"><input type="submit" value="Logout"></td>
</tr>
</tbody>
</table>
</form>
<script type="text/javascript">
var form = document.getElementsByClassName('api_form');
for(var i = 0; i < form.length; i++) {
form[i].onsubmit = function(e) {
e.preventDefault();
var formData = {};
for(var y = 0, yy = this.length; y < yy; y++) {
var input = this[y];
if(input.name) {
formData[input.name] = input.value;
}
}
console.log(formData); //debugging
console.log(this.action);
var xhr = new XMLHttpRequest();
xhr.open(this.method, this.action, true);
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
xhr.send(JSON.stringify(formData));
xhr.onloadend = function() {
console.log(xhr.response)
console.log(xhr.status);
}
}
}
</script>
</body>
</html>

View file

@ -39,6 +39,9 @@
<button type="button" class="btn btn-outline-primary">Forced Shutdown</button>
<button type="button" class="btn btn-outline-primary">Restart</button>
</div>
<div class="card-footer text-muted text-center">
Server Status: <span class="text-success">ON</span>
</div>
</div>
</div>
</div>

View file

@ -4,6 +4,8 @@ package api
import (
"net/http"
"strings"
"github.com/Ennovar/gPanel/pkg/api/user"
)
// HandleAPI function takes a path and determines if it is an API call, if it is it will
@ -15,11 +17,11 @@ func HandleAPI(path string, res http.ResponseWriter, req *http.Request) (bool, b
switch suspectApi {
case "user_auth":
return true, UserAuthentication(res, req)
return true, user.UserAuthentication(res, req)
case "user_register":
return true, UserRegistration(res, req)
return true, user.UserRegistration(res, req)
case "user_logout":
return true, UserLogout(res, req)
return true, user.UserLogout(res, req)
default:
return false, false
}

View file

@ -1,139 +0,0 @@
// Package api handles all API calls
package api
import (
"encoding/json"
"net/http"
"github.com/Ennovar/gPanel/pkg/database"
"github.com/Ennovar/gPanel/pkg/encryption"
"github.com/Ennovar/gPanel/pkg/networking"
)
// userRequestData struct is the structure of the JSON data to be
// retrieved from the authentication API request
var userRequestData struct {
User string `json:"user"`
Pass string `json:"pass"`
}
// userDatabaseData struct is the structure of the JSON data to be retrieved from
// the bolt database inside of the user bucket, using the username as the key.
var userDatabaseData struct {
Pass string `json:"pass"`
}
// UserAuthentication function is accessed by an API call from the webhost root
// by accessing /user_auth and sending it a post request with userRequestData
// struct in JSON format.
func UserAuthentication(res http.ResponseWriter, req *http.Request) bool {
if req.Method != "POST" {
http.Error(res, req.Method+" HTTP method is unsupported for this API.", http.StatusMethodNotAllowed)
return false
}
err := json.NewDecoder(req.Body).Decode(&userRequestData)
if err != nil {
http.Error(res, err.Error(), http.StatusBadRequest)
return false
}
ds, err := database.Open(database.DBLOC_MAIN)
if err != nil || ds == nil {
http.Error(res, err.Error(), http.StatusBadRequest)
return false
}
defer ds.Close()
err = ds.Get(database.BUCKET_USERS, []byte(userRequestData.User), &userDatabaseData)
if err == database.ErrKeyNotExist {
http.Error(res, "User does not exist.", http.StatusUnauthorized)
return false
}
err = encryption.CheckPassword([]byte(userDatabaseData.Pass), []byte(userRequestData.Pass))
if err != nil {
http.Error(res, err.Error(), http.StatusUnauthorized)
return false
}
store := networking.GetStore(networking.COOKIES_USER_AUTH)
err = store.Set(res, req, "auth", true, (60 * 60 * 24))
if err != nil {
http.Error(res, http.StatusText(500), http.StatusInternalServerError)
return false
}
res.WriteHeader(http.StatusNoContent)
return true
}
// UserRegistration function is accessed by an API call from the webhost root
// by accessing /user_register and sending it a post request with userRequestData
// struct in JSON format.
func UserRegistration(res http.ResponseWriter, req *http.Request) bool {
if req.Method != "POST" {
http.Error(res, req.Method+" HTTP method is unsupported for this API.", http.StatusMethodNotAllowed)
return false
}
err := json.NewDecoder(req.Body).Decode(&userRequestData)
if err != nil {
http.Error(res, err.Error(), http.StatusBadRequest)
return false
} else if len(userRequestData.User) == 0 || len(userRequestData.Pass) == 0 {
http.Error(res, "Username or password field cannot be blank", http.StatusBadRequest)
return false
}
ds, err := database.Open(database.DBLOC_MAIN)
if err != nil || ds == nil {
http.Error(res, err.Error(), http.StatusBadRequest)
return false
}
defer ds.Close()
err = ds.Get(database.BUCKET_USERS, []byte(userRequestData.User), &userDatabaseData)
if err != database.ErrKeyNotExist {
http.Error(res, "Username already exists in the database", http.StatusBadRequest)
return false
}
userDatabaseData.Pass, err = encryption.HashPassword(userRequestData.Pass)
if err != nil {
http.Error(res, err.Error(), http.StatusBadRequest)
return false
}
err = ds.Put(database.BUCKET_USERS, []byte(userRequestData.User), userDatabaseData)
if err != nil {
http.Error(res, err.Error(), http.StatusBadRequest)
return false
}
res.WriteHeader(http.StatusNoContent)
return true
}
// UserRegistration function is accessed by an API call from the webhost root
// by accessing /user_logout and sending it an empty POST request. This function will
// delete the user-auth cookie session store
func UserLogout(res http.ResponseWriter, req *http.Request) bool {
if req.Method != "POST" {
http.Error(res, req.Method+" HTTP method is unsupported for this API.", http.StatusMethodNotAllowed)
return false
}
store := networking.GetStore(networking.COOKIES_USER_AUTH)
err := store.Delete(res, req)
if err != nil {
http.Error(res, http.StatusText(500), http.StatusInternalServerError)
return false
}
res.WriteHeader(http.StatusNoContent)
return true
}

16
pkg/api/user/user.go Normal file
View file

@ -0,0 +1,16 @@
// Package user is a child of package api to handle api calls concerning users
package user
// userRequestData struct is the structure of the JSON data to be
// retrieved from the authentication API request
var userRequestData struct {
User string `json:"user"`
Pass string `json:"pass"`
}
// userDatabaseData struct is the structure of the JSON data to be retrieved from
// the bolt database inside of the user bucket, using the username as the key.
var userDatabaseData struct {
Pass string `json:"pass"`
Secret string `json:"secret"`
}

87
pkg/api/user/user_auth.go Normal file
View file

@ -0,0 +1,87 @@
// Package user is a child of package api to handle api calls concerning users
package user
import (
"encoding/json"
"net/http"
"time"
"github.com/Ennovar/gPanel/pkg/database"
"github.com/Ennovar/gPanel/pkg/encryption"
"github.com/Ennovar/gPanel/pkg/networking"
jwt "github.com/dgrijalva/jwt-go"
)
// UserAuthentication function is accessed by an API call from the webhost root
// by accessing /user_auth and sending it a post request with userRequestData
// struct in JSON format.
func UserAuthentication(res http.ResponseWriter, req *http.Request) bool {
if req.Method != "POST" {
http.Error(res, req.Method+" HTTP method is unsupported for this API.", http.StatusMethodNotAllowed)
return false
}
err := json.NewDecoder(req.Body).Decode(&userRequestData)
if err != nil {
http.Error(res, err.Error(), http.StatusBadRequest)
return false
}
ds, err := database.Open(database.DBLOC_MAIN)
if err != nil || ds == nil {
http.Error(res, err.Error(), http.StatusInternalServerError)
return false
}
defer ds.Close()
err = ds.Get(database.BUCKET_USERS, []byte(userRequestData.User), &userDatabaseData)
if err == database.ErrKeyNotExist {
http.Error(res, "User does not exist.", http.StatusUnauthorized)
return false
}
err = encryption.CheckPassword([]byte(userDatabaseData.Pass), []byte(userRequestData.Pass))
if err != nil {
http.Error(res, err.Error(), http.StatusUnauthorized)
return false
}
secret, err := encryption.RandomString()
if err != nil {
http.Error(res, err.Error(), http.StatusInternalServerError)
return false
}
userDatabaseData.Secret = secret
err = ds.Put(database.BUCKET_USERS, []byte(userRequestData.User), userDatabaseData)
if err != nil {
http.Error(res, err.Error(), http.StatusBadRequest)
return false
}
now := time.Now().UTC()
claims := jwt.StandardClaims{
Subject: userRequestData.User,
NotBefore: now.Unix(),
IssuedAt: now.Unix(),
ExpiresAt: now.Add(24 * time.Hour).Unix(),
}
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret))
if err != nil {
http.Error(res, err.Error(), http.StatusInternalServerError)
return false
}
store := networking.GetStore(networking.COOKIES_USER_AUTH)
err = store.Set(res, req, "token", token, (60 * 60 * 24))
err2 := store.Set(res, req, "user", userRequestData.User, (60 * 60 * 24))
if err != nil || err2 != nil {
http.Error(res, http.StatusText(500), http.StatusInternalServerError)
return false
}
res.WriteHeader(http.StatusNoContent)
return true
}

View file

@ -0,0 +1,29 @@
// Package user is a child of package api to handle api calls concerning users
package user
import (
"net/http"
"github.com/Ennovar/gPanel/pkg/networking"
)
// UserLogout function is accessed by an API call from the webhost root
// by accessing /user_logout and sending it an empty POST request. This function will
// delete the user-auth cookie session store
func UserLogout(res http.ResponseWriter, req *http.Request) bool {
if req.Method != "POST" {
http.Error(res, req.Method+" HTTP method is unsupported for this API.", http.StatusMethodNotAllowed)
return false
}
store := networking.GetStore(networking.COOKIES_USER_AUTH)
err := store.Delete(res, req)
if err != nil {
http.Error(res, http.StatusText(500), http.StatusInternalServerError)
return false
}
res.WriteHeader(http.StatusNoContent)
return true
}

View file

@ -0,0 +1,59 @@
// Package user is a child of package api to handle api calls concerning users
package user
import (
"encoding/json"
"net/http"
"github.com/Ennovar/gPanel/pkg/database"
"github.com/Ennovar/gPanel/pkg/encryption"
)
// UserRegistration function is accessed by an API call from the webhost root
// by accessing /user_register and sending it a post request with userRequestData
// struct in JSON format.
func UserRegistration(res http.ResponseWriter, req *http.Request) bool {
if req.Method != "POST" {
http.Error(res, req.Method+" HTTP method is unsupported for this API.", http.StatusMethodNotAllowed)
return false
}
err := json.NewDecoder(req.Body).Decode(&userRequestData)
if err != nil {
http.Error(res, err.Error(), http.StatusBadRequest)
return false
} else if len(userRequestData.User) == 0 || len(userRequestData.Pass) == 0 {
http.Error(res, "Username or password field cannot be blank", http.StatusBadRequest)
return false
}
ds, err := database.Open(database.DBLOC_MAIN)
if err != nil || ds == nil {
http.Error(res, err.Error(), http.StatusInternalServerError)
return false
}
defer ds.Close()
err = ds.Get(database.BUCKET_USERS, []byte(userRequestData.User), &userDatabaseData)
if err != database.ErrKeyNotExist {
http.Error(res, "Username already exists in the database", http.StatusBadRequest)
return false
}
userDatabaseData.Pass, err = encryption.HashPassword(userRequestData.Pass)
if err != nil {
http.Error(res, err.Error(), http.StatusInternalServerError)
return false
}
userDatabaseData.Secret = ""
err = ds.Put(database.BUCKET_USERS, []byte(userRequestData.User), userDatabaseData)
if err != nil {
http.Error(res, err.Error(), http.StatusInternalServerError)
return false
}
res.WriteHeader(http.StatusNoContent)
return true
}

View file

@ -0,0 +1,21 @@
// Package user is a child of package api to handle api calls concerning users
package user
import "github.com/Ennovar/gPanel/pkg/database"
// UserSecret is not accessible from the any client side request. It is
// only used on the server side to help verify users are who they say they
// are.
func UserSecret(user string) (string, error) {
ds, err := database.Open(database.DBLOC_MAIN)
if err != nil {
return "", err
}
err = ds.Get(database.BUCKET_USERS, []byte(user), &userDatabaseData)
if err != nil {
return "", err
}
return userDatabaseData.Secret, nil
}

View file

@ -0,0 +1,19 @@
// Encryption package has functions inside of it that utilize various encypting and hashing techniques
package encryption
import (
"crypto/rand"
"fmt"
)
func RandomString() (string, error) {
n := 5
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return "", err
}
s := fmt.Sprintf("%X", b)
return s, nil
}

View file

@ -11,19 +11,26 @@ import (
)
type PublicWeb struct {
Directory string
Directory string
MaintenanceMode int
}
// NewPublicWeb returns a new PublicWeb type.
func NewPublicWeb() PublicWeb {
return PublicWeb{
Directory: "document_roots/public/",
Directory: "document_roots/public/",
MaintenanceMode: 0,
}
}
// ServeHTTP function routes all requests for the public web server. It is used in the main
// function inside of the http.ListenAndServe() function for the public host.
func (pub *PublicWeb) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if pub.MaintenanceMode == 1 {
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("We are currently in maintenance mode, please check back later."))
}
path := req.URL.Path[1:]
if len(path) == 0 {
path = (pub.Directory + "index.html")

View file

@ -8,9 +8,11 @@ import (
"strings"
"github.com/Ennovar/gPanel/pkg/api"
"github.com/Ennovar/gPanel/pkg/api/user"
"github.com/Ennovar/gPanel/pkg/logging"
"github.com/Ennovar/gPanel/pkg/networking"
"github.com/Ennovar/gPanel/pkg/routing"
jwt "github.com/dgrijalva/jwt-go"
)
type PrivateHost struct {
@ -36,11 +38,10 @@ func reqAuth(path string) bool {
}
dismissibleFiles := []string{
"api_testing.html",
"index.html",
"user_auth",
"user_register",
"user_logut",
"user_logout",
}
for _, f := range dismissibleFiles {
if strings.HasSuffix(path, f) {
@ -64,18 +65,81 @@ func (priv *PrivateHost) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if reqAuth(path) {
store := networking.GetStore(networking.COOKIES_USER_AUTH)
session_value, err := store.Read(w, req, "auth")
session_value, err := store.Read(w, req, "user")
if err != nil {
logging.Console("DEBUG::", logging.NORMAL_LOG, "1")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if session_value == nil {
logging.Console("DEBUG::", logging.NORMAL_LOG, "2")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
if auth, ok := session_value.(bool); !ok || !auth {
username, ok := session_value.(string)
if !ok {
logging.Console("DEBUG::", logging.NORMAL_LOG, "3")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
stored_secret, err := user.UserSecret(username)
if stored_secret == "" {
logging.Console("DEBUG::", logging.NORMAL_LOG, "4")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
session_value, err = store.Read(w, req, "token")
if err != nil {
logging.Console("DEBUG::", logging.NORMAL_LOG, "5")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if session_value == nil {
logging.Console("DEBUG::", logging.NORMAL_LOG, "6")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
tokenString, ok := session_value.(string)
if !ok {
logging.Console("DEBUG::", logging.NORMAL_LOG, "7")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
// if len(tokenString) < 7 {
// logging.Console("DEBUG::", logging.NORMAL_LOG, "8")
// http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
// return
// }
// tokenString = tokenString[7:]
keyfunc := func(t *jwt.Token) (interface{}, error) {
return []byte(stored_secret), nil
}
p := jwt.Parser{
ValidMethods: []string{"HS256", "HS384", "HS512"},
}
t, err := p.ParseWithClaims(tokenString, &jwt.StandardClaims{}, keyfunc)
if err != nil {
logging.Console("DEBUG::", logging.NORMAL_LOG, username)
logging.Console("DEBUG::", logging.NORMAL_LOG, tokenString)
logging.Console("DEBUG::", logging.NORMAL_LOG, stored_secret)
logging.Console("DEBUG::", logging.NORMAL_LOG, err.Error())
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
claims := t.Claims.(*jwt.StandardClaims)
if claims.Subject != username {
logging.Console("DEBUG::", logging.NORMAL_LOG, "10")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}