Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5ee24b3
feat: Adding oidc middleware and keycloak server services
Janmtbehrens Apr 21, 2026
c75883a
feat: Redirect to keycloak login
Janmtbehrens Apr 21, 2026
7dc1cb4
fix: Public issuer url is now equal to keycloaks known issuer url
Janmtbehrens Apr 21, 2026
c13f283
feat: Adding identity service. Expanding oidc provider configurations
Janmtbehrens Jun 2, 2026
8c5d77b
feat: Created local-only plugin to append user-id header to traefik h…
Janmtbehrens Jun 4, 2026
72951c0
fix: Writing own JWT interpreter to avoid vendoring. Adding local plu…
Janmtbehrens Jun 4, 2026
4c6f5cb
fix: Move files in dockerfile again
Janmtbehrens Jun 4, 2026
c93e5b2
feat: Plugin now loaded correctly. User ID written into the response,…
Janmtbehrens Jun 4, 2026
a777602
feat: Flusher
Janmtbehrens Jun 5, 2026
5ed181e
feat: Adding additional debugging message
Janmtbehrens Jun 5, 2026
590af32
feat: It works
Janmtbehrens Jun 11, 2026
3b2a641
Fix http flush in user id middleware
bastianjoel Jun 12, 2026
d301d9e
feat: Access Token Plugin Attempt
Janmtbehrens Jun 18, 2026
5c5af38
feat: Blocklist Plugin. Identity Service Removal. PKCE enabled, Clien…
Janmtbehrens Jul 6, 2026
749306c
feat: Remove blocklist plugin until revisions were made. Change keycl…
Janmtbehrens Jul 6, 2026
8eb03bd
feat: Trying to fetch Client ID and Secret from API (unfinsihed)
Janmtbehrens Jul 7, 2026
b06bf28
feat: Reading client id and secret from volume file
Janmtbehrens Jul 9, 2026
fe6999d
Add zitadel router and service
rrenkert Jul 9, 2026
3e12770
feat: Environment Variable Update. Removing import data object
Janmtbehrens Jul 13, 2026
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
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ WORKDIR /app
ENV APP_CONTEXT=${CONTEXT}

# curl for healthcheck, gettext for templating (envsubst)
RUN apk add --no-cache curl gettext
RUN apk add --no-cache curl gettext jq

# Copy configuration files
COPY entrypoint.sh /entrypoint.sh
COPY certs /certs
COPY services /services
COPY templates /templates
COPY plugins-local /plugins-local

# Create dynamic config directory and make entrypoint executable
RUN mkdir -p /etc/traefik/dynamic
Expand Down Expand Up @@ -56,6 +57,7 @@ FROM base AS prod
RUN adduser -S -D -H appuser
RUN chown -R appuser /app/ && \
chown -R appuser /etc/traefik/ && \
chown -R appuser ./plugins-local/ && \
chown appuser /entrypoint.sh

USER appuser
120 changes: 120 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,104 @@ MEDIA_HOST="${MEDIA_HOST:-media}"
MEDIA_PORT="${MEDIA_PORT:-9006}"
MANAGE_HOST="${MANAGE_HOST:-manage}"
MANAGE_PORT="${MANAGE_PORT:-9008}"
IDENTITY_HOST="${IDENTITY_HOST:-identity}"
IDENTITY_PORT="${IDENTITY_PORT:-9014}"
VOTE_HOST="${VOTE_HOST:-vote}"
VOTE_PORT="${VOTE_PORT:-9013}"
CLIENT_HOST="${CLIENT_HOST:-client}"
CLIENT_PORT="${CLIENT_PORT:-9001}"
IDP_HOST="${IDP_HOST:-zitadel-api}"
IDP_HOST_PORT="${IDP_HOST_PORT:-8080}"
INSTANCE_URL="${INSTANCE_URL:-https://localhost:8000}"
IDP_URL_EXTERNAL="${IDP_URL_EXTERNAL:-https://localhost:8000}"
IDP_URL_INTERNAL="${IDP_URL_INTERNAL:-http://zitadel-api:8080}"


# =================================
# = Build static / install config =
# =================================

# Get Zitadel Client ID
IDP_PAT="$(cat /zitadel/bootstrap/admin.pat)"
IDP_CLIENT_ID="$(cat /zitadel/bootstrap/client-id)"
IDP_CLIENT_SECRET="$(cat /zitadel/bootstrap/client-secret)"

# echo $IDP_PAT

# Import Data

#PAYLOAD=$(cat <<EOF
#{
# "timeout" : "5m",
# "dataOrgs": $(cat /import-data.json)
#}
#EOF
#)

#echo $PAYLOAD

#RESPONSE=$(curl -X POST "$IDP_API_URL/admin/v1/import" \
# --header "Authorization: Bearer ${IDP_PAT}" \
# --header "Content-Type: application/json"\
# --header "Host: ${INSTANCE_URL}" \
# --data "${PAYLOAD}")

#echo $PAYLOAD

#IDP_APP_INFORMATION=$(curl -sS -X POST \
# "${IDP_API_URL}/zitadel.application.v2.ApplicationService/ListApplications" \
# -H "Authorization: Bearer $IDP_PAT" \
# -H "Content-Type: application/json" \
# -H "Host: ${INSTANCE_URL}" \
# -d '{}')

#IDP_CLIENT_ID="$(echo $IDP_APP_INFORMATION | jq -r '.applications[0].oidcConfiguration.clientId')"
#IDP_PROJECT_ID="$(echo $IDP_APP_INFORMATION | jq -r '.applications[0].projectId')"
#IDP_APPLICATION_ID="$(echo $IDP_APP_INFORMATION | jq -r '.applications[0].applicationId')"

#echo "CLIENT ID: --- $IDP_CLIENT_ID"
#echo "Project ID: --- $IDP_PROJECT_ID"
#echo "App ID: --- $IDP_APPLICATION_ID"

#if [ "$IDP_CLIENT_ID" == "null" ]
#then
# echo "No client ID has been returned by zitadel"
# echo "Response: $RESPONSE"
# sleep infinity
#fi

#IDP_CLIENT_SECRET=$(curl -sS -X POST \
# "${IDP_API_URL}/zitadel.application.v2.ApplicationService/GenerateClientSecret" \
# -H "Host: ${INSTANCE_URL}" \
# -H "Authorization: Bearer $IDP_PAT" \
# -H "Content-Type: application/json" \
# -H "Connect-Protocol-Version: 1" \
# -d '{
# \"projectId\": \"$IDP_PROJECT_ID\",
# \"applicationId\": \"$IDP_APPLICATION_ID\"
# }' \
#| jq -r '.clientSecret')

#echo "CLIENT SECRET: --- $IDP_CLIENT_SECRET"

# Generate base config from template
envsubst < /templates/traefik.yml > "$TRAEFIK_CONFIG"

# Add OIDC plugin
echo "Adding OIDC Plugin"
cat >> "$TRAEFIK_CONFIG" << 'EOF'

experimental:
plugins:
traefik-oidc-auth:
moduleName: github.com/sevensolutions/traefik-oidc-auth
version: v0.20.0
localPlugins:
user_id_header:
moduleName: github.com/openslides/user_id_header
EOF


# Add dashboard if enabled
if [ -n "$ENABLE_DASHBOARD" ]; then
echo "Enabling dashboard. 'debug: true' for now. NOT FOR PRODUCTION"
Expand Down Expand Up @@ -174,6 +259,41 @@ for service in $SERVICES; do
envsubst < "$SERVICES_DIR/${service}.service" >> "$DYNAMIC_CONFIG"
done

# OIDC Middleware
echo "Enabling OIDC authentication middleware"
cat >> "$DYNAMIC_CONFIG" << EOF

middlewares:
oidc-auth:
plugin:
traefik-oidc-auth:
LogLevel: DEBUG
Provider:
Url: "${IDP_URL_EXTERNAL}"
ClientId: "${IDP_CLIENT_ID}"
UsePkce: true
ValidateIssuer: true
ValidIssuer: "${IDP_URL_EXTERNAL}"
InsecureSkipVerify: true
UnauthorizedBehavior: Forward
BypassAuthenticationRule: "PathPrefix(\`/\`)"
LoginUri: "/ui/v2/login/login"
LogoutUri: "/system/logout"
Headers:
- Name: "Authorization"
Value: "{{\`Bearer: {{ .accessToken }}\`}}"
IncludeWhen: "Public"
Scopes: ["openid", "profile", "email"]
Authorization:
AssertClaims:
- Name: roles
AnyOf: ["admin", "media"]
user-id:
plugin:
user_id_header: {}
EOF

cat $DYNAMIC_CONFIG

# Finally start CMD
exec "$@"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
displayName: Access Token Blocklist
type: middleware
import: github.com/openslides/access_token_blocklist
summary: Invalidates access tokens linked to a logout call
testData: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) Since 2011 Authors of OpenSlides, see https://github.com/OpenSlides/OpenSlides/blob/master/AUTHORS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/openslides/access_token_blocklist

go 1.23.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package access_token_blocklist

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strings"
)

// Mandatory config struct
type Config struct {
}

func CreateConfig() *Config {
return &Config{}
}

type AccessTokenBlocklist struct {
next http.Handler
name string
blockedSessionIDs map[string]int
id int
}

// Create new plugin instance
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
return &AccessTokenBlocklist{
next: next,
name: name,
blockedSessionIDs: make(map[string]int),
}, nil
}

type responseWriter struct {
http.ResponseWriter
flusher http.Flusher
userID string
headerSet bool
}

func (w *responseWriter) WriteHeader(code int) {
w.Header().Set(userIDHeader, w.userID)
w.headerSet = true
w.ResponseWriter.WriteHeader(code)
}

func (w *responseWriter) Write(b []byte) (int, error) {
if !w.headerSet {
w.Header().Set(userIDHeader, w.userID)
}

n, err := w.ResponseWriter.Write(b)
if w.flusher != nil {
w.flusher.Flush()
}

return n, err
}

func (w *responseWriter) Flush() {
if w.flusher != nil {
w.flusher.Flush()
}
}

func extractSessionID(r *http.Request) string {
header := r.Header.Get("Authorization")
encodedToken := strings.TrimPrefix(header, "Bearer: ")

if header == encodedToken {
// No token. Handle the request as public access requst.
fmt.Println("header error: no token")
return ""
}

parts := strings.Split(encodedToken, ".")
if len(parts) != 3 {
fmt.Println("header error: JWT partition not length 3")
return ""
}

payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
fmt.Println("header error: decoding token payload:", err)
return ""
}

var claims map[string]any
if err := json.Unmarshal(payload, &claims); err != nil {
fmt.Println("header error: parsing token claims:", err)
return ""
}

sid, exists := claims["sid"]
if !exists {
fmt.Println("header error: missing sid in token claims")
return ""
}

return sid.(string)
}

func (a *AccessTokenBlocklist) blockSessionID(sessionID string) {
a.blockedSessionIDs[sessionID] = 1
}

func (a *AccessTokenBlocklist) isSessionIDBlocked(sessionID string) bool {
if _, exists := a.blockedSessionIDs[sessionID]; exists {
return true
}
return false
}

func (a *AccessTokenBlocklist) ServeHTTP(w http.ResponseWriter, r *http.Request) {
sessionID := extractSessionID(r)

if strings.HasSuffix(r.URL.Path, logoutRoute) {
a.blockSessionID(sessionID)
}

var flusher http.Flusher
if f, ok := w.(http.Flusher); ok {
flusher = f
}
if a.isSessionIDBlocked(sessionID) {
// Write it as a new header
r.Header.Set(userIDHeader, "0")

// Pass
a.next.ServeHTTP(&responseWriter{
flusher: flusher,
ResponseWriter: w,
userID: "0",
}, r)
return
}

// Pass
a.next.ServeHTTP(&responseWriter{
flusher: flusher,
ResponseWriter: w,
}, r)

}

const (
userIDHeader string = "X-User-ID"
logoutRoute string = "system/logout"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
displayName: User ID Header Insert
type: middleware
import: github.com/openslides/user_id_header
summary: Appends user id to headers
testData: {}
21 changes: 21 additions & 0 deletions plugins-local/src/github.com/openslides/user_id_header/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) Since 2011 Authors of OpenSlides, see https://github.com/OpenSlides/OpenSlides/blob/master/AUTHORS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
3 changes: 3 additions & 0 deletions plugins-local/src/github.com/openslides/user_id_header/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/openslides/user_id_header

go 1.23.0
Loading