mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-10-30 18:22:41 -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, | ||||
| 			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) { | ||||
|  | ||||
| @ -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 | ||||
| } | ||||
|  | ||||
| @ -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 | ||||
| } | ||||
|  | ||||
| @ -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 | ||||
| } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										58
									
								
								auth/jwt.go
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								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 { | ||||
|  | ||||
							
								
								
									
										36
									
								
								auth/main.go
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								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 { | ||||
|  | ||||
| @ -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 | ||||
| 	* | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user