diff --git a/README.md b/README.md
index 76efb0f90..cae9530cb 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ Unless otherwise noted, all code is released under the APACHE 2.0 License http:/
## Installation and further documentation
Detailed installation instructions for a virtual machine environment using Vagrant are located at:
-[Vagrant virtual machine installation](./installation.md)
+[Vagrant virtual machine installation](./installation.md)
Information for installation to a local machine environment can be found at:
@@ -41,7 +41,7 @@ configuration file for this should be located here
openstack/_config/cloudassets.yml
-and with following content
+and with following content
* https://docs.openstack.org/keystone/rocky/user/application_credentials.html
@@ -62,8 +62,8 @@ CloudAssets:
ApplicationCredentialId: application credential id
ApplicationCredentialSecret: application credential secret
ProjectName: your project name
- AuthURL: keystone base url
- LocalCopy: false
+ AuthURL: keystone base url
+ LocalCopy: false
````
## OIDC
@@ -73,19 +73,23 @@ settings for oidc configuration on _ss_environment.php file
````PHP
// OIDC
define('OIDC_CLIENT', '');
-
define('OIDC_CLIENT_SECRET', '');
+//set true on production mode, otherwise false
+define('OIDC_VERIFY_HOST',false);
+define('SESSION_CHECKER_OAUTH2_APP_CLIENT_ID', '');
+define('SESSION_CHECKER_OAUTH2_APP_CLIENT_SECRET', '');
//set true on production mode, otherwise false
-define('OIDC_VERIFY_HOST',false);
+define('SESSION_CHECKER_OAUTH2_APP_VERIFY_HOST', false);
+
````
on idp under allowed URIs you need to register following one
* https://hostname/openstackidauthenticator
-under security settings you need to set Id Token Signed Response Algorithm
+under security settings you need to set Id Token Signed Response Algorithm
diff --git a/composer.json b/composer.json
index 74b2d5163..011fca2b3 100644
--- a/composer.json
+++ b/composer.json
@@ -92,7 +92,8 @@
"php-opencloud/openstack": "dev-master",
"jumbojett/openid-connect-php": "dev-master",
"mikehaertl/phpwkhtmltopdf": "dev-master",
- "spatie/dropbox-api": "dev-master"
+ "spatie/dropbox-api": "dev-master",
+ "caseyamcl/guzzle_retry_middleware": "2.13.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0",
diff --git a/composer.lock b/composer.lock
index 512d58615..b1415c125 100644
--- a/composer.lock
+++ b/composer.lock
@@ -1,11 +1,84 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
- "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "588fea3911bc0293ed0a1a2e87f1ce79",
+ "content-hash": "abf949ebcacd420c81c40530c4f73ff3",
"packages": [
+ {
+ "name": "caseyamcl/guzzle_retry_middleware",
+ "version": "v2.13.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/caseyamcl/guzzle_retry_middleware.git",
+ "reference": "17c9299cde438b00bbeb099c6480319a81636a60"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/caseyamcl/guzzle_retry_middleware/zipball/17c9299cde438b00bbeb099c6480319a81636a60",
+ "reference": "17c9299cde438b00bbeb099c6480319a81636a60",
+ "shasum": ""
+ },
+ "require": {
+ "guzzlehttp/guzzle": "^6.3|^7.0",
+ "php": "^7.1|^8.0"
+ },
+ "require-dev": {
+ "jaschilz/php-coverage-badger": "^2.0",
+ "nesbot/carbon": "^2.0|^3.0",
+ "phpstan/extension-installer": "^1.0",
+ "phpstan/phpstan": "^1.0",
+ "phpunit/phpunit": "^7.5|^8.0|^9.0",
+ "squizlabs/php_codesniffer": "^3.5",
+ "symfony/var-dumper": "^5.0|^6.0|^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleRetry\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Casey McLaughlin",
+ "email": "caseyamcl@gmail.com",
+ "homepage": "https://caseymclaughlin.com",
+ "role": "Developer"
+ }
+ ],
+ "description": "Guzzle v6+ retry middleware that handles 429/503 status codes and connection timeouts",
+ "homepage": "https://github.com/caseyamcl/guzzle_retry_middleware",
+ "keywords": [
+ "Guzzle",
+ "back-off",
+ "caseyamcl",
+ "guzzle_retry_middleware",
+ "middleware",
+ "retry",
+ "retry-after"
+ ],
+ "support": {
+ "issues": "https://github.com/caseyamcl/guzzle_retry_middleware/issues",
+ "source": "https://github.com/caseyamcl/guzzle_retry_middleware/tree/v2.13.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/caseyamcl",
+ "type": "github"
+ }
+ ],
+ "time": "2025-07-11T12:33:22+00:00"
+ },
{
"name": "composer/installers",
"version": "dev-master",
@@ -1480,11 +1553,6 @@
"reference": "38654be8419d4abb59114ff6b9982fb094406bff",
"shasum": ""
},
- "archive": {
- "exclude": [
- ".*"
- ]
- },
"require": {
"ext-curl": "*",
"ext-json": "*",
@@ -1497,6 +1565,11 @@
"src/"
]
},
+ "archive": {
+ "exclude": [
+ ".*"
+ ]
+ },
"license": [
"Apache-2.0"
],
@@ -6089,25 +6162,26 @@
"aliases": [],
"minimum-stability": "dev",
"stability-flags": {
- "unclecheese/dropzone": 20,
- "unclecheese/bootstrap-tagfield": 20,
- "silverstripe/gridfieldajaxrefresh": 20,
- "tractorcow/silverstripe-colorpicker": 20,
- "tractorcow/silverstripe-opengraph": 20,
- "nnnick/chartjs": 20,
- "openid/php-openid": 20,
"google/recaptcha": 20,
- "smarcet/silverstripe-cloudassets-swift": 20,
- "markguinn/silverstripe-cloudassets": 20,
- "php-opencloud/openstack": 20,
"jumbojett/openid-connect-php": 20,
+ "markguinn/silverstripe-cloudassets": 20,
"mikehaertl/phpwkhtmltopdf": 20,
- "spatie/dropbox-api": 20
+ "nnnick/chartjs": 20,
+ "openid/php-openid": 20,
+ "php-opencloud/openstack": 20,
+ "silverstripe/gridfieldajaxrefresh": 20,
+ "smarcet/silverstripe-cloudassets-swift": 20,
+ "spatie/dropbox-api": 20,
+ "tractorcow/silverstripe-colorpicker": 20,
+ "tractorcow/silverstripe-opengraph": 20,
+ "unclecheese/bootstrap-tagfield": 20,
+ "unclecheese/dropzone": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "^7.1.3"
},
- "platform-dev": []
+ "platform-dev": {},
+ "plugin-api-version": "2.6.0"
}
diff --git a/openstack/code/Page.php b/openstack/code/Page.php
index 1097f949f..719582c9a 100644
--- a/openstack/code/Page.php
+++ b/openstack/code/Page.php
@@ -137,7 +137,7 @@ public static function IconShortCodeHandler($arguments, $caption = null, $parser
//return the customized template
return $template->process(new ArrayData($customise));
- }
+ }
function requireDefaultRecords()
@@ -274,6 +274,22 @@ public function getTime()
return time();
}
+ public function getSecurityToken()
+ {
+ return SecurityToken::inst() ? str_replace('"', '\\"', SecurityToken::inst()->getValue()) : null;
+ }
+
+ public function getIsSSOBootstrapEnabled()
+ {
+ $enabled = (bool) (defined('SHELL_SSO_BOOTSTRAP_ENABLED') and SHELL_SSO_BOOTSTRAP_ENABLED);
+ return $enabled and Member::currentUserID() !== null;
+ }
+
+ public function getMemberIsLoggedIn()
+ {
+ return Member::currentUserID() !== null;
+ }
+
protected function CustomScripts()
{
$js_files = [
diff --git a/openstack/code/utils/apis/AbstractRestfulJsonApi.php b/openstack/code/utils/apis/AbstractRestfulJsonApi.php
index 41ea3fb24..1ac820db1 100644
--- a/openstack/code/utils/apis/AbstractRestfulJsonApi.php
+++ b/openstack/code/utils/apis/AbstractRestfulJsonApi.php
@@ -11,8 +11,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
-use Doctrine\Common\Annotations\AnnotationReader;
use Openstack\Annotations\CachedMethod;
+use Doctrine\Common\Annotations\AnnotationReader;
/**
* Class AbstractRestfulJsonApi
*/
@@ -521,6 +521,19 @@ protected function published()
return $response;
}
+ /**
+ * @return SS_HTTPResponse
+ */
+ protected function noContent(): SS_HTTPResponse
+ {
+ $response = new SS_HTTPResponse();
+ $response->setStatusCode(204);
+ $response->addHeader('Content-Type', 'application/json');
+ $response->setBody('');
+
+ return $response;
+ }
+
/**
* @return SS_HTTPResponse
*/
@@ -550,10 +563,10 @@ public function forbiddenError()
* @param $messages
* @return SS_HTTPResponse
*/
- public function validationError($messages)
+ public function validationError($messages, $code = 412)
{
$response = new SS_HTTPResponse();
- $response->setStatusCode(412);
+ $response->setStatusCode($code);
$response->addHeader('Content-Type', 'application/json');
if (!is_array($messages)) {
$messages = [['message' => $messages]];
@@ -579,6 +592,21 @@ protected function created($id)
return $response;
}
+ /**
+ * @return SS_HTTPResponse
+ */
+ public function badRequest($message = "Bad Request")
+ {
+ $response = new SS_HTTPResponse();
+ $response->setStatusCode(400);
+ $response->addHeader('Content-Type', 'application/json');
+ $response->setBody(json_encode(
+ ['error' => 'validation', 'messages' => [['message' => $message]]]
+ ));
+
+ return $response;
+ }
+
/**
* @return SS_HTTPResponse
diff --git a/openstackid/_config/routes.yml b/openstackid/_config/routes.yml
index 9379bbb60..4adedd4f0 100644
--- a/openstackid/_config/routes.yml
+++ b/openstackid/_config/routes.yml
@@ -4,4 +4,5 @@ Name: openstackidroutes
Director:
rules:
'OpenStackIdAuthenticator': 'OpenStackIdAuthenticator'
+ 'oidc/session/bootstrap': 'OIDCSessionBootstrapApi'
diff --git a/openstackid/code/interfaces/restfull_api/Libs/OAuth2/InvalidGrantTypeException.php b/openstackid/code/interfaces/restfull_api/Libs/OAuth2/InvalidGrantTypeException.php
new file mode 100644
index 000000000..90292de60
--- /dev/null
+++ b/openstackid/code/interfaces/restfull_api/Libs/OAuth2/InvalidGrantTypeException.php
@@ -0,0 +1,31 @@
+ self::OAuth2Protocol_ResponseType_Code,
+ self::OAuth2Protocol_ResponseType_Token => self::OAuth2Protocol_ResponseType_Token
+ );
+ public static $protocol_definition = array(
+ self::OAuth2Protocol_ResponseType => self::OAuth2Protocol_ResponseType,
+ self::OAuth2Protocol_ClientId => self::OAuth2Protocol_ClientId,
+ self::OAuth2Protocol_RedirectUri => self::OAuth2Protocol_RedirectUri,
+ self::OAuth2Protocol_Scope => self::OAuth2Protocol_Scope,
+ self::OAuth2Protocol_State => self::OAuth2Protocol_State
+ );
+
+}
\ No newline at end of file
diff --git a/openstackid/code/interfaces/restfull_api/OIDCSessionBootstrapApi.php b/openstackid/code/interfaces/restfull_api/OIDCSessionBootstrapApi.php
new file mode 100644
index 000000000..381ee3d2a
--- /dev/null
+++ b/openstackid/code/interfaces/restfull_api/OIDCSessionBootstrapApi.php
@@ -0,0 +1,383 @@
+ 'index',
+ ];
+
+ /**
+ * @var array
+ */
+ private static $allowed_actions = [
+ 'index',
+ ];
+
+ /**
+ * @return bool
+ */
+ protected function isApiCall()
+ {
+ $request = $this->getRequest();
+ if (is_null($request))
+ return false;
+ return true;
+ }
+
+ /**
+ * @return bool
+ */
+ protected function authorize()
+ {
+ return true;
+ }
+
+ /**
+ * @return bool
+ */
+ protected function authenticate()
+ {
+ return true;
+ }
+
+ /**
+ * Bootstrap OIDC session
+ * @param SS_HTTPRequest $request
+ * @return SS_HTTPResponse
+ */
+ public function index(SS_HTTPRequest $request)
+ {
+ try {
+ $response = $this->validateRequestData($request);
+ if ($response instanceof SS_HTTPResponse) {
+ return $response;
+ }
+ $accessToken = &$response;
+
+ try {
+ $tokenData = $this->doIntrospectionRequest($accessToken);
+
+ if ($tokenData instanceof SS_HTTPResponse) {
+ return $tokenData;
+ }
+
+ if ($tokenData === false) {
+ SS_Log::log(sprintf("%s token %s introspection returned false", __METHOD__, $accessToken), SS_Log::WARN);
+ }
+
+ if (
+ $tokenData === false or
+ (is_array($tokenData) and isset($tokenData['error']))
+ ) {
+ $error = 'Invalid or expired access token';
+ if (is_array($tokenData) and !empty($tokenData['error'])) {
+ $error = $tokenData['error'];
+ }
+ return $this->validationError([$error], 400);
+ }
+
+ // at this point we have a valid and active token
+ // we can create a session for the user
+ $member = null;
+
+ if (!$member && $tokenData['user_external_id']) {
+ $member = Member::get()->filter('ID', $tokenData['user_external_id'])->first();
+ }
+
+ if (!$member && $tokenData['user_email']) {
+ $member = Member::get()->filter('Email', $tokenData['user_email'])->first();
+ }
+
+ if (!$member) {
+ return $this->validationError(['User not found'], 404);
+ }
+ // log in the user
+ $member->logIn();
+ SS_Log::log(
+ sprintf('OIDC session bootstrap successful for token: %s', substr($accessToken, 0, 10) . '...'),
+ SS_Log::DEBUG
+ );
+
+ return $this->noContent();
+ } catch (Exception $ex) {
+ return $this->validationError([$ex->getMessage()], 400);
+ }
+
+ } catch (EntityValidationException $ex1) {
+ SS_Log::log($ex1, SS_Log::WARN);
+ return $this->validationError($ex1->getMessages());
+ } catch (Exception $ex) {
+ SS_Log::log($ex, SS_Log::ERR);
+ return $this->serverError();
+ }
+ }
+
+ /**
+ * Validate request data
+ * @param array $data
+ * @return array|SS_HTTPResponse
+ */
+ private function validateRequestData(SS_HTTPRequest $request)
+ {
+ // Check if request method is POST
+ if (!$request->isPOST()) {
+ return $this->methodNotAllowed();
+ }
+ // Check Authorization header
+ $authHeader = $request->getHeader('Authorization');
+ $accessToken = str_replace('Bearer ', '', $authHeader ?: '');
+ if (empty($authHeader) || strpos($authHeader, 'Bearer ') !== 0) {
+ return $this->badRequest('Authorization header with Bearer token is required');
+ }
+
+ // Check X-CSRF-Token header
+ $csrfToken = $request->getHeader('X-CSRF-Token') ?: $request->getHeader('X-Csrf-Token');
+ if (empty($csrfToken)) {
+ return $this->badRequest('X-CSRF-Token header is required');
+ }
+
+ // Check X-CSRF-Token header value
+ if ($csrfToken !== $this->getSecurityToken()) {
+ return $this->badRequest('X-CSRF-Token header is invalid');
+ }
+
+ // Check Sec-Fetch-Site header
+ $SecFetchSite = $request->getHeader('Sec-Fetch-Site') ?: "";
+ if ($SecFetchSite !== 'same-origin') {
+ return $this->badRequest('Sec-Fetch-Site header is required and must be same-origin');
+ }
+
+ // Check Referer and Origin headers for same-origin
+ $origin = $request->getHeader('Origin') ?: "";
+ $referer = $request->getHeader('Referer') ?: "";
+
+ if (empty($referer) || empty($origin)) {
+ return $this->badRequest('same-origin is in place, Referer and Origin headers must be present');
+ }
+
+ // Parse origins from both URLs
+ $originParsed = parse_url($origin);
+ $refererParsed = parse_url($referer);
+
+ if (!$originParsed || !$refererParsed) {
+ return $this->badRequest('Invalid Origin or Referer URL format');
+ }
+
+ // Compare scheme, host, and port for same-origin check
+ $originScheme = $originParsed['scheme'] ?? '';
+ $originHost = $originParsed['host'] ?? '';
+ $originPort = $originParsed['port'] ?? ($originScheme === 'https' ? 443 : 80);
+
+ $refererScheme = $refererParsed['scheme'] ?? '';
+ $refererHost = $refererParsed['host'] ?? '';
+ $refererPort = $refererParsed['port'] ?? ($refererScheme === 'https' ? 443 : 80);
+
+ if ($originScheme !== $refererScheme || $originHost !== $refererHost || $originPort !== $refererPort) {
+ SS_Log::log(
+ sprintf(
+ 'Same-origin check failed: Origin=%s (scheme=%s, host=%s, port=%d) vs Referer=%s (scheme=%s, host=%s, port=%d)',
+ $origin,
+ $originScheme,
+ $originHost,
+ $originPort,
+ $referer,
+ $refererScheme,
+ $refererHost,
+ $refererPort
+ ),
+ SS_Log::DEBUG
+ );
+ return $this->badRequest('same-origin check failed: Origin and Referer headers must have the same origin');
+ }
+
+ return $accessToken;
+ }
+
+ /**
+ * Make a dummy call to an external server
+ * @param string $token_value
+ * @param array $data
+ * @return array|bool|SS_HTTPResponse
+ */
+ private function doIntrospectionRequest(string $token_value)
+ {
+ try {
+ SS_Log::log(sprintf(__METHOD__ . " token %s", $token_value), SS_Log::DEBUG);
+
+ $client_id = defined('SESSION_CHECKER_OAUTH2_APP_CLIENT_ID') ? SESSION_CHECKER_OAUTH2_APP_CLIENT_ID : '';
+ $client_secret = defined('SESSION_CHECKER_OAUTH2_APP_CLIENT_SECRET') ? SESSION_CHECKER_OAUTH2_APP_CLIENT_SECRET : '';
+ $auth_server_url = defined('IDP_OPENSTACKID_URL') ? IDP_OPENSTACKID_URL : '';
+ $verify = defined('SESSION_CHECKER_OAUTH2_APP_VERIFY_HOST') ? SESSION_CHECKER_OAUTH2_APP_VERIFY_HOST : false;
+
+ if (empty($client_id)) {
+ return $this->validationError('SESSION_CHECKER_OAUTH2_APP_CLIENT_ID is not configured');
+ }
+
+ if (empty($client_secret)) {
+ return $this->validationError('SESSION_CHECKER_OAUTH2_APP_CLIENT_SECRET is not configured');
+ }
+
+ if (empty($auth_server_url)) {
+ return $this->validationError('IDP_OPENSTACKID_URL is not configured');
+ }
+
+ $stack = HandlerStack::create();
+ $stack->push(GuzzleRetryMiddleware::factory());
+
+ $client = new Client([
+ 'base_uri' => $auth_server_url,
+ 'handler' => $stack,
+ 'verify' => $verify,
+ 'timeout' => 120,
+ ]);
+
+ // http://docs.guzzlephp.org/en/stable/request-options.html
+ $options = [
+ 'form_params' => ['token' => $token_value],
+ 'auth' => [$client_id, $client_secret],
+ 'http_errors' => true,
+ 'synchronous' => true,
+ ];
+ $endpoint = "/oauth2/token/introspection";
+ SS_Log::log(sprintf(__METHOD__ . " DEBUG options %s", json_encode($options)), SS_Log::DEBUG);
+ $response = $client->request('POST', $endpoint, $options);
+
+ $content_type = $response->getHeaderLine('content-type');
+ $is_json = in_array("application/json", explode(';', $content_type));
+ $body = $response->getBody()->getContents();
+ if (!$is_json) {
+ // invalid content type
+ $status = $response->getStatusCode();
+ SS_Log::log(sprintf(__METHOD__ . " status %s content type %s body %s", $status, $content_type, $body), SS_Log::WARN);
+ return $this->validationError($body, 500);
+ }
+ SS_Log::log(sprintf(__METHOD__ . " DEBUG response %s", $body), SS_Log::DEBUG);
+
+ $jsonResponse = json_decode($body, true);
+ if (!$jsonResponse) {
+ SS_Log::log(sprintf(__METHOD__ . " invalid JSON response %s", $body), SS_Log::ERR);
+ return $this->validationError('Invalid JSON response from IDP', 500);
+ }
+
+ SS_Log::log(sprintf(__METHOD__ . " DEBUG JSON RESPONSE %s", json_encode($jsonResponse, JSON_PRETTY_PRINT)), SS_Log::DEBUG);
+ $defaults = array_fill_keys(["error", "user_external_id", "user_identifier", "user_email"], null);
+ $defaults['active'] = false;
+ $jsonResponse = array_merge($defaults, $jsonResponse);
+
+ $json = [
+ 'user_external_id' => $jsonResponse['user_external_id'],
+ 'user_email' => $jsonResponse['user_email'] ?: $jsonResponse['user_identifier'],
+ ];
+ SS_Log::log(sprintf(__METHOD__ . " DEBUG JSON RETURN %s", json_encode($json, JSON_PRETTY_PRINT)), SS_Log::DEBUG);
+ return $json;
+
+ } catch (RequestException $ex) {
+ $this->handleInstropectionException($ex, $token_value);
+ } catch (Exception $ex) {
+ SS_Log::log($ex, SS_Log::ERR);
+
+ $data = [
+ "error" => "Server Error",
+ "message" => $ex->getMessage(),
+ ];
+ if (defined('SS_ENVIRONMENT_TYPE') && SS_ENVIRONMENT_TYPE !== 'production') {
+ $data['trace'] = $ex->getTrace();
+ }
+ $response = new SS_HTTPResponse();
+ $response->setStatusCode(500);
+ $response->addHeader('Content-Type', 'application/json');
+ $response->setBody(json_encode($data));
+ return $response;
+ }
+
+ return false;
+ }
+
+ protected function handleInstropectionException(RequestException &$ex, string $token_value)
+ {
+ SS_Log::log($ex, SS_Log::WARN);
+ $response = $ex->getResponse();
+
+ if (is_null($response))
+ throw new Exception(sprintf('http code %s', $ex->getCode()));
+
+ $content_type = $response->getHeaderLine('content-type');
+ $is_json = in_array("application/json", explode(';', $content_type));
+ $body = $response->getBody()->getContents();
+ $code = $response->getStatusCode();
+
+ if ($is_json) {
+ $body = json_decode($body, true);
+ }
+ SS_Log::log("Error Response:", SS_Log::WARN, $body);
+
+ $invalid = [
+ OAuth2Protocol::OAuth2Protocol_Error_InvalidToken,
+ OAuth2Protocol::OAuth2Protocol_Error_InvalidGrant
+ ];
+
+ if ($code === 400 && $is_json && isset($body['error']) && in_array($body['error'], $invalid)) {
+ SS_Log::log(sprintf("%s token %s invalid (400 %s)", __METHOD__, $token_value, $body['error']), SS_Log::WARN);
+ throw new InvalidGrantTypeException($body['error']);
+ }
+
+ if ($code == 503) {
+ SS_Log::log(sprintf("%s token %s invalid (503 offline IDP)", __METHOD__, $token_value), SS_Log::WARN);
+ throw new InvalidGrantTypeException(OAuth2Protocol::OAuth2Protocol_Error_InvalidToken);
+ }
+ SS_Log::log(sprintf("%s token %s OAuth2InvalidIntrospectionResponse (%s %s)", __METHOD__, $token_value, $ex->getCode(), $body), SS_Log::WARN);
+ throw new OAuth2InvalidIntrospectionResponse(sprintf('http code %s - body %s', $code, $body));
+ }
+
+ /**
+ * Return method not allowed response
+ * @param string $message
+ * @return SS_HTTPResponse
+ */
+ protected function methodNotAllowed()
+ {
+ return parent::methodNotAllowed()
+ ->setBody(json_encode("Only POST requests are allowed"))
+ ->addHeader('Allow', 'POST');
+ }
+
+ protected function getSecurityToken()
+ {
+ return SecurityToken::inst() ? SecurityToken::inst()->getValue() : null;
+ }
+}
\ No newline at end of file
diff --git a/sample._ss_environment.php b/sample._ss_environment.php
index 7cc1cf59d..3b6015650 100644
--- a/sample._ss_environment.php
+++ b/sample._ss_environment.php
@@ -1,12 +1,12 @@
+
+<% end_if %>
\ No newline at end of file
diff --git a/themes/openstack/templates/Page.ss b/themes/openstack/templates/Page.ss
index 3c16e53b8..f82667ae7 100644
--- a/themes/openstack/templates/Page.ss
+++ b/themes/openstack/templates/Page.ss
@@ -32,6 +32,7 @@
<% include Page_GoogleAnalytics %>
<% include Page_MicrosoftAdvertising %>
<% include Page_LinkedinInsightTracker %>
+ <% include Page_LoggedInVerification %>