Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"os"
"strconv"
"strings"
"time"
)

// ServerPort returns the port the HTTP server should listen on.
Expand Down Expand Up @@ -197,6 +198,18 @@ func StaticAPIKey() string {
return key
}

// TokenTTL returns the default lifetime for bearer tokens issued by the server.
// It reads BIFROST_TOKEN_TTL (any Go duration string, e.g. "1h", "30m") and
// defaults to 24h when unset or unparseable.
func TokenTTL() time.Duration {
if v := os.Getenv("BIFROST_TOKEN_TTL"); v != "" {
if d, err := time.ParseDuration(v); err == nil && d > 0 {
return d
}
}
return 24 * time.Hour
}

// TrackTokens reports whether token counting is enabled via BIFROST_TRACK_TOKENS=true.
func TrackTokens() bool {
switch os.Getenv("BIFROST_TRACK_TOKENS") {
Expand Down
25 changes: 24 additions & 1 deletion docs/swagger/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -1354,21 +1354,40 @@ const docTemplate = `{
"BearerAuth": []
}
],
"description": "Accepts a valid bearer token and returns a new one with a fresh 24h expiry.",
"description": "Accepts a valid bearer token and returns a new one. An optional JSON body may specify a \"ttl\" field (e.g. \"1h\"); falls back to BIFROST_TOKEN_TTL or 24h.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Refresh bearer token",
"parameters": [
{
"description": "Optional TTL override: {\\",
"name": "body",
"in": "body",
"schema": {
"type": "object"
}
}
],
"responses": {
"200": {
"description": "New token: {\\\"token\\\":\\\"...\\\"}",
"schema": {
"type": "object"
}
},
"400": {
"description": "invalid ttl",
"schema": {
"$ref": "#/definitions/routes.ErrorResponse"
}
},
"401": {
"description": "invalid or expired token",
"schema": {
Expand Down Expand Up @@ -1612,6 +1631,10 @@ const docTemplate = `{
"role": {
"type": "string",
"example": "member"
},
"ttl": {
"type": "string",
"example": "1h"
}
}
},
Expand Down
25 changes: 24 additions & 1 deletion docs/swagger/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -1348,21 +1348,40 @@
"BearerAuth": []
}
],
"description": "Accepts a valid bearer token and returns a new one with a fresh 24h expiry.",
"description": "Accepts a valid bearer token and returns a new one. An optional JSON body may specify a \"ttl\" field (e.g. \"1h\"); falls back to BIFROST_TOKEN_TTL or 24h.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Refresh bearer token",
"parameters": [
{
"description": "Optional TTL override: {\\",
"name": "body",
"in": "body",
"schema": {
"type": "object"
}
}
],
"responses": {
"200": {
"description": "New token: {\\\"token\\\":\\\"...\\\"}",
"schema": {
"type": "object"
}
},
"400": {
"description": "invalid ttl",
"schema": {
"$ref": "#/definitions/routes.ErrorResponse"
}
},
"401": {
"description": "invalid or expired token",
"schema": {
Expand Down Expand Up @@ -1606,6 +1625,10 @@
"role": {
"type": "string",
"example": "member"
},
"ttl": {
"type": "string",
"example": "1h"
}
}
},
Expand Down
20 changes: 18 additions & 2 deletions docs/swagger/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ definitions:
role:
example: member
type: string
ttl:
example: 1h
type: string
type: object
routes.CreateUserResponse:
properties:
Expand Down Expand Up @@ -1040,15 +1043,28 @@ paths:
- setup
/v1/token/refresh:
post:
description: Accepts a valid bearer token and returns a new one with a fresh
24h expiry.
consumes:
- application/json
description: Accepts a valid bearer token and returns a new one. An optional
JSON body may specify a "ttl" field (e.g. "1h"); falls back to BIFROST_TOKEN_TTL
or 24h.
parameters:
- description: 'Optional TTL override: {\'
in: body
name: body
schema:
type: object
produces:
- application/json
responses:
"200":
description: 'New token: {\"token\":\"...\"}'
schema:
type: object
"400":
description: invalid ttl
schema:
$ref: '#/definitions/routes.ErrorResponse'
"401":
description: invalid or expired token
schema:
Expand Down
3 changes: 2 additions & 1 deletion routes/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"net/http"

"github.com/farovictor/bifrost/config"
"github.com/farovictor/bifrost/pkg/orgs"
"github.com/farovictor/bifrost/pkg/users"
"github.com/farovictor/bifrost/pkg/utils"
Expand Down Expand Up @@ -86,7 +87,7 @@ func (s *Server) Setup(w http.ResponseWriter, r *http.Request) {
return
}

token, err := buildAuthToken(u.ID, o.ID)
token, err := buildAuthToken(u.ID, o.ID, config.TokenTTL())
if err != nil {
writeError(w, "internal error", http.StatusInternalServerError)
return
Expand Down
50 changes: 44 additions & 6 deletions routes/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package routes

import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"

"github.com/farovictor/bifrost/config"
"github.com/farovictor/bifrost/pkg/auth"
"github.com/farovictor/bifrost/pkg/logging"
"github.com/farovictor/bifrost/pkg/orgs"
Expand All @@ -20,6 +22,7 @@ type CreateUserRequest struct {
OrgID string `json:"org_id" example:""`
OrgName string `json:"org_name" example:"Acme"`
Role string `json:"role" example:"member"`
TTL string `json:"ttl" example:"1h"`
}

// CreateUserResponse is returned on successful user creation.
Expand Down Expand Up @@ -126,7 +129,12 @@ func (s *Server) CreateUser(w http.ResponseWriter, r *http.Request) {
}
}

token, err := buildAuthToken(u.ID, orgID)
ttl, err := parseTTL(req.TTL)
if err != nil {
writeError(w, "invalid ttl: use a Go duration string (e.g. \"1h\", \"30m\")", http.StatusBadRequest)
return
}
token, err := buildAuthToken(u.ID, orgID, ttl)
if err != nil {
writeError(w, "internal error", http.StatusInternalServerError)
return
Expand All @@ -143,13 +151,16 @@ func (s *Server) CreateUser(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(resp)
}

// RefreshToken handles POST /token/refresh and issues a fresh 24h token.
// RefreshToken handles POST /token/refresh and issues a fresh token.
//
// @Summary Refresh bearer token
// @Description Accepts a valid bearer token and returns a new one with a fresh 24h expiry.
// @Description Accepts a valid bearer token and returns a new one. An optional JSON body may specify a "ttl" field (e.g. "1h"); falls back to BIFROST_TOKEN_TTL or 24h.
// @Tags users
// @Accept json
// @Produce json
// @Param body body object false "Optional TTL override: {\"ttl\":\"1h\"}"
// @Success 200 {object} object "New token: {\"token\":\"...\"}"
// @Failure 400 {object} ErrorResponse "invalid ttl"
// @Failure 401 {object} ErrorResponse "invalid or expired token"
// @Failure 500 {object} ErrorResponse
// @Security BearerAuth
Expand All @@ -166,7 +177,20 @@ func (s *Server) RefreshToken(w http.ResponseWriter, r *http.Request) {
writeError(w, "unauthorized", http.StatusUnauthorized)
return
}
token, err := buildAuthToken(tok.UserID, tok.OrgID)

var body struct {
TTL string `json:"ttl"`
}
// Body is optional — ignore decode errors.
json.NewDecoder(r.Body).Decode(&body) //nolint:errcheck

ttl, err := parseTTL(body.TTL)
if err != nil {
writeError(w, "invalid ttl: use a Go duration string (e.g. \"1h\", \"30m\")", http.StatusBadRequest)
return
}

token, err := buildAuthToken(tok.UserID, tok.OrgID, ttl)
if err != nil {
writeError(w, "internal error", http.StatusInternalServerError)
return
Expand All @@ -177,11 +201,25 @@ func (s *Server) RefreshToken(w http.ResponseWriter, r *http.Request) {
}{Token: token})
}

func buildAuthToken(userID, orgID string) (string, error) {
// parseTTL converts a duration string to a time.Duration, falling back to
// config.TokenTTL() when s is empty. Returns an error when s is non-empty
// but unparseable or <= 0.
func parseTTL(s string) (time.Duration, error) {
if s == "" {
return config.TokenTTL(), nil
}
d, err := time.ParseDuration(s)
if err != nil || d <= 0 {
return 0, fmt.Errorf("invalid duration %q", s)
}
return d, nil
}

func buildAuthToken(userID, orgID string, ttl time.Duration) (string, error) {
t := auth.AuthToken{
UserID: userID,
OrgID: orgID,
ExpiresAt: time.Now().Add(24 * time.Hour),
ExpiresAt: time.Now().Add(ttl),
}
return auth.Sign(t)
}
Expand Down
1 change: 1 addition & 0 deletions tests/routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func setupRouter(s *routes.Server) http.Handler {
r.With(rl.OrgCtxMiddleware(s.MembershipStore)).Get("/user", s.GetUserInfo)
r.With(rl.OrgCtxMiddleware(s.MembershipStore)).Post("/user/rootkeys", s.CreateRootKey)

r.Post("/token/refresh", s.RefreshToken)
r.Post("/service-token", s.ServiceToken)
r.With(rl.RateLimitMiddleware(s.KeyStore)).Handle("/proxy/*", http.HandlerFunc(v1h.Proxy))

Expand Down
Loading
Loading