1
0
Fork 0
mirror of synced 2024-06-28 19:20:25 +12:00

Merge branch '1.5.x' of https://github.com/appwrite/appwrite into feat-mfa

This commit is contained in:
Torsten Dittmann 2024-01-18 13:10:15 +01:00
commit 1ab3dc2236
49 changed files with 2750 additions and 2027 deletions

View file

@ -7,21 +7,21 @@ return [
'name' => 'Email/Password',
'key' => 'emailPassword',
'icon' => '/images/users/email.png',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateEmailSession',
'docs' => 'https://appwrite.io/docs/references/cloud/client-web/account#accountCreateEmailPasswordSession',
'enabled' => true,
],
'magic-url' => [
'name' => 'Magic URL',
'key' => 'usersAuthMagicURL',
'icon' => '/images/users/magic-url.png',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateMagicURLSession',
'docs' => 'https://appwrite.io/docs/references/cloud/client-web/account#accountCreateMagicURLToken',
'enabled' => true,
],
'anonymous' => [
'name' => 'Anonymous',
'key' => 'anonymous',
'icon' => '/images/users/anonymous.png',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateAnonymousSession',
'docs' => 'https://appwrite.io/docs/references/cloud/client-web/account#accountCreateAnonymousSession',
'enabled' => true,
],
'invites' => [
@ -42,7 +42,7 @@ return [
'name' => 'Phone',
'key' => 'phone',
'icon' => '/images/users/phone.png',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreatePhoneSession',
'docs' => 'https://appwrite.io/docs/references/cloud/client-web/account#accountCreatePhoneToken',
'enabled' => true,
],
];

View file

@ -58,6 +58,14 @@ return [
'$description' => 'This event triggers when a user\'s target is deleted.',
],
],
'tokens' => [
'$model' => Response::MODEL_TOKEN,
'$resource' => true,
'$description' => 'This event triggers on any user\'s token event.',
'create' => [
'$description' => 'This event triggers when a user\'s token is created.',
],
],
'create' => [
'$description' => 'This event triggers when a user is created.'
],

View file

@ -0,0 +1,234 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=DM+Sans:wght@500;600&display=swap"
rel="stylesheet"
/>
<style>
.main a {
color: currentColor;
}
.main {
padding: 32px;
line-height: 1.5;
color: #616b7c;
font-size: 15px;
font-weight: 400;
font-family: "Inter", sans-serif;
}
table {
width: 100%;
border-spacing: 0;
}
table,
tr,
th,
td {
margin: 0;
padding: 0;
}
td {
vertical-align: top;
}
h1 {
font-size: 22px;
margin-bottom: 0px;
margin-top: 0px;
color: #373b4d;
}
h2 {
font-size: 20px;
font-weight: 600;
color: #373b4d;
}
h3,
td h3 {
font-size: 14px;
font-weight: 500;
color: #373b4d;
line-height: 21px;
margin: 0;
padding: 0;
}
h4 {
font-family: "DM Sans", sans-serif;
font-weight: 600;
font-size: 12px;
color: #4f5769;
margin: 0;
padding: 0;
}
hr {
border: none;
border-top: 1px solid #e8e9f0;
}
.main a.button {
display: inline-block;
background: #fd366e;
color: #ffffff;
border-radius: 8px;
height: 48px;
padding: 12px 20px;
box-sizing: border-box;
cursor: pointer;
text-align: center;
text-decoration: none;
border-color: #fd366e;
border-style: solid;
border-width: 1px;
margin-right: 24px;
margin-top: 8px;
}
.main a.button:hover,
.main a.button:focus {
opacity: 0.8;
}
.main a.button.is-reverse {
background: #fff;
color: #fd366e;
}
.fs12 {
font-size: 12px;
}
.ta-right {
text-align: right !important;
}
.ta-left {
text-align: left !important;
}
.ff-dmsans {
font-family: "DM Sans", sans-serif !important;
}
.ff-inter {
font-family: "Inter", sans-serif !important;
}
.w500 {
font-weight: 500 !important;
}
.w400 {
font-weight: 400 !important;
}
.w600 {
font-weight: 600 !important;
}
.tc-accent {
color: #da1a5b;
}
.pt-16 {
padding-top: 16px !important;
}
.pt-32 {
padding-top: 32px !important;
}
.divider {
padding-top: 32px;
border-bottom: solid 1px #d8d8db;
}
.social-icon {
border-radius: 6px;
background: rgba(216, 216, 219, 0.1);
width: 32px;
height: 32px;
line-height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.social-icon > img {
margin: auto;
}
@media only screen and (max-width: 600px) {
.button {
width: 100%;
}
}
</style>
</head>
<body style="background-color: #ffffff; margin: 0; padding: 0;>
<div class="main" style="max-width: 650px; margin: 0 auto">
<table>
<tr>
<td>
<img
height="32px"
src="https://appwrite.io/assets/logotype/white.png"
/>
</td>
</tr>
</table>
<table style="margin-top: 32px">
<tr>
<td>
<h1>{{subject}}</h1>
</td>
</tr>
</table>
<table style="margin-top: 40px">
<tr>
<td>{{message}}</td>
</tr>
</table>
<table
style="
padding-top: 32px;
margin-top: 32px;
border-top: solid 1px #e8e9f0;
"
>
<tr>
<td></td>
</tr>
</table>
<table style="width: auto; margin: 0 auto">
<tr>
<td style="padding-left: 4px; padding-right: 4px">
<a
href="https://twitter.com/appwrite"
class="social-icon"
title="Twitter"
>
<img src="https://appwrite.io/email/x.png" height="24" width="24" />
</a>
</td>
<td style="padding-left: 4px; padding-right: 4px">
<a
href="https://appwrite.io/discord"
class="social-icon"
>
<img src="https://appwrite.io/email/discord.png" height="24" width="24" />
</a>
</td>
<td style="padding-left: 4px; padding-right: 4px">
<a
href="https://github.com/appwrite/appwrite"
class="social-icon"
>
<img src="https://appwrite.io/email/github.png" height="24" width="24" />
</a>
</td>
</tr>
</table>
<table style="width: auto; margin: 0 auto; margin-top: 60px">
<tr>
<td><a href="https://appwrite.io/terms">Terms</a></td>
<td style="color: #e8e9f0">
<div style="margin: 0 8px">|</div>
</td>
<td><a href="https://appwrite.io/privacy">Privacy</a></td>
</tr>
</table>
<p style="text-align: center" align="center">
&copy; {{year}} Appwrite | 251 Little Falls Drive, Wilmington 19808,
Delaware, United States
</p>
</div>
</body>
</html>

View file

@ -8,7 +8,9 @@ $member = [
'home',
'console',
'graphql',
'account',
'sessions.write',
'accounts.read',
'accounts.write',
'teams.read',
'teams.write',
'documents.read',
@ -31,6 +33,7 @@ $member = [
$admins = [
'global',
'graphql',
'sessions.write',
'teams.read',
'teams.write',
'documents.read',
@ -85,6 +88,7 @@ return [
'home',
'console',
'graphql',
'sessions.write',
'documents.read',
'documents.write',
'files.read',

View file

@ -1,6 +1,15 @@
<?php
return [ // List of publicly visible scopes
'accounts.read' => [
'description' => 'Access to read your active user account',
],
'accounts.write' => [
'description' => 'Access to create, update, and delete your active user account',
],
'sessions.write' => [
'description' => 'Access to create, update, and delete user sessions',
],
'users.read' => [
'description' => 'Access to read your project\'s users',
],

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

File diff suppressed because it is too large Load diff

View file

@ -671,12 +671,11 @@ App::post('/v1/messaging/providers/apns')
->param('authKeyId', '', new Text(0), 'APNS authentication key ID.', true)
->param('teamId', '', new Text(0), 'APNS team ID.', true)
->param('bundleId', '', new Text(0), 'APNS bundle ID.', true)
->param('endpoint', '', new Text(0), 'APNS endpoint.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
->action(function (string $providerId, string $name, string $authKey, string $authKeyId, string $teamId, string $bundleId, string $endpoint, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
->action(function (string $providerId, string $name, string $authKey, string $authKeyId, string $teamId, string $bundleId, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
$providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
$credentials = [];
@ -697,17 +696,12 @@ App::post('/v1/messaging/providers/apns')
$credentials['bundleId'] = $bundleId;
}
if (!empty($endpoint)) {
$credentials['endpoint'] = $endpoint;
}
if (
$enabled === true
&& \array_key_exists('authKey', $credentials)
&& \array_key_exists('authKeyId', $credentials)
&& \array_key_exists('teamId', $credentials)
&& \array_key_exists('bundleId', $credentials)
&& \array_key_exists('endpoint', $credentials)
) {
$enabled = true;
} else {
@ -1565,11 +1559,10 @@ App::patch('/v1/messaging/providers/apns/:providerId')
->param('authKeyId', '', new Text(0), 'APNS authentication key ID.', true)
->param('teamId', '', new Text(0), 'APNS team ID.', true)
->param('bundleId', '', new Text(0), 'APNS bundle ID.', true)
->param('endpoint', '', new Text(0), 'APNS endpoint.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
->action(function (string $providerId, string $name, ?bool $enabled, string $authKey, string $authKeyId, string $teamId, string $bundleId, string $endpoint, Event $queueForEvents, Database $dbForProject, Response $response) {
->action(function (string $providerId, string $name, ?bool $enabled, string $authKey, string $authKeyId, string $teamId, string $bundleId, Event $queueForEvents, Database $dbForProject, Response $response) {
$provider = $dbForProject->getDocument('providers', $providerId);
if ($provider->isEmpty()) {
@ -1603,10 +1596,6 @@ App::patch('/v1/messaging/providers/apns/:providerId')
$credentials['bundle'] = $bundleId;
}
if (!empty($endpoint)) {
$credentials['endpoint'] = $endpoint;
}
$provider->setAttribute('credentials', $credentials);
if ($enabled === true || $enabled === false) {
@ -1616,7 +1605,6 @@ App::patch('/v1/messaging/providers/apns/:providerId')
&& \array_key_exists('authKeyId', $credentials)
&& \array_key_exists('teamId', $credentials)
&& \array_key_exists('bundleId', $credentials)
&& \array_key_exists('endpoint', $credentials)
) {
$enabled = true;
} else {
@ -1696,12 +1684,9 @@ App::post('/v1/messaging/topics')
$topic = new Document([
'$id' => $topicId,
'name' => $name,
'description' => $description
]);
if ($description) {
$topic->setAttribute('description', $description);
}
try {
$topic = $dbForProject->createDocument('topics', $topic);
} catch (DuplicateException) {

View file

@ -474,6 +474,7 @@ App::post('/v1/teams/:teamId/memberships')
'phone' => empty($phone) ? null : $phone,
'emailVerification' => false,
'status' => true,
// TODO: Set password empty?
'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,

View file

@ -14,6 +14,7 @@ use Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Users;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Audit\Audit;
@ -35,6 +36,7 @@ use Utopia\Validator\Assoc;
use Utopia\Validator\WhiteList;
use Utopia\Validator\Text;
use Utopia\Validator\Boolean;
use Utopia\Validator\Range;
use MaxMind\Db\Reader;
use Utopia\Validator\Integer;
use Appwrite\Auth\Validator\PasswordHistory;
@ -1420,6 +1422,134 @@ App::patch('/v1/users/:userId/targets/:targetId')
->dynamic($target, Response::MODEL_TARGET);
});
App::post('/v1/users/:userId/sessions')
->desc('Create session')
->groups(['api', 'users'])
->label('event', 'users.[userId].sessions.[sessionId].create')
->label('scope', 'users.write')
->label('audits.event', 'session.create')
->label('audits.resource', 'user/{request.userId}')
->label('usage.metric', 'sessions.{scope}.requests.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createSession')
->label('sdk.description', '/docs/references/users/create-session.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_SESSION)
->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.')
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('locale')
->inject('geodb')
->inject('queueForEvents')
->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents) {
$user = $dbForProject->getDocument('users', $userId);
if ($user === false || $user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$secret = Auth::codeGenerator();
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$session = new Document(array_merge(
[
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_SERVER,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
],
$detector->getOS(),
$detector->getClient(),
$detector->getDevice()
));
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
$session = $dbForProject->createDocument('sessions', $session);
$session
->setAttribute('secret', $secret)
->setAttribute('expire', $expire)
->setAttribute('countryName', $countryName);
$queueForEvents
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
->setPayload($response->output($session, Response::MODEL_SESSION));
return $response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($session, Response::MODEL_SESSION);
});
App::post('/v1/users/:userId/tokens')
->desc('Create token')
->groups(['api', 'users'])
->label('event', 'users.[userId].tokens.[tokenId].create')
->label('scope', 'users.write')
->label('audits.event', 'tokens.create')
->label('audits.resource', 'user/{request.userId}')
->label('usage.metric', 'tokens.requests.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createToken')
->label('sdk.description', '/docs/references/users/create-token.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TOKEN)
->param('userId', '', new UID(), 'User ID.')
->param('length', 6, new Range(4, 128), 'Token length in characters. The default length is 6 characters', true)
->param('expire', Auth::TOKEN_EXPIRATION_GENERIC, new Range(60, Auth::TOKEN_EXPIRATION_LOGIN_LONG), 'Token expiration period in seconds. The default expiration is 15 minutes.', true)
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $userId, int $length, int $expire, Request $request, Response $response, Database $dbForProject, Event $queueForEvents) {
$user = $dbForProject->getDocument('users', $userId);
if ($user === false || $user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$secret = Auth::tokenGenerator($length);
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $expire));
$token = new Document([
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Auth::TOKEN_TYPE_GENERIC,
'secret' => Auth::hash($secret),
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP()
]);
$token = $dbForProject->createDocument('tokens', $token);
$dbForProject->deleteCachedDocument('users', $user->getId());
$token->setAttribute('secret', $secret);
$queueForEvents
->setParam('userId', $user->getId())
->setParam('tokenId', $token->getId())
->setPayload($response->output($token, Response::MODEL_TOKEN));
return $response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($token, Response::MODEL_TOKEN);
});
App::delete('/v1/users/:userId/sessions/:sessionId')
->desc('Delete user session')
->groups(['api', 'users'])

View file

@ -424,8 +424,8 @@ App::init()
->addHeader('Server', 'Appwrite')
->addHeader('X-Content-Type-Options', 'nosniff')
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma')
->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Forwarded-For, X-Forwarded-User-Agent')
->addHeader('Access-Control-Expose-Headers', 'X-Appwrite-Session, X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Origin', $refDomain)
->addHeader('Access-Control-Allow-Credentials', 'true');
@ -601,8 +601,8 @@ App::options()
$response
->addHeader('Server', 'Appwrite')
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Fallback-Cookies')
->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Appwrite-Session, X-Fallback-Cookies, X-Forwarded-For, X-Forwarded-User-Agent')
->addHeader('Access-Control-Expose-Headers', 'X-Appwrite-Session, X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Origin', $origin)
->addHeader('Access-Control-Allow-Credentials', 'true')
->noContent();

View file

@ -311,6 +311,12 @@ App::init()
}
break;
case 'phone':
if (($auths['phone'] ?? true) === false) {
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Phone authentication is disabled for this project');
}
break;
default:
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Unsupported authentication route');
break;

View file

@ -55,6 +55,12 @@ App::init()
}
break;
case 'phone':
if (($auths['phone'] ?? true) === false) {
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Phone authentication is disabled for this project');
}
break;
default:
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Unsupported authentication route');
break;

View file

@ -1125,9 +1125,18 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
Auth::$cookieName, // Get sessions
$request->getCookie(Auth::$cookieName . '_legacy', '')
)
);// Get fallback session from old clients (no SameSite support)
);
// Get fallback session from clients who block 3rd-party cookies
// Get session from header for SSR clients
if (empty($session['id']) && empty($session['secret'])) {
$sessionHeader = $request->getHeader('x-appwrite-session', '');
if (!empty($sessionHeader)) {
$session = Auth::decodeSession($sessionHeader);
}
}
// Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies
if ($response) {
$response->addHeader('X-Debug-Fallback', 'false');
}

View file

@ -0,0 +1 @@
Use this endpoint to create a session from token. Provide the **userId** and **secret** parameters from the successful response of authentication flows initiated by token creation. For example, magic URL and phone login.

View file

@ -1,3 +0,0 @@
Use this endpoint to complete creating the session with the Magic URL. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST /account/sessions/magic-url](https://appwrite.io/docs/references/cloud/client-web/account#createMagicURLSession) endpoint.
Please note that in order to avoid a [Redirect Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.

View file

@ -1 +0,0 @@
Use this endpoint to complete creating a session with SMS. Use the **userId** from the [createPhoneSession](https://appwrite.io/docs/references/cloud/client-web/account#createPhoneSession) endpoint and the **secret** received via SMS to successfully update and confirm the phone session.

View file

@ -0,0 +1,3 @@
Creates a session for a user. Returns an immediately usable session object.
If you want to generate a token for a custom authentication flow, use the [POST /users/{userId}/tokens](https://appwrite.io/docs/server/users#createToken) endpoint.

View file

@ -0,0 +1 @@
Returns a token with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [PUT /account/sessions/custom](https://appwrite.io/docs/references/cloud/client-web/account#updateCustomSession) endpoint to complete the login process.

View file

@ -52,6 +52,8 @@ class Auth
public const TOKEN_TYPE_INVITE = 4;
public const TOKEN_TYPE_MAGIC_URL = 5;
public const TOKEN_TYPE_PHONE = 6;
public const TOKEN_TYPE_OAUTH2 = 7;
public const TOKEN_TYPE_GENERIC = 8;
/**
* Session Providers.
@ -60,6 +62,9 @@ class Auth
public const SESSION_PROVIDER_ANONYMOUS = 'anonymous';
public const SESSION_PROVIDER_MAGIC_URL = 'magic-url';
public const SESSION_PROVIDER_PHONE = 'phone';
public const SESSION_PROVIDER_OAUTH2 = 'oauth2';
public const SESSION_PROVIDER_TOKEN = 'token';
public const SESSION_PROVIDER_SERVER = 'server';
/**
* Token Expiration times.
@ -69,6 +74,16 @@ class Auth
public const TOKEN_EXPIRATION_RECOVERY = 3600; /* 1 hour */
public const TOKEN_EXPIRATION_CONFIRM = 3600 * 1; /* 1 hour */
public const TOKEN_EXPIRATION_PHONE = 60 * 15; /* 15 minutes */
public const TOKEN_EXPIRATION_GENERIC = 60 * 15; /* 15 minutes */
/**
* Token Lengths.
*/
public const TOKEN_LENGTH_MAGIC_URL = 64;
public const TOKEN_LENGTH_VERIFICATION = 256;
public const TOKEN_LENGTH_RECOVERY = 256;
public const TOKEN_LENGTH_OAUTH2 = 64;
public const TOKEN_LENGTH_SESSION = 256;
/**
* @var string
@ -117,6 +132,27 @@ class Auth
]));
}
/**
* Token type to session provider mapping.
*/
public static function getSessionProviderByTokenType(int $type): string
{
switch ($type) {
case Auth::TOKEN_TYPE_VERIFICATION:
case Auth::TOKEN_TYPE_RECOVERY:
case Auth::TOKEN_TYPE_INVITE:
return Auth::SESSION_PROVIDER_EMAIL;
case Auth::TOKEN_TYPE_MAGIC_URL:
return Auth::SESSION_PROVIDER_MAGIC_URL;
case Auth::TOKEN_TYPE_PHONE:
return Auth::SESSION_PROVIDER_PHONE;
case Auth::TOKEN_TYPE_OAUTH2:
return Auth::SESSION_PROVIDER_OAUTH2;
default:
return Auth::SESSION_PROVIDER_TOKEN;
}
}
/**
* Decode Session.
*
@ -270,13 +306,20 @@ class Auth
*
* Generate random password string
*
* @param int $length
* @param int $length Length of returned token
*
* @return string
*/
public static function tokenGenerator(int $length = 128): string
public static function tokenGenerator(int $length = 256): string
{
return \bin2hex(\random_bytes($length));
if ($length <= 0) {
throw new \Exception('Token length must be greater than 0');
}
$bytesLength = (int) ceil($length / 2);
$token = \bin2hex(\random_bytes($bytesLength));
return substr($token, 0, $length);
}
/**
@ -303,48 +346,23 @@ class Auth
* Verify token and check that its not expired.
*
* @param array<Document> $tokens
* @param int $type
* @param int $type Type of token to verify, if null will verify any type
* @param string $secret
*
* @return bool|string
* @return false|Document
*/
public static function tokenVerify(array $tokens, int $type, string $secret)
public static function tokenVerify(array $tokens, int $type = null, string $secret): false|Document
{
foreach ($tokens as $token) {
if (
$token->isSet('type') &&
$token->isSet('secret') &&
$token->isSet('expire') &&
$token->getAttribute('type') == $type &&
$token->isSet('type') &&
($type === null || $token->getAttribute('type') === $type) &&
$token->getAttribute('secret') === self::hash($secret) &&
DateTime::formatTz($token->getAttribute('expire')) >= DateTime::formatTz(DateTime::now())
) {
return (string)$token->getId();
}
}
return false;
}
/**
* Verify phone token and check that its not expired.
*
* @param array<Document> $tokens
* @param string $secret
* @return string|false
*/
public static function phoneTokenVerify(array $tokens, string $secret)
{
foreach ($tokens as $token) {
if (
$token->isSet('type') &&
$token->isSet('secret') &&
$token->isSet('expire') &&
$token->getAttribute('type') == Auth::TOKEN_TYPE_PHONE &&
$token->getAttribute('secret') === self::hash($secret) &&
DateTime::formatTz($token->getAttribute('expire')) >= DateTime::formatTz(DateTime::now())
) {
return (string) $token->getId();
return $token;
}
}

View file

@ -99,26 +99,33 @@ class Schema
/** @var Route $route */
$namespace = $route->getLabel('sdk.namespace', '');
$method = $route->getLabel('sdk.method', '');
$name = $namespace . \ucfirst($method);
$methods = $route->getLabel('sdk.method', '');
if (empty($name)) {
continue;
if (!\is_array($methods)) {
$methods = [$methods];
}
foreach (Mapper::route($utopia, $route, $complexity) as $field) {
switch ($route->getMethod()) {
case 'GET':
$queries[$name] = $field;
break;
case 'POST':
case 'PUT':
case 'PATCH':
case 'DELETE':
$mutations[$name] = $field;
break;
default:
throw new \Exception("Unsupported method: {$route->getMethod()}");
foreach ($methods as $method) {
$name = $namespace . \ucfirst($method);
if (empty($name)) {
continue;
}
foreach (Mapper::route($utopia, $route, $complexity) as $field) {
switch ($route->getMethod()) {
case 'GET':
$queries[$name] = $field;
break;
case 'POST':
case 'PUT':
case 'PATCH':
case 'DELETE':
$mutations[$name] = $field;
break;
default:
throw new \Exception("Unsupported method: {$route->getMethod()}");
}
}
}
}

View file

@ -82,6 +82,12 @@ class Specs extends Action
'description' => '',
'in' => 'header',
],
'Session' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Session',
'description' => 'The user session to authenticate with',
'in' => 'header',
]
],
APP_PLATFORM_SERVER => [
'Project' => [
@ -108,6 +114,24 @@ class Specs extends Action
'description' => '',
'in' => 'header',
],
'Session' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Session',
'description' => 'The user session to authenticate with',
'in' => 'header',
],
'ForwardedFor' => [
'type' => 'apiKey',
'name' => 'X-Forwarded-For',
'description' => 'The IP address of the client that made the request',
'in' => 'header',
],
'ForwardedUserAgent' => [
'type' => 'apiKey',
'name' => 'X-Forwarded-User-Agent',
'description' => 'The user agent string of the client that made the request',
'in' => 'header',
],
],
APP_PLATFORM_CONSOLE => [
'Project' => [
@ -173,6 +197,7 @@ class Specs extends Action
if (empty($routeSecurity)) {
$sdkPlaforms[] = APP_PLATFORM_CLIENT;
$sdkPlaforms[] = APP_PLATFORM_SERVER;
}
if (!$route->getLabel('docs', true)) {

View file

@ -348,7 +348,6 @@ class Messaging extends Action
$credentials['authKeyId'],
$credentials['teamId'],
$credentials['bundleId'],
$credentials['endpoint']
),
'fcm' => new FCM($credentials['serviceAccountJSON']),
default => null

View file

@ -124,7 +124,11 @@ class OpenAPI3 extends Format
continue;
}
$id = $route->getLabel('sdk.method', \uniqid());
$method = $route->getLabel('sdk.method', [\uniqid()]);
if (\is_array($method)) {
$method = $method[0];
}
$desc = (!empty($route->getLabel('sdk.description', ''))) ? \realpath(__DIR__ . '/../../../../' . $route->getLabel('sdk.description', '')) : null;
$produces = $route->getLabel('sdk.response.type', null);
$model = $route->getLabel('sdk.response.model', 'none');
@ -149,21 +153,26 @@ class OpenAPI3 extends Format
}
if (empty($routeSecurity)) {
$sdkPlatforms[] = APP_PLATFORM_CLIENT;
if (!$route->getLabel('sdk.hideServer', false)) {
$sdkPlatforms[] = APP_PLATFORM_SERVER;
}
if (!$route->getLabel('sdk.hideClient', false)) {
$sdkPlatforms[] = APP_PLATFORM_CLIENT;
}
}
$temp = [
'summary' => $route->getDesc(),
'operationId' => $route->getLabel('sdk.namespace', 'default') . ucfirst($id),
'operationId' => $route->getLabel('sdk.namespace', 'default') . ucfirst($method),
'tags' => [$route->getLabel('sdk.namespace', 'default')],
'description' => ($desc) ? \file_get_contents($desc) : '',
'responses' => [],
'x-appwrite' => [ // Appwrite related metadata
'method' => $route->getLabel('sdk.method', \uniqid()),
'method' => $method,
'weight' => $route->getOrder(),
'cookies' => $route->getLabel('sdk.cookies', false),
'type' => $route->getLabel('sdk.methodType', ''),
'demo' => Template::fromCamelCaseToDash($route->getLabel('sdk.namespace', 'default')) . '/' . Template::fromCamelCaseToDash($id) . '.md',
'demo' => Template::fromCamelCaseToDash($route->getLabel('sdk.namespace', 'default')) . '/' . Template::fromCamelCaseToDash($method) . '.md',
'edit' => 'https://github.com/appwrite/appwrite/edit/master' . $route->getLabel('sdk.description', ''),
'rate-limit' => $route->getLabel('abuse-limit', 0),
'rate-time' => $route->getLabel('abuse-time', 3600),
@ -423,7 +432,7 @@ class OpenAPI3 extends Format
foreach ($this->enumBlacklist as $blacklist) {
if (
$blacklist['namespace'] == $route->getLabel('sdk.namespace', '')
&& $blacklist['method'] == $route->getLabel('sdk.method', '')
&& $blacklist['method'] == $method
&& $blacklist['parameter'] == $name
) {
$allowed = false;
@ -433,8 +442,8 @@ class OpenAPI3 extends Format
if ($allowed) {
$node['schema']['enum'] = $validator->getList();
$node['schema']['x-enum-name'] = $this->getEnumName($route->getLabel('sdk.namespace', ''), $route->getLabel('sdk.method', ''), $name);
$node['schema']['x-enum-keys'] = $this->getEnumKeys($route->getLabel('sdk.namespace', ''), $route->getLabel('sdk.method', ''), $name);
$node['schema']['x-enum-name'] = $this->getEnumName($route->getLabel('sdk.namespace', ''), $method, $name);
$node['schema']['x-enum-keys'] = $this->getEnumKeys($route->getLabel('sdk.namespace', ''), $method, $name);
}
if ($validator->getType() === 'integer') {
$node['format'] = 'int32';

View file

@ -123,7 +123,11 @@ class Swagger2 extends Format
continue;
}
$id = $route->getLabel('sdk.method', \uniqid());
$method = $route->getLabel('sdk.method', [\uniqid()]);
if (\is_array($method)) {
$method = $method[0];
}
$desc = (!empty($route->getLabel('sdk.description', ''))) ? \realpath(__DIR__ . '/../../../../' . $route->getLabel('sdk.description', '')) : null;
$produces = $route->getLabel('sdk.response.type', null);
$model = $route->getLabel('sdk.response.model', 'none');
@ -149,22 +153,23 @@ class Swagger2 extends Format
if (empty($routeSecurity)) {
$sdkPlatforms[] = APP_PLATFORM_CLIENT;
$sdkPlatforms[] = APP_PLATFORM_SERVER;
}
$temp = [
'summary' => $route->getDesc(),
'operationId' => $route->getLabel('sdk.namespace', 'default') . ucfirst($id),
'operationId' => $route->getLabel('sdk.namespace', 'default') . ucfirst($method),
'consumes' => [],
'produces' => [],
'tags' => [$route->getLabel('sdk.namespace', 'default')],
'description' => ($desc) ? \file_get_contents($desc) : '',
'responses' => [],
'x-appwrite' => [ // Appwrite related metadata
'method' => $route->getLabel('sdk.method', \uniqid()),
'method' => $method,
'weight' => $route->getOrder(),
'cookies' => $route->getLabel('sdk.cookies', false),
'type' => $route->getLabel('sdk.methodType', ''),
'demo' => Template::fromCamelCaseToDash($route->getLabel('sdk.namespace', 'default')) . '/' . Template::fromCamelCaseToDash($id) . '.md',
'demo' => Template::fromCamelCaseToDash($route->getLabel('sdk.namespace', 'default')) . '/' . Template::fromCamelCaseToDash($method) . '.md',
'edit' => 'https://github.com/appwrite/appwrite/edit/master' . $route->getLabel('sdk.description', ''),
'rate-limit' => $route->getLabel('abuse-limit', 0),
'rate-time' => $route->getLabel('abuse-time', 3600),
@ -424,7 +429,7 @@ class Swagger2 extends Format
// Do not add the enum
$allowed = true;
foreach ($this->enumBlacklist as $blacklist) {
if ($blacklist['namespace'] == $route->getLabel('sdk.namespace', '') && $blacklist['method'] == $route->getLabel('sdk.method', '') && $blacklist['parameter'] == $name) {
if ($blacklist['namespace'] == $route->getLabel('sdk.namespace', '') && $blacklist['method'] == $method && $blacklist['parameter'] == $name) {
$allowed = false;
break;
}
@ -432,8 +437,8 @@ class Swagger2 extends Format
if ($allowed) {
$node['enum'] = $validator->getList();
$node['x-enum-name'] = $this->getEnumName($route->getLabel('sdk.namespace', ''), $route->getLabel('sdk.method', ''), $name);
$node['x-enum-keys'] = $this->getEnumKeys($route->getLabel('sdk.namespace', ''), $route->getLabel('sdk.method', ''), $name);
$node['x-enum-name'] = $this->getEnumName($route->getLabel('sdk.namespace', ''), $method, $name);
$node['x-enum-keys'] = $this->getEnumKeys($route->getLabel('sdk.namespace', ''), $method, $name);
}
if ($validator->getType() === 'integer') {

View file

@ -25,7 +25,11 @@ class Request extends UtopiaRequest
$parameters = parent::getParams();
if (self::hasFilter() && self::hasRoute()) {
$endpointIdentifier = self::getRoute()->getLabel('sdk.namespace', 'unknown') . '.' . self::getRoute()->getLabel('sdk.method', 'unknown');
$method = self::getRoute()->getLabel('sdk.method', ['unknown']);
if (\is_array($method)) {
$method = $method[0];
}
$endpointIdentifier = self::getRoute()->getLabel('sdk.namespace', 'unknown') . '.' . $method;
$parameters = self::getFilter()->parse($parameters, $endpointIdentifier);
}

View file

@ -166,6 +166,12 @@ class Session extends Model
'default' => 1,
'example' => 1,
])
->addRule('secret', [
'type' => self::TYPE_STRING,
'description' => 'Secret used to authenticate the user. Only included if the request was made with an API key',
'default' => '',
'example' => '5e5bb8c16897e',
])
;
}

View file

@ -31,8 +31,8 @@ class HTTPTest extends Scope
$this->assertEquals(204, $response['headers']['status-code']);
$this->assertEquals('Appwrite', $response['headers']['server']);
$this->assertEquals('GET, POST, PUT, PATCH, DELETE', $response['headers']['access-control-allow-methods']);
$this->assertEquals('Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Fallback-Cookies', $response['headers']['access-control-allow-headers']);
$this->assertEquals('X-Fallback-Cookies', $response['headers']['access-control-expose-headers']);
$this->assertEquals('Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Appwrite-Session, X-Fallback-Cookies, X-Forwarded-For, X-Forwarded-User-Agent', $response['headers']['access-control-allow-headers']);
$this->assertEquals('X-Appwrite-Session, X-Fallback-Cookies', $response['headers']['access-control-expose-headers']);
$this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']);
$this->assertEquals('true', $response['headers']['access-control-allow-credentials']);
$this->assertEmpty($response['body']);

View file

@ -83,6 +83,9 @@ trait ProjectCustom
'health.read',
'rules.read',
'rules.write',
'sessions.write',
'accounts.write',
'accounts.read',
'targets.read',
'targets.write',
'providers.read',

File diff suppressed because it is too large Load diff

View file

@ -2,13 +2,9 @@
namespace Tests\E2E\Services\Account;
use Appwrite\Extend\Exception;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectConsole;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\Helpers\ID;
use Tests\E2E\Client;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
class AccountConsoleClientTest extends Scope
{

File diff suppressed because it is too large Load diff

View file

@ -6,35 +6,259 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Helpers\ID;
class AccountCustomServerTest extends Scope
{
use AccountBase;
use ProjectCustom;
use SideServer;
public function testCreateAccount(): array
/**
* @depends testCreateAccount
*/
public function testCreateAccountSession($data): array
{
$email = uniqid() . 'user@localhost.test';
$password = 'password';
$name = 'User Name';
$email = $data['email'] ?? '';
$password = $data['password'] ?? '';
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'email' => $email,
'password' => $password,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotFalse(\DateTime::createFromFormat('Y-m-d\TH:i:s.uP', $response['body']['expire']));
$sessionId = $response['body']['$id'];
$session = $response['body']['secret'];
$userId = $response['body']['userId'];
$response = $this->client->call(Client::METHOD_GET, '/users/' . $userId, array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertArrayHasKey('accessedAt', $response['body']);
$this->assertNotEmpty($response['body']['accessedAt']);
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'email' => $email,
'password' => $password,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['secret']);
$this->assertNotFalse(\DateTime::createFromFormat('Y-m-d\TH:i:s.uP', $response['body']['expire']));
// already logged in
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-session' => $session,
], $this->getHeaders()), [
'email' => $email,
'password' => $password,
]);
$this->assertEquals(201, $response['headers']['status-code']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_POST, '/account', [
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'userId' => ID::unique(),
'email' => $email,
], $this->getHeaders()), [
'email' => $email . 'x',
'password' => $password,
'name' => $name,
]);
$this->assertEquals(401, $response['headers']['status-code']);
return [];
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'email' => $email,
'password' => $password . 'x',
]);
$this->assertEquals(401, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'email' => '',
'password' => '',
]);
$this->assertEquals(400, $response['headers']['status-code']);
return array_merge($data, [
'sessionId' => $sessionId,
'session' => $session,
]);
}
public function testCreateAnonymousAccount()
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/anonymous', array_merge(
[
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
],
$this->getHeaders()
));
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertIsArray($response['body']);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertNotEmpty($response['body']['secret']);
\usleep(1000 * 30); // wait for 30ms to let the shutdown update accessedAt
$userId = $response['body']['userId'];
$response = $this->client->call(Client::METHOD_GET, '/users/' . $userId, array_merge(
[
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
],
$this->getHeaders(),
));
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertArrayHasKey('accessedAt', $response['body']);
$this->assertNotEmpty($response['body']['accessedAt']);
}
public function testCreateMagicUrl(): array
{
$email = \time() . 'user@appwrite.io';
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_POST, '/account/tokens/magic-url', array_merge(
[
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
],
$this->getHeaders()
), [
'userId' => ID::unique(),
'email' => $email,
// 'url' => 'http://localhost/magiclogin',
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertNotEmpty($response['body']['secret']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['expire']));
$userId = $response['body']['userId'];
$lastEmail = $this->getLastEmail();
$this->assertEquals($email, $lastEmail['to'][0]['address']);
$this->assertEquals($this->getProject()['name'] . ' Login', $lastEmail['subject']);
$token = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 64);
$expireTime = strpos($lastEmail['text'], 'expire=' . urlencode($response['body']['expire']), 0);
$this->assertNotFalse($expireTime);
$secretTest = strpos($lastEmail['text'], 'secret=' . $response['body']['secret'], 0);
$this->assertNotFalse($secretTest);
$userIDTest = strpos($lastEmail['text'], 'userId=' . $response['body']['userId'], 0);
$this->assertNotFalse($userIDTest);
$data['token'] = $token;
$data['id'] = $userId;
$data['email'] = $email;
return $data;
}
/**
* @depends testCreateMagicUrl
*/
public function testCreateSessionWithMagicUrl($data): array
{
$id = $data['id'] ?? '';
$token = $data['token'] ?? '';
$email = $data['email'] ?? '';
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_PUT, '/account/sessions/magic-url', array_merge(
[
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
],
$this->getHeaders()
), [
'userId' => $id,
'secret' => $token,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertIsArray($response['body']);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertNotEmpty($response['body']['userId']);
$this->assertNotEmpty($response['body']['secret']);
$sessionId = $response['body']['$id'];
$session = $response['body']['secret'];
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge(
[
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-session' => $session
],
$this->getHeaders()
));
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['registration']));
$this->assertEquals($response['body']['email'], $email);
$this->assertTrue($response['body']['emailVerification']);
$data['sessionId'] = $sessionId;
$data['session'] = $session;
return $data;
}
}

View file

@ -1271,8 +1271,8 @@ trait Base
}
}';
case self::$UPDATE_PASSWORD_RECOVERY:
return 'mutation confirmPasswordRecovery($userId: String!, $secret: String!, $password: String!, $passwordAgain: String!) {
accountUpdateRecovery(userId: $userId, secret: $secret, password: $password, passwordAgain: $passwordAgain) {
return 'mutation confirmPasswordRecovery($userId: String!, $secret: String!, $password: String!) {
accountUpdateRecovery(userId: $userId, secret: $secret, password: $password) {
userId
secret
expire
@ -1870,8 +1870,8 @@ trait Base
}
}';
case self::$CREATE_APNS_PROVIDER:
return 'mutation createApnsProvider($providerId: String!, $name: String!, $authKey: String!, $authKeyId: String!, $teamId: String!, $bundleId: String!, $endpoint: String!) {
messagingCreateApnsProvider(providerId: $providerId, name: $name, authKey: $authKey, authKeyId: $authKeyId, teamId: $teamId, bundleId: $bundleId, endpoint: $endpoint) {
return 'mutation createApnsProvider($providerId: String!, $name: String!, $authKey: String!, $authKeyId: String!, $teamId: String!, $bundleId: String!) {
messagingCreateApnsProvider(providerId: $providerId, name: $name, authKey: $authKey, authKeyId: $authKeyId, teamId: $teamId, bundleId: $bundleId) {
_id
name
provider
@ -1984,8 +1984,8 @@ trait Base
}
}';
case self::$UPDATE_APNS_PROVIDER:
return 'mutation updateApnsProvider($providerId: String!, $name: String!, $authKey: String!, $authKeyId: String!, $teamId: String!, $bundleId: String!, $endpoint: String!) {
messagingUpdateApnsProvider(providerId: $providerId, name: $name, authKey: $authKey, authKeyId: $authKeyId, teamId: $teamId, bundleId: $bundleId, endpoint: $endpoint) {
return 'mutation updateApnsProvider($providerId: String!, $name: String!, $authKey: String!, $authKeyId: String!, $teamId: String!, $bundleId: String!) {
messagingUpdateApnsProvider(providerId: $providerId, name: $name, authKey: $authKey, authKeyId: $authKeyId, teamId: $teamId, bundleId: $bundleId) {
_id
name
provider

View file

@ -87,7 +87,6 @@ class MessagingTest extends Scope
'authKeyId' => 'my-authkeyid',
'teamId' => 'my-teamid',
'bundleId' => 'my-bundleid',
'endpoint' => 'my-endpoint',
],
];
@ -177,7 +176,6 @@ class MessagingTest extends Scope
'authKeyId' => 'my-authkeyid',
'teamId' => 'my-teamid',
'bundleId' => 'my-bundleid',
'endpoint' => 'my-endpoint',
],
];
foreach (\array_keys($providersParams) as $index => $key) {

View file

@ -80,7 +80,6 @@ trait MessagingBase
'authKeyId' => 'my-authkeyid',
'teamId' => 'my-teamid',
'bundleId' => 'my-bundleid',
'endpoint' => 'my-endpoint',
],
];
$providers = [];
@ -155,7 +154,6 @@ trait MessagingBase
'authKeyId' => 'my-authkeyid',
'teamId' => 'my-teamid',
'bundleId' => 'my-bundleid',
'endpoint' => 'my-endpoint',
],
];
foreach (\array_keys($providersParams) as $index => $key) {
@ -245,6 +243,7 @@ trait MessagingBase
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertEquals('my-app', $response['body']['name']);
$this->assertEquals('', $response['body']['description']);
return $response['body'];
}

View file

@ -617,7 +617,6 @@ class RealtimeCustomClientTest extends Scope
'userId' => $userId,
'secret' => $recovery,
'password' => 'test-recovery',
'passwordAgain' => 'test-recovery',
]);
$response = json_decode($client->receive(), true);

View file

@ -223,10 +223,10 @@ trait TeamsBaseClient
*/
$secondEmail = uniqid() . 'foe@localhost.test';
$secondName = 'Another Foe';
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
$response = $this->client->call(Client::METHOD_POST, '/account', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
], [
'userId' => 'unique()',
'email' => $secondEmail,
'password' => 'password',

View file

@ -230,6 +230,65 @@ trait UsersBase
}
}
/**
* @depends testCreateUser
*/
public function testCreateToken(array $data): void
{
/**
* Test for SUCCESS
*/
$token = $this->client->call(Client::METHOD_POST, '/users/' . $data['userId'] . '/tokens', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(201, $token['headers']['status-code']);
$this->assertEquals($data['userId'], $token['body']['userId']);
$this->assertNotEmpty($token['body']['secret']);
$this->assertNotEmpty($token['body']['expire']);
$token = $this->client->call(Client::METHOD_POST, '/users/' . $data['userId'] . '/tokens', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'length' => 15,
'expire' => 60,
]);
$this->assertEquals(201, $token['headers']['status-code']);
$this->assertEquals($data['userId'], $token['body']['userId']);
$this->assertEquals(15, strlen($token['body']['secret']));
$this->assertNotEmpty($token['body']['expire']);
/**
* Test for FAILURE
*/
$token = $this->client->call(Client::METHOD_POST, '/users/' . $data['userId'] . '/tokens', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'length' => 1,
'expire' => 1,
]);
$this->assertEquals(400, $token['headers']['status-code']);
$this->assertArrayNotHasKey('userId', $token['body']);
$this->assertArrayNotHasKey('secret', $token['body']);
$token = $this->client->call(Client::METHOD_POST, '/users/' . $data['userId'] . '/tokens', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'expire' => 999999999999999,
]);
$this->assertEquals(400, $token['headers']['status-code']);
$this->assertArrayNotHasKey('userId', $token['body']);
$this->assertArrayNotHasKey('secret', $token['body']);
}
/**
* Tests all optional parameters of createUser (email, phone, anonymous..)
*

View file

@ -824,7 +824,6 @@ class WebhooksCustomClientTest extends Scope
'userId' => $id,
'secret' => $secret,
'password' => $password,
'passwordAgain' => $password,
]);
$recoveryId = $recovery['body']['$id'];

View file

@ -189,8 +189,8 @@ class AuthTest extends TestCase
public function testTokenGenerator(): void
{
$this->assertEquals(\mb_strlen(Auth::tokenGenerator()), 256);
$this->assertEquals(\mb_strlen(Auth::tokenGenerator(5)), 10);
$this->assertEquals(\strlen(Auth::tokenGenerator()), 256);
$this->assertEquals(\strlen(Auth::tokenGenerator(5)), 5);
}
public function testCodeGenerator(): void
@ -294,7 +294,8 @@ class AuthTest extends TestCase
]),
];
$this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_RECOVERY, $secret), 'token1');
$this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_RECOVERY, $secret), $tokens1[0]);
$this->assertEquals(Auth::tokenVerify($tokens1, null, $secret), $tokens1[0]);
$this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false);
$this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_RECOVERY, $secret), false);
$this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false);