feat(proxy): add /auth/token and /auth/refresh endpoints (#18)
CI / CI (push) Successful in 4m2s
CI / CI (push) Successful in 4m2s
Closes homelab#6 (P3.1) and homelab#8 (P3.3)
## Endpoints
| Path | Method | Body | What it does |
|------|--------|------|-------------|
| `/auth/token` | POST | `{username, password, scope?}` | Password grant → JWT |
| `/auth/refresh` | POST | `{refresh_token, scope?}` | Refresh grant → new JWT |
Both proxy to Authentik `tokenUrl` (from P3.7 config). Upstream response forwarded verbatim — client sees Authentik errors directly.
This commit was merged in pull request #18.
This commit is contained in:
@@ -0,0 +1,160 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forgejo.riotpiao.com/rock/homelab-frontend/internal/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
// tokenRequest is the JSON body for POST /auth/token.
|
||||||
|
type tokenRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Scope string `json:"scope,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshRequest is the JSON body for POST /auth/refresh.
|
||||||
|
type refreshRequest struct {
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
Scope string `json:"scope,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// authClient handles token exchange with the upstream identity provider.
|
||||||
|
// Extracted for testability — production uses http.DefaultClient,
|
||||||
|
// tests inject a stub.
|
||||||
|
type authClient interface {
|
||||||
|
PostForm(url string, data url.Values) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// httpAuthClient wraps http.Client to implement authClient.
|
||||||
|
type httpAuthClient struct {
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *httpAuthClient) PostForm(url string, data url.Values) (*http.Response, error) {
|
||||||
|
return c.client.PostForm(url, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAuthClient() authClient {
|
||||||
|
return &httpAuthClient{
|
||||||
|
client: &http.Client{Timeout: 10 * time.Second},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAuthToken exchanges username+password for a JWT via the upstream
|
||||||
|
// identity provider's token endpoint using grant_type=password.
|
||||||
|
func (h *Handler) handleAuthToken(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
writeProblemDetail(w, http.StatusMethodNotAllowed,
|
||||||
|
"about:blank#method-not-allowed", "Method Not Allowed",
|
||||||
|
"POST only", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.config.Auth.TokenURL == "" || h.config.Auth.ClientID == "" {
|
||||||
|
writeProblemDetail(w, http.StatusServiceUnavailable,
|
||||||
|
"about:blank#not-configured", "Not Configured",
|
||||||
|
"token endpoint not configured", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req tokenRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeProblemDetail(w, http.StatusBadRequest,
|
||||||
|
"about:blank#bad-request", "Bad Request",
|
||||||
|
"invalid JSON body", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Username == "" || req.Password == "" {
|
||||||
|
writeProblemDetail(w, http.StatusBadRequest,
|
||||||
|
"about:blank#bad-request", "Bad Request",
|
||||||
|
"username and password are required", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scope := req.Scope
|
||||||
|
if scope == "" {
|
||||||
|
scope = "openid roles permissions"
|
||||||
|
}
|
||||||
|
|
||||||
|
form := url.Values{
|
||||||
|
"grant_type": {"password"},
|
||||||
|
"username": {req.Username},
|
||||||
|
"password": {req.Password},
|
||||||
|
"client_id": {h.config.Auth.ClientID},
|
||||||
|
"client_secret": {h.config.Auth.ClientSecret},
|
||||||
|
"scope": {scope},
|
||||||
|
}
|
||||||
|
|
||||||
|
h.forwardTokenResponse(w, form, "/auth/token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAuthRefresh exchanges a refresh token for a new JWT.
|
||||||
|
func (h *Handler) handleAuthRefresh(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
writeProblemDetail(w, http.StatusMethodNotAllowed,
|
||||||
|
"about:blank#method-not-allowed", "Method Not Allowed",
|
||||||
|
"POST only", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.config.Auth.TokenURL == "" || h.config.Auth.ClientID == "" {
|
||||||
|
writeProblemDetail(w, http.StatusServiceUnavailable,
|
||||||
|
"about:blank#not-configured", "Not Configured",
|
||||||
|
"token endpoint not configured", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req refreshRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeProblemDetail(w, http.StatusBadRequest,
|
||||||
|
"about:blank#bad-request", "Bad Request",
|
||||||
|
"invalid JSON body", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.RefreshToken == "" {
|
||||||
|
writeProblemDetail(w, http.StatusBadRequest,
|
||||||
|
"about:blank#bad-request", "Bad Request",
|
||||||
|
"refresh_token is required", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form := url.Values{
|
||||||
|
"grant_type": {"refresh_token"},
|
||||||
|
"refresh_token": {req.RefreshToken},
|
||||||
|
"client_id": {h.config.Auth.ClientID},
|
||||||
|
"client_secret": {h.config.Auth.ClientSecret},
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Scope != "" {
|
||||||
|
form.Set("scope", req.Scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.forwardTokenResponse(w, form, "/auth/refresh")
|
||||||
|
}
|
||||||
|
|
||||||
|
// forwardTokenResponse posts form data to the token endpoint and
|
||||||
|
// forwards the response verbatim to the client.
|
||||||
|
func (h *Handler) forwardTokenResponse(w http.ResponseWriter, form url.Values, logPath string) {
|
||||||
|
resp, err := h.authHTTP.PostForm(h.config.Auth.TokenURL, form)
|
||||||
|
if err != nil {
|
||||||
|
writeProblemDetail(w, http.StatusBadGateway,
|
||||||
|
"about:blank#bad-gateway", "Bad Gateway",
|
||||||
|
"identity provider unreachable", nil)
|
||||||
|
logging.Errorf("auth upstream error", err, map[string]string{
|
||||||
|
"path": logPath,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(resp.StatusCode)
|
||||||
|
io.Copy(w, resp.Body)
|
||||||
|
}
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// stubAuthClient captures the form data sent and returns a canned response.
|
||||||
|
type stubAuthClient struct {
|
||||||
|
lastForm url.Values
|
||||||
|
statusCode int
|
||||||
|
body string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubAuthClient) PostForm(u string, data url.Values) (*http.Response, error) {
|
||||||
|
s.lastForm = data
|
||||||
|
if s.err != nil {
|
||||||
|
return nil, s.err
|
||||||
|
}
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: s.statusCode,
|
||||||
|
Body: io.NopCloser(strings.NewReader(s.body)),
|
||||||
|
Header: http.Header{"Content-Type": {"application/json"}},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestHandler(tokenURL, clientID, clientSecret string, client authClient) *Handler {
|
||||||
|
cfg := &config.Config{
|
||||||
|
Auth: config.AuthConfig{
|
||||||
|
TokenURL: tokenURL,
|
||||||
|
ClientID: clientID,
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
h := &Handler{
|
||||||
|
config: cfg,
|
||||||
|
authHTTP: client,
|
||||||
|
routes: make(map[string]*Route),
|
||||||
|
transports: make(map[string]*http.Transport),
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthToken_Success(t *testing.T) {
|
||||||
|
stub := &stubAuthClient{
|
||||||
|
statusCode: 200,
|
||||||
|
body: `{"access_token":"jwt.token.here","refresh_token":"refresh","expires_in":3600}`,
|
||||||
|
}
|
||||||
|
h := newTestHandler("https://auth.example.com/token/", "api-gw", "secret", stub)
|
||||||
|
|
||||||
|
body := `{"username":"rock","password":"pass123"}`
|
||||||
|
r := httptest.NewRequest("POST", "/auth/token", strings.NewReader(body))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.handleAuthToken(w, r)
|
||||||
|
|
||||||
|
if w.Code != 200 {
|
||||||
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify form sent to upstream
|
||||||
|
if stub.lastForm.Get("grant_type") != "password" {
|
||||||
|
t.Errorf("grant_type = %q, want password", stub.lastForm.Get("grant_type"))
|
||||||
|
}
|
||||||
|
if stub.lastForm.Get("username") != "rock" {
|
||||||
|
t.Errorf("username = %q, want rock", stub.lastForm.Get("username"))
|
||||||
|
}
|
||||||
|
if stub.lastForm.Get("client_id") != "api-gw" {
|
||||||
|
t.Errorf("client_id = %q, want api-gw", stub.lastForm.Get("client_id"))
|
||||||
|
}
|
||||||
|
if stub.lastForm.Get("client_secret") != "secret" {
|
||||||
|
t.Errorf("client_secret = %q, want secret", stub.lastForm.Get("client_secret"))
|
||||||
|
}
|
||||||
|
if stub.lastForm.Get("scope") != "openid roles permissions" {
|
||||||
|
t.Errorf("scope = %q, want default scope", stub.lastForm.Get("scope"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify response forwarded
|
||||||
|
var resp map[string]interface{}
|
||||||
|
json.NewDecoder(w.Body).Decode(&resp)
|
||||||
|
if resp["access_token"] != "jwt.token.here" {
|
||||||
|
t.Errorf("access_token not forwarded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthToken_CustomScope(t *testing.T) {
|
||||||
|
stub := &stubAuthClient{statusCode: 200, body: `{}`}
|
||||||
|
h := newTestHandler("https://auth.example.com/token/", "api-gw", "secret", stub)
|
||||||
|
|
||||||
|
body := `{"username":"rock","password":"pass","scope":"openid roles"}`
|
||||||
|
r := httptest.NewRequest("POST", "/auth/token", strings.NewReader(body))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.handleAuthToken(w, r)
|
||||||
|
|
||||||
|
if stub.lastForm.Get("scope") != "openid roles" {
|
||||||
|
t.Errorf("scope = %q, want custom scope", stub.lastForm.Get("scope"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthToken_MissingUsername(t *testing.T) {
|
||||||
|
h := newTestHandler("https://auth.example.com/token/", "api-gw", "secret", &stubAuthClient{})
|
||||||
|
|
||||||
|
body := `{"password":"pass"}`
|
||||||
|
r := httptest.NewRequest("POST", "/auth/token", strings.NewReader(body))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.handleAuthToken(w, r)
|
||||||
|
|
||||||
|
if w.Code != 400 {
|
||||||
|
t.Errorf("expected 400, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthToken_MissingPassword(t *testing.T) {
|
||||||
|
h := newTestHandler("https://auth.example.com/token/", "api-gw", "secret", &stubAuthClient{})
|
||||||
|
|
||||||
|
body := `{"username":"rock"}`
|
||||||
|
r := httptest.NewRequest("POST", "/auth/token", strings.NewReader(body))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.handleAuthToken(w, r)
|
||||||
|
|
||||||
|
if w.Code != 400 {
|
||||||
|
t.Errorf("expected 400, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthToken_InvalidJSON(t *testing.T) {
|
||||||
|
h := newTestHandler("https://auth.example.com/token/", "api-gw", "secret", &stubAuthClient{})
|
||||||
|
|
||||||
|
r := httptest.NewRequest("POST", "/auth/token", strings.NewReader("not json"))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.handleAuthToken(w, r)
|
||||||
|
|
||||||
|
if w.Code != 400 {
|
||||||
|
t.Errorf("expected 400, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthToken_WrongMethod(t *testing.T) {
|
||||||
|
h := newTestHandler("https://auth.example.com/token/", "api-gw", "secret", &stubAuthClient{})
|
||||||
|
|
||||||
|
r := httptest.NewRequest("GET", "/auth/token", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.handleAuthToken(w, r)
|
||||||
|
|
||||||
|
if w.Code != 405 {
|
||||||
|
t.Errorf("expected 405, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthToken_NotConfigured(t *testing.T) {
|
||||||
|
h := newTestHandler("", "", "", &stubAuthClient{})
|
||||||
|
|
||||||
|
body := `{"username":"rock","password":"pass"}`
|
||||||
|
r := httptest.NewRequest("POST", "/auth/token", strings.NewReader(body))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.handleAuthToken(w, r)
|
||||||
|
|
||||||
|
if w.Code != 503 {
|
||||||
|
t.Errorf("expected 503, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthToken_UpstreamError(t *testing.T) {
|
||||||
|
stub := &stubAuthClient{err: io.ErrUnexpectedEOF}
|
||||||
|
h := newTestHandler("https://auth.example.com/token/", "api-gw", "secret", stub)
|
||||||
|
|
||||||
|
body := `{"username":"rock","password":"pass"}`
|
||||||
|
r := httptest.NewRequest("POST", "/auth/token", strings.NewReader(body))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.handleAuthToken(w, r)
|
||||||
|
|
||||||
|
if w.Code != 502 {
|
||||||
|
t.Errorf("expected 502, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthToken_UpstreamRejectsCredentials(t *testing.T) {
|
||||||
|
stub := &stubAuthClient{
|
||||||
|
statusCode: 400,
|
||||||
|
body: `{"error":"invalid_grant","error_description":"bad password"}`,
|
||||||
|
}
|
||||||
|
h := newTestHandler("https://auth.example.com/token/", "api-gw", "secret", stub)
|
||||||
|
|
||||||
|
body := `{"username":"rock","password":"wrong"}`
|
||||||
|
r := httptest.NewRequest("POST", "/auth/token", strings.NewReader(body))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.handleAuthToken(w, r)
|
||||||
|
|
||||||
|
// Upstream error forwarded verbatim
|
||||||
|
if w.Code != 400 {
|
||||||
|
t.Errorf("expected 400 (forwarded), got %d", w.Code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(w.Body.String(), "invalid_grant") {
|
||||||
|
t.Errorf("expected upstream error forwarded, got %s", w.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- /auth/refresh tests ---
|
||||||
|
|
||||||
|
func TestAuthRefresh_Success(t *testing.T) {
|
||||||
|
stub := &stubAuthClient{
|
||||||
|
statusCode: 200,
|
||||||
|
body: `{"access_token":"new.jwt","refresh_token":"new.refresh","expires_in":3600}`,
|
||||||
|
}
|
||||||
|
h := newTestHandler("https://auth.example.com/token/", "api-gw", "secret", stub)
|
||||||
|
|
||||||
|
body := `{"refresh_token":"old.refresh"}`
|
||||||
|
r := httptest.NewRequest("POST", "/auth/refresh", strings.NewReader(body))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.handleAuthRefresh(w, r)
|
||||||
|
|
||||||
|
if w.Code != 200 {
|
||||||
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
if stub.lastForm.Get("grant_type") != "refresh_token" {
|
||||||
|
t.Errorf("grant_type = %q, want refresh_token", stub.lastForm.Get("grant_type"))
|
||||||
|
}
|
||||||
|
if stub.lastForm.Get("refresh_token") != "old.refresh" {
|
||||||
|
t.Errorf("refresh_token not sent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthRefresh_MissingToken(t *testing.T) {
|
||||||
|
h := newTestHandler("https://auth.example.com/token/", "api-gw", "secret", &stubAuthClient{})
|
||||||
|
|
||||||
|
r := httptest.NewRequest("POST", "/auth/refresh", strings.NewReader(`{}`))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.handleAuthRefresh(w, r)
|
||||||
|
|
||||||
|
if w.Code != 400 {
|
||||||
|
t.Errorf("expected 400, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthRefresh_WrongMethod(t *testing.T) {
|
||||||
|
h := newTestHandler("https://auth.example.com/token/", "api-gw", "secret", &stubAuthClient{})
|
||||||
|
|
||||||
|
r := httptest.NewRequest("GET", "/auth/refresh", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.handleAuthRefresh(w, r)
|
||||||
|
|
||||||
|
if w.Code != 405 {
|
||||||
|
t.Errorf("expected 405, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthRefresh_NotConfigured(t *testing.T) {
|
||||||
|
h := newTestHandler("", "", "", &stubAuthClient{})
|
||||||
|
|
||||||
|
r := httptest.NewRequest("POST", "/auth/refresh", strings.NewReader(`{"refresh_token":"x"}`))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.handleAuthRefresh(w, r)
|
||||||
|
|
||||||
|
if w.Code != 503 {
|
||||||
|
t.Errorf("expected 503, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthRefresh_ExpiredToken(t *testing.T) {
|
||||||
|
stub := &stubAuthClient{
|
||||||
|
statusCode: 401,
|
||||||
|
body: `{"error":"invalid_grant","error_description":"token expired"}`,
|
||||||
|
}
|
||||||
|
h := newTestHandler("https://auth.example.com/token/", "api-gw", "secret", stub)
|
||||||
|
|
||||||
|
r := httptest.NewRequest("POST", "/auth/refresh", strings.NewReader(`{"refresh_token":"expired"}`))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.handleAuthRefresh(w, r)
|
||||||
|
|
||||||
|
if w.Code != 401 {
|
||||||
|
t.Errorf("expected 401 (forwarded), got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no credentials are leaked in response bodies
|
||||||
|
func TestAuthToken_NoCredentialLeak(t *testing.T) {
|
||||||
|
stub := &stubAuthClient{statusCode: 200, body: `{"access_token":"tok"}`}
|
||||||
|
h := newTestHandler("https://auth.example.com/token/", "api-gw", "super-secret", stub)
|
||||||
|
|
||||||
|
body := `{"username":"rock","password":"my-password"}`
|
||||||
|
r := httptest.NewRequest("POST", "/auth/token", bytes.NewReader([]byte(body)))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.handleAuthToken(w, r)
|
||||||
|
|
||||||
|
respBody := w.Body.String()
|
||||||
|
if strings.Contains(respBody, "super-secret") {
|
||||||
|
t.Error("client_secret leaked in response")
|
||||||
|
}
|
||||||
|
if strings.Contains(respBody, "my-password") {
|
||||||
|
t.Error("password leaked in response")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ type Handler struct {
|
|||||||
config *config.Config
|
config *config.Config
|
||||||
// jwtValidator validates JWT tokens for authenticated endpoints
|
// jwtValidator validates JWT tokens for authenticated endpoints
|
||||||
jwtValidator *auth.Validator
|
jwtValidator *auth.Validator
|
||||||
|
// authHTTP is the HTTP client for token exchange with the identity provider.
|
||||||
|
authHTTP authClient
|
||||||
// Default timeouts for synthesized routes (model-based dispatch)
|
// Default timeouts for synthesized routes (model-based dispatch)
|
||||||
defaultConnectTimeout time.Duration
|
defaultConnectTimeout time.Duration
|
||||||
defaultReadTimeout time.Duration
|
defaultReadTimeout time.Duration
|
||||||
@@ -86,6 +88,10 @@ func New(cfg *config.Config) *Handler {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.Auth.TokenURL != "" {
|
||||||
|
h.authHTTP = newAuthClient()
|
||||||
|
}
|
||||||
|
|
||||||
for name, route := range cfg.Routes {
|
for name, route := range cfg.Routes {
|
||||||
// Create a transport per unique upstream address for connection reuse
|
// Create a transport per unique upstream address for connection reuse
|
||||||
transport := h.getOrCreateTransport(route.Upstream.Address, &route.Upstream)
|
transport := h.getOrCreateTransport(route.Upstream.Address, &route.Upstream)
|
||||||
@@ -228,6 +234,16 @@ func writeProblemDetail(w http.ResponseWriter, status int, problemType, title, d
|
|||||||
|
|
||||||
// ServeHTTP implements http.Handler.
|
// ServeHTTP implements http.Handler.
|
||||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Auth endpoints — no JWT required (they issue tokens)
|
||||||
|
if r.URL.Path == "/auth/token" {
|
||||||
|
h.handleAuthToken(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.URL.Path == "/auth/refresh" {
|
||||||
|
h.handleAuthRefresh(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Handle /v1/models endpoint (no routing needed, derived from config)
|
// Handle /v1/models endpoint (no routing needed, derived from config)
|
||||||
if r.URL.Path == "/v1/models" && r.Method == "GET" {
|
if r.URL.Path == "/v1/models" && r.Method == "GET" {
|
||||||
h.handleModelsEndpoint(w, r)
|
h.handleModelsEndpoint(w, r)
|
||||||
|
|||||||
Reference in New Issue
Block a user