Description
Package containing the bug
next (Drupal module)
Describe the bug
A clear and concise description of what the bug is.
simple_oauth 6.x contains breaking changes to the /oauth/token route which appear not to work with next-drupal 1.6 and maybe version 2.x as well. This results in previews returning 500 errors to the end user when DRUPAL_CLIENT_ID
and DRUPAL_CLIENT_SECRET
authentication.
The Drupal logs contain warnings like:
The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Hint: Check the `client_id` parameter.
simple_oauth/src/Controller/Oauth2Token.php:
public function token(Request $request): ResponseInterface {
$server_request = $this->httpMessageFactory->createRequest($request);
$server_response = new Response();
$client_id = $request->get('client_id');
$grant_type = $request->get('grant_type');
$scopes = $request->get('scope');
$lock_key = $this->createLockKey($request);
try {
// Try to acquire the lock.
while (!$this->lock->acquire($lock_key)) {
// If we can't acquire the lock, wait for it.
if ($this->lock->wait($lock_key)) {
// Timeout reached after 30 seconds.
throw OAuthServerException::accessDenied('Request timed out. Could not acquire lock.');
}
}
if (empty($client_id)) {
throw OAuthServerException::invalidRequest('client_id');
}
$client_entity = $this->clientRepository->getClientEntity($client_id);
if (empty($client_entity)) {
throw OAuthServerException::invalidClient($server_request);
}
$client_drupal_entity = $client_entity->getDrupalEntity();
// Omitting scopes is not allowed when dealing with client_credentials
// and no default scopes are set.
if (
$grant_type === 'client_credentials' &&
empty($scopes) &&
$client_drupal_entity->get('scopes')->isEmpty()
) {
throw OAuthServerException::invalidRequest('scope');
}
// Respond to the incoming request and fill in the response.
$server = $this->authorizationServerFactory->get($client_drupal_entity);
$response = $server->respondToAccessTokenRequest($server_request, $server_response);
}
catch (OAuthServerException $exception) {
$this->logger->log(
$exception->getCode() < 500 ? LogLevel::NOTICE : LogLevel::ERROR,
$exception->getMessage() . ' Hint: ' . $exception->getHint() . '.'
);
$response = $exception->generateHttpResponse($server_response);
}
finally {
// Release the lock.
$this->lock->release($lock_key);
}
return $response;
}
next-drupal package client:
async getAccessToken(
opts?: DrupalClientAuthClientIdSecret
): Promise<AccessToken> {
if (this.accessToken && this.accessTokenScope === opts?.scope) {
return this.accessToken
}
if (!opts?.clientId || !opts?.clientSecret) {
if (typeof this._auth === "undefined") {
throw new Error(
"auth is not configured. See https://next-drupal.org/docs/client/auth"
)
}
}
if (
!isClientIdSecretAuth(this._auth) ||
(opts && !isClientIdSecretAuth(opts))
) {
throw new Error(
`'clientId' and 'clientSecret' required. See https://next-drupal.org/docs/client/auth`
)
}
const clientId = opts?.clientId || this._auth.clientId
const clientSecret = opts?.clientSecret || this._auth.clientSecret
const url = this.buildUrl(opts?.url || this._auth.url || DEFAULT_AUTH_URL)
if (
this.accessTokenScope === opts?.scope &&
this._token &&
Date.now() < this.tokenExpiresOn
) {
this._debug(`Using existing access token.`)
return this._token
}
this._debug(`Fetching new access token.`)
const basic = Buffer.from(`${clientId}:${clientSecret}`).toString("base64")
let body = `grant_type=client_credentials`
if (opts?.scope) {
body = `${body}&scope=${opts.scope}`
this._debug(`Using scope: ${opts.scope}`)
}
const response = await this.fetch(url.toString(), {
method: "POST",
headers: {
Authorization: `Basic ${basic}`,
Accept: "application/json",
"Content-Type": "application/x-www-form-urlencoded",
},
body,
})
if (!response?.ok) {
await this.handleJsonApiErrors(response)
}
const result: AccessToken = await response.json()
this._debug(result)
this.token = result
this.accessTokenScope = opts?.scope
return result
}
Seems like the simple_oauth module expects client_id
in the body and that scope
is required but next-drupal sends the client_id
& client_secret
as basic auth and I believe scope
is optional (?).
composer.json (https://git.drupalcode.org/project/next/-/blob/1.0.x/composer.json?ref_type=heads#L17) has "drupal/simple_oauth": "^5.0 || ^6.0"
which allows the module to be upgraded to 6.x version.
Expected behavior
client_id should be passed in the body and perhaps the basic auth isn't needed although I believe it's ok in the OAuth2 spec (?).
Steps to reproduce:
- Set up a next-drupal project with preview mode as per https://v1-6.next-drupal.org/learn/preview-mode with the oauth authentication using the simple_oauth module (https://v1-6.next-drupal.org/learn/preview-mode/create-oauth-client)
- Then create content in an entity type rendered by Next (https://v1-6.next-drupal.org/learn/preview-mode/configure-content-types)
- View the content and the preview iframe should show a 500 error and the Drupal logs contain simple_oauth warning messages
Additional context
Had this happen on 2 sites when upgrading from simple_oauth 5.x to 6.x, the workaround we used was to switch to basic_auth authentication instead since the simple_oauth upgrade contained database updates which made it harder to rollback.
I guess this can be "fixed" either by updating getAccessToken
or in the shorter term changing "drupal/simple_oauth": "^5.0 || ^6.0"
to "drupal/simple_oauth": "^5.0"
This was my first dip into the inner workings of the module & package so apologies if any of my assumptions are incorrect :)