package handlers import ( "context" "crypto/rand" "encoding/base64" "fmt" "net/http" auth "nyanimedb/auth" sqlc "nyanimedb/sql" "strconv" "time" "github.com/alexedwards/argon2id" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" log "github.com/sirupsen/logrus" ) type Server struct { db *sqlc.Queries JwtPrivateKey string } func NewServer(db *sqlc.Queries, JwtPrivatekey string) Server { return Server{db: db, JwtPrivateKey: JwtPrivatekey} } func parseInt64(s string) (int32, error) { i, err := strconv.ParseInt(s, 10, 64) return int32(i), err } func HashPassword(password string) (string, error) { params := &argon2id.Params{ Memory: 64 * 1024, Iterations: 3, Parallelism: 2, SaltLength: 16, KeyLength: 32, } return argon2id.CreateHash(password, params) } func CheckPassword(password, hash string) (bool, error) { return argon2id.ComparePasswordAndHash(password, hash) } func (s Server) generateImpersonationToken(userID string, impersonated_by string) (accessToken string, err error) { accessClaims := jwt.MapClaims{ "user_id": userID, "exp": time.Now().Add(15 * time.Minute).Unix(), "imp_id": impersonated_by, } at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) accessToken, err = at.SignedString(s.JwtPrivateKey) if err != nil { return "", err } return accessToken, nil } func (s Server) generateTokens(userID string) (accessToken string, refreshToken string, csrfToken string, err error) { accessClaims := jwt.MapClaims{ "user_id": userID, "exp": time.Now().Add(15 * time.Minute).Unix(), //TODO: add created_at } at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) accessToken, err = at.SignedString([]byte(s.JwtPrivateKey)) if err != nil { return "", "", "", err } refreshClaims := jwt.MapClaims{ "user_id": userID, "exp": time.Now().Add(7 * 24 * time.Hour).Unix(), } rt := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims) refreshToken, err = rt.SignedString([]byte(s.JwtPrivateKey)) if err != nil { return "", "", "", err } csrfBytes := make([]byte, 32) _, err = rand.Read(csrfBytes) if err != nil { return "", "", "", err } csrfToken = base64.RawURLEncoding.EncodeToString(csrfBytes) return accessToken, refreshToken, csrfToken, nil } func (s Server) PostSignUp(ctx context.Context, req auth.PostSignUpRequestObject) (auth.PostSignUpResponseObject, error) { passhash, err := HashPassword(req.Body.Pass) if err != nil { log.Errorf("failed to hash password: %v", err) // TODO: return 500 } user_id, err := s.db.CreateNewUser(context.Background(), sqlc.CreateNewUserParams{ Passhash: passhash, Nickname: req.Body.Nickname, }) if err != nil { log.Errorf("failed to create user %s: %v", req.Body.Nickname, err) // TODO: check err and retyrn 400/500 } return auth.PostSignUp200JSONResponse{ UserId: user_id, }, nil } func (s Server) PostSignIn(ctx context.Context, req auth.PostSignInRequestObject) (auth.PostSignInResponseObject, error) { ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context) if !ok { log.Print("failed to get gin context") // TODO: change to 500 return auth.PostSignIn200JSONResponse{}, fmt.Errorf("failed to get gin.Context from context.Context") } user, err := s.db.GetUserByNickname(context.Background(), req.Body.Nickname) if err != nil { log.Errorf("failed to get user by nickname %s: %v", req.Body.Nickname, err) // TODO: return 400/500 } ok, err = CheckPassword(req.Body.Pass, user.Passhash) if err != nil { log.Errorf("failed to check password for user %s: %v", req.Body.Nickname, err) // TODO: return 500 } if !ok { return auth.PostSignIn401Response{}, nil } accessToken, refreshToken, csrfToken, err := s.generateTokens(req.Body.Nickname) if err != nil { log.Errorf("failed to generate tokens for user %s: %v", req.Body.Nickname, err) // TODO: return 500 } // TODO: check cookie settings carefully ginCtx.SetSameSite(http.SameSiteStrictMode) ginCtx.SetCookie("access_token", accessToken, 900, "/api", "", false, true) ginCtx.SetCookie("refresh_token", refreshToken, 1209600, "/auth", "", false, true) ginCtx.SetCookie("xsrf_token", csrfToken, 1209600, "/", "", false, false) result := auth.PostSignIn200JSONResponse{ UserId: user.ID, UserName: user.Nickname, } return result, nil } func (s Server) GetImpersonationToken(ctx context.Context, request auth.GetImpersonationTokenRequestObject) (auth.GetImpersonationTokenResponseObject, error) { ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context) if !ok { log.Print("failed to get gin context") // TODO: change to 500 return auth.GetImpersonationToken200JSONResponse{}, fmt.Errorf("failed to get gin.Context from context.Context") } token := ginCtx.Request.Header.Get("Authorization") log.Printf("got auth token: %s", token) //s.db.GetExternalServiceByToken() return auth.PostSignIn401Response{}, nil } // func (s Server) PostAuthVerifyToken(ctx context.Context, req auth.PostAuthVerifyTokenRequestObject) (auth.PostAuthVerifyTokenResponseObject, error) { // valid := false // var userID *string // var errStr *string // token, err := jwt.Parse(req.Body.Token, func(t *jwt.Token) (interface{}, error) { // if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { // return nil, fmt.Errorf("unexpected signing method") // } // return accessSecret, nil // }) // if err != nil { // e := err.Error() // errStr = &e // return auth.PostAuthVerifyToken200JSONResponse{ // Valid: &valid, // UserId: userID, // Error: errStr, // }, nil // } // if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { // if uid, ok := claims["user_id"].(string); ok { // valid = true // userID = &uid // } else { // e := "user_id not found in token" // errStr = &e // } // } else { // e := "invalid token claims" // errStr = &e // } // return auth.PostAuthVerifyToken200JSONResponse{ // Valid: &valid, // UserId: userID, // Error: errStr, // }, nil // } // func (s Server) PostAuthRefreshToken(ctx context.Context, req auth.PostAuthRefreshTokenRequestObject) (auth.PostAuthRefreshTokenResponseObject, error) { // valid := false // var userID *string // var errStr *string // token, err := jwt.Parse(req.Body.Token, func(t *jwt.Token) (interface{}, error) { // if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { // return nil, fmt.Errorf("unexpected signing method") // } // return refreshSecret, nil // }) // if err != nil { // e := err.Error() // errStr = &e // return auth.PostAuthVerifyToken200JSONResponse{ // Valid: &valid, // UserId: userID, // Error: errStr, // }, nil // } // if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { // if uid, ok := claims["user_id"].(string); ok { // // Refresh token is valid, generate new tokens // newAccessToken, newRefreshToken, _ := generateTokens(uid) // valid = true // userID = &uid // return auth.PostAuthVerifyToken200JSONResponse{ // Valid: &valid, // UserId: userID, // Error: nil, // Token: &newAccessToken, // return new access token // // optionally return newRefreshToken as well // }, nil // } else { // e := "user_id not found in refresh token" // errStr = &e // } // } else { // e := "invalid refresh token claims" // errStr = &e // } // return auth.PostAuthVerifyToken200JSONResponse{ // Valid: &valid, // UserId: userID, // Error: errStr, // }, nil // }