diff --git a/app/controllers/authorize.go b/app/controllers/authorize.go index ad0ffea..f06c47a 100644 --- a/app/controllers/authorize.go +++ b/app/controllers/authorize.go @@ -1,7 +1,9 @@ package controllers import ( + "git.secnex.io/secnex/masterlog" "git.secnex.io/secnex/oauth2-api/services" + "git.secnex.io/secnex/oauth2-api/utils" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" ) @@ -15,15 +17,21 @@ type AuthorizeRequest struct { } func AuthorizeController(c *fiber.Ctx) error { + masterlog.Debug("Authorize request received", map[string]interface{}{"path": c.Path()}) var request AuthorizeRequest if err := c.BodyParser(&request); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + masterlog.Debug("Failed to parse request", map[string]interface{}{"error": err.Error()}) + return utils.NewHTTPResponse(fiber.StatusBadRequest, &fiber.Map{"error": err.Error()}, "", nil, nil).Send(c) } if err := validator.New().Struct(request); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + masterlog.Debug("Failed to validate request", map[string]interface{}{"error": err.Error()}) + return utils.NewHTTPResponse(fiber.StatusBadRequest, &fiber.Map{"error": err.Error()}, "", nil, nil).Send(c) } + masterlog.Debug("Authorize request validated", map[string]interface{}{"path": c.Path()}) + response := services.Authorize(c.Locals("user").(string), request.ClientID, request.RedirectURI, request.ResponseType, request.Scope, request.State) + masterlog.Debug("Authorize response sent", map[string]interface{}{"path": c.Path()}) return response.Send(c) } diff --git a/app/controllers/token.go b/app/controllers/token.go new file mode 100644 index 0000000..059443e --- /dev/null +++ b/app/controllers/token.go @@ -0,0 +1,37 @@ +package controllers + +import ( + "git.secnex.io/secnex/masterlog" + "git.secnex.io/secnex/oauth2-api/services" + "git.secnex.io/secnex/oauth2-api/utils" + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" +) + +type TokenRequest struct { + ClientID string `json:"client_id" validate:"required"` + GrantType string `json:"grant_type" validate:"required"` + Code string `json:"code" validate:"required"` + RedirectURI string `json:"redirect_uri" validate:"required"` + ClientSecret string `json:"client_secret" validate:"required"` +} + +func TokenController(c *fiber.Ctx) error { + masterlog.Debug("Token request received", map[string]interface{}{"path": c.Path()}) + var request TokenRequest + if err := c.BodyParser(&request); err != nil { + masterlog.Debug("Failed to parse request", map[string]interface{}{"error": err.Error()}) + return utils.NewHTTPResponse(fiber.StatusBadRequest, &fiber.Map{"error": err.Error()}, "", nil, nil).Send(c) + } + + if err := validator.New().Struct(request); err != nil { + masterlog.Debug("Failed to validate request", map[string]interface{}{"error": err.Error()}) + return utils.NewHTTPResponse(fiber.StatusBadRequest, &fiber.Map{"error": err.Error()}, "", nil, nil).Send(c) + } + + masterlog.Debug("Token request validated", map[string]interface{}{"path": c.Path()}) + + response := services.Token(request.ClientID, request.GrantType, request.Code, request.RedirectURI, request.ClientSecret) + masterlog.Debug("Token response sent", map[string]interface{}{"path": c.Path()}) + return response.Send(c) +} diff --git a/app/controllers/userinfo.go b/app/controllers/userinfo.go new file mode 100644 index 0000000..622e7fe --- /dev/null +++ b/app/controllers/userinfo.go @@ -0,0 +1,12 @@ +package controllers + +import ( + "git.secnex.io/secnex/masterlog" + "git.secnex.io/secnex/oauth2-api/utils" + "github.com/gofiber/fiber/v2" +) + +func UserinfoController(c *fiber.Ctx) error { + masterlog.Debug("Userinfo request received", map[string]interface{}{"path": c.Path()}) + return utils.NewHTTPResponse(fiber.StatusOK, &fiber.Map{"message": "Userinfo request received"}, "", nil, nil).Send(c) +} diff --git a/app/go.mod b/app/go.mod index c9332d1..dcd9e4e 100644 --- a/app/go.mod +++ b/app/go.mod @@ -6,6 +6,7 @@ require ( git.secnex.io/secnex/masterlog v0.1.0 github.com/go-playground/validator/v10 v10.30.1 github.com/gofiber/fiber/v2 v2.52.10 + github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.6.0 github.com/valkey-io/valkey-go v1.0.70 golang.org/x/crypto v0.46.0 diff --git a/app/go.sum b/app/go.sum index 94fb696..733af9b 100644 --- a/app/go.sum +++ b/app/go.sum @@ -17,6 +17,8 @@ github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy0 github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY= github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= diff --git a/app/main.go b/app/main.go index 7c01e7d..5f0b9d8 100644 --- a/app/main.go +++ b/app/main.go @@ -31,6 +31,10 @@ func main() { allModels := []interface{}{ &models.User{}, &models.Tenant{}, + &models.Token{}, + &models.Session{}, + &models.Application{}, + &models.Authorization{}, } dbConfig := database.NewDatabaseConfigurationFromConfig(config) @@ -62,6 +66,7 @@ func main() { // Controllers app.Post("/authorize", controllers.AuthorizeController) + app.Post("/token", controllers.TokenController) masterlog.Info("Starting server", map[string]interface{}{"address": config.Address}) if err := app.Listen(config.Address); err != nil { diff --git a/app/middlewares/auth.go b/app/middlewares/auth.go index dd8b440..453880e 100644 --- a/app/middlewares/auth.go +++ b/app/middlewares/auth.go @@ -6,7 +6,10 @@ import ( "git.secnex.io/secnex/masterlog" "git.secnex.io/secnex/oauth2-api/config" + "git.secnex.io/secnex/oauth2-api/repositories" + "git.secnex.io/secnex/oauth2-api/utils" "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" ) func AuthMiddleware() fiber.Handler { @@ -15,24 +18,52 @@ func AuthMiddleware() fiber.Handler { masterlog.Debug("Unprotected endpoint", map[string]interface{}{"path": c.Path()}) return c.Next() } - token := c.Get("Authorization") - if token == "" { + authHeader := c.Get("Authorization") + if authHeader == "" { masterlog.Debug("No token provided", map[string]interface{}{"path": c.Path(), "authorization": c.Get("Authorization")}) - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "Unauthorized"}) + return utils.NewHTTPResponse(fiber.StatusUnauthorized, &fiber.Map{"message": "Unauthorized"}, "", nil, nil).Send(c) } - tokenParts := strings.Split(token, " ") + tokenParts := strings.Split(authHeader, " ") if len(tokenParts) != 2 { masterlog.Debug("Invalid token parts", map[string]interface{}{"token_parts": tokenParts}) - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "Unauthorized"}) + return utils.NewHTTPResponse(fiber.StatusUnauthorized, &fiber.Map{"message": "Unauthorized"}, "", nil, nil).Send(c) } - tokenPartType, _ := tokenParts[0], tokenParts[1] + tokenPartType := tokenParts[0] + tokenString := tokenParts[1] + if tokenPartType != "Bearer" { masterlog.Debug("Invalid token type", map[string]interface{}{"token_type": tokenPartType}) - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "Unauthorized"}) + return utils.NewHTTPResponse(fiber.StatusUnauthorized, &fiber.Map{"message": "Unauthorized"}, "", nil, nil).Send(c) } + if tokenString == "" { + masterlog.Debug("Empty token string", map[string]interface{}{}) + return utils.NewHTTPResponse(fiber.StatusUnauthorized, &fiber.Map{"message": "Unauthorized"}, "", nil, nil).Send(c) + } + + masterlog.Debug("Token string", map[string]interface{}{"token_string": tokenString}) + + // Validate jwt token and get claims + claims, err := jwt.ParseWithClaims(tokenString, &jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(config.CONFIG.JwtSecret), nil + }) + if err != nil { + masterlog.Debug("Invalid token", map[string]interface{}{"error": err.Error(), "token_string": tokenString}) + return utils.NewHTTPResponse(fiber.StatusUnauthorized, &fiber.Map{"message": "Unauthorized"}, "", nil, nil).Send(c) + } + + claimsMap := claims.Claims.(*jwt.MapClaims) + sessionID := (*claimsMap)["sub"].(string) + + session := repositories.GetSessionCache(sessionID) + if session == nil { + masterlog.Debug("Session not found", map[string]interface{}{"session_id": sessionID}) + return utils.NewHTTPResponse(fiber.StatusUnauthorized, &fiber.Map{"message": "Unauthorized"}, "", nil, nil).Send(c) + } + + c.Locals("user", session.UserID.String()) return c.Next() } } diff --git a/app/models/authorization.go b/app/models/authorization.go index f7b99c9..b93ab33 100644 --- a/app/models/authorization.go +++ b/app/models/authorization.go @@ -33,7 +33,7 @@ func (authorization *Authorization) BeforeCreate(tx *gorm.DB) (err error) { } authorization.Code = codeHash if authorization.ExpiresAt == nil { - expiresAt := time.Now().Add(time.Minute * 10) + expiresAt := time.Now().Add(time.Minute * 2) authorization.ExpiresAt = &expiresAt } return nil diff --git a/app/models/session.go b/app/models/session.go new file mode 100644 index 0000000..da2075d --- /dev/null +++ b/app/models/session.go @@ -0,0 +1,22 @@ +package models + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type Session struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` + + User *User `gorm:"foreignKey:UserID" json:"user"` +} + +func (Session) TableName() string { + return "sessions" +} diff --git a/app/models/token.go b/app/models/token.go new file mode 100644 index 0000000..d8ddab9 --- /dev/null +++ b/app/models/token.go @@ -0,0 +1,43 @@ +package models + +import ( + "time" + + "git.secnex.io/secnex/oauth2-api/utils" + "github.com/google/uuid" + "gorm.io/gorm" +) + +type Token struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"` + RefreshToken string `gorm:"not null" json:"refresh_token"` + SessionExpiresAt *time.Time `gorm:"not null" json:"session_expires_at"` + RefreshTokenExpiresAt *time.Time `gorm:"not null" json:"refresh_token_expires_at"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` + + User *User `gorm:"foreignKey:UserID" json:"user"` +} + +func (Token) TableName() string { + return "tokens" +} + +func (token *Token) BeforeCreate(tx *gorm.DB) (err error) { + refreshTokenHash, err := utils.Hash(token.RefreshToken) + if err != nil { + return err + } + token.RefreshToken = refreshTokenHash + if token.SessionExpiresAt == nil { + sessionExpiresAt := time.Now().Add(time.Hour * 24) + token.SessionExpiresAt = &sessionExpiresAt + } + if token.RefreshTokenExpiresAt == nil { + refreshTokenExpiresAt := time.Now().Add(time.Hour * 24 * 30) + token.RefreshTokenExpiresAt = &refreshTokenExpiresAt + } + return nil +} diff --git a/app/models/users.go b/app/models/user.go similarity index 82% rename from app/models/users.go rename to app/models/user.go index d7d3795..be598d5 100644 --- a/app/models/users.go +++ b/app/models/user.go @@ -22,7 +22,11 @@ type User struct { UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` - Tenant *Tenant `gorm:"foreignKey:TenantID" json:"tenant"` + Tenant *Tenant `gorm:"foreignKey:TenantID"` + + Authorizations []Authorization `gorm:"foreignKey:UserID" json:"-"` + Sessions []Session `gorm:"foreignKey:UserID" json:"-"` + Tokens []Token `gorm:"foreignKey:UserID" json:"-"` } func (User) TableName() string { diff --git a/app/repositories/applications.go b/app/repositories/applications.go index b56f2e9..79864e6 100644 --- a/app/repositories/applications.go +++ b/app/repositories/applications.go @@ -7,9 +7,9 @@ import ( "git.secnex.io/secnex/oauth2-api/models" ) -func GetApplicationByClientID(clientID string) (*models.Application, error) { +func GetApplicationByID(applicationID string) (*models.Application, error) { var application *models.Application - if err := database.DB.Where("client_id = ? AND expires_at > ?", clientID, time.Now().UTC()).First(&application).Error; err != nil { + if err := database.DB.Where("id = ? AND expires_at > ?", applicationID, time.Now().UTC()).First(&application).Error; err != nil { return nil, err } return application, nil diff --git a/app/repositories/authorization.go b/app/repositories/authorization.go index 29b86b4..7936b6a 100644 --- a/app/repositories/authorization.go +++ b/app/repositories/authorization.go @@ -1,10 +1,24 @@ package repositories import ( + "time" + "git.secnex.io/secnex/oauth2-api/database" "git.secnex.io/secnex/oauth2-api/models" ) +func GetAuthorizationByID(id string) (*models.Authorization, error) { + var authorization *models.Authorization + if err := database.DB.Where("id = ? AND expires_at > ?", id, time.Now().UTC()).First(&authorization).Error; err != nil { + return nil, err + } + return authorization, nil +} + func CreateAuthorization(authorization *models.Authorization) error { return database.DB.Create(authorization).Error } + +func ExpireAuthorization(authorizationID string) error { + return database.DB.Model(&models.Authorization{}).Where("id = ?", authorizationID).Update("expires_at", time.Now().UTC()).Error +} diff --git a/app/repositories/session.go b/app/repositories/session.go new file mode 100644 index 0000000..0f4c1f5 --- /dev/null +++ b/app/repositories/session.go @@ -0,0 +1,62 @@ +package repositories + +import ( + "encoding/json" + + "git.secnex.io/secnex/masterlog" + "git.secnex.io/secnex/oauth2-api/cache" + "github.com/google/uuid" +) + +type SessionDetails struct { + UserID uuid.UUID `json:"user_id"` + Username string `json:"username"` + Email string `json:"email"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` +} + +func GetSessionCache(sessionID string) *SessionDetails { + masterlog.Debug("Retrieving session from cache", map[string]interface{}{"session_id": sessionID}) + if cache.Cache.Client == nil { + masterlog.Debug("Redis client not initialized", map[string]interface{}{"session_id": sessionID}) + return nil + } + + res := cache.Cache.Client.Do(cache.Cache.Context, cache.Cache.Client.B().Get().Key(sessionID).Build()) + if res.Error() != nil { + masterlog.Debug("Failed to get session from cache", map[string]interface{}{"error": res.Error(), "session_id": sessionID}) + return nil + } + + rawStr := res.String() + if rawStr == "" { + masterlog.Debug("Session not found in cache", map[string]interface{}{"session_id": sessionID}) + return nil + } + + // Parse the valkey response structure to extract the actual JSON string + var valkeyResponse struct { + Message struct { + Value string `json:"Value"` + Type string `json:"Type"` + } `json:"Message"` + } + if err := json.Unmarshal([]byte(rawStr), &valkeyResponse); err != nil { + // If it's not the wrapped format, use it directly + masterlog.Debug("Cache response not in wrapped format, using directly", map[string]interface{}{"session_id": sessionID}) + } else { + // Extract the actual JSON string from Message.Value + rawStr = valkeyResponse.Message.Value + masterlog.Debug("Extracted JSON from cache response", map[string]interface{}{"session_id": sessionID}) + } + + var sessionDetails SessionDetails + if err := json.Unmarshal([]byte(rawStr), &sessionDetails); err != nil { + masterlog.Debug("Failed to unmarshal session details", map[string]interface{}{"error": err.Error(), "session_id": sessionID}) + return nil + } + + masterlog.Debug("Session retrieved from cache", map[string]interface{}{"session_id": sessionID, "user_id": sessionDetails.UserID}) + return &sessionDetails +} diff --git a/app/repositories/token.go b/app/repositories/token.go new file mode 100644 index 0000000..afd7d1c --- /dev/null +++ b/app/repositories/token.go @@ -0,0 +1,10 @@ +package repositories + +import ( + "git.secnex.io/secnex/oauth2-api/database" + "git.secnex.io/secnex/oauth2-api/models" +) + +func CreateToken(token *models.Token) error { + return database.DB.Create(token).Error +} diff --git a/app/repositories/users.go b/app/repositories/users.go index 9edb73a..6a5816c 100644 --- a/app/repositories/users.go +++ b/app/repositories/users.go @@ -6,6 +6,14 @@ import ( "git.secnex.io/secnex/oauth2-api/models" ) +func GetUserByID(id string) (*models.User, error) { + var user *models.User + if err := database.DB.Where("id = ?", id).First(&user).Error; err != nil { + return nil, err + } + return user, nil +} + func GetUserByUsername(username string) (*models.User, error) { var user *models.User if err := database.DB.Where("username = ?", username).First(&user).Error; err != nil { diff --git a/app/services/authorize.go b/app/services/authorize.go index f46ba49..cf18651 100644 --- a/app/services/authorize.go +++ b/app/services/authorize.go @@ -6,6 +6,7 @@ import ( "net/http" "time" + "git.secnex.io/secnex/masterlog" "git.secnex.io/secnex/oauth2-api/models" "git.secnex.io/secnex/oauth2-api/repositories" "git.secnex.io/secnex/oauth2-api/utils" @@ -19,11 +20,13 @@ type AuthorizeResponse struct { } func Authorize(userID, clientID, redirectURI, responseType, scope, state string) *utils.HTTPResponse { - application, err := repositories.GetApplicationByClientID(clientID) + application, err := repositories.GetApplicationByID(clientID) if err != nil { + masterlog.Debug("Application not found", map[string]interface{}{"error": err.Error(), "client_id": clientID}) return utils.NewHTTPResponse(http.StatusUnauthorized, &fiber.Map{"error": "Application not found"}, "", nil, nil) } if application.ExpiresAt.Before(time.Now().UTC()) { + masterlog.Debug("Application expired", map[string]interface{}{"client_id": clientID}) return utils.NewHTTPResponse(http.StatusUnauthorized, &fiber.Map{"error": "Application expired"}, "", nil, nil) } authorizationID := uuid.New() @@ -35,6 +38,7 @@ func Authorize(userID, clientID, redirectURI, responseType, scope, state string) UserID: uuid.MustParse(userID), } if err := repositories.CreateAuthorization(authorization); err != nil { + masterlog.Debug("Failed to create authorization", map[string]interface{}{"error": err.Error(), "client_id": clientID}) return utils.NewHTTPResponse(http.StatusInternalServerError, &fiber.Map{"error": "Failed to create authorization"}, "", nil, nil) } diff --git a/app/services/token.go b/app/services/token.go new file mode 100644 index 0000000..306e68d --- /dev/null +++ b/app/services/token.go @@ -0,0 +1,141 @@ +package services + +import ( + "encoding/base64" + "net/http" + "strings" + "time" + + "git.secnex.io/secnex/masterlog" + "git.secnex.io/secnex/oauth2-api/config" + "git.secnex.io/secnex/oauth2-api/models" + "git.secnex.io/secnex/oauth2-api/repositories" + "git.secnex.io/secnex/oauth2-api/utils" + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" +) + +type TokenResponse struct { + TokenType string `json:"token_type"` + Scope string `json:"scope"` + ExpiresIn int `json:"expires_in"` + ExtExpiresIn int `json:"ext_expires_in"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + +func Token(clientID, grantType, code, redirectURI, clientSecret string) *utils.HTTPResponse { + now := time.Now().UTC() + application, err := repositories.GetApplicationByID(clientID) + if err != nil { + masterlog.Debug("Application not found", map[string]interface{}{"error": err.Error(), "client_id": clientID}) + return utils.NewHTTPResponse(http.StatusUnauthorized, &fiber.Map{"error": "Application not found"}, "", nil, nil) + } + if application.ExpiresAt.Before(time.Now().UTC()) { + masterlog.Debug("Application expired", map[string]interface{}{"client_id": clientID}) + return utils.NewHTTPResponse(http.StatusUnauthorized, &fiber.Map{"error": "Application expired"}, "", nil, nil) + } + valid, err := utils.Verify(clientSecret, application.Secret) + if err != nil { + masterlog.Debug("Invalid client secret", map[string]interface{}{"error": err.Error(), "client_id": clientID}) + return utils.NewHTTPResponse(http.StatusUnauthorized, &fiber.Map{"error": "Invalid client secret"}, "", nil, nil) + } + if !valid { + masterlog.Debug("Invalid client secret", map[string]interface{}{"client_id": clientID}) + return utils.NewHTTPResponse(http.StatusUnauthorized, &fiber.Map{"error": "Invalid client secret"}, "", nil, nil) + } + + authorizationCodePlain, err := base64.StdEncoding.DecodeString(code) + if err != nil { + masterlog.Debug("Invalid authorization code", map[string]interface{}{"error": err.Error(), "client_id": clientID}) + return utils.NewHTTPResponse(http.StatusUnauthorized, &fiber.Map{"error": "Invalid authorization code"}, "", nil, nil) + } + authorizationCodeParts := strings.Split(string(authorizationCodePlain), ":") + if len(authorizationCodeParts) != 2 { + masterlog.Debug("Invalid authorization code", map[string]interface{}{"client_id": clientID}) + return utils.NewHTTPResponse(http.StatusUnauthorized, &fiber.Map{"error": "Invalid authorization code"}, "", nil, nil) + } + authorizationID := uuid.MustParse(authorizationCodeParts[0]) + authorizationCode := authorizationCodeParts[1] + authorization, err := repositories.GetAuthorizationByID(authorizationID.String()) + if err != nil { + masterlog.Debug("Authorization not found", map[string]interface{}{"error": err.Error(), "client_id": clientID}) + return utils.NewHTTPResponse(http.StatusUnauthorized, &fiber.Map{"error": "Authorization not found"}, "", nil, nil) + } + codeValid, err := utils.Verify(authorizationCode, authorization.Code) + if err != nil { + masterlog.Debug("Invalid authorization code", map[string]interface{}{"error": err.Error(), "client_id": clientID}) + return utils.NewHTTPResponse(http.StatusUnauthorized, &fiber.Map{"error": "Invalid authorization code"}, "", nil, nil) + } + if !codeValid { + masterlog.Debug("Invalid authorization code", map[string]interface{}{"client_id": clientID}) + return utils.NewHTTPResponse(http.StatusUnauthorized, &fiber.Map{"error": "Invalid authorization code"}, "", nil, nil) + } + + tokenID := uuid.New() + refreshToken := utils.GenerateRandomString(64) + token := &models.Token{ + ID: tokenID, + RefreshToken: refreshToken, + UserID: authorization.UserID, + } + if err := repositories.CreateToken(token); err != nil { + masterlog.Debug("Failed to create token", map[string]interface{}{"error": err.Error(), "client_id": clientID}) + return utils.NewHTTPResponse(http.StatusInternalServerError, &fiber.Map{"error": "Failed to create token"}, "", nil, nil) + } + + if err := repositories.ExpireAuthorization(authorizationID.String()); err != nil { + masterlog.Debug("Failed to expire authorization", map[string]interface{}{"error": err.Error(), "client_id": clientID}) + return utils.NewHTTPResponse(http.StatusInternalServerError, &fiber.Map{"error": "Failed to expire authorization"}, "", nil, nil) + } + + user, err := repositories.GetUserByID(authorization.UserID.String()) + if err != nil { + masterlog.Debug("Failed to get user", map[string]interface{}{"error": err.Error(), "client_id": clientID}) + return utils.NewHTTPResponse(http.StatusInternalServerError, &fiber.Map{"error": "Failed to get user"}, "", nil, nil) + } + + userClaims := map[string]interface{}{ + "id": user.ID.String(), + "username": user.Username, + "email": user.Email, + "first_name": user.FirstName, + "last_name": user.LastName, + } + if user.TenantID != nil { + userClaims["tenant_id"] = user.TenantID.String() + } + if user.ExternalID != nil { + userClaims["external_id"] = user.ExternalID.String() + } + + accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "sub": tokenID.String(), + "exp": token.SessionExpiresAt.Unix(), + "iat": now.Unix(), + "aud": clientID, + "iss": "https://secnex.io", + "user": userClaims, + }) + + secret := config.CONFIG.JwtSecret + accessTokenString, err := accessToken.SignedString([]byte(secret)) + if err != nil { + masterlog.Debug("Failed to sign access token", map[string]interface{}{"error": err.Error(), "client_id": clientID}) + return utils.NewHTTPResponse(http.StatusInternalServerError, &fiber.Map{"error": "Failed to sign access token"}, "", nil, nil) + } + + tokenExpiresAt := token.SessionExpiresAt.Unix() + extTokenExpiresAt := token.RefreshTokenExpiresAt.Unix() + response := TokenResponse{ + TokenType: "Bearer", + Scope: "", + ExpiresIn: int(tokenExpiresAt - now.Unix()), + ExtExpiresIn: int(extTokenExpiresAt - now.Unix()), + AccessToken: accessTokenString, + RefreshToken: refreshToken, + } + masterlog.Debug("Token created successfully", map[string]interface{}{"client_id": clientID}) + return utils.NewHTTPResponse(http.StatusOK, &fiber.Map{"response": response}, "", nil, nil) +} diff --git a/app/services/userinfo.go b/app/services/userinfo.go new file mode 100644 index 0000000..e69de29