From 3d2c2e3a90eaf70a0badac0fa083818238ccc191 Mon Sep 17 00:00:00 2001 From: Gustavo J Gallardo Date: Mon, 23 Jun 2025 13:37:28 -0300 Subject: [PATCH 01/24] refactor - wip --- Dockerfile | 18 +- README.md | 22 - conf/demoapp.conf | 64 --- conf/info.conf | 30 -- conf/info_multi.conf | 15 + conf/info_single.conf | 13 + conf/multi-host.conf | 33 ++ conf/multi-host/README.md | 4 + conf/multi-path/README.md | 4 + conf/multi-path/single.conf | 26 ++ conf/multi-path_landing.conf | 7 + conf/multi-path_private.conf | 8 + conf/multi-path_root.conf | 3 + ...bles.conf => multi_default_variables.conf} | 12 +- conf/nginx.conf.template | 13 +- conf/server.conf | 38 +- html/index.html | 18 +- .../multi-host}/compose.yaml | 17 +- .../multi-host/conf}/demoapp1.conf | 7 +- .../multi-host/conf}/demoapp2.conf | 10 +- .../multi-host}/keycloak/demorealm.json | 5 +- localhost/multi-path/compose.yaml | 39 ++ localhost/multi-path/conf/demoapp1.conf | 15 + .../multi-path/conf/demoapp1_variables.conf | 11 + localhost/multi-path/conf/demoapp2.conf | 16 + .../multi-path/conf/demoapp2_variables.conf | 11 + localhost/multi-path/keycloak/demorealm.json | 113 +++++ compose.yaml => localhost/single/compose.yaml | 24 +- .../single/keycloak}/demorealm.json | 2 +- lua/ipax.lua | 441 +++++++----------- lua/ipax_openidc.lua | 170 ------- lua/multiapps.lua | 116 ----- multiapps/server.conf | 9 - templates/info.html | 6 +- templates/landing.html | 4 +- 35 files changed, 563 insertions(+), 781 deletions(-) delete mode 100644 conf/demoapp.conf delete mode 100644 conf/info.conf create mode 100644 conf/info_multi.conf create mode 100644 conf/info_single.conf create mode 100644 conf/multi-host.conf create mode 100644 conf/multi-host/README.md create mode 100644 conf/multi-path/README.md create mode 100644 conf/multi-path/single.conf create mode 100644 conf/multi-path_landing.conf create mode 100644 conf/multi-path_private.conf create mode 100644 conf/multi-path_root.conf rename conf/{demoapp_default_variables.conf => multi_default_variables.conf} (62%) rename {multiapps => localhost/multi-host}/compose.yaml (57%) rename {multiapps/conf.d => localhost/multi-host/conf}/demoapp1.conf (77%) rename {multiapps/conf.d => localhost/multi-host/conf}/demoapp2.conf (64%) rename {multiapps => localhost/multi-host}/keycloak/demorealm.json (96%) create mode 100644 localhost/multi-path/compose.yaml create mode 100644 localhost/multi-path/conf/demoapp1.conf create mode 100644 localhost/multi-path/conf/demoapp1_variables.conf create mode 100644 localhost/multi-path/conf/demoapp2.conf create mode 100644 localhost/multi-path/conf/demoapp2_variables.conf create mode 100644 localhost/multi-path/keycloak/demorealm.json rename compose.yaml => localhost/single/compose.yaml (59%) rename {keycloak => localhost/single/keycloak}/demorealm.json (98%) delete mode 100644 lua/ipax_openidc.lua delete mode 100644 lua/multiapps.lua delete mode 100644 multiapps/server.conf diff --git a/Dockerfile b/Dockerfile index 82fc955..a40588a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,10 @@ -FROM openresty/openresty:1.25.3.1-alpine-fat +FROM openresty/openresty:1.25.3.2-alpine-fat -RUN luarocks install lua-resty-openidc -RUN luarocks install lua-resty-template +RUN luarocks install lua-resty-http 0.17.2 +RUN luarocks install lua-resty-session 4.0.5 +RUN luarocks install lua-resty-jwt 0.2.3 +RUN luarocks install lua-resty-openidc 1.8.0 +RUN luarocks install lua-resty-template 2.0 COPY conf/ /usr/local/openresty/nginx/conf/ COPY lua/ /etc/ipax/lua/ @@ -11,9 +14,9 @@ COPY templates /var/ipax/templates ENV NGINX_LOG_LEVEL=warn \ NGINX_RESOLVER=8.8.8.8 \ SESSION_SECRET="ipax_default_secret" \ - SESSION_COOKIE_PERSISTENT=off \ - SESSION_COOKIE_LIFETIME=86400 \ + SESSION_COOKIE_REMEMBER="true" \ SESSION_COOKIE_SAMESITE="Lax" \ + SESSION_IDLETIMEOUT="86400" \ OIDC_DISCOVERY="" \ OIDC_SSL_VERIFY="yes" \ OIDC_CLIENT_ID="" \ @@ -34,8 +37,11 @@ ENV NGINX_LOG_LEVEL=warn \ KC_ENROL_BIOMETRICS_ACTION="" \ KC_ENROL_BIOMETRICS_LABEL="Enrol biometrics" \ IPAX_APP_NAME="IPAx" \ + IPAX_BASEURL="http://localhost" \ API_BASEURL="" WORKDIR /usr/local/openresty/nginx -CMD ["sh", "-c", "envsubst < conf/nginx.conf.template > conf/nginx.conf && /usr/local/openresty/bin/openresty -g 'daemon off;'"] +HEALTHCHECK --interval=30s --timeout=1s --start-period=5s --retries=3 CMD [ "curl", "-f", "http://localhost/ipax/health" ] + +CMD ["sh", "-c", "envsubst < /etc/ipax/conf/nginx.conf.template > conf/nginx.conf && /usr/local/openresty/bin/openresty -g 'daemon off;'"] diff --git a/README.md b/README.md index 3958ff3..84cb5c5 100644 --- a/README.md +++ b/README.md @@ -32,25 +32,3 @@ docker run -d \ ``` > To use PKCE, remove `OIDC_CLIENT_SECRET` and add `OIDC_USE_PKCE` with value "true" - -## Certificates (optional) -Issue as many certificates as necessary to be used in your reverse proxy. -IPAx supports [wildcard certificates](https://en.wikipedia.org/wiki/Wildcard_certificate) and [SNI](https://en.wikipedia.org/wiki/Server_Name_Indication). - -If you want to use HTTPS, add mapping for port 443 and mount volume `./certs/` as /etc/ipax/certs/ - -### Self-signed certificate -To test using a self-signed certificate, run the following command (replace with your domain): -```sh -openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout wildcard_identicum_com.key -out wildcard_identicum_com.cer -``` - - Country Name (2 letter code) [XX]: `AR` - State or Province Name (full name) []: `CABA` - Locality Name (eg, city) [Default City]: `Buenos Aires` - Organization Name (eg, company) [Default Company Ltd]: `Identicum` - Organizational Unit Name (eg, section) []: ` ` - Common Name (eg, your name or your server hostname) []: `*.identicum.com` - Email Address []: `no-reply@identicum.com` - -Put the generated certificate files into your local `./certs/` directory. \ No newline at end of file diff --git a/conf/demoapp.conf b/conf/demoapp.conf deleted file mode 100644 index c3190f8..0000000 --- a/conf/demoapp.conf +++ /dev/null @@ -1,64 +0,0 @@ -listen 80; -set $template_root /var/ipax/templates; - -location / { - root /var/ipax/html/; - add_header Cache-Control no-store; - add_header Pragma no-cache; -} -location /landing { - add_header Cache-Control no-store; - add_header Pragma no-cache; - default_type text/html; - content_by_lua_block { - local data = { app_name = ngx.var.demoapp_alias } - require("resty.template").render_file("landing.html", data) - } -} -location /private { - add_header Cache-Control no-store; - add_header Pragma no-cache; - access_by_lua_block { - local oidc_opts = { - discovery = ngx.var.oidc_discovery, - client_id = ngx.var.client_id, - client_secret = ngx.var.client_secret, - scope = ngx.var.scope - } - local res = require("multiapps").get_res(oidc_opts, ngx.var.demoapp_base_url, ngx.var.prompt_override); - } -} -location /private/info { - add_header Cache-Control no-store; - add_header Pragma no-cache; - default_type text/html; - content_by_lua_block { - local multiapps = require("multiapps") - local oidc_opts = { - discovery = ngx.var.oidc_discovery, - client_id = ngx.var.client_id, - client_secret = ngx.var.client_secret, - scope = ngx.var.scope - } - local kc_actions = { - delete_account = ngx.var.kc_delete_account_action, - update_password = ngx.var.kc_update_password_action, - update_email = ngx.var.kc_update_email_action, - enrol_biometrics = ngx.var.kc_enrol_biometrics_action - } - local res = multiapps.get_res(oidc_opts, ngx.var.demoapp_base_url, ngx.var.prompt_override); - local data = { - access_token = multiapps.get_access_token(res), - refresh_token = multiapps.get_refresh_token(res), - app_name = ngx.var.demoapp_alias, - headers = ngx.resp.get_headers(), - id_token = multiapps.get_id_token(res), - logout_uri = os.getenv("OIDC_LOGOUT_URI"), - user = res.user, - user_actions = multiapps.get_user_actions(oidc_opts, kc_actions), - userinfo_json = multiapps.get_userinfo_json(res), - username = multiapps.get_preferred_username_from_userinfo_or_idtoken(res) - } - require("resty.template").render_file("info.html", data) - } -} diff --git a/conf/info.conf b/conf/info.conf deleted file mode 100644 index fe550ee..0000000 --- a/conf/info.conf +++ /dev/null @@ -1,30 +0,0 @@ -default_type text/html; - -content_by_lua_block { - local ipax = require("ipax"); - local template = require("resty.template") - - -- id_token is returned as a lua table - local id_token = ipax.get_id_token(); - -- access_token is returned as string - local access_token = ipax.get_access_token(); - ngx.log(ngx.DEBUG, "access_token: " .. access_token) - local refresh_token = ipax.get_refresh_token(); - ngx.log(ngx.DEBUG, "refresh_token: " .. tostring(refresh_token)) - - - local data = { - user = ipax.get_user(), - headers = ngx.resp.get_headers(), - access_token = access_token, - refresh_token = refresh_token or "Not Provided in token endpoint response", - id_token = id_token, - userinfo_json = ipax.get_userinfo_json(), - username = ipax.get_preferred_username_from_userinfo_or_idtoken() or "Not Informed", - user_actions = ipax.get_user_actions(), - logout_uri = os.getenv("OIDC_LOGOUT_URI"), - app_name = os.getenv("IPAX_APP_NAME") - } - - template.render_file("info.html", data) -} diff --git a/conf/info_multi.conf b/conf/info_multi.conf new file mode 100644 index 0000000..c582460 --- /dev/null +++ b/conf/info_multi.conf @@ -0,0 +1,15 @@ +default_type text/html; +add_header Cache-Control no-store; +add_header Pragma no-cache; + +content_by_lua_block { + local ipax = require("ipax") + local oidc_opts = ipax.get_oidc_opts_multi() + local session_opts = ipax.get_session_opts_multi() + local app_name = ngx.var.demoapp_alias + local base_url = ngx.var.demoapp_base_url + local logout_uri = ngx.var.demoapp_base_url .. os.getenv("OIDC_LOGOUT_URI") + local headers = ngx.resp.get_headers() + local data = ipax.get_info_data(oidc_opts, session_opts, app_name, base_url, logout_uri, headers) + require("resty.template").render_file("info.html", data) +} diff --git a/conf/info_single.conf b/conf/info_single.conf new file mode 100644 index 0000000..9ce7f65 --- /dev/null +++ b/conf/info_single.conf @@ -0,0 +1,13 @@ +default_type text/html; + +content_by_lua_block { + local ipax = require("ipax") + local oidc_opts = ipax.get_oidc_opts_single() + local session_opts = ipax.get_session_opts_single() + local app_name = os.getenv("IPAX_APP_NAME") + local base_url = os.getenv("IPAX_BASEURL") + local logout_uri = os.getenv("OIDC_LOGOUT_URI") + local headers = ngx.resp.get_headers() + local data = ipax.get_info_data(oidc_opts, session_opts, app_name, base_url, logout_uri, headers) + require("resty.template").render_file("info.html", data) +} diff --git a/conf/multi-host.conf b/conf/multi-host.conf new file mode 100644 index 0000000..7b005e7 --- /dev/null +++ b/conf/multi-host.conf @@ -0,0 +1,33 @@ +listen 80; +set $template_root /var/ipax/templates; + +location / { + root /var/ipax/html/; + add_header Cache-Control no-store; + add_header Pragma no-cache; +} +location /landing { + add_header Cache-Control no-store; + add_header Pragma no-cache; + default_type text/html; + content_by_lua_block { + local data = { app_name = ngx.var.demoapp_alias } + require("resty.template").render_file("landing.html", data) + } +} +location /private { + add_header Cache-Control no-store; + add_header Pragma no-cache; + access_by_lua_block { + local ipax = require("ipax") + local oidc_opts = ipax.get_oidc_opts_multi() + local session_opts = ipax.get_session_opts_multi() + local res = ipax.get_res(oidc_opts, session_opts); + } +} +location /private/info { + add_header Cache-Control no-store; + add_header Pragma no-cache; + ngx.say('{"info": "ok"}'); + # include /etc/ipax/conf/info_multi.conf; +} diff --git a/conf/multi-host/README.md b/conf/multi-host/README.md new file mode 100644 index 0000000..941bef7 --- /dev/null +++ b/conf/multi-host/README.md @@ -0,0 +1,4 @@ +# Host-based multihoming - config files + +When using host-based multihoming, this folder is locally mounted. +See examples [here](../../localhost/multi-host/conf/) \ No newline at end of file diff --git a/conf/multi-path/README.md b/conf/multi-path/README.md new file mode 100644 index 0000000..c7a2152 --- /dev/null +++ b/conf/multi-path/README.md @@ -0,0 +1,4 @@ +# Path-based multihoming - config files + +When using path-based multihoming, this folder is locally mounted +See examples [here](../../localhost/multi-path/conf/) \ No newline at end of file diff --git a/conf/multi-path/single.conf b/conf/multi-path/single.conf new file mode 100644 index 0000000..ecbda4a --- /dev/null +++ b/conf/multi-path/single.conf @@ -0,0 +1,26 @@ +location / { + root /var/ipax/html/; + add_header Cache-Control no-store; + add_header Pragma no-cache; +} +location /landing { + add_header Cache-Control no-store; + add_header Pragma no-cache; + default_type text/html; + content_by_lua_block { + local data = { app_name = os.getenv("IPAX_APP_NAME") } + require("resty.template").render_file("landing.html", data) + } +} +location /private/ { + add_header Cache-Control no-store; + add_header Pragma no-cache; + access_by_lua_block { + local res = require("ipax").get_res(); + } +} +location /private/info { + add_header Cache-Control no-store; + add_header Pragma no-cache; + include /etc/ipax/conf/info_single.conf; +} \ No newline at end of file diff --git a/conf/multi-path_landing.conf b/conf/multi-path_landing.conf new file mode 100644 index 0000000..5e743f5 --- /dev/null +++ b/conf/multi-path_landing.conf @@ -0,0 +1,7 @@ +add_header Cache-Control no-store; +add_header Pragma no-cache; +default_type text/html; +content_by_lua_block { + local data = { app_name = ngx.var.demoapp_alias } + require("resty.template").render_file("landing.html", data) +} \ No newline at end of file diff --git a/conf/multi-path_private.conf b/conf/multi-path_private.conf new file mode 100644 index 0000000..97530bd --- /dev/null +++ b/conf/multi-path_private.conf @@ -0,0 +1,8 @@ +add_header Cache-Control no-store; +add_header Pragma no-cache; +access_by_lua_block { + local ipax = require("ipax") + local oidc_opts = ipax.get_oidc_opts_multi() + local session_opts = ipax.get_session_opts_multi() + local res = ipax.get_res(oidc_opts, session_opts) +} \ No newline at end of file diff --git a/conf/multi-path_root.conf b/conf/multi-path_root.conf new file mode 100644 index 0000000..c75af0a --- /dev/null +++ b/conf/multi-path_root.conf @@ -0,0 +1,3 @@ +alias /var/ipax/html/; +add_header Cache-Control no-store; +add_header Pragma no-cache; \ No newline at end of file diff --git a/conf/demoapp_default_variables.conf b/conf/multi_default_variables.conf similarity index 62% rename from conf/demoapp_default_variables.conf rename to conf/multi_default_variables.conf index 32cdc1a..6d1e573 100644 --- a/conf/demoapp_default_variables.conf +++ b/conf/multi_default_variables.conf @@ -1,11 +1,19 @@ set $demoapp_alias 'IPAx'; set $demoapp_base_url 'http://ipax'; + +# oidc_opts set $oidc_discovery ''; set $client_id ''; -set $client_secret ''; +set $use_pkce 'false'; set $scope 'openid profile'; -set $prompt_override ''; +set $client_secret ''; + +# session_opts +set $session_secret 'ipax_default_secret'; +set $session_cookie_samesite 'Lax'; +set $session_idle_timeout '3600'; +# user_actions set $kc_update_password_action ''; set $kc_update_email_action ''; set $kc_delete_account_action ''; diff --git a/conf/nginx.conf.template b/conf/nginx.conf.template index 6d75090..6c792fc 100644 --- a/conf/nginx.conf.template +++ b/conf/nginx.conf.template @@ -1,7 +1,7 @@ env SESSION_SECRET; -env SESSION_COOKIE_PERSISTENT; -env SESSION_COOKIE_LIFETIME; +env SESSION_COOKIE_REMEMBER; env SESSION_COOKIE_SAMESITE; +env SESSION_IDLETIMEOUT; env OIDC_DISCOVERY; env OIDC_SSL_VERIFY; env OIDC_CLIENT_ID; @@ -22,6 +22,7 @@ env KC_UPDATE_PASSWORD_LABEL; env KC_ENROL_BIOMETRICS_ACTION; env KC_ENROL_BIOMETRICS_LABEL; env IPAX_APP_NAME; +env IPAX_BASEURL; env API_BASEURL; error_log stderr $NGINX_LOG_LEVEL; @@ -43,9 +44,9 @@ http { proxy_buffers 8 32k; proxy_buffer_size 32k; - include lua.conf; - include filter_cookie.conf; - include server.conf; + include /etc/ipax/conf/lua.conf; + include /etc/ipax/conf/filter_cookie.conf; + include /etc/ipax/conf/server.conf; - include /etc/ipax/conf.d/*.conf; + include /etc/ipax/conf/multi-host/*.conf; } diff --git a/conf/server.conf b/conf/server.conf index ec9f1cb..5b1200d 100644 --- a/conf/server.conf +++ b/conf/server.conf @@ -1,46 +1,16 @@ server { set $template_root /var/ipax/templates; listen 80; - location / { - root /var/ipax/html/; - add_header Cache-Control no-store; - add_header Pragma no-cache; - # if ($request_uri = /){ - # return 302 $scheme://$server_name:$server_port/landing; - # } - } - location /landing { - add_header Cache-Control no-store; - add_header Pragma no-cache; - default_type text/html; - content_by_lua_block { - local template = require("resty.template") - local data = { - app_name = os.getenv("IPAX_APP_NAME") - } - template.render_file("landing.html", data) - } - } - location /private/ { - add_header Cache-Control no-store; - add_header Pragma no-cache; - access_by_lua_block { - local user = require("ipax").get_user(); - } - } - location /private/info { - add_header Cache-Control no-store; - add_header Pragma no-cache; - include info.conf; - } + location /robots.txt { add_header Cache-Control no-store; add_header Pragma no-cache; - include norobots.conf; + include /etc/ipax/conf/norobots.conf; } location /ipax/health { add_header Cache-Control no-store; add_header Pragma no-cache; - include health.conf; + include /etc/ipax/conf/health.conf; } + include /etc/ipax/conf/multi-path/*.conf; } \ No newline at end of file diff --git a/html/index.html b/html/index.html index 266a964..c5f7e0b 100644 --- a/html/index.html +++ b/html/index.html @@ -1,12 +1,12 @@ - - - - - - - + + + + + + + \ No newline at end of file diff --git a/multiapps/compose.yaml b/localhost/multi-host/compose.yaml similarity index 57% rename from multiapps/compose.yaml rename to localhost/multi-host/compose.yaml index 4fc76d7..707ea63 100644 --- a/multiapps/compose.yaml +++ b/localhost/multi-host/compose.yaml @@ -7,8 +7,8 @@ services: ports: - 8080:8080 environment: - KEYCLOAK_ADMIN: admin - KEYCLOAK_ADMIN_PASSWORD: admin + KC_BOOTSTRAP_ADMIN_USERNAME: admin + KC_BOOTSTRAP_ADMIN_PASSWORD: admin KC_FEATURES: update-email volumes: - ./keycloak/:/opt/keycloak/data/import/ @@ -16,7 +16,6 @@ services: demoapps: container_name: demoapps image: ghcr.io/identicum/ipax:latest - pull_policy: always restart: always ports: - 80:80 @@ -24,13 +23,11 @@ services: NGINX_RESOLVER: 127.0.0.11 valid=5s NGINX_LOG_LEVEL: debug volumes: - - ../lua/:/etc/ipax/lua/ - - ../conf/nginx.conf.template:/usr/local/openresty/nginx/conf/nginx.conf.template - - ../conf/demoapp.conf:/usr/local/openresty/nginx/conf/demoapp.conf - - ../conf/demoapp_default_variables.conf:/usr/local/openresty/nginx/conf/demoapp_default_variables.conf - - ../templates:/var/ipax/templates/ - - ./server.conf:/usr/local/openresty/nginx/conf/server.conf - - ./conf.d/:/etc/ipax/conf.d/ + - ../../conf/:/etc/ipax/conf/ + - ./conf/:/etc/ipax/conf/multi-host/ + - ../../html/:/var/ipax/html/ + - ../../lua/:/etc/ipax/lua/ + - ../../templates:/var/ipax/templates/ depends_on: idp: condition: service_healthy diff --git a/multiapps/conf.d/demoapp1.conf b/localhost/multi-host/conf/demoapp1.conf similarity index 77% rename from multiapps/conf.d/demoapp1.conf rename to localhost/multi-host/conf/demoapp1.conf index 4eac5e2..db8ff33 100644 --- a/multiapps/conf.d/demoapp1.conf +++ b/localhost/multi-host/conf/demoapp1.conf @@ -1,15 +1,16 @@ server { server_name demoapp1; - include demoapp_default_variables.conf; + include multi_default_variables.conf; set $demoapp_alias 'DemoApp1'; set $demoapp_base_url 'http://demoapp1'; + set $oidc_discovery 'http://idp:8080/realms/demorealm/.well-known/openid-configuration'; set $client_id 'demoapp1_client_id'; set $client_secret 'demoapp1_client_secret'; - set $scope 'openid profile email roles phone'; + set $kc_update_password_action 'UPDATE_PASSWORD'; set $kc_update_email_action 'UPDATE_EMAIL'; - include demoapp.conf; + include multi-host.conf; } diff --git a/multiapps/conf.d/demoapp2.conf b/localhost/multi-host/conf/demoapp2.conf similarity index 64% rename from multiapps/conf.d/demoapp2.conf rename to localhost/multi-host/conf/demoapp2.conf index 4c6094b..16f49c9 100644 --- a/multiapps/conf.d/demoapp2.conf +++ b/localhost/multi-host/conf/demoapp2.conf @@ -1,14 +1,16 @@ server { server_name demoapp2; - include demoapp_default_variables.conf; + include multi_default_variables.conf; set $demoapp_alias 'DemoApp2'; set $demoapp_base_url 'http://demoapp2'; + set $oidc_discovery 'http://idp:8080/realms/demorealm/.well-known/openid-configuration'; set $client_id 'demoapp2_client_id'; - set $client_secret 'demoapp2_client_secret'; - set $scope 'openid profile email roles phone'; + set $use_pkce 'true'; + set $scope 'openid profile email'; + set $kc_delete_account_action 'delete_account'; - include demoapp.conf; + include multi-host.conf; } diff --git a/multiapps/keycloak/demorealm.json b/localhost/multi-host/keycloak/demorealm.json similarity index 96% rename from multiapps/keycloak/demorealm.json rename to localhost/multi-host/keycloak/demorealm.json index 7cace92..f1e0997 100644 --- a/multiapps/keycloak/demorealm.json +++ b/localhost/multi-host/keycloak/demorealm.json @@ -19,7 +19,7 @@ "serviceAccountsEnabled": false, "frontchannelLogout": true, "rootUrl": "http://demoapp1", - "baseUrl": "http://demoapp1", + "baseUrl": "/", "redirectUris": [ "/private/redirect_uri", "/private/info" @@ -33,14 +33,13 @@ { "name": "demoapp2", "clientId": "demoapp2_client_id", - "secret": "demoapp2_client_secret", "standardFlowEnabled": true, "implicitFlowEnabled": false, "directAccessGrantsEnabled": false, "serviceAccountsEnabled": false, "frontchannelLogout": true, "rootUrl": "http://demoapp2", - "baseUrl": "http://demoapp2", + "baseUrl": "/", "redirectUris": [ "/private/redirect_uri", "/private/info" diff --git a/localhost/multi-path/compose.yaml b/localhost/multi-path/compose.yaml new file mode 100644 index 0000000..a3a5a3a --- /dev/null +++ b/localhost/multi-path/compose.yaml @@ -0,0 +1,39 @@ +services: + idp: + container_name: idp + image: ghcr.io/identicum/keycloak:26.2 + restart: always + pull_policy: always + ports: + - 8080:8080 + environment: + KC_BOOTSTRAP_ADMIN_USERNAME: admin + KC_BOOTSTRAP_ADMIN_PASSWORD: admin + KC_FEATURES: update-email + volumes: + - ./keycloak/:/opt/keycloak/data/import/ + command: "start-dev --import-realm" + demoapps: + container_name: demoapps + image: ghcr.io/identicum/ipax:latest + restart: always + ports: + - 80:80 + environment: + NGINX_RESOLVER: 127.0.0.11 valid=5s + NGINX_LOG_LEVEL: debug + volumes: + - ../../conf/:/etc/ipax/conf/ + - ./conf/:/etc/ipax/conf/multi-path/ + - ../../html/:/var/ipax/html/ + - ../../lua/:/etc/ipax/lua/ + - ../../templates:/var/ipax/templates/ + depends_on: + idp: + condition: service_healthy + mailcatcher: + container_name: mailcatcher + image: ghcr.io/identicum/mailcatcher:latest + restart: always + ports: + - 1080:1080 \ No newline at end of file diff --git a/localhost/multi-path/conf/demoapp1.conf b/localhost/multi-path/conf/demoapp1.conf new file mode 100644 index 0000000..44b9616 --- /dev/null +++ b/localhost/multi-path/conf/demoapp1.conf @@ -0,0 +1,15 @@ +location /demoapp1 { + include /etc/ipax/conf/multi-path_root.conf; +} +location /demoapp1/landing { + include /etc/ipax/conf/multi-path/demoapp1_variables.conf; + include /etc/ipax/conf/multi-path_landing.conf; +} +location /demoapp1/private/ { + include /etc/ipax/conf/multi-path/demoapp1_variables.conf; + include /etc/ipax/conf/multi-path_private.conf; +} +location /demoapp1/private/info { + include /etc/ipax/conf/multi-path/demoapp1_variables.conf; + include /etc/ipax/conf/info_multi.conf; +} diff --git a/localhost/multi-path/conf/demoapp1_variables.conf b/localhost/multi-path/conf/demoapp1_variables.conf new file mode 100644 index 0000000..87d98c2 --- /dev/null +++ b/localhost/multi-path/conf/demoapp1_variables.conf @@ -0,0 +1,11 @@ +include multi_default_variables.conf; + +set $demoapp_alias 'DemoApp1'; +set $demoapp_base_url 'http://localhost/demoapp1'; + +set $oidc_discovery 'http://idp:8080/realms/demorealm/.well-known/openid-configuration'; +set $client_id 'demoapp1_client_id'; +set $client_secret 'demoapp1_client_secret'; + +set $kc_update_password_action 'UPDATE_PASSWORD'; +set $kc_update_email_action 'UPDATE_EMAIL'; \ No newline at end of file diff --git a/localhost/multi-path/conf/demoapp2.conf b/localhost/multi-path/conf/demoapp2.conf new file mode 100644 index 0000000..e2deede --- /dev/null +++ b/localhost/multi-path/conf/demoapp2.conf @@ -0,0 +1,16 @@ +location /demoapp2/ { + include /etc/ipax/conf/multi-path_root.conf; +} +location /demoapp2/landing { + include /etc/ipax/conf/multi-path/demoapp2_variables.conf; + include /etc/ipax/conf/multi-path_landing.conf; +} +location /demoapp2/private/ { + include /etc/ipax/conf/multi-path/demoapp2_variables.conf; + include /etc/ipax/conf/multi-path_private.conf; +} +location /demoapp2/private/info { + include /etc/ipax/conf/multi-path/demoapp2_variables.conf; + include /etc/ipax/conf/info_multi.conf; +} + diff --git a/localhost/multi-path/conf/demoapp2_variables.conf b/localhost/multi-path/conf/demoapp2_variables.conf new file mode 100644 index 0000000..de60711 --- /dev/null +++ b/localhost/multi-path/conf/demoapp2_variables.conf @@ -0,0 +1,11 @@ +include multi_default_variables.conf; + +set $demoapp_alias 'DemoApp2'; +set $demoapp_base_url 'http://localhost/demoapp2'; + +set $oidc_discovery 'http://idp:8080/realms/demorealm/.well-known/openid-configuration'; +set $client_id 'demoapp2_client_id'; +set $use_pkce 'true'; +set $scope 'openid profile email'; + +set $kc_delete_account_action 'delete_account'; \ No newline at end of file diff --git a/localhost/multi-path/keycloak/demorealm.json b/localhost/multi-path/keycloak/demorealm.json new file mode 100644 index 0000000..110c923 --- /dev/null +++ b/localhost/multi-path/keycloak/demorealm.json @@ -0,0 +1,113 @@ +{ + "realm": "demorealm", + "enabled": true, + "registrationAllowed" : true, + "registrationEmailAsUsername" : false, + "verifyEmail" : true, + "editUsernameAllowed": true, + "loginWithEmailAllowed" : true, + "duplicateEmailsAllowed" : false, + "resetPasswordAllowed" : true, + "clients" : [ + { + "name": "demoapp1", + "clientId": "demoapp1_client_id", + "secret": "demoapp1_client_secret", + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "frontchannelLogout": true, + "rootUrl": "http://localhost/demoapp1", + "baseUrl": "/", + "redirectUris": [ + "/private/redirect_uri", + "/private/info" + ], + "defaultClientScopes": [ ], + "optionalClientScopes": [ "profile", "email", "roles", "phone" ], + "attributes": { + "post.logout.redirect.uris": "/logoutSuccess.html" + } + }, + { + "name": "demoapp2", + "clientId": "demoapp2_client_id", + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "frontchannelLogout": true, + "rootUrl": "http://localhost/demoapp2", + "baseUrl": "/", + "redirectUris": [ + "/private/redirect_uri", + "/private/info" + ], + "defaultClientScopes": [ ], + "optionalClientScopes": [ "profile", "email", "roles", "phone" ], + "attributes": { + "post.logout.redirect.uris": "/logoutSuccess.html" + } + } + ], + "users" : [ + { + "username": "demo", + "enabled" : true, + "createdTimestamp" : 1672531200000, + "email": "demo@identicum.com", + "emailVerified" : true, + "firstName": "Demo", + "lastName": "User", + "credentials" : [ {"type": "password", "value": "demo"} ], + "clientRoles": { + "account": ["delete-account", "view-profile", "manage-account"] + } + } + ], + "requiredActions": [ + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": true, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "UPDATE_EMAIL", + "name": "Update Email", + "providerId": "UPDATE_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 61, + "config": {} + } + ], + "smtpServer": { + "port": "1025", + "host": "mailcatcher", + "from": "no-reply@identicum.com", + "ssl": "false" + } +} diff --git a/compose.yaml b/localhost/single/compose.yaml similarity index 59% rename from compose.yaml rename to localhost/single/compose.yaml index 1c79550..826e843 100644 --- a/compose.yaml +++ b/localhost/single/compose.yaml @@ -7,8 +7,8 @@ services: ports: - 8080:8080 environment: - KEYCLOAK_ADMIN: admin - KEYCLOAK_ADMIN_PASSWORD: admin + KC_BOOTSTRAP_ADMIN_USERNAME: admin + KC_BOOTSTRAP_ADMIN_PASSWORD: admin KC_FEATURES: update-email volumes: - ./keycloak/:/opt/keycloak/data/import/ @@ -34,22 +34,10 @@ services: KC_UPDATE_EMAIL_ACTION: UPDATE_EMAIL KC_UPDATE_PASSWORD_ACTION: UPDATE_PASSWORD volumes: - - ./conf/health.conf:/usr/local/openresty/nginx/conf/health.conf - - ./conf/info.conf:/usr/local/openresty/nginx/conf/info.conf - - ./conf/lua.conf:/usr/local/openresty/nginx/conf/lua.conf - - ./conf/filter_cookie.conf:/usr/local/openresty/nginx/conf/filter_cookie.conf - - ./conf/nginx.conf.template:/usr/local/openresty/nginx/conf/nginx.conf.template - - ./conf/norobots.conf:/usr/local/openresty/nginx/conf/norobots.conf - - ./conf/server.conf:/usr/local/openresty/nginx/conf/server.conf - - ./html/:/var/ipax/html/ - - ./lua/:/etc/ipax/lua/ - - ./templates:/var/ipax/templates/ - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost/ipax/health"] - interval: 10s - timeout: 1s - retries: 5 - start_period: 5s + - ../../conf/:/etc/ipax/conf/ + - ../../html/:/var/ipax/html/ + - ../../lua/:/etc/ipax/lua/ + - ../../templates:/var/ipax/templates/ depends_on: idp: condition: service_healthy diff --git a/keycloak/demorealm.json b/localhost/single/keycloak/demorealm.json similarity index 98% rename from keycloak/demorealm.json rename to localhost/single/keycloak/demorealm.json index 0594159..1deb0b8 100644 --- a/keycloak/demorealm.json +++ b/localhost/single/keycloak/demorealm.json @@ -19,7 +19,7 @@ "serviceAccountsEnabled": false, "frontchannelLogout": true, "rootUrl": "http://localhost", - "baseUrl": "http://localhost", + "baseUrl": "/", "redirectUris": [ "/private/redirect_uri", "/private/info" diff --git a/lua/ipax.lua b/lua/ipax.lua index 20844dc..b33b838 100644 --- a/lua/ipax.lua +++ b/lua/ipax.lua @@ -1,355 +1,258 @@ --- -------------------------------------------------------------------------------------------------------------- --- -------------------------------------------------------------------------------------------------------------- --- https://github.com/zmartzone/lua-resty-openidc/blob/master/lib/resty/openidc.lua -local http = require("resty.http") -local function openidc_cache_get(type, key) - local dict = ngx.shared[type] - local value - if dict then - value = dict:get(key) - if value then ngx.log(ngx.DEBUG, "cache hit: type=", type, " key=", key) end - end - return value - end - local function openidc_configure_timeouts(httpc, timeout) - if timeout then - if type(timeout) == "table" then - local r, e = httpc:set_timeouts(timeout.connect or 0, timeout.send or 0, timeout.read or 0) - else - local r, e = httpc:set_timeout(timeout) - end - end - end - -- Set outgoing proxy options -local function openidc_configure_proxy(httpc, proxy_opts) - if httpc and proxy_opts and type(proxy_opts) == "table" then - ngx.log(ngx.DEBUG, "openidc_configure_proxy : use http proxy") - httpc:set_proxy_options(proxy_opts) - else - ngx.log(ngx.DEBUG, "openidc_configure_proxy : don't use http proxy") - end - end --- get the Discovery metadata from the specified URL -local function openidc_discover(url, ssl_verify, keepalive, timeout, exptime, proxy_opts, http_request_decorator) - ngx.log(ngx.DEBUG, "openidc_discover: URL is: " .. url) - local json, err - local v = openidc_cache_get("discovery", url) - if not v then - ngx.log(ngx.DEBUG, "discovery data not in cache, making call to discovery endpoint") - -- make the call to the discovery endpoint - local httpc = http.new() - openidc_configure_timeouts(httpc, timeout) - openidc_configure_proxy(httpc, proxy_opts) - local res, error = httpc:request_uri(url, decorate_request(http_request_decorator, { - ssl_verify = (ssl_verify ~= "no"), - keepalive = (keepalive ~= "no") - })) - if not res then - err = "accessing discovery url (" .. url .. ") failed: " .. error - ngx.log(ngx.DEBUG, err) - else - ngx.log(ngx.DEBUG, "response data: " .. res.body) - json, err = openidc_parse_json_response(res) - if json then - openidc_cache_set("discovery", url, cjson.encode(json), exptime or 24 * 60 * 60) - else - err = "could not decode JSON from Discovery data" .. (err and (": " .. err) or '') - ngx.log(ngx.DEBUG, err) - end - end +local _M = {} + +local function is_true(input) + if string.lower(input) == "true" then + return true else - json = cjson.decode(v) - end - return json, err + return false + end end --- turn a discovery url set in the opts dictionary into the discovered information -local function openidc_ensure_discovered_data(opts) - local err - if type(opts.discovery) == "string" then - local discovery - discovery, err = openidc_discover(opts.discovery, opts.ssl_verify, opts.keepalive, opts.timeout, opts.discovery_expires_in, opts.proxy_opts, opts.http_request_decorator) - if not err then - opts.discovery = discovery - end +local function get_authorization_params(acr_values) + local authorization_params_table = {} + if acr_values ~= '' then + authorization_params_table["acr_values"]=acr_values end - return err + return authorization_params_table end -local function get_first(table_or_string) - local res = table_or_string - if table_or_string and type(table_or_string) == 'table' then - res = table_or_string[1] +local function get_oidc_opts(discovery, ssl_verify, client_id, use_pkce, client_secret, scope, redirect_uri, logout_path, post_logout_redirect_uri, acr_values, prompt_override) + local oidc_opts = { + discovery = discovery, + ssl_verify = ssl_verify, + client_id = client_id, + use_pkce = is_true(use_pkce), + client_secret = client_secret, + scope = scope, + redirect_uri = redirect_uri, + logout_path = logout_path, + post_logout_redirect_uri = post_logout_redirect_uri, + authorization_params = get_authorization_params(acr_values), + renew_access_token_on_expiry = true, + session_contents = {id_token=true, enc_id_token=true, access_token=true, user=true} + } + if prompt_override ~= '' then + oidc_opts["prompt"]=prompt_override end - return res -end - -local function get_first_header(headers, header_name) - local header = headers[header_name] - return get_first(header) -end - -local function get_first_header_and_strip_whitespace(headers, header_name) - local header = get_first_header(headers, header_name) - return header and header:gsub('%s', '') + return oidc_opts end -local function get_forwarded_parameter(headers, param_name) - local forwarded = get_first_header(headers, 'Forwarded') - local params = {} - if forwarded then - local function parse_parameter(pv) - local name, value = pv:match("^%s*([^=]+)%s*=%s*(.-)%s*$") - if name and value then - if value:sub(1, 1) == '"' then - value = value:sub(2, -2) - end - params[name:lower()] = value - end - end - -- this assumes there is no quoted comma inside the header's value which should be fine as comma is not legal inside a node name, a URI scheme or a host name. The only thing that might bite us are extensions. - local first_part = forwarded - local first_comma = forwarded:find("%s*,%s*") - if first_comma then - first_part = forwarded:sub(1, first_comma - 1) - end - first_part:gsub("[^;]+", parse_parameter) - end - return params[param_name:gsub("^%s*(.-)%s*$", "%1"):lower()] +function _M.get_oidc_opts_single() + ngx.log(ngx.DEBUG, "Getting global oidc_opts (single configuration)") + local discovery = os.getenv("OIDC_DISCOVERY") + local ssl_verify = os.getenv("OIDC_SSL_VERIFY") + local client_id = os.getenv("OIDC_CLIENT_ID") + local use_pkce = os.getenv("OIDC_USE_PKCE") + local client_secret = os.getenv("OIDC_CLIENT_SECRET") + local scope = os.getenv("OIDC_SCOPE") + local redirect_uri = os.getenv("OIDC_REDIRECT_URI") + local logout_path = os.getenv("OIDC_LOGOUT_URI") + local post_logout_redirect_uri = os.getenv("OIDC_POST_LOGOUT_REDIRECT_URI") + local acr_values = os.getenv("OIDC_ACR_VALUES") + local prompt_override = os.getenv("OIDC_PROMPT") + local oidc_opts = get_oidc_opts(discovery, ssl_verify, client_id, use_pkce, client_secret, scope, redirect_uri, logout_path, post_logout_redirect_uri, acr_values, prompt_override) + return oidc_opts end -local function get_scheme(headers) - return get_forwarded_parameter(headers, 'proto') - or get_first_header_and_strip_whitespace(headers, 'X-Forwarded-Proto') - or ngx.var.scheme +function _M.get_oidc_opts_multi() + ngx.log(ngx.DEBUG, "Getting oidc_opts (multi configuration)") + local discovery = ngx.var.oidc_discovery + local ssl_verify = os.getenv("OIDC_SSL_VERIFY") + local client_id = ngx.var.client_id + local use_pkce = ngx.var.use_pkce or "false" + local client_secret = ngx.var.client_secret + local scope = ngx.var.scope or os.getenv("OIDC_SCOPE") + local redirect_uri = ngx.var.demoapp_base_url .. os.getenv("OIDC_REDIRECT_URI") + local logout_path = ngx.var.demoapp_base_url .. os.getenv("OIDC_LOGOUT_URI") + ngx.log(ngx.DEBUG, "logout_path: " .. logout_path) + local post_logout_redirect_uri = ngx.var.demoapp_base_url .. "/logoutSuccess.html" + local acr_values = ngx.var.acr_values or "" + local prompt_override = ngx.var.oidc_prompt or "" + local oidc_opts = get_oidc_opts(discovery, ssl_verify, client_id, use_pkce, client_secret, scope, redirect_uri, logout_path, post_logout_redirect_uri, acr_values, prompt_override) + return oidc_opts end -local function get_host_name_from_x_header(headers) - local header = get_first_header_and_strip_whitespace(headers, 'X-Forwarded-Host') - return header and header:gsub('^([^,]+),?.*$', '%1') -end - -local function get_host_name(headers) - return get_forwarded_parameter(headers, 'host') - or get_host_name_from_x_header(headers) - or ngx.var.http_host +local function get_session_opts(secret, cookie_samesite, idle_timeout) + local session_opts = { + secret = secret, + cookie_http_only = true, + cookie_secure = true, + cookie_samesite = cookie_samesite, + idling_timeout = tonumber(idle_timeout), + remember = true + } + return session_opts end --- -------------------------------------------------------------------------------------------------------------- --- -------------------------------------------------------------------------------------------------------------- --- IPAx module -local _M = {} - - -local function isTrue(input) - if string.lower(input) == "true" then - return true - else - return false - end +function _M.get_session_opts_single() + ngx.log(ngx.DEBUG, "Getting global session_opts (single configuration)") + local secret = os.getenv("SESSION_SECRET") + local cookie_samesite = os.getenv("SESSION_COOKIE_SAMESITE") + local idle_timeout = os.getenv("SESSION_IDLETIMEOUT") + return get_session_opts(secret, cookie_samesite, idle_timeout) end -local function getAuthorizationParams() - local authorizationParamsTable = {} - - local acr_values = os.getenv("OIDC_ACR_VALUES") - if acr_values ~= '' then - authorizationParamsTable["acr_values"]=acr_values - end - - return authorizationParamsTable +function _M.get_session_opts_multi() + ngx.log(ngx.DEBUG, "Getting session_opts (multi configuration)") + local secret = ngx.var.session_secret or os.getenv("SESSION_SECRET") + local cookie_samesite = ngx.var.session_cookie_samesite or os.getenv("SESSION_COOKIE_SAMESITE") + local idle_timeout = ngx.var.session_idle_timeout or os.getenv("SESSION_IDLETIMEOUT") + return get_session_opts(secret, cookie_samesite, idle_timeout) end -local oidc_opts = { - discovery = os.getenv("OIDC_DISCOVERY"), - ssl_verify = os.getenv("OIDC_SSL_VERIFY"), - client_id = os.getenv("OIDC_CLIENT_ID"), - use_pkce = isTrue(os.getenv("OIDC_USE_PKCE")), - client_secret = os.getenv("OIDC_CLIENT_SECRET"), - scope = os.getenv("OIDC_SCOPE"), - redirect_uri = os.getenv("OIDC_REDIRECT_URI"), - logout_path = os.getenv("OIDC_LOGOUT_URI"), - post_logout_redirect_uri = os.getenv("OIDC_POST_LOGOUT_REDIRECT_URI"), - authorization_params = getAuthorizationParams(), - renew_access_token_on_expiry = true, - session_contents = {id_token=true, enc_id_token=true, access_token=true, user=true} -} -local prompt_override = os.getenv("OIDC_PROMPT") -if prompt_override ~= '' then - oidc_opts["prompt"]=prompt_override +function _M.get_id_token(res) + ngx.log(ngx.DEBUG, "ipax.get_id_token()") + return res.id_token end -local session_opts = { - secret = os.getenv("SESSION_SECRET"), - cookie = { - persistent = os.getenv("SESSION_COOKIE_PERSISTENT"), - lifetime = os.getenv("SESSION_COOKIE_LIFETIME"), - samesite = os.getenv("SESSION_COOKIE_SAMESITE") - } -} - -local function split(input, separator) - if separator == nil then - separator = "%s" - end - local t={} - for str in string.gmatch(input, "([^" .. separator .. "]+)") do - table.insert(t, str) - end - return t +function _M.get_access_token(res) + ngx.log(ngx.DEBUG, "ipax.get_access_token()") + return res.access_token end -function _M.check_authentication(err) - if err then - -- ngx.log(ngx.DEBUG, "check_authentication() err: " .. err) - error = string.match(err, "error=(.*)&+") - if (error == 'login_required') then - return ngx.redirect("/loginRequired.html", ngx.HTTP_MOVED_TEMPORARILY) - else - return ngx.redirect("/authenticationError.html", ngx.HTTP_MOVED_TEMPORARILY) - end - end - return true +local function get_refresh_token(res) + ngx.log(ngx.DEBUG, "ipax.get_refresh_token()") + local refresh_token = res.refresh_token or nil + return refresh_token end function _M.get_user() + ngx.log(ngx.DEBUG, "ipax.get_user()") local res = _M.get_res() return res.user end function _M.get_userinfo_json() + ngx.log(ngx.DEBUG, "ipax.get_userinfo_json()") local res = _M.get_res() local json = require("json").encode(res.user) ngx.log(ngx.DEBUG, "userinfo_json: " .. json) return json end -function _M.get_preferred_name_from_userinfo() - ngx.log(ngx.DEBUG, "preferred_username: " .. (preferred_username or "nil")) +local function get_preferred_name_from_userinfo() + ngx.log(ngx.DEBUG, "ipax.get_preferred_name_from_userinfo()") local userinfo_json = _M.get_userinfo_json() local userinfo_table = require("json").decode(userinfo_json) - local preferred_username = userinfo_table.preferred_username - ngx.log(ngx.DEBUG, "preferred_username: " .. (preferred_username or "nil")) + local preferred_username = userinfo_table.preferred_username or "nil" + ngx.log(ngx.DEBUG, "returning preferred_username: " .. preferred_username) return preferred_username end -function _M.get_preferred_username_from_userinfo_or_idtoken() - local userinfo_preferred_username = _M.get_preferred_name_from_userinfo() - +local function get_preferred_username_from_userinfo_or_idtoken() + ngx.log(ngx.DEBUG, "ipax.get_preferred_username_from_userinfo_or_idtoken()") + local userinfo_preferred_username = get_preferred_name_from_userinfo() if userinfo_preferred_username == nil then local id_token = _M.get_id_token() return id_token.preferred_username else return userinfo_preferred_username end - -end - -function _M.get_id_token() - local res = _M.get_res() - return res.id_token -end - -function _M.get_access_token() - local res = _M.get_res() - return res.access_token -end - -function _M.get_refresh_token() - local res = _M.get_res() - local refresh_token = res.refresh_token or nil - return refresh_token -end - -function _M.get_res() - local res, err, target, session = require("resty.openidc").authenticate(oidc_opts, null, action, session_opts) - --ngx.log(ngx.DEBUG, "refresh_token: " .. session:get("refresh_token")) - res["refresh_token"] = session:get("refresh_token") - session:close() - local authentication_feedback = _M.check_authentication(err) - return res end -function _M.check_multivalued_user_claim(claim_values, check_item) - for index, value in pairs(claim_values) do - -- ToDo: compare case-insensitive - if value == check_item then - return true - end +function _M.get_res(oidc_opts, session_opts) + ngx.log(ngx.DEBUG, "ipax.get_res()") + if not oidc_opts then + oidc_opts = _M.get_oidc_opts_single() end - ngx.exit(ngx.HTTP_FORBIDDEN) - return false -end - -function _M.is_value_in_list(value_list, check_item) - for index, value in pairs(value_list) do - if value == check_item then - return true - end + if not session_opts then + session_opts = _M.get_session_opts_single() end - return false -end - -function _M.get_names_from_dns(object_dns) - local object_names={} - if object_dns == nil then - ngx.log(ngx.DEBUG, 'get_names_from_dns() object_dns is nil') - return object_names + for k, v in pairs(oidc_opts) do + ngx.log(ngx.DEBUG, "Using oidc_opts[" .. k .. "] = " .. tostring(v)) end - for index, value in pairs(object_dns) do - local object_rdn = split(value, ",")[1] - local object_name = split(object_rdn, "=")[2] - object_names[index] = object_name + for k, v in pairs(session_opts) do + ngx.log(ngx.DEBUG, "Using session_opts[" .. k .. "] = " .. tostring(v)) end - return object_names -end - -function _M.get_group_names(claim_values, separator) - if separator == nil then - separator = "|" - end - local group_names = _M.get_names_from_dns(claim_values) - return table.concat(group_names, separator) + local res, err, target, session = require("resty.openidc").authenticate(oidc_opts, nil, nil, session_opts) + --ngx.log(ngx.DEBUG, "refresh_token: " .. session:get("refresh_token")) + res["refresh_token"] = session:get("refresh_token") + session:close() + -- local authentication_feedback = _M.check_authentication(err) + return res end -local function get_kc_user_action_url(kc_action) - local headers = ngx.req.get_headers() - local redirect_uri = get_scheme(headers) .. "://" .. get_host_name(headers) .. "/private/info" +local function get_kc_user_action_url(base_url, client_id, kc_action, authorization_endpoint) + local redirect_uri = base_url .. "/private/info" local params = { - client_id = oidc_opts.client_id, + client_id = client_id, response_type = "code", scope = "openid", redirect_uri = redirect_uri, kc_action = kc_action } - openidc_ensure_discovered_data(oidc_opts) - return oidc_opts.discovery.authorization_endpoint .. "?" .. ngx.encode_args(params) + return authorization_endpoint .. "?" .. ngx.encode_args(params) +end + +local function get_discovery_document(oidc_opts) + ngx.log(ngx.DEBUG, "ipax.get_discovery_document()") + local http = require("resty.http") + local httpc = http.new() + local res, err = httpc:request_uri(oidc_opts.discovery, { method = "GET", oidc_opts.ssl_verify }) + if not res then + ngx.log(ngx.ERR, "failed to request discovery document: ", err) + return nil + end + if res.status ~= 200 then + ngx.log(ngx.ERR, "discovery document request failed with status: ", res.status) + return nil + end + return require("cjson").decode(res.body) end -function _M.get_user_actions() +local function get_user_actions(oidc_opts, base_url) local userActionsTable = {} + local discovery_document = get_discovery_document(oidc_opts) + local authorization_endpoint = discovery_document.authorization_endpoint + local client_id = oidc_opts.client_id local kc_delete_account_action = os.getenv("KC_DELETE_ACCOUNT_ACTION") if kc_delete_account_action ~= '' then - userActionsTable["kc_delete_account_action"]='' .. os.getenv("KC_DELETE_ACCOUNT_LABEL") .. '' + userActionsTable["kc_delete_account_action"]='' .. os.getenv("KC_DELETE_ACCOUNT_LABEL") .. '' end local kc_update_password_action = os.getenv("KC_UPDATE_PASSWORD_ACTION") if kc_update_password_action ~= '' then - userActionsTable["kc_update_password_action"]='' .. os.getenv("KC_UPDATE_PASSWORD_LABEL") .. '' + userActionsTable["kc_update_password_action"]='' .. os.getenv("KC_UPDATE_PASSWORD_LABEL") .. '' end local kc_update_email_action = os.getenv("KC_UPDATE_EMAIL_ACTION") if kc_update_email_action ~= '' then - userActionsTable["kc_update_email_action"]='' .. os.getenv("KC_UPDATE_EMAIL_LABEL") .. '' + userActionsTable["kc_update_email_action"]='' .. os.getenv("KC_UPDATE_EMAIL_LABEL") .. '' end local kc_enrol_biometrics_action = os.getenv("KC_ENROL_BIOMETRICS_ACTION") if kc_enrol_biometrics_action ~= '' then - userActionsTable["kc_enrol_biometrics_action"]='' .. os.getenv("KC_ENROL_BIOMETRICS_LABEL") .. '' + userActionsTable["kc_enrol_biometrics_action"]='' .. os.getenv("KC_ENROL_BIOMETRICS_LABEL") .. '' end return userActionsTable end +function _M.get_info_data(oidc_opts, session_opts, app_name, base_url, logout_uri, headers) + ngx.log(ngx.DEBUG, "ipax.get_info_data()") + local res = _M.get_res(oidc_opts, session_opts) + -- id_token is returned as a lua table + local id_token = _M.get_id_token(res); + -- access_token is returned as string + local access_token = _M.get_access_token(res); + -- ngx.log(ngx.DEBUG, "access_token: " .. access_token) + local refresh_token = get_refresh_token(res); + -- ngx.log(ngx.DEBUG, "refresh_token: " .. tostring(refresh_token)) + + local data = { + user = _M.get_user(res), + headers = headers, + access_token = access_token, + refresh_token = refresh_token or "Not Provided in token endpoint response", + id_token = id_token, + userinfo_json = _M.get_userinfo_json(res), + username = get_preferred_username_from_userinfo_or_idtoken(res) or "Not Informed", + user_actions = get_user_actions(oidc_opts, base_url), + logout_uri = logout_uri, + app_name = app_name + } + return data +end + return _M diff --git a/lua/ipax_openidc.lua b/lua/ipax_openidc.lua deleted file mode 100644 index 22ead0d..0000000 --- a/lua/ipax_openidc.lua +++ /dev/null @@ -1,170 +0,0 @@ --- -------------------------------------------------------------------------------------------------------------- --- -------------------------------------------------------------------------------------------------------------- --- https://github.com/zmartzone/lua-resty-openidc/blob/master/lib/resty/openidc.lua -local _M = {} -local http = require("resty.http") -local cjson = require("cjson") -local cjson_s = require("cjson.safe") - -local function openidc_cache_get(type, key) - local dict = ngx.shared[type] - local value - if dict then - value = dict:get(key) - if value then ngx.log(ngx.DEBUG, "cache hit: type=", type, " key=", key) end - end - return value -end - -local function openidc_configure_timeouts(httpc, timeout) - if timeout then - if type(timeout) == "table" then - local r, e = httpc:set_timeouts(timeout.connect or 0, timeout.send or 0, timeout.read or 0) - else - local r, e = httpc:set_timeout(timeout) - end - end -end - --- Set outgoing proxy options -local function openidc_configure_proxy(httpc, proxy_opts) - if httpc and proxy_opts and type(proxy_opts) == "table" then - ngx.log(ngx.DEBUG, "openidc_configure_proxy : use http proxy") - httpc:set_proxy_options(proxy_opts) - else - ngx.log(ngx.DEBUG, "openidc_configure_proxy : don't use http proxy") - end -end - -local function decorate_request(http_request_decorator, req) - return http_request_decorator and http_request_decorator(req) or req -end - --- parse the JSON result from a call to the OP -local function openidc_parse_json_response(response, ignore_body_on_success) - local ignore_body_on_success = ignore_body_on_success or false - local err - local res - -- check the response from the OP - if response.status ~= 200 then - err = "response indicates failure, status=" .. response.status .. ", body=" .. response.body - else - if ignore_body_on_success then - return nil, nil - end - -- decode the response and extract the JSON object - res = cjson_s.decode(response.body) - if not res then - err = "JSON decoding failed" - end - end - return res, err -end - --- get the Discovery metadata from the specified URL -local function openidc_discover(url, ssl_verify, keepalive, timeout, exptime, proxy_opts, http_request_decorator) - ngx.log(ngx.DEBUG, "openidc_discover: URL is: " .. url) - local json, err - local v = openidc_cache_get("discovery", url) - if not v then - ngx.log(ngx.DEBUG, "discovery data not in cache, making call to discovery endpoint") - -- make the call to the discovery endpoint - local httpc = http.new() - openidc_configure_timeouts(httpc, timeout) - openidc_configure_proxy(httpc, proxy_opts) - local res, error = httpc:request_uri(url, decorate_request(http_request_decorator, { - ssl_verify = (ssl_verify ~= "no"), - keepalive = (keepalive ~= "no") - })) - if not res then - err = "accessing discovery url (" .. url .. ") failed: " .. error - ngx.log(ngx.DEBUG, err) - else - ngx.log(ngx.DEBUG, "response data: " .. res.body) - json, err = openidc_parse_json_response(res) - if json then - openidc_cache_set("discovery", url, cjson.encode(json), exptime or 24 * 60 * 60) - else - err = "could not decode JSON from Discovery data" .. (err and (": " .. err) or '') - ngx.log(ngx.DEBUG, err) - end - end - else - json = cjson.decode(v) - end - return json, err -end - --- turn a discovery url set in the opts dictionary into the discovered information -function _M.openidc_ensure_discovered_data(opts) - local err - if type(opts.discovery) == "string" then - local discovery - discovery, err = openidc_discover(opts.discovery, opts.ssl_verify, opts.keepalive, opts.timeout, opts.discovery_expires_in, opts.proxy_opts, opts.http_request_decorator) - if not err then - opts.discovery = discovery - end - end - return err -end - -local function get_first(table_or_string) - local res = table_or_string - if table_or_string and type(table_or_string) == 'table' then - res = table_or_string[1] - end - return res -end - -local function get_first_header(headers, header_name) - local header = headers[header_name] - return get_first(header) -end - -local function get_first_header_and_strip_whitespace(headers, header_name) - local header = get_first_header(headers, header_name) - return header and header:gsub('%s', '') -end - -local function get_forwarded_parameter(headers, param_name) - local forwarded = get_first_header(headers, 'Forwarded') - local params = {} - if forwarded then - local function parse_parameter(pv) - local name, value = pv:match("^%s*([^=]+)%s*=%s*(.-)%s*$") - if name and value then - if value:sub(1, 1) == '"' then - value = value:sub(2, -2) - end - params[name:lower()] = value - end - end - -- this assumes there is no quoted comma inside the header's value which should be fine as comma is not legal inside a node name, a URI scheme or a host name. The only thing that might bite us are extensions. - local first_part = forwarded - local first_comma = forwarded:find("%s*,%s*") - if first_comma then - first_part = forwarded:sub(1, first_comma - 1) - end - first_part:gsub("[^;]+", parse_parameter) - end - return params[param_name:gsub("^%s*(.-)%s*$", "%1"):lower()] -end - -function _M.get_scheme(headers) - return get_forwarded_parameter(headers, 'proto') - or get_first_header_and_strip_whitespace(headers, 'X-Forwarded-Proto') - or ngx.var.scheme -end - -local function get_host_name_from_x_header(headers) - local header = get_first_header_and_strip_whitespace(headers, 'X-Forwarded-Host') - return header and header:gsub('^([^,]+),?.*$', '%1') -end - -function _M.get_host_name(headers) - return get_forwarded_parameter(headers, 'host') - or get_host_name_from_x_header(headers) - or ngx.var.http_host -end - -return _M diff --git a/lua/multiapps.lua b/lua/multiapps.lua deleted file mode 100644 index 048b60e..0000000 --- a/lua/multiapps.lua +++ /dev/null @@ -1,116 +0,0 @@ --- -------------------------------------------------------------------------------------------------------------- --- -------------------------------------------------------------------------------------------------------------- --- IPAx multiapps -local _M = {} -local ipax = require("ipax") -local ipax_openidc = require("ipax_openidc") - -local session_opts = { - secret = os.getenv("SESSION_SECRET"), - cookie = { - persistent = os.getenv("SESSION_COOKIE_PERSISTENT"), - lifetime = os.getenv("SESSION_COOKIE_LIFETIME"), - samesite = os.getenv("SESSION_COOKIE_SAMESITE") - } -} - -function _M.get_res(oidc_opts, base_url, prompt_override) - oidc_opts["renew_access_token_on_expiry"] = true - oidc_opts["session_contents"] = {id_token=true, enc_id_token=true, access_token=true, user=true} - oidc_opts["redirect_uri"] = base_url .. os.getenv("OIDC_REDIRECT_URI") - oidc_opts["logout_path"] = os.getenv("OIDC_LOGOUT_URI") - oidc_opts["post_logout_redirect_uri"] = base_url .. "/logoutSuccess.html" - ngx.log(ngx.DEBUG, "prompt_override: " .. prompt_override) - if prompt_override ~= '' then - oidc_opts["prompt"]=prompt_override - end - local res, err, target, session = require("resty.openidc").authenticate(oidc_opts, null, action, session_opts) - --ngx.log(ngx.DEBUG, "refresh_token: " .. session:get("refresh_token")) - res["refresh_token"] = session:get("refresh_token") - session:close() - local authentication_feedback = ipax.check_authentication(err) - return res -end - -function _M.get_userinfo_json(res) - local json = require("json").encode(res.user) - return json -end - -function _M.get_id_token(res) - return res.id_token -end - -function _M.get_access_token(res) - return res.access_token -end - -function _M.get_refresh_token(res) - return res.refresh_token -end - -function _M.get_userinfo_json(res) - local json = require("json").encode(res.user) - -- ngx.log(ngx.DEBUG, "userinfo_json: " .. json) - return json -end - -function _M.get_preferred_name_from_userinfo(res) - local userinfo_json = _M.get_userinfo_json(res) - local userinfo_table = require("json").decode(userinfo_json) - local preferred_username = userinfo_table.preferred_username - if preferred_username == nil then - return "(unknown)" - else - return preferred_username - end -end - -function _M.get_preferred_username_from_userinfo_or_idtoken(res) - local id_token = _M.get_id_token(res) - local preferred_username = id_token.preferred_username - - if preferred_username == nil then - return _M.get_preferred_name_from_userinfo(res) - else - return preferred_username - end -end - -local function get_kc_user_action_url(oidc_opts, kc_action) - local headers = ngx.req.get_headers() - local redirect_uri = ipax_openidc.get_scheme(headers) .. "://" .. ipax_openidc.get_host_name(headers) .. "/private/info" - local params = { - client_id = oidc_opts.client_id, - response_type = "code", - scope = "openid", - redirect_uri = redirect_uri, - kc_action = kc_action - } - ipax_openidc.openidc_ensure_discovered_data(oidc_opts) - return oidc_opts.discovery.authorization_endpoint .. "?" .. ngx.encode_args(params) -end - -function _M.get_user_actions(oidc_opts, kc_actions) - local userActionsTable = {} - - if kc_actions.delete_account ~= '' then - userActionsTable["kc_delete_account_action"]='' .. os.getenv("KC_DELETE_ACCOUNT_LABEL") .. '' - end - - if kc_actions.update_password ~= '' then - userActionsTable["kc_update_password_action"]='' .. os.getenv("KC_UPDATE_PASSWORD_LABEL") .. '' - end - - if kc_actions.update_email ~= '' then - userActionsTable["kc_update_email_action"]='' .. os.getenv("KC_UPDATE_EMAIL_LABEL") .. '' - end - - if kc_actions.enrol_biometrics ~= '' then - userActionsTable["kc_enrol_biometrics_action"]='' .. os.getenv("KC_ENROL_BIOMETRICS_LABEL") .. '' - end - - return userActionsTable -end - -return _M diff --git a/multiapps/server.conf b/multiapps/server.conf deleted file mode 100644 index 736b979..0000000 --- a/multiapps/server.conf +++ /dev/null @@ -1,9 +0,0 @@ -server { - listen 80; - server_name localhost; - location / { - root /var/ipax/html/; - add_header Cache-Control no-store; - add_header Pragma no-cache; - } -} diff --git a/templates/info.html b/templates/info.html index e5bb5f9..48fec79 100644 --- a/templates/info.html +++ b/templates/info.html @@ -91,7 +91,7 @@