From d772a7045e9824889765a8e8f864f981446f4586 Mon Sep 17 00:00:00 2001
From: EarthlingDavey <15802017+EarthlingDavey@users.noreply.github.com>
Date: Wed, 24 Jul 2024 11:03:09 +0100
Subject: [PATCH 1/9] WIP
---
config/application.php | 2 +-
deploy/config/local/nginx/server.conf | 33 +++++++++++++++++++++
public/app/mu-plugins/moj-auth/moj-auth.php | 33 ++++++++++++++++++++-
3 files changed, 66 insertions(+), 2 deletions(-)
diff --git a/config/application.php b/config/application.php
index e48301304..ae56f20dc 100644
--- a/config/application.php
+++ b/config/application.php
@@ -138,7 +138,7 @@
Config::define('COMPRESS_SCRIPTS', false);
// Enable the authentication mu-plugin.
-Config::define('MOJ_AUTH_ENABLED', false);
+Config::define('MOJ_AUTH_ENABLED', true);
/**
* Debugging Settings
diff --git a/deploy/config/local/nginx/server.conf b/deploy/config/local/nginx/server.conf
index 04f1d258a..3cfc14203 100644
--- a/deploy/config/local/nginx/server.conf
+++ b/deploy/config/local/nginx/server.conf
@@ -26,6 +26,20 @@ fastcgi_cache_use_stale updating error timeout invalid_header http_500;
fastcgi_cache_key "$request_method$host$request_uri:$cookie_dw_agency";
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
+# geo $geo {
+# default 0;
+
+# include ip-ranges.conf
+
+
+# read from include a .conf file
+
+# ??
+# load an env variable
+
+# # Maybe use init script to substitude env vars
+# }
+
server {
listen 8080 default_server; # For default requests.
server_name localhost;
@@ -72,6 +86,12 @@ server {
rewrite ^/wp-content/uploads/(.*)$ /app/uploads/$1 permanent;
}
+ location = /index.php?auth=subrequest {
+ # internal;
+ include /etc/nginx/php-fpm.conf;
+ fastcgi_pass fpm;
+ }
+
##
# CACHING
##
@@ -102,7 +122,20 @@ server {
fastcgi_cache_purge mojiCache "$request_method$host$1:*";
}
+ # if X-IP-Status set on request then deny all
+
+ # add_header X-IP-Status $geo;
+
+ # ??
+ # if IP is allowed AND no JWT from Azure {
+ # redirect to Azure
+ # }
+
location / {
+
+ auth_request /index.php?auth=subrequest;
+ auth_request_set $auth_status $upstream_status;
+
# First attempt to serve request as file, then
# as a directory, then pass the request to
# WordPress's front controller.
diff --git a/public/app/mu-plugins/moj-auth/moj-auth.php b/public/app/mu-plugins/moj-auth/moj-auth.php
index 1d3dcdf5c..aa19921b4 100644
--- a/public/app/mu-plugins/moj-auth/moj-auth.php
+++ b/public/app/mu-plugins/moj-auth/moj-auth.php
@@ -12,13 +12,14 @@
*/
namespace MOJ\Intranet;
+
use Roots\WPConfig\Config;
// Do not allow access outside WP
defined('ABSPATH') || exit;
// If the plugin isn't enabled, return early.
-if(Config::get('MOJ_AUTH_ENABLED') === false) {
+if (Config::get('MOJ_AUTH_ENABLED') === false) {
return;
}
@@ -78,6 +79,11 @@ public function handlePageRequest(string $required_role = 'reader'): void
{
$this->log('handlePageRequest()');
+ if (isset($_GET['auth']) && $_GET['auth'] !== 'subrequest'){
+ return;
+ }
+
+ // TODO - is \headers_sent() a good idea? Could the user exploit a bug?
// If headers are already sent or we're doing a cron job, return early.
if (\headers_sent() || defined('DOING_CRON')) {
return;
@@ -159,6 +165,30 @@ public function handlePageRequest(string $required_role = 'reader'): void
$this->oauthLogin();
}
+ public function handleAuthRequest(string $required_role = 'reader'): void
+ {
+ $this->log('handleAuthRequest()');
+
+ // var_dump($_GET['auth']);
+ // exit();
+
+ if (empty($_GET['auth']) || $_GET['auth'] !== 'subrequest') {
+ return;
+ }
+
+ // Get the JWT token from the request. Do this early so that we populate $this->sub if it's known.
+ $jwt = $this->getJwt();
+
+ // Get the roles from the JWT and check that they're sufficient.
+ $jwt_correct_role = $jwt && $jwt->roles ? in_array($required_role, $jwt->roles) : false;
+
+ $response_code = $jwt_correct_role ? 200 : 401;
+
+ $this->log('handleAuthRequest returning: ' . $response_code);
+
+ http_response_code($response_code) && exit();
+ }
+
/**
* Log a user out.
*
@@ -176,3 +206,4 @@ public function logout(): void
$auth = new Auth(['debug' => Config::get('MOJ_AUTH_DEBUG')]);
$auth->handlePageRequest('reader');
+$auth->handleAuthRequest('reader');
From 3d7962f9270f56465003950789ca69cb24698a3c Mon Sep 17 00:00:00 2001
From: EarthlingDavey <15802017+EarthlingDavey@users.noreply.github.com>
Date: Thu, 25 Jul 2024 11:20:34 +0100
Subject: [PATCH 2/9] non-authd users are not served cache
---
deploy/config/local/nginx/server.conf | 45 ++++++++++++++++++++++-----
1 file changed, 38 insertions(+), 7 deletions(-)
diff --git a/deploy/config/local/nginx/server.conf b/deploy/config/local/nginx/server.conf
index 3cfc14203..bc0246b78 100644
--- a/deploy/config/local/nginx/server.conf
+++ b/deploy/config/local/nginx/server.conf
@@ -86,11 +86,9 @@ server {
rewrite ^/wp-content/uploads/(.*)$ /app/uploads/$1 permanent;
}
- location = /index.php?auth=subrequest {
- # internal;
- include /etc/nginx/php-fpm.conf;
- fastcgi_pass fpm;
- }
+ # Every request must go through...
+ auth_request /auth/verify;
+ auth_request_set $auth_status $upstream_status;
##
# CACHING
@@ -113,6 +111,11 @@ server {
set $skip_cache 1;
}
+ # Don't cache any auth paths.
+ if ($request_uri ~* "/auth/") {
+ set $skip_cache 1;
+ }
+
# ...it's from a logged in user, the cookie 'wordpress_no_cache' exists, or we're mid moj-auth flow.
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in|OAUTH_.*") {
set $skip_cache 1;
@@ -122,6 +125,11 @@ server {
fastcgi_cache_purge mojiCache "$request_method$host$1:*";
}
+ # Skip cache if if the user is not auth'd
+ if ($auth_status != "200") {
+ set $skip_cache 1;
+ }
+
# if X-IP-Status set on request then deny all
# add_header X-IP-Status $geo;
@@ -131,10 +139,13 @@ server {
# redirect to Azure
# }
+
+
+
location / {
- auth_request /index.php?auth=subrequest;
- auth_request_set $auth_status $upstream_status;
+ # auth_request_set variables are not accessible inside location blocks
+ # satisfy all;
# First attempt to serve request as file, then
# as a directory, then pass the request to
@@ -142,6 +153,26 @@ server {
try_files $uri $uri/ /index.php?$args;
}
+ # @see https://gock.net/blog/2020/nginx-subrequest-authentication-server
+ location = /auth/verify {
+
+ # internaly only, /auth can not be accessed from outside
+ # internal;
+
+ fastcgi_param SCRIPT_FILENAME $document_root/app/mu-plugins/moj-auth/standalone.php;
+ include fastcgi_params;
+ # override SCRIPT_NAME which was set in fastcgi_params
+ fastcgi_param SCRIPT_NAME /app/mu-plugins/moj-auth/standalone.php;
+ fastcgi_pass fpm;
+ }
+
+
+
+ # Rewrite auth endpoints to fpm (WordPress's index.php)
+ location = /auth/(?:login|callback|logout) {
+ rewrite "/auth/$1" /index.php?$args;
+ }
+
# deny access to dotfiles accept .well-known
# this will deny access to .git, .htaccess, .env, and other sensitive files
location ~ /\.(?!well-known).* {
From 5ef58f99534201663a47f314c8e4dae5ef8d289b Mon Sep 17 00:00:00 2001
From: EarthlingDavey <15802017+EarthlingDavey@users.noreply.github.com>
Date: Thu, 25 Jul 2024 12:13:54 +0100
Subject: [PATCH 3/9] auth subrequest working and auth endpoints always allowed
---
deploy/config/local/nginx/server.conf | 34 ++++++-----
public/app/mu-plugins/moj-auth/jwt.php | 4 +-
public/app/mu-plugins/moj-auth/standalone.php | 58 +++++++++++++++++++
public/app/mu-plugins/moj-auth/utils.php | 4 +-
4 files changed, 83 insertions(+), 17 deletions(-)
create mode 100644 public/app/mu-plugins/moj-auth/standalone.php
diff --git a/deploy/config/local/nginx/server.conf b/deploy/config/local/nginx/server.conf
index bc0246b78..d15d679a6 100644
--- a/deploy/config/local/nginx/server.conf
+++ b/deploy/config/local/nginx/server.conf
@@ -86,9 +86,7 @@ server {
rewrite ^/wp-content/uploads/(.*)$ /app/uploads/$1 permanent;
}
- # Every request must go through...
- auth_request /auth/verify;
- auth_request_set $auth_status $upstream_status;
+
##
# CACHING
@@ -125,6 +123,10 @@ server {
fastcgi_cache_purge mojiCache "$request_method$host$1:*";
}
+ location = /purge-all-cache {
+ fastcgi_cache_purge mojiCache "$request_method$host*";
+ }
+
# Skip cache if if the user is not auth'd
if ($auth_status != "200") {
set $skip_cache 1;
@@ -139,11 +141,12 @@ server {
# redirect to Azure
# }
-
-
-
location / {
+ # Every request must go through...
+ auth_request /auth/verify;
+ auth_request_set $auth_status $upstream_status;
+
# auth_request_set variables are not accessible inside location blocks
# satisfy all;
@@ -155,21 +158,26 @@ server {
# @see https://gock.net/blog/2020/nginx-subrequest-authentication-server
location = /auth/verify {
+ # Internal only, so /auth/verify can not be accessed from outside.
+ internal;
+
+ # This is needed to be set explicitly, as this endpoint is hit as a subrequest.
+ fastcgi_no_cache 1;
- # internaly only, /auth can not be accessed from outside
- # internal;
+ # The subrequest handler, WordPress is not loaded in this file.
+ set $script_name /app/mu-plugins/moj-auth/standalone.php;
- fastcgi_param SCRIPT_FILENAME $document_root/app/mu-plugins/moj-auth/standalone.php;
+ # Prepare and pass the request to fpm.
+ fastcgi_param SCRIPT_FILENAME $document_root$script_name;
include fastcgi_params;
# override SCRIPT_NAME which was set in fastcgi_params
- fastcgi_param SCRIPT_NAME /app/mu-plugins/moj-auth/standalone.php;
+ fastcgi_param SCRIPT_NAME $script_name;
fastcgi_pass fpm;
}
-
-
# Rewrite auth endpoints to fpm (WordPress's index.php)
- location = /auth/(?:login|callback|logout) {
+ location ~* ^/auth/(?:login|callback|logout)$ {
+ auth_request off;
rewrite "/auth/$1" /index.php?$args;
}
diff --git a/public/app/mu-plugins/moj-auth/jwt.php b/public/app/mu-plugins/moj-auth/jwt.php
index a4b28b127..9c844d80b 100644
--- a/public/app/mu-plugins/moj-auth/jwt.php
+++ b/public/app/mu-plugins/moj-auth/jwt.php
@@ -2,8 +2,8 @@
namespace MOJ\Intranet;
-// Do not allow access outside WP
-defined('ABSPATH') || exit;
+// Do not allow access outside WP or standalone.php
+defined('ABSPATH') || defined('DOING_STANDALONE_AUTH') || exit;
/**
* JWT functions for MOJ\Intranet\Auth.
diff --git a/public/app/mu-plugins/moj-auth/standalone.php b/public/app/mu-plugins/moj-auth/standalone.php
new file mode 100644
index 000000000..079b9d411
--- /dev/null
+++ b/public/app/mu-plugins/moj-auth/standalone.php
@@ -0,0 +1,58 @@
+debug = $args['debug'] ?? false;
+
+ $this->initJwt();
+ }
+
+ public function handleAuthRequest(string $required_role = 'reader'): void
+ {
+ $this->log('handleAuthRequest()');
+
+ // Get the JWT token from the request. Do this early so that we populate $this->sub if it's known.
+ $jwt = $this->getJwt();
+
+ // Get the roles from the JWT and check that they're sufficient.
+ $jwt_correct_role = $jwt && $jwt->roles ? in_array($required_role, $jwt->roles) : false;
+
+ $status_code = $jwt_correct_role ? 200 : 401;
+ // $status_code = 401;
+ $status_code = 200;
+
+ http_response_code($status_code) && exit();
+ }
+}
+
+$standalone_auth = new StandaloneAuth(['debug' => true]);
+$standalone_auth->handleAuthRequest();
diff --git a/public/app/mu-plugins/moj-auth/utils.php b/public/app/mu-plugins/moj-auth/utils.php
index 90f2e5670..c5b6dd88d 100644
--- a/public/app/mu-plugins/moj-auth/utils.php
+++ b/public/app/mu-plugins/moj-auth/utils.php
@@ -2,8 +2,8 @@
namespace MOJ\Intranet;
-// Do not allow access outside WP
-defined('ABSPATH') || exit;
+// Do not allow access outside WP or standalone.php
+defined('ABSPATH') || defined('DOING_STANDALONE_AUTH') || exit;
/**
* Util functions for MOJ\Intranet\Auth.
From ff3c68067694e20483dfd257506be256520f0763 Mon Sep 17 00:00:00 2001
From: EarthlingDavey <15802017+EarthlingDavey@users.noreply.github.com>
Date: Thu, 25 Jul 2024 13:58:35 +0100
Subject: [PATCH 4/9] Geo IP working with hardcoded IP ranges
---
deploy/config/local/nginx/server.conf | 65 +++++++++----------
public/app/mu-plugins/moj-auth/standalone.php | 2 -
2 files changed, 31 insertions(+), 36 deletions(-)
diff --git a/deploy/config/local/nginx/server.conf b/deploy/config/local/nginx/server.conf
index d15d679a6..3b4b7fd51 100644
--- a/deploy/config/local/nginx/server.conf
+++ b/deploy/config/local/nginx/server.conf
@@ -26,19 +26,19 @@ fastcgi_cache_use_stale updating error timeout invalid_header http_500;
fastcgi_cache_key "$request_method$host$request_uri:$cookie_dw_agency";
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
-# geo $geo {
-# default 0;
-# include ip-ranges.conf
-
-
-# read from include a .conf file
-
-# ??
-# load an env variable
+geo $geo {
+ # Trusted IPs where 'X-Forwarded-For' is used.
+ proxy 172.17.0.0/16;
+ proxy 172.25.0.0/16;
+
+ # TODO
+ # read from include a .conf file
+ # Maybe use init script to substitude env vars
+ default 0;
-# # Maybe use init script to substitude env vars
-# }
+ 192.168.65.1 1;
+}
server {
listen 8080 default_server; # For default requests.
@@ -119,6 +119,11 @@ server {
set $skip_cache 1;
}
+ # Skip cache if the concatenated value doesn't contain 200 - the user is not allowed.
+ if ($auth_status !~ 200) {
+ set $skip_cache 1;
+ }
+
location ~ /purge-cache(/.*) {
fastcgi_cache_purge mojiCache "$request_method$host$1:*";
}
@@ -127,28 +132,12 @@ server {
fastcgi_cache_purge mojiCache "$request_method$host*";
}
- # Skip cache if if the user is not auth'd
- if ($auth_status != "200") {
- set $skip_cache 1;
- }
-
- # if X-IP-Status set on request then deny all
-
- # add_header X-IP-Status $geo;
-
- # ??
- # if IP is allowed AND no JWT from Azure {
- # redirect to Azure
- # }
-
location / {
- # Every request must go through...
+ # Every request must go through this subrequest.
auth_request /auth/verify;
- auth_request_set $auth_status $upstream_status;
-
- # auth_request_set variables are not accessible inside location blocks
- # satisfy all;
+ # Concatenate the status returned by $geo and fpm query.
+ auth_request_set $auth_status $status$upstream_status;
# First attempt to serve request as file, then
# as a directory, then pass the request to
@@ -159,10 +148,13 @@ server {
# @see https://gock.net/blog/2020/nginx-subrequest-authentication-server
location = /auth/verify {
# Internal only, so /auth/verify can not be accessed from outside.
- internal;
+ # internal;
- # This is needed to be set explicitly, as this endpoint is hit as a subrequest.
- fastcgi_no_cache 1;
+ if ( $geo != 0 ) {
+ add_header X-Geo $geo;
+ add_header Content-Type text/plain;
+ return 200;
+ }
# The subrequest handler, WordPress is not loaded in this file.
set $script_name /app/mu-plugins/moj-auth/standalone.php;
@@ -173,10 +165,13 @@ server {
# override SCRIPT_NAME which was set in fastcgi_params
fastcgi_param SCRIPT_NAME $script_name;
fastcgi_pass fpm;
+
+ # This is needed to be set explicitly, as this endpoint is hit as a subrequest.
+ fastcgi_no_cache 1;
}
# Rewrite auth endpoints to fpm (WordPress's index.php)
- location ~* ^/auth/(?:login|callback|logout)$ {
+ location ~ ^/auth/(?:login|callback|logout)$ {
auth_request off;
rewrite "/auth/$1" /index.php?$args;
}
@@ -209,6 +204,8 @@ server {
}
location ~ \.php$ {
+ # Send the IP status along, to use in in the application.
+ fastcgi_param HTTP_X_IP_GROUP $geo;
include /etc/nginx/php-fpm.conf;
fastcgi_pass fpm;
}
diff --git a/public/app/mu-plugins/moj-auth/standalone.php b/public/app/mu-plugins/moj-auth/standalone.php
index 079b9d411..f4ad95860 100644
--- a/public/app/mu-plugins/moj-auth/standalone.php
+++ b/public/app/mu-plugins/moj-auth/standalone.php
@@ -47,8 +47,6 @@ public function handleAuthRequest(string $required_role = 'reader'): void
$jwt_correct_role = $jwt && $jwt->roles ? in_array($required_role, $jwt->roles) : false;
$status_code = $jwt_correct_role ? 200 : 401;
- // $status_code = 401;
- $status_code = 200;
http_response_code($status_code) && exit();
}
From 4d245765d9a8f5098be09aded374481c888d358e Mon Sep 17 00:00:00 2001
From: EarthlingDavey <15802017+EarthlingDavey@users.noreply.github.com>
Date: Fri, 26 Jul 2024 12:58:25 +0100
Subject: [PATCH 5/9] Move traits to folder, add dynamic 401 page
---
deploy/config/local/nginx/php-fpm-auth.conf | 12 ++
deploy/config/local/nginx/server.conf | 33 +++---
public/app/mu-plugins/moj-auth/401.php | 98 +++++++++++++++
public/app/mu-plugins/moj-auth/moj-auth.php | 112 +++++++++++-------
.../moj-auth/templates/401-redirect.php | 32 +++++
.../mu-plugins/moj-auth/{ => traits}/jwt.php | 35 ++++--
.../moj-auth/{ => traits}/oauth.php | 16 +--
.../moj-auth/{ => traits}/utils.php | 8 +-
.../moj-auth/{standalone.php => verify.php} | 17 ++-
9 files changed, 282 insertions(+), 81 deletions(-)
create mode 100644 deploy/config/local/nginx/php-fpm-auth.conf
create mode 100644 public/app/mu-plugins/moj-auth/401.php
create mode 100644 public/app/mu-plugins/moj-auth/templates/401-redirect.php
rename public/app/mu-plugins/moj-auth/{ => traits}/jwt.php (65%)
rename public/app/mu-plugins/moj-auth/{ => traits}/oauth.php (93%)
rename public/app/mu-plugins/moj-auth/{ => traits}/utils.php (94%)
rename public/app/mu-plugins/moj-auth/{standalone.php => verify.php} (73%)
diff --git a/deploy/config/local/nginx/php-fpm-auth.conf b/deploy/config/local/nginx/php-fpm-auth.conf
new file mode 100644
index 000000000..2389f0d4c
--- /dev/null
+++ b/deploy/config/local/nginx/php-fpm-auth.conf
@@ -0,0 +1,12 @@
+# Config file to pass requests to specific scripts in the fpm container.
+# Used for *some* auth requests where we don't want to load WordPress.
+
+# Prepare and pass the request to fpm.
+fastcgi_param SCRIPT_FILENAME $document_root$script_name;
+include fastcgi_params;
+# override SCRIPT_NAME which was set in fastcgi_params
+fastcgi_param SCRIPT_NAME $script_name;
+fastcgi_pass fpm;
+
+# This is needed to be set explicitly, as this endpoint is hit as a subrequest.
+fastcgi_no_cache 1;
diff --git a/deploy/config/local/nginx/server.conf b/deploy/config/local/nginx/server.conf
index 3b4b7fd51..a9f8e19f9 100644
--- a/deploy/config/local/nginx/server.conf
+++ b/deploy/config/local/nginx/server.conf
@@ -62,7 +62,7 @@ server {
# CUSTOM ERROR PAGES
###
error_page 400 /app/themes/clarity/error-pages/400.html;
- error_page 401 /app/themes/clarity/error-pages/401.html;
+ error_page 401 /auth/401; # Use a dynamic 401 page, to conditionally rediect to login.
error_page 403 /app/themes/clarity/error-pages/403.html;
error_page 404 /app/themes/clarity/error-pages/404.html;
error_page 500 /app/themes/clarity/error-pages/500.html;
@@ -86,7 +86,10 @@ server {
rewrite ^/wp-content/uploads/(.*)$ /app/uploads/$1 permanent;
}
-
+ # We use the header X-Ip-Group internally, if the client sends this header return Bad Request.
+ if ($http_x_ip_group) {
+ return 400;
+ }
##
# CACHING
@@ -133,7 +136,6 @@ server {
}
location / {
-
# Every request must go through this subrequest.
auth_request /auth/verify;
# Concatenate the status returned by $geo and fpm query.
@@ -148,7 +150,7 @@ server {
# @see https://gock.net/blog/2020/nginx-subrequest-authentication-server
location = /auth/verify {
# Internal only, so /auth/verify can not be accessed from outside.
- # internal;
+ internal;
if ( $geo != 0 ) {
add_header X-Geo $geo;
@@ -157,23 +159,24 @@ server {
}
# The subrequest handler, WordPress is not loaded in this file.
- set $script_name /app/mu-plugins/moj-auth/standalone.php;
+ set $script_name /app/mu-plugins/moj-auth/verify.php;
+ include /etc/nginx/php-fpm-auth.conf;
+ }
- # Prepare and pass the request to fpm.
- fastcgi_param SCRIPT_FILENAME $document_root$script_name;
- include fastcgi_params;
- # override SCRIPT_NAME which was set in fastcgi_params
- fastcgi_param SCRIPT_NAME $script_name;
- fastcgi_pass fpm;
+ location ~ ^/auth/(401) {
+ # Internal only, so /auth/verify can not be accessed from outside.
+ internal;
- # This is needed to be set explicitly, as this endpoint is hit as a subrequest.
- fastcgi_no_cache 1;
+ # The 401 handler, WordPress is not loaded in this file.
+ set $script_name /app/mu-plugins/moj-auth/$1.php;
+ include /etc/nginx/php-fpm-auth.conf;
}
# Rewrite auth endpoints to fpm (WordPress's index.php)
- location ~ ^/auth/(?:login|callback|logout)$ {
+ location ~ ^/auth/(login|callback|logout) {
auth_request off;
- rewrite "/auth/$1" /index.php?$args;
+ fastcgi_param HTTP_X_IP_GROUP $geo;
+ rewrite /auth/* /index.php?$args;
}
# deny access to dotfiles accept .well-known
diff --git a/public/app/mu-plugins/moj-auth/401.php b/public/app/mu-plugins/moj-auth/401.php
new file mode 100644
index 000000000..f3b8b885a
--- /dev/null
+++ b/public/app/mu-plugins/moj-auth/401.php
@@ -0,0 +1,98 @@
+now = time();
+ $this->debug = $args['debug'] ?? false;
+ $this->https = isset($_SERVER['HTTPS']) || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO']);
+ $this->success_uri = $_SERVER['REQUEST_URI'];
+
+ if (!file_exists($this::STATIC_401)) {
+ error_log('moj-auth/401.php 401.html was not found.');
+ http_response_code(401) && exit();
+ }
+
+ if (!file_exists($this::REDIRECT_TEMPLATE)) {
+ error_log('moj-auth/401.php template was not found.');
+ http_response_code(401) && exit();
+ }
+
+ $this->initJwt();
+ }
+
+ public function handle401Request(): void
+ {
+ $this->log('handle401Request()');
+
+ // Get the JWT token from the request. Do this early so that we populate $this->sub if it's known.
+ $jwt = $this->getJwt();
+
+ // Set loginAttempts with a default of 1, or add one to the existing value.
+ $this->login_attempts = empty($jwt->login_attempts) ? 1 : ((int) $jwt->login_attempts) + 1;
+
+ // Set a JWT without a role, to persist the user's ID, login attempts and success_uri.
+ $jwt = $this->setJwt();
+
+ // Is this the first few times a visitor has hit the 401 page?
+ if ($jwt->login_attempts <= $this::MAX_AUTO_LOGIN_ATTEMPTS) {
+
+ // This template will redirect them to login.
+ require_once $this::REDIRECT_TEMPLATE;
+
+ // Return early.
+ return;
+ }
+
+ // The user has hit a 401 too many times, we won't redirect.
+ require_once $this::STATIC_401;
+ }
+}
+
+$standalone_401 = new Standalone401(['debug' => true]);
+$standalone_401->handle401Request();
+exit();
diff --git a/public/app/mu-plugins/moj-auth/moj-auth.php b/public/app/mu-plugins/moj-auth/moj-auth.php
index aa19921b4..28997e5e0 100644
--- a/public/app/mu-plugins/moj-auth/moj-auth.php
+++ b/public/app/mu-plugins/moj-auth/moj-auth.php
@@ -18,14 +18,16 @@
// Do not allow access outside WP
defined('ABSPATH') || exit;
+
+
// If the plugin isn't enabled, return early.
if (Config::get('MOJ_AUTH_ENABLED') === false) {
return;
}
-require_once 'jwt.php';
-require_once 'oauth.php';
-require_once 'utils.php';
+require_once 'traits/jwt.php';
+require_once 'traits/oauth.php';
+require_once 'traits/utils.php';
/**
* Class Auth
@@ -43,10 +45,12 @@ class Auth
use AuthOauth;
use AuthUtils;
- private $now = null;
- private $debug = false;
- private $https = false;
- private $sub = '';
+ private $now = null;
+ private $debug = false;
+ private $https = false;
+ private $sub = '';
+ private $login_attempts = null;
+ private $success_uri = null;
/**
* Constructor
@@ -65,6 +69,45 @@ public function __construct(array $args = [])
$this->initOauth();
}
+ public function handleLoginRequest(): void
+ {
+ // Get the JWT token from the request. Do this early so that we populate $this->sub if it's known.
+ $jwt = $this->getJwt();
+
+ // Set a JWT without a role, to persist the user's ID.
+ if (!$jwt) {
+ $jwt = $this->setJwt();
+ }
+
+ if ($this->oauth_action === 'login') {
+ // Handle Azure AD/Entra ID OAuth. It redirects to Azure or exits with 401 if disabled. php code execution always stops here.
+ $this->oauthLogin();
+ }
+ }
+
+ public function handleCallbackRequest(): void
+ {
+ $this->log('handleCallbackRequest()');
+
+ // If we've hit the callback endpoint, then handle it here. On fail it exits with 401 & php code execution stops here.
+ $oauth_access_token = 'callback' === $this->oauth_action ? $this->oauthCallback() : null;
+
+ // The callback has returned an access token.
+ if (is_object($oauth_access_token) && !$oauth_access_token->hasExpired()) {
+ $this->log('Access token is valid. Will set JWT and store refresh token.');
+ // Set a JWT cookie.
+ $this->setJwt((object)[
+ 'expiry' => $oauth_access_token->getExpires(),
+ 'roles' => ['reader'],
+ ]);
+ // Store the tokens.
+ $this->storeTokens($this->sub, $oauth_access_token, 'refresh');
+ // Redirect the user to the page they were trying to access.
+ header('Location: ' . \home_url($this->success_uri ?? '/'));
+ exit();
+ }
+ }
+
/**
* Handle the page request
*
@@ -79,10 +122,6 @@ public function handlePageRequest(string $required_role = 'reader'): void
{
$this->log('handlePageRequest()');
- if (isset($_GET['auth']) && $_GET['auth'] !== 'subrequest'){
- return;
- }
-
// TODO - is \headers_sent() a good idea? Could the user exploit a bug?
// If headers are already sent or we're doing a cron job, return early.
if (\headers_sent() || defined('DOING_CRON')) {
@@ -97,6 +136,13 @@ public function handlePageRequest(string $required_role = 'reader'): void
$jwt = $this->setJwt();
}
+ $this->log('$this->oauth_action: ' . $this->oauth_action);
+
+ if ('login' === $this->oauth_action) {
+ // Handle Azure AD/Entra ID OAuth. It redirects to Azure or exits with 401 if disabled. php code execution always stops here.
+ $this->oauthLogin();
+ }
+
// If we've hit the callback endpoint, then handle it here. On fail it exits with 401 & php code execution stops here.
$oauth_access_token = 'callback' === $this->oauth_action ? $this->oauthCallback() : null;
@@ -104,7 +150,7 @@ public function handlePageRequest(string $required_role = 'reader'): void
if (is_object($oauth_access_token) && !$oauth_access_token->hasExpired()) {
$this->log('Access token is valid. Will set JWT and store refresh token.');
// Set a JWT cookie.
- $this->setJwt([
+ $this->setJwt((object)[
'expiry' => $oauth_access_token->getExpires(),
'roles' => ['reader']
]);
@@ -136,7 +182,7 @@ public function handlePageRequest(string $required_role = 'reader'): void
// If the IP address is allowed, set a JWT and return.
if ($this->ipAddressIsAllowed()) {
- $this->setJwt(['roles' => ['reader']]);
+ $this->setJwt((object)['roles' => ['reader']]);
return;
}
@@ -147,7 +193,7 @@ public function handlePageRequest(string $required_role = 'reader'): void
if (is_object($oauth_refreshed_access_token) && !$oauth_refreshed_access_token->hasExpired()) {
$this->log('Refreshed access token is valid. Will set JWT and store refresh token.');
// Set a JWT cookie.
- $jwt = $this->setJwt([
+ $jwt = $this->setJwt((object)[
'expiry' => $oauth_refreshed_access_token->getExpires(),
'roles' => ['reader']
]);
@@ -162,31 +208,7 @@ public function handlePageRequest(string $required_role = 'reader'): void
}
// Handle Azure AD/Entra ID OAuth. It redirects to Azure or exits with 401 if disabled. php code execution always stops here.
- $this->oauthLogin();
- }
-
- public function handleAuthRequest(string $required_role = 'reader'): void
- {
- $this->log('handleAuthRequest()');
-
- // var_dump($_GET['auth']);
- // exit();
-
- if (empty($_GET['auth']) || $_GET['auth'] !== 'subrequest') {
- return;
- }
-
- // Get the JWT token from the request. Do this early so that we populate $this->sub if it's known.
- $jwt = $this->getJwt();
-
- // Get the roles from the JWT and check that they're sufficient.
- $jwt_correct_role = $jwt && $jwt->roles ? in_array($required_role, $jwt->roles) : false;
-
- $response_code = $jwt_correct_role ? 200 : 401;
-
- $this->log('handleAuthRequest returning: ' . $response_code);
-
- http_response_code($response_code) && exit();
+ // $this->oauthLogin();
}
/**
@@ -204,6 +226,12 @@ public function logout(): void
}
}
-$auth = new Auth(['debug' => Config::get('MOJ_AUTH_DEBUG')]);
-$auth->handlePageRequest('reader');
-$auth->handleAuthRequest('reader');
+
+$auth = new Auth(['debug' => true]); // Config::get('MOJ_AUTH_DEBUG')]);
+$auth->handleLoginRequest();
+$auth->handleCallbackRequest();
+
+// Handle a normal page view
+// - refresh token if necessary
+// - clean up the success_uri
+// - clean up the login_attempts
diff --git a/public/app/mu-plugins/moj-auth/templates/401-redirect.php b/public/app/mu-plugins/moj-auth/templates/401-redirect.php
new file mode 100644
index 000000000..3ec0f2c89
--- /dev/null
+++ b/public/app/mu-plugins/moj-auth/templates/401-redirect.php
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+ Page Redirection
+
+
+
+
+ If you are not redirected automatically, follow this link to login.
+
+
+
diff --git a/public/app/mu-plugins/moj-auth/jwt.php b/public/app/mu-plugins/moj-auth/traits/jwt.php
similarity index 65%
rename from public/app/mu-plugins/moj-auth/jwt.php
rename to public/app/mu-plugins/moj-auth/traits/jwt.php
index 9c844d80b..7b50464c7 100644
--- a/public/app/mu-plugins/moj-auth/jwt.php
+++ b/public/app/mu-plugins/moj-auth/traits/jwt.php
@@ -2,8 +2,8 @@
namespace MOJ\Intranet;
-// Do not allow access outside WP or standalone.php
-defined('ABSPATH') || defined('DOING_STANDALONE_AUTH') || exit;
+// Do not allow access outside WP, 401.php or verify.php
+defined('ABSPATH') || defined('DOING_STANDALONE_401') || defined('DOING_STANDALONE_VERIFY') || exit;
/**
* JWT functions for MOJ\Intranet\Auth.
@@ -55,17 +55,29 @@ public function getJwt(): bool | object
try {
$decoded = JWT::decode($jwt, new Key($this->jwt_secret, $this::JWT_ALGORITHM));
} catch (\Exception $e) {
- if($e->getMessage() !== 'Expired token') {
+ if ($e->getMessage() !== 'Expired token') {
\Sentry\captureException($e);
}
$this->log($e->getMessage());
return false;
}
- if ($decoded && $decoded->sub) {
+ if(!$decoded) {
+ return $decoded;
+ }
+
+ if ($decoded->sub) {
$this->sub = $decoded->sub;
}
+ if (!empty($decoded->login_attempts)) {
+ $this->login_attempts = $decoded->login_attempts;
+ }
+
+ if (!empty($decoded->success_uri)) {
+ $this->success_uri = $decoded->success_uri;
+ }
+
return $decoded;
}
@@ -75,11 +87,11 @@ public function getJwt(): bool | object
* @return object Returns the JWT payload.
*/
- public function setJwt(array $args = []): object
+ public function setJwt(object $args = new \stdClass()): object
{
$this->log('setJwt()');
- $expiry = isset($args['expiry']) ? $args['expiry'] : $this->now + $this::JWT_DURATION;
+ $expiry = isset($args->expiry) ? $args->expiry : $this->now + $this::JWT_DURATION;
if (!$this->sub) {
$this->sub = bin2hex(random_bytes(16));
@@ -90,8 +102,17 @@ public function setJwt(array $args = []): object
'sub' => $this->sub,
'exp' => $expiry,
// Public claims - https://www.iana.org/assignments/jwt/jwt.xhtml
- 'roles' => isset($args['roles']) ? $args['roles'] : [],
+ 'roles' => isset($args->roles) ? $args->roles : [],
];
+
+ // Custom claims - conditionally add login_attempts & success_uri.
+ if(!empty($this->login_attempts)) {
+ $payload['login_attempts'] = $this->login_attempts;
+ }
+
+ if(!empty($this->success_uri)) {
+ $payload['success_uri'] = $this->success_uri;
+ }
$jwt = JWT::encode($payload, $this->jwt_secret, $this::JWT_ALGORITHM);
diff --git a/public/app/mu-plugins/moj-auth/oauth.php b/public/app/mu-plugins/moj-auth/traits/oauth.php
similarity index 93%
rename from public/app/mu-plugins/moj-auth/oauth.php
rename to public/app/mu-plugins/moj-auth/traits/oauth.php
index e59c36184..d3f5ef475 100644
--- a/public/app/mu-plugins/moj-auth/oauth.php
+++ b/public/app/mu-plugins/moj-auth/traits/oauth.php
@@ -24,7 +24,7 @@ trait AuthOauth
private $oauth_scopes = [];
private $oauth_action = '';
- const OAUTH_CALLBACK_URI = '/oauth2?action=callback';
+ const OAUTH_CALLBACK_URI = '/auth/callback';
const OAUTH_AUTHORIZE_ENDPOINT = '/oauth2/v2.0/authorize';
const OAUTH_TOKEN_ENDPOINT = '/oauth2/v2.0/token';
const OAUTH_STATE_COOKIE_NAME = 'OAUTH_STATE';
@@ -47,16 +47,14 @@ public function initOauth()
$this->oauth_app_id = $_ENV['OAUTH_CLIENT_ID'];
$this->oauth_app_secret = $_ENV['OAUTH_CLIENT_SECRET'];
$this->oauth_scopes = [
- 'api://' . $this->oauth_app_id . '/user_impersonation',
+ 'User.Read',
'offline_access' // To get a refresh token
];
if (
- isset($_SERVER['REQUEST_URI'])
- && str_starts_with($_SERVER['REQUEST_URI'], '/oauth2')
- && isset($_GET['action'])
- && in_array($_GET['action'], ['callback', 'login', 'logout'])
+ isset($_SERVER['REQUEST_URI']) && str_starts_with ($_SERVER['REQUEST_URI'], '/auth/' )
) {
- $this->oauth_action = $_GET['action'];
+ $path = explode('?', $_SERVER['REQUEST_URI'])[0];
+ $this->oauth_action = explode('/', $path )[2];
}
// Clear OAUTH_CLIENT_SECRET from $_ENV global. It's not required elsewhere in the app.
@@ -110,13 +108,11 @@ public function oauthLogin(): void
// Use a cookie to store oauth state.
$this->setCookie($this::OAUTH_STATE_COOKIE_NAME, $state_hashed, -1);
- // Store the user's origin URL in a transient.
- set_transient('oauth_user_url_' . $this->sub, $_SERVER['REQUEST_URI'] ?? '', 60 * 5); // 5 minutes
-
// Storing pkce prevents an attacker from potentially intercepting the auth code and using it.
set_transient('oauth_pkce_' . $state_hashed, $oauth_client->getPkceCode(), 60 * 5); // 5 minutes
header('Location: ' . $authUrl);
+ // echo 'will redirect to: ' . $authUrl;
exit();
}
diff --git a/public/app/mu-plugins/moj-auth/utils.php b/public/app/mu-plugins/moj-auth/traits/utils.php
similarity index 94%
rename from public/app/mu-plugins/moj-auth/utils.php
rename to public/app/mu-plugins/moj-auth/traits/utils.php
index c5b6dd88d..ff56604bf 100644
--- a/public/app/mu-plugins/moj-auth/utils.php
+++ b/public/app/mu-plugins/moj-auth/traits/utils.php
@@ -2,8 +2,8 @@
namespace MOJ\Intranet;
-// Do not allow access outside WP or standalone.php
-defined('ABSPATH') || defined('DOING_STANDALONE_AUTH') || exit;
+// Do not allow access outside WP, 401.php or verify.php
+defined('ABSPATH') || defined('DOING_STANDALONE_401') || defined('DOING_STANDALONE_VERIFY') || exit;
/**
* Util functions for MOJ\Intranet\Auth.
@@ -73,6 +73,8 @@ public function ipAddressIsAllowed(): bool
{
$this->log('ipAddressIsAllowed()');
+ return false;
+
if (empty($_ENV['ALLOWED_IPS']) || empty($_SERVER['HTTP_X_REAL_IP'])) {
return false;
}
@@ -129,6 +131,8 @@ public function setCookie(string $name, string $value, int $expiry = 0): void
...($expiry > 0 ? ['Expires=' . gmdate('D, d M Y H:i:s T', $expiry)] : []),
];
+ // $this->log('setCookie()', $cookie_parts);
+
header('Set-Cookie: ' . implode('; ', $cookie_parts), false);
}
diff --git a/public/app/mu-plugins/moj-auth/standalone.php b/public/app/mu-plugins/moj-auth/verify.php
similarity index 73%
rename from public/app/mu-plugins/moj-auth/standalone.php
rename to public/app/mu-plugins/moj-auth/verify.php
index f4ad95860..ae3e04a54 100644
--- a/public/app/mu-plugins/moj-auth/standalone.php
+++ b/public/app/mu-plugins/moj-auth/verify.php
@@ -4,22 +4,22 @@
// Exit if this file is included within a WordPress request.
if (defined('ABSPATH')) {
- error_log('moj-auth/standalone.php was accessed within the context of WordPress.');
+ error_log('moj-auth/verify.php was accessed within the context of WordPress.');
http_response_code(401) && exit();
}
-define('DOING_STANDALONE_AUTH', true);
+define('DOING_STANDALONE_VERIFY', true);
$autoload = '../../../../vendor/autoload.php';
if (!file_exists($autoload)) {
- error_log('moj-auth/standalone.php autoloader.php was not found.');
+ error_log('moj-auth/verify.php autoloader.php was not found.');
http_response_code(401) && exit();
}
require_once $autoload;
-require_once 'jwt.php';
-require_once 'utils.php';
+require_once 'traits/jwt.php';
+require_once 'traits/utils.php';
class StandaloneAuth
{
@@ -43,11 +43,18 @@ public function handleAuthRequest(string $required_role = 'reader'): void
// Get the JWT token from the request. Do this early so that we populate $this->sub if it's known.
$jwt = $this->getJwt();
+ $this->log('jwt in auth request', $jwt);
+
// Get the roles from the JWT and check that they're sufficient.
$jwt_correct_role = $jwt && $jwt->roles ? in_array($required_role, $jwt->roles) : false;
$status_code = $jwt_correct_role ? 200 : 401;
+ $status_code = 200;
+ // $status_code = 401;
+
+ $this->log('handleAuthRequest status: ' . $status_code);
+
http_response_code($status_code) && exit();
}
}
From 586231bbed98ba73af11b42db89ee961c6df73c1 Mon Sep 17 00:00:00 2001
From: EarthlingDavey <15802017+EarthlingDavey@users.noreply.github.com>
Date: Fri, 26 Jul 2024 15:47:45 +0100
Subject: [PATCH 6/9] Add heartbeat request
---
public/app/themes/clarity/src/globals/js/auth-heartbeat.js | 6 ++++++
public/app/themes/clarity/src/globals/js/script-loader.js | 1 +
2 files changed, 7 insertions(+)
create mode 100644 public/app/themes/clarity/src/globals/js/auth-heartbeat.js
diff --git a/public/app/themes/clarity/src/globals/js/auth-heartbeat.js b/public/app/themes/clarity/src/globals/js/auth-heartbeat.js
new file mode 100644
index 000000000..11b649813
--- /dev/null
+++ b/public/app/themes/clarity/src/globals/js/auth-heartbeat.js
@@ -0,0 +1,6 @@
+export default (function ($) {
+ // Sent a request to the heartbeat endpoint, this will refresh the oauth token.
+ setInterval(function(){
+ $.get( "/auth/heartbeat" )
+ }, 30000)
+})(jQuery)
diff --git a/public/app/themes/clarity/src/globals/js/script-loader.js b/public/app/themes/clarity/src/globals/js/script-loader.js
index 1717c40dc..80bc6416b 100644
--- a/public/app/themes/clarity/src/globals/js/script-loader.js
+++ b/public/app/themes/clarity/src/globals/js/script-loader.js
@@ -13,6 +13,7 @@ import "../../components/c-input-container/on-change.js";
import "../../components/c-notes-from-antonia/lazy_load.js";
// Global scripts
+import "./auth-heartbeat.js";
import "./blog-content_filter.js";
import "./condolences-filter.js";
import "./equaliser.js";
From ede507bdb83d0cc835e9aea49901a34b5cbc0fd0 Mon Sep 17 00:00:00 2001
From: EarthlingDavey <15802017+EarthlingDavey@users.noreply.github.com>
Date: Fri, 26 Jul 2024 15:49:23 +0100
Subject: [PATCH 7/9] Cleanup custom JWT properties. Add heartbeat endpoint.
---
deploy/config/local/nginx/server.conf | 2 +-
public/app/mu-plugins/moj-auth/401.php | 12 +-
public/app/mu-plugins/moj-auth/moj-auth.php | 180 ++++++++----------
public/app/mu-plugins/moj-auth/traits/jwt.php | 23 +--
.../app/mu-plugins/moj-auth/traits/oauth.php | 4 +-
public/app/mu-plugins/moj-auth/verify.php | 7 -
6 files changed, 94 insertions(+), 134 deletions(-)
diff --git a/deploy/config/local/nginx/server.conf b/deploy/config/local/nginx/server.conf
index a9f8e19f9..ecb4a0fa0 100644
--- a/deploy/config/local/nginx/server.conf
+++ b/deploy/config/local/nginx/server.conf
@@ -173,7 +173,7 @@ server {
}
# Rewrite auth endpoints to fpm (WordPress's index.php)
- location ~ ^/auth/(login|callback|logout) {
+ location ~ ^/auth/(login|callback|heartbeat|logout) {
auth_request off;
fastcgi_param HTTP_X_IP_GROUP $geo;
rewrite /auth/* /index.php?$args;
diff --git a/public/app/mu-plugins/moj-auth/401.php b/public/app/mu-plugins/moj-auth/401.php
index f3b8b885a..4fba2b8e8 100644
--- a/public/app/mu-plugins/moj-auth/401.php
+++ b/public/app/mu-plugins/moj-auth/401.php
@@ -37,8 +37,6 @@ class Standalone401
private $debug = false;
private $https = false;
private $sub = '';
- private $login_attempts = null;
- private $success_uri = null;
const OAUTH_LOGIN_URI = '/auth/login';
const MAX_AUTO_LOGIN_ATTEMPTS = 5;
@@ -50,7 +48,6 @@ public function __construct(array $args = [])
$this->now = time();
$this->debug = $args['debug'] ?? false;
$this->https = isset($_SERVER['HTTPS']) || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO']);
- $this->success_uri = $_SERVER['REQUEST_URI'];
if (!file_exists($this::STATIC_401)) {
error_log('moj-auth/401.php 401.html was not found.');
@@ -70,13 +67,16 @@ public function handle401Request(): void
$this->log('handle401Request()');
// Get the JWT token from the request. Do this early so that we populate $this->sub if it's known.
- $jwt = $this->getJwt();
+ $jwt = $this->getJwt() ?: (object)[];
// Set loginAttempts with a default of 1, or add one to the existing value.
- $this->login_attempts = empty($jwt->login_attempts) ? 1 : ((int) $jwt->login_attempts) + 1;
+ $jwt->login_attempts = empty($jwt->login_attempts) ? 1 : ((int) $jwt->login_attempts) + 1;
+
+ // Where to redirect the user after successful login.
+ $jwt->success_uri = $_SERVER['REQUEST_URI'];
// Set a JWT without a role, to persist the user's ID, login attempts and success_uri.
- $jwt = $this->setJwt();
+ $jwt = $this->setJwt($jwt);
// Is this the first few times a visitor has hit the 401 page?
if ($jwt->login_attempts <= $this::MAX_AUTO_LOGIN_ATTEMPTS) {
diff --git a/public/app/mu-plugins/moj-auth/moj-auth.php b/public/app/mu-plugins/moj-auth/moj-auth.php
index 28997e5e0..2606a3dc0 100644
--- a/public/app/mu-plugins/moj-auth/moj-auth.php
+++ b/public/app/mu-plugins/moj-auth/moj-auth.php
@@ -18,8 +18,6 @@
// Do not allow access outside WP
defined('ABSPATH') || exit;
-
-
// If the plugin isn't enabled, return early.
if (Config::get('MOJ_AUTH_ENABLED') === false) {
return;
@@ -49,8 +47,6 @@ class Auth
private $debug = false;
private $https = false;
private $sub = '';
- private $login_attempts = null;
- private $success_uri = null;
/**
* Constructor
@@ -69,8 +65,14 @@ public function __construct(array $args = [])
$this->initOauth();
}
- public function handleLoginRequest(): void
+ public function handleRequest(): void
{
+ $this->log('handleRequest()');
+
+ if(!$this->oauth_action) {
+ return;
+ }
+
// Get the JWT token from the request. Do this early so that we populate $this->sub if it's known.
$jwt = $this->getJwt();
@@ -79,136 +81,116 @@ public function handleLoginRequest(): void
$jwt = $this->setJwt();
}
- if ($this->oauth_action === 'login') {
- // Handle Azure AD/Entra ID OAuth. It redirects to Azure or exits with 401 if disabled. php code execution always stops here.
- $this->oauthLogin();
+ if ('login' === $this->oauth_action) {
+ $this->handleLoginRequest();
+ exit();
+ }
+
+ if ('callback' === $this->oauth_action) {
+ $this->handleCallbackRequest();
+ exit();
+ }
+
+ if ('heartbeat' === $this->oauth_action) {
+ $this->handleHeartbeatRequest();
+ exit();
+ }
+
+ if (!empty($this->oauth_action)) {
+ $this->log('Unknown oauth action');
+ exit();
}
}
+ public function handleLoginRequest(): void
+ {
+ $this->log('handleLoginRequest()');
+
+ // Handle Azure AD/Entra ID OAuth. It redirects to Azure or exits with 401 if disabled.
+ $this->oauthLogin();
+ }
+
public function handleCallbackRequest(): void
{
$this->log('handleCallbackRequest()');
// If we've hit the callback endpoint, then handle it here. On fail it exits with 401 & php code execution stops here.
- $oauth_access_token = 'callback' === $this->oauth_action ? $this->oauthCallback() : null;
+ $oauth_access_token = $this->oauthCallback();
// The callback has returned an access token.
- if (is_object($oauth_access_token) && !$oauth_access_token->hasExpired()) {
- $this->log('Access token is valid. Will set JWT and store refresh token.');
- // Set a JWT cookie.
- $this->setJwt((object)[
- 'expiry' => $oauth_access_token->getExpires(),
- 'roles' => ['reader'],
- ]);
- // Store the tokens.
- $this->storeTokens($this->sub, $oauth_access_token, 'refresh');
- // Redirect the user to the page they were trying to access.
- header('Location: ' . \home_url($this->success_uri ?? '/'));
- exit();
+ if (!is_object($oauth_access_token) || $oauth_access_token->hasExpired()) {
+ $this->log('Access token is not valid, or expired.');
+ return;
}
- }
- /**
- * Handle the page request
- *
- * This method is called on every page request.
- * It checks the JWT cookie and the IP address to determine if the user should be allowed access.
- *
- * @param string $required_role The necessary role required to access the page.
- * @return void
- */
+ $this->log('Access token is valid. Will set JWT and store refresh token.');
- public function handlePageRequest(string $required_role = 'reader'): void
- {
- $this->log('handlePageRequest()');
+ $jwt = $this->getJwt() ?: (object)[];
- // TODO - is \headers_sent() a good idea? Could the user exploit a bug?
- // If headers are already sent or we're doing a cron job, return early.
- if (\headers_sent() || defined('DOING_CRON')) {
- return;
- }
+ $jwt->expiry = $oauth_access_token->getExpires();
+ $jwt->roles = ['reader'];
- // Get the JWT token from the request. Do this early so that we populate $this->sub if it's known.
- $jwt = $this->getJwt();
+ // Set a JWT cookie.
+ $this->setJwt($jwt);
- // Set a JWT without a role, to persist the user's ID.
- if (!$jwt) {
- $jwt = $this->setJwt();
- }
+ // Store the tokens.
+ $this->storeTokens($this->sub, $oauth_access_token, 'refresh');
- $this->log('$this->oauth_action: ' . $this->oauth_action);
+ // Redirect the user to the page they were trying to access.
+ header('Location: ' . \home_url($jwt->success_uri ?? '/')) && exit();
+ }
- if ('login' === $this->oauth_action) {
- // Handle Azure AD/Entra ID OAuth. It redirects to Azure or exits with 401 if disabled. php code execution always stops here.
- $this->oauthLogin();
- }
+ public function handleHeartbeatRequest(): void
+ {
+ $this->log('handleHeartbeatRequest()');
- // If we've hit the callback endpoint, then handle it here. On fail it exits with 401 & php code execution stops here.
- $oauth_access_token = 'callback' === $this->oauth_action ? $this->oauthCallback() : null;
+ // Get the JWT token from the request. Do this early so that we populate $this->sub if it's known.
+ $jwt = $this->getJwt();
- // The callback has returned an access token.
- if (is_object($oauth_access_token) && !$oauth_access_token->hasExpired()) {
- $this->log('Access token is valid. Will set JWT and store refresh token.');
- // Set a JWT cookie.
- $this->setJwt((object)[
- 'expiry' => $oauth_access_token->getExpires(),
- 'roles' => ['reader']
- ]);
- // Store the tokens.
- $this->storeTokens($this->sub, $oauth_access_token, 'refresh');
- // Get the origin request from the cookie.
- $user_redirect = get_transient('oauth_user_url_' . $this->sub);
- // Remove the transient.
- delete_transient('oauth_user_url_' . $this->sub);
- // Redirect the user to the page they were trying to access.
- header('Location: ' . \home_url($user_redirect ?? '/'));
- exit();
+ if(!$jwt) {
+ return;
}
- // Get the roles from the JWT and check that they're sufficient.
- $jwt_correct_role = $jwt && $jwt->roles ? in_array($required_role, $jwt->roles) : false;
+ // Keep track of JWT mutations.
+ $mutated_jwt = false;
+
+ // Clear success_uri & login_attempts here?
+ if (!empty($jwt->login_attempts) || !empty($jwt->success_uri)) {
+ $mutated_jwt = true;
+ $jwt->login_attempts = null;
+ $jwt->success_uri = null;
+ }
// Calculate the remaining time on the JWT token.
$jwt_remaining_time = $jwt && $jwt->exp ? $jwt->exp - $this->now : 0;
- // JWT is valid and it's not time to refresh it.
- if ($jwt_correct_role && $jwt_remaining_time > $this::JWT_REFRESH) {
+ // It's not time to refresh the JWT, and we need to update the JWT.
+ if ($jwt_remaining_time > $this::JWT_REFRESH && $mutated_jwt) {
+ $jwt = $this->setJwt($jwt);
+ }
+
+ // It's not time to refresh the JWT, return early.
+ if ($jwt_remaining_time > $this::JWT_REFRESH) {
return;
}
/*
- * There is no valid JWT, or it's about to expire.
+ * The JWT is about to expire.
*/
- // If the IP address is allowed, set a JWT and return.
- if ($this->ipAddressIsAllowed()) {
- $this->setJwt((object)['roles' => ['reader']]);
- return;
- }
-
// Refresh OAuth token if it's about to expire.
$oauth_refresh_token = $this->sub ? $this->getStoredTokens($this->sub, 'refresh') : null;
$oauth_refreshed_access_token = $oauth_refresh_token ? $this->oauthRefreshToken($oauth_refresh_token) : null;
-
+
if (is_object($oauth_refreshed_access_token) && !$oauth_refreshed_access_token->hasExpired()) {
$this->log('Refreshed access token is valid. Will set JWT and store refresh token.');
// Set a JWT cookie.
- $jwt = $this->setJwt((object)[
- 'expiry' => $oauth_refreshed_access_token->getExpires(),
- 'roles' => ['reader']
- ]);
+ $jwt->expiry = $oauth_refreshed_access_token->getExpires();
+ $jwt->roles = ['reader'];
+ $jwt = $this->setJwt($jwt);
// Store the tokens.
$this->storeTokens($this->sub, $oauth_refreshed_access_token, 'refresh');
- return;
- }
-
- // If there's any time left on the JWT then return.
- if ($jwt_correct_role && $jwt_remaining_time > 0) {
- return;
}
-
- // Handle Azure AD/Entra ID OAuth. It redirects to Azure or exits with 401 if disabled. php code execution always stops here.
- // $this->oauthLogin();
}
/**
@@ -227,11 +209,5 @@ public function logout(): void
}
-$auth = new Auth(['debug' => true]); // Config::get('MOJ_AUTH_DEBUG')]);
-$auth->handleLoginRequest();
-$auth->handleCallbackRequest();
-
-// Handle a normal page view
-// - refresh token if necessary
-// - clean up the success_uri
-// - clean up the login_attempts
+$auth = new Auth(['debug' => Config::get('MOJ_AUTH_DEBUG')]);
+$auth->handleRequest();
diff --git a/public/app/mu-plugins/moj-auth/traits/jwt.php b/public/app/mu-plugins/moj-auth/traits/jwt.php
index 7b50464c7..18785409a 100644
--- a/public/app/mu-plugins/moj-auth/traits/jwt.php
+++ b/public/app/mu-plugins/moj-auth/traits/jwt.php
@@ -41,7 +41,7 @@ public function initJwt(): void
* @return bool|object Returns false if the JWT is not found or an object if it is found.
*/
- public function getJwt(): bool | object
+ public function getJwt(): false | object
{
$this->log('getJwt()');
@@ -70,14 +70,6 @@ public function getJwt(): bool | object
$this->sub = $decoded->sub;
}
- if (!empty($decoded->login_attempts)) {
- $this->login_attempts = $decoded->login_attempts;
- }
-
- if (!empty($decoded->success_uri)) {
- $this->success_uri = $decoded->success_uri;
- }
-
return $decoded;
}
@@ -105,13 +97,14 @@ public function setJwt(object $args = new \stdClass()): object
'roles' => isset($args->roles) ? $args->roles : [],
];
- // Custom claims - conditionally add login_attempts & success_uri.
- if(!empty($this->login_attempts)) {
- $payload['login_attempts'] = $this->login_attempts;
+ // Custom claims - conditionally add login_attempts from $args or class property.
+ if(!empty($args->login_attempts)) {
+ $payload['login_attempts'] = $args->login_attempts;
}
-
- if(!empty($this->success_uri)) {
- $payload['success_uri'] = $this->success_uri;
+
+ // Custom claims - conditionally add success_uri from $args or class property.
+ if(!empty($args->success_uri)) {
+ $payload['success_uri'] = $args->success_uri;
}
$jwt = JWT::encode($payload, $this->jwt_secret, $this::JWT_ALGORITHM);
diff --git a/public/app/mu-plugins/moj-auth/traits/oauth.php b/public/app/mu-plugins/moj-auth/traits/oauth.php
index d3f5ef475..2c0bfdb80 100644
--- a/public/app/mu-plugins/moj-auth/traits/oauth.php
+++ b/public/app/mu-plugins/moj-auth/traits/oauth.php
@@ -111,9 +111,7 @@ public function oauthLogin(): void
// Storing pkce prevents an attacker from potentially intercepting the auth code and using it.
set_transient('oauth_pkce_' . $state_hashed, $oauth_client->getPkceCode(), 60 * 5); // 5 minutes
- header('Location: ' . $authUrl);
- // echo 'will redirect to: ' . $authUrl;
- exit();
+ header('Location: ' . $authUrl) && exit();
}
/**
diff --git a/public/app/mu-plugins/moj-auth/verify.php b/public/app/mu-plugins/moj-auth/verify.php
index ae3e04a54..fc5daed7b 100644
--- a/public/app/mu-plugins/moj-auth/verify.php
+++ b/public/app/mu-plugins/moj-auth/verify.php
@@ -43,18 +43,11 @@ public function handleAuthRequest(string $required_role = 'reader'): void
// Get the JWT token from the request. Do this early so that we populate $this->sub if it's known.
$jwt = $this->getJwt();
- $this->log('jwt in auth request', $jwt);
-
// Get the roles from the JWT and check that they're sufficient.
$jwt_correct_role = $jwt && $jwt->roles ? in_array($required_role, $jwt->roles) : false;
$status_code = $jwt_correct_role ? 200 : 401;
- $status_code = 200;
- // $status_code = 401;
-
- $this->log('handleAuthRequest status: ' . $status_code);
-
http_response_code($status_code) && exit();
}
}
From 9848caeaed2c4614e34e6cc9130740a5297a9893 Mon Sep 17 00:00:00 2001
From: EarthlingDavey <15802017+EarthlingDavey@users.noreply.github.com>
Date: Fri, 26 Jul 2024 16:55:37 +0100
Subject: [PATCH 8/9] Add IPS_FORMATTED to develop
---
.github/workflows/deploy.yml | 10 +++++++---
.github/workflows/integration.yml | 4 ++++
.github/workflows/ip-ranges-configure.yml | 14 ++++++++++++++
deploy/config/local/nginx/geo.conf | 7 +++++++
deploy/development/deployment.tpl.yml | 6 ++++++
deploy/development/secret.tpl.yml | 1 +
6 files changed, 39 insertions(+), 3 deletions(-)
create mode 100644 deploy/config/local/nginx/geo.conf
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 88360b2a1..2b6124900 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -51,6 +51,7 @@ jobs:
BASIC_AUTH_USER: ${{ secrets.BASIC_AUTH_USER }}
BASIC_AUTH_PASS: ${{ secrets.BASIC_AUTH_PASS }}
IP_RANGES: ${{ inputs.ip_ranges }}
+ IPS_FORMATTED: ${{ inputs.ips_formatted }}
IGNORE_IP_RANGES: ${{ vars.IGNORE_IP_RANGES }}
run: |
## - - - - - - - - - -
@@ -83,16 +84,19 @@ jobs:
## IP Ranges - - - - -
## - - - - - - - - - -
- ## Allow IP rangges to be ignored.
+ ## Allow IP ranges to be ignored.
## Nb. set IGNORE_IP_RANGES env var to `true` for the intended GH environment.
if [ "$IGNORE_IP_RANGES" = "true" ]; then
- IP_RANGES=$'{"ignore":true}'
+ IP_RANGES =$'{"ignore":true}'
+ IPS_FORMATTED=""
fi
- IP_RANGES_BASE64=$(echo -n "$IP_RANGES" | base64 -w 0)
+ IP_RANGES_BASE64 =$(echo -n "$IP_RANGES" | base64 -w 0)
+ IPS_FORMATTED_BASE64=$(echo -n "$IPS_FORMATTED" | base64 -w 0)
export IP_RANGES_BASE64
+ export IPS_FORMATTED_BASE64
## - - - - - - - - - - -
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 0ae3ccac9..a84b48f54 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -33,6 +33,7 @@ jobs:
environment: development
registry: ${{ needs.image.outputs.registry }}
ip_ranges: ${{ needs.get_ip_ranges.outputs.ip_ranges }}
+ ips_formatted: ${{ needs.get_ip_ranges.outputs.ips_formatted }}
secrets: inherit
deploy_staging:
@@ -44,6 +45,7 @@ jobs:
environment: staging
registry: ${{ needs.image.outputs.registry }}
ip_ranges: ${{ needs.get_ip_ranges.outputs.ip_ranges }}
+ ips_formatted: ${{ needs.get_ip_ranges.outputs.ips_formatted }}
secrets: inherit
deploy_demo:
@@ -55,6 +57,7 @@ jobs:
environment: demo
registry: ${{ needs.image.outputs.registry }}
ip_ranges: ${{ needs.get_ip_ranges.outputs.ip_ranges }}
+ ips_formatted: ${{ needs.get_ip_ranges.outputs.ips_formatted }}
secrets: inherit
deploy_production:
@@ -66,4 +69,5 @@ jobs:
environment: production
registry: ${{ needs.image.outputs.registry }}
ip_ranges: ${{ needs.get_ip_ranges.outputs.ip_ranges }}
+ ips_formatted: ${{ needs.get_ip_ranges.outputs.ips_formatted }}
secrets: inherit
diff --git a/.github/workflows/ip-ranges-configure.yml b/.github/workflows/ip-ranges-configure.yml
index a6590c8a9..194d05a56 100644
--- a/.github/workflows/ip-ranges-configure.yml
+++ b/.github/workflows/ip-ranges-configure.yml
@@ -6,6 +6,9 @@ on:
ip_ranges:
description: "IPs Ranges"
value: ${{ jobs.get_ip_ranges.outputs.ip_ranges }}
+ ips_formatted:
+ description: "IPs Ranges (formatted)"
+ value: ${{ jobs.get_ip_ranges.outputs.ips_formatted }}
jobs:
get_ip_ranges:
@@ -13,6 +16,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
ip_ranges: ${{ steps.get-ips.outputs.ip_ranges }}
+ ips_formatted: ${{ steps.get-ips.outputs.ips_formatted }}
steps:
- name: "Checkout"
uses: actions/checkout@v4
@@ -30,3 +34,13 @@ jobs:
cmd: |
IP_RANGES=$(yq 'explode(.) | {"deprecating": .deprecating_access_to_moj_intranet | flatten, "allow": .allow_access_to_moj_intranet | flatten }' -o json -I=0 moj-cidr-addresses.yml)
echo "ip_ranges=$IP_RANGES" >> $GITHUB_OUTPUT
+
+ # Transform into nginx geo format. 1 IP range per line, each range is followed by it's value.
+ # @see https://nginx.org/en/docs/http/ngx_http_geo_module.html
+ ALLOW_VALUE=1
+ DEPRI_VALUE=2
+
+ ALLOW_FORMATTED=$(yq 'explode(.) | .allow_access_to_moj_intranet | flatten | map(. + " '$ALLOW_VALUE';") | join("\n")' moj-cidr-addresses.yml)
+ DEPRI_FORMATTED=$(yq 'explode(.) | .deprecating_access_to_moj_intranet | flatten | map(. + " '$DEPRI_VALUE';") | join("\n")' moj-cidr-addresses.yml)
+
+ echo "ips_formatted=$ALLOW_FORMATTED"$'\n'"$DEPRI_FORMATTED" >> $GITHUB_OUTPUT
diff --git a/deploy/config/local/nginx/geo.conf b/deploy/config/local/nginx/geo.conf
new file mode 100644
index 000000000..5ffeef8ae
--- /dev/null
+++ b/deploy/config/local/nginx/geo.conf
@@ -0,0 +1,7 @@
+# Source IPs where 'X-Forwarded-For' is to be truested.
+proxy 172.17.0.0/16;
+proxy 172.25.0.0/16;
+
+# Maybe use init script to substitude env vars
+
+192.168.65.1 1;
\ No newline at end of file
diff --git a/deploy/development/deployment.tpl.yml b/deploy/development/deployment.tpl.yml
index ac4de15db..b45c7722f 100644
--- a/deploy/development/deployment.tpl.yml
+++ b/deploy/development/deployment.tpl.yml
@@ -37,6 +37,12 @@ spec:
mountPath: /var/www/html/public/app/uploads
- name: php-socket
mountPath: /sock
+ env:
+ - name: IPS_FORMATTED
+ valueFrom:
+ secretKeyRef:
+ name: ${KUBE_NAMESPACE}-base64-secrets
+ key: IPS_FORMATTED
- name: cron
image: ${ECR_URL}:${IMAGE_TAG_CRON}
diff --git a/deploy/development/secret.tpl.yml b/deploy/development/secret.tpl.yml
index 3d5cffa72..9b3f1c07e 100644
--- a/deploy/development/secret.tpl.yml
+++ b/deploy/development/secret.tpl.yml
@@ -28,6 +28,7 @@ type: Opaque
data:
AWS_CLOUDFRONT_PRIVATE_KEY: "${AWS_CLOUDFRONT_PRIVATE_KEY_BASE64}"
IP_RANGES: "${IP_RANGES_BASE64}"
+ IPS_FORMATTED: "${IPS_FORMATTED_BASE64}"
---
apiVersion: v1
kind: Secret
From a37f3a676a00ee9d4cdfa423020ae66c19a9f01d Mon Sep 17 00:00:00 2001
From: EarthlingDavey <15802017+EarthlingDavey@users.noreply.github.com>
Date: Fri, 26 Jul 2024 17:25:45 +0100
Subject: [PATCH 9/9] Remove previous IP_RANGES, use nginx init script to
populate conf file.
---
.env.example | 13 ++--
.github/workflows/deploy.yml | 4 --
.github/workflows/ip-ranges-configure.yml | 9 +--
Dockerfile | 3 +-
deploy/config/init/nginx-geo.sh | 3 +
deploy/config/local/nginx/geo.conf | 7 ---
deploy/config/local/nginx/server.conf | 12 +---
deploy/demo/config.yml | 7 ---
deploy/development/config.yml | 3 -
deploy/development/secret.tpl.yml | 1 -
deploy/staging/config.yml | 7 ---
docker-compose.yml | 3 +
.../app/mu-plugins/moj-auth/traits/utils.php | 62 -------------------
13 files changed, 18 insertions(+), 116 deletions(-)
create mode 100644 deploy/config/init/nginx-geo.sh
delete mode 100644 deploy/config/local/nginx/geo.conf
diff --git a/.env.example b/.env.example
index a613ca782..8cda8baef 100644
--- a/.env.example
+++ b/.env.example
@@ -62,11 +62,14 @@ OAUTH_CLIENT_ID=
OAUTH_TENANT_ID=
OAUTH_CLIENT_SECRET=""
-# IP addresses, with optional CIDR notation. Separated by newlines & # comments.
-ALLOWED_IPS="
-# Home netowrk IP range - at http://intranet.docker/info.php > HTTP_X_REAL_IP
-192.168.0.0/16
-"
+# IP ranges in nginx geo format. 1 IP range per line, each range is followed by it's value.
+# @see https://nginx.org/en/docs/http/ngx_http_geo_module.html
+IPS_FORMATTED="
+proxy 172.17.0.0/16;
+proxy 172.25.0.0/16;
+
+192.168.65.1 1;
+192.168.65.3 2;"
# The following 4 environment variables can be generated with `make key-gen`.
JWT_SECRET="generated-key"
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 2b6124900..8d93c0c8c 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -50,7 +50,6 @@ jobs:
# AWS_CLOUDFRONT_PUBLIC_KEY_EXPIRING: "${{ secrets.AWS_CLOUDFRONT_PUBLIC_KEY_B }}"
BASIC_AUTH_USER: ${{ secrets.BASIC_AUTH_USER }}
BASIC_AUTH_PASS: ${{ secrets.BASIC_AUTH_PASS }}
- IP_RANGES: ${{ inputs.ip_ranges }}
IPS_FORMATTED: ${{ inputs.ips_formatted }}
IGNORE_IP_RANGES: ${{ vars.IGNORE_IP_RANGES }}
run: |
@@ -88,14 +87,11 @@ jobs:
## Nb. set IGNORE_IP_RANGES env var to `true` for the intended GH environment.
if [ "$IGNORE_IP_RANGES" = "true" ]; then
- IP_RANGES =$'{"ignore":true}'
IPS_FORMATTED=""
fi
- IP_RANGES_BASE64 =$(echo -n "$IP_RANGES" | base64 -w 0)
IPS_FORMATTED_BASE64=$(echo -n "$IPS_FORMATTED" | base64 -w 0)
- export IP_RANGES_BASE64
export IPS_FORMATTED_BASE64
diff --git a/.github/workflows/ip-ranges-configure.yml b/.github/workflows/ip-ranges-configure.yml
index 194d05a56..56d1e7293 100644
--- a/.github/workflows/ip-ranges-configure.yml
+++ b/.github/workflows/ip-ranges-configure.yml
@@ -3,9 +3,6 @@ name: "Get IP ranges"
on:
workflow_call:
outputs:
- ip_ranges:
- description: "IPs Ranges"
- value: ${{ jobs.get_ip_ranges.outputs.ip_ranges }}
ips_formatted:
description: "IPs Ranges (formatted)"
value: ${{ jobs.get_ip_ranges.outputs.ips_formatted }}
@@ -15,7 +12,6 @@ jobs:
name: "Build"
runs-on: ubuntu-latest
outputs:
- ip_ranges: ${{ steps.get-ips.outputs.ip_ranges }}
ips_formatted: ${{ steps.get-ips.outputs.ips_formatted }}
steps:
- name: "Checkout"
@@ -32,10 +28,7 @@ jobs:
uses: mikefarah/yq@master
with:
cmd: |
- IP_RANGES=$(yq 'explode(.) | {"deprecating": .deprecating_access_to_moj_intranet | flatten, "allow": .allow_access_to_moj_intranet | flatten }' -o json -I=0 moj-cidr-addresses.yml)
- echo "ip_ranges=$IP_RANGES" >> $GITHUB_OUTPUT
-
- # Transform into nginx geo format. 1 IP range per line, each range is followed by it's value.
+ # Transform IPs into nginx geo format. 1 IP range per line, each range is followed by it's value.
# @see https://nginx.org/en/docs/http/ngx_http_geo_module.html
ALLOW_VALUE=1
DEPRI_VALUE=2
diff --git a/Dockerfile b/Dockerfile
index 647149da2..555b71d6b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -75,8 +75,7 @@ RUN mkdir /var/run/nginx-cache && \
# contains gzip and module include
COPY --chown=nginx:nginx deploy/config/nginx.conf /etc/nginx/nginx.conf
-# Disabled on 22 July 2024 - no deployment init files for Nginx
-#COPY deploy/config/init/nginx-* /docker-entrypoint.d/
+COPY deploy/config/init/nginx-* /docker-entrypoint.d/
RUN chmod +x /docker-entrypoint.d/*; \
echo "# This file is configured at runtime." > /etc/nginx/real_ip.conf
diff --git a/deploy/config/init/nginx-geo.sh b/deploy/config/init/nginx-geo.sh
new file mode 100644
index 000000000..dd2408e83
--- /dev/null
+++ b/deploy/config/init/nginx-geo.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "$IPS_FORMATTED" >> /etc/nginx/geo.conf
diff --git a/deploy/config/local/nginx/geo.conf b/deploy/config/local/nginx/geo.conf
deleted file mode 100644
index 5ffeef8ae..000000000
--- a/deploy/config/local/nginx/geo.conf
+++ /dev/null
@@ -1,7 +0,0 @@
-# Source IPs where 'X-Forwarded-For' is to be truested.
-proxy 172.17.0.0/16;
-proxy 172.25.0.0/16;
-
-# Maybe use init script to substitude env vars
-
-192.168.65.1 1;
\ No newline at end of file
diff --git a/deploy/config/local/nginx/server.conf b/deploy/config/local/nginx/server.conf
index ecb4a0fa0..9ecec1dbf 100644
--- a/deploy/config/local/nginx/server.conf
+++ b/deploy/config/local/nginx/server.conf
@@ -28,16 +28,8 @@ fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
geo $geo {
- # Trusted IPs where 'X-Forwarded-For' is used.
- proxy 172.17.0.0/16;
- proxy 172.25.0.0/16;
-
- # TODO
- # read from include a .conf file
- # Maybe use init script to substitude env vars
- default 0;
-
- 192.168.65.1 1;
+ default 0;
+ include /etc/nginx/geo.conf;
}
server {
diff --git a/deploy/demo/config.yml b/deploy/demo/config.yml
index bc22be3c5..db119a5db 100644
--- a/deploy/demo/config.yml
+++ b/deploy/demo/config.yml
@@ -12,10 +12,3 @@ data:
# See Azure Setup for more information on how to get these values.
OAUTH_CLIENT_ID: "8d928bcf-c45e-41ec-aedf-059828aa6e3f"
OAUTH_TENANT_ID: "c6874728-71e6-41fe-a9e1-2e8c36776ad8"
- # IP addresses, with optional CIDR notation. Separated by newlines and using # for comments.
- ALLOWED_IPS: |
- # Global Protect
- 18.169.147.172
- 35.176.93.186
- 18.130.148.126
- 35.176.148.126
diff --git a/deploy/development/config.yml b/deploy/development/config.yml
index 64acc99ce..7d73d5858 100644
--- a/deploy/development/config.yml
+++ b/deploy/development/config.yml
@@ -11,6 +11,3 @@ data:
# See Azure Setup for more information on how to get these values.
OAUTH_CLIENT_ID: "1dac3cbf-91d2-4c0e-9c80-0bf3f8fabd75"
OAUTH_TENANT_ID: "c6874728-71e6-41fe-a9e1-2e8c36776ad8"
- # IP addresses, with optional CIDR notation. Separated by newlines and using # for comments.
- # TODO: This block will be removed as part of CDPT-887.
- ALLOWED_IPS: ""
diff --git a/deploy/development/secret.tpl.yml b/deploy/development/secret.tpl.yml
index 9b3f1c07e..7c82586ec 100644
--- a/deploy/development/secret.tpl.yml
+++ b/deploy/development/secret.tpl.yml
@@ -27,7 +27,6 @@ metadata:
type: Opaque
data:
AWS_CLOUDFRONT_PRIVATE_KEY: "${AWS_CLOUDFRONT_PRIVATE_KEY_BASE64}"
- IP_RANGES: "${IP_RANGES_BASE64}"
IPS_FORMATTED: "${IPS_FORMATTED_BASE64}"
---
apiVersion: v1
diff --git a/deploy/staging/config.yml b/deploy/staging/config.yml
index bf06c8976..6b7a56c2a 100644
--- a/deploy/staging/config.yml
+++ b/deploy/staging/config.yml
@@ -11,10 +11,3 @@ data:
# See Azure Setup in the README for more information on how to get these values.
OAUTH_CLIENT_ID: "ffb808d2-312b-4ffe-a6e5-d6eacfd9f06f"
OAUTH_TENANT_ID: "c6874728-71e6-41fe-a9e1-2e8c36776ad8"
- # IP addresses, with optional CIDR notation. Separated by newlines and using # for comments.
- ALLOWED_IPS: |
- # Global Protect
- 18.169.147.172
- 35.176.93.186
- 18.130.148.126
- 35.176.148.126
diff --git a/docker-compose.yml b/docker-compose.yml
index 19b213de3..c5aa3cfa8 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -37,17 +37,20 @@ services:
- php-socket:/sock
### Deploy scripts
- ./deploy/config/local/nginx/php-fpm.conf:/etc/nginx/php-fpm.conf
+ - ./deploy/config/local/nginx/php-fpm-auth.conf:/etc/nginx/php-fpm-auth.conf
- ./deploy/config/local/nginx/server.conf:/etc/nginx/conf.d/default.conf
- ./deploy/config/nginx.conf:/etc/nginx/nginx.conf
### Assets
- ./public/app/plugins:/var/www/html/public/app/plugins
- ./public/app/themes/clarity/screenshot.png:/var/www/html/public/app/themes/clarity/screenshot.png
+ - ./public/app/themes/clarity/error-pages:/var/www/html/public/app/themes/clarity/error-pages
- ./public/wp:/var/www/html/public/wp
- ./public/index.php:/var/www/html/public/index.php
environment:
VIRTUAL_HOST: ${SERVER_NAME}
VIRTUAL_PORT: 8080
SERVER_NAME: ${SERVER_NAME}
+ IPS_FORMATTED: ${IPS_FORMATTED}
ports:
- "8080:8080"
depends_on:
diff --git a/public/app/mu-plugins/moj-auth/traits/utils.php b/public/app/mu-plugins/moj-auth/traits/utils.php
index ff56604bf..53c850166 100644
--- a/public/app/mu-plugins/moj-auth/traits/utils.php
+++ b/public/app/mu-plugins/moj-auth/traits/utils.php
@@ -28,68 +28,6 @@ public function log(string $message, $data = null): void
error_log($message . ' ' . print_r($data, true));
}
- /**
- * Checks if a given IP address matches the specified CIDR subnet/s
- *
- * @see https://gist.github.com/tott/7684443?permalink_comment_id=2108696#gistcomment-2108696
- *
- * @param string $ip The IP address to check
- * @param mixed $cidrs The IP subnet (string) or subnets (array) in CIDR notation
- * @param string $match optional If provided, will contain the first matched IP subnet
- * @return boolean TRUE if the IP matches a given subnet or FALSE if it does not
- */
-
- public function ipMatch($ip, $cidrs, &$match = null): bool
- {
- $this->log('ipMatch()');
-
- foreach ((array) $cidrs as $cidr) {
- if (empty($cidr)) {
- continue;
- }
- $parts = explode('/', $cidr);
- $subnet = $parts[0];
- $mask = $parts[1] ?? 32;
- if (((ip2long($ip) & ($mask = ~((1 << (32 - $mask)) - 1))) == (ip2long($subnet) & $mask))) {
- $match = $cidr;
- return true;
- }
- }
-
- return false;
- }
-
-
- /**
- * Check if the IP address is allowed.
- *
- * Checks that we have the environment variable ALLOWED_IPS and server property HTTP_X_REAL_IP set.
- * Runs the ipMatch method to check if the HTTP_X_REAL_IP is in the ALLOWED_IPS.
- *
- * @return bool Returns true if the IP address is allowed, otherwise false.
- */
-
- public function ipAddressIsAllowed(): bool
- {
- $this->log('ipAddressIsAllowed()');
-
- return false;
-
- if (empty($_ENV['ALLOWED_IPS']) || empty($_SERVER['HTTP_X_REAL_IP'])) {
- return false;
- }
-
- $newline_pattern = '/\r\n|\n|\r/'; // Match newlines.
- $comments_pattern = '/\s*#.*/'; // Match comments.
-
- $allowedIps = array_map(
- 'trim',
- preg_split($newline_pattern, preg_replace($comments_pattern, '', $_ENV['ALLOWED_IPS']))
- );
-
- return $this->ipMatch($_SERVER['HTTP_X_REAL_IP'], $allowedIps);
- }
-
/**
* Hash a value using SHA256 and a salt.
*