New ticket ways

This commit is contained in:
2024-04-20 19:04:33 +08:00
parent 0d78f34535
commit 87cccefddb
32 changed files with 6280 additions and 668 deletions

View File

@ -114,7 +114,7 @@ func killSession(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("sessionId", 0)
if err := database.C.Delete(&models.AuthSession{}, &models.AuthSession{
if err := database.C.Delete(&models.AuthTicket{}, &models.AuthTicket{
BaseModel: models.BaseModel{ID: uint(id)},
AccountID: user.ID,
}).Error; err != nil {

145
pkg/server/auth_api.go Normal file
View File

@ -0,0 +1,145 @@
package server
import (
"fmt"
"time"
"github.com/gofiber/fiber/v2"
"git.solsynth.dev/hydrogen/passport/pkg/services"
)
func doAuthenticate(c *fiber.Ctx) error {
var data struct {
Username string `json:"username"`
Password string `json:"password" validate:"required"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
user, err := services.LookupAccount(data.Username)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("account was not found: %v", err.Error()))
}
ticket, err := services.NewTicket(user, c.IP(), c.Get(fiber.HeaderUserAgent))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable setup ticket: %v", err.Error()))
}
ticket, err = services.ActiveTicketWithPassword(ticket, data.Password)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("invalid password: %v", err.Error()))
}
return c.JSON(fiber.Map{
"is_finished": ticket.IsAvailable(),
"ticket": ticket,
})
}
func doMultiFactorAuthenticate(c *fiber.Ctx) error {
var data struct {
TicketID uint `json:"ticket_id" validate:"required"`
FactorID uint `json:"factor_id" validate:"required"`
Code string `json:"code" validate:"required"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
ticket, err := services.GetTicket(data.TicketID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ticket was not found: %v", err.Error()))
}
factor, err := services.GetFactor(data.FactorID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("factor was not found: %v", err.Error()))
}
ticket, err = services.ActiveTicketWithMFA(ticket, factor, data.Code)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("invalid code: %v", err.Error()))
}
return c.JSON(fiber.Map{
"is_finished": ticket.IsAvailable(),
"ticket": ticket,
})
}
func getToken(c *fiber.Ctx) error {
var data struct {
Code string `json:"code" form:"code"`
RefreshToken string `json:"refresh_token" form:"refresh_token"`
ClientID string `json:"client_id" form:"client_id"`
ClientSecret string `json:"client_secret" form:"client_secret"`
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
RedirectUri string `json:"redirect_uri" form:"redirect_uri"`
GrantType string `json:"grant_type" form:"grant_type"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
var err error
var access, refresh string
switch data.GrantType {
case "refresh_token":
// Refresh Token
access, refresh, err = services.RefreshToken(data.RefreshToken)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
case "authorization_code":
// Authorization Code Mode
access, refresh, err = services.ExchangeOauthToken(data.ClientID, data.ClientSecret, data.RedirectUri, data.Code)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
case "password":
// Password Mode
user, err := services.LookupAccount(data.Username)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("account was not found: %v", err.Error()))
}
ticket, err := services.NewTicket(user, c.IP(), c.Get(fiber.HeaderUserAgent))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable setup ticket: %v", err.Error()))
}
ticket, err = services.ActiveTicketWithPassword(ticket, data.Password)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("invalid password: %v", err.Error()))
} else if ticket.GrantToken == nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to get grant token to get token"))
}
access, refresh, err = services.ExchangeOauthToken(data.ClientID, data.ClientSecret, data.RedirectUri, *ticket.GrantToken)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
case "grant_token":
// Internal Usage
access, refresh, err = services.ExchangeToken(data.Code)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
default:
return fiber.NewError(fiber.StatusBadRequest, "unsupported exchange token type")
}
services.SetJwtCookieSet(c, access, refresh)
return c.JSON(fiber.Map{
"id_token": access,
"access_token": access,
"refresh_token": refresh,
"token_type": "Bearer",
"expires_in": (30 * time.Minute).Seconds(),
})
}

View File

@ -3,14 +3,13 @@ package server
import (
"strings"
"git.solsynth.dev/hydrogen/passport/pkg/security"
"git.solsynth.dev/hydrogen/passport/pkg/services"
"github.com/gofiber/fiber/v2"
)
func authMiddleware(c *fiber.Ctx) error {
var token string
if cookie := c.Cookies(security.CookieAccessKey); len(cookie) > 0 {
if cookie := c.Cookies(services.CookieAccessKey); len(cookie) > 0 {
token = cookie
}
if header := c.Get(fiber.HeaderAuthorization); len(header) > 0 {
@ -42,10 +41,10 @@ func authFunc(c *fiber.Ctx, overrides ...string) error {
}
}
rtk := c.Cookies(security.CookieRefreshKey)
rtk := c.Cookies(services.CookieRefreshKey)
if user, atk, rtk, err := services.Authenticate(token, rtk, 0); err == nil {
if atk != token {
security.SetJwtCookieSet(c, atk, rtk)
services.SetJwtCookieSet(c, atk, rtk)
}
c.Locals("principal", user)
return nil

View File

@ -1,140 +0,0 @@
package server
import (
"time"
"github.com/gofiber/fiber/v2"
"git.solsynth.dev/hydrogen/passport/pkg/security"
"git.solsynth.dev/hydrogen/passport/pkg/services"
"github.com/samber/lo"
)
func startChallenge(c *fiber.Ctx) error {
var data struct {
ID string `json:"id" validate:"required"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
user, err := services.LookupAccount(data.ID)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
factors, err := services.LookupFactorsByUser(user.ID)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
challenge, err := security.NewChallenge(user, factors, c.IP(), c.Get(fiber.HeaderUserAgent))
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
services.AddEvent(user, "challenges.start", data.ID, c.IP(), c.Get(fiber.HeaderUserAgent))
return c.JSON(fiber.Map{
"display_name": user.Nick,
"challenge": challenge,
"factors": factors,
})
}
func doChallenge(c *fiber.Ctx) error {
var data struct {
ChallengeID uint `json:"challenge_id" validate:"required"`
FactorID uint `json:"factor_id" validate:"required"`
Secret string `json:"secret" validate:"required"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
challenge, err := services.LookupChallengeWithFingerprint(data.ChallengeID, c.IP(), c.Get(fiber.HeaderUserAgent))
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
factor, err := services.LookupFactor(data.FactorID)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if err := security.DoChallenge(challenge, factor, data.Secret); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
challenge, err = services.LookupChallenge(data.ChallengeID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if challenge.Progress >= challenge.Requirements {
session, err := security.GrantSession(challenge, []string{"*"}, []string{"passport"}, nil, lo.ToPtr(time.Now()))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(fiber.Map{
"is_finished": true,
"challenge": challenge,
"session": session,
})
}
return c.JSON(fiber.Map{
"is_finished": false,
"challenge": challenge,
"session": nil,
})
}
func exchangeToken(c *fiber.Ctx) error {
var data struct {
Code string `json:"code" form:"code"`
RefreshToken string `json:"refresh_token" form:"refresh_token"`
ClientID string `json:"client_id" form:"client_id"`
ClientSecret string `json:"client_secret" form:"client_secret"`
RedirectUri string `json:"redirect_uri" form:"redirect_uri"`
GrantType string `json:"grant_type" form:"grant_type"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
var err error
var access, refresh string
switch data.GrantType {
case "authorization_code":
// Authorization Code Mode
access, refresh, err = security.ExchangeOauthToken(data.ClientID, data.ClientSecret, data.RedirectUri, data.Code)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
case "grant_token":
// Internal Usage
access, refresh, err = security.ExchangeToken(data.Code)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
case "refresh_token":
// Refresh Token
access, refresh, err = security.RefreshToken(data.RefreshToken)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
default:
return fiber.NewError(fiber.StatusBadRequest, "unsupported exchange token type")
}
security.SetJwtCookieSet(c, access, refresh)
return c.JSON(fiber.Map{
"id_token": access,
"access_token": access,
"refresh_token": refresh,
"token_type": "Bearer",
"expires_in": (30 * time.Minute).Seconds(),
})
}

View File

@ -8,7 +8,7 @@ import (
func requestFactorToken(c *fiber.Ctx) error {
id, _ := c.ParamsInt("factorId", 0)
factor, err := services.LookupFactor(uint(id))
factor, err := services.GetFactor(uint(id))
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}

View File

@ -6,7 +6,6 @@ import (
"git.solsynth.dev/hydrogen/passport/pkg/database"
"git.solsynth.dev/hydrogen/passport/pkg/models"
"git.solsynth.dev/hydrogen/passport/pkg/security"
"git.solsynth.dev/hydrogen/passport/pkg/services"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
@ -29,8 +28,8 @@ func preConnect(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
var session models.AuthSession
if err := database.C.Where(&models.AuthSession{
var session models.AuthTicket
if err := database.C.Where(&models.AuthTicket{
AccountID: user.ID,
ClientID: &client.ID,
}).Where("last_grant_at IS NULL").First(&session).Error; err == nil {
@ -40,7 +39,7 @@ func preConnect(c *fiber.Ctx) error {
"session": nil,
})
} else {
session, err = security.RegenSession(session)
session, err = services.RegenSession(session)
}
return c.JSON(fiber.Map{
@ -73,13 +72,11 @@ func doConnect(c *fiber.Ctx) error {
switch response {
case "code":
// OAuth Authorization Mode
session, err := security.GrantOauthSession(
ticket, err := services.NewOauthTicket(
user,
client,
strings.Split(scope, " "),
[]string{"passport", client.Alias},
nil,
lo.ToPtr(time.Now()),
c.IP(),
c.Get(fiber.HeaderUserAgent),
)
@ -89,26 +86,24 @@ func doConnect(c *fiber.Ctx) error {
} else {
services.AddEvent(user, "oauth.connect", client.Alias, c.IP(), c.Get(fiber.HeaderUserAgent))
return c.JSON(fiber.Map{
"session": session,
"session": ticket,
"redirect_uri": redirect,
})
}
case "token":
// OAuth Implicit Mode
session, err := security.GrantOauthSession(
ticket, err := services.NewOauthTicket(
user,
client,
strings.Split(scope, " "),
[]string{"passport", client.Alias},
nil,
lo.ToPtr(time.Now()),
c.IP(),
c.Get(fiber.HeaderUserAgent),
)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else if access, refresh, err := security.GetToken(session); err != nil {
} else if access, refresh, err := services.GetToken(ticket); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
services.AddEvent(user, "oauth.connect", client.Alias, c.IP(), c.Get(fiber.HeaderUserAgent))
@ -116,7 +111,7 @@ func doConnect(c *fiber.Ctx) error {
"access_token": access,
"refresh_token": refresh,
"redirect_uri": redirect,
"session": session,
"ticket": ticket,
})
}
default:

View File

@ -6,52 +6,23 @@ import (
"github.com/gofiber/fiber/v2"
)
func getChallenges(c *fiber.Ctx) error {
func getTickets(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
var count int64
var challenges []models.AuthChallenge
var sessions []models.AuthTicket
if err := database.C.
Where(&models.AuthChallenge{AccountID: user.ID}).
Model(&models.AuthChallenge{}).
Where(&models.AuthTicket{AccountID: user.ID}).
Model(&models.AuthTicket{}).
Count(&count).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
if err := database.C.
Order("created_at desc").
Where(&models.AuthChallenge{AccountID: user.ID}).
Limit(take).
Offset(offset).
Find(&challenges).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(fiber.Map{
"count": count,
"data": challenges,
})
}
func getSessions(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
var count int64
var sessions []models.AuthSession
if err := database.C.
Where(&models.AuthSession{AccountID: user.ID}).
Model(&models.AuthSession{}).
Count(&count).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
if err := database.C.
Order("created_at desc").
Where(&models.AuthSession{AccountID: user.ID}).
Where(&models.AuthTicket{AccountID: user.ID}).
Limit(take).
Offset(offset).
Find(&sessions).Error; err != nil {

View File

@ -87,8 +87,7 @@ func NewServer() {
me.Put("/", authMiddleware, editUserinfo)
me.Put("/page", authMiddleware, editPersonalPage)
me.Get("/events", authMiddleware, getEvents)
me.Get("/challenges", authMiddleware, getChallenges)
me.Get("/sessions", authMiddleware, getSessions)
me.Get("/tickets", authMiddleware, getTickets)
me.Delete("/sessions/:sessionId", authMiddleware, killSession)
me.Post("/confirm", doRegisterConfirm)
@ -112,9 +111,8 @@ func NewServer() {
api.Post("/users", doRegister)
api.Put("/auth", startChallenge)
api.Post("/auth", doChallenge)
api.Post("/auth/token", exchangeToken)
api.Post("/auth", doAuthenticate)
api.Post("/auth/token", getToken)
api.Post("/auth/factors/:factorId", requestFactorToken)
api.Get("/auth/o/connect", authMiddleware, preConnect)