aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend/api/handler.go
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-13 23:46:16 +0900
committernsfisis <nsfisis@gmail.com>2026-02-13 23:46:16 +0900
commit7258ca81812a24edd382438ce6e9ebc538549427 (patch)
tree9bbc034be62777a2412d871211188268d7c56da4 /backend/api/handler.go
parent7757f26295cbf19c4d6fa068e2cb6bdc2589d01a (diff)
downloadphperkaigi-2026-albatross-7258ca81812a24edd382438ce6e9ebc538549427.tar.gz
phperkaigi-2026-albatross-7258ca81812a24edd382438ce6e9ebc538549427.tar.zst
phperkaigi-2026-albatross-7258ca81812a24edd382438ce6e9ebc538549427.zip
feat(auth): store JWT in HTTP-only cookie instead of JS-accessible cookie
Prevent XSS-based token theft by making the JWT inaccessible to JavaScript. The backend now sets/clears the cookie via Set-Cookie headers, and the frontend retrieves user info from /api/me instead of decoding the JWT directly. - Add JWTCookieMiddleware to parse cookie and inject claims into context - Add /me and /logout endpoints to OpenAPI spec and handlers - Update PostLogin to return user object + Set-Cookie header - Replace Authorization header auth with cookie-based auth throughout - Rewrite frontend auth to use /api/me instead of jwt-decode - Remove jwt-decode dependency - Configure CORS with credentials for local dev Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'backend/api/handler.go')
-rw-r--r--backend/api/handler.go90
1 files changed, 84 insertions, 6 deletions
diff --git a/backend/api/handler.go b/backend/api/handler.go
index d2883a9..3b04665 100644
--- a/backend/api/handler.go
+++ b/backend/api/handler.go
@@ -2,6 +2,7 @@ package api
import (
"context"
+ "encoding/json"
"errors"
"log"
"net/http"
@@ -12,12 +13,14 @@ import (
"github.com/oapi-codegen/nullable"
"albatross-2026-backend/auth"
+ "albatross-2026-backend/config"
"albatross-2026-backend/db"
)
type Handler struct {
- q *db.Queries
- hub GameHubInterface
+ q *db.Queries
+ hub GameHubInterface
+ conf *config.Config
}
type GameHubInterface interface {
@@ -25,6 +28,18 @@ type GameHubInterface interface {
EnqueueTestTasks(ctx context.Context, submissionID, gameID, userID int, language, code string) error
}
+type postLoginCookieResponse struct {
+ cookie http.Cookie
+ body PostLogin200JSONResponse
+}
+
+func (r postLoginCookieResponse) VisitPostLoginResponse(w http.ResponseWriter) error {
+ http.SetCookie(w, &r.cookie)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(200)
+ return json.NewEncoder(w).Encode(r.body)
+}
+
func (h *Handler) PostLogin(ctx context.Context, request PostLoginRequestObject) (PostLoginResponseObject, error) {
username := request.Body.Username
password := request.Body.Password
@@ -44,7 +59,7 @@ func (h *Handler) PostLogin(ctx context.Context, request PostLoginRequestObject)
}, nil
}
- user, err := h.q.GetUserByID(ctx, int32(userID))
+ dbUser, err := h.q.GetUserByID(ctx, int32(userID))
if err != nil {
return PostLogin401JSONResponse{
UnauthorizedJSONResponse: UnauthorizedJSONResponse{
@@ -53,13 +68,76 @@ func (h *Handler) PostLogin(ctx context.Context, request PostLoginRequestObject)
}, nil
}
- jwt, err := auth.NewJWT(&user)
+ jwt, err := auth.NewJWT(&dbUser)
if err != nil {
return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
- return PostLogin200JSONResponse{
- Token: jwt,
+ return postLoginCookieResponse{
+ cookie: http.Cookie{
+ Name: "albatross_token",
+ Value: jwt,
+ Path: h.conf.BasePath,
+ MaxAge: 86400,
+ HttpOnly: true,
+ Secure: !h.conf.IsLocal,
+ SameSite: http.SameSiteLaxMode,
+ },
+ body: PostLogin200JSONResponse{
+ User: User{
+ UserID: int(dbUser.UserID),
+ Username: dbUser.Username,
+ DisplayName: dbUser.DisplayName,
+ IconPath: dbUser.IconPath,
+ IsAdmin: dbUser.IsAdmin,
+ Label: toNullable(dbUser.Label),
+ },
+ },
+ }, nil
+}
+
+func (h *Handler) GetMe(ctx context.Context, _ GetMeRequestObject, claims *auth.JWTClaims) (GetMeResponseObject, error) {
+ dbUser, err := h.q.GetUserByID(ctx, int32(claims.UserID))
+ if err != nil {
+ return GetMe401JSONResponse{
+ UnauthorizedJSONResponse: UnauthorizedJSONResponse{
+ Message: "Unauthorized",
+ },
+ }, nil
+ }
+ return GetMe200JSONResponse{
+ User: User{
+ UserID: int(dbUser.UserID),
+ Username: dbUser.Username,
+ DisplayName: dbUser.DisplayName,
+ IconPath: dbUser.IconPath,
+ IsAdmin: dbUser.IsAdmin,
+ Label: toNullable(dbUser.Label),
+ },
+ }, nil
+}
+
+type postLogoutCookieResponse struct {
+ cookie http.Cookie
+}
+
+func (r postLogoutCookieResponse) VisitPostLogoutResponse(w http.ResponseWriter) error {
+ http.SetCookie(w, &r.cookie)
+ w.WriteHeader(200)
+ return nil
+}
+
+func (h *Handler) PostLogout(_ context.Context, _ PostLogoutRequestObject, _ *auth.JWTClaims) (PostLogoutResponseObject, error) {
+ return postLogoutCookieResponse{
+ cookie: http.Cookie{
+ Name: "albatross_token",
+ Value: "",
+ Path: h.conf.BasePath,
+ MaxAge: -1,
+ HttpOnly: true,
+ Secure: !h.conf.IsLocal,
+ SameSite: http.SameSiteLaxMode,
+ },
}, nil
}