- Issue created by @εΈε
- Merge request !176Issue #3509299: Do return empty scopes directely when the grant type is refresh_token even no user identifier is provided β (Merged) created by εΈε
- π³π±Netherlands bojan_dev
I can't reproduce this, it's not possible to refresh the token for any grant type, it is only applicable for the "Authorization Code".
Can you please provide more info about the configured consumer, scope and authorize request?By the way: please don't set the issue status to reviewed, a peer review should not be done on your own work.
- π³π±Netherlands idebr
Can you check the Grant type 'Refresh Token' is enable on your Consumer edit form?
- π¨π³China εΈε
I use a custom grant type which like the deprecated grant type
password
. - π¨π³China εΈε
Consumer created by the lines:
// Create client of oauth2 service. Consumer::create([ 'client_id' => 'xxx_app', 'label' => 'XXX App', 'description' => 'Oauth2 client for XXX.', 'is_default' => FALSE, 'grant_types' => [ 'authorization_code', 'refresh_token', 'password', 'sms', ], 'secret' => '123', 'confidential' => TRUE, 'redirect' => 'https://app.xxx.cn/', 'access_token_expiration' => 300, 'refresh_token_expiration' => 1209600, ])->save();
namespace Drupal\xxx\Plugin\Oauth2Grant; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\consumers\Entity\Consumer; use Drupal\simple_oauth\Plugin\Oauth2GrantBase; use League\OAuth2\Server\Grant\GrantTypeInterface; use League\OAuth2\Server\Grant\PasswordGrant; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\Repositories\UserRepositoryInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** * @Oauth2Grant( * id = "password", * label = @Translation("Password") * ) */ class Password extends Oauth2GrantBase implements ContainerFactoryPluginInterface { /** * @var \League\OAuth2\Server\Repositories\UserRepositoryInterface */ protected $userRepository; /** * @var \League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface */ protected $refreshTokenRepository; /** * The config factory. * * @var \Drupal\Core\Config\ConfigFactoryInterface */ protected $configFactory; /** * Class constructor. */ public function __construct(array $configuration, $plugin_id, $plugin_definition, UserRepositoryInterface $user_repository, RefreshTokenRepositoryInterface $refresh_token_repository, ConfigFactoryInterface $config_factory) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->userRepository = $user_repository; $this->refreshTokenRepository = $refresh_token_repository; $this->configFactory = $config_factory; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { return new static( $configuration, $plugin_id, $plugin_definition, $container->get('user_phone.oauth2.repositories.user'), $container->get('simple_oauth.repositories.refresh_token'), $container->get('config.factory') ); } /** * {@inheritdoc} */ public function getGrantType(Consumer $client): GrantTypeInterface { $refresh_token_enabled = $this->isRefreshTokenEnabled($client); /** @var \Drupal\simple_oauth\Repositories\OptionalRefreshTokenRepositoryInterface $refresh_token_repository */ $refresh_token_repository = $this->refreshTokenRepository; if (!$refresh_token_enabled) { $refresh_token_repository->disableRefreshToken(); } $grant_type = $this->createGrantType(); if ($refresh_token_enabled) { $refresh_token = !$client->get('refresh_token_expiration')->isEmpty ? $client->get('refresh_token_expiration')->value : 1209600; $refresh_token_ttl = new \DateInterval( sprintf('PT%dS', $refresh_token) ); $grant_type->setRefreshTokenTTL($refresh_token_ttl); } return $grant_type; } /** * Create Gran type object. * * @return \League\OAuth2\Server\Grant\GrantTypeInterface * The created object. */ protected function createGrantType(): GrantTypeInterface { return new PasswordGrant($this->userRepository, $this->refreshTokenRepository); } /** * Checks if refresh token is enabled on the client. * * @param \Drupal\consumers\Entity\Consumer $client * The consumer entity. * * @return bool * Returns boolean. */ protected function isRefreshTokenEnabled(Consumer $client): bool { foreach ($client->get('grant_types')->getValue() as $grant_type) { if ($grant_type['value'] === 'refresh_token') { return TRUE; } } return FALSE; } }
namespace Drupal\user_phone\Plugin\Oauth2Grant; use Drupal\user_phone\Oauth2\Grant\SMSGrant; use League\OAuth2\Server\Grant\GrantTypeInterface; /** * Add a custom grant type to allow PhoneNumber + SMS message authentication. * * @Oauth2Grant( * id = "sms", * label = @Translation("SMS") * ) */ class SMS extends Password { /** * {@inheritDoc} */ protected function createGrantType(): GrantTypeInterface { return new SMSGrant( $this->userRepository, $this->refreshTokenRepository, ); } }
namespace Drupal\xxx\Oauth2\Grant; use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; use Drupal\Component\Plugin\Exception\PluginNotFoundException; use Drupal\Core\Extension\MissingDependencyException; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\simple_oauth\Entities\UserEntity; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Grant\PasswordGrant; use League\OAuth2\Server\RequestEvent; use Psr\Http\Message\ServerRequestInterface; /** * SMS grant class. */ class SMSGrant extends PasswordGrant { use StringTranslationTrait; /** * {@inheritdoc} * * @throws \Drupal\Core\Extension\MissingDependencyException * User module must be enabled. */ protected function validateUser(ServerRequestInterface $request, ClientEntityInterface $client): UserEntityInterface { $country = $this->getRequestParameter('country', $request); if (is_null($country)) { throw OAuthServerException::invalidRequest('country'); } $number = $this->getRequestParameter('number', $request); if (is_null($number)) { throw OAuthServerException::invalidRequest('number'); } $code = $this->getRequestParameter('code', $request); if (is_null($code)) { throw OAuthServerException::invalidRequest('code'); } /** @var \Drupal\mobile_number\MobileNumberUtilInterface $util */ $util = \Drupal::service('mobile_number.util'); $mobileNumber = $util->getMobileNumber($number, $country); if ($mobileNumber) { // Check whether the phone number have been registered. try { $users = \Drupal::entityTypeManager() ->getStorage('user') ->loadByProperties(['phone' => $util->getCallableNumber($mobileNumber)]); } catch (InvalidPluginDefinitionException | PluginNotFoundException $e) { throw new MissingDependencyException('Can not find user entity storage, user module of drupal core is necessary.'); } if (count($users) > 1) { // Multiple user have been found. throw OAuthServerException::invalidRequest('number', 'Multi accounts found for the mobile number, please contact administrator.'); } elseif (count($users) === 0) { // No user can be found. throw OAuthServerException::invalidRequest('number', 'Phone number not exist.'); } $user = new UserEntity(); $user->setIdentifier(reset($users)->id()); /** @var \Drupal\user_phone\SmsCodeVerifierInterface $phone_verify */ $phone_verify = \Drupal::service('user_phone.sms_code_verifier'); if ($phone_verify->verify($util->getCallableNumber($mobileNumber), $code)) { return $user; } } $this->getEmitter() ->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidCredentials(); } /** * {@inheritdoc} */ public function getIdentifier(): string { return 'sms'; } }
Scope config entity I use:
langcode: en status: true dependencies: { } id: role_anonymous name: role_anonymous description: 'Role scope for anonymous.' grant_types: refresh_token: status: true description: '' authorization_code: status: true description: '' client_credentials: status: true description: '' password: status: true description: '' sms: status: true description: '' umbrella: false parent: _none granularity_id: role granularity_configuration: role: anonymous
langcode: en status: true dependencies: { } id: role_authenticated name: role_authenticated description: 'Role scope for authenticated.' grant_types: refresh_token: status: true description: '' authorization_code: status: true description: '' client_credentials: status: true description: '' password: status: true description: '' sms: status: true description: '' umbrella: false parent: _none granularity_id: role granularity_configuration: role: authenticated
- π¨π³China εΈε
Why is it not possible to refresh the token for any grant type, it is only applicable for the "Authorization Code"?
Is there any oauth2 protocol pointting this?
- π³π±Netherlands bojan_dev
I was assuming you were only using the grant types that are provided by simple_oauth, based on that assumption that would mean that only the Authorization code grant type supports refresh tokens. But I see you are using the Password grant, which is not supported by simple_oauth: 6.0 due to the following reason: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#s...
You could take a look in 5.2 which still supports the password grant.
- πΊπΈUnited States m.stenta
@εΈε I'm not sure if this is related, but I just ran into this issue with the
simple_oauth_password_grant
module after updating tosimple_oauth
6.0.0-beta10
: π Refreshed access_token is missing scope with league/oauth2-server ^9 Active - πΊπΈUnited States m.stenta
I just tested the change proposed by @εΈε and it fixes the issue in π Refreshed access_token is missing scope with league/oauth2-server ^9 Active !
So maybe it's all the same bug in
simple_oauth
after all.Setting this back to Needs Review.
@bojan_dev please see the steps to reproduce and screenshots in π Refreshed access_token is missing scope with league/oauth2-server ^9 Active - even though it is in the other module's issue queue, the fix proposed by @εΈε fixes what I am seeing.
It makes me wonder if Authorization code grant types are affected too - I haven't tested them. It's very easy to test the password grant issue and see the missing scope in the Simple OAuth UI.
- First commit to issue fork.
- π³π±Netherlands bojan_dev
It appears the following change in thephpleague/oauth2-server is the reason for the BC: https://github.com/thephpleague/oauth2-server/pull/1094.
finalizeScopes
is now being called in the RefreshTokenGrant, which does not set the$userIdentifier
, this means that no scopes are being returned. This has impact on all grant types that support refresh tokens. Sorry that I missed this, also I have read your comments @m.stenta on π± Move to thephpleague/oauth2-server 9.0 Active , those are valid points I will definitely make a major version release next time.Merging MR !176 and will roll out a new stable release.
-
bojan_dev β
committed 418ede91 on 6.0.x authored by
εΈε β
Issue #3509299: Do return empty scopes directely when the grant type is...
-
bojan_dev β
committed 418ede91 on 6.0.x authored by
εΈε β
- πΊπΈUnited States m.stenta
Thank you @bojan_dev! I appreciate everything you do for this module (and Drupal generally)! I am truly grateful!
Automatically closed - issue fixed for 2 weeks with no activity.