mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
feat: browser based cli login
This commit is contained in:
@@ -52,4 +52,11 @@ return [
|
||||
'docs' => 'https://appwrite.io/docs/references/cloud/client-web/account#accountCreatePhoneToken',
|
||||
'enabled' => true,
|
||||
],
|
||||
'cli' => [
|
||||
'name' => 'CLI',
|
||||
'key' => 'cli',
|
||||
'icon' => '/images/users/cli.png',
|
||||
'docs' => 'https://appwrite.io/docs/references/cloud/client-web/account#accountCreateCLIToken',
|
||||
'enabled' => true,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -520,6 +520,28 @@ return [
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('publicKey'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 2048,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('name'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 256,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
]
|
||||
],
|
||||
'indexes' => [
|
||||
|
||||
@@ -171,6 +171,11 @@ return [
|
||||
'description' => 'Invalid token passed in the request.',
|
||||
'code' => 401,
|
||||
],
|
||||
Exception::USER_TOKEN_ALREADY_EXISTS => [
|
||||
'name' => Exception::USER_TOKEN_ALREADY_EXISTS,
|
||||
'description' => 'A token with the same ID already exists.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::USER_PASSWORD_RESET_REQUIRED => [
|
||||
'name' => Exception::USER_PASSWORD_RESET_REQUIRED,
|
||||
'description' => 'The current user requires a password reset.',
|
||||
|
||||
@@ -200,6 +200,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||
$factor = (match ($verifiedToken->getAttribute('type')) {
|
||||
TOKEN_TYPE_MAGIC_URL,
|
||||
TOKEN_TYPE_OAUTH2,
|
||||
TOKEN_TYPE_CLI,
|
||||
TOKEN_TYPE_EMAIL => Type::EMAIL,
|
||||
TOKEN_TYPE_PHONE => Type::PHONE,
|
||||
TOKEN_TYPE_GENERIC => 'token',
|
||||
@@ -2784,6 +2785,145 @@ App::post('/v1/account/tokens/phone')
|
||||
->dynamic($token, Response::MODEL_TOKEN);
|
||||
});
|
||||
|
||||
App::post('/v1/account/tokens/cli')
|
||||
->desc('Create CLI token')
|
||||
->groups(['api', 'account', 'auth'])
|
||||
->label('scope', 'sessions.write')
|
||||
->label('auth.type', 'cli')
|
||||
->label('audits.event', 'session.create')
|
||||
->label('audits.resource', 'user/{response.userId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'account',
|
||||
group: 'tokens',
|
||||
name: 'createCLIToken',
|
||||
description: '/docs/references/account/create-token-cli.md',
|
||||
auth: [AuthType::SESSION, AuthType::JWT],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
model: Response::MODEL_TOKEN,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON,
|
||||
))
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'url:{url},userId:{userId}')
|
||||
->param('publicKey', '', new Text(2048), 'Public key associated with the CLI token. Used for cryptographic authentication.', false)
|
||||
->param('name', '', new Text(256), 'A descriptive name for the CLI token to help identify its purpose or usage.', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('proofForCode')
|
||||
->action(function (string $publicKey, string $name, Request $request, Response $response, Document $user, Database $dbForProject, ProofsCode $proofForCode) {
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$secret = strtoupper(substr($proofForCode->generate(), 0, 6));
|
||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), TOKEN_EXPIRATION_CLI));
|
||||
|
||||
$token = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'userId' => $user->getId(),
|
||||
'userInternalId' => $user->getSequence(),
|
||||
'type' => TOKEN_TYPE_CLI,
|
||||
'secret' => $proofForCode->hash($secret),
|
||||
'expire' => $expire,
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
'publicKey' => $publicKey,
|
||||
'name' => $name,
|
||||
]);
|
||||
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
|
||||
try {
|
||||
$token = $dbForProject->createDocument('tokens', $token
|
||||
->setAttribute('$permissions', [
|
||||
Permission::read(Role::user($user->getId())),
|
||||
Permission::update(Role::user($user->getId())),
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
]));
|
||||
} catch (Duplicate) {
|
||||
throw new Exception(Exception::USER_TOKEN_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
$token->setAttribute('secret', $secret);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->json([
|
||||
'$id' => $token->getId(),
|
||||
'$createdAt' => DateTime::formatTz($token->getAttribute('$createdAt')),
|
||||
'userId' => $token->getAttribute('userId'),
|
||||
'secret' => $secret,
|
||||
'expire' => DateTime::formatTz($token->getAttribute('expire')),
|
||||
]);
|
||||
});
|
||||
|
||||
App::post('/v1/account/sessions/cli')
|
||||
->alias('/v1/account/sessions')
|
||||
->desc('Create CLI session')
|
||||
->groups(['api', 'account', 'auth', 'session'])
|
||||
->label('event', 'users.[userId].sessions.[sessionId].create')
|
||||
->label('scope', 'sessions.write')
|
||||
->label('auth.type', 'cli')
|
||||
->label('audits.event', 'session.create')
|
||||
->label('audits.resource', 'user/{response.userId}')
|
||||
->label('audits.userId', '{response.userId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'account',
|
||||
group: 'sessions',
|
||||
name: 'createCLISession',
|
||||
description: '/docs/references/account/create-session-cli.md',
|
||||
auth: [],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
model: Response::MODEL_SESSION,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'url:{url},userId:{param-userId}')
|
||||
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
|
||||
->param('secret', '', new Text(256), 'Secret of a CLI token generated by login methods. For example, the `createCLIToken` method.')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMails')
|
||||
->inject('hooks')
|
||||
->inject('store')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) {
|
||||
/** @var Utopia\Database\Document $user */
|
||||
$userFromRequest = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId));
|
||||
|
||||
if ($userFromRequest->isEmpty()) {
|
||||
throw new Exception(Exception::USER_INVALID_TOKEN);
|
||||
}
|
||||
|
||||
$verifiedToken = Auth::tokenVerify($userFromRequest->getAttribute('tokens', []), null, $secret, $proofForToken);
|
||||
if (!$verifiedToken) {
|
||||
throw new Exception(Exception::USER_INVALID_TOKEN);
|
||||
}
|
||||
|
||||
$user->setAttributes($userFromRequest->getArrayCopy());
|
||||
|
||||
// work in progress
|
||||
// idea is to create a session, create a JWT and encrypt it using the the public and send it to the client
|
||||
// for providerUid we will use token name, for eg. cli_chiragaggarwal@Chirags-MacBook-Pro.local_1760805986
|
||||
});
|
||||
|
||||
App::post('/v1/account/jwts')
|
||||
->alias('/v1/account/jwt')
|
||||
->desc('Create JWT')
|
||||
|
||||
@@ -100,6 +100,12 @@ App::init()
|
||||
}
|
||||
break;
|
||||
|
||||
case 'cli':
|
||||
if (($auths[Config::getParam('auth')['cli']['key']] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'CLI authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Unsupported authentication route');
|
||||
}
|
||||
|
||||
@@ -111,6 +111,7 @@ const TOKEN_EXPIRATION_RECOVERY = 3600; /* 1 hour */
|
||||
const TOKEN_EXPIRATION_CONFIRM = 3600 * 1; /* 1 hour */
|
||||
const TOKEN_EXPIRATION_OTP = 60 * 15; /* 15 minutes */
|
||||
const TOKEN_EXPIRATION_GENERIC = 60 * 15; /* 15 minutes */
|
||||
const TOKEN_EXPIRATION_CLI = 60 * 15; /* 15 minutes */
|
||||
|
||||
/**
|
||||
* Token Lengths.
|
||||
@@ -133,6 +134,7 @@ const TOKEN_TYPE_PHONE = 6;
|
||||
const TOKEN_TYPE_OAUTH2 = 7;
|
||||
const TOKEN_TYPE_GENERIC = 8;
|
||||
const TOKEN_TYPE_EMAIL = 9; // OTP
|
||||
const TOKEN_TYPE_CLI = 10;
|
||||
|
||||
/**
|
||||
* Session Providers.
|
||||
@@ -144,6 +146,7 @@ const SESSION_PROVIDER_PHONE = 'phone';
|
||||
const SESSION_PROVIDER_OAUTH2 = 'oauth2';
|
||||
const SESSION_PROVIDER_TOKEN = 'token';
|
||||
const SESSION_PROVIDER_SERVER = 'server';
|
||||
const SESSION_PROVIDER_CLI = 'cli';
|
||||
|
||||
/**
|
||||
* Activity associated with user or the app.
|
||||
|
||||
@@ -70,6 +70,7 @@ class Exception extends \Exception
|
||||
public const string USER_ALREADY_EXISTS = 'user_already_exists';
|
||||
public const string USER_BLOCKED = 'user_blocked';
|
||||
public const string USER_INVALID_TOKEN = 'user_invalid_token';
|
||||
public const string USER_TOKEN_ALREADY_EXISTS = 'user_token_already_exists';
|
||||
public const string USER_PASSWORD_RESET_REQUIRED = 'user_password_reset_required';
|
||||
public const string USER_EMAIL_NOT_WHITELISTED = 'user_email_not_whitelisted';
|
||||
public const string USER_IP_NOT_WHITELISTED = 'user_ip_not_whitelisted';
|
||||
|
||||
Reference in New Issue
Block a user