patch to put: OAuth API

This commit is contained in:
Matej Bačo
2026-05-20 13:09:39 +02:00
parent 159127ced5
commit c947cb9f3d
13 changed files with 165 additions and 305 deletions
@@ -28,7 +28,7 @@ class Update extends Action
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) // Should be PUT
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) // Behaves as PUT
->setHttpPath('/v1/project/auth-methods/:methodId')
->httpAlias('/v1/projects/:projectId/auth/:methodId')
->desc('Update project auth method status')
@@ -14,7 +14,6 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Base
@@ -108,7 +107,7 @@ class Update extends Base
$providerLabel = static::getProviderLabel();
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) // Behaves as PUT
->setHttpPath('/v1/project/oauth2/' . $providerId)
->desc('Update project OAuth2 ' . $providerLabel)
->groups(['api', 'project'])
@@ -129,11 +128,11 @@ class Update extends Base
)
],
))
->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
->param('keyId', null, new Nullable(new Text(256, 0)), '\'Key ID\' of Apple OAuth2 app. For example: P4000000N8', optional: true)
->param('teamId', null, new Nullable(new Text(256, 0)), '\'Team ID\' of Apple OAuth2 app. For example: D4000000R6', optional: true)
->param('p8File', null, new Nullable(new Text(4096, 0)), 'Contents of the Apple OAuth2 app .p8 private key file. The secret key wrapped by the PEM markers is 200 characters long. For example: -----BEGIN PRIVATE KEY-----MIGTAg...jy2Xbna-----END PRIVATE KEY-----', optional: true)
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
->param(static::getClientIdParamName(), null, new Text(256, 0), static::getClientIdDescription())
->param('keyId', null, new Text(256, 0), '\'Key ID\' of Apple OAuth2 app. For example: P4000000N8')
->param('teamId', null, new Text(256, 0), '\'Team ID\' of Apple OAuth2 app. For example: D4000000R6')
->param('p8File', null, new Text(4096, 0), 'Contents of the Apple OAuth2 app .p8 private key file. The secret key wrapped by the PEM markers is 200 characters long. For example: -----BEGIN PRIVATE KEY-----MIGTAg...jy2Xbna-----END PRIVATE KEY-----')
->param('enabled', false, new Boolean(), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.')
->inject('response')
->inject('dbForPlatform')
->inject('project')
@@ -166,11 +165,11 @@ class Update extends Base
* avoid an LSP-incompatible override of Base::action().
*/
public function handle(
?string $serviceId,
?string $keyId,
?string $teamId,
?string $p8File,
?bool $enabled,
string $serviceId,
string $keyId,
string $teamId,
string $p8File,
bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
@@ -180,23 +179,11 @@ class Update extends Base
$providerId = static::getProviderId();
$queueForEvents->setParam('providerId', $providerId);
// The secret is stored as JSON `{"p8": "...", "keyID": "...", "teamID": "..."}`
// to match the shape Apple's OAuth2 adapter expects in getAppSecret().
// Merge new values with what's already stored so that submitting only
// some of the fields leaves the rest untouched.
$encodedSecret = null;
if (!\is_null($keyId) || !\is_null($teamId) || !\is_null($p8File)) {
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
$existing = [];
if (!empty($storedRaw)) {
$existing = \json_decode($storedRaw, true) ?: [];
}
$encodedSecret = \json_encode([
'p8' => $p8File ?? ($existing['p8'] ?? ''),
'keyID' => $keyId ?? ($existing['keyID'] ?? ''),
'teamID' => $teamId ?? ($existing['teamID'] ?? ''),
]);
}
$encodedSecret = \json_encode([
'p8' => $p8File,
'keyID' => $keyId,
'teamID' => $teamId,
]);
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $serviceId, $encodedSecret, $enabled);
@@ -14,7 +14,6 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Base
@@ -82,7 +81,7 @@ class Update extends Base
$providerLabel = static::getProviderLabel();
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) // Behaves as PUT
->setHttpPath('/v1/project/oauth2/' . $providerId)
->desc('Update project OAuth2 ' . $providerLabel)
->groups(['api', 'project'])
@@ -103,10 +102,10 @@ class Update extends Base
)
],
))
->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
->param('endpoint', null, new Nullable(new Text(256, 0)), 'Domain of Auth0 instance. For example: example.us.auth0.com', optional: true)
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
->param(static::getClientIdParamName(), null, new Text(256, 0), static::getClientIdDescription())
->param(static::getClientSecretParamName(), null, new Text(512, 0), static::getClientSecretDescription())
->param('endpoint', null, new Text(256, 0), 'Domain of Auth0 instance. For example: example.us.auth0.com')
->param('enabled', false, new Boolean(), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.')
->inject('response')
->inject('dbForPlatform')
->inject('project')
@@ -136,10 +135,10 @@ class Update extends Base
* differently to avoid an LSP-incompatible override of Base::action().
*/
public function handle(
?string $clientId,
?string $clientSecret,
?string $endpoint,
?bool $enabled,
string $clientId,
string $clientSecret,
string $endpoint,
bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
@@ -149,22 +148,10 @@ class Update extends Base
$providerId = static::getProviderId();
$queueForEvents->setParam('providerId', $providerId);
// The secret is stored as JSON `{"clientSecret": "...", "auth0Domain": "..."}`
// to match the shape Auth0's OAuth2 adapter expects (getAuth0Domain()).
// Merge new values with existing storage so that submitting only one of
// `clientSecret`/`endpoint` leaves the other untouched.
$encodedSecret = null;
if (!\is_null($clientSecret) || !\is_null($endpoint)) {
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
$existing = [];
if (!empty($storedRaw)) {
$existing = \json_decode($storedRaw, true) ?: [];
}
$encodedSecret = \json_encode([
'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''),
'auth0Domain' => $endpoint ?? ($existing['auth0Domain'] ?? ''),
]);
}
$encodedSecret = \json_encode([
'clientSecret' => $clientSecret,
'auth0Domain' => $endpoint,
]);
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled);
@@ -14,7 +14,6 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Base
@@ -82,7 +81,7 @@ class Update extends Base
$providerLabel = static::getProviderLabel();
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) // Behaves as PUT
->setHttpPath('/v1/project/oauth2/' . $providerId)
->desc('Update project OAuth2 ' . $providerLabel)
->groups(['api', 'project'])
@@ -103,10 +102,10 @@ class Update extends Base
)
],
))
->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
->param('endpoint', null, new Nullable(new Text(256, 0)), 'Domain of Authentik instance. For example: example.authentik.com', optional: true)
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
->param(static::getClientIdParamName(), null, new Text(256, 0), static::getClientIdDescription())
->param(static::getClientSecretParamName(), null, new Text(512, 0), static::getClientSecretDescription())
->param('endpoint', null, new Text(256, 0), 'Domain of Authentik instance. For example: example.authentik.com')
->param('enabled', false, new Boolean(), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.')
->inject('response')
->inject('dbForPlatform')
->inject('project')
@@ -136,10 +135,10 @@ class Update extends Base
* differently to avoid an LSP-incompatible override of Base::action().
*/
public function handle(
?string $clientId,
?string $clientSecret,
?string $endpoint,
?bool $enabled,
string $clientId,
string $clientSecret,
string $endpoint,
bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
@@ -149,18 +148,9 @@ class Update extends Base
$providerId = static::getProviderId();
$queueForEvents->setParam('providerId', $providerId);
// The secret is stored as JSON `{"clientSecret": "...", "authentikDomain": "..."}`
// to match the shape Authentik's OAuth2 adapter expects (getAuthentikDomain()).
// The `endpoint` param is optional; if omitted, the existing stored endpoint is preserved.
// `clientSecret` is optional; if omitted, the existing stored secret is preserved.
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
$existing = [];
if (!empty($storedRaw)) {
$existing = \json_decode($storedRaw, true) ?: [];
}
$encodedSecret = \json_encode([
'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''),
'authentikDomain' => $endpoint ?? ($existing['authentikDomain'] ?? ''),
'clientSecret' => $clientSecret,
'authentikDomain' => $endpoint,
]);
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled);
@@ -15,7 +15,6 @@ use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
abstract class Base extends Action
@@ -234,7 +233,7 @@ abstract class Base extends Action
$providerLabel = static::getProviderLabel();
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) // Behaves as PUT
->setHttpPath('/v1/project/oauth2/' . $providerId)
->desc('Update project OAuth2 ' . $providerLabel)
->groups(['api', 'project'])
@@ -255,9 +254,9 @@ abstract class Base extends Action
)
],
))
->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
->param(static::getClientIdParamName(), null, new Text(256, 0), static::getClientIdDescription())
->param(static::getClientSecretParamName(), null, new Text(512, 0), static::getClientSecretDescription())
->param('enabled', false, new Boolean(), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.')
->inject('response')
->inject('dbForPlatform')
->inject('project')
@@ -372,9 +371,9 @@ abstract class Base extends Action
Document $project,
Database $dbForPlatform,
Authorization $authorization,
?string $clientId,
?string $clientSecret,
?bool $enabled
string $clientId,
string $clientSecret,
bool $enabled
): Document {
$providerId = static::getProviderId();
if (!(\in_array($providerId, \array_keys(Config::getParam('oAuthProviders'))))) {
@@ -387,19 +386,11 @@ abstract class Base extends Action
$appSecretKey = $providerId . 'Secret';
$enabledKey = $providerId . 'Enabled';
if (!\is_null($clientId)) {
$oAuthProviders[$appIdKey] = $clientId;
}
$oAuthProviders[$appIdKey] = $clientId;
$oAuthProviders[$appSecretKey] = $clientSecret;
$oAuthProviders[$enabledKey] = $enabled;
if (!\is_null($clientSecret)) {
$oAuthProviders[$appSecretKey] = $clientSecret;
}
if (!\is_null($enabled)) {
$oAuthProviders[$enabledKey] = $enabled;
}
if ($enabled === true || \is_null($enabled)) {
if ($enabled === true) {
try {
if (empty($oAuthProviders[$appIdKey]) || empty($oAuthProviders[$appSecretKey])) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Client ID and Client Secret are required when enabling OAuth2 provider.');
@@ -412,12 +403,8 @@ abstract class Base extends Action
if (\method_exists($providerInstance, 'verifyCredentials')) {
$providerInstance->verifyCredentials();
}
$oAuthProviders[$enabledKey] = true;
} catch (\Throwable $err) {
if ($enabled === true) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Could not enable OAuth2 provider: ' . $err->getMessage());
}
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Could not enable OAuth2 provider: ' . $err->getMessage());
}
}
@@ -429,9 +416,9 @@ abstract class Base extends Action
}
public function action(
?string $clientId,
?string $clientSecret,
?bool $enabled,
string $clientId,
string $clientSecret,
bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
@@ -14,7 +14,6 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Base
@@ -82,7 +81,7 @@ class Update extends Base
$providerLabel = static::getProviderLabel();
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) // Behaves as PUT
->setHttpPath('/v1/project/oauth2/' . $providerId)
->desc('Update project OAuth2 ' . $providerLabel)
->groups(['api', 'project'])
@@ -103,10 +102,10 @@ class Update extends Base
)
],
))
->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
->param('endpoint', null, new Nullable(new Text(256, 0)), 'Domain of FusionAuth instance. For example: example.fusionauth.io', optional: true)
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
->param(static::getClientIdParamName(), null, new Text(256, 0), static::getClientIdDescription())
->param(static::getClientSecretParamName(), null, new Text(512, 0), static::getClientSecretDescription())
->param('endpoint', null, new Text(256, 0), 'Domain of FusionAuth instance. For example: example.fusionauth.io')
->param('enabled', false, new Boolean(), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.')
->inject('response')
->inject('dbForPlatform')
->inject('project')
@@ -136,10 +135,10 @@ class Update extends Base
* differently to avoid an LSP-incompatible override of Base::action().
*/
public function handle(
?string $clientId,
?string $clientSecret,
?string $endpoint,
?bool $enabled,
string $clientId,
string $clientSecret,
string $endpoint,
bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
@@ -149,18 +148,9 @@ class Update extends Base
$providerId = static::getProviderId();
$queueForEvents->setParam('providerId', $providerId);
// The secret is stored as JSON `{"clientSecret": "...", "fusionAuthDomain": "..."}`
// to match the shape FusionAuth's OAuth2 adapter expects (getFusionAuthDomain()).
// The `endpoint` param is optional; if omitted, the existing stored endpoint is preserved.
// `clientSecret` is optional; if omitted, the existing stored secret is preserved.
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
$existing = [];
if (!empty($storedRaw)) {
$existing = \json_decode($storedRaw, true) ?: [];
}
$encodedSecret = \json_encode([
'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''),
'fusionAuthDomain' => $endpoint ?? ($existing['fusionAuthDomain'] ?? ''),
'clientSecret' => $clientSecret,
'fusionAuthDomain' => $endpoint,
]);
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled);
@@ -14,7 +14,6 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
@@ -93,7 +92,7 @@ class Update extends Base
$providerLabel = static::getProviderLabel();
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) // Behaves as PUT
->setHttpPath('/v1/project/oauth2/' . $providerId)
->desc('Update project OAuth2 ' . $providerLabel)
->groups(['api', 'project'])
@@ -114,10 +113,10 @@ class Update extends Base
)
],
))
->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
->param('endpoint', null, new Nullable(new URL(allowEmpty: true)), 'Endpoint URL of self-hosted GitLab instance. For example: https://gitlab.com', optional: true)
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
->param(static::getClientIdParamName(), null, new Text(256, 0), static::getClientIdDescription())
->param(static::getClientSecretParamName(), null, new Text(512, 0), static::getClientSecretDescription())
->param('endpoint', null, new URL(allowEmpty: true), 'Endpoint URL of self-hosted GitLab instance. For example: https://gitlab.com')
->param('enabled', false, new Boolean(), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.')
->inject('response')
->inject('dbForPlatform')
->inject('project')
@@ -147,10 +146,10 @@ class Update extends Base
* differently to avoid an LSP-incompatible override of Base::action().
*/
public function handle(
?string $applicationId,
?string $secret,
?string $endpoint,
?bool $enabled,
string $applicationId,
string $secret,
string $endpoint,
bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
@@ -160,22 +159,10 @@ class Update extends Base
$providerId = static::getProviderId();
$queueForEvents->setParam('providerId', $providerId);
// The secret is stored as JSON `{"clientSecret": "...", "endpoint": "..."}`
// so that the Gitlab OAuth2 adapter can extract the endpoint via getEndpoint().
// Merge the new values with what's already stored so that submitting only
// one of `secret`/`endpoint` leaves the other untouched.
$encodedSecret = null;
if (!\is_null($secret) || !\is_null($endpoint)) {
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
$existing = [];
if (!empty($storedRaw)) {
$existing = \json_decode($storedRaw, true) ?: [];
}
$encodedSecret = \json_encode([
'clientSecret' => $secret ?? ($existing['clientSecret'] ?? ''),
'endpoint' => $endpoint ?? ($existing['endpoint'] ?? ''),
]);
}
$encodedSecret = \json_encode([
'clientSecret' => $secret,
'endpoint' => $endpoint,
]);
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $applicationId, $encodedSecret, $enabled);
@@ -16,7 +16,6 @@ use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@@ -85,7 +84,7 @@ class Update extends Base
$providerLabel = static::getProviderLabel();
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) // Behaves as PUT
->setHttpPath('/v1/project/oauth2/' . $providerId)
->desc('Update project OAuth2 ' . $providerLabel)
->groups(['api', 'project'])
@@ -106,10 +105,10 @@ class Update extends Base
)
],
))
->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
->param('prompt', null, new Nullable(new ArrayList(new WhiteList(['none', 'consent', 'select_account'], true), 3)), 'Array of Google OAuth2 prompt values. If "none" is included, it must be the only element. "none" means: don\'t display any authentication or consent screens. Must not be specified with other values. "consent" means: prompt the user for consent. "select_account" means: prompt the user to select an account.', optional: true)
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
->param(static::getClientIdParamName(), null, new Text(256, 0), static::getClientIdDescription())
->param(static::getClientSecretParamName(), null, new Text(512, 0), static::getClientSecretDescription())
->param('prompt', null, new ArrayList(new WhiteList(['none', 'consent', 'select_account'], true), 3), 'Array of Google OAuth2 prompt values. If "none" is included, it must be the only element. "none" means: don\'t display any authentication or consent screens. Must not be specified with other values. "consent" means: prompt the user for consent. "select_account" means: prompt the user to select an account.')
->param('enabled', false, new Boolean(), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.')
->inject('response')
->inject('dbForPlatform')
->inject('project')
@@ -139,10 +138,10 @@ class Update extends Base
* differently to avoid an LSP-incompatible override of Base::action().
*/
public function handle(
?string $clientId,
?string $clientSecret,
?array $prompt,
?bool $enabled,
string $clientId,
string $clientSecret,
array $prompt,
bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
@@ -152,28 +151,17 @@ class Update extends Base
$providerId = static::getProviderId();
$queueForEvents->setParam('providerId', $providerId);
if ($prompt !== null) {
if (empty($prompt)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Prompt array cannot be empty.');
}
if (\in_array('none', $prompt) && \count($prompt) > 1) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When "none" is used as a prompt value, it must be the only element in the array.');
}
if (empty($prompt)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Prompt array cannot be empty.');
}
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
$existing = $this->decodeStoredSecret($project);
// Backwards compatibility: secrets stored before the prompt feature
// were saved as plain strings. Treat the raw value as clientSecret.
if (!empty($storedRaw) && empty($existing)) {
$existing = ['clientSecret' => $storedRaw];
if (\in_array('none', $prompt) && \count($prompt) > 1) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When "none" is used as a prompt value, it must be the only element in the array.');
}
$encodedSecret = \json_encode([
'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''),
'prompt' => $prompt ?? ($existing['prompt'] ?? ['consent']),
'clientSecret' => $clientSecret,
'prompt' => $prompt,
]);
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled);
@@ -14,7 +14,6 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Base
@@ -88,7 +87,7 @@ class Update extends Base
$providerLabel = static::getProviderLabel();
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) // Behaves as PUT
->setHttpPath('/v1/project/oauth2/' . $providerId)
->desc('Update project OAuth2 ' . $providerLabel)
->groups(['api', 'project'])
@@ -109,11 +108,11 @@ class Update extends Base
)
],
))
->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
->param('endpoint', null, new Nullable(new Text(256, 0)), 'Domain of Keycloak instance. For example: keycloak.example.com', optional: true)
->param('realmName', null, new Nullable(new Text(256, 0)), 'Keycloak realm name. For example: appwrite-realm', optional: true)
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
->param(static::getClientIdParamName(), null, new Text(256, 0), static::getClientIdDescription())
->param(static::getClientSecretParamName(), null, new Text(512, 0), static::getClientSecretDescription())
->param('endpoint', null, new Text(256, 0), 'Domain of Keycloak instance. For example: keycloak.example.com')
->param('realmName', null, new Text(256, 0), 'Keycloak realm name. For example: appwrite-realm')
->param('enabled', false, new Boolean(), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.')
->inject('response')
->inject('dbForPlatform')
->inject('project')
@@ -145,11 +144,11 @@ class Update extends Base
* Base::action().
*/
public function handle(
?string $clientId,
?string $clientSecret,
?string $endpoint,
?string $realmName,
?bool $enabled,
string $clientId,
string $clientSecret,
string $endpoint,
string $realmName,
bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
@@ -159,19 +158,10 @@ class Update extends Base
$providerId = static::getProviderId();
$queueForEvents->setParam('providerId', $providerId);
// The secret is stored as JSON `{"clientSecret": "...", "keycloakDomain": "...", "keycloakRealm": "..."}`
// to match the shape Keycloak's OAuth2 adapter expects (getKeycloakDomain(), getKeycloakRealm()).
// The `endpoint` and `realmName` params are optional; if omitted, existing stored values are preserved.
// `clientSecret` is optional; if omitted, the existing stored secret is preserved.
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
$existing = [];
if (!empty($storedRaw)) {
$existing = \json_decode($storedRaw, true) ?: [];
}
$encodedSecret = \json_encode([
'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''),
'keycloakDomain' => $endpoint ?? ($existing['keycloakDomain'] ?? ''),
'keycloakRealm' => $realmName ?? ($existing['keycloakRealm'] ?? ''),
'clientSecret' => $clientSecret,
'keycloakDomain' => $endpoint,
'keycloakRealm' => $realmName,
]);
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled);
@@ -14,7 +14,6 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Base
@@ -92,7 +91,7 @@ class Update extends Base
$providerLabel = static::getProviderLabel();
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) // Behaves as PUT
->setHttpPath('/v1/project/oauth2/' . $providerId)
->desc('Update project OAuth2 ' . $providerLabel)
->groups(['api', 'project'])
@@ -113,10 +112,10 @@ class Update extends Base
)
],
))
->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
->param('tenant', null, new Nullable(new Text(256, 0)), 'Microsoft Entra ID tenant identifier. Use \'common\', \'organizations\', \'consumers\' or a specific tenant ID. For example: common', true)
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
->param(static::getClientIdParamName(), null, new Text(256, 0), static::getClientIdDescription())
->param(static::getClientSecretParamName(), null, new Text(512, 0), static::getClientSecretDescription())
->param('tenant', null, new Text(256, 0), 'Microsoft Entra ID tenant identifier. Use \'common\', \'organizations\', \'consumers\' or a specific tenant ID. For example: common')
->param('enabled', false, new Boolean(), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.')
->inject('response')
->inject('dbForPlatform')
->inject('project')
@@ -146,10 +145,10 @@ class Update extends Base
* differently to avoid an LSP-incompatible override of Base::action().
*/
public function handle(
?string $applicationId,
?string $applicationSecret,
?string $tenant,
?bool $enabled,
string $applicationId,
string $applicationSecret,
string $tenant,
bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
@@ -159,18 +158,9 @@ class Update extends Base
$providerId = static::getProviderId();
$queueForEvents->setParam('providerId', $providerId);
// The secret is stored as JSON `{"clientSecret": "...", "tenantID": "..."}`
// to match the shape Microsoft's OAuth2 adapter expects (getTenantID()).
// The `tenant` param is optional; if omitted, the existing stored tenant is preserved.
// `applicationSecret` is optional; if omitted, the existing stored secret is preserved.
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
$existing = [];
if (!empty($storedRaw)) {
$existing = \json_decode($storedRaw, true) ?: [];
}
$encodedSecret = \json_encode([
'clientSecret' => $applicationSecret ?? ($existing['clientSecret'] ?? ''),
'tenantID' => $tenant ?? ($existing['tenantID'] ?? ''),
'clientSecret' => $applicationSecret,
'tenantID' => $tenant,
]);
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $applicationId, $encodedSecret, $enabled);
@@ -15,7 +15,6 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
@@ -102,7 +101,7 @@ class Update extends Base
$providerLabel = static::getProviderLabel();
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) // Behaves as PUT
->setHttpPath('/v1/project/oauth2/' . $providerId)
->desc('Update project OAuth2 ' . $providerLabel)
->groups(['api', 'project'])
@@ -123,13 +122,13 @@ class Update extends Base
)
],
))
->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
->param('wellKnownURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect well-known configuration URL. When provided, authorization, token, and user info endpoints can be discovered automatically. For example: https://myoauth.com/.well-known/openid-configuration', optional: true)
->param('authorizationURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect authorization endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/authorize', optional: true)
->param('tokenURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect token endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/token', optional: true, aliases: ['tokenUrl'])
->param('userInfoURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect user info endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/userinfo', optional: true, aliases: ['userInfoUrl'])
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
->param(static::getClientIdParamName(), null, new Text(256, 0), static::getClientIdDescription())
->param(static::getClientSecretParamName(), null, new Text(512, 0), static::getClientSecretDescription())
->param('wellKnownURL', null, new URL(allowEmpty: true), 'OpenID Connect well-known configuration URL. When provided, authorization, token, and user info endpoints can be discovered automatically. For example: https://myoauth.com/.well-known/openid-configuration')
->param('authorizationURL', null, new URL(allowEmpty: true), 'OpenID Connect authorization endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/authorize')
->param('tokenURL', null, new URL(allowEmpty: true), 'OpenID Connect token endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/token', aliases: ['tokenUrl'])
->param('userInfoURL', null, new URL(allowEmpty: true), 'OpenID Connect user info endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/userinfo', aliases: ['userInfoUrl'])
->param('enabled', false, new Boolean(), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.')
->inject('response')
->inject('dbForPlatform')
->inject('project')
@@ -170,13 +169,13 @@ class Update extends Base
* that were configured previously.
*/
public function handle(
?string $clientId,
?string $clientSecret,
?string $wellKnownURL,
?string $authorizationURL,
?string $tokenURL,
?string $userInfoURL,
?bool $enabled,
string $clientId,
string $clientSecret,
string $wellKnownURL,
string $authorizationURL,
string $tokenURL,
string $userInfoURL,
bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
@@ -186,41 +185,25 @@ class Update extends Base
$providerId = static::getProviderId();
$queueForEvents->setParam('providerId', $providerId);
// The secret is stored as JSON
// `{"clientSecret": "...", "wellKnownEndpoint": "...", "authorizationEndpoint": "...", "tokenEndpoint": "...", "userInfoEndpoint": "..."}`
// so that the OIDC OAuth2 adapter can extract each endpoint individually.
// Merge new values with what's already stored so that submitting only a
// subset of fields leaves the others untouched.
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
$existing = [];
if (!empty($storedRaw)) {
$existing = \json_decode($storedRaw, true) ?: [];
}
$encodedSecret = \json_encode([
'clientSecret' => $clientSecret,
'wellKnownEndpoint' => $wellKnownURL,
'authorizationEndpoint' => $authorizationURL,
'tokenEndpoint' => $tokenURL,
'userInfoEndpoint' => $userInfoURL,
]);
$merged = [
'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''),
'wellKnownEndpoint' => $wellKnownURL ?? ($existing['wellKnownEndpoint'] ?? ''),
'authorizationEndpoint' => $authorizationURL ?? ($existing['authorizationEndpoint'] ?? ''),
'tokenEndpoint' => $tokenURL ?? ($existing['tokenEndpoint'] ?? ''),
'userInfoEndpoint' => $userInfoURL ?? ($existing['userInfoEndpoint'] ?? ''),
];
// When enabling, require either wellKnownEndpoint alone, or all three
// discovery URLs (authorization, token, user info). Skip this check
// when disabling or when leaving the enabled flag unchanged.
if ($enabled === true) {
$hasWellKnown = !empty($merged['wellKnownEndpoint']);
$hasAllDiscovery = !empty($merged['authorizationEndpoint'])
&& !empty($merged['tokenEndpoint'])
&& !empty($merged['userInfoEndpoint']);
$hasWellKnown = !empty($wellKnownURL);
$hasAllDiscovery = !empty($authorizationURL)
&& !empty($tokenURL)
&& !empty($userInfoURL);
if (!$hasWellKnown && !$hasAllDiscovery) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Enabling OpenID Connect requires either wellKnownURL, or all of authorizationURL, tokenURL, and userInfoURL.');
}
}
$encodedSecret = \json_encode($merged);
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled);
// Reuse buildReadResponse to keep PATCH/GET shapes identical and
@@ -16,7 +16,6 @@ use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Boolean;
use Utopia\Validator\Domain as ValidatorDomain;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Base
@@ -90,7 +89,7 @@ class Update extends Base
$providerLabel = static::getProviderLabel();
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) // Behaves as PUT
->setHttpPath('/v1/project/oauth2/' . $providerId)
->desc('Update project OAuth2 ' . $providerLabel)
->groups(['api', 'project'])
@@ -111,11 +110,11 @@ class Update extends Base
)
],
))
->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
->param('domain', null, new Nullable(new ValidatorDomain(allowEmpty: true)), 'Okta company domain. Required when enabling the provider. For example: trial-6400025.okta.com. Example of wrong value: trial-6400025-admin.okta.com, or https://trial-6400025.okta.com/', optional: true)
->param('authorizationServerId', null, new Nullable(new Text(256, 0)), 'Custom Authorization Servers. Optional, can be left empty or unconfigured. For example: aus000000000000000h7z', optional: true)
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
->param(static::getClientIdParamName(), null, new Text(256, 0), static::getClientIdDescription())
->param(static::getClientSecretParamName(), null, new Text(512, 0), static::getClientSecretDescription())
->param('domain', null, new ValidatorDomain(allowEmpty: true), 'Okta company domain. Required when enabling the provider. For example: trial-6400025.okta.com. Example of wrong value: trial-6400025-admin.okta.com, or https://trial-6400025.okta.com/')
->param('authorizationServerId', null, new Text(256, 0), 'Custom Authorization Servers. Optional, can be left empty or unconfigured. For example: aus000000000000000h7z')
->param('enabled', false, new Boolean(), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.')
->inject('response')
->inject('dbForPlatform')
->inject('project')
@@ -147,11 +146,11 @@ class Update extends Base
* Base::action().
*/
public function handle(
?string $clientId,
?string $clientSecret,
?string $domain,
?string $authorizationServerId,
?bool $enabled,
string $clientId,
string $clientSecret,
string $domain,
string $authorizationServerId,
bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
@@ -161,32 +160,14 @@ class Update extends Base
$providerId = static::getProviderId();
$queueForEvents->setParam('providerId', $providerId);
// The secret is stored as JSON `{"clientSecret": "...", "oktaDomain": "...", "authorizationServerId": "..."}`
// to match the shape Okta's OAuth2 adapter expects.
// Merge new values with existing storage so that submitting only some of
// the parameters leaves the others untouched.
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
$existing = [];
if (!empty($storedRaw)) {
$existing = \json_decode($storedRaw, true) ?: [];
}
$encodedSecret = \json_encode([
'clientSecret' => $clientSecret,
'oktaDomain' => $domain,
'authorizationServerId' => $authorizationServerId,
]);
$encodedSecret = null;
if (!\is_null($clientSecret) || !\is_null($domain) || !\is_null($authorizationServerId)) {
$encodedSecret = \json_encode([
'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''),
'oktaDomain' => $domain ?? ($existing['oktaDomain'] ?? ''),
'authorizationServerId' => $authorizationServerId ?? ($existing['authorizationServerId'] ?? ''),
]);
}
// Domain is required when enabling the provider, since Okta builds its
// authorization, token and userinfo URLs from it.
if ($enabled === true) {
$effectiveDomain = $domain ?? ($existing['oktaDomain'] ?? '');
if (empty($effectiveDomain)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain is required when enabling Okta OAuth2 provider.');
}
if ($enabled === true && empty($domain)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain is required when enabling Okta OAuth2 provider.');
}
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled);
@@ -30,7 +30,7 @@ class Update extends Action
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) // Should be PUT
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) // Behaves as PUT
->setHttpPath('/v1/project/templates/email')
->httpAlias('/v1/projects/:projectId/templates/email')
->httpAlias('/v1/projects/:projectId/templates/email/:templateId/:locale')