Alert on new session creation

This commit is contained in:
Khushboo Verma
2023-12-06 17:35:09 +01:00
20 changed files with 270 additions and 49 deletions
+3 -1
View File
@@ -98,7 +98,9 @@ _APP_VCS_GITHUB_CLIENT_SECRET=
_APP_VCS_GITHUB_WEBHOOK_SECRET=
_APP_MIGRATIONS_FIREBASE_CLIENT_ID=
_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET=
_APP_ASSISTANT_OPENAI_API_KEY=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
_APP_ASSISTANT_OPENAI_API_KEY=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
_APP_MESSAGE_SMS_TEST_DSN=
_APP_MESSAGE_EMAIL_TEST_DSN=
_APP_MESSAGE_PUSH_TEST_DSN=
_APP_CONSOLE_SUPABASE_APP_ID=d64cd75f-2786-4b94-aeda-eae01e165941
_APP_CONSOLE_SUPABASE_SECRET=sba_d5a802fbdcdf86d27f7d071a2778bff0419040de
+1 -1
View File
@@ -207,7 +207,7 @@ return [
],
Exception::USER_PASSWORD_AI => [
'name' => Exception::USER_PASSWORD_AI,
'description' => 'AI does not like security of the password you are trying to use. For your security, please choose a different password and try again.',
'description' => 'As per AI, your password is as strong as Grandior wifi. Please choose a different password and try again.',
'code' => 400,
],
Exception::USER_SESSION_NOT_FOUND => [
@@ -6,6 +6,8 @@
<a href="{{redirect}}" target="_blank">{{redirect}}</a>
<b>{{code}}</b>
<p>{{footer}}</p>
<br>
+1
View File
@@ -12,6 +12,7 @@
"emails.magicSession.subject": "Login",
"emails.magicSession.hello": "Hey,",
"emails.magicSession.body": "Follow this link to login.",
"emails.magicSession.codeBody": "Enter this code to login.",
"emails.magicSession.footer": "If you didnt ask to login using this email, you can ignore this message.",
"emails.magicSession.thanks": "Thanks",
"emails.magicSession.signature": "{{project}} team",
+10
View File
@@ -152,6 +152,16 @@ return [
'beta' => false,
'mock' => false,
],
'supabase' => [
'name' => 'Supabase',
'developers' => 'https://supabase.com/docs',
'icon' => 'icon-supabase',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false,
],
'gitlab' => [
'name' => 'GitLab',
'developers' => 'https://docs.gitlab.com/ee/api/',
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+21 -4
View File
@@ -1014,7 +1014,8 @@ App::post('/v1/account/sessions/magic-url')
->label('abuse-key', 'url:{url},email:{param-email}')
->param('userId', '', new CustomId(), 'Unique 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('email', '', new Email(), 'User email.')
->param('url', '', fn ($clients) => new Host($clients), 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients'])
->param('url', '', fn($clients) => new Host($clients), 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients'])
->param('type', 'link', new WhiteList(['link', 'code']), 'The type of verification email to be sent. ', true)
->inject('request')
->inject('response')
->inject('user')
@@ -1023,7 +1024,7 @@ App::post('/v1/account/sessions/magic-url')
->inject('locale')
->inject('queueForEvents')
->inject('queueForMails')
->action(function (string $userId, string $email, string $url, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) {
->action(function (string $userId, string $email, string $url, string $type, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) {
if (empty(App::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
@@ -1088,6 +1089,14 @@ App::post('/v1/account/sessions/magic-url')
$loginSecret = Auth::tokenGenerator();
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM));
if ($type === 'code') {
$loginSecret = Auth::codeGenerator();
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_PHONE));
} else if($type === 'link') {
$loginSecret = Auth::tokenGenerator();
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM));
}
$token = new Document([
'$id' => ID::unique(),
'userId' => $user->getId(),
@@ -1118,7 +1127,7 @@ App::post('/v1/account/sessions/magic-url')
$url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $user->getId(), 'secret' => $loginSecret, 'expire' => $expire, 'project' => $project->getId()]);
$url = Template::unParseURL($url);
$body = $locale->getText("emails.magicSession.body");
$body = $type === 'code' ? $locale->getText("emails.magicSession.codeBody") : $locale->getText("emails.magicSession.body");
$subject = $locale->getText("emails.magicSession.subject");
$customTemplate = $project->getAttribute('templates', [])['email.magicSession-' . $locale->default] ?? [];
@@ -1183,7 +1192,8 @@ App::post('/v1/account/sessions/magic-url')
'user' => '',
'team' => '',
'project' => $project->getAttribute('name'),
'redirect' => $url
'redirect' => $type === 'link' ? $url : '',
'code' => $type === 'code' ? $loginSecret : '',
];
$queueForMails
@@ -2138,6 +2148,13 @@ App::patch('/v1/account/password')
}
}
if ($project->getAttribute('auths', [])['passwordAi'] ?? false) {
$passwordAiValidator = new PasswordAi();
if (!$passwordAiValidator->isValid($password)) {
throw new Exception(Exception::USER_PASSWORD_AI);
}
}
if ($project->getAttribute('auths', [])['passwordDictionary'] ?? false) {
$passwordDictionaryLength = $project->getAttribute('auths', [])['passwordDictionaryLength'] ?? '10k';
if ($passwordDictionaryLength == '10k') {
+4 -33
View File
@@ -85,7 +85,7 @@ App::post('/v1/projects')
}
$auth = Config::getParam('auth', []);
$auths = ['limit' => 0, 'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT, 'passwordHistory' => 0, 'passwordAi' => false, 'sessionRefresh' => false, 'passwordDictionary' => false, 'passwordDictionaryLength' => '10k', 'notify' => true, 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, 'personalDataCheck' => false];
$auths = ['limit' => 0, 'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT, 'passwordHistory' => 0, 'passwordAi' => false, 'renewal' => false, 'passwordDictionary' => false, 'passwordDictionaryLength' => '10k', 'notify' => false, 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, 'personalDataCheck' => false];
foreach ($auth as $index => $method) {
$auths[$method['key'] ?? ''] = true;
}
@@ -674,9 +674,10 @@ App::patch('/v1/projects/:projectId/auth/duration')
->label('sdk.response.model', Response::MODEL_PROJECT)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('duration', 31536000, new Range(0, 31536000), 'Project session length in seconds. Max length: 31536000 seconds.')
->param('renewal', false, new Boolean(), 'Automatic session refresh. If enabled, sessions are automatically extended to session duration on every request.', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, int $duration, Response $response, Database $dbForConsole) {
->action(function (string $projectId, int $duration, bool $renewal, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -686,37 +687,7 @@ App::patch('/v1/projects/:projectId/auth/duration')
$auths = $project->getAttribute('auths', []);
$auths['duration'] = $duration;
$dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/auth/session-refresh')
->desc('Update project session refresh')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'updateSessionRefresh')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PROJECT)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('sessionRefresh', false, new Boolean(), 'Automatic session refresh. If enabled, sessions are automatically extended to session duration on every request.', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, int $sessionRefresh, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$auths = $project->getAttribute('auths', []);
$auths['sessionRefresh'] = $sessionRefresh;
$auths['renewal'] = $renewal;
$dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
+15
View File
@@ -2,6 +2,7 @@
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\Password;
use Appwrite\Auth\Validator\PasswordAi;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
@@ -71,6 +72,13 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
}
}
if ($project->getAttribute('auths', [])['passwordAi'] ?? false) {
$passwordAiValidator = new PasswordAi();
if (!$passwordAiValidator->isValid($password)) {
throw new Exception(Exception::USER_PASSWORD_AI);
}
}
$password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null;
$user = $dbForProject->createDocument('users', new Document([
'$id' => $userId,
@@ -1114,6 +1122,13 @@ App::patch('/v1/users/:userId/password')
throw new Exception(Exception::USER_PASSWORD_PERSONAL_DATA);
}
}
if ($project->getAttribute('auths', [])['passwordAi'] ?? false) {
$passwordAiValidator = new PasswordAi();
if (!$passwordAiValidator->isValid($password)) {
throw new Exception(Exception::USER_PASSWORD_AI);
}
}
if ($project->getAttribute('auths', [])['passwordDictionary'] ?? false) {
$passwordDictionaryLength = $project->getAttribute('auths', [])['passwordDictionaryLength'] ?? '10k';
+1 -1
View File
@@ -172,7 +172,7 @@ App::init()
/*
* Session refresh
*/
if ($project->getAttribute('auths', [])['sessionRefresh'] ?? false) {
if ($project->getAttribute('auths', [])['renewal'] ?? false) {
if ($user && !$user->isEmpty()) {
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$currentSessionId = Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret, $authDuration);
+6 -1
View File
@@ -175,6 +175,8 @@ const COMPRESSION_TYPE_NONE = 'none';
const COMPRESSION_TYPE_GZIP = 'gzip';
const COMPRESSION_TYPE_ZSTD = 'zstd';
// Mail Types
const MAIL_TYPE_VERIFICATION_URL = 'verificationUrl';
const MAIL_TYPE_VERIFICATION_CODE = 'verificationCode';
const MAIL_TYPE_VERIFICATION = 'verification';
const MAIL_TYPE_MAGIC_SESSION = 'magicSession';
const MAIL_TYPE_RECOVERY = 'recovery';
@@ -1231,7 +1233,10 @@ App::setResource('console', function () {
'oAuthProviders' => [
'githubEnabled' => true,
'githubSecret' => App::getEnv('_APP_CONSOLE_GITHUB_SECRET', ''),
'githubAppid' => App::getEnv('_APP_CONSOLE_GITHUB_APP_ID', '')
'githubAppid' => App::getEnv('_APP_CONSOLE_GITHUB_APP_ID', ''),
'supabaseEnabled' => true,
'supabaseSecret' => App::getEnv('_APP_CONSOLE_SUPABASE_SECRET', ''),
'supabaseAppid' => App::getEnv('_APP_CONSOLE_SUPABASE_APP_ID', ''),
],
]);
}, []);
+2
View File
@@ -191,6 +191,8 @@ services:
- _APP_MESSAGE_SMS_TEST_DSN
- _APP_MESSAGE_EMAIL_TEST_DSN
- _APP_MESSAGE_PUSH_TEST_DSN
- _APP_CONSOLE_SUPABASE_SECRET
- _APP_CONSOLE_SUPABASE_APP_ID
appwrite-realtime:
entrypoint: realtime
<<: *x-logging
+183
View File
@@ -0,0 +1,183 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
// Reference Material
// https://supabase.com/docs/guides/platform/oauth-apps/build-a-supabase-integration
class Supabase extends OAuth2
{
/**
* @var string
*/
private string $endpoint = 'https://api.supabase.com/v1';
/**
* @var array
*/
protected array $user = [];
/**
* @var array
*/
protected array $tokens = [];
/**
* @return string
*/
public function getName(): string
{
return 'supabase';
}
/**
* @return string
*/
public function getLoginURL(): string
{
$url = $this->endpoint . '/oauth/authorize?' .
\http_build_query([
'response_type' => 'code',
'client_id' => $this->appID,
'state' => \json_encode($this->state),
'redirect_uri' => $this->callback
]);
return $url;
}
/**
* @param string $code
*
* @return array
*/
protected function getTokens(string $code): array
{
if (empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . '/oauth/token',
['Content-Type: application/x-www-form-urlencoded'],
\http_build_query([
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => $this->callback,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken): array
{
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . '/oauth/token',
['Content-Type: application/x-www-form-urlencoded'],
\http_build_query([
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
])
), true);
if (empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserID(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['id'] ?? '';
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserEmail(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['email'] ?? '';
}
/**
* Check if the OAuth email is verified
*
* @link https://discord.com/developers/docs/resources/user
*
* @param string $accessToken
*
* @return bool
*/
public function isEmailVerified(string $accessToken): bool
{
$user = $this->getUser($accessToken);
if ($user['verified'] ?? false) {
return true;
}
return false;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserName(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['username'] ?? '';
}
/**
* @param string $accessToken
*
* @return array
*/
protected function getUser(string $accessToken): array
{
if (empty($this->user)) {
$user = $this->request(
'GET',
$this->endpoint . '/organizations',
['Authorization: Bearer ' . \urlencode($accessToken)]
);
\var_dump($user);
$this->user = \json_decode($user, true);
$this->user = [
'username' => $this->user[0]['name'],
'verified' => false,
'email' => $this->user[0]['name'] . '@supabase.internal',
'id' => $this->user[0]['id']
];
}
return $this->user;
}
}
@@ -9,6 +9,7 @@ class Users extends Base
'email',
'phone',
'status',
'hash',
'passwordUpdate',
'registration',
'emailVerification',
+14 -2
View File
@@ -133,7 +133,7 @@ class Project extends Model
'example' => true,
])
->addRule('authPasswordDictionaryLength', [
'type' => self::TYPE_BOOLEAN,
'type' => self::TYPE_STRING,
'description' => 'How many most commonly used password to check against. Possible values are: 10k, 100k, 1m, 10m',
'default' => '10k',
'example' => '1m',
@@ -156,6 +156,18 @@ class Project extends Model
'default' => false,
'example' => true,
])
->addRule('authPasswordAi', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether or not to check user\'s password against against AI opinion',
'default' => false,
'example' => true,
])
->addRule('authRenewal', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether or not sessions are automatically extended to session duration on every request',
'default' => false,
'example' => true,
])
->addRule('authPersonalDataCheck', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether or not to check the user password for similarity with their personal data.',
@@ -344,7 +356,7 @@ class Project extends Model
$document->setAttribute('authPasswordDictionaryLength', $authValues['passwordDictionaryLength'] ?? false);
$document->setAttribute('authPasswordAi', $authValues['passwordAi'] ?? false);
$document->setAttribute('authNotify', $authValues['notify'] ?? false);
$document->setAttribute('authSessionRefresh', $authValues['sessionRefresh'] ?? false);
$document->setAttribute('authRenewal', $authValues['renewal'] ?? false);
$document->setAttribute('authPersonalDataCheck', $authValues['personalDataCheck'] ?? false);
foreach ($auth as $index => $method) {