Add OIDC endpoint

This commit is contained in:
Matej Bačo
2026-04-25 10:20:00 +02:00
parent 8200d079c6
commit ffd0dbd406
5 changed files with 261 additions and 0 deletions
+2
View File
@@ -125,6 +125,7 @@ use Appwrite\Utopia\Response\Model\OAuth2Gitlab;
use Appwrite\Utopia\Response\Model\OAuth2Google;
use Appwrite\Utopia\Response\Model\OAuth2Linkedin;
use Appwrite\Utopia\Response\Model\OAuth2Notion;
use Appwrite\Utopia\Response\Model\OAuth2Oidc;
use Appwrite\Utopia\Response\Model\OAuth2Paypal;
use Appwrite\Utopia\Response\Model\OAuth2PaypalSandbox;
use Appwrite\Utopia\Response\Model\OAuth2Podio;
@@ -421,6 +422,7 @@ Response::setModel(new OAuth2PaypalSandbox());
Response::setModel(new OAuth2Gitlab());
Response::setModel(new OAuth2Authentik());
Response::setModel(new OAuth2Auth0());
Response::setModel(new OAuth2Oidc());
Response::setModel(new OAuth2Apple());
Response::setModel(new PolicyPasswordDictionary());
Response::setModel(new PolicyPasswordHistory());
@@ -0,0 +1,182 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Oidc;
use Appwrite\Auth\OAuth2\Oidc;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
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;
class Update extends Base
{
public static function getProviderId(): string
{
return 'oidc';
}
public static function getProviderClass(): string
{
return Oidc::class;
}
public static function getProviderLabel(): string
{
return 'Oidc';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Oidc';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_OIDC;
}
public static function getClientIdDescription(): string
{
return 'Client ID of OpenID Connect OAuth2 app. For example: qibI2x0000000000000000000000000006L2YFoG';
}
public static function getClientSecretDescription(): string
{
return 'Client Secret of OpenID Connect OAuth2 app. For example: Ah68ed000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003qpcHV';
}
public function __construct()
{
$providerId = static::getProviderId();
$providerLabel = static::getProviderLabel();
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/project/oauth2/' . $providerId)
->desc('Update project OAuth2 ' . $providerLabel)
->groups(['api', 'project'])
->label('scope', 'oauth2.write')
->label('event', 'oauth2.' . $providerId . '.update')
->label('audits.event', 'project.oauth2.' . $providerId . '.update')
->label('audits.resource', 'project.oauth2/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'oauth2',
name: static::getProviderSDKMethod(),
description: 'Update the project OAuth2 ' . $providerLabel . ' configuration.',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: static::getResponseModel(),
)
],
))
->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()), '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()), '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()), 'OpenID Connect token endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/token', optional: true)
->param('userInfoUrl', null, new Nullable(new URL()), 'OpenID Connect user info endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/userinfo', 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)
->inject('response')
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->callback($this->handle(...));
}
/**
* Custom callback used instead of the parent's `action()` because OIDC takes
* a well-known URL plus three discovery URLs (authorization, token, user
* info), all stored together with the client secret as JSON. The method is
* named differently to avoid an LSP-incompatible override of Base::action().
*
* Enabling the provider requires either a non-empty `wellKnownEndpoint`,
* or all three of `authorizationEndpoint`, `tokenEndpoint`, and
* `userInfoEndpoint` to be set. The check considers the merged state of
* existing stored values plus the new values from the request, so callers
* can enable the provider in a single request without re-sending fields
* that were configured previously.
*/
public function handle(
?string $clientId,
?string $clientSecret,
?string $wellKnownURL,
?string $authorizationURL,
?string $tokenUrl,
?string $userInfoUrl,
?bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
Authorization $authorization
): void {
$providerId = static::getProviderId();
// 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) ?: [];
}
$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']);
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);
$oAuthProviders = $project->getAttribute('oAuthProviders', []);
$storedRaw = $oAuthProviders[$providerId . 'Secret'] ?? '';
$decoded = [];
if (!empty($storedRaw)) {
$decoded = \json_decode($storedRaw, true) ?: [];
}
$response->dynamic(new Document([
'$id' => $providerId,
'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false,
static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '',
static::getClientSecretParamName() => $decoded['clientSecret'] ?? '',
'wellKnownURL' => $decoded['wellKnownEndpoint'] ?? '',
'authorizationURL' => $decoded['authorizationEndpoint'] ?? '',
'tokenUrl' => $decoded['tokenEndpoint'] ?? '',
'userInfoUrl' => $decoded['userInfoEndpoint'] ?? '',
]), static::getResponseModel());
}
}
@@ -35,6 +35,7 @@ use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Gitlab\Update as Updat
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Google\Update as UpdateOAuth2Google;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Linkedin\Update as UpdateOAuth2Linkedin;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Notion\Update as UpdateOAuth2Notion;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Oidc\Update as UpdateOAuth2Oidc;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Paypal\Update as UpdateOAuth2Paypal;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\PaypalSandbox\Update as UpdateOAuth2PaypalSandbox;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Podio\Update as UpdateOAuth2Podio;
@@ -201,5 +202,6 @@ class Http extends Service
$this->addAction(UpdateOAuth2Gitlab::getName(), new UpdateOAuth2Gitlab());
$this->addAction(UpdateOAuth2Authentik::getName(), new UpdateOAuth2Authentik());
$this->addAction(UpdateOAuth2Auth0::getName(), new UpdateOAuth2Auth0());
$this->addAction(UpdateOAuth2Oidc::getName(), new UpdateOAuth2Oidc());
}
}
+1
View File
@@ -313,6 +313,7 @@ class Response extends SwooleResponse
public const MODEL_OAUTH2_GITLAB = 'oAuth2Gitlab';
public const MODEL_OAUTH2_AUTHENTIK = 'oAuth2Authentik';
public const MODEL_OAUTH2_AUTH0 = 'oAuth2Auth0';
public const MODEL_OAUTH2_OIDC = 'oAuth2Oidc';
public const MODEL_OAUTH2_APPLE = 'oAuth2Apple';
// Health
@@ -0,0 +1,74 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Oidc extends OAuth2Base
{
public function getProviderLabel(): string
{
return 'OpenID Connect';
}
public function getClientIdExample(): string
{
return 'qibI2x0000000000000000000000000006L2YFoG';
}
public function getClientSecretExample(): string
{
return 'Ah68ed000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003qpcHV';
}
public function __construct()
{
parent::__construct();
$this
->addRule('wellKnownURL', [
'type' => self::TYPE_STRING,
'description' => 'OpenID Connect well-known configuration URL. When set, authorization, token, and user info endpoints can be discovered automatically.',
'default' => '',
'example' => 'https://myoauth.com/.well-known/openid-configuration',
])
->addRule('authorizationURL', [
'type' => self::TYPE_STRING,
'description' => 'OpenID Connect authorization endpoint URL.',
'default' => '',
'example' => 'https://myoauth.com/oauth2/authorize',
])
->addRule('tokenUrl', [
'type' => self::TYPE_STRING,
'description' => 'OpenID Connect token endpoint URL.',
'default' => '',
'example' => 'https://myoauth.com/oauth2/token',
])
->addRule('userInfoUrl', [
'type' => self::TYPE_STRING,
'description' => 'OpenID Connect user info endpoint URL.',
'default' => '',
'example' => 'https://myoauth.com/oauth2/userinfo',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Oidc';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_OIDC;
}
}