diff --git a/auth/jwt.go b/auth/jwt.go new file mode 100644 index 00000000..91aacec4 --- /dev/null +++ b/auth/jwt.go @@ -0,0 +1,92 @@ +package main + +import ( + "context" + "crypto/x509" + "encoding/pem" + "maps" + "net/http" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/labstack/echo/v4" +) + +type Jwt struct { + // The jwt token you can use for all authorized call to either keibi or other services. + Token string `json:"token"` +} + +type Info struct { + // The public key used to sign jwt tokens. It can be used by your services to check if the jwt is valid. + PublicKey string `json:"publicKey"` +} + +// @Summary Get JWT +// @Description Convert a session token to a short lived JWT. +// @Tags jwt +// @Produce json +// @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) 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["sid"] = session.Id.String() + claims["iss"] = h.config.Issuer + claims["exp"] = &jwt.NumericDate{ + Time: time.Now().UTC().Add(time.Hour), + } + claims["iss"] = &jwt.NumericDate{ + Time: time.Now().UTC(), + } + jwt := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + t, err := jwt.SignedString(h.config.JwtPrivateKey) + if err != nil { + return err + } + return c.JSON(http.StatusOK, Jwt{ + Token: t, + }) +} + +// @Summary Info +// @Description Get info like the public key used to sign the jwts. +// @Tags jwt +// @Produce json +// @Success 200 {object} Info +// @Router /info [get] +func (h *Handler) GetInfo(c echo.Context) error { + key := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PUBLIC KEY", + Bytes: x509.MarshalPKCS1PublicKey(h.config.JwtPublicKey), + }, + ) + + return c.JSON(200, Info{ + PublicKey: string(key), + }) +} diff --git a/auth/main.go b/auth/main.go index 16943aed..e4713d74 100644 --- a/auth/main.go +++ b/auth/main.go @@ -165,6 +165,8 @@ func main() { r.GET("/users", h.ListUsers) r.GET("/users/:id", h.GetUser) r.GET("/users/me", h.GetMe) + r.DELETE("/users/:id", h.DeleteUser) + r.DELETE("/users/me", h.DeleteSelf) e.POST("/users", h.Register) e.POST("/sessions", h.Login) @@ -172,6 +174,7 @@ func main() { r.DELETE("/sessions/:id", h.Logout) e.GET("/jwt", h.CreateJwt) + e.GET("/info", h.GetInfo) e.GET("/swagger/*", echoSwagger.WrapHandler) diff --git a/auth/sessions.go b/auth/sessions.go index 5df466a0..5f61bb5d 100644 --- a/auth/sessions.go +++ b/auth/sessions.go @@ -5,9 +5,7 @@ import ( "context" "crypto/rand" "encoding/base64" - "maps" "net/http" - "strings" "time" "github.com/alexedwards/argon2id" @@ -113,60 +111,6 @@ 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 -// @Produce json -// @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) 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["sid"] = session.Id.String() - claims["iss"] = h.config.Issuer - claims["exp"] = &jwt.NumericDate{ - Time: time.Now().UTC().Add(time.Hour), - } - claims["iss"] = &jwt.NumericDate{ - Time: time.Now().UTC(), - } - jwt := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) - t, err := jwt.SignedString(h.config.JwtPrivateKey) - if err != nil { - return err - } - return c.JSON(http.StatusOK, Jwt{ - Token: t, - }) -} - // @Summary Logout // @Description Delete a session and logout // @Tags sessions