Add custom claims for first user

This commit is contained in:
Zoe Roux 2025-04-04 21:21:29 +02:00
parent 8ef4fe5e55
commit 431055ec49
No known key found for this signature in database
7 changed files with 61 additions and 12 deletions

View File

@ -7,6 +7,11 @@ KEIBI_PREFIX=""
# path of the private key used to sign jwts. If this is empty, a new one will be generated on startup # path of the private key used to sign jwts. If this is empty, a new one will be generated on startup
RSA_PRIVATE_KEY_PATH="" RSA_PRIVATE_KEY_PATH=""
# json object with the claims to add to every jwt (this is read when creating a new user)
EXTRA_CLAIMS='{}'
# json object with the claims to add to every jwt of the FIRST user (this can be used to mark the first user as admin).
# Those claims are merged with the `EXTRA_CLAIMS`.
FIRST_USER_CLAIMS='{}'
# The url you can use to reach your kyoo instance. This is used during oidc to redirect users to your instance. # The url you can use to reach your kyoo instance. This is used during oidc to redirect users to your instance.
PUBLIC_URL=http://localhost:8901 PUBLIC_URL=http://localhost:8901

View File

@ -1,11 +1,12 @@
package main package main
import ( import (
"context"
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/json"
"encoding/pem" "encoding/pem"
"maps"
"os" "os"
"time" "time"
@ -19,11 +20,13 @@ type Configuration struct {
JwtPublicKey *rsa.PublicKey JwtPublicKey *rsa.PublicKey
PublicUrl string PublicUrl string
DefaultClaims jwt.MapClaims DefaultClaims jwt.MapClaims
FirstUserClaims jwt.MapClaims
ExpirationDelay time.Duration ExpirationDelay time.Duration
} }
var DefaultConfig = Configuration{ var DefaultConfig = Configuration{
DefaultClaims: make(jwt.MapClaims), DefaultClaims: make(jwt.MapClaims),
FirstUserClaims: make(jwt.MapClaims),
ExpirationDelay: 30 * 24 * time.Hour, ExpirationDelay: 30 * 24 * time.Hour,
} }
@ -33,6 +36,25 @@ func LoadConfiguration(db *dbc.Queries) (*Configuration, error) {
ret.PublicUrl = os.Getenv("PUBLIC_URL") ret.PublicUrl = os.Getenv("PUBLIC_URL")
ret.Prefix = os.Getenv("KEIBI_PREFIX") ret.Prefix = os.Getenv("KEIBI_PREFIX")
claims := os.Getenv("EXTRA_CLAIMS")
if claims != "" {
err := json.Unmarshal([]byte(claims), &ret.DefaultClaims)
if err != nil {
return nil, err
}
}
claims = os.Getenv("FIRST_USER_CLAIMS")
if claims != "" {
err := json.Unmarshal([]byte(claims), &ret.FirstUserClaims)
if err != nil {
return nil, err
}
maps.Insert(ret.FirstUserClaims, maps.All(ret.DefaultClaims))
} else {
ret.FirstUserClaims = ret.DefaultClaims
}
rsa_pk_path := os.Getenv("RSA_PRIVATE_KEY_PATH") rsa_pk_path := os.Getenv("RSA_PRIVATE_KEY_PATH")
if rsa_pk_path != "" { if rsa_pk_path != "" {
privateKeyData, err := os.ReadFile(rsa_pk_path) privateKeyData, err := os.ReadFile(rsa_pk_path)

View File

@ -14,16 +14,25 @@ import (
const createUser = `-- name: CreateUser :one const createUser = `-- name: CreateUser :one
insert into users(username, email, password, claims) insert into users(username, email, password, claims)
values ($1, $2, $3, $4) values ($1, $2, $3, case when not exists (
select
pk, id, username, email, password, claims, created_date, last_seen
from
users) then
$4::jsonb
else
$5::jsonb
end)
returning returning
pk, id, username, email, password, claims, created_date, last_seen pk, id, username, email, password, claims, created_date, last_seen
` `
type CreateUserParams struct { type CreateUserParams struct {
Username string `json:"username"` Username string `json:"username"`
Email string `json:"email"` Email string `json:"email"`
Password *string `json:"password"` Password *string `json:"password"`
Claims jwt.MapClaims `json:"claims"` FirstClaims interface{} `json:"firstClaims"`
Claims interface{} `json:"claims"`
} }
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
@ -31,6 +40,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
arg.Username, arg.Username,
arg.Email, arg.Email,
arg.Password, arg.Password,
arg.FirstClaims,
arg.Claims, arg.Claims,
) )
var i User var i User

View File

@ -51,7 +51,15 @@ where
-- name: CreateUser :one -- name: CreateUser :one
insert into users(username, email, password, claims) insert into users(username, email, password, claims)
values ($1, $2, $3, $4) values ($1, $2, $3, case when not exists (
select
*
from
users) then
sqlc.arg(first_claims)::jsonb
else
sqlc.arg(claims)::jsonb
end)
returning returning
*; *;

View File

@ -27,6 +27,9 @@ sql:
go_type: go_type:
import: "github.com/google/uuid" import: "github.com/google/uuid"
type: "UUID" type: "UUID"
- db_type: "jsonb"
go_type:
type: "interface{}"
- column: "users.claims" - column: "users.claims"
go_type: go_type:
import: "github.com/golang-jwt/jwt/v5" import: "github.com/golang-jwt/jwt/v5"

View File

@ -208,10 +208,11 @@ func (h *Handler) Register(c echo.Context) error {
} }
duser, err := h.db.CreateUser(context.Background(), dbc.CreateUserParams{ duser, err := h.db.CreateUser(context.Background(), dbc.CreateUserParams{
Username: req.Username, Username: req.Username,
Email: req.Email, Email: req.Email,
Password: &pass, Password: &pass,
Claims: h.config.DefaultClaims, Claims: h.config.DefaultClaims,
FirstClaims: h.config.FirstUserClaims,
}) })
if ErrIs(err, pgerrcode.UniqueViolation) { if ErrIs(err, pgerrcode.UniqueViolation) {
return echo.NewHTTPError(409, "Email or username already taken") return echo.NewHTTPError(409, "Email or username already taken")

View File

@ -40,7 +40,7 @@ in
go-migrate go-migrate
sqlc sqlc
go-swag go-swag
robotframework-tidy # robotframework-tidy
bun bun
pkg-config pkg-config
node-gyp node-gyp