Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User provider token latest #280

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ $CONFIG = array (
// - 'plain'
// The default value is empty, which won't apply the PKCE flow.
'oidc_login_code_challenge_method' => '',

// If you want to explicitly disable usage of access/refresh
// tokens. Defaults to false.
'oidc_refresh_tokens_disabled' => false,
);
```
### Usage with [Keycloak](https://www.keycloak.org/)
Expand Down
75 changes: 58 additions & 17 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use OC\AppFramework\Utility\ControllerMethodReflector;
use OCA\OIDCLogin\OIDCLoginOption;
use OCA\OIDCLogin\WebDAV\BasicAuthBackend;
use OCA\OIDCLogin\Service\TokenService;
use OCA\OIDCLogin\WebDAV\BearerAuthBackend;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
Expand All @@ -26,8 +26,11 @@ class Application extends App implements IBootstrap
protected IURLGenerator $url;
protected IL10N $l;
protected IConfig $config;
/** @var TokenService */
private $tokenService;

private $appName = 'oidc_login';
private const TOKEN_LOGIN_KEY = 'is_oidc_token_login';

public function __construct()
{
Expand All @@ -36,38 +39,35 @@ public function __construct()

public function register(IRegistrationContext $context): void
{
$context->registerAlternativeLogin(OIDCLoginOption::class);

$context->registerEventListener(
'OCA\DAV\Connector\Sabre::authInit',
BearerAuthBackend::class
\OCA\OIDCLogin\WebDAV\BearerAuthBackend::class
);

$context->registerEventListener(
'OCA\DAV\Connector\Sabre::addPlugin',
BearerAuthBackend::class
\OCA\OIDCLogin\WebDAV\BearerAuthBackend::class
);

$context->registerEventListener(
'OCA\DAV\Connector\Sabre::authInit',
BasicAuthBackend::class
\OCA\OIDCLogin\WebDAV\BasicAuthBackend::class
);

$context->registerEventListener(
'OCA\DAV\Connector\Sabre::addPlugin',
BasicAuthBackend::class
\OCA\OIDCLogin\WebDAV\BasicAuthBackend::class
);
}

public function boot(IBootContext $context): void
{
$container = $context->getAppContainer();
$this->l = $container->get(IL10N::class);
$this->url = $container->get(IURLGenerator::class);
$this->config = $container->get(IConfig::class);

/** @var IRequest */
$request = $container->get(IRequest::class);
$this->l = $container->query(IL10N::class);
$this->url = $container->query(IURLGenerator::class);
$this->config = $container->query(IConfig::class);
$this->tokenService = $container->query(TokenService::class);
$request = $container->query(IRequest::class);

// Check if automatic redirection is enabled
$useLoginRedirect = $this->config->getSystemValue('oidc_login_auto_redirect', false);
Expand All @@ -81,7 +81,23 @@ public function boot(IBootContext $context): void
// Get logged in user's session
$userSession = $container->get(IUserSession::class);
$session = $container->get(ISession::class);
// If it is an OCS request, try to authenticate with bearer token if not logged in
$isBearerAuth = str_starts_with($request->getHeader('Authorization'), 'Bearer ');
if (!$userSession->isLoggedIn()
&& ($request->getHeader('OCS-APIREQUEST') === 'true')
&& $isBearerAuth) {
$bearerAuthBackend = $container->get(BearerAuthBackend::class);
$this->loginWithBearerToken($request, $bearerAuthBackend, $session);
}

// For non-OCS routes, perform validation even if logged in via session
if ($isBearerAuth && $request->getHeader('OIDC-LOGIN-WITH-TOKEN') === 'true') {
// Invalidate existing session's oidc login
$session->remove(self::TOKEN_LOGIN_KEY);
$bearerAuthBackend = $container->get(BearerAuthBackend::class);
$this->loginWithBearerToken($request, $bearerAuthBackend, $session);
}

// Check if the user is logged in
if ($userSession->isLoggedIn()) {
// Halt processing if not logged in with OIDC
Expand All @@ -92,9 +108,18 @@ public function boot(IBootContext $context): void
// Disable password confirmation for user
$session->set('last-password-confirm', $container->get(ITimeFactory::class)->getTime());

$refreshTokensEnabled = $session->exists('oidc_refresh_tokens_enabled');
/* Redirect to logout URL on completing logout
If do not have logout URL, go to noredir on logout */
if ($logoutUrl = $session->get('oidc_logout_url', $noRedirLoginUrl)) {
$userSession->listen('\OC\User', 'logout', function () use (&$logoutUrl, $refreshTokensEnabled, $session) {
if ($refreshTokensEnabled) {
// Refresh tokens before logout
$this->tokenService->refreshTokens();
$logoutUrl = $session->get('oidc_logout_url');
}
});

$userSession->listen('\OC\User', 'postLogout', function () use ($logoutUrl, $session) {
// Do nothing if this is a CORS request
if ($this->getContainer()->get(ControllerMethodReflector::class)->hasAnnotation('CORS')) {
Expand All @@ -114,6 +139,10 @@ public function boot(IBootContext $context): void
});
}

if ($refreshTokensEnabled && !$this->tokenService->refreshTokens()) {
$userSession->logout();
}

// Hide password change form
if ($this->config->getSystemValue('oidc_login_hide_password_form', false)) {
Util::addStyle($this->appName, 'oidc.hidepasswordform');
Expand Down Expand Up @@ -158,9 +187,21 @@ public function boot(IBootContext $context): void
}
}
}

public function isApiRequest()
{
return isset($_SERVER['HTTP_ACCEPT']) && false !== strpos($_SERVER['HTTP_ACCEPT'], 'application/json');
public function isApiRequest() {
// Check if the request includes an 'Accept' header with value 'application/json'
return isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false;
}
private function loginWithBearerToken(IRequest $request, BearerAuthBackend $bearerAuthBackend, ISession $session) {
$authHeader = $request->getHeader('Authorization');
$bearerToken = substr($authHeader, 7);
if (empty($bearerToken)) {
return;
}
try {
$bearerAuthBackend->login($bearerToken);
$session->set(self::TOKEN_LOGIN_KEY, 1);
} catch (\Exception $e) {
$this->logger->debug("OIDC Bearer token validation failed with: {$e->getMessage()}", ['app' => $this->appName]);
}
}
}
82 changes: 76 additions & 6 deletions lib/Controller/LoginController.php
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
<?php

declare(strict_types=1);

namespace OCA\OIDCLogin\Controller;

use OCA\OIDCLogin\Provider\OpenIDConnectClient;
use OCA\OIDCLogin\Service\LoginService;
use OCA\OIDCLogin\Service\TokenService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Files\IRootFolder;
use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserSession;
use OCP\IL10N;

class LoginController extends Controller
{

/** @var IUserManager */
private $userManager;

/** @var IGroupManager */
private $groupManager;

/** @var TokenService */
private $tokenService;

/** @var IL10N */
private $l;


private IConfig $config;
private IURLGenerator $urlGenerator;
private IUserSession $userSession;
Expand All @@ -30,14 +46,17 @@ public function __construct(
IURLGenerator $urlGenerator,
IUserSession $userSession,
ISession $session,
LoginService $loginService
IL10N $l,
LoginService $loginService,
TokenService $tokenService
) {
parent::__construct($appName, $request);
$this->config = $config;
$this->urlGenerator = $urlGenerator;
$this->userSession = $userSession;
$this->session = $session;
$this->loginService = $loginService;
$this->tokenService = $tokenService;
}

/**
Expand All @@ -57,6 +76,19 @@ public function oidc(): RedirectResponse

// Authenticate
$oidc->authenticate();
$user = null;
if ($this->config->getSystemValue('oidc_login_use_id_token', false)) {
// Get user information from ID Token
$user = $oidc->getIdTokenPayload();
} else {
// Get user information from OIDC
$user = $oidc->requestUserInfo();
}

$this->tokenService->prepareLogout($oidc);

// Convert to PHP array and process
return $this->authSuccess(json_decode(json_encode($user), true), $oidc);

// Get user info
$profile = $oidc->getProfile();
Expand All @@ -80,6 +112,15 @@ public function oidc(): RedirectResponse
}
}

private function authSuccess($profile, $oidc)
{
if ($redirectUrl = $this->request->getParam('login_redirect_url')) {
$this->session->set('login_redirect_url', $redirectUrl);
}

return $this->login($profile, $oidc);
}

private function prepareLogout(OpenIDConnectClient $oidc): void
{
if ($oidc_login_logout_url = $this->config->getSystemValue('oidc_login_logout_url', false)) {
Expand All @@ -94,18 +135,32 @@ private function prepareLogout(OpenIDConnectClient $oidc): void
}
}

private function login(array $profile): RedirectResponse
private function login($profile, $oidc): RedirectResponse
{
// Redirect if already logged in
if ($this->userSession->isLoggedIn()) {
return new RedirectResponse($this->urlGenerator->getAbsoluteURL('/'));
}

/** @var IUser $user */
/** @var \OCP\IUser $user */
[$user, $password] = $this->loginService->login($profile);

$refreshTokensEnabled = false;
$refreshTokensDisabledExplicitly = $this->config->getSystemValue('oidc_refresh_tokens_disabled', false);

$tokenResponse = $oidc->getTokenResponse();
if (!$refreshTokensDisabledExplicitly) {
$scopes = $oidc->getScopes();
$refreshTokensEnabled = $this->shouldEnableRefreshTokens($scopes, $tokenResponse);
}

if ($refreshTokensEnabled) {
$this->session->set('oidc_refresh_tokens_enabled', 1);
$this->tokenService->updateTokens($user, $tokenResponse);
}

// Workaround to create user files folder. Remove it later.
\OC::$server->get(IRootFolder::class)->getUserFolder($user->getUID());
\OC::$server->get(\OCP\Files\IRootFolder::class)->getUserFolder($user->getUID());

// Prevent being asked to change password
$this->session->set('last-password-confirm', \OC::$server->get(ITimeFactory::class)->getTime());
Expand All @@ -123,4 +178,19 @@ private function login(array $profile): RedirectResponse

return new RedirectResponse($this->urlGenerator->getAbsoluteURL($redir));
}

private function shouldEnableRefreshTokens(array $scopes, object $tokenResponse): bool
{
foreach ($scopes as $scope) {
if (str_contains($scope, 'offline_access')) {
return true;
}
}

if (isset($tokenResponse->refresh_token) && !empty($tokenResponse->refresh_token)) {
return true;
}

return false;
}
}
Empty file.
46 changes: 46 additions & 0 deletions lib/Db/Mappers/RefreshTokenMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace OCA\OIDCLogin\Db\Mappers;

use OCA\OIDCLogin\Db\Entities\RefreshToken;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\QBMapper;
use OCP\IDBConnection;
use OCP\IUser;

class RefreshTokenMapper extends QBMapper
{
public const TABLENAME = 'oidc_refresh_tokens';

public function __construct(IDBConnection $db)
{
parent::__construct($db, self::TABLENAME, RefreshToken::class);
}

/**
* Get all signatories of a specific type for an user.
*
* @throws DoesNotExistException
*/
public function getTokenByUser(IUser $user): RefreshToken
{
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from(self::TABLENAME)
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($user->getUID())))
;

return $this->findEntity($qb);
}

public function deleteTokenForUser(IUser $user): void
{
$qb = $this->db->getQueryBuilder();
$qb->delete(self::TABLENAME)
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($user->getUID())))
;
$qb->execute();
}
}
31 changes: 31 additions & 0 deletions lib/Events/AccessTokenUpdatedEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace OCA\OIDCLogin\Events;

use OCP\EventDispatcher\Event;

/**
* Class AccessTokenUpdatedEvent.
*/
class AccessTokenUpdatedEvent extends Event
{
/** @var string */
private $accessToken;

/**
* AccessTokenUpdatedEvent constructor.
*/
public function __construct(
string $accessToken
) {
parent::__construct();
$this->accessToken = $accessToken;
}

public function getAccessToken(): string
{
return $this->accessToken;
}
}
Empty file.
Loading
Loading