diff --git a/Dockerfile b/Dockerfile index 2725acd..c46cc6b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,37 @@ # ---- Build Stage ---- -FROM golang:latest AS builder +FROM golang:1.26 AS builder WORKDIR /app -# Copy go mod files first (better caching) -COPY go.mod ./ -RUN go mod download +# ---- Step 1: Copy go.mod/go.sum for caching ---- +# Copy only the minimal files first for layer caching +COPY libshared/go.mod libshared/go.sum ./libshared/ +COPY identity-manager/go.mod identity-manager/go.sum ./identity-manager/ -# Copy source code -COPY . . +# ---- Step 2: Download dependencies ---- +RUN go -C libshared mod download +RUN go -C identity-manager mod download -# Build static binary +# ---- Step 3: Copy full source code ---- +COPY libshared ./libshared +COPY identity-manager ./identity-manager + +# ---- Step 4: Create Go workspace inside container ---- +RUN go work init ./identity-manager ./libshared + +## Optional: verify workspace +#RUN go work list + +# ---- Step 5: Build binary ---- +WORKDIR /app/identity-manager RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o server # ---- Runtime Stage ---- FROM gcr.io/distroless/base-debian12 WORKDIR /app +COPY --from=builder /app/identity-manager/server . -# 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"] - +ENTRYPOINT ["/app/server"] \ No newline at end of file diff --git a/authenticate.go b/authenticate.go index 881ddf8..097afcb 100644 --- a/authenticate.go +++ b/authenticate.go @@ -4,9 +4,9 @@ import ( "context" "encoding/json" "fmt" + "libshared" "log" "net/http" - "time" "github.com/jackc/pgx/v5" ) @@ -17,16 +17,12 @@ type AuthenticateRequest struct { Password string `json:"password"` } +type AuthenticateResponse struct { + Token string `json:"token"` +} + func authenticateHandler(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 - */ var authenticaterequest AuthenticateRequest var err error var checkExisting pgx.Row @@ -35,24 +31,28 @@ func authenticateHandler(w http.ResponseWriter, r *http.Request) { var token string secret := []byte("super-secret-key") - 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 + + w.Header().Set("Content-Type", "application/json") + apiresponse := libshared.NewAPIResponse("fail", "POST method required", AuthenticateResponse{}) + json.NewEncoder(w).Encode(apiresponse) + + return } // Optional: enforce content type if r.Header.Get("Content-Type") != "application/json" { + apiresponse := libshared.NewAPIResponse("fail", "Content-Type must be application/json", AuthenticateResponse{}) w.WriteHeader(http.StatusUnsupportedMediaType) - response["status"] = "error" - response["message"] = "Content-Type must be application/json" - goto ExitAPICall + //response["status"] = "error" + //response["message"] = "Content-Type must be application/json" + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(apiresponse) + return } // Read body with size limit (protect against huge requests) @@ -63,15 +63,18 @@ func authenticateHandler(w http.ResponseWriter, r *http.Request) { log.Println(r.Body) err = json.NewDecoder(r.Body).Decode(&authenticaterequest) if err != nil { + apiresponse := libshared.NewAPIResponse("fail", "Invalid JSON: "+err.Error(), AuthenticateResponse{}) w.WriteHeader(http.StatusInternalServerError) - response["status"] = "error" - response["message"] = "Invalid JSON: " + err.Error() - goto ExitAPICall + //response["status"] = "error" + //response["message"] = "Invalid JSON: " + err.Error() + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(apiresponse) + return } log.Println(authenticaterequest) - checkExisting = pool.QueryRow(context.Background(), + checkExisting = libshared.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) @@ -79,37 +82,39 @@ func authenticateHandler(w http.ResponseWriter, r *http.Request) { err = checkExisting.Scan(&hashText) if err != nil { - response["status"] = "fail" - response["message"] = "User account does not exist" + apiresponse := libshared.NewAPIResponse("fail", "User account does not exist", AuthenticateResponse{}) log.Println(err) w.WriteHeader(http.StatusUnauthorized) - goto ExitAPICall + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(apiresponse) + return } log.Println(hashText) ok = verifyPassword(authenticaterequest.Password, hashText) if ok == false { - response["status"] = "fail" - response["message"] = "Bad password" + apiresponse := libshared.NewAPIResponse("fail", "Incorrect username or password", AuthenticateResponse{}) + //response["message"] = "Bad password" w.WriteHeader(http.StatusUnauthorized) - goto ExitAPICall + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(apiresponse) + return } token, err = createJWT(secret, fmt.Sprintf("%d", authenticaterequest.Accountid), authenticaterequest.Username, "user") if err != nil { w.WriteHeader(http.StatusInternalServerError) - response["status"] = "error" - response["message"] = "Failed to create JWT" - goto ExitAPICall + apiresponse := libshared.NewAPIResponse("fail", "Failed to create JWT", AuthenticateResponse{}) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(apiresponse) + + return } + apiresponse := libshared.NewAPIResponse("success", "Authentication successful", AuthenticateResponse{Token: token}) w.WriteHeader(http.StatusOK) - response["status"] = "success" - response["token"] = token -ExitAPICall: w.Header().Set("Content-Type", "application/json") - response["timestamp"] = fmt.Sprintf("%d", time.Now().Unix()) - json.NewEncoder(w).Encode(response) + json.NewEncoder(w).Encode(apiresponse) } diff --git a/create-local-identity.go b/create-local-identity.go index fed1949..a03b1e9 100644 --- a/create-local-identity.go +++ b/create-local-identity.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "libshared" "log" "net/http" "time" @@ -73,7 +74,7 @@ func createLocalHandler(w http.ResponseWriter, r *http.Request) { 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(), + checkExisting = libshared.Pool.QueryRow(context.Background(), "SELECT FROM identities WHERE accountid = $1 AND provider_user_id = $2", accountid, req.Username) err = checkExisting.Scan() @@ -87,7 +88,7 @@ func createLocalHandler(w http.ResponseWriter, r *http.Request) { salt, _ = generateSalt() hashText = hashPassword(req.Password, salt) - _, err = pool.Exec(context.Background(), + _, err = libshared.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 { diff --git a/db.go b/db.go deleted file mode 100644 index fc677c2..0000000 --- a/db.go +++ /dev/null @@ -1,38 +0,0 @@ -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/jwt.go b/jwt.go new file mode 100644 index 0000000..5b90c34 --- /dev/null +++ b/jwt.go @@ -0,0 +1,22 @@ +package main + +import ( + "time" + + "github.com/golang-jwt/jwt/v5" +) + +// CreateJWT generates a signed JWT +func createJWT(secret []byte, account string, user string, purpose string) (string, error) { + claims := jwt.MapClaims{ + "sub": user, // subject (user id) + "exp": time.Now().Add(time.Hour).Unix(), // expiration + "iat": time.Now().Unix(), // issued at + "purpose": purpose, + "account": account, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + return token.SignedString(secret) +} diff --git a/login.go b/login.go new file mode 100644 index 0000000..bb3201f --- /dev/null +++ b/login.go @@ -0,0 +1,58 @@ +package main + +import ( + "context" + "encoding/json" + "libshared" + "log" + "net/http" +) + +type LoginRequest struct { + AccountID int64 `json:"account_id"` + Username string `json:"username"` + Password string `json:"password"` +} + +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 = libshared.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")) +} diff --git a/main.go b/main.go index f92bb3b..2772192 100644 --- a/main.go +++ b/main.go @@ -1,91 +1,12 @@ -/* 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" + "libshared" "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 @@ -99,7 +20,7 @@ func decodeHash(encoded string) (*argonParams, []byte, []byte, error) { } func main() { - pool = getDbPool() + libshared.Pool = libshared.GetDbPool() http.HandleFunc("/identity/create-local-identity", createLocalHandler) http.HandleFunc("/identity/authenticate", authenticateHandler)