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

fix: token verification

This commit is contained in:
loks0n 2023-10-05 11:18:19 +01:00
parent dbf1874439
commit e94631f730
3 changed files with 59 additions and 53 deletions

View file

@ -768,6 +768,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]));
}
// Add token for server platforms
$loginSecret = Auth::tokenGenerator();
$token = new Document([
@ -777,7 +778,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'type' => Auth::TOKEN_TYPE_OAUTH2,
'secret' => Auth::hash($loginSecret), // One way hash encryption to protect DB leak
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'userAgent' => 'UNKNOWN',
'ip' => $request->getIP(),
]);
@ -1139,14 +1140,12 @@ App::put('/v1/account/sessions/token')
->action(function (string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $events) {
/** @var Utopia\Database\Document $user */
$userFromRequest = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId));
if ($userFromRequest->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$token = Auth::tokenVerify($userFromRequest->getAttribute('tokens', []), null, $secret);
if (!$token) {
$verifiedToken = Auth::tokenVerify($userFromRequest->getAttribute('tokens', []), null, $secret);
if (!$verifiedToken) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
@ -1157,6 +1156,8 @@ App::put('/v1/account/sessions/token')
$record = $geodb->get($request->getIP());
$secret = Auth::tokenGenerator();
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$provider = Auth::getSessionProviderByTokenType($verifiedToken->getAttribute('type'));
$session = new Document(array_merge(
[
@ -1258,9 +1259,9 @@ App::put('/v1/account/sessions/magic-url')
throw new Exception(Exception::USER_NOT_FOUND);
}
$token = Auth::tokenVerify($userFromRequest->getAttribute('tokens', []), Auth::TOKEN_TYPE_MAGIC_URL, $secret);
$verifiedToken = Auth::tokenVerify($userFromRequest->getAttribute('tokens', []), Auth::TOKEN_TYPE_MAGIC_URL, $secret);
if (!$token) {
if (!$verifiedToken) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
@ -1305,7 +1306,7 @@ App::put('/v1/account/sessions/magic-url')
* We act like we're updating and validating
* the recovery token but actually we don't need it anymore.
*/
$dbForProject->deleteDocument('tokens', $token);
$dbForProject->deleteDocument('tokens', $verifiedToken->getId());
$dbForProject->deleteCachedDocument('users', $user->getId());
$user->setAttribute('emailVerification', true);
@ -1509,9 +1510,9 @@ App::put('/v1/account/sessions/phone')
throw new Exception(Exception::USER_NOT_FOUND);
}
$token = Auth::phoneTokenVerify($userFromRequest->getAttribute('tokens', []), $secret);
$verifiedToken = Auth::tokenVerify($userFromRequest->getAttribute('tokens', []), Auth::TOKEN_TYPE_PHONE, $secret);
if (!$token) {
if (!$verifiedToken) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
@ -1554,7 +1555,7 @@ App::put('/v1/account/sessions/phone')
* We act like we're updating and validating
* the recovery token but actually we don't need it anymore.
*/
$dbForProject->deleteDocument('tokens', $token);
$dbForProject->deleteDocument('tokens', $verifiedToken->getId());
$dbForProject->deleteCachedDocument('users', $user->getId());
$user->setAttribute('phoneVerification', true);
@ -2721,9 +2722,9 @@ App::put('/v1/account/recovery')
}
$tokens = $profile->getAttribute('tokens', []);
$recovery = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_RECOVERY, $secret);
$verifiedToken = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_RECOVERY, $secret);
if (!$recovery) {
if (!$verifiedToken) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
@ -2753,13 +2754,13 @@ App::put('/v1/account/recovery')
$user->setAttributes($profile->getArrayCopy());
$recoveryDocument = $dbForProject->getDocument('tokens', $recovery);
$recoveryDocument = $dbForProject->getDocument('tokens', $verifiedToken->getId());
/**
* We act like we're updating and validating
* the recovery token but actually we don't need it anymore.
*/
$dbForProject->deleteDocument('tokens', $recovery);
$dbForProject->deleteDocument('tokens', $verifiedToken->getId());
$dbForProject->deleteCachedDocument('users', $profile->getId());
$events
@ -2963,9 +2964,9 @@ App::put('/v1/account/verification')
}
$tokens = $profile->getAttribute('tokens', []);
$verification = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_VERIFICATION, $secret);
$verifiedToken = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_VERIFICATION, $secret);
if (!$verification) {
if (!$verifiedToken) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
@ -2975,13 +2976,13 @@ App::put('/v1/account/verification')
$user->setAttributes($profile->getArrayCopy());
$verificationDocument = $dbForProject->getDocument('tokens', $verification);
$verificationDocument = $dbForProject->getDocument('tokens', $verifiedToken->getId());
/**
* We act like we're updating and validating
* the verification token but actually we don't need it anymore.
*/
$dbForProject->deleteDocument('tokens', $verification);
$dbForProject->deleteDocument('tokens', $verifiedToken->getId());
$dbForProject->deleteCachedDocument('users', $profile->getId());
$events
@ -3119,9 +3120,9 @@ App::put('/v1/account/verification/phone')
throw new Exception(Exception::USER_NOT_FOUND);
}
$verification = Auth::phoneTokenVerify($user->getAttribute('tokens', []), $secret);
$verifiedToken = Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_PHONE, $secret);
if (!$verification) {
if (!$verifiedToken) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
@ -3131,12 +3132,12 @@ App::put('/v1/account/verification/phone')
$user->setAttributes($profile->getArrayCopy());
$verificationDocument = $dbForProject->getDocument('tokens', $verification);
$verificationDocument = $dbForProject->getDocument('tokens', $verifiedToken->getId());
/**
* We act like we're updating and validating the verification token but actually we don't need it anymore.
*/
$dbForProject->deleteDocument('tokens', $verification);
$dbForProject->deleteDocument('tokens', $verifiedToken->getId());
$dbForProject->deleteCachedDocument('users', $profile->getId());
$events

View file

@ -1081,25 +1081,23 @@ App::patch('/v1/users/:userId/prefs')
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
App::post('/v1/users/:userId/token')
App::post('/v1/users/:userId/tokens')
->desc('Create universal token')
->groups(['api', 'users'])
->label('event', 'users.[userId].sessions.create')
->label('event', 'users.[userId].tokens.create')
->label('scope', 'users.write')
->label('audits.event', 'session.create')
->label('audits.event', 'tokens.create')
->label('audits.resource', 'user/{request.userId}')
->label('usage.metric', 'sessions.{scope}.requests.create')
->label('usage.metric', 'tokens.requests.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createSession')
->label('sdk.method', 'createToken')
->label('sdk.description', '/docs/references/users/create-user-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.')
->inject('request')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('events')
->action(function (string $userId, Response $response, Database $dbForProject, Event $events) {
@ -1125,6 +1123,11 @@ App::post('/v1/users/:userId/token')
$token = $dbForProject->createDocument('tokens', $token);
$events
->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);

View file

@ -121,6 +121,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_UNIVERSAL;
}
}
/**
* Decode Session.
*
@ -307,10 +328,10 @@ class Auth
* Verify token and check that its not expired.
*
* @param array $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 bool|Document
*/
public static function tokenVerify(array $tokens, int $type = null, string $secret)
{
@ -319,30 +340,11 @@ class Auth
if (
$token->isSet('secret') &&
$token->isSet('expire') &&
$type === null || $token->getAttribute('type') === $type &&
($type === null || $token->isSet('type') && $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;
}
public static function phoneTokenVerify(array $tokens, string $secret)
{
foreach ($tokens as $token) {
/** @var Document $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;
}
}