51 lines
1.6 KiB
Go
51 lines
1.6 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const SessionIdleSeconds = 30 * 24 * 60 * 60
|
|
const SessionExpiryHeader = "X-Foundry-Session-Expires-In"
|
|
|
|
var ErrInvalidSession = errors.New("invalid session")
|
|
|
|
type Session struct {
|
|
AccountID string
|
|
ExpiresIn int64
|
|
}
|
|
|
|
// AuthenticateSession atomically validates and renews a live session, bounded by
|
|
// its absolute deadline. Expired, revoked and wrong-project tokens never renew.
|
|
func AuthenticateSession(ctx context.Context, db *sql.DB, project, authorization string) (Session, error) {
|
|
value, ok := strings.CutPrefix(authorization, "Bearer ")
|
|
if !ok || !tokenPattern.MatchString(value) {
|
|
return Session{}, ErrInvalidSession
|
|
}
|
|
var session Session
|
|
err := db.QueryRowContext(ctx, `
|
|
UPDATE project_sessions
|
|
SET expires_at=LEAST(now()+interval '30 days',absolute_expires_at)
|
|
WHERE project=$1 AND token_hash=$2 AND expires_at>now() AND absolute_expires_at>now()
|
|
RETURNING account_id::text,GREATEST(0,FLOOR(EXTRACT(EPOCH FROM (expires_at-now()))))::bigint
|
|
`, project, digest(value)).Scan(&session.AccountID, &session.ExpiresIn)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return Session{}, ErrInvalidSession
|
|
}
|
|
return session, err
|
|
}
|
|
|
|
func WriteSessionHeaders(w http.ResponseWriter, session Session) {
|
|
w.Header().Set(SessionExpiryHeader, strconv.FormatInt(session.ExpiresIn, 10))
|
|
}
|
|
|
|
// Authenticate preserves the existing API for clients that only need an account ID.
|
|
func Authenticate(ctx context.Context, db *sql.DB, project, authorization string) (string, error) {
|
|
session, err := AuthenticateSession(ctx, db, project, authorization)
|
|
return session.AccountID, err
|
|
}
|