From 04317129c2283ce91150eca83bf388dda527521f Mon Sep 17 00:00:00 2001 From: Farhan Khan Date: Thu, 19 Mar 2026 16:08:28 -0400 Subject: [PATCH] bismillah --- Dockerfile | 31 +++++++++++ authenticate.go | 100 +++++++++++++++++++++++++++++++++++ create-local-identity.go | 106 +++++++++++++++++++++++++++++++++++++ crypto.go | 71 +++++++++++++++++++++++++ db.go | 38 ++++++++++++++ go.mod | 17 ++++++ go.sum | 30 +++++++++++ main.go | 109 +++++++++++++++++++++++++++++++++++++++ notes.md | 27 ++++++++++ 9 files changed, 529 insertions(+) create mode 100644 Dockerfile create mode 100644 authenticate.go create mode 100644 create-local-identity.go create mode 100644 crypto.go create mode 100644 db.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 notes.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2725acd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +# ---- Build Stage ---- +FROM golang:latest AS builder + +WORKDIR /app + +# Copy go mod files first (better caching) +COPY go.mod ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build static binary +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o server + +# ---- Runtime Stage ---- +FROM gcr.io/distroless/base-debian12 + +WORKDIR /app + +# Copy only the binary from builder +COPY --from=builder /app/server . + +# Expose port +EXPOSE 8080 + +# Run as non-root user +USER nonroot:nonroot + +ENTRYPOINT ["/app/server"] + diff --git a/authenticate.go b/authenticate.go new file mode 100644 index 0000000..f2de4b5 --- /dev/null +++ b/authenticate.go @@ -0,0 +1,100 @@ +/* + * This API call authenticates a user + */ + +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + "github.com/jackc/pgx/v5" +) + +type AuthenticateRequest struct { + Accountid int64 `json:"accountid,string"` + Username string `json:"username"` + Password string `json:"password"` +} + +func authenticateHandler(w http.ResponseWriter, r *http.Request) { + + var authenticaterequest AuthenticateRequest + var err error + var checkExisting pgx.Row + var hashText string + var ok bool + + response := map[string]interface{}{} + w.Header().Set("Content-Type", "application/json") + + // Only allow POST method + if r.Method != http.MethodPost { + //http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + w.WriteHeader(http.StatusMethodNotAllowed) + response["status"] = "error" + response["message"] = "HTTP POST Method not allowed" + goto ExitAPICall + } + + // Optional: enforce content type + if r.Header.Get("Content-Type") != "application/json" { + w.WriteHeader(http.StatusUnsupportedMediaType) + response["status"] = "error" + response["message"] = "Content-Type must be application/json" + goto ExitAPICall + } + + // Read body with size limit (protect against huge requests) + const maxBodyBytes = 1 << 20 // 1 MB + r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes) + + // Decode JSON from request body directly into struct + log.Println(r.Body) + err = json.NewDecoder(r.Body).Decode(&authenticaterequest) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + response["status"] = "error" + response["message"] = "Invalid JSON: " + err.Error() + goto ExitAPICall + } + + log.Println(authenticaterequest) + + checkExisting = pool.QueryRow(context.Background(), + "SELECT password_hash FROM identities WHERE accountid = $1 AND provider = $2 AND provider_user_id = $3", + authenticaterequest.Accountid, "local", authenticaterequest.Username) + + log.Println("Received Authentication Request: AccountID:", authenticaterequest.Accountid, "Username:", authenticaterequest.Username, "Password:", authenticaterequest.Password) + + err = checkExisting.Scan(&hashText) + if err != nil { + response["status"] = "fail" + response["message"] = "User account does not exist" + log.Println(err) + w.WriteHeader(http.StatusUnauthorized) + goto ExitAPICall + } + + log.Println(hashText) + + ok = verifyPassword(authenticaterequest.Password, hashText) + if ok == false { + response["status"] = "fail" + response["message"] = "Bad password" + w.WriteHeader(http.StatusUnauthorized) + goto ExitAPICall + } + + w.WriteHeader(http.StatusOK) + response["status"] = "success" +ExitAPICall: + w.Header().Set("Content-Type", "application/json") + response["timestamp"] = fmt.Sprintf("%d", time.Now().Unix()) + json.NewEncoder(w).Encode(response) + +} diff --git a/create-local-identity.go b/create-local-identity.go new file mode 100644 index 0000000..fed1949 --- /dev/null +++ b/create-local-identity.go @@ -0,0 +1,106 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + "github.com/jackc/pgx/v5" +) + +type CreateIdentityRequest struct { + Username string `json:"username"` + Password string `json:"password"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` +} + +func createLocalHandler(w http.ResponseWriter, r *http.Request) { + + var hashText string + var salt []byte + var err error + var checkExisting pgx.Row + var req CreateIdentityRequest + var accountid string + + response := map[string]interface{}{} + w.Header().Set("Content-Type", "application/json") + + // Only allow POST method + if r.Method != http.MethodPost { + //http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + w.WriteHeader(http.StatusMethodNotAllowed) + response["status"] = "error" + response["message"] = "HTTP POST Method not allowed" + goto ExitAPICall + } + + // XXX Temporary Account Handler + accountid = r.Header.Get("Account") + if accountid == "" { + + w.WriteHeader(http.StatusUnauthorized) + response["status"] = "error" + response["message"] = "Missing Authentication header" + goto ExitAPICall + } + + // Optional: enforce content type + if r.Header.Get("Content-Type") != "application/json" { + w.WriteHeader(http.StatusUnsupportedMediaType) + response["status"] = "error" + response["message"] = "Content-Type must be application/json" + goto ExitAPICall + } + + // Read body with size limit (protect against huge requests) + const maxBodyBytes = 1 << 20 // 1 MB + r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes) + + // Decode JSON from request body directly into struct + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + response["status"] = "error" + response["message"] = "Invalid JSON: " + err.Error() + goto ExitAPICall + } + + log.Println("Received Identity Request: AccountID:", accountid, "Username:", req.Username, "Password:", req.Password, "First Name:", req.FirstName, "LastName:", req.LastName) + + // Check if policy with the same name already exists for the account + checkExisting = pool.QueryRow(context.Background(), + "SELECT FROM identities WHERE accountid = $1 AND provider_user_id = $2", + accountid, req.Username) + err = checkExisting.Scan() + if err == nil { + response["status"] = "fail" + response["message"] = "Identity " + req.Username + " already exists: " + w.WriteHeader(http.StatusConflict) + goto ExitAPICall + } + + salt, _ = generateSalt() + hashText = hashPassword(req.Password, salt) + + _, err = pool.Exec(context.Background(), + "INSERT INTO identities (accountid, provider, provider_user_id, password_hash) VALUES($1, $2, $3, $4) ON CONFLICT DO NOTHING", + accountid, "local", req.Username, hashText) + if err != nil { + response["status"] = "fail" + response["message"] = "Internal Server Error: " + err.Error() + w.WriteHeader(http.StatusInternalServerError) + goto ExitAPICall + } + + w.WriteHeader(http.StatusOK) + response["status"] = "success" +ExitAPICall: + response["timestamp"] = fmt.Sprintf("%d", time.Now().Unix()) + json.NewEncoder(w).Encode(response) + +} diff --git a/crypto.go b/crypto.go new file mode 100644 index 0000000..0b10546 --- /dev/null +++ b/crypto.go @@ -0,0 +1,71 @@ +package main + +import ( + "crypto/rand" + "crypto/subtle" + "encoding/base64" + "fmt" + "strings" + + "golang.org/x/crypto/argon2" +) + +const ( + memory = 64 * 1024 // 64 MB + iterations = 3 + parallelism = 2 + saltLength = 16 + keyLength = 32 +) + +func generateSalt() ([]byte, error) { + salt := make([]byte, saltLength) + _, err := rand.Read(salt) + return salt, err +} + +func verifyPassword(password, encodedHash string) bool { + + parts := strings.Split(encodedHash, "$") + + params := parts[3] + salt := parts[4] + hash := parts[5] + + var memory uint32 + var iterations uint32 + var parallelism uint8 + + fmt.Sscanf(params, "m=%d,t=%d,p=%d", &memory, &iterations, ¶llelism) + + saltBytes, _ := base64.RawStdEncoding.DecodeString(salt) + hashBytes, _ := base64.RawStdEncoding.DecodeString(hash) + + comparisonHash := argon2.IDKey( + []byte(password), + saltBytes, + iterations, + memory, + parallelism, + uint32(len(hashBytes)), + ) + + return subtle.ConstantTimeCompare(hashBytes, comparisonHash) == 1 +} + +func hashPassword(password string, salt []byte) string { + hash := argon2.IDKey( + []byte(password), + salt, + iterations, + memory, + parallelism, + keyLength, + ) + + b64Salt := base64.RawStdEncoding.EncodeToString(salt) + b64Hash := base64.RawStdEncoding.EncodeToString(hash) + + return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s", + memory, iterations, parallelism, b64Salt, b64Hash) +} diff --git a/db.go b/db.go new file mode 100644 index 0000000..fc677c2 --- /dev/null +++ b/db.go @@ -0,0 +1,38 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/jackc/pgx/v5/pgxpool" +) + +var pool *pgxpool.Pool + +func getDbPool() *pgxpool.Pool { + // Construct the connection string + // Note: Ensure your Docker Compose env vars match these keys! + dburl := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", + os.Getenv("POSTGRES_USER"), + os.Getenv("POSTGRES_PASSWORD"), + os.Getenv("POSTGRES_HOSTNAME"), + os.Getenv("POSTGRES_DB"), + ) + + var err error + // Use pgxpool.New instead of Connect for v5 + pool, err = pgxpool.New(context.Background(), dburl) + if err != nil { + log.Fatalf("Unable to create connection pool: %v", err) + } + + // Ping the database to verify the connection is actually live + err = pool.Ping(context.Background()) + if err != nil { + log.Fatalf("Unable to ping database: %v", err) + } + + return pool +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bf83af4 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module identity-manager + +go 1.25.0 + +require ( + github.com/jackc/pgx/v5 v5.8.0 + golang.org/x/crypto v0.49.0 +) + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f3a5617 --- /dev/null +++ b/go.sum @@ -0,0 +1,30 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..f92bb3b --- /dev/null +++ b/main.go @@ -0,0 +1,109 @@ +/* +package main + +import ( + "log" + "net/http" +) + +func newIdentity(w http.ResponseWriter, r *http.Request) { + log.Println("New Account") + return +} + +func main() { + + //pool = getDbPool() + + http.HandleFunc("/identity/new-account", newIdentity) + log.Println("Server running on :8082") + log.Fatal(http.ListenAndServe(":8082", nil)) +} +*/ + +package main + +import ( + "context" + "encoding/json" + "errors" + "log" + "net/http" +) + +type LoginRequest struct { + AccountID int64 `json:"account_id"` + Username string `json:"username"` + Password string `json:"password"` +} + +/* +type Server struct { + DB *pgxpool.Pool +} +*/ + +func LoginHandler(w http.ResponseWriter, r *http.Request) { + //ctx := r.Context() + + var req LoginRequest + var ok bool + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + http.Error(w, "invalid request", http.StatusBadRequest) + return + } + + var storedHash string + + err = pool.QueryRow( + context.Background(), + `SELECT password_hash + FROM identities + WHERE accountid=$1 AND provider_user_id=$2`, + req.AccountID, + req.Username, + ).Scan(&storedHash) + + if err != nil { + http.Error(w, "invalid credentials", http.StatusUnauthorized) + return + } + + ok = verifyPassword(req.Password, storedHash) + if err != nil { + log.Println(err) + http.Error(w, "authentication error", http.StatusInternalServerError) + return + } + + if !ok { + http.Error(w, "invalid credentials", http.StatusUnauthorized) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("login successful")) +} + +type argonParams struct { + memory uint32 + iterations uint32 + parallelism uint8 + keyLength uint32 +} + +func decodeHash(encoded string) (*argonParams, []byte, []byte, error) { + // Placeholder for PHC parsing implementation + return nil, nil, nil, errors.New("decodeHash not implemented") +} + +func main() { + pool = getDbPool() + + http.HandleFunc("/identity/create-local-identity", createLocalHandler) + http.HandleFunc("/identity/authenticate", authenticateHandler) + + log.Println("server running on :8080") + http.ListenAndServe(":8080", nil) +} diff --git a/notes.md b/notes.md new file mode 100644 index 0000000..02bb8a3 --- /dev/null +++ b/notes.md @@ -0,0 +1,27 @@ +## Identity Manager + + +### Create Local Account + +``` +curl -X POST http://localhost:8080/identity/create-local-identity \ + -H "Content-Type: application/json" \ + -H "Account: 987272956921" \ + -d '{ + "username": "farhan", + "first_name": "Farhan", + "last_name": "Khan" + }' +``` + +### Authenticate + +``` +curl -X POST http://localhost:8080/identity/authenticate \ + -H "Content-Type: application/json" \ + -d '{ + "accountid": "987272956921", + "username": "farhan3", + "password": "letmein" + }' +``` \ No newline at end of file