From 5ee24b38a2c8eb652d98369a402a2cdf682e569c Mon Sep 17 00:00:00 2001 From: Jan Malte Behrens Date: Tue, 21 Apr 2026 10:03:02 +0200 Subject: [PATCH 01/13] feat: Adding oidc middleware and keycloak server services --- entrypoint.sh | 62 ++++++++++++++++++++++++++++++++++++++ services/action.router | 2 ++ services/autoupdate.router | 2 ++ services/client.router | 2 ++ services/icc.router | 2 ++ services/keycloak.router | 5 +++ services/keycloak.service | 6 ++++ services/media.router | 2 ++ services/presenter.router | 2 ++ services/projector.router | 2 ++ services/search.router | 2 ++ services/vote.router | 2 ++ 12 files changed, 91 insertions(+) create mode 100644 services/keycloak.router create mode 100644 services/keycloak.service diff --git a/entrypoint.sh b/entrypoint.sh index 1083260..5aba728 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -41,6 +41,12 @@ VOTE_HOST="${VOTE_HOST:-vote}" VOTE_PORT="${VOTE_PORT:-9013}" CLIENT_HOST="${CLIENT_HOST:-client}" CLIENT_PORT="${CLIENT_PORT:-9001}" +KEYCLOAK_HOST="${KEYCLOAK_HOST:-keycloak-server}" +KEYCLOAK_HOST_PORT="${KEYCLOAK_HOST_PORT:-8080}" +OIDC_KEYCLOAK_URL="${OIDC_KEYCLOAK_URL:-http://localhost:8080/realms/openslides}" +OIDC_CLIENT_ID="${OIDC_CLIENT_ID:-proxy-client}" +OIDC_CLIENT_SECRET="${OIDC_CLIENT_SECRET:-proxy-secret}" +OIDC_SECRET="${OIDC_SECRET:-qvAcTGWBIGg7aWKCKRyUsTf33jK3lsmK}" # ================================= @@ -50,6 +56,18 @@ CLIENT_PORT="${CLIENT_PORT:-9001}" # 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.19.0 +EOF + + # Add dashboard if enabled if [ -n "$ENABLE_DASHBOARD" ]; then echo "Enabling dashboard. 'debug: true' for now. NOT FOR PRODUCTION" @@ -174,6 +192,50 @@ for service in $SERVICES; do envsubst < "$SERVICES_DIR/${service}.service" >> "$DYNAMIC_CONFIG" done +# OIDC Middleware +cat >> "$DYNAMIC_CONFIG" << EOF + keycloak-server: + loadBalancer: + servers: + - url: "http://localhost:8080" + passHostHeader: true +EOF + +echo "Enabling OIDC authentication middleware" + cat >> "$DYNAMIC_CONFIG" << EOF + + middlewares: + oidc-auth: + plugin: + traefik-oidc-auth: + LogLevel: debug + Secret: "${OIDC_SECRET}" + Provider: + Url: "${OIDC_KEYCLOAK_URL}" + ClientId: "${OIDC_CLIENT_ID}" + ClientSecret: "${OIDC_CLIENT_SECRET}" + UsePkce: true + ValidateIssuer: true + ValidIssuer: "${OIDC_KEYCLOAK_URL}" + Scopes: + - openid + - profile + - email + LoginUri: /login + CallbackUri: /callback + LogoutUri: /logout + UnauthorizedBehavior: Challenge + SessionCookie: + SameSite: lax + HttpOnly: false + Headers: + - Name: Authentication + Value: 'bearer {{ "{{ .accessToken }}" }}' + - Name: X-Forwarded-User + Value: '{{ "{{ .claims.preferred_username }}" }}' + - Name: X-Auth-Request-Email + Value: '{{ "{{ .claims.email }}" }}' +EOF # Finally start CMD exec "$@" diff --git a/services/action.router b/services/action.router index 8cdc6b6..5f7db26 100644 --- a/services/action.router +++ b/services/action.router @@ -3,3 +3,5 @@ service: action entryPoints: - main + middlewares: + - oidc-auth diff --git a/services/autoupdate.router b/services/autoupdate.router index 19a3806..9aeda44 100644 --- a/services/autoupdate.router +++ b/services/autoupdate.router @@ -3,3 +3,5 @@ service: autoupdate entryPoints: - main + middlewares: + - oidc-auth diff --git a/services/client.router b/services/client.router index 5064e01..a962d0e 100644 --- a/services/client.router +++ b/services/client.router @@ -5,3 +5,5 @@ - main # Priority ensures this catch-all route is evaluated last priority: 1 + middlewares: + - oidc-auth diff --git a/services/icc.router b/services/icc.router index eb7ac0f..9ec790a 100644 --- a/services/icc.router +++ b/services/icc.router @@ -3,3 +3,5 @@ service: icc entryPoints: - main + middlewares: + - oidc-auth diff --git a/services/keycloak.router b/services/keycloak.router new file mode 100644 index 0000000..b5983d6 --- /dev/null +++ b/services/keycloak.router @@ -0,0 +1,5 @@ + keycloak: + rule: "PathPrefix(`/system/keycloak`) + service: keycloak + entryPoints: + - main diff --git a/services/keycloak.service b/services/keycloak.service new file mode 100644 index 0000000..6c8fc17 --- /dev/null +++ b/services/keycloak.service @@ -0,0 +1,6 @@ + keycloak: + loadBalancer: + servers: + - url: "http://${KEYCLOAK_HOST}:${KEYCLOAK_PORT}" + # Forward the original Host header to the backend service + passHostHeader: true diff --git a/services/media.router b/services/media.router index c428375..f44a71b 100644 --- a/services/media.router +++ b/services/media.router @@ -3,3 +3,5 @@ service: media entryPoints: - main + middlewares: + - oidc-auth diff --git a/services/presenter.router b/services/presenter.router index 62fd449..b9a9b64 100644 --- a/services/presenter.router +++ b/services/presenter.router @@ -3,3 +3,5 @@ service: presenter entryPoints: - main + middlewares: + - oidc-auth diff --git a/services/projector.router b/services/projector.router index 5e43d5c..7e94244 100644 --- a/services/projector.router +++ b/services/projector.router @@ -3,3 +3,5 @@ service: projector entryPoints: - main + middlewares: + - oidc-auth diff --git a/services/search.router b/services/search.router index d6af74c..bb0e24f 100644 --- a/services/search.router +++ b/services/search.router @@ -3,3 +3,5 @@ service: search entryPoints: - main + middlewares: + - oidc-auth diff --git a/services/vote.router b/services/vote.router index a357117..5c654eb 100644 --- a/services/vote.router +++ b/services/vote.router @@ -3,3 +3,5 @@ service: vote entryPoints: - main + middlewares: + - oidc-auth From c75883a4baa51d57b5d2d9381870361c0ff39938 Mon Sep 17 00:00:00 2001 From: Jan Malte Behrens Date: Tue, 21 Apr 2026 13:43:58 +0200 Subject: [PATCH 02/13] feat: Redirect to keycloak login --- entrypoint.sh | 34 ++++------------------------------ services/keycloak.router | 5 ----- services/keycloak.service | 6 ------ 3 files changed, 4 insertions(+), 41 deletions(-) delete mode 100644 services/keycloak.router delete mode 100644 services/keycloak.service diff --git a/entrypoint.sh b/entrypoint.sh index 5aba728..ef51aba 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -43,7 +43,8 @@ CLIENT_HOST="${CLIENT_HOST:-client}" CLIENT_PORT="${CLIENT_PORT:-9001}" KEYCLOAK_HOST="${KEYCLOAK_HOST:-keycloak-server}" KEYCLOAK_HOST_PORT="${KEYCLOAK_HOST_PORT:-8080}" -OIDC_KEYCLOAK_URL="${OIDC_KEYCLOAK_URL:-http://localhost:8080/realms/openslides}" +OIDC_KEYCLOAK_URL="${OIDC_KEYCLOAK_URL:-http://localhost:8000/auth/realms/openslides}" +OIDC_KEYCLOAK_URL_DOCKER="${OIDC_KEYCLOAK_URL_DOCKER:-http://keycloak-server:8080/realms/openslides}" OIDC_CLIENT_ID="${OIDC_CLIENT_ID:-proxy-client}" OIDC_CLIENT_SECRET="${OIDC_CLIENT_SECRET:-proxy-secret}" OIDC_SECRET="${OIDC_SECRET:-qvAcTGWBIGg7aWKCKRyUsTf33jK3lsmK}" @@ -193,14 +194,6 @@ for service in $SERVICES; do done # OIDC Middleware -cat >> "$DYNAMIC_CONFIG" << EOF - keycloak-server: - loadBalancer: - servers: - - url: "http://localhost:8080" - passHostHeader: true -EOF - echo "Enabling OIDC authentication middleware" cat >> "$DYNAMIC_CONFIG" << EOF @@ -208,33 +201,14 @@ echo "Enabling OIDC authentication middleware" oidc-auth: plugin: traefik-oidc-auth: - LogLevel: debug Secret: "${OIDC_SECRET}" Provider: - Url: "${OIDC_KEYCLOAK_URL}" + Url: "${OIDC_KEYCLOAK_URL_DOCKER}" ClientId: "${OIDC_CLIENT_ID}" ClientSecret: "${OIDC_CLIENT_SECRET}" - UsePkce: true ValidateIssuer: true ValidIssuer: "${OIDC_KEYCLOAK_URL}" - Scopes: - - openid - - profile - - email - LoginUri: /login - CallbackUri: /callback - LogoutUri: /logout - UnauthorizedBehavior: Challenge - SessionCookie: - SameSite: lax - HttpOnly: false - Headers: - - Name: Authentication - Value: 'bearer {{ "{{ .accessToken }}" }}' - - Name: X-Forwarded-User - Value: '{{ "{{ .claims.preferred_username }}" }}' - - Name: X-Auth-Request-Email - Value: '{{ "{{ .claims.email }}" }}' + Scopes: ["openid", "profile", "email"] EOF # Finally start CMD diff --git a/services/keycloak.router b/services/keycloak.router deleted file mode 100644 index b5983d6..0000000 --- a/services/keycloak.router +++ /dev/null @@ -1,5 +0,0 @@ - keycloak: - rule: "PathPrefix(`/system/keycloak`) - service: keycloak - entryPoints: - - main diff --git a/services/keycloak.service b/services/keycloak.service deleted file mode 100644 index 6c8fc17..0000000 --- a/services/keycloak.service +++ /dev/null @@ -1,6 +0,0 @@ - keycloak: - loadBalancer: - servers: - - url: "http://${KEYCLOAK_HOST}:${KEYCLOAK_PORT}" - # Forward the original Host header to the backend service - passHostHeader: true From 7dc1cb42078577341ce00d529688ba8bcb0cefff Mon Sep 17 00:00:00 2001 From: Jan Malte Behrens Date: Tue, 21 Apr 2026 16:28:03 +0200 Subject: [PATCH 03/13] fix: Public issuer url is now equal to keycloaks known issuer url --- entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrypoint.sh b/entrypoint.sh index ef51aba..84e8981 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -43,7 +43,7 @@ CLIENT_HOST="${CLIENT_HOST:-client}" CLIENT_PORT="${CLIENT_PORT:-9001}" KEYCLOAK_HOST="${KEYCLOAK_HOST:-keycloak-server}" KEYCLOAK_HOST_PORT="${KEYCLOAK_HOST_PORT:-8080}" -OIDC_KEYCLOAK_URL="${OIDC_KEYCLOAK_URL:-http://localhost:8000/auth/realms/openslides}" +OIDC_KEYCLOAK_URL="${OIDC_KEYCLOAK_URL:-http://localhost:8080/realms/openslides}" OIDC_KEYCLOAK_URL_DOCKER="${OIDC_KEYCLOAK_URL_DOCKER:-http://keycloak-server:8080/realms/openslides}" OIDC_CLIENT_ID="${OIDC_CLIENT_ID:-proxy-client}" OIDC_CLIENT_SECRET="${OIDC_CLIENT_SECRET:-proxy-secret}" From c13f28347d0ae79e0852b566f5778307d78118cb Mon Sep 17 00:00:00 2001 From: Jan Malte Behrens Date: Tue, 2 Jun 2026 16:55:36 +0200 Subject: [PATCH 04/13] feat: Adding identity service. Expanding oidc provider configurations --- entrypoint.sh | 23 ++++++++++++++++++----- services/identity.router | 7 +++++++ services/identity.service | 6 ++++++ 3 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 services/identity.router create mode 100644 services/identity.service diff --git a/entrypoint.sh b/entrypoint.sh index 84e8981..86eea20 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -37,14 +37,16 @@ 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}" KEYCLOAK_HOST="${KEYCLOAK_HOST:-keycloak-server}" KEYCLOAK_HOST_PORT="${KEYCLOAK_HOST_PORT:-8080}" -OIDC_KEYCLOAK_URL="${OIDC_KEYCLOAK_URL:-http://localhost:8080/realms/openslides}" -OIDC_KEYCLOAK_URL_DOCKER="${OIDC_KEYCLOAK_URL_DOCKER:-http://keycloak-server:8080/realms/openslides}" +OIDC_ISSUER_URL="${OIDC_ISSUER_URL:-http://localhost:8080/realms/openslides}" +OIDC_ISSUER_URL_DOCKER="${OIDC_ISSUER_URL_DOCKER:-http://keycloak-server:8080/realms/openslides}" OIDC_CLIENT_ID="${OIDC_CLIENT_ID:-proxy-client}" OIDC_CLIENT_SECRET="${OIDC_CLIENT_SECRET:-proxy-secret}" OIDC_SECRET="${OIDC_SECRET:-qvAcTGWBIGg7aWKCKRyUsTf33jK3lsmK}" @@ -65,7 +67,7 @@ experimental: plugins: traefik-oidc-auth: moduleName: github.com/sevensolutions/traefik-oidc-auth - version: v0.19.0 + version: v0.20.0 EOF @@ -202,14 +204,25 @@ echo "Enabling OIDC authentication middleware" plugin: traefik-oidc-auth: Secret: "${OIDC_SECRET}" + LogLevel: DEBUG Provider: - Url: "${OIDC_KEYCLOAK_URL_DOCKER}" + Url: "${OIDC_ISSUER_URL_DOCKER}" ClientId: "${OIDC_CLIENT_ID}" ClientSecret: "${OIDC_CLIENT_SECRET}" ValidateIssuer: true - ValidIssuer: "${OIDC_KEYCLOAK_URL}" + ValidIssuer: "${OIDC_ISSUER_URL}" + UnauthorizedBehavior: Forward + BypassAuthenticationRule: "PathPrefix(\`/\`)" + LoginUri: "/system/login" + LogoutUri: "/system/logout" + Headers: + - Name: "Authorization" + Value: "{{\`Bearer: {{ .accessToken }}\`}}" + IncludeWhen: "Public" Scopes: ["openid", "profile", "email"] EOF +cat $DYNAMIC_CONFIG + # Finally start CMD exec "$@" diff --git a/services/identity.router b/services/identity.router new file mode 100644 index 0000000..a3478d0 --- /dev/null +++ b/services/identity.router @@ -0,0 +1,7 @@ + identity: + rule: "PathPrefix(`/system/identity`)" + service: identity + entryPoints: + - main + middlewares: + - oidc-auth diff --git a/services/identity.service b/services/identity.service new file mode 100644 index 0000000..09f8c08 --- /dev/null +++ b/services/identity.service @@ -0,0 +1,6 @@ + identity: + loadBalancer: + servers: + - url: "http://${IDENTITY_HOST}:${IDENTITY_PORT}" + # Forward the original Host header to the backend service + passHostHeader: true From 8c5d77bbce29ea4fbaa6f3363bfc4bf636b5b450 Mon Sep 17 00:00:00 2001 From: Jan Malte Behrens Date: Thu, 4 Jun 2026 15:01:10 +0200 Subject: [PATCH 05/13] feat: Created local-only plugin to append user-id header to traefik headers --- entrypoint.sh | 6 ++ .../openslides/user-id-header/.traefik.yaml | 5 ++ .../openslides/user-id-header/LICENSE | 21 +++++ .../openslides/user-id-header/go.mod | 5 ++ .../openslides/user-id-header/go.sum | 2 + .../openslides/user-id-header/plugin.go | 77 +++++++++++++++++++ services/action.router | 1 + services/autoupdate.router | 1 + services/client.router | 1 + services/icc.router | 1 + services/identity.router | 1 + services/media.router | 1 + services/presenter.router | 1 + services/projector.router | 1 + services/search.router | 1 + services/vote.router | 1 + 16 files changed, 126 insertions(+) create mode 100644 plugins-local/src/github.com/openslides/user-id-header/.traefik.yaml create mode 100644 plugins-local/src/github.com/openslides/user-id-header/LICENSE create mode 100644 plugins-local/src/github.com/openslides/user-id-header/go.mod create mode 100644 plugins-local/src/github.com/openslides/user-id-header/go.sum create mode 100644 plugins-local/src/github.com/openslides/user-id-header/plugin.go diff --git a/entrypoint.sh b/entrypoint.sh index 86eea20..627ebdd 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -68,6 +68,9 @@ experimental: 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 @@ -220,6 +223,9 @@ echo "Enabling OIDC authentication middleware" Value: "{{\`Bearer: {{ .accessToken }}\`}}" IncludeWhen: "Public" Scopes: ["openid", "profile", "email"] + user-id: + plugin: + user-id-header: {} EOF cat $DYNAMIC_CONFIG diff --git a/plugins-local/src/github.com/openslides/user-id-header/.traefik.yaml b/plugins-local/src/github.com/openslides/user-id-header/.traefik.yaml new file mode 100644 index 0000000..01fc6d4 --- /dev/null +++ b/plugins-local/src/github.com/openslides/user-id-header/.traefik.yaml @@ -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: {} diff --git a/plugins-local/src/github.com/openslides/user-id-header/LICENSE b/plugins-local/src/github.com/openslides/user-id-header/LICENSE new file mode 100644 index 0000000..da02d3d --- /dev/null +++ b/plugins-local/src/github.com/openslides/user-id-header/LICENSE @@ -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. diff --git a/plugins-local/src/github.com/openslides/user-id-header/go.mod b/plugins-local/src/github.com/openslides/user-id-header/go.mod new file mode 100644 index 0000000..fda9df0 --- /dev/null +++ b/plugins-local/src/github.com/openslides/user-id-header/go.mod @@ -0,0 +1,5 @@ +module github.com/openslides/user-id-header + +go 1.25.0 + +require github.com/golang-jwt/jwt/v4 v4.5.2 diff --git a/plugins-local/src/github.com/openslides/user-id-header/go.sum b/plugins-local/src/github.com/openslides/user-id-header/go.sum new file mode 100644 index 0000000..adbc53d --- /dev/null +++ b/plugins-local/src/github.com/openslides/user-id-header/go.sum @@ -0,0 +1,2 @@ +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= diff --git a/plugins-local/src/github.com/openslides/user-id-header/plugin.go b/plugins-local/src/github.com/openslides/user-id-header/plugin.go new file mode 100644 index 0000000..7433171 --- /dev/null +++ b/plugins-local/src/github.com/openslides/user-id-header/plugin.go @@ -0,0 +1,77 @@ +package myplugin + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/golang-jwt/jwt/v4" +) + +// Mandatory config struct +type Config struct { +} + +func CreateConfig() *Config { + return &Config{} +} + +type UserIDHeaderInsert struct { + next http.Handler + name string + sourceHeader string + targetHeader string +} + +// Create new plugin instance +func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) { + return &UserIDHeaderInsert{ + next: next, + name: name, + }, nil +} + +type payloadKeycloak struct { + jwt.RegisteredClaims + KeycloakID string `json:"sub"` + SessionID string `json:"sid"` // Keycloak session ID + Email string `json:"email"` + Username string `json:"preferred_username"` + ClientName string `json:"azp"` + OSUserID string `json:"os_id"` +} + +func extractUserID(r *http.Request) int { + header := r.Header.Get("Authorization") + encodedToken := strings.TrimPrefix(header, "Bearer: ") + + if header == encodedToken { + // No token. Handle the request as public access requst. + return 0 + } + + token, _, err := new(jwt.Parser).ParseUnverified(header, &payloadKeycloak{}) + if err != nil { + fmt.Println("parsing token: %w", err) + return 0 + } + + user_id, ok := token.Header["os_id"].(int) + if !ok { + fmt.Println("missing user id in token header") + return 0 + } + return user_id +} + +func (p *UserIDHeaderInsert) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Extract User ID + user_id := extractUserID(r) + + // Write it as a new header + r.Header.Set("X-User-ID", fmt.Sprint(user_id)) + + // Pass + p.next.ServeHTTP(w, r) +} diff --git a/services/action.router b/services/action.router index 5f7db26..4423e3b 100644 --- a/services/action.router +++ b/services/action.router @@ -5,3 +5,4 @@ - main middlewares: - oidc-auth + - user-id diff --git a/services/autoupdate.router b/services/autoupdate.router index 9aeda44..ddae1c9 100644 --- a/services/autoupdate.router +++ b/services/autoupdate.router @@ -5,3 +5,4 @@ - main middlewares: - oidc-auth + - user-id diff --git a/services/client.router b/services/client.router index a962d0e..63c501a 100644 --- a/services/client.router +++ b/services/client.router @@ -7,3 +7,4 @@ priority: 1 middlewares: - oidc-auth + - user-id diff --git a/services/icc.router b/services/icc.router index 9ec790a..67cd54e 100644 --- a/services/icc.router +++ b/services/icc.router @@ -5,3 +5,4 @@ - main middlewares: - oidc-auth + - user-id diff --git a/services/identity.router b/services/identity.router index a3478d0..19a8e13 100644 --- a/services/identity.router +++ b/services/identity.router @@ -5,3 +5,4 @@ - main middlewares: - oidc-auth + - user-id diff --git a/services/media.router b/services/media.router index f44a71b..567a417 100644 --- a/services/media.router +++ b/services/media.router @@ -5,3 +5,4 @@ - main middlewares: - oidc-auth + - user-id diff --git a/services/presenter.router b/services/presenter.router index b9a9b64..8610b88 100644 --- a/services/presenter.router +++ b/services/presenter.router @@ -5,3 +5,4 @@ - main middlewares: - oidc-auth + - user-id diff --git a/services/projector.router b/services/projector.router index 7e94244..93dc66b 100644 --- a/services/projector.router +++ b/services/projector.router @@ -5,3 +5,4 @@ - main middlewares: - oidc-auth + - user-id diff --git a/services/search.router b/services/search.router index bb0e24f..99cdf56 100644 --- a/services/search.router +++ b/services/search.router @@ -5,3 +5,4 @@ - main middlewares: - oidc-auth + - user-id diff --git a/services/vote.router b/services/vote.router index 5c654eb..b513a40 100644 --- a/services/vote.router +++ b/services/vote.router @@ -5,3 +5,4 @@ - main middlewares: - oidc-auth + - user-id From 72951c0b5e83befd568d3634cd044602af1b916b Mon Sep 17 00:00:00 2001 From: Jan Malte Behrens Date: Thu, 4 Jun 2026 15:23:06 +0200 Subject: [PATCH 06/13] fix: Writing own JWT interpreter to avoid vendoring. Adding local plugin to docker image --- Dockerfile | 11 +++--- .../openslides/user-id-header/go.mod | 2 -- .../openslides/user-id-header/plugin.go | 36 ++++++++++--------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/Dockerfile b/Dockerfile index 126a006..56df51d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,14 +11,15 @@ ENV APP_CONTEXT=${CONTEXT} RUN apk add --no-cache curl gettext # Copy configuration files -COPY entrypoint.sh /entrypoint.sh -COPY certs /certs -COPY services /services -COPY templates /templates +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 -RUN chmod +x /entrypoint.sh +RUN chmod +x ./entrypoint.sh # External Information LABEL org.opencontainers.image.title="OpenSlides Traefik Proxy" diff --git a/plugins-local/src/github.com/openslides/user-id-header/go.mod b/plugins-local/src/github.com/openslides/user-id-header/go.mod index fda9df0..33605d0 100644 --- a/plugins-local/src/github.com/openslides/user-id-header/go.mod +++ b/plugins-local/src/github.com/openslides/user-id-header/go.mod @@ -1,5 +1,3 @@ module github.com/openslides/user-id-header go 1.25.0 - -require github.com/golang-jwt/jwt/v4 v4.5.2 diff --git a/plugins-local/src/github.com/openslides/user-id-header/plugin.go b/plugins-local/src/github.com/openslides/user-id-header/plugin.go index 7433171..0173e31 100644 --- a/plugins-local/src/github.com/openslides/user-id-header/plugin.go +++ b/plugins-local/src/github.com/openslides/user-id-header/plugin.go @@ -2,11 +2,11 @@ package myplugin import ( "context" + "encoding/base64" + "encoding/json" "fmt" "net/http" "strings" - - "github.com/golang-jwt/jwt/v4" ) // Mandatory config struct @@ -32,16 +32,6 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h }, nil } -type payloadKeycloak struct { - jwt.RegisteredClaims - KeycloakID string `json:"sub"` - SessionID string `json:"sid"` // Keycloak session ID - Email string `json:"email"` - Username string `json:"preferred_username"` - ClientName string `json:"azp"` - OSUserID string `json:"os_id"` -} - func extractUserID(r *http.Request) int { header := r.Header.Get("Authorization") encodedToken := strings.TrimPrefix(header, "Bearer: ") @@ -51,18 +41,30 @@ func extractUserID(r *http.Request) int { return 0 } - token, _, err := new(jwt.Parser).ParseUnverified(header, &payloadKeycloak{}) + parts := strings.Split(encodedToken, ".") + if len(parts) != 3 { + fmt.Println("JWT partition not length 3") + return 0 + } + + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil { - fmt.Println("parsing token: %w", err) + fmt.Println("decoding token payload:", err) + return 0 + } + + var claims map[string]interface{} + if err := json.Unmarshal(payload, &claims); err != nil { + fmt.Println("parsing token claims:", err) return 0 } - user_id, ok := token.Header["os_id"].(int) + osID, ok := claims["os_id"].(float64) if !ok { - fmt.Println("missing user id in token header") + fmt.Println("missing or invalid os_id in token claims") return 0 } - return user_id + return int(osID) } func (p *UserIDHeaderInsert) ServeHTTP(w http.ResponseWriter, r *http.Request) { From 4c6f5cb19fbc16c2b4d882c14da9d03c695c5825 Mon Sep 17 00:00:00 2001 From: Jan Malte Behrens Date: Thu, 4 Jun 2026 15:39:30 +0200 Subject: [PATCH 07/13] fix: Move files in dockerfile again --- Dockerfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 56df51d..972e59a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,15 +11,15 @@ ENV APP_CONTEXT=${CONTEXT} RUN apk add --no-cache curl gettext # Copy configuration files -COPY entrypoint.sh ./entrypoint.sh -COPY certs ./certs -COPY services ./services -COPY templates ./templates -COPY plugins-local ./plugins-local +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 -RUN chmod +x ./entrypoint.sh +RUN chmod +x /entrypoint.sh # External Information LABEL org.opencontainers.image.title="OpenSlides Traefik Proxy" From c93e5b20dda051aa2aac5e4b518ab114ef99200d Mon Sep 17 00:00:00 2001 From: Jan Malte Behrens Date: Thu, 4 Jun 2026 16:25:47 +0200 Subject: [PATCH 08/13] feat: Plugin now loaded correctly. User ID written into the response, making it available for client --- Dockerfile | 3 +- entrypoint.sh | 6 +-- .../openslides/user-id-header/go.mod | 3 -- .../openslides/user-id-header/go.sum | 2 - .../.traefik.yml} | 2 +- .../LICENSE | 0 .../openslides/user_id_header/go.mod | 3 ++ .../plugin.go | 53 +++++++++++++++---- 8 files changed, 51 insertions(+), 21 deletions(-) delete mode 100644 plugins-local/src/github.com/openslides/user-id-header/go.mod delete mode 100644 plugins-local/src/github.com/openslides/user-id-header/go.sum rename plugins-local/src/github.com/openslides/{user-id-header/.traefik.yaml => user_id_header/.traefik.yml} (69%) rename plugins-local/src/github.com/openslides/{user-id-header => user_id_header}/LICENSE (100%) create mode 100644 plugins-local/src/github.com/openslides/user_id_header/go.mod rename plugins-local/src/github.com/openslides/{user-id-header => user_id_header}/plugin.go (58%) diff --git a/Dockerfile b/Dockerfile index 972e59a..6c21ee5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ COPY entrypoint.sh /entrypoint.sh COPY certs /certs COPY services /services COPY templates /templates -COPY plugins-local /plugins-local +COPY plugins-local ./plugins-local # Create dynamic config directory and make entrypoint executable RUN mkdir -p /etc/traefik/dynamic @@ -57,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 diff --git a/entrypoint.sh b/entrypoint.sh index 627ebdd..b6bb57a 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -69,8 +69,8 @@ experimental: moduleName: github.com/sevensolutions/traefik-oidc-auth version: v0.20.0 localPlugins: - user-id-header: - moduleName: github.com/openslides/user-id-header + user_id_header: + moduleName: github.com/openslides/user_id_header EOF @@ -225,7 +225,7 @@ echo "Enabling OIDC authentication middleware" Scopes: ["openid", "profile", "email"] user-id: plugin: - user-id-header: {} + user_id_header: {} EOF cat $DYNAMIC_CONFIG diff --git a/plugins-local/src/github.com/openslides/user-id-header/go.mod b/plugins-local/src/github.com/openslides/user-id-header/go.mod deleted file mode 100644 index 33605d0..0000000 --- a/plugins-local/src/github.com/openslides/user-id-header/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/openslides/user-id-header - -go 1.25.0 diff --git a/plugins-local/src/github.com/openslides/user-id-header/go.sum b/plugins-local/src/github.com/openslides/user-id-header/go.sum deleted file mode 100644 index adbc53d..0000000 --- a/plugins-local/src/github.com/openslides/user-id-header/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= -github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= diff --git a/plugins-local/src/github.com/openslides/user-id-header/.traefik.yaml b/plugins-local/src/github.com/openslides/user_id_header/.traefik.yml similarity index 69% rename from plugins-local/src/github.com/openslides/user-id-header/.traefik.yaml rename to plugins-local/src/github.com/openslides/user_id_header/.traefik.yml index 01fc6d4..574767f 100644 --- a/plugins-local/src/github.com/openslides/user-id-header/.traefik.yaml +++ b/plugins-local/src/github.com/openslides/user_id_header/.traefik.yml @@ -1,5 +1,5 @@ displayName: User ID Header Insert type: middleware -import: github.com/openslides/user-id-header +import: github.com/openslides/user_id_header summary: Appends user id to headers testData: {} diff --git a/plugins-local/src/github.com/openslides/user-id-header/LICENSE b/plugins-local/src/github.com/openslides/user_id_header/LICENSE similarity index 100% rename from plugins-local/src/github.com/openslides/user-id-header/LICENSE rename to plugins-local/src/github.com/openslides/user_id_header/LICENSE diff --git a/plugins-local/src/github.com/openslides/user_id_header/go.mod b/plugins-local/src/github.com/openslides/user_id_header/go.mod new file mode 100644 index 0000000..292ece6 --- /dev/null +++ b/plugins-local/src/github.com/openslides/user_id_header/go.mod @@ -0,0 +1,3 @@ +module github.com/openslides/user_id_header + +go 1.23.0 diff --git a/plugins-local/src/github.com/openslides/user-id-header/plugin.go b/plugins-local/src/github.com/openslides/user_id_header/plugin.go similarity index 58% rename from plugins-local/src/github.com/openslides/user-id-header/plugin.go rename to plugins-local/src/github.com/openslides/user_id_header/plugin.go index 0173e31..f8583ae 100644 --- a/plugins-local/src/github.com/openslides/user-id-header/plugin.go +++ b/plugins-local/src/github.com/openslides/user_id_header/plugin.go @@ -1,4 +1,4 @@ -package myplugin +package user_id_header import ( "context" @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "net/http" + "strconv" "strings" ) @@ -18,10 +19,8 @@ func CreateConfig() *Config { } type UserIDHeaderInsert struct { - next http.Handler - name string - sourceHeader string - targetHeader string + next http.Handler + name string } // Create new plugin instance @@ -59,12 +58,37 @@ func extractUserID(r *http.Request) int { return 0 } - osID, ok := claims["os_id"].(float64) - if !ok { - fmt.Println("missing or invalid os_id in token claims") + rawOsID, exists := claims["os_id"] + if !exists { + fmt.Println("missing os_id in token claims") return 0 } - return int(osID) + + osID, err := strconv.Atoi(fmt.Sprintf("%v", rawOsID)) + if err != nil { + fmt.Println("invalid os_id value:", rawOsID) + return 0 + } + return osID +} + +type responseWriter struct { + http.ResponseWriter + 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) + } + return w.ResponseWriter.Write(b) } func (p *UserIDHeaderInsert) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -72,8 +96,15 @@ func (p *UserIDHeaderInsert) ServeHTTP(w http.ResponseWriter, r *http.Request) { user_id := extractUserID(r) // Write it as a new header - r.Header.Set("X-User-ID", fmt.Sprint(user_id)) + r.Header.Set(userIDHeader, fmt.Sprint(user_id)) // Pass - p.next.ServeHTTP(w, r) + p.next.ServeHTTP(&responseWriter{ + ResponseWriter: w, + userID: fmt.Sprint(user_id), + }, r) } + +const ( + userIDHeader string = "X-User-ID" +) From a77760229dc0560a9d3dd7702f84e7afc7fb463c Mon Sep 17 00:00:00 2001 From: Jan Malte Behrens Date: Fri, 5 Jun 2026 12:17:20 +0200 Subject: [PATCH 09/13] feat: Flusher --- .../src/github.com/openslides/user_id_header/plugin.go | 6 ++++++ services/client.router | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins-local/src/github.com/openslides/user_id_header/plugin.go b/plugins-local/src/github.com/openslides/user_id_header/plugin.go index f8583ae..225af8a 100644 --- a/plugins-local/src/github.com/openslides/user_id_header/plugin.go +++ b/plugins-local/src/github.com/openslides/user_id_header/plugin.go @@ -91,6 +91,12 @@ func (w *responseWriter) Write(b []byte) (int, error) { return w.ResponseWriter.Write(b) } +func (w *responseWriter) Flush() { + if f, ok := w.ResponseWriter.(http.Flusher); ok { + f.Flush() + } +} + func (p *UserIDHeaderInsert) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Extract User ID user_id := extractUserID(r) diff --git a/services/client.router b/services/client.router index 63c501a..a962d0e 100644 --- a/services/client.router +++ b/services/client.router @@ -7,4 +7,3 @@ priority: 1 middlewares: - oidc-auth - - user-id From 5ed181e2e51cdee705301a74983222d2d2c075e6 Mon Sep 17 00:00:00 2001 From: Jan Malte Behrens Date: Fri, 5 Jun 2026 12:22:13 +0200 Subject: [PATCH 10/13] feat: Adding additional debugging message --- plugins-local/src/github.com/openslides/user_id_header/plugin.go | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins-local/src/github.com/openslides/user_id_header/plugin.go b/plugins-local/src/github.com/openslides/user_id_header/plugin.go index 225af8a..03fbeea 100644 --- a/plugins-local/src/github.com/openslides/user_id_header/plugin.go +++ b/plugins-local/src/github.com/openslides/user_id_header/plugin.go @@ -37,6 +37,7 @@ func extractUserID(r *http.Request) int { if header == encodedToken { // No token. Handle the request as public access requst. + fmt.Println("No token") return 0 } From 590af32d219ef4ca797e47e24101490802941fda Mon Sep 17 00:00:00 2001 From: Jan Malte Behrens Date: Thu, 11 Jun 2026 17:12:02 +0200 Subject: [PATCH 11/13] feat: It works --- .../github.com/openslides/user_id_header/plugin.go | 14 ++++++++------ services/auth.router | 3 +++ services/client.router | 1 + 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins-local/src/github.com/openslides/user_id_header/plugin.go b/plugins-local/src/github.com/openslides/user_id_header/plugin.go index 03fbeea..d0f0012 100644 --- a/plugins-local/src/github.com/openslides/user_id_header/plugin.go +++ b/plugins-local/src/github.com/openslides/user_id_header/plugin.go @@ -37,37 +37,37 @@ func extractUserID(r *http.Request) int { if header == encodedToken { // No token. Handle the request as public access requst. - fmt.Println("No token") + fmt.Println("header error: no token") return 0 } parts := strings.Split(encodedToken, ".") if len(parts) != 3 { - fmt.Println("JWT partition not length 3") + fmt.Println("header error: JWT partition not length 3") return 0 } payload, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil { - fmt.Println("decoding token payload:", err) + fmt.Println("header error: decoding token payload:", err) return 0 } var claims map[string]interface{} if err := json.Unmarshal(payload, &claims); err != nil { - fmt.Println("parsing token claims:", err) + fmt.Println("header error: parsing token claims:", err) return 0 } rawOsID, exists := claims["os_id"] if !exists { - fmt.Println("missing os_id in token claims") + fmt.Println("header error: missing os_id in token claims") return 0 } osID, err := strconv.Atoi(fmt.Sprintf("%v", rawOsID)) if err != nil { - fmt.Println("invalid os_id value:", rawOsID) + fmt.Println("header error: invalid os_id value:", rawOsID) return 0 } return osID @@ -92,11 +92,13 @@ func (w *responseWriter) Write(b []byte) (int, error) { return w.ResponseWriter.Write(b) } +/* func (w *responseWriter) Flush() { if f, ok := w.ResponseWriter.(http.Flusher); ok { f.Flush() } } +*/ func (p *UserIDHeaderInsert) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Extract User ID diff --git a/services/auth.router b/services/auth.router index 0282964..4b2396d 100644 --- a/services/auth.router +++ b/services/auth.router @@ -3,3 +3,6 @@ service: auth entryPoints: - main + middlewares: + - oidc-auth + - user-id diff --git a/services/client.router b/services/client.router index a962d0e..63c501a 100644 --- a/services/client.router +++ b/services/client.router @@ -7,3 +7,4 @@ priority: 1 middlewares: - oidc-auth + - user-id From 3b2a641074e6c7293500cd34811ab9f06a73c633 Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Fri, 12 Jun 2026 19:29:05 +0200 Subject: [PATCH 12/13] Fix http flush in user id middleware --- .../openslides/user_id_header/plugin.go | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/plugins-local/src/github.com/openslides/user_id_header/plugin.go b/plugins-local/src/github.com/openslides/user_id_header/plugin.go index d0f0012..cc3ea95 100644 --- a/plugins-local/src/github.com/openslides/user_id_header/plugin.go +++ b/plugins-local/src/github.com/openslides/user_id_header/plugin.go @@ -53,7 +53,7 @@ func extractUserID(r *http.Request) int { return 0 } - var claims map[string]interface{} + var claims map[string]any if err := json.Unmarshal(payload, &claims); err != nil { fmt.Println("header error: parsing token claims:", err) return 0 @@ -75,6 +75,7 @@ func extractUserID(r *http.Request) int { type responseWriter struct { http.ResponseWriter + flusher http.Flusher userID string headerSet bool } @@ -89,16 +90,20 @@ func (w *responseWriter) Write(b []byte) (int, error) { if !w.headerSet { w.Header().Set(userIDHeader, w.userID) } - return w.ResponseWriter.Write(b) + + n, err := w.ResponseWriter.Write(b) + if w.flusher != nil { + w.flusher.Flush() + } + + return n, err } -/* func (w *responseWriter) Flush() { - if f, ok := w.ResponseWriter.(http.Flusher); ok { - f.Flush() + if w.flusher != nil { + w.flusher.Flush() } } -*/ func (p *UserIDHeaderInsert) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Extract User ID @@ -107,8 +112,14 @@ func (p *UserIDHeaderInsert) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Write it as a new header r.Header.Set(userIDHeader, fmt.Sprint(user_id)) + var flusher http.Flusher + if f, ok := w.(http.Flusher); ok { + flusher = f + } + // Pass p.next.ServeHTTP(&responseWriter{ + flusher: flusher, ResponseWriter: w, userID: fmt.Sprint(user_id), }, r) From d301d9edc49f77fe934dcefd67eabeaa3ff8c96a Mon Sep 17 00:00:00 2001 From: Jan Malte Behrens Date: Thu, 18 Jun 2026 16:24:05 +0200 Subject: [PATCH 13/13] feat: Access Token Plugin Attempt --- entrypoint.sh | 5 + .../access_token_blocklist/.traefik.yml | 5 + .../openslides/access_token_blocklist/LICENSE | 21 +++ .../openslides/access_token_blocklist/go.mod | 3 + .../access_token_blocklist/plugin.go | 151 ++++++++++++++++++ 5 files changed, 185 insertions(+) create mode 100644 plugins-local/src/github.com/openslides/access_token_blocklist/.traefik.yml create mode 100644 plugins-local/src/github.com/openslides/access_token_blocklist/LICENSE create mode 100644 plugins-local/src/github.com/openslides/access_token_blocklist/go.mod create mode 100644 plugins-local/src/github.com/openslides/access_token_blocklist/plugin.go diff --git a/entrypoint.sh b/entrypoint.sh index b6bb57a..8651c27 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -71,6 +71,8 @@ experimental: localPlugins: user_id_header: moduleName: github.com/openslides/user_id_header + access_token_blocklist: + moduleName: github.com/openslides/access_token_blocklist EOF @@ -226,6 +228,9 @@ echo "Enabling OIDC authentication middleware" user-id: plugin: user_id_header: {} + access-token-blocklist: + plugin: + access_token_blocklist: {} EOF cat $DYNAMIC_CONFIG diff --git a/plugins-local/src/github.com/openslides/access_token_blocklist/.traefik.yml b/plugins-local/src/github.com/openslides/access_token_blocklist/.traefik.yml new file mode 100644 index 0000000..33a0a1a --- /dev/null +++ b/plugins-local/src/github.com/openslides/access_token_blocklist/.traefik.yml @@ -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: {} diff --git a/plugins-local/src/github.com/openslides/access_token_blocklist/LICENSE b/plugins-local/src/github.com/openslides/access_token_blocklist/LICENSE new file mode 100644 index 0000000..da02d3d --- /dev/null +++ b/plugins-local/src/github.com/openslides/access_token_blocklist/LICENSE @@ -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. diff --git a/plugins-local/src/github.com/openslides/access_token_blocklist/go.mod b/plugins-local/src/github.com/openslides/access_token_blocklist/go.mod new file mode 100644 index 0000000..22258c5 --- /dev/null +++ b/plugins-local/src/github.com/openslides/access_token_blocklist/go.mod @@ -0,0 +1,3 @@ +module github.com/openslides/access_token_blocklist + +go 1.23.0 diff --git a/plugins-local/src/github.com/openslides/access_token_blocklist/plugin.go b/plugins-local/src/github.com/openslides/access_token_blocklist/plugin.go new file mode 100644 index 0000000..bd2b2e3 --- /dev/null +++ b/plugins-local/src/github.com/openslides/access_token_blocklist/plugin.go @@ -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" +)