feat: use SetCookie for access and refresh tokens

This commit is contained in:
nihonium 2025-11-23 03:32:58 +03:00
parent 2929a6e4bc
commit 69e8a8dc79
Signed by: nihonium
GPG key ID: 0251623741027CFC
3 changed files with 246 additions and 182 deletions

View file

@ -25,21 +25,12 @@ type PostAuthSignUpJSONBody struct {
Pass string `json:"pass"` Pass string `json:"pass"`
} }
// PostAuthVerifyTokenJSONBody defines parameters for PostAuthVerifyToken.
type PostAuthVerifyTokenJSONBody struct {
// Token JWT token to validate
Token string `json:"token"`
}
// PostAuthSignInJSONRequestBody defines body for PostAuthSignIn for application/json ContentType. // PostAuthSignInJSONRequestBody defines body for PostAuthSignIn for application/json ContentType.
type PostAuthSignInJSONRequestBody PostAuthSignInJSONBody type PostAuthSignInJSONRequestBody PostAuthSignInJSONBody
// PostAuthSignUpJSONRequestBody defines body for PostAuthSignUp for application/json ContentType. // PostAuthSignUpJSONRequestBody defines body for PostAuthSignUp for application/json ContentType.
type PostAuthSignUpJSONRequestBody PostAuthSignUpJSONBody type PostAuthSignUpJSONRequestBody PostAuthSignUpJSONBody
// PostAuthVerifyTokenJSONRequestBody defines body for PostAuthVerifyToken for application/json ContentType.
type PostAuthVerifyTokenJSONRequestBody PostAuthVerifyTokenJSONBody
// ServerInterface represents all server handlers. // ServerInterface represents all server handlers.
type ServerInterface interface { type ServerInterface interface {
// Sign in a user and return JWT // Sign in a user and return JWT
@ -48,9 +39,6 @@ type ServerInterface interface {
// Sign up a new user // Sign up a new user
// (POST /auth/sign-up) // (POST /auth/sign-up)
PostAuthSignUp(c *gin.Context) PostAuthSignUp(c *gin.Context)
// Verify JWT validity
// (POST /auth/verify-token)
PostAuthVerifyToken(c *gin.Context)
} }
// ServerInterfaceWrapper converts contexts to parameters. // ServerInterfaceWrapper converts contexts to parameters.
@ -88,19 +76,6 @@ func (siw *ServerInterfaceWrapper) PostAuthSignUp(c *gin.Context) {
siw.Handler.PostAuthSignUp(c) siw.Handler.PostAuthSignUp(c)
} }
// PostAuthVerifyToken operation middleware
func (siw *ServerInterfaceWrapper) PostAuthVerifyToken(c *gin.Context) {
for _, middleware := range siw.HandlerMiddlewares {
middleware(c)
if c.IsAborted() {
return
}
}
siw.Handler.PostAuthVerifyToken(c)
}
// GinServerOptions provides options for the Gin server. // GinServerOptions provides options for the Gin server.
type GinServerOptions struct { type GinServerOptions struct {
BaseURL string BaseURL string
@ -130,7 +105,6 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options
router.POST(options.BaseURL+"/auth/sign-in", wrapper.PostAuthSignIn) router.POST(options.BaseURL+"/auth/sign-in", wrapper.PostAuthSignIn)
router.POST(options.BaseURL+"/auth/sign-up", wrapper.PostAuthSignUp) router.POST(options.BaseURL+"/auth/sign-up", wrapper.PostAuthSignUp)
router.POST(options.BaseURL+"/auth/verify-token", wrapper.PostAuthVerifyToken)
} }
type PostAuthSignInRequestObject struct { type PostAuthSignInRequestObject struct {
@ -144,9 +118,6 @@ type PostAuthSignInResponseObject interface {
type PostAuthSignIn200JSONResponse struct { type PostAuthSignIn200JSONResponse struct {
Error *string `json:"error"` Error *string `json:"error"`
Success *bool `json:"success,omitempty"` Success *bool `json:"success,omitempty"`
// Token JWT token to access protected endpoints
Token *string `json:"token"`
UserId *string `json:"user_id"` UserId *string `json:"user_id"`
} }
@ -157,6 +128,17 @@ func (response PostAuthSignIn200JSONResponse) VisitPostAuthSignInResponse(w http
return json.NewEncoder(w).Encode(response) return json.NewEncoder(w).Encode(response)
} }
type PostAuthSignIn401JSONResponse struct {
Error *string `json:"error,omitempty"`
}
func (response PostAuthSignIn401JSONResponse) VisitPostAuthSignInResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(401)
return json.NewEncoder(w).Encode(response)
}
type PostAuthSignUpRequestObject struct { type PostAuthSignUpRequestObject struct {
Body *PostAuthSignUpJSONRequestBody Body *PostAuthSignUpJSONRequestBody
} }
@ -178,32 +160,6 @@ func (response PostAuthSignUp200JSONResponse) VisitPostAuthSignUpResponse(w http
return json.NewEncoder(w).Encode(response) return json.NewEncoder(w).Encode(response)
} }
type PostAuthVerifyTokenRequestObject struct {
Body *PostAuthVerifyTokenJSONRequestBody
}
type PostAuthVerifyTokenResponseObject interface {
VisitPostAuthVerifyTokenResponse(w http.ResponseWriter) error
}
type PostAuthVerifyToken200JSONResponse struct {
// Error Error message if token is invalid
Error *string `json:"error"`
// UserId User ID extracted from token if valid
UserId *string `json:"user_id"`
// Valid True if token is valid
Valid *bool `json:"valid,omitempty"`
}
func (response PostAuthVerifyToken200JSONResponse) VisitPostAuthVerifyTokenResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
// StrictServerInterface represents all server handlers. // StrictServerInterface represents all server handlers.
type StrictServerInterface interface { type StrictServerInterface interface {
// Sign in a user and return JWT // Sign in a user and return JWT
@ -212,9 +168,6 @@ type StrictServerInterface interface {
// Sign up a new user // Sign up a new user
// (POST /auth/sign-up) // (POST /auth/sign-up)
PostAuthSignUp(ctx context.Context, request PostAuthSignUpRequestObject) (PostAuthSignUpResponseObject, error) PostAuthSignUp(ctx context.Context, request PostAuthSignUpRequestObject) (PostAuthSignUpResponseObject, error)
// Verify JWT validity
// (POST /auth/verify-token)
PostAuthVerifyToken(ctx context.Context, request PostAuthVerifyTokenRequestObject) (PostAuthVerifyTokenResponseObject, error)
} }
type StrictHandlerFunc = strictgin.StrictGinHandlerFunc type StrictHandlerFunc = strictgin.StrictGinHandlerFunc
@ -294,36 +247,3 @@ func (sh *strictHandler) PostAuthSignUp(ctx *gin.Context) {
ctx.Error(fmt.Errorf("unexpected response type: %T", response)) ctx.Error(fmt.Errorf("unexpected response type: %T", response))
} }
} }
// PostAuthVerifyToken operation middleware
func (sh *strictHandler) PostAuthVerifyToken(ctx *gin.Context) {
var request PostAuthVerifyTokenRequestObject
var body PostAuthVerifyTokenJSONRequestBody
if err := ctx.ShouldBindJSON(&body); err != nil {
ctx.Status(http.StatusBadRequest)
ctx.Error(err)
return
}
request.Body = &body
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
return sh.ssi.PostAuthVerifyToken(ctx, request.(PostAuthVerifyTokenRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "PostAuthVerifyToken")
}
response, err := handler(ctx, request)
if err != nil {
ctx.Error(err)
ctx.Status(http.StatusInternalServerError)
} else if validResponse, ok := response.(PostAuthVerifyTokenResponseObject); ok {
if err := validResponse.VisitPostAuthVerifyTokenResponse(ctx.Writer); err != nil {
ctx.Error(err)
}
} else if response != nil {
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
}
}

View file

@ -1,4 +1,4 @@
openapi: 3.1.0 openapi: 3.1.1
info: info:
title: Auth Service title: Auth Service
version: 1.0.0 version: 1.0.0
@ -58,6 +58,14 @@ paths:
responses: responses:
"200": "200":
description: Sign-in result with JWT description: Sign-in result with JWT
# headers:
# Set-Cookie:
# schema:
# type: array
# items:
# type: string
# explode: true
# style: simple
content: content:
application/json: application/json:
schema: schema:
@ -71,42 +79,89 @@ paths:
user_id: user_id:
type: string type: string
nullable: true nullable: true
token: "401":
type: string description: Access denied due to invalid credentials
description: JWT token to access protected endpoints
nullable: true
/auth/verify-token:
post:
summary: Verify JWT validity
tags: [Auth]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [token]
properties:
token:
type: string
description: JWT token to validate
responses:
"200":
description: Token validation result
content: content:
application/json: application/json:
schema: schema:
type: object type: object
properties: properties:
valid:
type: boolean
description: True if token is valid
user_id:
type: string
nullable: true
description: User ID extracted from token if valid
error: error:
type: string type: string
nullable: true example: "Access denied"
description: Error message if token is invalid # /auth/verify-token:
# post:
# summary: Verify JWT validity
# tags: [Auth]
# requestBody:
# required: true
# content:
# application/json:
# schema:
# type: object
# required: [token]
# properties:
# token:
# type: string
# description: JWT token to validate
# responses:
# "200":
# description: Token validation result
# content:
# application/json:
# schema:
# type: object
# properties:
# valid:
# type: boolean
# description: True if token is valid
# user_id:
# type: string
# nullable: true
# description: User ID extracted from token if valid
# error:
# type: string
# nullable: true
# description: Error message if token is invalid
# /auth/refresh-token:
# post:
# summary: Refresh JWT using a refresh token
# tags: [Auth]
# requestBody:
# required: true
# content:
# application/json:
# schema:
# type: object
# required: [refresh_token]
# properties:
# refresh_token:
# type: string
# description: JWT refresh token obtained from sign-in
# responses:
# "200":
# description: New access (and optionally refresh) token
# content:
# application/json:
# schema:
# type: object
# properties:
# valid:
# type: boolean
# description: True if refresh token was valid
# user_id:
# type: string
# nullable: true
# description: User ID extracted from refresh token
# access_token:
# type: string
# description: New access token
# nullable: true
# refresh_token:
# type: string
# description: New refresh token (optional)
# nullable: true
# error:
# type: string
# nullable: true
# description: Error message if refresh token is invalid

View file

@ -3,27 +3,21 @@ package handlers
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"net/http"
auth "nyanimedb/auth" auth "nyanimedb/auth"
sqlc "nyanimedb/sql" sqlc "nyanimedb/sql"
"strconv" "strconv"
"time" "time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
) )
var secretKey = []byte("my_secret_key") var accessSecret = []byte("my_access_secret_key")
var refreshSecret = []byte("my_refresh_secret_key")
func generateToken(userID string) (string, error) { var UserDb = make(map[string]string) // TEMP: stores passwords
claims := jwt.MapClaims{
"user_id": userID,
"exp": time.Now().Add(time.Hour * 24).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(secretKey)
}
var UserDb = make(map[string]string) //TEMP
type Server struct { type Server struct {
db *sqlc.Queries db *sqlc.Queries
@ -38,19 +32,28 @@ func parseInt64(s string) (int32, error) {
return int32(i), err return int32(i), err
} }
func (s Server) PostAuthSignIn(ctx context.Context, req auth.PostAuthSignInRequestObject) (auth.PostAuthSignInResponseObject, error) { func generateTokens(userID string) (accessToken string, refreshToken string, err error) {
err := "" accessClaims := jwt.MapClaims{
success := true "user_id": userID,
t, _ := generateToken(req.Body.Nickname) "exp": time.Now().Add(15 * time.Minute).Unix(),
}
at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
accessToken, err = at.SignedString(accessSecret)
if err != nil {
return "", "", err
}
UserDb[req.Body.Nickname] = req.Body.Pass 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(refreshSecret)
if err != nil {
return "", "", err
}
return auth.PostAuthSignIn200JSONResponse{ return accessToken, refreshToken, nil
Error: &err,
Success: &success,
UserId: &req.Body.Nickname,
Token: &t,
}, nil
} }
func (s Server) PostAuthSignUp(ctx context.Context, req auth.PostAuthSignUpRequestObject) (auth.PostAuthSignUpResponseObject, error) { func (s Server) PostAuthSignUp(ctx context.Context, req auth.PostAuthSignUpRequestObject) (auth.PostAuthSignUpResponseObject, error) {
@ -65,44 +68,130 @@ func (s Server) PostAuthSignUp(ctx context.Context, req auth.PostAuthSignUpReque
}, nil }, nil
} }
func (s Server) PostAuthVerifyToken(ctx context.Context, req auth.PostAuthVerifyTokenRequestObject) (auth.PostAuthVerifyTokenResponseObject, error) { func (s Server) PostAuthSignIn(ctx context.Context, req auth.PostAuthSignInRequestObject) (auth.PostAuthSignInResponseObject, error) {
valid := false // ctx.SetCookie("122")
var userID *string ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context)
var errStr *string if !ok {
log.Print("failed to get gin context")
token, err := jwt.Parse(req.Body.Token, func(t *jwt.Token) (interface{}, error) { // TODO: change to 500
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return auth.PostAuthSignIn200JSONResponse{}, fmt.Errorf("failed to get gin.Context from context.Context")
return nil, fmt.Errorf("unexpected signing method")
} }
return secretKey, nil
})
if err != nil { err := ""
e := err.Error() success := true
errStr = &e
return auth.PostAuthVerifyToken200JSONResponse{ pass, ok := UserDb[req.Body.Nickname]
Valid: &valid, if !ok || pass != req.Body.Pass {
UserId: userID, e := "invalid credentials"
Error: errStr, return auth.PostAuthSignIn401JSONResponse{
Error: &e,
}, nil }, nil
} }
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { accessToken, refreshToken, _ := generateTokens(req.Body.Nickname)
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{ ginCtx.SetSameSite(http.SameSiteStrictMode)
Valid: &valid, ginCtx.SetCookie("access_token", accessToken, 604800, "/auth", "", true, true)
UserId: userID, ginCtx.SetCookie("refresh_token", refreshToken, 604800, "/api", "", true, true)
Error: errStr,
}, nil // Return access token; refresh token can be returned in response or HttpOnly cookie
result := auth.PostAuthSignIn200JSONResponse{
Error: &err,
Success: &success,
UserId: &req.Body.Nickname,
}
return result, 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
// }