Skip to content

Commit

Permalink
feature(localauth) middleware with context
Browse files Browse the repository at this point in the history
  • Loading branch information
mtmr0x committed Sep 12, 2024
1 parent b6c6e85 commit 9968c40
Show file tree
Hide file tree
Showing 17 changed files with 181 additions and 46 deletions.
12 changes: 8 additions & 4 deletions gateway/api/localauth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import (
)

type Claims struct {
UserID string `json:"user_id"`
UserID string `json:"user_id"`
UserEmail string `json:"user_email"`
jwt.RegisteredClaims
}

Expand All @@ -40,7 +41,8 @@ func Login(c *gin.Context) {

expirationTime := time.Now().Add(168 * time.Hour) // 7 days
claims := &Claims{
UserID: dbUser.ID,
UserID: dbUser.ID,
UserEmail: dbUser.Email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
},
Expand All @@ -56,16 +58,18 @@ func Login(c *gin.Context) {
newLocalAuthSession := pgrest.LocalAuthSession{
ID: uuid.New().String(),
UserID: dbUser.ID,
UserEmail: dbUser.Email,
Token: tokenString,
ExpiresAt: expirationTime,
ExpiresAt: expirationTime.Format(time.RFC3339),
}
_, err = pglocalauthsession.CreateSession(newLocalAuthSession)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to store session"})
return
}

c.Header("token", tokenString)
c.Header("Access-Control-Expose-Headers", "Token")
c.Header("Token", tokenString)

c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
42 changes: 41 additions & 1 deletion gateway/api/localauth/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import (
"fmt"
"libhoop/log"
"net/http"
"time"

"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/hoophq/hoop/gateway/appconfig"
"github.com/hoophq/hoop/gateway/pgrest"
pglocalauthsession "github.com/hoophq/hoop/gateway/pgrest/localauthsession"
pgorgs "github.com/hoophq/hoop/gateway/pgrest/orgs"
pgusers "github.com/hoophq/hoop/gateway/pgrest/users"
"github.com/hoophq/hoop/gateway/storagev2/types"
Expand All @@ -22,6 +26,7 @@ func Register(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
fmt.Printf("Registering user %+v", user)

log.Debugf("looking for existing user %v", user.Email)
// fetch user by email
Expand All @@ -43,14 +48,17 @@ func Register(c *gin.Context) {
}

hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
log.Debugf("hashed password %v", string(hashedPassword))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
return
}

adminGroupName := types.GroupAdmin
userID := uuid.New().String()
err = pgusers.New().Upsert(pgrest.User{
ID: uuid.New().String(),
ID: userID,
Subject: fmt.Sprintf("local|%v", userID),
OrgID: newOrgID,
Email: user.Email,
Status: "active",
Expand All @@ -59,11 +67,43 @@ func Register(c *gin.Context) {
})

if err != nil {
fmt.Printf("failed creating user, err=%v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
return
}

expirationTime := time.Now().Add(168 * time.Hour) // 7 days
claims := &Claims{
UserID: userID,
UserEmail: user.Email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
},
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
fmt.Printf("token %+v", token)
tokenString, err := token.SignedString(appconfig.Get().JWTSecretKey())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
return
}

newLocalAuthSession := pgrest.LocalAuthSession{
ID: uuid.New().String(),
UserID: userID,
UserEmail: user.Email,
Token: tokenString,
ExpiresAt: expirationTime.Format(time.RFC3339),
}
_, err = pglocalauthsession.CreateSession(newLocalAuthSession)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to store session"})
return
}

// TODO: generate token and return it
c.Header("token", tokenString)

c.JSON(http.StatusCreated, gin.H{"message": "User created successfully"})
}
71 changes: 64 additions & 7 deletions gateway/api/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"

Expand All @@ -31,14 +32,18 @@ import (

var errInvalidAuthHeaderErr = errors.New("invalid authorization header")

func localAuthMiddleware(c *gin.Context) {
func (a *Api) localAuthMiddleware(c *gin.Context) {
tokenString := c.GetHeader("Authorization")
fmt.Printf("tokenString: %v\n", tokenString)
// remove bearer from token
tokenString = strings.Replace(tokenString, "Bearer ", "", 1)
if tokenString == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing authorization token"})
c.Abort()
return
}

fmt.Printf("tokenString 2: %v\n", tokenString)
jwtKey := appconfig.Get().JWTSecretKey()
claims := &localauthapi.Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
Expand All @@ -51,19 +56,71 @@ func localAuthMiddleware(c *gin.Context) {
return
}

sessionByToken, err := pglocalauthsession.GetSessionByToken("")
fmt.Printf("sessionByToken: %+v\n", sessionByToken)
sessionByToken, err := pglocalauthsession.GetSessionByToken(tokenString)
fmt.Printf("sessionByToken: %+v %v\n", sessionByToken, err)
if err != nil {
fmt.Printf("err: %v\n", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired session"})
return
}

if err != nil || time.Now().After(sessionByToken.ExpiresAt) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired session"})
// TODO change ExpiresAt at the database for date with timezone
sessionExpiresAt, err := time.Parse("2006-01-02T15:04:05", sessionByToken.ExpiresAt)

if err != nil {
fmt.Printf("Error parsing expiration time: %v\n", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error processing session"})
c.Abort()
return
}
if time.Now().After(sessionExpiresAt) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Session has expired"})
c.Abort()
return
}

c.Set("user_id", claims.UserID)
user, err := pgusers.GetOneByEmail(sessionByToken.UserID)
if err != nil {
log.Errorf("failed fetching user, subject=%v, err=%v", user.Subject, err)
c.AbortWithStatus(http.StatusUnauthorized)
return
}
ctx, err := pguserauth.New().FetchUserContext(user.Subject)
if err != nil {
log.Errorf("failed building context, subject=%v, err=%v", user.Subject, err)
c.AbortWithStatus(http.StatusUnauthorized)
return
}

if a.logger != nil && !ctx.IsEmpty() {
zaplogger := a.logger.With(
zap.String("org", ctx.OrgName),
zap.String("user", ctx.UserEmail),
zap.Bool("isadm", ctx.IsAdmin()),
)
c.Set(pgusers.ContextLoggerKey, zaplogger.Sugar())
}

grpcURL := os.Getenv("GRPC_URL")
if grpcURL == "" {
scheme := "grpcs"
if appconfig.Get().ApiScheme() == "http" {
scheme = "grpc"
}
grpcURL = fmt.Sprintf("%s://%s:8443", scheme, appconfig.Get().ApiHostname())
}

c.Set(storagev2.ContextKey,
storagev2.NewContext(ctx.UserSubject, ctx.OrgID).
WithUserInfo(ctx.UserName, ctx.UserEmail, string(ctx.UserStatus), ctx.UserPicture, ctx.UserGroups).
WithSlackID(ctx.UserSlackID).
WithOrgName(ctx.OrgName).
WithOrgLicense(ctx.OrgLicense).
// WithApiURL(a.IDProvider.ApiURL).
WithGrpcURL(grpcURL),
)
fmt.Printf("end of localAuthMiddleware\n")
// c.Set("user_id", claims.UserID)
c.Next()
}

Expand All @@ -72,7 +129,7 @@ func (a *Api) Authenticate(c *gin.Context) {
switch authMethod {
case "local":
fmt.Println("local")
localAuthMiddleware(c)
a.localAuthMiddleware(c)
default:
roleName := RoleFromContext(c)
fmt.Printf("roleName: %v\n", roleName)
Expand Down
1 change: 1 addition & 0 deletions gateway/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ func (api *Api) buildRoutes(route *gin.RouterGroup) {
///////////////////////
// local auth routes //
///////////////////////
// TODO do not allow these routes be called if auth is not set as local
route.POST("/localauth/register", localauthapi.Register)
route.POST("/localauth/login", localauthapi.Login)
///////////////////////////
Expand Down
2 changes: 1 addition & 1 deletion gateway/pgrest/current_state.template.sql
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ CREATE VIEW serviceaccounts AS
SELECT id, org_id, subject, name, status, created_at, updated_at FROM private.service_accounts;

CREATE VIEW local_auth_sessions AS
SELECT id, token, user_id, expires_at FROM private.local_auth_sessions;
SELECT id, token, user_id, user_email, expires_at FROM private.local_auth_sessions;

CREATE FUNCTION groups(serviceaccounts) RETURNS TEXT[] AS $$
SELECT ARRAY(
Expand Down
6 changes: 5 additions & 1 deletion gateway/pgrest/localauthsession/localauthsession.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package pglocalauthsession

import (
"fmt"
"net/url"

"github.com/google/uuid"
"github.com/hoophq/hoop/gateway/pgrest"
)
Expand All @@ -14,7 +17,8 @@ func CreateSession(authSession pgrest.LocalAuthSession) (string, error) {

func GetSessionByToken(sessionToken string) (*pgrest.LocalAuthSession, error) {
var sess pgrest.LocalAuthSession
err := pgrest.New("/local_auth_sessions?token=eq.%s", sessionToken).
fmt.Printf("GetSessionByToken sessionToken: %v\n", sessionToken)
err := pgrest.New("/local_auth_sessions?token=eq.%v", url.QueryEscape(sessionToken)).
FetchOne().
DecodeInto(&sess)
if err != nil {
Expand Down
3 changes: 1 addition & 2 deletions gateway/pgrest/orgs/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ func CreateDefaultOrganization() (pgrest.OrgContext, error) {
return nil, fmt.Errorf("failed creating the default organization, err=%v", err)
}
return pgrest.NewOrgContext(orgID), nil
case len(orgList) == 1:
default:
return pgrest.NewOrgContext(orgList[0].ID), nil
}
return nil, fmt.Errorf("multiple organizations were found")
}
10 changes: 6 additions & 4 deletions gateway/pgrest/orgs/orgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package pgorgs
import (
"encoding/json"
"fmt"
"libhoop/log"
"net/url"

"github.com/google/uuid"
Expand Down Expand Up @@ -55,10 +56,11 @@ func (o *org) CreateOrGetOrg(name string, licenseDataJSON []byte) (orgID string,
return org.ID, nil
}
orgID = uuid.NewString()
licenseData, err := decodeLicenseToMap(licenseDataJSON)
if err != nil {
return "", fmt.Errorf("unable to encode license data properly: %v", err)
}
licenseData, _ := decodeLicenseToMap(licenseDataJSON)
log.Debugf("licenseData: %v", licenseData)
// if err != nil {
// return "", fmt.Errorf("unable to encode license data properly: %v", err)
// }
return orgID, pgrest.New("/orgs").
Create(map[string]any{"id": orgID, "name": name, "license_data": licenseData}).
Error()
Expand Down
10 changes: 5 additions & 5 deletions gateway/pgrest/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package pgrest

import (
"encoding/json"
"time"
)

type Context interface {
Expand Down Expand Up @@ -62,10 +61,11 @@ type User struct {
}

type LocalAuthSession struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
ID string `json:"id"`
UserID string `json:"user_id"`
UserEmail string `json:"user_email"`
Token string `json:"token"`
ExpiresAt string `json:"expires_at"`
}

type EnvVar struct {
Expand Down
4 changes: 2 additions & 2 deletions gateway/pgrest/users/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,21 @@ func GetOneByEmail(email string) (*pgrest.User, error) {
}
return nil, err
}
fmt.Printf("\n\n>>>>user: %+v\n\n\n", usr)
return &usr, nil
}

func New() *user { return &user{} }

func (u *user) Upsert(v pgrest.User) (err error) {
return pgrest.New("/rpc/update_users?select=id,org_id,subject,email,name,picture,verified,status,slack_id,created_at,updated_at,groups").
return pgrest.New("/rpc/update_users?select=id,org_id,subject,email,password,name,picture,verified,status,slack_id,created_at,updated_at,groups").
RpcCreate(map[string]any{
"id": v.ID,
"subject": v.Subject,
"org_id": v.OrgID,
"name": v.Name,
"picture": v.Picture,
"email": v.Email,
"password": v.Password,
"verified": v.Verified,
"status": v.Status,
"slack_id": v.SlackID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ ALTER TABLE users ADD COLUMN password TEXT;
CREATE TABLE local_auth_sessions (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
user_email TEXT NOT NULL,
token TEXT,
expires_at TIMESTAMP NOT NULL
expires_at TIMESTAMP WITH TIME ZONE NOT NULL
);

COMMIT;
4 changes: 2 additions & 2 deletions webapp/src/webapp/app.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
destiny (if error :login-hoop :home)]
(.removeItem js/localStorage "login_error")
(when error (.setItem js/localStorage "login_error" error))
(.setItem js/localStorage "jwt-token" token)
(.setItem js/sessionStorage "jwt-token" token)
(if (nil? redirect-after-auth)
(rf/dispatch [:navigate destiny])
(let [_ (.replace (. js/window -location) redirect-after-auth)
Expand All @@ -94,7 +94,7 @@
destiny (if error :login-hoop :signup-hoop)]
(.removeItem js/localStorage "login_error")
(when error (.setItem js/localStorage "login_error" error))
(.setItem js/localStorage "jwt-token" token)
(.setItem js/sessionStorage "jwt-token" token)
(rf/dispatch [:navigate destiny])

[:div "Verifying authentication"
Expand Down
Loading

0 comments on commit 9968c40

Please sign in to comment.