feat(auth): Add /token endpoint to request a access token

This commit is contained in:
Björn Benouarets
2026-01-27 11:19:52 +01:00
parent 346100feb6
commit d8241a2491
19 changed files with 418 additions and 14 deletions

View File

@@ -1,7 +1,9 @@
package controllers package controllers
import ( import (
"git.secnex.io/secnex/masterlog"
"git.secnex.io/secnex/oauth2-api/services" "git.secnex.io/secnex/oauth2-api/services"
"git.secnex.io/secnex/oauth2-api/utils"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@@ -15,15 +17,21 @@ type AuthorizeRequest struct {
} }
func AuthorizeController(c *fiber.Ctx) error { func AuthorizeController(c *fiber.Ctx) error {
masterlog.Debug("Authorize request received", map[string]interface{}{"path": c.Path()})
var request AuthorizeRequest var request AuthorizeRequest
if err := c.BodyParser(&request); err != nil { 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 { 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) 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) return response.Send(c)
} }

37
app/controllers/token.go Normal file
View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -6,6 +6,7 @@ require (
git.secnex.io/secnex/masterlog v0.1.0 git.secnex.io/secnex/masterlog v0.1.0
github.com/go-playground/validator/v10 v10.30.1 github.com/go-playground/validator/v10 v10.30.1
github.com/gofiber/fiber/v2 v2.52.10 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/google/uuid v1.6.0
github.com/valkey-io/valkey-go v1.0.70 github.com/valkey-io/valkey-go v1.0.70
golang.org/x/crypto v0.46.0 golang.org/x/crypto v0.46.0

View File

@@ -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/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 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY=
github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= 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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=

View File

@@ -31,6 +31,10 @@ func main() {
allModels := []interface{}{ allModels := []interface{}{
&models.User{}, &models.User{},
&models.Tenant{}, &models.Tenant{},
&models.Token{},
&models.Session{},
&models.Application{},
&models.Authorization{},
} }
dbConfig := database.NewDatabaseConfigurationFromConfig(config) dbConfig := database.NewDatabaseConfigurationFromConfig(config)
@@ -62,6 +66,7 @@ func main() {
// Controllers // Controllers
app.Post("/authorize", controllers.AuthorizeController) app.Post("/authorize", controllers.AuthorizeController)
app.Post("/token", controllers.TokenController)
masterlog.Info("Starting server", map[string]interface{}{"address": config.Address}) masterlog.Info("Starting server", map[string]interface{}{"address": config.Address})
if err := app.Listen(config.Address); err != nil { if err := app.Listen(config.Address); err != nil {

View File

@@ -6,7 +6,10 @@ import (
"git.secnex.io/secnex/masterlog" "git.secnex.io/secnex/masterlog"
"git.secnex.io/secnex/oauth2-api/config" "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/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
) )
func AuthMiddleware() fiber.Handler { func AuthMiddleware() fiber.Handler {
@@ -15,24 +18,52 @@ func AuthMiddleware() fiber.Handler {
masterlog.Debug("Unprotected endpoint", map[string]interface{}{"path": c.Path()}) masterlog.Debug("Unprotected endpoint", map[string]interface{}{"path": c.Path()})
return c.Next() return c.Next()
} }
token := c.Get("Authorization") authHeader := c.Get("Authorization")
if token == "" { if authHeader == "" {
masterlog.Debug("No token provided", map[string]interface{}{"path": c.Path(), "authorization": c.Get("Authorization")}) 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 { if len(tokenParts) != 2 {
masterlog.Debug("Invalid token parts", map[string]interface{}{"token_parts": tokenParts}) 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" { if tokenPartType != "Bearer" {
masterlog.Debug("Invalid token type", map[string]interface{}{"token_type": tokenPartType}) 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() return c.Next()
} }
} }

View File

@@ -33,7 +33,7 @@ func (authorization *Authorization) BeforeCreate(tx *gorm.DB) (err error) {
} }
authorization.Code = codeHash authorization.Code = codeHash
if authorization.ExpiresAt == nil { if authorization.ExpiresAt == nil {
expiresAt := time.Now().Add(time.Minute * 10) expiresAt := time.Now().Add(time.Minute * 2)
authorization.ExpiresAt = &expiresAt authorization.ExpiresAt = &expiresAt
} }
return nil return nil

22
app/models/session.go Normal file
View File

@@ -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"
}

43
app/models/token.go Normal file
View File

@@ -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
}

View File

@@ -22,7 +22,11 @@ type User struct {
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_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 { func (User) TableName() string {

View File

@@ -7,9 +7,9 @@ import (
"git.secnex.io/secnex/oauth2-api/models" "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 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 nil, err
} }
return application, nil return application, nil

View File

@@ -1,10 +1,24 @@
package repositories package repositories
import ( import (
"time"
"git.secnex.io/secnex/oauth2-api/database" "git.secnex.io/secnex/oauth2-api/database"
"git.secnex.io/secnex/oauth2-api/models" "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 { func CreateAuthorization(authorization *models.Authorization) error {
return database.DB.Create(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
}

View File

@@ -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
}

10
app/repositories/token.go Normal file
View File

@@ -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
}

View File

@@ -6,6 +6,14 @@ import (
"git.secnex.io/secnex/oauth2-api/models" "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) { func GetUserByUsername(username string) (*models.User, error) {
var user *models.User var user *models.User
if err := database.DB.Where("username = ?", username).First(&user).Error; err != nil { if err := database.DB.Where("username = ?", username).First(&user).Error; err != nil {

View File

@@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"time" "time"
"git.secnex.io/secnex/masterlog"
"git.secnex.io/secnex/oauth2-api/models" "git.secnex.io/secnex/oauth2-api/models"
"git.secnex.io/secnex/oauth2-api/repositories" "git.secnex.io/secnex/oauth2-api/repositories"
"git.secnex.io/secnex/oauth2-api/utils" "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 { func Authorize(userID, clientID, redirectURI, responseType, scope, state string) *utils.HTTPResponse {
application, err := repositories.GetApplicationByClientID(clientID) application, err := repositories.GetApplicationByID(clientID)
if err != nil { 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) return utils.NewHTTPResponse(http.StatusUnauthorized, &fiber.Map{"error": "Application not found"}, "", nil, nil)
} }
if application.ExpiresAt.Before(time.Now().UTC()) { 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) return utils.NewHTTPResponse(http.StatusUnauthorized, &fiber.Map{"error": "Application expired"}, "", nil, nil)
} }
authorizationID := uuid.New() authorizationID := uuid.New()
@@ -35,6 +38,7 @@ func Authorize(userID, clientID, redirectURI, responseType, scope, state string)
UserID: uuid.MustParse(userID), UserID: uuid.MustParse(userID),
} }
if err := repositories.CreateAuthorization(authorization); err != nil { 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) return utils.NewHTTPResponse(http.StatusInternalServerError, &fiber.Map{"error": "Failed to create authorization"}, "", nil, nil)
} }

141
app/services/token.go Normal file
View File

@@ -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)
}

0
app/services/userinfo.go Normal file
View File