1.6 version incompatible with simple_oauth 6.x
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 :)
Getting the same error on next-drupal v2.0 using the Pages Router.
Below is how I set up my Drupal client:
const baseUrl = process.env.NEXT_PUBLIC_DRUPAL_BASE_URL as string
const clientId = process.env.OAUTH_CLIENT_ID as string
const clientSecret = process.env.OAUTH_CLIENT_SECRET as string
export const drupal = new NextDrupalPages(baseUrl, {
apiPrefix: '/api/v1',
auth: {
clientId,
clientSecret,
},
withAuth: true,
})
Do we have any latest update on this issue?
We were able to fix this in our case:
-
we updated
simple_oauthfrom version 5 to 6, and you do need to go and check the configuration (in our case, the new scope config entity type did not have the role property set after the update, adding it made it work -
we also had this bit of custom code implementing hook__next_simple_oauth_private_claims_alter that suddenly failed badly because it assumed that the token would have a user id in it, and for this type of grant it does not have it anymore. (so we added an if to check if the user id is defined).
hope this helps :-)