From 1b3054fcde3eb42beb57e35aa8ba4c206f5149f6 Mon Sep 17 00:00:00 2001 From: blaz-cerpnjak Date: Wed, 17 Apr 2024 18:34:17 +0200 Subject: [PATCH] Implement authentication for api gateway with api tokens --- API_GatewayWeb/DataStructures/main.go | 6 ++ API_GatewayWeb/HTTP_API/Authentication.go | 26 ++++++++ API_GatewayWeb/HTTP_API/Middleware.go | 62 +++++++++++++++++++ API_GatewayWeb/HTTP_API/Routes.go | 3 + API_GatewayWeb/HTTP_API/main.go | 10 +-- API_GatewayWeb/Logic/Authentication.go | 33 ++++++++++ API_GatewayWeb/Logic/main.go | 4 +- API_GatewayWeb/main.go | 4 +- .../basket/src/services/OrderService.js | 1 + WEB_Microfrontends/home/src/App.jsx | 4 +- WEB_Microfrontends/home/src/Navbar.js | 5 +- .../home/src/services/OrdersService.js | 20 ------ .../home/src/services/ProductsService.js | 3 +- .../orders/src/services/OrdersService.js | 2 + docker-compose.yml | 7 +++ 15 files changed, 157 insertions(+), 33 deletions(-) create mode 100644 API_GatewayWeb/HTTP_API/Authentication.go create mode 100644 API_GatewayWeb/HTTP_API/Middleware.go create mode 100644 API_GatewayWeb/Logic/Authentication.go delete mode 100644 WEB_Microfrontends/home/src/services/OrdersService.js diff --git a/API_GatewayWeb/DataStructures/main.go b/API_GatewayWeb/DataStructures/main.go index 85b1711..4762e6b 100644 --- a/API_GatewayWeb/DataStructures/main.go +++ b/API_GatewayWeb/DataStructures/main.go @@ -1,9 +1,15 @@ package DataStructures import ( + "github.com/dgrijalva/jwt-go" "go.mongodb.org/mongo-driver/bson/primitive" ) +type ApiKey struct { + Id primitive.ObjectID `json:"id" bson:"_id,omitempty"` + jwt.StandardClaims +} + type User struct { Id primitive.ObjectID `json:"id" bson:"_id,omitempty"` Name string `json:"name" bson:"name"` diff --git a/API_GatewayWeb/HTTP_API/Authentication.go b/API_GatewayWeb/HTTP_API/Authentication.go new file mode 100644 index 0000000..bb6f9bd --- /dev/null +++ b/API_GatewayWeb/HTTP_API/Authentication.go @@ -0,0 +1,26 @@ +package HTTP_API + +import ( + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +func (a *Controller) generateApiToken(c *gin.Context) { + + id, err := primitive.ObjectIDFromHex(c.Param("userId")) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + expirationTime := time.Now().AddDate(0, 0, 30) + + tokenString, err := a.logic.GenerateApiToken(id, expirationTime) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + + c.JSON(200, gin.H{"api_token": tokenString}) +} diff --git a/API_GatewayWeb/HTTP_API/Middleware.go b/API_GatewayWeb/HTTP_API/Middleware.go new file mode 100644 index 0000000..ecf8bf4 --- /dev/null +++ b/API_GatewayWeb/HTTP_API/Middleware.go @@ -0,0 +1,62 @@ +package HTTP_API + +import ( + "errors" + "github.com/dgrijalva/jwt-go" + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" + "net/http" + "strings" +) + +/* +Middleware for checking api key/token for each request. +*/ +func (a *Controller) checkAuth() gin.HandlerFunc { + return func(c *gin.Context) { + var jwtString string + var err error + + tokenHeader := c.GetHeader("Authorization") + if len(tokenHeader) > 0 { + headerArr := strings.Split(tokenHeader, " ") + if len(headerArr) != 2 { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + jwtString = headerArr[1] + } else { + jwtString, err = c.Cookie("JWT") + if err != nil || jwtString == "" { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + } + + token, err := jwt.Parse(jwtString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + c.AbortWithStatus(http.StatusUnauthorized) + return nil, errors.New("api token is not valid") + } + return a.jwtSecret, nil + }) + if err != nil { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + userId, err := primitive.ObjectIDFromHex(claims["id"].(string)) + if err != nil { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + c.Set("userId", userId) + } else { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + c.Next() + } +} diff --git a/API_GatewayWeb/HTTP_API/Routes.go b/API_GatewayWeb/HTTP_API/Routes.go index 7349936..87ad8df 100644 --- a/API_GatewayWeb/HTTP_API/Routes.go +++ b/API_GatewayWeb/HTTP_API/Routes.go @@ -6,6 +6,9 @@ func (a *Controller) registerRoutes(engine *gin.Engine) { api := engine.Group("/api/v1") api.GET("/", a.Ping) + api.GET("/token/generate/:userId", a.generateApiToken) + + api.Use(a.checkAuth()) a.registerUserRoutes(api.Group("/users")) a.registerRestaurantRoutes(api.Group("/restaurants")) diff --git a/API_GatewayWeb/HTTP_API/main.go b/API_GatewayWeb/HTTP_API/main.go index 2e88925..c93e863 100644 --- a/API_GatewayWeb/HTTP_API/main.go +++ b/API_GatewayWeb/HTTP_API/main.go @@ -11,13 +11,15 @@ import ( ) type Controller struct { - logic *Logic.Controller - done chan bool + logic *Logic.Controller + jwtSecret []byte + done chan bool } -func New(logic *Logic.Controller) *Controller { +func New(logic *Logic.Controller, jwtSecret string) *Controller { return &Controller{ - logic: logic, + logic: logic, + jwtSecret: []byte(jwtSecret), } } diff --git a/API_GatewayWeb/Logic/Authentication.go b/API_GatewayWeb/Logic/Authentication.go new file mode 100644 index 0000000..2c45a7e --- /dev/null +++ b/API_GatewayWeb/Logic/Authentication.go @@ -0,0 +1,33 @@ +package Logic + +import ( + "API_GatewayWeb/DataStructures" + "fmt" + "github.com/dgrijalva/jwt-go" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +func (c *Controller) GenerateApiToken(userId primitive.ObjectID, expirationTime time.Time) (tokenString string, err error) { + + // First should also check if the user exists in the database + // and has paid for the service or has the right to access the service, etc. + // We should also save api tokens in key-value store like Redis so that we can + // invalidate the token if needed. + + tk := &DataStructures.ApiKey{ + Id: userId, + StandardClaims: jwt.StandardClaims{ + ExpiresAt: expirationTime.Unix(), + }} + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, tk) + + tokenString, err = token.SignedString(c.jwtSecret) + if err != nil { + fmt.Println(err.Error()) + return + } + + return +} diff --git a/API_GatewayWeb/Logic/main.go b/API_GatewayWeb/Logic/main.go index 2cb07ca..d07d0a0 100644 --- a/API_GatewayWeb/Logic/main.go +++ b/API_GatewayWeb/Logic/main.go @@ -9,14 +9,16 @@ import ( type Controller struct { httpClient *http.Client grpc *gRPC_Client.Controller + jwtSecret []byte } -func New(grpc *gRPC_Client.Controller) (*Controller, error) { +func New(grpc *gRPC_Client.Controller, jwtSecret string) (*Controller, error) { httpClient := &http.Client{} return &Controller{ httpClient: httpClient, grpc: grpc, + jwtSecret: []byte(jwtSecret), }, nil } diff --git a/API_GatewayWeb/main.go b/API_GatewayWeb/main.go index 38d8044..8021840 100644 --- a/API_GatewayWeb/main.go +++ b/API_GatewayWeb/main.go @@ -20,13 +20,13 @@ func main() { fmt.Println("gRPC connection established") - logicController, err := Logic.New(grpcClient) + logicController, err := Logic.New(grpcClient, getEnv("JWT_SECRET", "")) if err != nil { fmt.Println(err.Error()) return } - httpAPI := HTTP_API.New(logicController) + httpAPI := HTTP_API.New(logicController, getEnv("JWT_SECRET", "")) httpAPI.Start() quit := make(chan os.Signal, 0) diff --git a/WEB_Microfrontends/basket/src/services/OrderService.js b/WEB_Microfrontends/basket/src/services/OrderService.js index 649aa0d..5342cc6 100644 --- a/WEB_Microfrontends/basket/src/services/OrderService.js +++ b/WEB_Microfrontends/basket/src/services/OrderService.js @@ -4,6 +4,7 @@ export const createOrder = (order) => headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', + 'Authorization': `Bearer ${process.env.API_TOKEN}`, }, body: JSON.stringify(order) }).then((res) => { diff --git a/WEB_Microfrontends/home/src/App.jsx b/WEB_Microfrontends/home/src/App.jsx index eca7df4..70f7c1b 100644 --- a/WEB_Microfrontends/home/src/App.jsx +++ b/WEB_Microfrontends/home/src/App.jsx @@ -13,7 +13,7 @@ import Navbar from "./Navbar"; import ProductsPage from "./ProductsPage"; import {createRoot} from "react-dom/client"; import {BasketProvider, useBasket} from "./context/BasketContext"; -import BasketPage2 from "./BasketPage"; +import BasketPage from "./BasketPage"; const App = () => ( @@ -25,7 +25,7 @@ const App = () => ( } /> } /> - } /> + } /> diff --git a/WEB_Microfrontends/home/src/Navbar.js b/WEB_Microfrontends/home/src/Navbar.js index d3b6170..71dd427 100644 --- a/WEB_Microfrontends/home/src/Navbar.js +++ b/WEB_Microfrontends/home/src/Navbar.js @@ -8,7 +8,7 @@ import { useBasket } from "./context/BasketContext"; export default function Navbar() { const { state: basketState } = useBasket(); - const badgeNumber = basketState.items.length; + const basketBadgeNumber = basketState.items.length; const itemRenderer = (item) => ( @@ -28,14 +28,13 @@ export default function Navbar() { label: 'Orders', icon: 'pi pi-envelope', path: '/orders', - badge: 3, template: itemRenderer }, { label: 'Basket', icon: 'pi pi-shopping-cart', path: '/basket', - badge: badgeNumber, + badge: basketBadgeNumber, template: itemRenderer } ]; diff --git a/WEB_Microfrontends/home/src/services/OrdersService.js b/WEB_Microfrontends/home/src/services/OrdersService.js deleted file mode 100644 index 87a0e0a..0000000 --- a/WEB_Microfrontends/home/src/services/OrdersService.js +++ /dev/null @@ -1,20 +0,0 @@ -export const createOrder = (order) => - fetch(`/api/v1/orders`, { - method: 'POSt', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify(order) - }).then((res) => { - if (!res.ok) { - throw new Error('Error creating order'); - } - return res.json(); - }).then((data) => { - console.log(data); - return data; - }).catch((error) => { - console.error(error); - return null; - }); diff --git a/WEB_Microfrontends/home/src/services/ProductsService.js b/WEB_Microfrontends/home/src/services/ProductsService.js index efe376a..d324970 100644 --- a/WEB_Microfrontends/home/src/services/ProductsService.js +++ b/WEB_Microfrontends/home/src/services/ProductsService.js @@ -4,5 +4,6 @@ export const getProducts = () => headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', + 'Authorization': `Bearer ${process.env.API_TOKEN}`, }, - }).then((res) => res.json()); + }).then((res) => res.json()) diff --git a/WEB_Microfrontends/orders/src/services/OrdersService.js b/WEB_Microfrontends/orders/src/services/OrdersService.js index 9dd2f6a..bc1a8ec 100644 --- a/WEB_Microfrontends/orders/src/services/OrdersService.js +++ b/WEB_Microfrontends/orders/src/services/OrdersService.js @@ -4,6 +4,7 @@ export const getOrders = (order) => headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', + 'Authorization': `Bearer ${process.env.API_TOKEN}`, } }).then((res) => { if (!res.ok) { @@ -18,6 +19,7 @@ export const cancelOrder = (order) => headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', + 'Authorization': `Bearer ${process.env.API_TOKEN}`, }, body: JSON.stringify({ ...order, diff --git a/docker-compose.yml b/docker-compose.yml index 26884a7..500af42 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -83,6 +83,7 @@ services: dockerfile: Dockerfile restart: always environment: + JWT_SECRET: A9BBD31CB41A4629875353E3A1AA9 USERS_API: http://user-management-api:8080/api/v1 RESTAURANTS_API: http://restaurant-management-api:8080 ORDERS_GRPC_API: order-processing-grpc-api:9000 @@ -117,6 +118,8 @@ services: build: context: ./WEB_Microfrontends/orders dockerfile: Dockerfile + environment: + API_TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY2MWZlYjU2YjI2ZTEwYjRjMDkzNzZjNiIsImV4cCI6NjQ0OTQ3MDg3OH0.fOWJzwFCdb6pWJkr8wJUxW1bvZ2PWsrU4qjanFq6tpU ports: - "3001:3001" networks: @@ -129,6 +132,8 @@ services: build: context: ./WEB_Microfrontends/basket dockerfile: Dockerfile + environment: + API_TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY2MWZlYjU2YjI2ZTEwYjRjMDkzNzZjNiIsImV4cCI6NjQ0OTQ3MDg3OH0.fOWJzwFCdb6pWJkr8wJUxW1bvZ2PWsrU4qjanFq6tpU ports: - "3002:3002" networks: @@ -141,6 +146,8 @@ services: build: context: ./WEB_Microfrontends/home dockerfile: Dockerfile + environment: + API_TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY2MWZlYjU2YjI2ZTEwYjRjMDkzNzZjNiIsImV4cCI6NjQ0OTQ3MDg3OH0.fOWJzwFCdb6pWJkr8wJUxW1bvZ2PWsrU4qjanFq6tpU ports: - "3000:3000" networks: