Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow access to the intranet-archive via JWT. #797

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ To view the intranet content, visitors must meet one of the following criteria.

- Be in an Allow List of IP ranges.
- Or, have a Microsoft Azure account, within the organisation.
- Or, in the case of the intranet-archive scraper, have a valid JWT token.

The visitor's IP is checked first, then if that check fails, they are redirected to the project's Entra application.

Expand Down Expand Up @@ -546,6 +547,16 @@ This is for 2 reasons:
- It will keep the OAuth session fresh, the endpoint handler will refresh OAuth tokens, and update JWTs before they expire.
- If a visitor's state has changed, e.g. they have moved from an office with an allowed IP, then their browser content is blurred and they are prompted to refresh the page.

### Access for the Intranet Archive service.

The intranet-archive service is a scraper that collects content from the intranet for archiving purposes.

It is granted access via a JWT token, which is generated manually by running the `wp gen-jwt intranet-archive` command from an fpm container.

The cookie has a role of `intranet-archive`. For this roll to be granted access to the intranet, the request IP must be one of Cloud Platform's egress IPs.

When the JWT_SECRET is rotated, a new JWT token will need to be generated, and the Intranet Archive service will need to be updated with the new JWT.

<!-- License -->

[License Link]: https://github.com/ministryofjustice/intranet/blob/main/LICENSE 'License.'
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ip-ranges-configure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,16 @@ jobs:
# @see https://nginx.org/en/docs/http/ngx_http_geo_module.html
ALLOW_VALUE=1
DEPRI_VALUE=2
CLOUD_EGRES=5

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)
CLOUD_FORMATTED=$(yq 'explode(.) | .cloud_platform | flatten | map(. + " '$CLOUD_EGRES';") | join("\n")' moj-cidr-addresses.yml)

CONFIG=$(
echo "$ALLOW_FORMATTED"
echo "$DEPRI_FORMATTED"
echo "$CLOUD_FORMATTED"
)

CONFIG_ENCRYPTED=$(
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@
"php-http/guzzle7-adapter": "^1.0"
},
"require-dev": {
"squizlabs/php_codesniffer": "^3.0.2"
"squizlabs/php_codesniffer": "^3.0.2",
"php-stubs/wp-cli-stubs": "^2.11"
},
"extra": {
"installer-paths": {
Expand Down
94 changes: 93 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions deploy/config/local/nginx/server.conf
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ map $ip_group $ip_skip_oauth {
2 1; # Allowed IPs (depricating)
3 0; # Cloud Platform IPs
4 1; # Local IP
5 0; # Cloud Platform Egress IPs
default 0; # Default value
}

Expand Down Expand Up @@ -202,6 +203,7 @@ server {

# The subrequest handler, WordPress is not loaded in this file.
set $script_name /app/mu-plugins/moj-auth/verify.php;
fastcgi_param HTTP_X_MOJ_IP_GROUP $ip_group;
include /etc/nginx/php-fpm-auth.conf;
}

Expand Down
2 changes: 2 additions & 0 deletions deploy/config/server.conf
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ map $ip_group $ip_skip_oauth {
2 1; # Allowed IPs (depricating)
3 0; # Cloud Platform IPs
4 1; # Local IP (127.0.0.1)
5 0; # Cloud Platform Egress IPs
default 0; # Default value
}

Expand Down Expand Up @@ -217,6 +218,7 @@ server {

# The subrequest handler, WordPress is not loaded in this file.
set $script_name /app/mu-plugins/moj-auth/verify.php;
fastcgi_param HTTP_X_MOJ_IP_GROUP $ip_group;
include /etc/nginx/php-fpm-auth.conf;
}

Expand Down
2 changes: 1 addition & 1 deletion public/app/mu-plugins/moj-auth/401.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public function handle401Request(): void
$jwt->failed_callbacks = $jwt->failed_callbacks ?? 0;

// Set a JWT without a role, to persist the user's ID, login attempts and success_url.
$jwt = $this->setJwt($jwt);
[$jwt] = $this->setJwt($jwt);

$this->log('handle401Request failed_callbacks: ' . $jwt->failed_callbacks);

Expand Down
13 changes: 10 additions & 3 deletions public/app/mu-plugins/moj-auth/moj-auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
return;
}

require_once 'traits/cli.php';
require_once 'traits/jwt.php';
require_once 'traits/oauth.php';
require_once 'traits/utils.php';
Expand All @@ -39,6 +40,7 @@

class Auth
{
use AuthCli;
use AuthJwt;
use AuthOauth;
use AuthUtils;
Expand All @@ -61,6 +63,7 @@ public function __construct(array $args = [])
$this->debug = $args['debug'] ?? false;
$this->https = isset($_SERVER['HTTPS']) || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO']);

$this->initCli();
$this->initJwt();
$this->initOauth();
}
Expand All @@ -69,6 +72,10 @@ public function handleRequest(): void
{
$this->log('handleRequest()');

if (defined('WP_CLI') && WP_CLI) {
return;
}

if (!$this->oauth_action) {
return;
}
Expand All @@ -78,7 +85,7 @@ public function handleRequest(): void

// Set a JWT without a role, to persist the user's ID.
if (!$jwt) {
$jwt = $this->setJwt();
$this->setJwt();
}

if ('login' === $this->oauth_action) {
Expand Down Expand Up @@ -187,7 +194,7 @@ public function handleHeartbeatRequest(): void

// 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);
[$jwt] = $this->setJwt($jwt);
$mutated_jwt = false;
}

Expand Down Expand Up @@ -218,7 +225,7 @@ public function handleHeartbeatRequest(): void

// Set the JWT, if it's been mutated. Either by clearing properties, or it's been refreshed.
if ($mutated_jwt) {
$jwt = $this->setJwt($jwt);
$this->setJwt($jwt);
}

return;
Expand Down
66 changes: 66 additions & 0 deletions public/app/mu-plugins/moj-auth/traits/cli.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

/**
* This command is used to generate a JWT token for the intranet-archive service.
*
* To test this command locally you can run, first run `make bash` to enter the container.
* To run this command on a deployed container, first run `kubectl -n $NSP exec -it $POD -c fpm -- ash` to enter the container.
*
* Usage:
* run: wp gen-jwt intranet-archive
*/

namespace MOJ\Intranet;

use WP_CLI;

trait AuthCli
{
const GENERATED_JWT_DURATION = 60 * 60 * 24 * 365 * 3; // 3 years

/**
* Init the WP CLI command.
*
* @return void
*/
public function initCli() : void
{
// If the WP_CLI constant is defined and true, then we're running in the WP CLI environment.
if (defined('WP_CLI') && WP_CLI) {
// Register the command.
WP_CLI::add_command('gen-jwt', [$this, 'generateJwtCommand']);
}
}

/**
* Generate a JWT token for the intranet-archive service.
*
* @param array $args The arguments passed to the command.
* @return void
*/
public function generateJwtCommand(array $args): void
{
$this->log('in generateJwtCommand');

WP_CLI::log('GenerateJwt starting');

if (empty($args[0]) || $args[0] !== 'intranet-archive') {
WP_CLI::log('GenerateJwt the command was missing the role argument.');
return;
}

// Generate a JWT with the intranet-archive role and a long expiry.
[$jwt, $jwt_string] = $this->setJwt((object) [
'roles' => [$args[0]],
'expiry' => time() + self::GENERATED_JWT_DURATION
]);

// Log the JWT object, so the user can see the contents.
WP_CLI::log('JWT contents: ' . print_r($jwt, true));

// Log the JWT string, this should be copied by the user and can be set as a cookie with name `jwt`.
WP_CLI::log('JWT cookie: ' . $jwt_string);

WP_CLI::log('GenerateJwt complete');
}
}
Loading
Loading