mirror of
https://github.com/renorris/openfsd
synced 2026-03-22 14:35:36 +08:00
295 lines
6.8 KiB
Go
295 lines
6.8 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/renorris/openfsd/db"
|
|
"github.com/renorris/openfsd/fsd"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// getAccessRefreshTokens returns access and refresh tokens given FSD login credentials
|
|
func (s *Server) getAccessRefreshTokens(c *gin.Context) {
|
|
type RequestBody struct {
|
|
CID int `json:"cid" binding:"min=1,required"`
|
|
Password string `json:"password" binding:"required"`
|
|
RememberMe bool `json:"remember_me"`
|
|
}
|
|
|
|
var reqBody RequestBody
|
|
if !bindJSONOrAbort(c, &reqBody) {
|
|
return
|
|
}
|
|
|
|
unauthRes := newAPIV1Failure("Bad CID and/or password")
|
|
|
|
user, err := s.dbRepo.UserRepo.GetUserByCID(reqBody.CID)
|
|
if err != nil {
|
|
writeAPIV1Response(c, http.StatusUnauthorized, &unauthRes)
|
|
return
|
|
}
|
|
|
|
if !s.dbRepo.UserRepo.VerifyPasswordHash(reqBody.Password, user.Password) {
|
|
writeAPIV1Response(c, http.StatusUnauthorized, &unauthRes)
|
|
return
|
|
}
|
|
|
|
access, refresh, err := s.makeAccessRefreshTokens(user, reqBody.RememberMe)
|
|
if err != nil {
|
|
writeAPIV1Response(c, http.StatusInternalServerError, &genericAPIV1InternalServerError)
|
|
return
|
|
}
|
|
|
|
type ResponseBody struct {
|
|
AccessToken string `json:"access_token"`
|
|
RefreshToken string `json:"refresh_token"`
|
|
}
|
|
|
|
resBody := ResponseBody{
|
|
AccessToken: access,
|
|
RefreshToken: refresh,
|
|
}
|
|
|
|
res := newAPIV1Success(&resBody)
|
|
c.JSON(http.StatusOK, &res)
|
|
}
|
|
|
|
// refreshAccessToken refreshes an access token given a refresh token
|
|
func (s *Server) refreshAccessToken(c *gin.Context) {
|
|
type RequestBody struct {
|
|
RefreshToken string `json:"refresh_token" binding:"required"`
|
|
}
|
|
|
|
var reqBody RequestBody
|
|
if !bindJSONOrAbort(c, &reqBody) {
|
|
return
|
|
}
|
|
|
|
badTokenRes := newAPIV1Failure("bad token")
|
|
|
|
refreshToken, err := fsd.ParseJwtToken(reqBody.RefreshToken, s.jwtSecret)
|
|
if err != nil {
|
|
writeAPIV1Response(c, http.StatusUnauthorized, &badTokenRes)
|
|
return
|
|
}
|
|
|
|
claims := refreshToken.CustomClaims()
|
|
|
|
if claims.TokenType != "refresh" {
|
|
writeAPIV1Response(c, http.StatusUnauthorized, &badTokenRes)
|
|
return
|
|
}
|
|
|
|
user, err := s.dbRepo.UserRepo.GetUserByCID(claims.CID)
|
|
if err != nil {
|
|
writeAPIV1Response(c, http.StatusUnauthorized, &badTokenRes)
|
|
return
|
|
}
|
|
|
|
access, err := s.makeAccessToken(user)
|
|
if err != nil {
|
|
writeAPIV1Response(c, http.StatusInternalServerError, &genericAPIV1InternalServerError)
|
|
return
|
|
}
|
|
|
|
type ResponseBody struct {
|
|
AccessToken string `json:"access_token"`
|
|
}
|
|
|
|
resBody := ResponseBody{
|
|
AccessToken: access,
|
|
}
|
|
|
|
res := newAPIV1Success(&resBody)
|
|
c.JSON(http.StatusOK, &res)
|
|
}
|
|
|
|
func (s *Server) getFsdJwt(c *gin.Context) {
|
|
type RequestBody struct {
|
|
CID string `json:"cid" form:"cid" binding:"required"`
|
|
Password string `json:"password" form:"password" binding:"required"`
|
|
}
|
|
|
|
var reqBody RequestBody
|
|
if err := c.ShouldBind(&reqBody); err != nil {
|
|
return
|
|
}
|
|
|
|
type ResponseBody struct {
|
|
Success bool `json:"success"`
|
|
Token string `json:"token,omitempty"`
|
|
ErrorMsg string `json:"error_msg,omitempty"`
|
|
}
|
|
|
|
cid, err := strconv.Atoi(reqBody.CID)
|
|
if err != nil || cid < 1 {
|
|
resBody := ResponseBody{
|
|
ErrorMsg: "Invalid CID",
|
|
}
|
|
c.JSON(http.StatusBadRequest, &resBody)
|
|
return
|
|
}
|
|
|
|
user, err := s.dbRepo.UserRepo.GetUserByCID(cid)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
resBody := ResponseBody{
|
|
ErrorMsg: "Invalid CID and/or password",
|
|
}
|
|
c.JSON(http.StatusUnauthorized, &resBody)
|
|
return
|
|
}
|
|
|
|
resBody := ResponseBody{
|
|
ErrorMsg: "Internal server error",
|
|
}
|
|
c.JSON(http.StatusInternalServerError, &resBody)
|
|
return
|
|
}
|
|
|
|
if user.NetworkRating <= int(fsd.NetworkRatingSuspended) {
|
|
resBody := ResponseBody{
|
|
ErrorMsg: "Certificate suspended or inactive",
|
|
}
|
|
c.JSON(http.StatusForbidden, &resBody)
|
|
return
|
|
}
|
|
|
|
fsdJwtToken, err := fsd.MakeJwtToken(&fsd.CustomFields{
|
|
TokenType: "fsd",
|
|
CID: user.CID,
|
|
FirstName: safeStr(user.FirstName),
|
|
LastName: safeStr(user.LastName),
|
|
NetworkRating: fsd.NetworkRating(user.NetworkRating),
|
|
}, 5*time.Minute)
|
|
if err != nil {
|
|
resBody := ResponseBody{
|
|
ErrorMsg: "Internal server error",
|
|
}
|
|
c.JSON(http.StatusInternalServerError, &resBody)
|
|
return
|
|
}
|
|
|
|
fsdJwtTokenStr, err := fsdJwtToken.SignedString(s.jwtSecret)
|
|
if err != nil {
|
|
resBody := ResponseBody{
|
|
ErrorMsg: "Internal server error",
|
|
}
|
|
c.JSON(http.StatusInternalServerError, &resBody)
|
|
return
|
|
}
|
|
|
|
resBody := ResponseBody{
|
|
Success: true,
|
|
Token: fsdJwtTokenStr,
|
|
}
|
|
|
|
c.JSON(http.StatusOK, &resBody)
|
|
}
|
|
|
|
// jwtBearerMiddleware verifies the existence of, validates, and parses JWT bearer tokens.
|
|
//
|
|
// No specific validation of verified claims are done in this function.
|
|
func (s *Server) jwtBearerMiddleware(c *gin.Context) {
|
|
authHeader := c.GetHeader("Authorization")
|
|
authHeader, found := strings.CutPrefix(authHeader, "Bearer ")
|
|
if !found {
|
|
res := newAPIV1Failure("bad bearer token")
|
|
writeAPIV1Response(c, http.StatusBadRequest, &res)
|
|
return
|
|
}
|
|
|
|
accessToken, err := fsd.ParseJwtToken(authHeader, s.jwtSecret)
|
|
if err != nil {
|
|
res := newAPIV1Failure("invalid bearer token")
|
|
writeAPIV1Response(c, http.StatusUnauthorized, &res)
|
|
return
|
|
}
|
|
|
|
setJwtContext(c, accessToken.CustomClaims())
|
|
|
|
c.Next()
|
|
}
|
|
|
|
const jwtContextKey = "jwtbearer"
|
|
|
|
func setJwtContext(c *gin.Context, claims *fsd.CustomClaims) {
|
|
c.Set(jwtContextKey, claims)
|
|
}
|
|
|
|
func getJwtContext(c *gin.Context) (claims *fsd.CustomClaims) {
|
|
val, exists := c.Get(jwtContextKey)
|
|
if !exists {
|
|
panic("attempted to load non-existent jwt context")
|
|
}
|
|
|
|
claims = val.(*fsd.CustomClaims)
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Server) makeAccessRefreshTokens(user *db.User, rememberMe bool) (access string, refresh string, err error) {
|
|
access, err = s.makeAccessToken(user)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
refresh, err = s.makeRefreshToken(user, rememberMe)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Server) makeAccessToken(user *db.User) (access string, err error) {
|
|
// Make access token
|
|
accessToken, err := fsd.MakeJwtToken(&fsd.CustomFields{
|
|
TokenType: "access",
|
|
CID: user.CID,
|
|
FirstName: safeStr(user.FirstName),
|
|
LastName: safeStr(user.LastName),
|
|
NetworkRating: fsd.NetworkRating(user.NetworkRating),
|
|
}, 15*time.Minute)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
access, err = accessToken.SignedString(s.jwtSecret)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Server) makeRefreshToken(user *db.User, rememberMe bool) (refresh string, err error) {
|
|
refreshTokenDuration := time.Hour * 24
|
|
if rememberMe {
|
|
refreshTokenDuration = time.Hour * 24 * 30
|
|
}
|
|
|
|
// Make refresh token
|
|
refreshToken, err := fsd.MakeJwtToken(&fsd.CustomFields{
|
|
TokenType: "refresh",
|
|
CID: user.CID,
|
|
FirstName: safeStr(user.FirstName),
|
|
LastName: safeStr(user.LastName),
|
|
NetworkRating: fsd.NetworkRating(user.NetworkRating),
|
|
}, refreshTokenDuration)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
refresh, err = refreshToken.SignedString(s.jwtSecret)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|