diff --git a/auth/config.go b/auth/config.go index dc0cec14..aa777216 100644 --- a/auth/config.go +++ b/auth/config.go @@ -4,6 +4,7 @@ import ( "context" "crypto/rand" "encoding/base64" + "time" "github.com/golang-jwt/jwt/v5" "github.com/zoriya/kyoo/keibi/dbc" @@ -13,6 +14,7 @@ type Configuration struct { JwtSecret []byte Issuer string DefaultClaims jwt.MapClaims + ExpirationDelay time.Duration } const ( diff --git a/auth/main.go b/auth/main.go index e21ba402..e3032b29 100644 --- a/auth/main.go +++ b/auth/main.go @@ -126,6 +126,10 @@ type Handler struct { // @host kyoo.zoriya.dev // @BasePath /auth + +// @securityDefinitions.apiKey Token +// @in header +// @name Authorization func main() { e := echo.New() e.Use(middleware.Logger()) @@ -151,6 +155,7 @@ func main() { e.GET("/users", h.ListUsers) e.POST("/users", h.Register) + e.GET("/jwt", h.CreateJwt) e.POST("/session", h.Login) e.GET("/swagger/*", echoSwagger.WrapHandler) diff --git a/auth/session.go b/auth/session.go index 40294e3c..6e9a265f 100644 --- a/auth/session.go +++ b/auth/session.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "maps" "net/http" + "strings" "time" "github.com/alexedwards/argon2id" @@ -17,7 +18,7 @@ import ( type LoginDto struct { // Either the email or the username. - Login string `json:"login" validate:"required"` + Login string `json:"login" validate:"required"` // Password of the account. Password string `json:"password" validate:"required"` } @@ -82,7 +83,7 @@ func (h *Handler) createSession(c echo.Context, user *User) error { session, err := h.db.CreateSession(ctx, dbc.CreateSessionParams{ Token: base64.StdEncoding.EncodeToString(id), - UserID: user.Id, + UserId: user.Id, Device: device, }) if err != nil { @@ -91,20 +92,43 @@ func (h *Handler) createSession(c echo.Context, user *User) error { return c.JSON(201, session) } +type Jwt struct { + // The jwt token you can use for all authorized call to either keibi or other services. + Token string `json:"token"` +} + // @Summary Get JWT // @Description Convert a session token to a short lived JWT. // @Tags sessions // @Accept json // @Produce json -// @Param user body LoginDto false "Account informations" -// @Success 200 {object} dbc.Session -// @Failure 400 {object} problem.Problem "Invalid login body" -// @Failure 400 {object} problem.Problem "Invalid password" -// @Failure 404 {object} problem.Problem "Account does not exists" +// @Security Token +// @Success 200 {object} Jwt +// @Failure 401 {object} problem.Problem "Missing session token" +// @Failure 403 {object} problem.Problem "Invalid session token (or expired)" // @Router /jwt [get] -func (h *Handler) CreateJwt(c echo.Context, user *User) error { - claims := maps.Clone(user.Claims) - claims["sub"] = user.Id.String() +func (h *Handler) CreateJwt(c echo.Context) error { + auth := c.Request().Header.Get("Authorization") + if !strings.HasPrefix(auth, "Bearer ") { + return echo.NewHTTPError(http.StatusUnauthorized, "Missing session token") + } + token := auth[len("Bearer "):] + + session, err := h.db.GetUserFromToken(context.Background(), token) + if err != nil { + return echo.NewHTTPError(http.StatusForbidden, "Invalid token") + } + if session.LastUsed.Add(h.config.ExpirationDelay).Compare(time.Now().UTC()) < 0 { + return echo.NewHTTPError(http.StatusForbidden, "Token has expired") + } + + go func() { + h.db.TouchSession(context.Background(), session.Id) + h.db.TouchUser(context.Background(), session.User.Id) + }() + + claims := maps.Clone(session.User.Claims) + claims["sub"] = session.User.Id.String() claims["iss"] = h.config.Issuer claims["exp"] = &jwt.NumericDate{ Time: time.Now().UTC().Add(time.Hour), @@ -112,12 +136,12 @@ func (h *Handler) CreateJwt(c echo.Context, user *User) error { claims["iss"] = &jwt.NumericDate{ Time: time.Now().UTC(), } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - t, err := token.SignedString(h.config.JwtSecret) + jwt := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + t, err := jwt.SignedString(h.config.JwtSecret) if err != nil { return err } - return c.JSON(http.StatusOK, echo.Map{ - "token": t, + return c.JSON(http.StatusOK, Jwt{ + Token: t, }) } diff --git a/auth/sql/queries/sessions.sql b/auth/sql/queries/sessions.sql index 5cfa829c..11006f6a 100644 --- a/auth/sql/queries/sessions.sql +++ b/auth/sql/queries/sessions.sql @@ -1,9 +1,9 @@ -- name: GetUserFromToken :one select - u.* + s.id, s.last_used, sqlc.embed(u) from users as u - left join sessions as s on u.id = s.user_id + inner join sessions as s on u.id = s.user_id where s.token = $1 limit 1; diff --git a/auth/sql/queries/users.sql b/auth/sql/queries/users.sql index ffdd37ef..b08f1787 100644 --- a/auth/sql/queries/users.sql +++ b/auth/sql/queries/users.sql @@ -38,6 +38,14 @@ where or username = sqlc.arg(login) limit 1; +-- name: TouchUser :exec +update + users +set + last_used = now()::timestamptz +where + id = $1; + -- name: CreateUser :one insert into users(username, email, password, claims) values ($1, $2, $3, $4) diff --git a/auth/sqlc.yaml b/auth/sqlc.yaml index 440f0e06..8a7af618 100644 --- a/auth/sqlc.yaml +++ b/auth/sqlc.yaml @@ -10,11 +10,18 @@ sql: out: "dbc" emit_pointers_for_null_types: true emit_json_tags: true + initialisms: [] overrides: - db_type: "timestamptz" go_type: import: "time" type: "Time" + - db_type: "timestamptz" + nullable: true + go_type: + import: "time" + type: "Time" + pointer: true - db_type: "uuid" go_type: import: "github.com/google/uuid" diff --git a/auth/users.go b/auth/users.go index fc4512b1..f54487fe 100644 --- a/auth/users.go +++ b/auth/users.go @@ -49,7 +49,7 @@ type RegisterDto struct { func MapDbUser(user *dbc.User) User { return User{ - Id: user.ID, + Id: user.Id, Username: user.Username, Email: user.Email, CreatedDate: user.CreatedDate, @@ -84,7 +84,7 @@ func (h *Handler) ListUsers(c echo.Context) error { } users, err = h.db.GetAllUsersAfter(ctx, dbc.GetAllUsersAfterParams{ Limit: limit, - AfterID: uid, + AfterId: uid, }) }