Skip to content
Merged
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
22 changes: 22 additions & 0 deletions oidc/class/oidc.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ class OIDC extends FOGController
// application behind that provider. See the note on the column in
// OIDCManager::createSql().
'singleLogout' => 'opSingleLogout',
// Send the login page straight to this provider instead of showing
// FOG's own form (#17). Off by default; see the note on the column
// in OIDCManager::createSql() for why this one in particular.
'autoRedirect' => 'opAutoRedirect',
'icon' => 'opIcon'
];
/**
Expand Down Expand Up @@ -301,6 +305,24 @@ public static function postLogoutUri()
{
return self::absoluteUrl('management/login.php');
}
/**
* The URL that begins a sign-in with one provider.
*
* Absolute, because core's LOGIN_PAGE_REDIRECT seam refuses anything
* that is not an absolute http(s) URL -- what a hook left in a variable
* is not a good enough answer for a Location header. The login-page
* button uses a relative form of the same path and does not need this.
*
* @param int $id the provider id
*
* @return string
*/
public static function startUrl($id)
{
return self::absoluteUrl(
sprintf('ext/oidc/start?provider=%d', (int)$id)
);
}
/**
* An absolute https URL for a path inside this FOG install.
*
Expand Down
84 changes: 83 additions & 1 deletion oidc/class/oidcflow.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,79 @@ private static function _rememberLogout($provider, array $config, array $token)
'idToken' => (string)$token['id_token']
];
}
/**
* The one provider the login page must redirect to, or 0 for none.
*
* Shared by the login-page listener and the logout listener, because
* both have to know the same thing: whether landing on
* management/index.php would bounce the visitor straight to a provider.
*
* TWO providers flagged is refused rather than resolved. The login page
* cannot redirect to both, and silently picking one -- lowest id, first
* row, whatever -- hides a misconfiguration on the single page an admin
* is least able to debug, while sending everybody to a provider half of
* them may not have an account at. Refusing renders FOG's own form,
* which is a working login for everyone and visibly not what was asked
* for.
*
* The complaint goes to the error log and NOT to the page. This runs
* for an anonymous visitor, and "this server has two misconfigured
* identity providers" is not something to tell one.
*
* @return int the provider id, or 0
*/
public static function forcedProvider()
{
$ids = (array)Route::getIds(
'oidc',
['enabled' => [1], 'autoRedirect' => [1]]
);
if (count($ids) < 1) {
return 0;
}
if (count($ids) > 1) {
error_log(
sprintf(
'FOG OIDC: providers %s all have automatic redirect'
. ' enabled; the login page cannot redirect to more than'
. ' one, so it is showing the local form instead',
implode(', ', array_map('intval', $ids))
)
);
return 0;
}
return (int)reset($ids);
}
/**
* Where the login page should send an anonymous visitor, or ''.
*
* Consumed by core's LOGIN_PAGE_REDIRECT seam (fogproject#1175), which
* fires only for a visitor who is NOT signed in and only on the form
* render -- so this can neither bounce a working session nor interrupt
* the callback coming back from the provider.
*
* The row is re-read and re-checked rather than trusted from the id,
* for the same reason _enabledProvider() re-checks at the start of every
* flow: a provider disabled a moment ago must not still be receiving
* people.
*
* @return string
*/
public static function loginRedirectUrl()
{
$id = self::forcedProvider();
if ($id < 1) {
return '';
}
$provider = self::getClass('OIDC', $id);
if (!$provider->isValid()
|| '1' !== (string)$provider->get('enabled')
|| '1' !== (string)$provider->get('autoRedirect')
) {
return '';
}
return OIDC::startUrl($id);
}
/**
* The provider logout URL for this session, or '' for none.
*
Expand Down Expand Up @@ -1224,6 +1297,15 @@ private static function _fail($message)
{
self::_session();
self::setMessage($message, _('Sign-in failed'), 'error');
self::_redirect(OIDC::webrootBase() . 'management/index.php');
/*
* login.php, not index.php. On an install with automatic redirect
* on (#17), index.php sends the visitor straight back to the
* provider that just refused them -- which is an infinite redirect
* for a provider that is down, and an unreadable flash message even
* when it is not, because nothing renders between the two hops.
* login.php always renders FOG's own form (fogproject#1175), so the
* explanation is attached to a page that stays put.
*/
self::_redirect(OIDC::webrootBase() . 'management/login.php');
}
}
20 changes: 20 additions & 0 deletions oidc/class/oidcmanager.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public function createSql()
'opJITProvision',
'opAllowAPI',
'opSingleLogout',
'opAutoRedirect',
'opIcon'
],
[
Expand All @@ -71,6 +72,7 @@ public function createSql()
"ENUM('0', '1')",
"ENUM('0', '1')",
"ENUM('0', '1')",
"ENUM('0', '1')",
'VARCHAR(255)'
],
[
Expand All @@ -89,6 +91,7 @@ public function createSql()
false,
false,
false,
false,
false
],
[
Expand Down Expand Up @@ -133,6 +136,14 @@ public function createSql()
// session because somebody left FOG is a surprise that
// reaches applications FOG has nothing to do with.
"'0'",
// Sending everyone straight to this provider ships off, and
// it is the most dangerous switch in this plugin: an
// unconditional redirect to a provider that is unreachable,
// whose certificate expired, or whose issuer was mistyped
// takes the login form away from every administrator at
// once. management/login.php (fogproject#1175) is the way
// back, and the management page names it next to the box.
"'0'",
"'fa fa-id-badge'"
],
[
Expand All @@ -157,6 +168,7 @@ public function createSql()
false,
false,
false,
false,
false
],
'InnoDB',
Expand Down Expand Up @@ -216,6 +228,14 @@ function () {
// the column -- runs this harmlessly too.
"ALTER TABLE `OIDCProviders` ADD COLUMN `opSingleLogout` "
. "ENUM('0', '1') NOT NULL DEFAULT '0'",
// 7 - send the login page straight to this provider (#17).
// Appended for the same reason as step 6, and defaulting off for
// a sharper one: an install that upgraded into this switched ON
// would find its login form replaced by a redirect nobody asked
// for, and the only URL that still shows the form is one nobody
// has been told about yet.
"ALTER TABLE `OIDCProviders` ADD COLUMN `opAutoRedirect` "
. "ENUM('0', '1') NOT NULL DEFAULT '0'",
];
}
/**
Expand Down
95 changes: 95 additions & 0 deletions oidc/hooks/oidcloginredirect.hook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php
/**
* Sends the login page straight to the identity provider.
*
* PHP version 7.4+
*
* @category OIDCLoginRedirect
* @package FOGProject
* @author Tom Elliott <tommygunsster@gmail.com>
* @license http://opensource.org/licenses/gpl-3.0 GPLv3
* @link https://fogproject.org
*/
/**
* Sends the login page straight to the identity provider.
*
* @category OIDCLoginRedirect
* @package FOGProject
* @author Tom Elliott <tommygunsster@gmail.com>
* @license http://opensource.org/licenses/gpl-3.0 GPLv3
* @link https://fogproject.org
*/
class OIDCLoginRedirect extends Hook
{
/**
* The name of this hook.
*
* @var string
*/
public $name = 'OIDCLoginRedirect';
/**
* The description.
*
* @var string
*/
public $description = 'Send the login page to the identity provider.';
/**
* For posterity.
*
* @var bool
*/
public $active = true;
/**
* The node to work with.
*
* @var string
*/
public $node = 'oidc';
/**
* Initialize object.
*
* @return void
*/
public function __construct()
{
parent::__construct();
$this->registerInstalled([
['LOGIN_PAGE_REDIRECT', 'loginRedirect']
]);
}
/**
* Where an anonymous visitor goes instead of FOG's login form.
*
* For an install where everyone signs in through one provider, landing
* on a username and password box is a dead end: the accounts are at the
* provider and the box cannot accept them. This is the setting that
* removes the extra click.
*
* It is also the most dangerous setting in this plugin, and the design
* of the seam is what contains it. Core only offers LOGIN_PAGE_REDIRECT
* when FOG_LOCAL_LOGIN is undefined, so on management/login.php this
* method is never reached -- not consulted and overruled, never asked.
* That is what makes the escape hatch survive a provider whose
* certificate expired, whose issuer was mistyped, or which is simply
* switched off; and it also means a bug in this method cannot take that
* page down, because the page does not run it.
*
* https://<fog>/fog/management/login.php
*
* A provider that refuses a sign-in sends the browser to that same page
* rather than back to index.php (OIDCFlow::_fail()), so a provider that
* is down produces one error message rather than a redirect loop.
*
* @param mixed $arguments where to send the browser instead
*
* @return void
*/
public function loginRedirect($arguments)
{
$url = OIDCFlow::loginRedirectUrl();
if ('' === $url) {
return;
}
$arguments['redirect'] = $url;
}
}
16 changes: 16 additions & 0 deletions oidc/hooks/oidclogout.hook.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,22 @@ public function __construct()
public function providerLogout($arguments)
{
$url = OIDCFlow::logoutUrl();
if ('' === $url && OIDCFlow::forcedProvider() > 0) {
/*
* No provider logout to do, but this install sends its login
* page straight to a provider (#17) -- so core's default
* landing spot, management/index.php, would bounce the person
* who just signed out back to a provider whose SSO session is
* still alive, and sign them silently back in. "Log out" that
* leaves you logged in is worse than no logout at all.
*
* management/login.php is the one page that cannot do that.
* It does not end the provider session -- only single logout
* does -- but it leaves somebody looking at a form instead of
* back where they started.
*/
$url = OIDC::postLogoutUri();
}
if ('' === $url) {
return;
}
Expand Down
1 change: 1 addition & 0 deletions oidc/js/fog.oidc.export.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
{data: 'jitProvision', visible: false},
{data: 'allowapi', visible: false},
{data: 'singleLogout', visible: false},
{data: 'autoRedirect', visible: false},
{data: 'icon', visible: false}
]);
})(jQuery);
35 changes: 34 additions & 1 deletion oidc/pages/oidcmanagement.page.php
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,8 @@ function (&$serverFault) {
->set('enabled', '0')
->set('jitProvision', '0')
->set('allowapi', '0')
->set('singleLogout', '0');
->set('singleLogout', '0')
->set('autoRedirect', '0');
if (!$OIDC->save()) {
$serverFault = true;
throw new \Exception(_('Add provider failed!'));
Expand Down Expand Up @@ -498,6 +499,34 @@ public function oidcGeneral()
// the provider's error page instead of back at FOG. That looks
// like this plugin is broken, and the fix is a value an admin
// has to copy from somewhere.
self::makeLabel(
$this->_labelClass,
'autoRedirect',
_('Redirect Login To This Provider')
. '<br/>('
. sprintf(
// The escape hatch is named right here, on purpose. An
// admin who ticks this without knowing about login.php
// has one bad certificate between themselves and being
// locked out of their own server -- and the URL is not
// something they could guess at that point.
_('the local login form stays available at %s'),
'<code>' . Initiator::e(OIDC::postLogoutUri()) . '</code>'
)
. ')'
) => self::makeInput(
'',
'autoRedirect',
'',
'checkbox',
'autoRedirect',
'',
false,
false,
-1,
-1,
$checked('autoRedirect')
),
self::makeLabel(
$this->_labelClass,
'postLogoutUri',
Expand Down Expand Up @@ -602,6 +631,10 @@ public function oidcGeneralPost()
->set(
'singleLogout',
isset($_POST['singleLogout']) ? '1' : '0'
)
->set(
'autoRedirect',
isset($_POST['autoRedirect']) ? '1' : '0'
);

// The secret is only written when the admin actually typed one. An
Expand Down
Loading
Loading