bismillah
This commit is contained in:
31
Dockerfile
Normal file
31
Dockerfile
Normal file
@@ -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"]
|
||||||
|
|
||||||
100
authenticate.go
Normal file
100
authenticate.go
Normal file
@@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
106
create-local-identity.go
Normal file
106
create-local-identity.go
Normal file
@@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
71
crypto.go
Normal file
71
crypto.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
38
db.go
Normal file
38
db.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
17
go.mod
Normal file
17
go.mod
Normal file
@@ -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
|
||||||
|
)
|
||||||
30
go.sum
Normal file
30
go.sum
Normal file
@@ -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=
|
||||||
109
main.go
Normal file
109
main.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
27
notes.md
Normal file
27
notes.md
Normal file
@@ -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"
|
||||||
|
}'
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user