feat(auth): Add /token endpoint to request a access token
This commit is contained in:
@@ -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
37
app/controllers/token.go
Normal 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)
|
||||||
|
}
|
||||||
12
app/controllers/userinfo.go
Normal file
12
app/controllers/userinfo.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
22
app/models/session.go
Normal 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
43
app/models/token.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
62
app/repositories/session.go
Normal file
62
app/repositories/session.go
Normal 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
10
app/repositories/token.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
141
app/services/token.go
Normal 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
0
app/services/userinfo.go
Normal file
Reference in New Issue
Block a user