Skip to content

Commit

Permalink
Added Authentication middleware & added authorization logic in all APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
ashiqYousuf committed Nov 9, 2024
1 parent c40d624 commit 08e18ea
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 20 deletions.
20 changes: 18 additions & 2 deletions api/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import (
"net/http"

db "github.com/ashiqYousuf/sbank/db/sqlc"
"github.com/ashiqYousuf/sbank/token"
"github.com/gin-gonic/gin"
"github.com/lib/pq"
)

type createAccountRequest struct {
Owner string `json:"owner" binding:"required"`
Currency string `json:"currency" binding:"required,currency"`
}

// Authorization Rule: Logged In user can only create account for himself
func (server *Server) createAccount(ctx *gin.Context) {
var req createAccountRequest

Expand All @@ -23,8 +24,10 @@ func (server *Server) createAccount(ctx *gin.Context) {
return
}

// Getting payload (user info) from context
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
arg := db.CreateAccountParams{
Owner: req.Owner,
Owner: authPayload.Username,
Currency: req.Currency,
Balance: 0,
}
Expand All @@ -49,6 +52,7 @@ type getAccountRequest struct {
ID int64 `uri:"id" binding:"required,min=1"`
}

// Authorization Rule: Logged In user can only get account that he owns
func (server *Server) getAccount(ctx *gin.Context) {
var req getAccountRequest

Expand All @@ -67,6 +71,14 @@ func (server *Server) getAccount(ctx *gin.Context) {
return
}

// Getting payload (user info) from context
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
if account.Owner != authPayload.Username {
err := errors.New("account doesn't belong to the authenticated user")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}

ctx.JSON(http.StatusOK, account)
}

Expand All @@ -75,6 +87,7 @@ type listAccountRequest struct {
PageSize int32 `form:"page_size" binding:"required,min=5,max=10"`
}

// Authorization Rule: Logged In user can only list accounts that he owns
func (server *Server) listAccount(ctx *gin.Context) {
var req listAccountRequest

Expand All @@ -83,7 +96,10 @@ func (server *Server) listAccount(ctx *gin.Context) {
return
}

// Getting payload (user info) from context
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
arg := db.ListAccountsParams{
Owner: authPayload.Username,
Limit: req.PageSize,
Offset: (req.PageID - 1) * req.PageSize,
}
Expand Down
55 changes: 55 additions & 0 deletions api/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package api

import (
"errors"
"fmt"
"net/http"
"strings"

"github.com/ashiqYousuf/sbank/token"
"github.com/gin-gonic/gin"
)

const (
authorizationHeaderKey = "authorization"
authorizationTypeBearer = "bearer"
authorizationPayloadKey = "authorization_payload"
)

// Using closure pattern to inject dependencies
// Or we can make middleware funcs methods against server struct
func authMiddleware(tokenMaker token.Maker) gin.HandlerFunc {
return func(ctx *gin.Context) {
authorizationHeader := ctx.GetHeader(authorizationHeaderKey)
if len(authorizationHeader) == 0 {
err := errors.New("authorization header is not provided")
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
return
}

fields := strings.Fields(authorizationHeader)
if len(fields) < 2 {
err := errors.New("invalid authorization header format")
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
return
}

authorizationType := strings.ToLower(fields[0])
if authorizationType != authorizationTypeBearer {
err := fmt.Errorf("unsupported authorization type %s", authorizationType)
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
return
}

accessToken := fields[1]
payload, err := tokenMaker.VerifyToken(accessToken)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
return
}

// Means a valid token is passed, store payload (user info) in context
ctx.Set(authorizationPayloadKey, payload)
ctx.Next()
}
}
10 changes: 6 additions & 4 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@ func (server *Server) setUpRouter() {
router.POST("/users", server.createUser)
router.POST("/users/login", server.loginUser)

router.POST("/accounts", server.createAccount)
router.GET("/accounts/:id", server.getAccount)
router.GET("/accounts", server.listAccount)
authRoutes := router.Group("/").Use(authMiddleware(server.tokenMaker))

router.POST("/transfers", server.createTransfer)
authRoutes.POST("/accounts", server.createAccount)
authRoutes.GET("/accounts/:id", server.getAccount)
authRoutes.GET("/accounts", server.listAccount)

authRoutes.POST("/transfers", server.createTransfer)

server.router = router
}
Expand Down
24 changes: 18 additions & 6 deletions api/transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"

db "github.com/ashiqYousuf/sbank/db/sqlc"
"github.com/ashiqYousuf/sbank/token"
"github.com/gin-gonic/gin"
)

Expand All @@ -17,6 +18,8 @@ type transferRequest struct {
Currency string `json:"currency" binding:"required,currency"`
}

// Authorization Rule: A Logged In user can only send money
// from his own account
func (server *Server) createTransfer(ctx *gin.Context) {
var req transferRequest

Expand All @@ -25,11 +28,20 @@ func (server *Server) createTransfer(ctx *gin.Context) {
return
}

if !server.validAccount(ctx, req.FromAccountID, req.Currency) {
fromAccount, valid := server.validAccount(ctx, req.FromAccountID, req.Currency)
if !valid {
return
}

if !server.validAccount(ctx, req.ToAccountID, req.Currency) {
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
if fromAccount.Owner != authPayload.Username {
err := errors.New("from account doesn't belong to the authenticated user")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}

_, valid = server.validAccount(ctx, req.ToAccountID, req.Currency)
if !valid {
return
}

Expand All @@ -49,22 +61,22 @@ func (server *Server) createTransfer(ctx *gin.Context) {
}

// Checks for currency mismatch
func (server *Server) validAccount(ctx *gin.Context, accountID int64, currency string) bool {
func (server *Server) validAccount(ctx *gin.Context, accountID int64, currency string) (db.Account, bool) {
account, err := server.store.GetAccount(ctx, accountID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
ctx.JSON(http.StatusNotFound, errorResponse(err))
} else {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
}
return false
return account, false
}

if account.Currency != currency {
err := fmt.Errorf("account [%d] currency mismatch: %s vs %s", accountID, account.Currency, currency)
ctx.JSON(http.StatusBadRequest, errorResponse(err))
return false
return account, false
}

return true
return account, true
}
3 changes: 2 additions & 1 deletion db/query/account.sql
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ FOR NO KEY UPDATE;

-- name: ListAccounts :many
SELECT * FROM accounts
WHERE owner = $1
ORDER BY id
LIMIT $1 OFFSET $2;
LIMIT $2 OFFSET $3;

-- name: UpdateAccount :one
UPDATE accounts
Expand Down
10 changes: 6 additions & 4 deletions db/sqlc/account.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions db/sqlc/account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,20 +82,23 @@ func TestDeleteAccount(t *testing.T) {
}

func TestListAccounts(t *testing.T) {
var lastAccount Account
for i := 0; i < 10; i++ {
createRandomAccount(t)
lastAccount = createRandomAccount(t)
}

arg := ListAccountsParams{
Owner: lastAccount.Owner,
Limit: 5,
Offset: 5,
Offset: 0,
}

accounts, err := testQueries.ListAccounts(context.Background(), arg)
require.NoError(t, err)
require.Len(t, accounts, 5)
require.NotEmpty(t, accounts)

for _, account := range accounts {
require.NotEmpty(t, account)
require.Equal(t, lastAccount.Owner, account.Owner)
}
}

0 comments on commit 08e18ea

Please sign in to comment.