auth + creating identities
This commit is contained in:
38
Dockerfile
38
Dockerfile
@@ -1,31 +1,37 @@
|
|||||||
# ---- Build Stage ----
|
# ---- Build Stage ----
|
||||||
FROM golang:latest AS builder
|
FROM golang:1.26 AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy go mod files first (better caching)
|
# ---- Step 1: Copy go.mod/go.sum for caching ----
|
||||||
COPY go.mod ./
|
# Copy only the minimal files first for layer caching
|
||||||
RUN go mod download
|
COPY libshared/go.mod libshared/go.sum ./libshared/
|
||||||
|
COPY identity-manager/go.mod identity-manager/go.sum ./identity-manager/
|
||||||
|
|
||||||
# Copy source code
|
# ---- Step 2: Download dependencies ----
|
||||||
COPY . .
|
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
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o server
|
||||||
|
|
||||||
# ---- Runtime Stage ----
|
# ---- Runtime Stage ----
|
||||||
FROM gcr.io/distroless/base-debian12
|
FROM gcr.io/distroless/base-debian12
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
COPY --from=builder /app/identity-manager/server .
|
||||||
|
|
||||||
# Copy only the binary from builder
|
|
||||||
COPY --from=builder /app/server .
|
|
||||||
|
|
||||||
# Expose port
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
# Run as non-root user
|
|
||||||
USER nonroot:nonroot
|
USER nonroot:nonroot
|
||||||
|
ENTRYPOINT ["/app/server"]
|
||||||
ENTRYPOINT ["/app/server"]
|
|
||||||
|
|
||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"libshared"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
)
|
)
|
||||||
@@ -17,16 +17,12 @@ type AuthenticateRequest struct {
|
|||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthenticateResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
func authenticateHandler(w http.ResponseWriter, r *http.Request) {
|
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 authenticaterequest AuthenticateRequest
|
||||||
var err error
|
var err error
|
||||||
var checkExisting pgx.Row
|
var checkExisting pgx.Row
|
||||||
@@ -35,24 +31,28 @@ func authenticateHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
var token string
|
var token string
|
||||||
secret := []byte("super-secret-key")
|
secret := []byte("super-secret-key")
|
||||||
|
|
||||||
response := map[string]interface{}{}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
// Only allow POST method
|
// Only allow POST method
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
//http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
//http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
response["status"] = "error"
|
|
||||||
response["message"] = "HTTP POST Method not allowed"
|
w.Header().Set("Content-Type", "application/json")
|
||||||
goto ExitAPICall
|
apiresponse := libshared.NewAPIResponse("fail", "POST method required", AuthenticateResponse{})
|
||||||
|
json.NewEncoder(w).Encode(apiresponse)
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional: enforce content type
|
// Optional: enforce content type
|
||||||
if r.Header.Get("Content-Type") != "application/json" {
|
if r.Header.Get("Content-Type") != "application/json" {
|
||||||
|
apiresponse := libshared.NewAPIResponse("fail", "Content-Type must be application/json", AuthenticateResponse{})
|
||||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||||
response["status"] = "error"
|
//response["status"] = "error"
|
||||||
response["message"] = "Content-Type must be application/json"
|
//response["message"] = "Content-Type must be application/json"
|
||||||
goto ExitAPICall
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(apiresponse)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read body with size limit (protect against huge requests)
|
// 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)
|
log.Println(r.Body)
|
||||||
err = json.NewDecoder(r.Body).Decode(&authenticaterequest)
|
err = json.NewDecoder(r.Body).Decode(&authenticaterequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
apiresponse := libshared.NewAPIResponse("fail", "Invalid JSON: "+err.Error(), AuthenticateResponse{})
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
response["status"] = "error"
|
//response["status"] = "error"
|
||||||
response["message"] = "Invalid JSON: " + err.Error()
|
//response["message"] = "Invalid JSON: " + err.Error()
|
||||||
goto ExitAPICall
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(apiresponse)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println(authenticaterequest)
|
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",
|
"SELECT password_hash FROM identities WHERE accountid = $1 AND provider = $2 AND provider_user_id = $3",
|
||||||
authenticaterequest.Accountid, "local", authenticaterequest.Username)
|
authenticaterequest.Accountid, "local", authenticaterequest.Username)
|
||||||
|
|
||||||
@@ -79,37 +82,39 @@ func authenticateHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
err = checkExisting.Scan(&hashText)
|
err = checkExisting.Scan(&hashText)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response["status"] = "fail"
|
apiresponse := libshared.NewAPIResponse("fail", "User account does not exist", AuthenticateResponse{})
|
||||||
response["message"] = "User account does not exist"
|
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
goto ExitAPICall
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(apiresponse)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println(hashText)
|
log.Println(hashText)
|
||||||
|
|
||||||
ok = verifyPassword(authenticaterequest.Password, hashText)
|
ok = verifyPassword(authenticaterequest.Password, hashText)
|
||||||
if ok == false {
|
if ok == false {
|
||||||
response["status"] = "fail"
|
apiresponse := libshared.NewAPIResponse("fail", "Incorrect username or password", AuthenticateResponse{})
|
||||||
response["message"] = "Bad password"
|
//response["message"] = "Bad password"
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
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")
|
token, err = createJWT(secret, fmt.Sprintf("%d", authenticaterequest.Accountid), authenticaterequest.Username, "user")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
response["status"] = "error"
|
apiresponse := libshared.NewAPIResponse("fail", "Failed to create JWT", AuthenticateResponse{})
|
||||||
response["message"] = "Failed to create JWT"
|
w.Header().Set("Content-Type", "application/json")
|
||||||
goto ExitAPICall
|
json.NewEncoder(w).Encode(apiresponse)
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apiresponse := libshared.NewAPIResponse("success", "Authentication successful", AuthenticateResponse{Token: token})
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response["status"] = "success"
|
|
||||||
response["token"] = token
|
|
||||||
ExitAPICall:
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
response["timestamp"] = fmt.Sprintf("%d", time.Now().Unix())
|
json.NewEncoder(w).Encode(apiresponse)
|
||||||
json.NewEncoder(w).Encode(response)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"libshared"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"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)
|
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
|
// 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",
|
"SELECT FROM identities WHERE accountid = $1 AND provider_user_id = $2",
|
||||||
accountid, req.Username)
|
accountid, req.Username)
|
||||||
err = checkExisting.Scan()
|
err = checkExisting.Scan()
|
||||||
@@ -87,7 +88,7 @@ func createLocalHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
salt, _ = generateSalt()
|
salt, _ = generateSalt()
|
||||||
hashText = hashPassword(req.Password, salt)
|
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",
|
"INSERT INTO identities (accountid, provider, provider_user_id, password_hash) VALUES($1, $2, $3, $4) ON CONFLICT DO NOTHING",
|
||||||
accountid, "local", req.Username, hashText)
|
accountid, "local", req.Username, hashText)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
38
db.go
38
db.go
@@ -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
|
|
||||||
}
|
|
||||||
22
jwt.go
Normal file
22
jwt.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
58
login.go
Normal file
58
login.go
Normal file
@@ -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"))
|
||||||
|
}
|
||||||
83
main.go
83
main.go
@@ -1,91 +1,12 @@
|
|||||||
/*
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
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"
|
"errors"
|
||||||
|
"libshared"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"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 {
|
type argonParams struct {
|
||||||
memory uint32
|
memory uint32
|
||||||
iterations uint32
|
iterations uint32
|
||||||
@@ -99,7 +20,7 @@ func decodeHash(encoded string) (*argonParams, []byte, []byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
pool = getDbPool()
|
libshared.Pool = libshared.GetDbPool()
|
||||||
|
|
||||||
http.HandleFunc("/identity/create-local-identity", createLocalHandler)
|
http.HandleFunc("/identity/create-local-identity", createLocalHandler)
|
||||||
http.HandleFunc("/identity/authenticate", authenticateHandler)
|
http.HandleFunc("/identity/authenticate", authenticateHandler)
|
||||||
|
|||||||
Reference in New Issue
Block a user