mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add apikey support to /jwt
This commit is contained in:
parent
85186a74c8
commit
a72ecdb21b
@ -41,7 +41,7 @@ func MapDbKey(key *dbc.Apikey) ApiKeyWToken {
|
|||||||
CreatedAt: key.CreatedAt,
|
CreatedAt: key.CreatedAt,
|
||||||
LastUsed: key.LastUsed,
|
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{
|
dbkey, err := h.db.CreateApiKey(context.Background(), dbc.CreateApiKeyParams{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Token: fmt.Sprintf("%s-%s", req.Name, base64.RawURLEncoding.EncodeToString(id)),
|
Token: base64.RawURLEncoding.EncodeToString(id),
|
||||||
Claims: req.Claims,
|
Claims: req.Claims,
|
||||||
})
|
})
|
||||||
if ErrIs(err, pgerrcode.UniqueViolation) {
|
if ErrIs(err, pgerrcode.UniqueViolation) {
|
||||||
|
@ -64,6 +64,37 @@ func (q *Queries) DeleteApiKey(ctx context.Context, id uuid.UUID) (Apikey, error
|
|||||||
return i, err
|
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
|
const listApiKeys = `-- name: ListApiKeys :many
|
||||||
select
|
select
|
||||||
pk, id, name, token, claims, created_by, created_at, last_used
|
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
|
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
|
||||||
|
}
|
||||||
|
@ -88,6 +88,7 @@ func (q *Queries) DeleteSession(ctx context.Context, arg DeleteSessionParams) (S
|
|||||||
|
|
||||||
const getUserFromToken = `-- name: GetUserFromToken :one
|
const getUserFromToken = `-- name: GetUserFromToken :one
|
||||||
select
|
select
|
||||||
|
s.pk,
|
||||||
s.id,
|
s.id,
|
||||||
s.last_used,
|
s.last_used,
|
||||||
u.pk, u.id, u.username, u.email, u.password, u.claims, u.created_date, u.last_seen
|
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 {
|
type GetUserFromTokenRow struct {
|
||||||
|
Pk int32 `json:"pk"`
|
||||||
Id uuid.UUID `json:"id"`
|
Id uuid.UUID `json:"id"`
|
||||||
LastUsed time.Time `json:"lastUsed"`
|
LastUsed time.Time `json:"lastUsed"`
|
||||||
User User `json:"user"`
|
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)
|
row := q.db.QueryRow(ctx, getUserFromToken, token)
|
||||||
var i GetUserFromTokenRow
|
var i GetUserFromTokenRow
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
|
&i.Pk,
|
||||||
&i.Id,
|
&i.Id,
|
||||||
&i.LastUsed,
|
&i.LastUsed,
|
||||||
&i.User.Pk,
|
&i.User.Pk,
|
||||||
@ -169,10 +172,10 @@ update
|
|||||||
set
|
set
|
||||||
last_used = now()::timestamptz
|
last_used = now()::timestamptz
|
||||||
where
|
where
|
||||||
id = $1
|
pk = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) TouchSession(ctx context.Context, id uuid.UUID) error {
|
func (q *Queries) TouchSession(ctx context.Context, pk int32) error {
|
||||||
_, err := q.db.Exec(ctx, touchSession, id)
|
_, err := q.db.Exec(ctx, touchSession, pk)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -261,11 +261,11 @@ update
|
|||||||
set
|
set
|
||||||
last_used = now()::timestamptz
|
last_used = now()::timestamptz
|
||||||
where
|
where
|
||||||
id = $1
|
pk = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) TouchUser(ctx context.Context, id uuid.UUID) error {
|
func (q *Queries) TouchUser(ctx context.Context, pk int32) error {
|
||||||
_, err := q.db.Exec(ctx, touchUser, id)
|
_, err := q.db.Exec(ctx, touchUser, pk)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
58
auth/jwt.go
58
auth/jwt.go
@ -9,8 +9,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/lestrrat-go/jwx/jwk"
|
"github.com/lestrrat-go/jwx/jwk"
|
||||||
|
"github.com/zoriya/kyoo/keibi/dbc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Jwt struct {
|
type Jwt struct {
|
||||||
@ -19,7 +21,7 @@ type Jwt struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Get JWT
|
// @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
|
// @Tags jwt
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security Token
|
// @Security Token
|
||||||
@ -28,6 +30,17 @@ type Jwt struct {
|
|||||||
// @Header 200 {string} Authorization "Jwt (same value as the returned token)"
|
// @Header 200 {string} Authorization "Jwt (same value as the returned token)"
|
||||||
// @Router /jwt [get]
|
// @Router /jwt [get]
|
||||||
func (h *Handler) CreateJwt(c echo.Context) error {
|
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")
|
auth := c.Request().Header.Get("Authorization")
|
||||||
var jwt *string
|
var jwt *string
|
||||||
|
|
||||||
@ -85,8 +98,8 @@ func (h *Handler) createJwt(token string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
h.db.TouchSession(context.Background(), session.Id)
|
h.db.TouchSession(context.Background(), session.Pk)
|
||||||
h.db.TouchUser(context.Background(), session.User.Id)
|
h.db.TouchUser(context.Background(), session.User.Pk)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
claims := maps.Clone(session.User.Claims)
|
claims := maps.Clone(session.User.Claims)
|
||||||
@ -108,6 +121,45 @@ func (h *Handler) createJwt(token string) (string, error) {
|
|||||||
return t, nil
|
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
|
// only used for the swagger doc
|
||||||
type JwkSet struct {
|
type JwkSet struct {
|
||||||
Keys []struct {
|
Keys []struct {
|
||||||
|
36
auth/main.go
36
auth/main.go
@ -131,24 +131,34 @@ type Handler struct {
|
|||||||
|
|
||||||
func (h *Handler) TokenToJwt(next echo.HandlerFunc) echo.HandlerFunc {
|
func (h *Handler) TokenToJwt(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
auth := c.Request().Header.Get("Authorization")
|
|
||||||
var jwt *string
|
var jwt *string
|
||||||
|
|
||||||
if auth == "" || !strings.HasPrefix(auth, "Bearer ") {
|
apikey := c.Request().Header.Get("X-Api-Key")
|
||||||
jwt = h.createGuestJwt()
|
if apikey != "" {
|
||||||
} else {
|
token, err := h.createApiJwt(apikey)
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
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 {
|
if jwt != nil {
|
||||||
|
@ -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
|
-- name: ListApiKeys :many
|
||||||
select
|
select
|
||||||
*
|
*
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
-- name: GetUserFromToken :one
|
-- name: GetUserFromToken :one
|
||||||
select
|
select
|
||||||
|
s.pk,
|
||||||
s.id,
|
s.id,
|
||||||
s.last_used,
|
s.last_used,
|
||||||
sqlc.embed(u)
|
sqlc.embed(u)
|
||||||
@ -16,7 +17,7 @@ update
|
|||||||
set
|
set
|
||||||
last_used = now()::timestamptz
|
last_used = now()::timestamptz
|
||||||
where
|
where
|
||||||
id = $1;
|
pk = $1;
|
||||||
|
|
||||||
-- name: GetUserSessions :many
|
-- name: GetUserSessions :many
|
||||||
select
|
select
|
||||||
|
@ -49,7 +49,7 @@ update
|
|||||||
set
|
set
|
||||||
last_used = now()::timestamptz
|
last_used = now()::timestamptz
|
||||||
where
|
where
|
||||||
id = $1;
|
pk = $1;
|
||||||
|
|
||||||
-- name: CreateUser :one
|
-- name: CreateUser :one
|
||||||
insert into users(username, email, password, claims)
|
insert into users(username, email, password, claims)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user