Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
07a6d76
First basic OpenID Connect implementation
jasperweyne Jul 1, 2020
25144a1
Deduplicated getOrRegisterUser method
jasperweyne Jul 6, 2020
10c8909
Token expiration and refreshing using the refresh_token flow
jasperweyne Jul 7, 2020
5df7db5
Ignore ID token expiry if unavailable
jasperweyne Jul 7, 2020
97cde9c
Generalize refresh failure handling
jasperweyne Jul 8, 2020
13d0260
Configurable OpenID Connect services
jasperweyne Jul 9, 2020
75b4a05
Add OpenIdService to OpenIdSessionGuard constructor call
jasperweyne Jul 9, 2020
46388a5
AccessToken empty array parameter on null
jasperweyne Jul 9, 2020
6feaf25
Increase robustness of the refresh method
jasperweyne Aug 4, 2020
23402ae
Initial unit tests for OpenID
jasperweyne Aug 4, 2020
f2d3208
Simplify refresh method
jasperweyne Aug 4, 2020
35c48b9
Method descriptions
jasperweyne Aug 4, 2020
69a4731
Default OpenID display name set to standard value
jasperweyne Aug 5, 2020
193d7fb
Merge branch 'openid' of https://github.com/jasperweyne/BookStack int…
ssddanbrown Oct 6, 2021
2ec0aa8
Started refactor for merge of OIDC
ssddanbrown Oct 6, 2021
41438ad
Continued review of #2169
ssddanbrown Oct 6, 2021
8ce696d
Started on a custom oidc oauth provider
ssddanbrown Oct 10, 2021
8c01c55
Added token and key handling elements for oidc jwt
ssddanbrown Oct 11, 2021
6b182a4
Got OIDC custom solution to a functional state
ssddanbrown Oct 11, 2021
f3d54e4
Added positive test case for OIDC implementation
ssddanbrown Oct 11, 2021
790723d
Added further OIDC core class testing
ssddanbrown Oct 12, 2021
06a0d82
Added OIDC basic autodiscovery support
ssddanbrown Oct 12, 2021
c167f40
Renamed OIDC files to all be aligned
ssddanbrown Oct 12, 2021
a5d72aa
Fleshed out testing for OIDC system
ssddanbrown Oct 13, 2021
855409b
Fixed lack of oidc discovery filtering during testing
ssddanbrown Oct 14, 2021
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
12 changes: 12 additions & 0 deletions .env.example.complete
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,18 @@ SAML2_USER_TO_GROUPS=false
SAML2_GROUP_ATTRIBUTE=group
SAML2_REMOVE_FROM_GROUPS=false

# OpenID Connect authentication configuration
OIDC_NAME=SSO
OIDC_DISPLAY_NAME_CLAIMS=name
OIDC_CLIENT_ID=null
OIDC_CLIENT_SECRET=null
OIDC_ISSUER=null
OIDC_ISSUER_DISCOVER=false
OIDC_PUBLIC_KEY=null
OIDC_AUTH_ENDPOINT=null
OIDC_TOKEN_ENDPOINT=null
OIDC_DUMP_USER_DETAILS=false

# Disable default third-party services such as Gravatar and Draw.IO
# Service-specific options will override this option
DISABLE_EXTERNAL_SERVICES=false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use BookStack\Auth\User;
use Illuminate\Support\Collection;

class ExternalAuthService
class GroupSyncService
{
/**
* Check a role against an array of group names to see if it matches.
Expand Down Expand Up @@ -60,17 +60,17 @@ protected function matchGroupsToSystemsRoles(array $groupNames): Collection
/**
* Sync the groups to the user roles for the current user.
*/
public function syncWithGroups(User $user, array $userGroups): void
public function syncUserWithFoundGroups(User $user, array $userGroups, bool $detachExisting): void
{
// Get the ids for the roles from the names
$groupsAsRoles = $this->matchGroupsToSystemsRoles($userGroups);

// Sync groups
if ($this->config['remove_from_groups']) {
if ($detachExisting) {
$user->roles()->sync($groupsAsRoles);
$user->attachDefaultRole();
} else {
$user->roles()->syncWithoutDetaching($groupsAsRoles);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* via the Saml2 controller & Saml2Service. This class provides a safer, thin
* version of SessionGuard.
*/
class Saml2SessionGuard extends ExternalBaseSessionGuard
class AsyncExternalBaseSessionGuard extends ExternalBaseSessionGuard
{
/**
* Validate a user's credentials.
Expand Down
16 changes: 7 additions & 9 deletions app/Auth/Access/LdapService.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
* Class LdapService
* Handles any app-specific LDAP tasks.
*/
class LdapService extends ExternalAuthService
class LdapService
{
protected $ldap;
protected $groupSyncService;
protected $ldapConnection;
protected $userAvatars;
protected $config;
Expand All @@ -24,20 +25,19 @@ class LdapService extends ExternalAuthService
/**
* LdapService constructor.
*/
public function __construct(Ldap $ldap, UserAvatars $userAvatars)
public function __construct(Ldap $ldap, UserAvatars $userAvatars, GroupSyncService $groupSyncService)
{
$this->ldap = $ldap;
$this->userAvatars = $userAvatars;
$this->groupSyncService = $groupSyncService;
$this->config = config('services.ldap');
$this->enabled = config('auth.method') === 'ldap';
}

/**
* Check if groups should be synced.
*
* @return bool
*/
public function shouldSyncGroups()
public function shouldSyncGroups(): bool
{
return $this->enabled && $this->config['user_to_groups'] !== false;
}
Expand Down Expand Up @@ -285,9 +285,7 @@ public function getUserGroups(string $userName): array
}

$userGroups = $this->groupFilter($user);
$userGroups = $this->getGroupsRecursive($userGroups, []);

return $userGroups;
return $this->getGroupsRecursive($userGroups, []);
}

/**
Expand Down Expand Up @@ -374,7 +372,7 @@ protected function groupFilter(array $userGroupSearchResponse): array
public function syncGroups(User $user, string $username)
{
$userLdapGroups = $this->getUserGroups($username);
$this->syncWithGroups($user, $userLdapGroups);
$this->groupSyncService->syncUserWithFoundGroups($user, $userLdapGroups, $this->config['remove_from_groups']);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion app/Auth/Access/LoginService.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public function login(User $user, string $method, bool $remember = false): void

// Authenticate on all session guards if a likely admin
if ($user->can('users-manage') && $user->can('user-roles-manage')) {
$guards = ['standard', 'ldap', 'saml2'];
$guards = ['standard', 'ldap', 'saml2', 'oidc'];
foreach ($guards as $guard) {
auth($guard)->login($user);
}
Expand Down
54 changes: 54 additions & 0 deletions app/Auth/Access/Oidc/OidcAccessToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace BookStack\Auth\Access\Oidc;

use InvalidArgumentException;
use League\OAuth2\Client\Token\AccessToken;

class OidcAccessToken extends AccessToken
{
/**
* Constructs an access token.
*
* @param array $options An array of options returned by the service provider
* in the access token request. The `access_token` option is required.
* @throws InvalidArgumentException if `access_token` is not provided in `$options`.
*/
public function __construct(array $options = [])
{
parent::__construct($options);
$this->validate($options);
}


/**
* Validate this access token response for OIDC.
* As per https://openid.net/specs/openid-connect-basic-1_0.html#TokenOK.
*/
private function validate(array $options): void
{
// access_token: REQUIRED. Access Token for the UserInfo Endpoint.
// Performed on the extended class

// token_type: REQUIRED. OAuth 2.0 Token Type value. The value MUST be Bearer, as specified in OAuth 2.0
// Bearer Token Usage [RFC6750], for Clients using this subset.
// Note that the token_type value is case-insensitive.
if (strtolower(($options['token_type'] ?? '')) !== 'bearer') {
throw new InvalidArgumentException('The response token type MUST be "Bearer"');
}

// id_token: REQUIRED. ID Token.
if (empty($options['id_token'])) {
throw new InvalidArgumentException('An "id_token" property must be provided');
}
}

/**
* Get the id token value from this access token response.
*/
public function getIdToken(): string
{
return $this->getValues()['id_token'];
}

}
Loading