mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Allow env to specify apikeys
This commit is contained in:
parent
a72ecdb21b
commit
e8acb31834
@ -22,6 +22,12 @@ PROTECTED_CLAIMS="permissions"
|
|||||||
# 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
|
||||||
|
|
||||||
|
# You can create apikeys at runtime via POST /apikey but you can also have some defined in the env.
|
||||||
|
# Replace $YOURNAME with the name of the key you want (only alpha are valid)
|
||||||
|
# The value will be the apikey (max 128 bytes)
|
||||||
|
# KEIBI_APIKEY_$YOURNAME=oaeushtaoesunthoaensuth
|
||||||
|
# KEIBI_APIKEY_$YOURNAME_CLAIMS='{"permissions": ["users.read"]}'
|
||||||
|
|
||||||
# Database things
|
# Database things
|
||||||
POSTGRES_USER=kyoo
|
POSTGRES_USER=kyoo
|
||||||
POSTGRES_PASSWORD=password
|
POSTGRES_PASSWORD=password
|
||||||
|
@ -5,7 +5,9 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
@ -17,6 +19,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ApiKey struct {
|
type ApiKey struct {
|
||||||
|
Id uuid.UUID `json:"id" example:"e05089d6-9179-4b5b-a63e-94dd5fc2a397"`
|
||||||
Name string `json:"name" example:"myapp"`
|
Name string `json:"name" example:"myapp"`
|
||||||
CreatedAt time.Time `json:"createAt" example:"2025-03-29T18:20:05.267Z"`
|
CreatedAt time.Time `json:"createAt" example:"2025-03-29T18:20:05.267Z"`
|
||||||
LastUsed time.Time `json:"lastUsed" example:"2025-03-29T18:20:05.267Z"`
|
LastUsed time.Time `json:"lastUsed" example:"2025-03-29T18:20:05.267Z"`
|
||||||
@ -36,6 +39,7 @@ type ApiKeyDto struct {
|
|||||||
func MapDbKey(key *dbc.Apikey) ApiKeyWToken {
|
func MapDbKey(key *dbc.Apikey) ApiKeyWToken {
|
||||||
return ApiKeyWToken{
|
return ApiKeyWToken{
|
||||||
ApiKey: ApiKey{
|
ApiKey: ApiKey{
|
||||||
|
Id: key.Id,
|
||||||
Name: key.Name,
|
Name: key.Name,
|
||||||
Claims: key.Claims,
|
Claims: key.Claims,
|
||||||
CreatedAt: key.CreatedAt,
|
CreatedAt: key.CreatedAt,
|
||||||
@ -66,6 +70,10 @@ func (h *Handler) CreateApiKey(c echo.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, conflict := h.config.EnvApiKeys[req.Name]; conflict {
|
||||||
|
return echo.NewHTTPError(409, "An env apikey is already defined with the same name")
|
||||||
|
}
|
||||||
|
|
||||||
id := make([]byte, 64)
|
id := make([]byte, 64)
|
||||||
_, err = rand.Read(id)
|
_, err = rand.Read(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -127,8 +135,57 @@ func (h *Handler) ListApiKey(c echo.Context) error {
|
|||||||
for _, key := range dbkeys {
|
for _, key := range dbkeys {
|
||||||
ret = append(ret, MapDbKey(&key).ApiKey)
|
ret = append(ret, MapDbKey(&key).ApiKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, key := range h.config.EnvApiKeys {
|
||||||
|
ret = append(ret, key.ApiKey)
|
||||||
|
}
|
||||||
|
|
||||||
return c.JSON(200, Page[ApiKey]{
|
return c.JSON(200, Page[ApiKey]{
|
||||||
Items: ret,
|
Items: ret,
|
||||||
This: c.Request().URL.String(),
|
This: c.Request().URL.String(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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, fromEnv := h.config.EnvApiKeys[info[0]]
|
||||||
|
if !fromEnv {
|
||||||
|
dbKey, 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(), dbKey.Pk)
|
||||||
|
}()
|
||||||
|
|
||||||
|
key = MapDbKey(&dbKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@ -25,6 +27,7 @@ type Configuration struct {
|
|||||||
GuestClaims jwt.MapClaims
|
GuestClaims jwt.MapClaims
|
||||||
ProtectedClaims []string
|
ProtectedClaims []string
|
||||||
ExpirationDelay time.Duration
|
ExpirationDelay time.Duration
|
||||||
|
EnvApiKeys map[string]ApiKeyWToken
|
||||||
}
|
}
|
||||||
|
|
||||||
var DefaultConfig = Configuration{
|
var DefaultConfig = Configuration{
|
||||||
@ -32,6 +35,7 @@ var DefaultConfig = Configuration{
|
|||||||
FirstUserClaims: make(jwt.MapClaims),
|
FirstUserClaims: make(jwt.MapClaims),
|
||||||
ProtectedClaims: []string{"permissions"},
|
ProtectedClaims: []string{"permissions"},
|
||||||
ExpirationDelay: 30 * 24 * time.Hour,
|
ExpirationDelay: 30 * 24 * time.Hour,
|
||||||
|
EnvApiKeys: make(map[string]ApiKeyWToken),
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfiguration(db *dbc.Queries) (*Configuration, error) {
|
func LoadConfiguration(db *dbc.Queries) (*Configuration, error) {
|
||||||
@ -98,5 +102,44 @@ func LoadConfiguration(db *dbc.Queries) (*Configuration, error) {
|
|||||||
ret.JwtPublicKey = &ret.JwtPrivateKey.PublicKey
|
ret.JwtPublicKey = &ret.JwtPrivateKey.PublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, env := range os.Environ() {
|
||||||
|
if !strings.HasPrefix(env, "KEIBI_APIKEY_") || strings.HasSuffix(env, "_CLAIMS") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
v := strings.Split(env, "=")
|
||||||
|
name := strings.TrimPrefix(v[0], "KEIBI_APIKEY_")
|
||||||
|
cstr := os.Getenv(fmt.Sprintf("KEIBI_APIKEY_%s_CLAIMS", name))
|
||||||
|
|
||||||
|
var claims jwt.MapClaims
|
||||||
|
if cstr != "" {
|
||||||
|
err := json.Unmarshal([]byte(cstr), &claims)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.EnvApiKeys[name] = ApiKeyWToken{
|
||||||
|
ApiKey: ApiKey{
|
||||||
|
Name: name,
|
||||||
|
Claims: claims,
|
||||||
|
},
|
||||||
|
Token: v[1],
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
apikeys, err := db.ListApiKeys(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, key := range apikeys {
|
||||||
|
if _, defined := ret.EnvApiKeys[key.Name]; defined {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"an api key with the name %s is already defined in database. Can't specify a new one via env var",
|
||||||
|
key.Name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &ret, nil
|
return &ret, nil
|
||||||
}
|
}
|
||||||
|
41
auth/jwt.go
41
auth/jwt.go
@ -9,10 +9,8 @@ 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 {
|
||||||
@ -121,45 +119,6 @@ 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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user