Archived
Init
This commit is contained in:
commit
da94d6b2b0
3 files changed
+223
No files matched your search
+20
@@ -0,0 +1,20 @@
|
|||||||
|
FROM golang:1.23-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN go mod init opencloud-oidc-webfinger-proxy || true
|
||||||
|
RUN go mod tidy
|
||||||
|
RUN go build -o opencloud-oidc-webfinger-proxy main.go
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /app/opencloud-oidc-webfinger-proxy .
|
||||||
|
|
||||||
|
EXPOSE 9210
|
||||||
|
ENV PORT=9210
|
||||||
|
ENV REDIRECT_TEMPLATE=""
|
||||||
|
|
||||||
|
CMD ["./opencloud-oidc-webfinger-proxy"]
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# opencloud-oidc-webfinger-proxy
|
||||||
|
|
||||||
|
A lightweight **Go-based reverse proxy** that dynamically rewrites `href` fields in **WebFinger responses** for OpenID Connect (OIDC) clients.
|
||||||
|
|
||||||
|
This service is primarily designed for **OpenCloud** environments but can be used with any OIDC implementation that requires dynamic issuer rewriting based on user agent or suffix mapping.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Features
|
||||||
|
|
||||||
|
- 🔄 Dynamically rewrites `href` values in WebFinger JSON responses
|
||||||
|
- ⚙️ Fully configurable via environment variables — no code changes required
|
||||||
|
- 🧩 Compatible with any OIDC-compliant identity provider (e.g. Authentik, Authelia, Dex, etc.)
|
||||||
|
- 🧠 Extremely lightweight and stateless — built with Go
|
||||||
|
- 🧱 Works seamlessly behind Nginx or any reverse proxy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 How it works
|
||||||
|
|
||||||
|
1. The proxy listens for requests on `/.well-known/webfinger`.
|
||||||
|
2. It forwards the request to the configured **upstream WebFinger service**.
|
||||||
|
3. The JSON response is inspected and any `href` fields matching `HREF_PATTERN` are replaced with
|
||||||
|
`HREF_REPLACEMENT + <issuer-suffix> + "/"`.
|
||||||
|
4. The **issuer suffix** is provided via the HTTP header `X-Issuer-Suffix`,
|
||||||
|
or falls back to the configured `DEFAULT_SUFFIX`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
# Upstream response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"href": "https://auth.domain.com/application/o/opencloud/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Modified by proxy (Header: X-Issuer-Suffix=opencloud-android)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"href": "https://auth.domain.com/application/o/opencloud-android/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚙️ Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Example |
|
||||||
|
|-----------|-------------|----------|
|
||||||
|
| `PORT` | Port on which the proxy listens | `9210` |
|
||||||
|
| `UPSTREAM_URL` | Base URL of the real WebFinger endpoint (including schema) | `https://cloud.domain.com` |
|
||||||
|
| `HREF_PATTERN` | The part of the `href` URL to be matched and replaced | `https://auth.domain.com/application/o/` |
|
||||||
|
| `HREF_REPLACEMENT` | The base part to replace the matched pattern with | `https://auth.domain.com/application/o/` |
|
||||||
|
| `DEFAULT_SUFFIX` | Default issuer suffix when none is provided via header | `opencloud` |
|
||||||
|
|
||||||
|
All variables are **required** — the service will exit with an error if any are missing.
|
||||||
|
|
||||||
|
## 🧩 Example usage
|
||||||
|
|
||||||
|
### Run locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PORT=9210
|
||||||
|
export UPSTREAM_URL=https://cloud.domain.com
|
||||||
|
export HREF_PATTERN=https://auth.domain.com/application/o/
|
||||||
|
export HREF_REPLACEMENT=https://auth.domain.com/application/o/
|
||||||
|
export DEFAULT_SUFFIX=opencloud
|
||||||
|
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
Then test it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "X-Issuer-Suffix: opencloud-android" "http://localhost:9210/.well-known/webfinger?rel=http%3A%2F%2Fopenid.net%2Fspecs%2Fconnect%2F1.0%2Fissuer&resource=https%3A%2F%2Fcloud.domain.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"rel": "http://openid.net/specs/connect/1.0/issuer",
|
||||||
|
"href": "https://auth.domain.com/application/o/opencloud-android/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subject": "https://cloud.domain.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func replaceHref(v any, pattern, replacement, suffix string) any {
|
||||||
|
re := regexp.MustCompile(regexp.QuoteMeta(pattern) + `[^/]+/`)
|
||||||
|
|
||||||
|
switch val := v.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
for k, v2 := range val {
|
||||||
|
if k == "href" {
|
||||||
|
if s, ok := v2.(string); ok && strings.Contains(s, pattern) {
|
||||||
|
newHref := re.ReplaceAllString(s, replacement+suffix+"/")
|
||||||
|
val[k] = newHref
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val[k] = replaceHref(v2, pattern, replacement, suffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []any:
|
||||||
|
for i, v2 := range val {
|
||||||
|
val[i] = replaceHref(v2, pattern, replacement, suffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func handler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
upstream := os.Getenv("UPSTREAM_URL")
|
||||||
|
pattern := os.Getenv("HREF_PATTERN")
|
||||||
|
replacement := os.Getenv("HREF_REPLACEMENT")
|
||||||
|
defaultSuffix := os.Getenv("DEFAULT_SUFFIX")
|
||||||
|
|
||||||
|
for k, v := range map[string]string{
|
||||||
|
"UPSTREAM_URL": upstream,
|
||||||
|
"HREF_PATTERN": pattern,
|
||||||
|
"HREF_REPLACEMENT": replacement,
|
||||||
|
"DEFAULT_SUFFIX": defaultSuffix,
|
||||||
|
} {
|
||||||
|
if v == "" {
|
||||||
|
errMsg := fmt.Sprintf("Missing required environment variable: %s", k)
|
||||||
|
log.Fatal("❌ " + errMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suffix := r.Header.Get("X-Issuer-Suffix")
|
||||||
|
if suffix == "" {
|
||||||
|
suffix = defaultSuffix
|
||||||
|
}
|
||||||
|
|
||||||
|
target := upstream + r.URL.RequestURI()
|
||||||
|
log.Printf("→ Fetching %s (suffix=%s)", target, suffix)
|
||||||
|
|
||||||
|
resp, err := http.Get(target)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data any
|
||||||
|
if err := json.Unmarshal(body, &data); err != nil {
|
||||||
|
for k, v := range resp.Header {
|
||||||
|
if len(v) > 0 {
|
||||||
|
w.Header().Set(k, v[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.WriteHeader(resp.StatusCode)
|
||||||
|
w.Write(body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data = replaceHref(data, pattern, replacement, suffix)
|
||||||
|
modified, _ := json.MarshalIndent(data, "", " ")
|
||||||
|
|
||||||
|
for k, v := range resp.Header {
|
||||||
|
if strings.ToLower(k) == "content-length" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(v) > 0 {
|
||||||
|
w.Header().Set(k, v[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
w.WriteHeader(resp.StatusCode)
|
||||||
|
w.Write(modified)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
port := os.Getenv("PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "8080"
|
||||||
|
}
|
||||||
|
http.HandleFunc("/.well-known/webfinger", handler)
|
||||||
|
log.Printf("✅ Listening on :%s", port)
|
||||||
|
log.Fatal(http.ListenAndServe(":"+port, nil))
|
||||||
|
}
|
||||||
Reference in new issue
Block a user