# Mail configuration
# Refer to https://www.bookstackapp.com/docs/admin/email-webhooks/#email-configuration
MAIL_DRIVER=smtp
-MAIL_FROM=mail@bookstackapp.com
+MAIL_FROM=bookstack@example.com
MAIL_FROM_NAME=BookStack
MAIL_HOST=localhost
OIDC_REMOVE_FROM_GROUPS=false
OIDC_EXTERNAL_ID_CLAIM=sub
+ # OIDC Logout Feature: Its value should be value of end_session_endpoint from <issuer>/.well-known/openid-configuration
+ OIDC_END_SESSION_ENDPOINT=null
+
+
# Disable default third-party services such as Gravatar and Draw.IO
# Service-specific options will override this option
DISABLE_EXTERNAL_SERVICES=false
# Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured.
ALLOWED_IFRAME_SOURCES="https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com"
+# A list of the sources/hostnames that can be reached by application SSR calls.
+# This is used wherever users can provide URLs/hosts in-platform, like for webhooks.
+# Host-specific functionality (usually controlled via other options) like auth
+# or user avatars for example, won't use this list.
+# Space seperated if multiple. Can use '*' as a wildcard.
+# Values will be compared prefix-matched, case-insensitive, against called SSR urls.
+# Defaults to allow all hosts.
+ALLOWED_SSR_HOSTS="*"
+
# The default and maximum item-counts for listing API requests.
API_DEFAULT_ITEM_COUNT=100
API_MAX_ITEM_COUNT=500
use BookStack\Exceptions\StoppedAuthenticationException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Facades\Theme;
+use BookStack\Http\HttpRequestService;
use BookStack\Theming\ThemeEvents;
use BookStack\Users\Models\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
-use Psr\Http\Client\ClientInterface as HttpClient;
/**
* Class OpenIdConnectService
public function __construct(
protected RegistrationService $registrationService,
protected LoginService $loginService,
- protected HttpClient $httpClient,
+ protected HttpRequestService $http,
protected GroupSyncService $groupService
) {
}
// Run discovery
if ($config['discover'] ?? false) {
try {
- $settings->discoverFromIssuer($this->httpClient, Cache::store(null), 15);
+ $settings->discoverFromIssuer($this->http->buildClient(5), Cache::store(null), 15);
} catch (OidcIssuerDiscoveryException $exception) {
throw new OidcException('OIDC Discovery Error: ' . $exception->getMessage());
}
protected function getProvider(OidcProviderSettings $settings): OidcOAuthProvider
{
$provider = new OidcOAuthProvider($settings->arrayForProvider(), [
- 'httpClient' => $this->httpClient,
+ 'httpClient' => $this->http->buildClient(5),
'optionProvider' => new HttpBasicAuthOptionProvider(),
]);
*/
protected function getUserDisplayName(OidcIdToken $token, string $defaultValue): string
{
- $displayNameAttr = $this->config()['display_name_claims'];
+ $displayNameAttrString = $this->config()['display_name_claims'] ?? '';
+ $displayNameAttrs = explode('|', $displayNameAttrString);
$displayName = [];
- foreach ($displayNameAttr as $dnAttr) {
+ foreach ($displayNameAttrs as $dnAttr) {
$dnComponent = $token->getClaim($dnAttr) ?? '';
if ($dnComponent !== '') {
$displayName[] = $dnComponent;
$settings->keys,
);
+ // OIDC Logout Feature: Temporarily save token in session
+ $access_token_for_logout = $idTokenText;
+ session()->put("oidctoken", $access_token_for_logout);
+
+
+
$returnClaims = Theme::dispatch(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $idToken->getAllClaims(), [
'access_token' => $accessToken->getToken(),
'expires_in' => $accessToken->getExpires(),
{
return $this->config()['user_to_groups'] !== false;
}
+
+
+ /**
+ * OIDC Logout Feature: Initiate a logout flow.
+ *
+ * @throws OidcException
+ *
+ * @return string
+ */
+ public function logout() {
+
+ $config = $this->config();
+ $app_url = env('APP_URL', '');
+ $end_session_endpoint = $config["end_session_endpoint"];
+
+ $oidctoken = session()->get("oidctoken");
+ session()->invalidate();
+
+ if (str_contains($app_url, 'https://')) {
+ $protocol = 'https://';
+ } else {
+ $protocol = 'http://';
+ }
+
+
+
+ return redirect($end_session_endpoint.'?id_token_hint='.$oidctoken."&post_logout_redirect_uri=".$protocol.$_SERVER['HTTP_HOST']."/");
+
+
+ }
+
+
+
}
'dump_user_details' => env('OIDC_DUMP_USER_DETAILS', false),
// Claim, within an OpenId token, to find the user's display name
- 'display_name_claims' => explode('|', env('OIDC_DISPLAY_NAME_CLAIMS', 'name')),
+ 'display_name_claims' => env('OIDC_DISPLAY_NAME_CLAIMS', 'name'),
// Claim, within an OpenID token, to use to connect a BookStack user to the OIDC user.
'external_id_claim' => env('OIDC_EXTERNAL_ID_CLAIM', 'sub'),
'groups_claim' => env('OIDC_GROUPS_CLAIM', 'groups'),
// When syncing groups, remove any groups that no longer match. Otherwise sync only adds new groups.
'remove_from_groups' => env('OIDC_REMOVE_FROM_GROUPS', false),
+
+ // OIDC Logout Feature: OAuth2 end_session_endpoint
+ 'end_session_endpoint' => env('OIDC_END_SESSION_ENDPOINT', null),
+
];
+
--- /dev/null
- <form action="{{ url(config('auth.method') === 'saml2' ? '/saml2/logout' : '/logout') }}"
- method="post">
- {{ csrf_field() }}
- <button class="icon-item" data-shortcut="logout">
- @icon('logout')
- <div>{{ trans('auth.logout') }}</div>
- </button>
- </form>
+<div class="dropdown-container" component="dropdown" option:dropdown:bubble-escapes="true">
+ <span class="user-name py-s hide-under-l" refs="dropdown@toggle"
+ aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('common.profile_menu') }}" tabindex="0">
+ <img class="avatar" src="{{$user->getAvatar(30)}}" alt="{{ $user->name }}">
+ <span class="name">{{ $user->getShortName(9) }}</span> @icon('caret-down')
+ </span>
+ <ul refs="dropdown@menu" class="dropdown-menu" role="menu">
+ <li>
+ <a href="{{ url('/favourites') }}" data-shortcut="favourites_view" class="icon-item">
+ @icon('star')
+ <div>{{ trans('entities.my_favourites') }}</div>
+ </a>
+ </li>
+ <li>
+ <a href="{{ $user->getProfileUrl() }}" data-shortcut="profile_view" class="icon-item">
+ @icon('user')
+ <div>{{ trans('common.view_profile') }}</div>
+ </a>
+ </li>
+ <li>
+ <a href="{{ url('/my-account') }}" class="icon-item">
+ @icon('user-preferences')
+ <div>{{ trans('preferences.my_account') }}</div>
+ </a>
+ </li>
+ <li><hr></li>
+ <li>
+ @include('common.dark-mode-toggle', ['classes' => 'icon-item'])
+ </li>
+ <li><hr></li>
+ <li>
++ <?php
++// OIDC Logout Feature: Use /oidc/logout if authentication method is oidc.
++ if (config('auth.method') === 'oidc') {
++ ?>
++ <form action="/oidc/logout"
++ method="get">
++ <?php
++// OIDC Logout Feature: Use /oidc/logout if authentication method is oidc.
++ } else {
++ ?>
++ <form action="{{ url(config('auth.method') === 'saml2' ? '/saml2/logout' : '/logout') }}"
++ method="post">
++ <?php
++// OIDC Logout Feature: Use /oidc/logout if authentication method is oidc.
++ }
++ ?>
++ {{ csrf_field() }}
++ <button class="icon-item" data-shortcut="logout">
++ @icon('logout')
++ <div>{{ trans('auth.logout') }}</div>
++ </button>
++ </form>
+ </li>
+ </ul>
+</div>
Route::get('/status', [SettingControllers\StatusController::class, 'show']);
Route::get('/robots.txt', [HomeController::class, 'robots']);
Route::get('/favicon.ico', [HomeController::class, 'favicon']);
+Route::get('/manifest.json', [HomeController::class, 'pwaManifest']);
// Authenticated routes...
Route::middleware('auth')->group(function () {
Route::post('/images/drawio', [UploadControllers\DrawioImageController::class, 'create']);
Route::get('/images/edit/{id}', [UploadControllers\ImageController::class, 'edit']);
Route::put('/images/{id}/file', [UploadControllers\ImageController::class, 'updateFile']);
+ Route::put('/images/{id}/rebuild-thumbnails', [UploadControllers\ImageController::class, 'rebuildThumbnails']);
Route::put('/images/{id}', [UploadControllers\ImageController::class, 'update']);
Route::delete('/images/{id}', [UploadControllers\ImageController::class, 'destroy']);
Route::put('/settings/users/{id}', [UserControllers\UserController::class, 'update']);
Route::delete('/settings/users/{id}', [UserControllers\UserController::class, 'destroy']);
- // User Preferences
- Route::get('/preferences', [UserControllers\UserPreferencesController::class, 'index']);
- Route::get('/preferences/shortcuts', [UserControllers\UserPreferencesController::class, 'showShortcuts']);
- Route::put('/preferences/shortcuts', [UserControllers\UserPreferencesController::class, 'updateShortcuts']);
- Route::get('/preferences/notifications', [UserControllers\UserPreferencesController::class, 'showNotifications']);
- Route::put('/preferences/notifications', [UserControllers\UserPreferencesController::class, 'updateNotifications']);
+ // User Account
+ Route::get('/my-account', [UserControllers\UserAccountController::class, 'redirect']);
+ Route::get('/my-account/profile', [UserControllers\UserAccountController::class, 'showProfile']);
+ Route::put('/my-account/profile', [UserControllers\UserAccountController::class, 'updateProfile']);
+ Route::get('/my-account/shortcuts', [UserControllers\UserAccountController::class, 'showShortcuts']);
+ Route::put('/my-account/shortcuts', [UserControllers\UserAccountController::class, 'updateShortcuts']);
+ Route::get('/my-account/notifications', [UserControllers\UserAccountController::class, 'showNotifications']);
+ Route::put('/my-account/notifications', [UserControllers\UserAccountController::class, 'updateNotifications']);
+ Route::get('/my-account/auth', [UserControllers\UserAccountController::class, 'showAuth']);
+ Route::put('/my-account/auth/password', [UserControllers\UserAccountController::class, 'updatePassword']);
+ Route::get('/my-account/delete', [UserControllers\UserAccountController::class, 'delete']);
+ Route::delete('/my-account', [UserControllers\UserAccountController::class, 'destroy']);
+
+ // User Preference Endpoints
Route::patch('/preferences/change-view/{type}', [UserControllers\UserPreferencesController::class, 'changeView']);
Route::patch('/preferences/change-sort/{type}', [UserControllers\UserPreferencesController::class, 'changeSort']);
Route::patch('/preferences/change-expansion/{type}', [UserControllers\UserPreferencesController::class, 'changeExpansion']);
Route::patch('/preferences/toggle-dark-mode', [UserControllers\UserPreferencesController::class, 'toggleDarkMode']);
Route::patch('/preferences/update-code-language-favourite', [UserControllers\UserPreferencesController::class, 'updateCodeLanguageFavourite']);
- Route::patch('/preferences/update-boolean', [UserControllers\UserPreferencesController::class, 'updateBooleanPreference']);
// User API Tokens
- Route::get('/settings/users/{userId}/create-api-token', [UserApiTokenController::class, 'create']);
- Route::post('/settings/users/{userId}/create-api-token', [UserApiTokenController::class, 'store']);
- Route::get('/settings/users/{userId}/api-tokens/{tokenId}', [UserApiTokenController::class, 'edit']);
- Route::put('/settings/users/{userId}/api-tokens/{tokenId}', [UserApiTokenController::class, 'update']);
- Route::get('/settings/users/{userId}/api-tokens/{tokenId}/delete', [UserApiTokenController::class, 'delete']);
- Route::delete('/settings/users/{userId}/api-tokens/{tokenId}', [UserApiTokenController::class, 'destroy']);
+ Route::get('/api-tokens/{userId}/create', [UserApiTokenController::class, 'create']);
+ Route::post('/api-tokens/{userId}/create', [UserApiTokenController::class, 'store']);
+ Route::get('/api-tokens/{userId}/{tokenId}', [UserApiTokenController::class, 'edit']);
+ Route::put('/api-tokens/{userId}/{tokenId}', [UserApiTokenController::class, 'update']);
+ Route::get('/api-tokens/{userId}/{tokenId}/delete', [UserApiTokenController::class, 'delete']);
+ Route::delete('/api-tokens/{userId}/{tokenId}', [UserApiTokenController::class, 'destroy']);
// Roles
Route::get('/settings/roles', [UserControllers\RoleController::class, 'index']);
// OIDC routes
Route::post('/oidc/login', [AccessControllers\OidcController::class, 'login']);
Route::get('/oidc/callback', [AccessControllers\OidcController::class, 'callback']);
+ // OIDC Logout Feature: Added to cater OIDC logout
+ Route::get('/oidc/logout', [AccessControllers\OidcController::class, 'logout']);
// User invitation routes
Route::get('/register/invite/{token}', [AccessControllers\UserInviteController::class, 'showSetPassword']);