/** * Log signed request pattern failures * * @Flow\AfterReturning("setting(Flowpack.SingleSignOn.Client.log.logFailedSignedRequests) && method(Flowpack\SingleSignOn\Client\Security\RequestPattern\SignedRequestPattern->emitSignatureNotVerified())") * @param \TYPO3\Flow\Aop\JoinPointInterface $joinPoint The current joinpoint */ public function logSignedRequestPatternFailures(\TYPO3\Flow\Aop\JoinPointInterface $joinPoint) { $request = $joinPoint->getMethodArgument('request'); if ($request instanceof \TYPO3\Flow\Mvc\RequestInterface) { if ($request->getControllerObjectName() === 'Flowpack\\SingleSignOn\\Client\\Controller\\SessionController') { $this->securityLogger->log('Signature for call to Session service could not be verified', LOG_NOTICE, array('identifier' => $joinPoint->getMethodArgument('identifier'), 'publicKeyFingerprint' => $joinPoint->getMethodArgument('publicKeyFingerprint'), 'signature' => base64_encode($joinPoint->getMethodArgument('signature')), 'signData' => $joinPoint->getMethodArgument('signData'), 'content' => $joinPoint->getMethodArgument('request')->getHttpRequest()->getContent())); } } }
/** * Tries to authenticate the given token. Sets isAuthenticated to TRUE if authentication succeeded. * * @param TokenInterface $authenticationToken The token to be authenticated * @throws \TYPO3\Flow\Security\Exception\UnsupportedAuthenticationTokenException * @return void */ public function authenticate(TokenInterface $authenticationToken) { if (!$authenticationToken instanceof AbstractClientToken) { throw new UnsupportedAuthenticationTokenException('This provider cannot authenticate the given token.', 1383754993); } $credentials = $authenticationToken->getCredentials(); // There is no way to validate the Token or check the scopes at the moment apart from "trying" (and possibly receiving an access denied) // we could check the validity of the Token and the scopes here in the future when Instagram provides that // Only check if an access Token is present at this time and do a single test call if (isset($credentials['accessToken']) && $credentials['accessToken'] !== NULL) { // check if a secure request is possible (https://www.instagram.com/developer/secure-api-requests/) $userInfo = $this->instagramTokenEndpoint->validateSecureRequestCapability($credentials['accessToken']); if ($userInfo === FALSE) { $authenticationToken->setAuthenticationStatus(TokenInterface::WRONG_CREDENTIALS); $this->securityLogger->log('A secure call to the API with the provided accessToken and clientSecret was not possible', LOG_NOTICE); return FALSE; } } else { } // From here, we surely know the user is considered authenticated against the remote service, // yet to check if there is an immanent account present. $authenticationToken->setAuthenticationStatus(TokenInterface::AUTHENTICATION_SUCCESSFUL); /** @var $account \TYPO3\Flow\Security\Account */ $account = NULL; $providerName = $this->name; $accountRepository = $this->accountRepository; $this->securityContext->withoutAuthorizationChecks(function () use($userInfo, $providerName, $accountRepository, &$account) { $account = $accountRepository->findByAccountIdentifierAndAuthenticationProviderName($userInfo['id'], $providerName); }); if ($account === NULL) { $account = new Account(); $account->setAccountIdentifier($userInfo['id']); $account->setAuthenticationProviderName($providerName); $this->accountRepository->add($account); } $authenticationToken->setAccount($account); // the access token is valid for an "undefined time" according to instagram (so we cannot know when the user needs to log in again) $account->setCredentialsSource($credentials['accessToken']); $this->accountRepository->update($account); // check if a user is already attached to this account if ($this->partyService->getAssignedPartyOfAccount($account) === null || count($this->partyService->getAssignedPartyOfAccount($account)) < 1) { $user = $this->userService->getCurrentUser(); if ($user !== null) { $user->addAccount($account); $this->userService->updateUser($user); $this->persistenceManager->whitelistObject($user); } else { $this->securityLogger->logException(new Exception("The InstagramProvider was unable to determine the backend user, make sure the configuration Typo3BackendProvider requestPattern matches the Instagram Controller and the authentication strategy is set to 'atLeastOne' Token")); } } // persistAll is called automatically at the end of this function, account gets whitelisted to allow // persisting for an object thats tinkered with via a GET request $this->persistenceManager->whitelistObject($account); }
/** * Logs calls and results of decideOnResource() * * @Flow\AfterThrowing("method(TYPO3\Flow\Security\Authorization\AccessDecisionVoterManager->decideOnResource())") * @param \TYPO3\Flow\Aop\JoinPointInterface $joinPoint * @throws \Exception * @return void */ public function logResourceAccessDecisions(\TYPO3\Flow\Aop\JoinPointInterface $joinPoint) { $exception = $joinPoint->getException(); $message = $exception->getMessage() . ' on resource "' . $joinPoint->getMethodArgument('resource') . '".'; $this->securityLogger->log($message, \LOG_INFO); throw $exception; }
/** * Tries to authenticate the given token. Sets isAuthenticated to TRUE if authentication succeeded. * * @param TokenInterface $authenticationToken The token to be authenticated * @throws \TYPO3\Flow\Security\Exception\UnsupportedAuthenticationTokenException * @return void */ public function authenticate(TokenInterface $authenticationToken) { if (!$authenticationToken instanceof AbstractClientToken) { throw new UnsupportedAuthenticationTokenException('This provider cannot authenticate the given token.', 1383754993); } $credentials = $authenticationToken->getCredentials(); // Inspect the received access token as documented in https://developers.facebook.com/docs/facebook-login/login-flow-for-web-no-jssdk/ $tokenInformation = $this->facebookTokenEndpoint->requestValidatedTokenInformation($credentials['accessToken']); if ($tokenInformation === FALSE) { $authenticationToken->setAuthenticationStatus(TokenInterface::WRONG_CREDENTIALS); return; } // Check if the permitted scopes suffice: $necessaryScopes = $this->options['scopes']; $scopesHavingPermissionFor = $tokenInformation['scopes']; $requiredButNotPermittedScopes = array_diff($necessaryScopes, $scopesHavingPermissionFor); if (count($requiredButNotPermittedScopes) > 0) { $authenticationToken->setAuthenticationStatus(TokenInterface::WRONG_CREDENTIALS); $this->securityLogger->log('The permitted scopes do not satisfy the required once.', LOG_NOTICE, array('necessaryScopes' => $necessaryScopes, 'allowedScopes' => $scopesHavingPermissionFor)); return; } // From here, we surely know the user is considered authenticated against the remote service, // yet to check if there is an immanent account present. $authenticationToken->setAuthenticationStatus(TokenInterface::AUTHENTICATION_SUCCESSFUL); /** @var $account \TYPO3\Flow\Security\Account */ $account = NULL; $providerName = $this->name; $accountRepository = $this->accountRepository; $this->securityContext->withoutAuthorizationChecks(function () use($tokenInformation, $providerName, $accountRepository, &$account) { $account = $accountRepository->findByAccountIdentifierAndAuthenticationProviderName($tokenInformation['user_id'], $providerName); }); if ($account === NULL) { $account = new Account(); $account->setAccountIdentifier($tokenInformation['user_id']); $account->setAuthenticationProviderName($providerName); $this->accountRepository->add($account); } $authenticationToken->setAccount($account); // request long-live token and attach that to the account $longLivedToken = $this->facebookTokenEndpoint->requestLongLivedToken($credentials['accessToken']); $account->setCredentialsSource($longLivedToken); $this->accountRepository->update($account); }
/** * Inspect the received access token as documented in https://developers.facebook.com/docs/facebook-login/access-tokens/, section Getting Info about Tokens and Debugging * * @param string $tokenToInspect * @return array * @throws OAuth2Exception */ public function requestValidatedTokenInformation($tokenToInspect) { $applicationToken = $this->requestClientCredentialsGrantAccessToken(); $requestArguments = array('input_token' => $tokenToInspect, 'access_token' => $applicationToken); $request = Request::create(new Uri('https://graph.facebook.com/debug_token?' . http_build_query($requestArguments))); $response = $this->requestEngine->sendRequest($request); $responseContent = $response->getContent(); if ($response->getStatusCode() !== 200) { throw new OAuth2Exception(sprintf('The response was not of type 200 but gave code and error %d "%s"', $response->getStatusCode(), $responseContent), 1383758360); } $responseArray = json_decode($responseContent, TRUE, 16, JSON_BIGINT_AS_STRING); $responseArray['data']['app_id'] = (string) $responseArray['data']['app_id']; $responseArray['data']['user_id'] = (string) $responseArray['data']['user_id']; if (!$responseArray['data']['is_valid'] || $responseArray['data']['app_id'] !== $this->clientIdentifier) { $this->securityLogger->log('Requesting validated token information from the Facebook endpoint did not succeed.', LOG_NOTICE, array('response' => var_export($responseArray, TRUE), 'clientIdentifier' => $this->clientIdentifier)); return FALSE; } else { return $responseArray['data']; } }
/** * Updates the authentication credentials, the authentication manager needs to authenticate this token. * This could be a username/password from a login controller. * This method is called while initializing the security context. By returning TRUE you * make sure that the authentication manager will (re-)authenticate the tokens with the current credentials. * Note: You should not persist the credentials! * * @param ActionRequest $actionRequest The current request instance * @throws \InvalidArgumentException * @return boolean TRUE if this token needs to be (re-)authenticated */ public function updateCredentials(ActionRequest $actionRequest) { if ($actionRequest->getHttpRequest()->getMethod() !== 'GET' || $actionRequest->getInternalArgument('__oauth2Provider') !== $this->authenticationProviderName) { return; } if (!$actionRequest->hasArgument('code')) { $this->setAuthenticationStatus(TokenInterface::WRONG_CREDENTIALS); $this->securityLogger->log('There was no argument `code` provided.', LOG_NOTICE); return; } $code = $actionRequest->getArgument('code'); $redirectUri = $this->oauthUriBuilder->getRedirectionEndpointUri($this->authenticationProviderName); try { $this->credentials['accessToken'] = $this->tokenEndpoint->requestAuthorizationCodeGrantAccessToken($code, $redirectUri); $this->setAuthenticationStatus(TokenInterface::AUTHENTICATION_NEEDED); } catch (Exception $exception) { $this->setAuthenticationStatus(TokenInterface::WRONG_CREDENTIALS); $this->securityLogger->logException($exception); return; } }
/** * @param string $resource * @param array $requestArguments * @param string $method * @return mixed * @throws OAuth2Exception */ public function query($resource, $requestArguments = array(), $method = 'GET') { $requestArguments['access_token'] = $this->currentAccessToken; $requestArguments['sig'] = $this->generate_sig($resource, $requestArguments); // test the secure API call by getting information of the own user - scope: basic (also available in sandbox mode) $request = Request::create(new Uri($this->endpoint . $resource . "?" . http_build_query($requestArguments))); $response = $this->requestEngine->sendRequest($request); $responseContent = $response->getContent(); if ($response->getStatusCode() !== 200) { $this->securityLogger->log('Error in Instagram Query: ' . $responseContent); throw new OAuth2Exception(sprintf('The response was not of type 200 but gave code and error %d "%s"', $response->getStatusCode(), $responseContent), 1455261376); } return json_decode($responseContent, true); }
/** * Advices the dispatch method so that illegal action requests are blocked before * invoking any controller. * * The "request" referred to within this method is an ActionRequest or some other * dispatchable request implementing RequestInterface. Note that we don't deal * with HTTP requests here. * * @Flow\Around("setting(TYPO3.Flow.security.enable) && method(TYPO3\Flow\Mvc\Dispatcher->dispatch())") * @param \TYPO3\Flow\Aop\JoinPointInterface $joinPoint The current joinpoint * @return mixed Result of the advice chain * @throws \Exception|\TYPO3\Flow\Security\Exception\AccessDeniedException * @throws \Exception|\TYPO3\Flow\Security\Exception\AuthenticationRequiredException */ public function blockIllegalRequestsAndForwardToAuthenticationEntryPoints(JoinPointInterface $joinPoint) { $request = $joinPoint->getMethodArgument('request'); if (!$request instanceof ActionRequest || $this->securityContext->areAuthorizationChecksDisabled()) { return $joinPoint->getAdviceChain()->proceed($joinPoint); } try { $this->firewall->blockIllegalRequests($request); return $joinPoint->getAdviceChain()->proceed($joinPoint); } catch (AuthenticationRequiredException $exception) { $response = $joinPoint->getMethodArgument('response'); $entryPointFound = FALSE; /** @var $token \TYPO3\Flow\Security\Authentication\TokenInterface */ foreach ($this->securityContext->getAuthenticationTokens() as $token) { $entryPoint = $token->getAuthenticationEntryPoint(); if ($entryPoint !== NULL) { $entryPointFound = TRUE; if ($entryPoint instanceof WebRedirect) { $this->securityLogger->log('Redirecting to authentication entry point', LOG_INFO, $entryPoint->getOptions()); } else { $this->securityLogger->log('Starting authentication with entry point of type ' . get_class($entryPoint), LOG_INFO); } $this->securityContext->setInterceptedRequest($request->getMainRequest()); $entryPoint->startAuthentication($request->getHttpRequest(), $response); } } if ($entryPointFound === FALSE) { $this->securityLogger->log('No authentication entry point found for active tokens, therefore cannot authenticate or redirect to authentication automatically.', LOG_NOTICE); throw $exception; } } catch (AccessDeniedException $exception) { $this->securityLogger->log('Access denied', LOG_WARNING); throw $exception; } return NULL; }
/** * Sets the roles for the LDAP account. * Extend this Provider class and implement this method to update the party * * @param Account $account * @param array $ldapSearchResult * @return void */ protected function setRoles(Account $account, array $ldapSearchResult) { if (is_array($this->rolesConfiguration)) { $contextVariables = array('ldapUser' => $ldapSearchResult); if (isset($this->defaultContext) && is_array($this->defaultContext)) { foreach ($this->defaultContext as $contextVariable => $objectName) { $object = $this->objectManager->get($objectName); $contextVariables[$contextVariable] = $object; } } foreach ($this->rolesConfiguration['default'] as $roleIdentifier) { $role = $this->policyService->getRole($roleIdentifier); $account->addRole($role); } $eelContext = new Context($contextVariables); if (isset($this->partyConfiguration['dn'])) { $dn = $this->eelEvaluator->evaluate($this->partyConfiguration['dn'], $eelContext); foreach ($this->rolesConfiguration['userMapping'] as $roleIdentifier => $userDns) { if (in_array($dn, $userDns)) { $role = $this->policyService->getRole($roleIdentifier); $account->addRole($role); } } } elseif (!empty($this->rolesConfiguration['userMapping'])) { $this->logger->log('User mapping found but no party mapping for dn set', LOG_ALERT); } if (isset($this->partyConfiguration['username'])) { $username = $this->eelEvaluator->evaluate($this->partyConfiguration['username'], $eelContext); $groupMembership = $this->directoryService->getGroupMembership($username); foreach ($this->rolesConfiguration['groupMapping'] as $roleIdentifier => $remoteRoleIdentifiers) { foreach ($remoteRoleIdentifiers as $remoteRoleIdentifier) { $role = $this->policyService->getRole($roleIdentifier); if (isset($groupMembership[$remoteRoleIdentifier])) { $account->addRole($role); } } } } elseif (!empty($this->rolesConfiguration['groupMapping'])) { $this->logger->log('Group mapping found but no party mapping for username set', LOG_ALERT); } } }
/** * Merges the session and manager tokens. All manager tokens types will be in the result array * If a specific type is found in the session this token replaces the one (of the same type) * given by the manager. * * @param array $managerTokens Array of tokens provided by the authentication manager * @param array $sessionTokens Array of tokens restored from the session * @return array Array of \TYPO3\Flow\Security\Authentication\TokenInterface objects */ protected function mergeTokens($managerTokens, $sessionTokens) { $resultTokens = array(); if (!is_array($managerTokens)) { return $resultTokens; } /** @var $managerToken \TYPO3\Flow\Security\Authentication\TokenInterface */ foreach ($managerTokens as $managerToken) { $noCorrespondingSessionTokenFound = true; if (!is_array($sessionTokens)) { continue; } /** @var $sessionToken \TYPO3\Flow\Security\Authentication\TokenInterface */ foreach ($sessionTokens as $sessionToken) { if ($sessionToken->getAuthenticationProviderName() === $managerToken->getAuthenticationProviderName()) { $session = $this->sessionManager->getCurrentSession(); $this->securityLogger->log(sprintf('Session %s contains auth token %s for provider %s. Status: %s', $session->getId(), get_class($sessionToken), $sessionToken->getAuthenticationProviderName(), $this->tokenStatusLabels[$sessionToken->getAuthenticationStatus()]), LOG_INFO, null, 'Flow'); $resultTokens[$sessionToken->getAuthenticationProviderName()] = $sessionToken; $noCorrespondingSessionTokenFound = false; } } if ($noCorrespondingSessionTokenFound) { $resultTokens[$managerToken->getAuthenticationProviderName()] = $managerToken; } } return $resultTokens; }
/** * Logs calls and result of isPrivilegeTargetGranted() * * @Flow\After("method(TYPO3\Flow\Security\Authorization\PrivilegeManager->isPrivilegeTargetGranted())") * @param JoinPointInterface $joinPoint * @return void */ public function logPrivilegeAccessDecisions(JoinPointInterface $joinPoint) { $decision = $joinPoint->getResult() === true ? 'GRANTED' : 'DENIED'; $message = sprintf('Decided "%s" on privilege "%s".', $decision, $joinPoint->getMethodArgument('privilegeTargetIdentifier')); $this->securityLogger->log($message, \LOG_INFO); }