161 lines
4.3 KiB
Go
161 lines
4.3 KiB
Go
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)
|
||
|
|
}
|