mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Add OIDC endpoint
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user