Add apikey support to /jwt

This commit is contained in:
Zoe Roux 2025-04-21 14:54:07 +02:00
parent 85186a74c8
commit a72ecdb21b
No known key found for this signature in database
9 changed files with 154 additions and 26 deletions

View File

@ -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) {

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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
*

View File

@ -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

View File

@ -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)