diff --git a/auth/apikey.go b/auth/apikey.go index b4e0a44d..81a0695f 100644 --- a/auth/apikey.go +++ b/auth/apikey.go @@ -41,7 +41,7 @@ func MapDbKey(key *dbc.Apikey) ApiKeyWToken { CreatedAt: key.CreatedAt, LastUsed: key.LastUsed, }, - Token: key.Token, + Token: fmt.Sprintf("%s-%s", key.Name, key.Token), } } @@ -74,7 +74,7 @@ func (h *Handler) CreateApiKey(c echo.Context) error { dbkey, err := h.db.CreateApiKey(context.Background(), dbc.CreateApiKeyParams{ Name: req.Name, - Token: fmt.Sprintf("%s-%s", req.Name, base64.RawURLEncoding.EncodeToString(id)), + Token: base64.RawURLEncoding.EncodeToString(id), Claims: req.Claims, }) if ErrIs(err, pgerrcode.UniqueViolation) { diff --git a/auth/dbc/apikeys.sql.go b/auth/dbc/apikeys.sql.go index 26b63e04..230a5367 100644 --- a/auth/dbc/apikeys.sql.go +++ b/auth/dbc/apikeys.sql.go @@ -64,6 +64,37 @@ func (q *Queries) DeleteApiKey(ctx context.Context, id uuid.UUID) (Apikey, error return i, err } +const getApiKey = `-- name: GetApiKey :one +select + pk, id, name, token, claims, created_by, created_at, last_used +from + apikeys +where + name = $1 + and token = $2 +` + +type GetApiKeyParams struct { + Name string `json:"name"` + Token string `json:"token"` +} + +func (q *Queries) GetApiKey(ctx context.Context, arg GetApiKeyParams) (Apikey, error) { + row := q.db.QueryRow(ctx, getApiKey, arg.Name, arg.Token) + var i Apikey + err := row.Scan( + &i.Pk, + &i.Id, + &i.Name, + &i.Token, + &i.Claims, + &i.CreatedBy, + &i.CreatedAt, + &i.LastUsed, + ) + return i, err +} + const listApiKeys = `-- name: ListApiKeys :many select pk, id, name, token, claims, created_by, created_at, last_used @@ -101,3 +132,17 @@ func (q *Queries) ListApiKeys(ctx context.Context) ([]Apikey, error) { } return items, nil } + +const touchApiKey = `-- name: TouchApiKey :exec +update + apikeys +set + last_used = now()::timestamptz +where + pk = $1 +` + +func (q *Queries) TouchApiKey(ctx context.Context, pk int32) error { + _, err := q.db.Exec(ctx, touchApiKey, pk) + return err +} diff --git a/auth/dbc/sessions.sql.go b/auth/dbc/sessions.sql.go index 212ab91f..bcb81869 100644 --- a/auth/dbc/sessions.sql.go +++ b/auth/dbc/sessions.sql.go @@ -88,6 +88,7 @@ func (q *Queries) DeleteSession(ctx context.Context, arg DeleteSessionParams) (S const getUserFromToken = `-- name: GetUserFromToken :one select + s.pk, s.id, s.last_used, u.pk, u.id, u.username, u.email, u.password, u.claims, u.created_date, u.last_seen @@ -100,6 +101,7 @@ limit 1 ` type GetUserFromTokenRow struct { + Pk int32 `json:"pk"` Id uuid.UUID `json:"id"` LastUsed time.Time `json:"lastUsed"` User User `json:"user"` @@ -109,6 +111,7 @@ func (q *Queries) GetUserFromToken(ctx context.Context, token string) (GetUserFr row := q.db.QueryRow(ctx, getUserFromToken, token) var i GetUserFromTokenRow err := row.Scan( + &i.Pk, &i.Id, &i.LastUsed, &i.User.Pk, @@ -169,10 +172,10 @@ update set last_used = now()::timestamptz where - id = $1 + pk = $1 ` -func (q *Queries) TouchSession(ctx context.Context, id uuid.UUID) error { - _, err := q.db.Exec(ctx, touchSession, id) +func (q *Queries) TouchSession(ctx context.Context, pk int32) error { + _, err := q.db.Exec(ctx, touchSession, pk) return err } diff --git a/auth/dbc/users.sql.go b/auth/dbc/users.sql.go index ecc89fbb..02964f00 100644 --- a/auth/dbc/users.sql.go +++ b/auth/dbc/users.sql.go @@ -261,11 +261,11 @@ update set last_used = now()::timestamptz where - id = $1 + pk = $1 ` -func (q *Queries) TouchUser(ctx context.Context, id uuid.UUID) error { - _, err := q.db.Exec(ctx, touchUser, id) +func (q *Queries) TouchUser(ctx context.Context, pk int32) error { + _, err := q.db.Exec(ctx, touchUser, pk) return err } diff --git a/auth/jwt.go b/auth/jwt.go index c854da19..9efa4f83 100644 --- a/auth/jwt.go +++ b/auth/jwt.go @@ -9,8 +9,10 @@ import ( "time" "github.com/golang-jwt/jwt/v5" + "github.com/jackc/pgx/v5" "github.com/labstack/echo/v4" "github.com/lestrrat-go/jwx/jwk" + "github.com/zoriya/kyoo/keibi/dbc" ) type Jwt struct { @@ -19,7 +21,7 @@ type Jwt struct { } // @Summary Get JWT -// @Description Convert a session token to a short lived JWT. +// @Description Convert a session token or an API key to a short lived JWT. // @Tags jwt // @Produce json // @Security Token @@ -28,6 +30,17 @@ type Jwt struct { // @Header 200 {string} Authorization "Jwt (same value as the returned token)" // @Router /jwt [get] func (h *Handler) CreateJwt(c echo.Context) error { + apikey := c.Request().Header.Get("X-Api-Key") + if apikey != "" { + token, err := h.createApiJwt(apikey) + if err != nil { + return err + } + return c.JSON(http.StatusOK, Jwt{ + Token: &token, + }) + } + auth := c.Request().Header.Get("Authorization") var jwt *string @@ -85,8 +98,8 @@ func (h *Handler) createJwt(token string) (string, error) { } go func() { - h.db.TouchSession(context.Background(), session.Id) - h.db.TouchUser(context.Background(), session.User.Id) + h.db.TouchSession(context.Background(), session.Pk) + h.db.TouchUser(context.Background(), session.User.Pk) }() claims := maps.Clone(session.User.Claims) @@ -108,6 +121,45 @@ func (h *Handler) createJwt(token string) (string, error) { return t, nil } +func (h *Handler) createApiJwt(apikey string) (string, error) { + info := strings.Split(apikey, "-") + if len(info) != 2 { + return "", echo.NewHTTPError(http.StatusForbidden, "Invalid api key format") + } + + key, err := h.db.GetApiKey(context.Background(), dbc.GetApiKeyParams{ + Name: info[0], + Token: info[1], + }) + if err == pgx.ErrNoRows { + return "", echo.NewHTTPError(http.StatusForbidden, "Invalid api key") + } else if err != nil { + return "", err + } + + go func() { + h.db.TouchApiKey(context.Background(), key.Pk) + }() + + claims := maps.Clone(key.Claims) + claims["username"] = key.Name + claims["sub"] = key.Id + claims["sid"] = key.Id + claims["iss"] = h.config.PublicUrl + claims["iat"] = &jwt.NumericDate{ + Time: time.Now().UTC(), + } + claims["exp"] = &jwt.NumericDate{ + Time: time.Now().UTC().Add(time.Hour), + } + jwt := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + t, err := jwt.SignedString(h.config.JwtPrivateKey) + if err != nil { + return "", err + } + return t, nil +} + // only used for the swagger doc type JwkSet struct { Keys []struct { diff --git a/auth/main.go b/auth/main.go index 20dbea8c..a48aebd8 100644 --- a/auth/main.go +++ b/auth/main.go @@ -131,24 +131,34 @@ type Handler struct { func (h *Handler) TokenToJwt(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - auth := c.Request().Header.Get("Authorization") var jwt *string - if auth == "" || !strings.HasPrefix(auth, "Bearer ") { - jwt = h.createGuestJwt() - } else { - token := auth[len("Bearer "):] - // this is only used to check if it is a session token or a jwt - _, err := base64.RawURLEncoding.DecodeString(token) - if err != nil { - return next(c) - } - - tkn, err := h.createJwt(token) + apikey := c.Request().Header.Get("X-Api-Key") + if apikey != "" { + token, err := h.createApiJwt(apikey) if err != nil { return err } - jwt = &tkn + jwt = &token + } else { + auth := c.Request().Header.Get("Authorization") + + if auth == "" || !strings.HasPrefix(auth, "Bearer ") { + jwt = h.createGuestJwt() + } else { + token := auth[len("Bearer "):] + // this is only used to check if it is a session token or a jwt + _, err := base64.RawURLEncoding.DecodeString(token) + if err != nil { + return next(c) + } + + tkn, err := h.createJwt(token) + if err != nil { + return err + } + jwt = &tkn + } } if jwt != nil { diff --git a/auth/sql/queries/apikeys.sql b/auth/sql/queries/apikeys.sql index fb02171e..257dfe2f 100644 --- a/auth/sql/queries/apikeys.sql +++ b/auth/sql/queries/apikeys.sql @@ -1,3 +1,20 @@ +-- name: GetApiKey :one +select + * +from + apikeys +where + name = $1 + and token = $2; + +-- name: TouchApiKey :exec +update + apikeys +set + last_used = now()::timestamptz +where + pk = $1; + -- name: ListApiKeys :many select * diff --git a/auth/sql/queries/sessions.sql b/auth/sql/queries/sessions.sql index b665848f..a2a06727 100644 --- a/auth/sql/queries/sessions.sql +++ b/auth/sql/queries/sessions.sql @@ -1,5 +1,6 @@ -- name: GetUserFromToken :one select + s.pk, s.id, s.last_used, sqlc.embed(u) @@ -16,7 +17,7 @@ update set last_used = now()::timestamptz where - id = $1; + pk = $1; -- name: GetUserSessions :many select diff --git a/auth/sql/queries/users.sql b/auth/sql/queries/users.sql index 8bc00feb..b80181c2 100644 --- a/auth/sql/queries/users.sql +++ b/auth/sql/queries/users.sql @@ -49,7 +49,7 @@ update set last_used = now()::timestamptz where - id = $1; + pk = $1; -- name: CreateUser :one insert into users(username, email, password, claims)