16 changed files with 1268 additions and 5 deletions
@ -0,0 +1,21 @@ |
|||||||
|
services: |
||||||
|
servoauth: |
||||||
|
image: go_auth |
||||||
|
container_name: servoauth |
||||||
|
ports: |
||||||
|
- "48002:48000" |
||||||
|
environment: |
||||||
|
- "DBHOST=host.docker.internal" |
||||||
|
- "DBPORT=5432" |
||||||
|
- "DBUSER=asterisk" |
||||||
|
- "DBPASS=asterisk" |
||||||
|
- "DBNAME=asterisk" |
||||||
|
- "DBDRIVER=postgres" |
||||||
|
- "DBPOOLSIZE=100" |
||||||
|
extra_hosts: |
||||||
|
- "host.docker.internal:host-gateway" |
||||||
|
networks: |
||||||
|
- dbcon |
||||||
|
networks: |
||||||
|
dbcon: |
||||||
|
external: true |
@ -0,0 +1,13 @@ |
|||||||
|
FROM golang:alpine |
||||||
|
|
||||||
|
RUN apk update && apk add --no-cache git |
||||||
|
|
||||||
|
WORKDIR /app |
||||||
|
|
||||||
|
COPY . . |
||||||
|
|
||||||
|
RUN go mod tidy |
||||||
|
|
||||||
|
RUN go build -o binary |
||||||
|
|
||||||
|
ENTRYPOINT ["/app/binary"] |
@ -0,0 +1,12 @@ |
|||||||
|
package appinfo |
||||||
|
|
||||||
|
var appName string = "MOFFAS-AUTH" |
||||||
|
var version string = "1.0.0.1" |
||||||
|
|
||||||
|
func Version() string { |
||||||
|
return version |
||||||
|
} |
||||||
|
|
||||||
|
func AppName() string { |
||||||
|
return appName |
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
package db |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"moffas_go/logger" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql" |
||||||
|
"github.com/jmoiron/sqlx" |
||||||
|
_ "github.com/lib/pq" |
||||||
|
) |
||||||
|
|
||||||
|
type DBPool struct { |
||||||
|
db *sqlx.DB |
||||||
|
count int |
||||||
|
mutex sync.Mutex |
||||||
|
poolSize int |
||||||
|
} |
||||||
|
|
||||||
|
var dbpool DBPool = DBPool{} |
||||||
|
|
||||||
|
func GetConnection() (*sqlx.DB, error) { |
||||||
|
dbpool.mutex.Lock() |
||||||
|
defer dbpool.mutex.Unlock() |
||||||
|
if dbpool.count >= dbpool.poolSize { |
||||||
|
dbpool.count = dbpool.poolSize |
||||||
|
return nil, errors.New("no connection available in pool") |
||||||
|
} else { |
||||||
|
dbpool.count++ |
||||||
|
return dbpool.db, nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func ReleaseConnection() { |
||||||
|
dbpool.mutex.Lock() |
||||||
|
defer dbpool.mutex.Unlock() |
||||||
|
if dbpool.count > 0 { |
||||||
|
dbpool.count-- |
||||||
|
} else { |
||||||
|
dbpool.count = 0 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func InitDB(driver string, host string, port int, user string, password string, dbname string, poolSize int) error { |
||||||
|
var err error |
||||||
|
connStr := fmt.Sprintf("%s://%s:%s@%s:%d/%s", driver, user, password, host, port, dbname) |
||||||
|
logger.Debug("DB", "CONNSTR : ", connStr) |
||||||
|
dbpool.db, err = sqlx.Connect(driver, connStr) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} else { |
||||||
|
dbpool.db.SetMaxOpenConns(poolSize) |
||||||
|
dbpool.db.SetMaxIdleConns(poolSize) |
||||||
|
dbpool.db.SetConnMaxLifetime(5 * time.Minute) |
||||||
|
dbpool.poolSize = poolSize |
||||||
|
dbpool.count = 0 |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
module moffas_go |
||||||
|
|
||||||
|
go 1.23.1 |
||||||
|
|
||||||
|
require ( |
||||||
|
github.com/go-sql-driver/mysql v1.8.1 |
||||||
|
github.com/jmoiron/sqlx v1.4.0 |
||||||
|
github.com/lib/pq v1.10.9 |
||||||
|
golang.org/x/crypto v0.27.0 |
||||||
|
) |
||||||
|
|
||||||
|
require filippo.io/edwards25519 v1.1.0 // indirect |
@ -0,0 +1,12 @@ |
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= |
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= |
||||||
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= |
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= |
||||||
|
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= |
||||||
|
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= |
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= |
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= |
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= |
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= |
||||||
|
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= |
||||||
|
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= |
@ -0,0 +1,245 @@ |
|||||||
|
package handlers |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"moffas_go/helper" |
||||||
|
"moffas_go/logger" |
||||||
|
"moffas_go/moffas" |
||||||
|
"net/http" |
||||||
|
) |
||||||
|
|
||||||
|
// ====== HANDLER FOR /auth ======
|
||||||
|
func Auth(w http.ResponseWriter, r *http.Request) { |
||||||
|
var err error |
||||||
|
var res string |
||||||
|
|
||||||
|
var ctxkey HTTPContextKey = "requsetID" |
||||||
|
reference_id := r.Context().Value(ctxkey).(string) |
||||||
|
defer func() { |
||||||
|
logger.Info(reference_id, "Response body: ", res) |
||||||
|
}() |
||||||
|
|
||||||
|
// ---- SET THE RESPONSE TYPE TO application/json ----
|
||||||
|
w.Header().Set("Content-Type", "application/json") |
||||||
|
response := map[string]any{ |
||||||
|
"error_code": "000000000", |
||||||
|
"error_message": "", |
||||||
|
} |
||||||
|
// ---- CHECK REQUEST METHOD ----
|
||||||
|
if r.Method == http.MethodPost { |
||||||
|
// ---- VERIFY THE CONTENT IS JSON ----
|
||||||
|
if contentType := r.Header.Get("Content-Type"); contentType != "application/json" { |
||||||
|
// NOT JSON
|
||||||
|
logger.Error(reference_id, "!!! INVALID CONTENT-TYPE: ", contentType) |
||||||
|
response["error_code"] = "400000001" |
||||||
|
response["error_message"] = "Bad Request. Not JSON" |
||||||
|
|
||||||
|
res, _ = helper.JSONencode(response) |
||||||
|
http.Error(w, res, http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// ---- READ THE POST BODY ----
|
||||||
|
var body []byte |
||||||
|
body, err = io.ReadAll(r.Body) |
||||||
|
if err != nil { |
||||||
|
// ---- FAILED TO READ BODY ----
|
||||||
|
logger.Error(reference_id, err) |
||||||
|
response["error_code"] = "400000002" |
||||||
|
response["error_message"] = "Bad Request. Can't read POST body" |
||||||
|
|
||||||
|
res, _ = helper.JSONencode(response) |
||||||
|
http.Error(w, res, http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
bodyString := string(body) |
||||||
|
logger.Info(reference_id, "-- POST BODY: ", bodyString) |
||||||
|
|
||||||
|
// ---- EXTRACT DATA FROM JSON BODY ----
|
||||||
|
type Request struct { |
||||||
|
Half_nonce string `json:"half_nonce"` |
||||||
|
Username string `json:"username"` |
||||||
|
} |
||||||
|
var req Request |
||||||
|
err = json.Unmarshal(body, &req) |
||||||
|
if err != nil { |
||||||
|
// ---- FAILED TO DECODE JSON ----
|
||||||
|
logger.Error(reference_id, err) |
||||||
|
response["error_code"] = "400000003" |
||||||
|
response["error_message"] = "Bad Request. Invalid JSON" |
||||||
|
|
||||||
|
res, _ = helper.JSONencode(response) |
||||||
|
http.Error(w, res, http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// ---- CHECK THE MANDATORY FIELDS ----
|
||||||
|
if len(req.Half_nonce) != 8 || req.Username == "" { |
||||||
|
// ---- MISSING OR INVALID MANDATORY FIELD ----
|
||||||
|
logger.Error(reference_id, "!!! MANDATORY FIELD NOT FOUND OR INVALID") |
||||||
|
response["error_code"] = "400000003" |
||||||
|
response["error_message"] = "Bad Request. Missing/Invalid Mandatory Fields" |
||||||
|
res, _ = helper.JSONencode(response) |
||||||
|
http.Error(w, res, http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// ---- GENERATE CHALLENGE ----
|
||||||
|
challenge, err := moffas.Generate_challenge(reference_id, req.Username, req.Half_nonce) |
||||||
|
if err != nil { |
||||||
|
// ---- ERROR QUERYING USER DATA ----
|
||||||
|
logger.Error(reference_id, "!!! ERROR GENERATING CHALLENGE") |
||||||
|
logger.Error(reference_id, err) |
||||||
|
challenge.Salt, _ = helper.GenerateRandomString(16) |
||||||
|
challenge.Iterations = 10000 |
||||||
|
} |
||||||
|
|
||||||
|
// ---- PREPARE RESPONSE BODY ----
|
||||||
|
response["full_nonce"] = challenge.Full_nonce |
||||||
|
response["salt"] = challenge.Salt |
||||||
|
response["iterations"] = challenge.Iterations |
||||||
|
|
||||||
|
res, _ = helper.JSONencode(response) |
||||||
|
fmt.Fprint(w, res) |
||||||
|
return |
||||||
|
|
||||||
|
} else { |
||||||
|
// ---- METHOD NOT ALLOWED (not POST) ----
|
||||||
|
response["error_code"] = "405000001" |
||||||
|
response["error_message"] = "Method Not Allowed" |
||||||
|
|
||||||
|
res, _ = helper.JSONencode(response) |
||||||
|
http.Error(w, res, http.StatusMethodNotAllowed) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// ====== HANDLER FOR /verify ======
|
||||||
|
func Verify(w http.ResponseWriter, r *http.Request) { |
||||||
|
var err error |
||||||
|
var res string |
||||||
|
|
||||||
|
var ctxkey HTTPContextKey = "requsetID" |
||||||
|
reference_id := r.Context().Value(ctxkey).(string) |
||||||
|
defer func() { |
||||||
|
logger.Info(reference_id, "Response body: ", res) |
||||||
|
}() |
||||||
|
|
||||||
|
// ---- SET THE RESPONSE TYPE TO application/json ----
|
||||||
|
w.Header().Set("Content-Type", "application/json") |
||||||
|
response := map[string]any{ |
||||||
|
"error_code": "000000000", |
||||||
|
"error_message": "", |
||||||
|
} |
||||||
|
// ---- CHECK REQUEST METHOD ----
|
||||||
|
if r.Method == http.MethodPost { |
||||||
|
// ---- VERIFY THE CONTENT IS JSON ----
|
||||||
|
if contentType := r.Header.Get("Content-Type"); contentType != "application/json" { |
||||||
|
// NOT JSON
|
||||||
|
logger.Error(reference_id, "!!! INVALID CONTENT-TYPE: ", contentType) |
||||||
|
response["error_code"] = "400000001" |
||||||
|
response["error_message"] = "Bad Request. Not JSON" |
||||||
|
|
||||||
|
res, _ = helper.JSONencode(response) |
||||||
|
http.Error(w, res, http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// ---- READ THE POST BODY ----
|
||||||
|
var body []byte |
||||||
|
body, err = io.ReadAll(r.Body) |
||||||
|
if err != nil { |
||||||
|
// FAILED TO READ BODY
|
||||||
|
logger.Error(reference_id, err) |
||||||
|
response["error_code"] = "400000002" |
||||||
|
response["error_message"] = "Bad Request. Can't read POST body" |
||||||
|
|
||||||
|
res, _ = helper.JSONencode(response) |
||||||
|
http.Error(w, res, http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
bodyString := string(body) |
||||||
|
logger.Info(reference_id, "-- POST BODY: ", bodyString) |
||||||
|
|
||||||
|
// ---- EXTRACT DATA FROM JSON BODY ----
|
||||||
|
type Request struct { |
||||||
|
Full_nonce string `json:"full_nonce"` |
||||||
|
Client_hash string `json:"client_hash"` |
||||||
|
Next_nonce string `json:"next_nonce"` |
||||||
|
} |
||||||
|
var req Request |
||||||
|
err = json.Unmarshal(body, &req) |
||||||
|
if err != nil { |
||||||
|
// ---- FAILED TO DECODE JSON ----
|
||||||
|
logger.Error(reference_id, "!!! FAILED TO DECODE JSON") |
||||||
|
logger.Error(reference_id, err) |
||||||
|
response["error_code"] = "400000003" |
||||||
|
response["error_message"] = "Bad Request. Invalid JSON" |
||||||
|
|
||||||
|
res, _ = helper.JSONencode(response) |
||||||
|
http.Error(w, res, http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// ---- CHECK MANDATORY FIELDS ----
|
||||||
|
if len(req.Full_nonce) != 16 || req.Client_hash == "" || len(req.Next_nonce) != 8 { |
||||||
|
// MISSING MANDATORY FIELD
|
||||||
|
logger.Error(reference_id, "!!! MISSING OR INVALID MANDATORY FIELD") |
||||||
|
logger.Error(reference_id, "len(full_nonce) : ", len(req.Full_nonce)) |
||||||
|
logger.Error(reference_id, "client_hash : ", req.Client_hash) |
||||||
|
logger.Error(reference_id, "len(next_nonce) : ", len(req.Next_nonce)) |
||||||
|
response["error_code"] = "400000003" |
||||||
|
response["error_message"] = "Bad Request. Missing/Invalid Mandatory Fields" |
||||||
|
res, _ = helper.JSONencode(response) |
||||||
|
http.Error(w, res, http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// ---- VERIFY CHALLENGE RESPONSE ----
|
||||||
|
challenge_data, err := moffas.Verify_challenge(reference_id, req.Full_nonce, req.Client_hash, req.Next_nonce) |
||||||
|
if err != nil { |
||||||
|
// ---- ERROR QUERYING USER DATA ----
|
||||||
|
logger.Error(reference_id, "!!! FAILED TO VERIFY CHALLENGE") |
||||||
|
logger.Error(reference_id, err) |
||||||
|
response["error_code"] = "401000001" |
||||||
|
response["error_message"] = "Unauthorized" |
||||||
|
res, _ = helper.JSONencode(response) |
||||||
|
http.Error(w, res, http.StatusUnauthorized) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// ---- PREPARE RESPONSE BODY ----
|
||||||
|
response["session_id"] = challenge_data.Session_id |
||||||
|
response["final_nonce"] = challenge_data.Final_nonce |
||||||
|
response["server_hash"] = challenge_data.Server_hash |
||||||
|
|
||||||
|
data := make(map[string]interface{}) |
||||||
|
premium_voice, ok := challenge_data.Organization_data["premium_voice"] |
||||||
|
if ok { |
||||||
|
data["premium_voice"] = premium_voice |
||||||
|
} else { |
||||||
|
data["premium_voice"] = 0 |
||||||
|
} |
||||||
|
data["privileges"] = challenge_data.Privileges |
||||||
|
data["tier"] = challenge_data.Tier |
||||||
|
data["full_name"] = challenge_data.Full_name |
||||||
|
response["user_data"] = data |
||||||
|
|
||||||
|
res, _ = helper.JSONencode(response) |
||||||
|
fmt.Fprint(w, res) |
||||||
|
return |
||||||
|
|
||||||
|
} else { |
||||||
|
// ---- METHOD NOT ALLOWED (not POST) ----
|
||||||
|
response["error_code"] = "405000001" |
||||||
|
response["error_message"] = "Method Not Allowed" |
||||||
|
|
||||||
|
res, _ = helper.JSONencode(response) |
||||||
|
http.Error(w, res, http.StatusMethodNotAllowed) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,167 @@ |
|||||||
|
package handlers |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"moffas_go/helper" |
||||||
|
"moffas_go/logger" |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
type HTTPContextKey string |
||||||
|
|
||||||
|
func Greeting(w http.ResponseWriter, r *http.Request) { |
||||||
|
// GENERATE RANDOMIZED reference_id TO IDENTIFY REQUESTS UNIQUELY
|
||||||
|
var err error |
||||||
|
var res string |
||||||
|
type reqStruct struct { |
||||||
|
Name string `json:"name"` |
||||||
|
Password string `json:"password"` |
||||||
|
Salt string `json:"salt"` |
||||||
|
Iterations int `json:"iterations"` |
||||||
|
} |
||||||
|
|
||||||
|
var req reqStruct = reqStruct{} |
||||||
|
|
||||||
|
var ctxkey HTTPContextKey = "requsetID" |
||||||
|
reference_id := r.Context().Value(ctxkey).(string) |
||||||
|
|
||||||
|
defer func() { |
||||||
|
logger.Info(reference_id, "Response body: ", res) |
||||||
|
}() |
||||||
|
|
||||||
|
// SET THE RESPONSE TYPE TO application/json
|
||||||
|
w.Header().Set("Content-Type", "application/json") |
||||||
|
response := map[string]any{ |
||||||
|
"error_code": "000000000", |
||||||
|
"error_message": "", |
||||||
|
} |
||||||
|
|
||||||
|
// GET THE REQUEST QUERY PARAMS AND THE RAW QUERY STRING
|
||||||
|
var queryString string = string(r.URL.RawQuery) |
||||||
|
var query url.Values = r.URL.Query() |
||||||
|
if r.Method == http.MethodGet { |
||||||
|
// GET NAME FROM QUERY PARAMS
|
||||||
|
names := query["name"] |
||||||
|
req.Name = strings.Join(names, "") |
||||||
|
|
||||||
|
// PREPARE THE RESPONSE
|
||||||
|
if req.Name == "" { |
||||||
|
response["message"] = "Hello!" |
||||||
|
} else { |
||||||
|
response["message"] = "Hello, " + req.Name + "!" |
||||||
|
} |
||||||
|
response["reference_id"] = reference_id |
||||||
|
response["query_string"] = queryString |
||||||
|
req.Password = query["password"][0] |
||||||
|
req.Salt = query["salt"][0] |
||||||
|
req.Iterations, _ = strconv.Atoi(query["iterations"][0]) |
||||||
|
|
||||||
|
if req.Password == "" { |
||||||
|
req.Password = "rahasia" |
||||||
|
} |
||||||
|
response["password"] = req.Password |
||||||
|
|
||||||
|
if req.Salt == "" { |
||||||
|
req.Salt, _ = helper.GenerateRandomString(16) |
||||||
|
} |
||||||
|
response["salt"] = req.Salt |
||||||
|
|
||||||
|
if req.Iterations <= 0 { |
||||||
|
req.Iterations = 10000 |
||||||
|
} |
||||||
|
response["iterations"] = req.Iterations |
||||||
|
response["salted_password"] = helper.PBKDF2_SHA256(req.Password, req.Salt, req.Iterations) |
||||||
|
|
||||||
|
// SEND THE RESPONSE AS JSON
|
||||||
|
res, _ = helper.JSONencode(response) |
||||||
|
fmt.Fprint(w, res) |
||||||
|
return |
||||||
|
|
||||||
|
} else if r.Method == http.MethodPost { |
||||||
|
// THE RECEIVED REQUEST METHOD IS POST
|
||||||
|
logger.Info(reference_id, "Request Method: POST") |
||||||
|
|
||||||
|
// VERIFY THE CONTENT IS JSON
|
||||||
|
if contentType := r.Header.Get("Content-Type"); contentType != "application/json" { |
||||||
|
// NOT JSON
|
||||||
|
logger.Error(reference_id, "Invalid content-type: ", contentType) |
||||||
|
response["error_code"] = "400000001" |
||||||
|
response["error_message"] = "Bad Request. Not JSON" |
||||||
|
res, _ = helper.JSONencode(response) |
||||||
|
http.Error(w, res, http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// READ THE POST BODY
|
||||||
|
var body []byte |
||||||
|
body, err = io.ReadAll(r.Body) |
||||||
|
if err != nil { |
||||||
|
// FAILED TO READ BODY
|
||||||
|
logger.Error(reference_id, err) |
||||||
|
response["error_code"] = "400000002" |
||||||
|
response["error_message"] = "Bad Request. Can't read POST body" |
||||||
|
// logger.E("ERROR : ", err)
|
||||||
|
res, _ = helper.JSONencode(response) |
||||||
|
http.Error(w, res, http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
bodyString := string(body) |
||||||
|
logger.Info(reference_id, "POST BODY: ", bodyString) |
||||||
|
|
||||||
|
// EXTRACT DATA FROM JSON BODY
|
||||||
|
err = json.Unmarshal(body, &req) |
||||||
|
if err != nil { |
||||||
|
// FAILED TO DECODE JSON
|
||||||
|
logger.Error(reference_id, err) |
||||||
|
response["error_code"] = "400000003" |
||||||
|
response["error_message"] = "Bad Request. Invalid JSON" |
||||||
|
// logger.E("ERROR : ", err)
|
||||||
|
res, _ = helper.JSONencode(response) |
||||||
|
http.Error(w, res, http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if req.Password == "" { |
||||||
|
req.Password = "rahasia" |
||||||
|
} |
||||||
|
response["password"] = req.Password |
||||||
|
|
||||||
|
if req.Salt == "" { |
||||||
|
req.Salt, _ = helper.GenerateRandomString(16) |
||||||
|
} |
||||||
|
response["salt"] = req.Salt |
||||||
|
|
||||||
|
if req.Iterations <= 0 { |
||||||
|
req.Iterations = 10000 |
||||||
|
} |
||||||
|
response["iterations"] = req.Iterations |
||||||
|
response["salted_password"] = helper.PBKDF2_SHA256(req.Password, req.Salt, req.Iterations) |
||||||
|
|
||||||
|
// PREPARE THE RESPONSE
|
||||||
|
if req.Name == "" { |
||||||
|
response["message"] = "Hello!" |
||||||
|
} else { |
||||||
|
response["message"] = "Hello, " + req.Name + "!" |
||||||
|
} |
||||||
|
response["reference_id"] = reference_id |
||||||
|
response["query_string"] = queryString |
||||||
|
|
||||||
|
// SEND THE RESPONSE AS JSON
|
||||||
|
res, _ = helper.JSONencode(response) |
||||||
|
fmt.Fprint(w, res) |
||||||
|
return |
||||||
|
|
||||||
|
} else { |
||||||
|
// METHOD NOT ALLOWED (not GET or POST)
|
||||||
|
response["error_code"] = "405000001" |
||||||
|
response["error_message"] = "Method Not Allowed" |
||||||
|
res, _ = helper.JSONencode(response) |
||||||
|
http.Error(w, res, http.StatusMethodNotAllowed) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
package handlers |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
) |
||||||
|
|
||||||
|
func Hello(w http.ResponseWriter, r *http.Request) { |
||||||
|
res := "Hello World!" |
||||||
|
fmt.Fprint(w, res) |
||||||
|
|
||||||
|
} |
@ -0,0 +1,70 @@ |
|||||||
|
package helper |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/hmac" |
||||||
|
"crypto/rand" |
||||||
|
"crypto/sha256" |
||||||
|
"encoding/hex" |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"golang.org/x/crypto/pbkdf2" |
||||||
|
) |
||||||
|
|
||||||
|
func GenerateRandomString(length int) (string, error) { |
||||||
|
// Character set untuk random string
|
||||||
|
charset := "0123456789abcdefghijklmnopqrstuvwxyz" |
||||||
|
|
||||||
|
// Membuat slice byte untuk menampung hasil random string
|
||||||
|
result := make([]byte, length) |
||||||
|
|
||||||
|
// Menggunakan crypto/rand untuk mengisi slice dengan data acak
|
||||||
|
for i := range result { |
||||||
|
// Membuat byte acak untuk digunakan sebagai indeks
|
||||||
|
randomByte := make([]byte, 1) |
||||||
|
_, err := rand.Read(randomByte) |
||||||
|
if err != nil { |
||||||
|
fmt.Println("Error generating random string:", err) |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
// Menentukan indeks dalam charset menggunakan byte acak
|
||||||
|
index := int(randomByte[0]) % len(charset) |
||||||
|
result[i] = charset[index] |
||||||
|
} |
||||||
|
|
||||||
|
return string(result), nil |
||||||
|
} |
||||||
|
|
||||||
|
func SHA256(data string) string { |
||||||
|
shaHash := sha256.Sum256([]byte(data)) |
||||||
|
shaHashStr := hex.EncodeToString(shaHash[:]) |
||||||
|
return shaHashStr |
||||||
|
} |
||||||
|
|
||||||
|
func HMAC_SHA256(inputData string, hexKey string) (string, error) { |
||||||
|
key, err := hex.DecodeString(hexKey) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
// Membuat HMAC-SHA256 menggunakan key yang didecode
|
||||||
|
hmacHash := hmac.New(sha256.New, key) |
||||||
|
_, err = hmacHash.Write([]byte(inputData)) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
// Menghasilkan hash dalam bentuk hexadecimal string
|
||||||
|
hmacHashStr := hex.EncodeToString(hmacHash.Sum(nil)) |
||||||
|
return hmacHashStr, nil |
||||||
|
} |
||||||
|
|
||||||
|
func PBKDF2_SHA256(password string, salt string, iterations int) string { |
||||||
|
// Menghasilkan key 256-bit (32 bytes)
|
||||||
|
bpassword := []byte(password) |
||||||
|
bsalt := []byte(salt) |
||||||
|
key := pbkdf2.Key(bpassword, bsalt, iterations, 32, sha256.New) |
||||||
|
|
||||||
|
// Mengonversi hasil key ke hexadecimal string
|
||||||
|
return hex.EncodeToString(key) |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
package helper |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
) |
||||||
|
|
||||||
|
func JSONencode(data any) (string, error) { |
||||||
|
var buffer bytes.Buffer |
||||||
|
|
||||||
|
// Buat encoder yang menulis ke buffer
|
||||||
|
encoder := json.NewEncoder(&buffer) |
||||||
|
|
||||||
|
// Set agar encoder tidak melakukan escaping HTML
|
||||||
|
encoder.SetEscapeHTML(false) |
||||||
|
|
||||||
|
// Encode data ke JSON dan simpan ke buffer
|
||||||
|
if err := encoder.Encode(data); err != nil { |
||||||
|
fmt.Println("Error encoding JSON:", err) |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
// Mendapatkan hasil JSON sebagai string
|
||||||
|
jsonString := buffer.String() |
||||||
|
return jsonString, nil |
||||||
|
} |
||||||
|
|
||||||
|
func GetEnv(key, fallback string) string { |
||||||
|
value := os.Getenv(key) |
||||||
|
if len(value) == 0 { |
||||||
|
return fallback |
||||||
|
} |
||||||
|
return value |
||||||
|
} |
@ -0,0 +1,102 @@ |
|||||||
|
package logger |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"moffas_go/appinfo" |
||||||
|
"runtime" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
CRITICAL = iota |
||||||
|
ERROR |
||||||
|
WARNING |
||||||
|
EVENT |
||||||
|
INFO |
||||||
|
DEBUG |
||||||
|
) |
||||||
|
|
||||||
|
var debugLevel int = ERROR |
||||||
|
|
||||||
|
func SetDebugLevel(level int) { |
||||||
|
debugLevel = level |
||||||
|
} |
||||||
|
|
||||||
|
func getLogPrefix() string { |
||||||
|
timestr := time.Now().Format(time.RFC3339Nano) |
||||||
|
// 2024-09-22T15:54:51.665494100
|
||||||
|
timestrs := strings.Split(timestr, "+") |
||||||
|
tstr := (timestrs[0] + "000000000") |
||||||
|
timestrs[0] = tstr[:29] |
||||||
|
timestr = strings.Join(timestrs, "+") |
||||||
|
|
||||||
|
version := appinfo.Version() |
||||||
|
|
||||||
|
// Mengambil informasi tentang caller satu level di atas (yaitu fungsi main dalam hal ini)
|
||||||
|
funcName := "" |
||||||
|
pc, _, line, ok := runtime.Caller(2) |
||||||
|
if !ok { |
||||||
|
fmt.Println("No caller information") |
||||||
|
} else { |
||||||
|
function := runtime.FuncForPC(pc) |
||||||
|
funcName = function.Name() |
||||||
|
} |
||||||
|
funcString := "" |
||||||
|
if debugLevel == DEBUG { |
||||||
|
funcString = funcName + ":" + strconv.Itoa(line) + " - " |
||||||
|
} |
||||||
|
|
||||||
|
// Mendapatkan nama fungsi dari program counter (pc)
|
||||||
|
return timestr + " - " + appinfo.AppName() + " - VERSION:" + version + " - " + funcString |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
func Debug(id string, v ...any) { |
||||||
|
if debugLevel >= DEBUG { |
||||||
|
prefix := getLogPrefix() + id + " - DEBUG - " |
||||||
|
msgs := fmt.Sprint(v...) |
||||||
|
fmt.Println(prefix + msgs) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func Info(id string, v ...any) { |
||||||
|
if debugLevel >= INFO { |
||||||
|
prefix := getLogPrefix() + id + " - INFO - " |
||||||
|
msgs := fmt.Sprint(v...) |
||||||
|
fmt.Println(prefix + msgs) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func Event(id string, v ...any) { |
||||||
|
if debugLevel >= EVENT { |
||||||
|
prefix := getLogPrefix() + id + " - EVENT - " |
||||||
|
msgs := fmt.Sprint(v...) |
||||||
|
fmt.Println(prefix + msgs) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func Warning(id string, v ...any) { |
||||||
|
if debugLevel >= WARNING { |
||||||
|
prefix := getLogPrefix() + id + " - WARNING - " |
||||||
|
msgs := fmt.Sprint(v...) |
||||||
|
fmt.Println(prefix + msgs) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func Error(id string, v ...any) { |
||||||
|
if debugLevel >= ERROR { |
||||||
|
prefix := getLogPrefix() + id + " - ERROR - " |
||||||
|
msgs := fmt.Sprint(v...) |
||||||
|
fmt.Println(prefix + msgs) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func Critical(id string, v ...any) { |
||||||
|
if debugLevel >= CRITICAL { |
||||||
|
prefix := getLogPrefix() + id + " - CRITICAL - " |
||||||
|
msgs := fmt.Sprint(v...) |
||||||
|
fmt.Println(prefix + msgs) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,129 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"moffas_go/db" |
||||||
|
"moffas_go/handlers" |
||||||
|
"moffas_go/helper" |
||||||
|
"moffas_go/logger" |
||||||
|
"net/http" |
||||||
|
"os" |
||||||
|
"strconv" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
func generate_reference_id(timer int64) string { |
||||||
|
timeBase36 := strconv.FormatUint(uint64(timer), 36) |
||||||
|
nonce8, _ := helper.GenerateRandomString(8) |
||||||
|
reference_id := timeBase36 + "." + nonce8 |
||||||
|
return reference_id |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// Middleware untuk menangani CORS
|
||||||
|
func corsMiddleware(next http.Handler) http.Handler { |
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*") // Mengizinkan semua asal
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") // Metode yang diizinkan
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type") // Header yang diizinkan
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" { |
||||||
|
// Jika request adalah OPTIONS (preflight), kembalikan status 204 tanpa body
|
||||||
|
w.WriteHeader(http.StatusNoContent) |
||||||
|
return |
||||||
|
} |
||||||
|
// GENERATE UNIQUE REQUEST ID FOR REFERENCE
|
||||||
|
start := time.Now() |
||||||
|
request_id := generate_reference_id(start.UnixNano()) |
||||||
|
|
||||||
|
// LOG START, METHOD AND PATH
|
||||||
|
logger.Event(request_id, "--- Handle Request Started", r.Method, " ", r.URL.Path) |
||||||
|
|
||||||
|
// LOG THE REQUEST RAW QUERY STRING
|
||||||
|
logger.Info(request_id, "--- Query String: ", string(r.URL.RawQuery)) |
||||||
|
|
||||||
|
// LOG HEADERS
|
||||||
|
logger.Info(request_id, "--- Headers: ") |
||||||
|
for name, values := range r.Header { |
||||||
|
// Loop over all values for the name.
|
||||||
|
for _, value := range values { |
||||||
|
logger.Info(request_id, "[", name, "] : ", value) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// CALL THE REAL HANDLER
|
||||||
|
var ctxkey handlers.HTTPContextKey = "requsetID" |
||||||
|
ctx := context.WithValue(r.Context(), ctxkey, request_id) |
||||||
|
next.ServeHTTP(w, r.WithContext(ctx)) |
||||||
|
|
||||||
|
// LOG COMPLETEION AND DURATION
|
||||||
|
duration := time.Since(start) |
||||||
|
logger.Event(request_id, "--- Handle Request Completed in : ", duration) |
||||||
|
|
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
// CONFIGURE LOGGER
|
||||||
|
dblv := helper.GetEnv("DEBUG_LEVEL", "INFO") |
||||||
|
|
||||||
|
if dblv == "DEBUG" { |
||||||
|
logger.SetDebugLevel(logger.DEBUG) |
||||||
|
} else if dblv == "INFO" { |
||||||
|
logger.SetDebugLevel(logger.INFO) |
||||||
|
} else if dblv == "EVENT" { |
||||||
|
logger.SetDebugLevel(logger.EVENT) |
||||||
|
} else if dblv == "WARNING" { |
||||||
|
logger.SetDebugLevel(logger.WARNING) |
||||||
|
} else if dblv == "ERROR" { |
||||||
|
logger.SetDebugLevel(logger.ERROR) |
||||||
|
} else { |
||||||
|
logger.SetDebugLevel(logger.INFO) |
||||||
|
} |
||||||
|
|
||||||
|
// CONFIGURE AND INITIALIZE DB
|
||||||
|
DBDRIVER := helper.GetEnv("DBDRIVER", "postgres") |
||||||
|
DBUSER := helper.GetEnv("DBUSER", "magang") |
||||||
|
DBPASS := helper.GetEnv("DBPASS", "magang") |
||||||
|
DBHOST := helper.GetEnv("DBHOST", "ningning.cayangqu.com") |
||||||
|
DBPORT, err := strconv.Atoi(helper.GetEnv("DBPORT", "5432")) |
||||||
|
if err != nil { |
||||||
|
DBPORT = 5432 |
||||||
|
} |
||||||
|
DBPOOLSIZE, err := strconv.Atoi(helper.GetEnv("DBPOOLSIZE", "20")) |
||||||
|
if err != nil { |
||||||
|
DBPOOLSIZE = 20 |
||||||
|
} |
||||||
|
DBNAME := helper.GetEnv("DBNAME", "magang") |
||||||
|
logger.Info("MAIN", "DBDRIVER : ", DBDRIVER) |
||||||
|
logger.Info("MAIN", "DBHOST : ", DBHOST) |
||||||
|
logger.Info("MAIN", "DBPORT : ", DBPORT) |
||||||
|
logger.Debug("MAIN", "DBUSER : ", DBUSER) |
||||||
|
logger.Debug("MAIN", "DBPASS : ", DBPASS) |
||||||
|
logger.Info("MAIN", "DBNAME : ", DBNAME) |
||||||
|
|
||||||
|
err = db.InitDB(DBDRIVER, DBHOST, DBPORT, DBUSER, DBPASS, DBNAME, DBPOOLSIZE) |
||||||
|
if err != nil { |
||||||
|
logger.Error("MAIN", "!!! FAILED TO INITIATE DB POOL..", err) |
||||||
|
os.Exit(1) |
||||||
|
} else { |
||||||
|
logger.Info("MAIN", "Database Connection Pool Initated.") |
||||||
|
} |
||||||
|
|
||||||
|
// CONFIGURE ENDPOINTS
|
||||||
|
paths := make(map[string]func(http.ResponseWriter, *http.Request)) |
||||||
|
paths["/"] = handlers.Greeting |
||||||
|
paths["/hello"] = handlers.Hello |
||||||
|
paths["/auth"] = handlers.Auth |
||||||
|
paths["/verify"] = handlers.Verify |
||||||
|
|
||||||
|
mux := http.NewServeMux() |
||||||
|
for path, handler := range paths { |
||||||
|
mux.HandleFunc(path, handler) |
||||||
|
} |
||||||
|
|
||||||
|
// START SERVER
|
||||||
|
logger.Info("MAIN", "Server started on Port :48000") |
||||||
|
http.ListenAndServe(":48000", corsMiddleware(mux)) |
||||||
|
} |
@ -0,0 +1,341 @@ |
|||||||
|
package moffas |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"errors" |
||||||
|
"moffas_go/db" |
||||||
|
"moffas_go/helper" |
||||||
|
"moffas_go/logger" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
type GeneratedChallenge struct { |
||||||
|
Full_nonce string |
||||||
|
User_id int64 |
||||||
|
Salt string |
||||||
|
Iterations int |
||||||
|
} |
||||||
|
|
||||||
|
type VerificationResult struct { |
||||||
|
Server_hash string |
||||||
|
Final_nonce string |
||||||
|
Session_id string |
||||||
|
Privileges map[string]interface{} |
||||||
|
Tier string |
||||||
|
Full_name string |
||||||
|
Organization_data map[string]interface{} |
||||||
|
} |
||||||
|
|
||||||
|
func Generate_challenge(reference_id, username string, half_nonce string) (GeneratedChallenge, error) { |
||||||
|
logger.Debug(reference_id, " -- start generate_challenge") |
||||||
|
startTime := time.Now() |
||||||
|
defer func() { |
||||||
|
dur := time.Since(startTime) |
||||||
|
logger.Debug(reference_id, " -- generate_challenge done in ", dur) |
||||||
|
}() |
||||||
|
|
||||||
|
conn, err := db.GetConnection() |
||||||
|
if err != nil { |
||||||
|
return GeneratedChallenge{}, err |
||||||
|
} |
||||||
|
defer db.ReleaseConnection() |
||||||
|
|
||||||
|
current_time := time.Now().Unix() |
||||||
|
nonce, err := helper.GenerateRandomString(8) |
||||||
|
if err != nil { |
||||||
|
return GeneratedChallenge{}, err |
||||||
|
} |
||||||
|
|
||||||
|
full_nonce := half_nonce + nonce |
||||||
|
|
||||||
|
logger.Info(reference_id, "HALF NONCE : ", half_nonce) |
||||||
|
logger.Info(reference_id, "USERNAME : ", username) |
||||||
|
logger.Info(reference_id, "FULL NONCE : ", full_nonce) |
||||||
|
|
||||||
|
type DBResult struct { |
||||||
|
User_id int64 `db:"id"` |
||||||
|
Salt string `db:"salt"` |
||||||
|
Iterations int `db:"iterations"` |
||||||
|
Timestamp int64 `db:"tstamp"` |
||||||
|
SaltedPasword string `db:"saltedpassword"` |
||||||
|
} |
||||||
|
|
||||||
|
query := ` |
||||||
|
SELECT
|
||||||
|
a.id,a.salt,a.saltedpassword,a.iterations, |
||||||
|
COALESCE(b.tstamp,0) as tstamp |
||||||
|
FROM servouser.user a |
||||||
|
LEFT JOIN servouser.challenge_response b ON b.user_id = a.id |
||||||
|
WHERE a.username = $1 AND a.st=1 LIMIT 1` |
||||||
|
|
||||||
|
sql := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(query, "\t", " "), "\n", " "), " ", " ") |
||||||
|
logger.Debug(reference_id, "SQL : ", sql) |
||||||
|
|
||||||
|
dbresult := DBResult{} |
||||||
|
err = conn.Get(&dbresult, sql, username) |
||||||
|
if err != nil { |
||||||
|
return GeneratedChallenge{}, err |
||||||
|
} |
||||||
|
|
||||||
|
logger.Info(reference_id, "USER ID : ", dbresult.User_id) |
||||||
|
waittime, err := strconv.Atoi(helper.GetEnv("AUTH_WAIT_TIME", "10")) |
||||||
|
if err != nil { |
||||||
|
waittime = 10 |
||||||
|
} |
||||||
|
wait_until := dbresult.Timestamp + int64(waittime) |
||||||
|
logger.Debug(reference_id, "LAST AUTH REQUEST : ", dbresult.Timestamp) |
||||||
|
logger.Debug(reference_id, "WAIT TIME : ", waittime) |
||||||
|
logger.Debug(reference_id, "WAIT UNTIL : ", wait_until) |
||||||
|
logger.Debug(reference_id, "CURRENT TIME : ", current_time) |
||||||
|
|
||||||
|
if wait_until > current_time { |
||||||
|
logger.Info(reference_id, "!!! WAIT TIME FOR THE USER ID HAS NOT ELAPSED") |
||||||
|
err = errors.New("wait time has not elapsed") |
||||||
|
return GeneratedChallenge{}, err |
||||||
|
} |
||||||
|
|
||||||
|
//----- CONTEKAN DOANG -----
|
||||||
|
sharedSecret := helper.GetEnv("SHARED_SECRET", "Winter is coming") |
||||||
|
logger.Info(reference_id, "SHARED SECRET : ", sharedSecret) |
||||||
|
logger.Info(reference_id, "SALTED PASSWORD : ", dbresult.SaltedPasword) |
||||||
|
|
||||||
|
hash1, err := helper.HMAC_SHA256(sharedSecret, dbresult.SaltedPasword) |
||||||
|
if err != nil { |
||||||
|
return GeneratedChallenge{}, err |
||||||
|
} |
||||||
|
logger.Info(reference_id, "HASH1 : ", hash1) |
||||||
|
|
||||||
|
calculated_client_hash, err := helper.HMAC_SHA256(full_nonce, hash1) |
||||||
|
if err != nil { |
||||||
|
return GeneratedChallenge{}, err |
||||||
|
} |
||||||
|
logger.Info(reference_id, "CALCULATED CLIENT HASH : ", calculated_client_hash) |
||||||
|
//----- CONTEKAN DOANG -----
|
||||||
|
|
||||||
|
err = upsert_challenge(reference_id, full_nonce, dbresult.User_id) |
||||||
|
if err != nil { |
||||||
|
return GeneratedChallenge{}, err |
||||||
|
} |
||||||
|
return GeneratedChallenge{ |
||||||
|
User_id: dbresult.User_id, |
||||||
|
Salt: dbresult.Salt, |
||||||
|
Iterations: dbresult.Iterations, |
||||||
|
Full_nonce: full_nonce, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
func Verify_challenge(reference_id string, full_nonce string, client_hash string, next_nonce string) (VerificationResult, error) { |
||||||
|
logger.Debug(reference_id, " start verify_challenge") |
||||||
|
startTime := time.Now() |
||||||
|
defer func() { |
||||||
|
dur := time.Since(startTime) |
||||||
|
logger.Debug(reference_id, " -- verify_challenge done in ", dur) |
||||||
|
}() |
||||||
|
|
||||||
|
conn, err := db.GetConnection() |
||||||
|
if err != nil { |
||||||
|
return VerificationResult{}, err |
||||||
|
} |
||||||
|
defer db.ReleaseConnection() |
||||||
|
|
||||||
|
current_time := time.Now().Unix() |
||||||
|
nonce, err := helper.GenerateRandomString(8) |
||||||
|
if err != nil { |
||||||
|
return VerificationResult{}, err |
||||||
|
} |
||||||
|
|
||||||
|
logger.Info(reference_id, "FULL NONCE : ", full_nonce) |
||||||
|
|
||||||
|
type DBResult struct { |
||||||
|
User_id int64 `db:"user_id"` |
||||||
|
Timestamp int64 `db:"tstamp"` |
||||||
|
Fullname string `db:"full_name"` |
||||||
|
Privileges string `db:"privileges"` |
||||||
|
Tier string `db:"tier"` |
||||||
|
SaltedPasword string `db:"saltedpassword"` |
||||||
|
Organization_data string `db:"organization_data"` |
||||||
|
} |
||||||
|
dbresult := DBResult{} |
||||||
|
|
||||||
|
query := ` |
||||||
|
SELECT
|
||||||
|
a.user_id, a.tstamp, |
||||||
|
b.full_name, b.saltedpassword, |
||||||
|
c.privileges, |
||||||
|
d.tier, d.data organization_data |
||||||
|
FROM servouser.challenge_response a |
||||||
|
LEFT JOIN servouser.user b ON b.id = a.user_id |
||||||
|
LEFT JOIN servouser.role c ON c.name = b.role |
||||||
|
LEFT JOIN servouser.organization d ON d.id = b.organization_id |
||||||
|
WHERE a.full_nonce = $1 AND b.id IS NOT NULL AND c.privileges IS NOT NULL AND b.st = 1 |
||||||
|
LIMIT 1` |
||||||
|
|
||||||
|
sql := strings.ReplaceAll(strings.ReplaceAll(query, "\t", " "), "\n", " ") |
||||||
|
logger.Debug(reference_id, "SQL : ", sql) |
||||||
|
err = conn.Get(&dbresult, sql, full_nonce) |
||||||
|
if err != nil { |
||||||
|
return VerificationResult{}, err |
||||||
|
} |
||||||
|
rep_nonce := full_nonce[0:15] |
||||||
|
logger.Debug(reference_id, "replacement nonce : ", rep_nonce) |
||||||
|
|
||||||
|
query = "UPDATE servouser.challenge_response SET full_nonce = $1 WHERE full_nonce = $2" |
||||||
|
logger.Debug(reference_id, "SQL : ", query) |
||||||
|
_, err = conn.Exec(query, rep_nonce, full_nonce) |
||||||
|
if err != nil { |
||||||
|
return VerificationResult{}, err |
||||||
|
} |
||||||
|
|
||||||
|
timeout, err := strconv.Atoi(helper.GetEnv("AUTH_TIMEOUT", "300")) |
||||||
|
if err != nil { |
||||||
|
timeout = 300 |
||||||
|
} |
||||||
|
logger.Info(reference_id, "AUTH TIMEOUT : ", timeout) |
||||||
|
|
||||||
|
expire_limit := dbresult.Timestamp + int64(timeout) |
||||||
|
logger.Info(reference_id, "AUTH EXPIRATION TIME : ", expire_limit) |
||||||
|
logger.Info(reference_id, "CURRENT TIME : ", current_time) |
||||||
|
if current_time > expire_limit { |
||||||
|
logger.Error(reference_id, "!!! CHALLENGE EXPIRED") |
||||||
|
err = errors.New("client expired") |
||||||
|
return VerificationResult{}, err |
||||||
|
} |
||||||
|
|
||||||
|
sharedSecret := helper.GetEnv("SHARED_SECRET", "Winter is coming") |
||||||
|
logger.Info(reference_id, "SHARED SECRET : ", sharedSecret) |
||||||
|
logger.Info(reference_id, "SALTED PASSWORD : ", dbresult.SaltedPasword) |
||||||
|
|
||||||
|
hash1, err := helper.HMAC_SHA256(sharedSecret, dbresult.SaltedPasword) |
||||||
|
if err != nil { |
||||||
|
return VerificationResult{}, err |
||||||
|
} |
||||||
|
logger.Info(reference_id, "HASH1 : ", hash1) |
||||||
|
|
||||||
|
calculated_client_hash, err := helper.HMAC_SHA256(full_nonce, hash1) |
||||||
|
if err != nil { |
||||||
|
return VerificationResult{}, err |
||||||
|
} |
||||||
|
logger.Info(reference_id, "CALCULATED CLIENT HASH : ", calculated_client_hash) |
||||||
|
logger.Info(reference_id, "CLIENT HASH : ", client_hash) |
||||||
|
if calculated_client_hash != client_hash { |
||||||
|
logger.Warning(reference_id, "!!! CLIENT HASH DOES NOT MATCH STORED CHALLENGE RESPONSE") |
||||||
|
err = errors.New("client hash does not match stored challenge response") |
||||||
|
return VerificationResult{}, err |
||||||
|
} |
||||||
|
|
||||||
|
logger.Info(reference_id, "NEXT NONCE : ", next_nonce) |
||||||
|
final_nonce := next_nonce + nonce |
||||||
|
logger.Info(reference_id, "FINAL NONCE : ", final_nonce) |
||||||
|
|
||||||
|
server_hash, err := helper.HMAC_SHA256(final_nonce, hash1) |
||||||
|
if err != nil { |
||||||
|
return VerificationResult{}, err |
||||||
|
} |
||||||
|
logger.Info(reference_id, "SERVER HASH : ", server_hash) |
||||||
|
|
||||||
|
session_secret, err := helper.HMAC_SHA256(full_nonce+final_nonce, hash1) |
||||||
|
if err != nil { |
||||||
|
return VerificationResult{}, err |
||||||
|
} |
||||||
|
logger.Info(reference_id, "SESSION SECRET : ", session_secret) |
||||||
|
session_id, _ := helper.GenerateRandomString(16) |
||||||
|
logger.Info(reference_id, "SESSION ID : ", session_id) |
||||||
|
|
||||||
|
err = upsert_session(reference_id, session_id, dbresult.User_id, session_secret) |
||||||
|
if err != nil { |
||||||
|
return VerificationResult{}, err |
||||||
|
} |
||||||
|
|
||||||
|
privileges := make(map[string]interface{}) |
||||||
|
organization_data := make(map[string]interface{}) |
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(dbresult.Privileges), &privileges) |
||||||
|
if err != nil { |
||||||
|
return VerificationResult{}, err |
||||||
|
} |
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(dbresult.Organization_data), &organization_data) |
||||||
|
if err != nil { |
||||||
|
return VerificationResult{}, err |
||||||
|
} |
||||||
|
|
||||||
|
return VerificationResult{ |
||||||
|
Final_nonce: final_nonce, |
||||||
|
Server_hash: server_hash, |
||||||
|
Session_id: session_id, |
||||||
|
Privileges: privileges, |
||||||
|
Tier: dbresult.Tier, |
||||||
|
Full_name: dbresult.Fullname, |
||||||
|
Organization_data: organization_data, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
func upsert_challenge(reference_id, full_nonce string, user_id int64) error { |
||||||
|
logger.Debug(reference_id, " start upsert_challenge") |
||||||
|
startTime := time.Now() |
||||||
|
defer func() { |
||||||
|
dur := time.Since(startTime) |
||||||
|
logger.Debug(reference_id, " -- upsert_challenge done in ", dur) |
||||||
|
}() |
||||||
|
|
||||||
|
conn, err := db.GetConnection() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer db.ReleaseConnection() |
||||||
|
|
||||||
|
query := "DELETE FROM servouser.challenge_response WHERE full_nonce = $1 OR user_id = $2" |
||||||
|
sql := strings.ReplaceAll(strings.ReplaceAll(query, "\t", " "), "\n", " ") |
||||||
|
logger.Debug(reference_id, "SQL : ", sql) |
||||||
|
_, err = conn.Exec(query, full_nonce, user_id) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
query = ` |
||||||
|
INSERT INTO
|
||||||
|
servouser.challenge_response (full_nonce,user_id,challenge_response,tstamp) |
||||||
|
VALUES
|
||||||
|
($1,$2,'0123456789abcdefghijklmnopqrstuv',$3)` |
||||||
|
sql = strings.ReplaceAll(strings.ReplaceAll(query, "\t", " "), "\n", " ") |
||||||
|
logger.Debug(reference_id, "SQL : ", sql) |
||||||
|
_, err = conn.Exec(sql, full_nonce, user_id, time.Now().Unix()) |
||||||
|
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
func upsert_session(reference_id, session_id string, user_id int64, session_secret string) error { |
||||||
|
logger.Debug(reference_id, " start upsert_challenge") |
||||||
|
|
||||||
|
startTime := time.Now() |
||||||
|
defer func() { |
||||||
|
dur := time.Since(startTime) |
||||||
|
logger.Debug(reference_id, " -- upsert_session done in ", dur) |
||||||
|
}() |
||||||
|
|
||||||
|
conn, err := db.GetConnection() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer db.ReleaseConnection() |
||||||
|
|
||||||
|
query := "DELETE FROM servouser.session WHERE session_id = $1 OR user_id = $2" |
||||||
|
sql := strings.ReplaceAll(strings.ReplaceAll(query, "\t", " "), "\n", " ") |
||||||
|
logger.Debug(reference_id, "SQL : ", sql) |
||||||
|
_, err = conn.Exec(query, session_id, user_id) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
query = ` |
||||||
|
INSERT INTO |
||||||
|
servouser.session (session_id,user_id,session_secret,tstamp,st,last_ms_tstamp,last_sequence) |
||||||
|
VALUES
|
||||||
|
($1,$2,$3,$4,1,0,0)` |
||||||
|
|
||||||
|
sql = strings.ReplaceAll(strings.ReplaceAll(query, "\t", " "), "\n", " ") |
||||||
|
logger.Debug(reference_id, "SQL : ", sql) |
||||||
|
_, err = conn.Exec(query, session_id, user_id, session_secret, time.Now().Unix()) |
||||||
|
return err |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
### AUTH |
||||||
|
POST https://servobot.ai/servoauth/auth |
||||||
|
Content-Type: application/json |
||||||
|
|
||||||
|
{ |
||||||
|
"username":"zadahouse", |
||||||
|
"half_nonce":"01234567" |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
### VERIFY |
||||||
|
POST https://servobot.ai/servoauth/verify |
||||||
|
Content-Type: application/json |
||||||
|
|
||||||
|
{ |
||||||
|
"full_nonce":"01234567eko42163", |
||||||
|
"client_hash":"3ec5cf7ea34a41ca4675f24b8f9cc1696cd1c60479020c083ba724bf6e93b705", |
||||||
|
"next_nonce":"76543210" |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
### GREETING |
||||||
|
POST https://servobot.ai/servoauth/ |
||||||
|
Content-Type: application/json |
||||||
|
|
||||||
|
{ |
||||||
|
"name":"emil", |
||||||
|
"password":"spider-man" |
||||||
|
} |
Loading…
Reference in new issue