Merge da82e70ca3cdd075f7ae148cb2f58fddb0d93627 into 0ec0913846f79ed3dcde9634739a7eda297f19e1

This commit is contained in:
Ruben Talstra 2025-03-17 12:14:40 +02:00 committed by GitHub
commit b1478de406
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 46 additions and 4 deletions

View File

@ -11,6 +11,7 @@ use BookStack\Exceptions\UserRegistrationException;
use BookStack\Facades\Theme;
use BookStack\Http\HttpRequestService;
use BookStack\Theming\ThemeEvents;
use BookStack\Uploads\UserAvatars;
use BookStack\Users\Models\User;
use Illuminate\Support\Facades\Cache;
use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
@ -26,7 +27,8 @@ class OidcService
protected RegistrationService $registrationService,
protected LoginService $loginService,
protected HttpRequestService $http,
protected GroupSyncService $groupService
protected GroupSyncService $groupService,
protected UserAvatars $userAvatars
) {
}
@ -227,6 +229,10 @@ class OidcService
$this->loginService->login($user, 'oidc');
if ($this->config()['fetch_avatars'] && $userDetails->picture) {
$this->userAvatars->assignToUserFromUrl($user, $userDetails->picture, $accessToken->getToken());
}
return $user;
}

View File

@ -11,6 +11,7 @@ class OidcUserDetails
public ?string $email = null,
public ?string $name = null,
public ?array $groups = null,
public ?string $picture = null,
) {
}
@ -40,6 +41,7 @@ class OidcUserDetails
$this->email = $claims->getClaim('email') ?? $this->email;
$this->name = static::getUserDisplayName($displayNameClaims, $claims) ?? $this->name;
$this->groups = static::getUserGroups($groupsClaim, $claims) ?? $this->groups;
$this->picture = $claims->getClaim('picture') ?: $this->picture;
}
protected static function getUserDisplayName(string $displayNameClaims, ProvidesClaims $token): string

View File

@ -54,4 +54,7 @@ return [
'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),
// When enabled, BookStack will fetch the user’s avatar from the 'picture' claim (SSRF risk if URLs are untrusted).
'fetch_avatars' => env('OIDC_FETCH_AVATARS', false),
];

View File

@ -53,6 +53,31 @@ class UserAvatars
}
}
/**
* Assign a new avatar image to the given user by fetching from a remote URL.
*/
public function assignToUserFromUrl(User $user, string $avatarUrl, ?string $accessToken = null): void
{
// Quickly skip invalid or non-HTTP URLs
if (!$avatarUrl || !str_starts_with($avatarUrl, 'http')) {
return;
}
try {
$this->destroyAllForUser($user);
$imageData = $this->getAvatarImageData($avatarUrl, $accessToken);
$avatar = $this->createAvatarImageFromData($user, $imageData, 'png');
$user->avatar()->associate($avatar);
$user->save();
} catch (Exception $e) {
Log::error('Failed to save user avatar image from URL', [
'exception' => $e,
'url' => $avatarUrl,
'user_id' => $user->id,
]);
}
}
/**
* Destroy all user avatars uploaded to the given user.
*/
@ -105,15 +130,21 @@ class UserAvatars
}
/**
* Gets an image from url and returns it as a string of image data.
* Gets an image from a URL (public or private) and returns it as a string of image data.
*
* @throws HttpFetchException
*/
protected function getAvatarImageData(string $url): string
protected function getAvatarImageData(string $url, ?string $accessToken = null): string
{
try {
$headers = [];
if (!empty($accessToken)) {
$headers['Authorization'] = 'Bearer ' . $accessToken;
}
$client = $this->http->buildClient(5);
$response = $client->sendRequest(new Request('GET', $url));
$response = $client->sendRequest(new Request('GET', $url, $headers));
if ($response->getStatusCode() !== 200) {
throw new HttpFetchException(trans('errors.cannot_get_image_from_url', ['url' => $url]));
}