Page MenuHomePhorge

D2494.1775201186.diff
No OneTemporary

Authored By
Unknown
Size
52 KB
Referenced Files
None
Subscribers
None

D2494.1775201186.diff

diff --git a/bin/quickstart.sh b/bin/quickstart.sh
--- a/bin/quickstart.sh
+++ b/bin/quickstart.sh
@@ -66,10 +66,11 @@
npm install
find bootstrap/cache/ -type f ! -name ".gitignore" -delete
./artisan key:generate
-./artisan jwt:secret -f
./artisan clear-compiled
./artisan cache:clear
./artisan horizon:install
+./artisan passport:keys --force
+
if [ ! -z "$(rpm -qv chromium 2>/dev/null)" ]; then
chver=$(rpmquery --queryformat="%{VERSION}" chromium | awk -F'.' '{print $1}')
diff --git a/src/.env.example b/src/.env.example
--- a/src/.env.example
+++ b/src/.env.example
@@ -132,8 +132,8 @@
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
-JWT_SECRET=
-JWT_TTL=60
+PROXY_OAUTH_CLIENT_ID=1
+PROXY_OAUTH_CLIENT_SECRET=JF4pL68ucLuMupaOviTeG8EJeQpjtZtcGLp4f0dq
COMPANY_NAME=
COMPANY_ADDRESS=
diff --git a/src/app/Auth/LDAPUserProvider.php b/src/app/Auth/LDAPUserProvider.php
--- a/src/app/Auth/LDAPUserProvider.php
+++ b/src/app/Auth/LDAPUserProvider.php
@@ -49,53 +49,6 @@
*/
public function validateCredentials(Authenticatable $user, array $credentials): bool
{
- $authenticated = false;
-
- if ($user->email === \strtolower($credentials['email'])) {
- if (!empty($user->password)) {
- if (Hash::check($credentials['password'], $user->password)) {
- $authenticated = true;
- }
- } elseif (!empty($user->password_ldap)) {
- if (substr($user->password_ldap, 0, 6) == "{SSHA}") {
- $salt = substr(base64_decode(substr($user->password_ldap, 6)), 20);
-
- $hash = '{SSHA}' . base64_encode(
- sha1($credentials['password'] . $salt, true) . $salt
- );
-
- if ($hash == $user->password_ldap) {
- $authenticated = true;
- }
- } elseif (substr($user->password_ldap, 0, 9) == "{SSHA512}") {
- $salt = substr(base64_decode(substr($user->password_ldap, 9)), 64);
-
- $hash = '{SSHA512}' . base64_encode(
- pack('H*', hash('sha512', $credentials['password'] . $salt)) . $salt
- );
-
- if ($hash == $user->password_ldap) {
- $authenticated = true;
- }
- }
- } else {
- \Log::error("Incomplete credentials for {$user->email}");
- }
- }
-
- if ($authenticated) {
- \Log::info("Successful authentication for {$user->email}");
-
- // TODO: update last login time
- if (empty($user->password) || empty($user->password_ldap)) {
- $user->password = $credentials['password'];
- $user->save();
- }
- } else {
- // TODO: Try actual LDAP?
- \Log::info("Authentication failed for {$user->email}");
- }
-
- return $authenticated;
+ return $user->validateCredentials($credentials['email'], $credentials['password']);
}
}
diff --git a/src/app/Auth/SecondFactor.php b/src/app/Auth/SecondFactor.php
--- a/src/app/Auth/SecondFactor.php
+++ b/src/app/Auth/SecondFactor.php
@@ -35,40 +35,50 @@
/**
* Validate 2-factor authentication code
*
- * @param \Illuminate\Http\Request $request The API request.
+ * @param string $secondfactor The 2-factor authentication code.
*
- * @return \Illuminate\Http\JsonResponse|null
+ * @throws \Exception on validation failure
*/
- public function requestHandler($request)
+ public function validate($secondfactor): void
{
// get list of configured authentication factors
$factors = $this->factors();
// do nothing if no factors configured
if (empty($factors)) {
- return null;
+ return;
}
- if (empty($request->secondfactor) || !is_string($request->secondfactor)) {
- $errors = ['secondfactor' => \trans('validation.2fareq')];
- return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ if (empty($secondfactor) || !is_string($secondfactor)) {
+ throw new \Exception(\trans('validation.2fareq'));
}
// try to verify each configured factor
foreach ($factors as $factor) {
// verify the submitted code
- // if (strpos($factor, 'dummy:') === 0 && (\app('env') != 'production') {
- // if ($request->secondfactor === 'dummy') {
- // return null;
- // }
- // } else
- if ($this->verify($factor, $request->secondfactor)) {
- return null;
+ if ($this->verify($factor, $secondfactor)) {
+ return;
}
}
+ throw new \Exception(\trans('validation.2fainvalid'));
+ }
- $errors = ['secondfactor' => \trans('validation.2fainvalid')];
- return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ /**
+ * Validate 2-factor authentication code
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\JsonResponse|null
+ */
+ public function requestHandler(\Illuminate\Http\Request $request)
+ {
+ try {
+ $this->validate($request->secondfactor);
+ } catch (\Exception $e) {
+ $errors = ['secondfactor' => $e->getMessage()];
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
+ return null;
}
/**
diff --git a/src/app/Exceptions/Handler.php b/src/app/Exceptions/Handler.php
--- a/src/app/Exceptions/Handler.php
+++ b/src/app/Exceptions/Handler.php
@@ -15,7 +15,8 @@
* @var array
*/
protected $dontReport = [
- //
+ \Laravel\Passport\Exceptions\OAuthServerException::class,
+ \League\OAuth2\Server\Exception\OAuthServerException::class
];
/**
diff --git a/src/app/Http/Controllers/API/AuthController.php b/src/app/Http/Controllers/API/AuthController.php
--- a/src/app/Http/Controllers/API/AuthController.php
+++ b/src/app/Http/Controllers/API/AuthController.php
@@ -7,6 +7,8 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
+use Laravel\Passport\TokenRepository;
+use Laravel\Passport\RefreshTokenRepository;
class AuthController extends Controller
{
@@ -20,9 +22,8 @@
$user = Auth::guard()->user();
$response = V4\UsersController::userResponse($user);
- if (!empty(request()->input('refresh_token'))) {
- // @phpstan-ignore-next-line
- return $this->respondWithToken(Auth::guard()->refresh(), $response);
+ if (!empty(request()->input('refresh'))) {
+ return $this->refreshAndRespond(request(), $response);
}
return response()->json($response);
@@ -32,20 +33,31 @@
* Helper method for other controllers with user auto-logon
* functionality
*
- * @param \App\User $user User model object
+ * @param \App\User $user User model object
+ * @param string $password Plain text password
+ * @param string|null $secondFactor Second factor code if available
*/
- public static function logonResponse(User $user)
+ public static function logonResponse(User $user, string $password, string $secondFactor = null)
{
- // @phpstan-ignore-next-line
- $token = Auth::guard()->login($user);
+ $proxyRequest = Request::create('/oauth/token', 'POST', [
+ 'username' => $user->email,
+ 'password' => $password,
+ 'grant_type' => 'password',
+ 'client_id' => config('auth.proxy.client_id'),
+ 'client_secret' => config('auth.proxy.client_secret'),
+ 'scopes' => '[*]',
+ 'secondfactor' => $secondFactor
+ ]);
+
+ $tokenResponse = app()->handle($proxyRequest);
+
$response = V4\UsersController::userResponse($user);
$response['status'] = 'success';
-
- return self::respondWithToken($token, $response);
+ return self::respondWithToken($tokenResponse, $response);
}
/**
- * Get a JWT token via given credentials.
+ * Get an oauth token via given credentials.
*
* @param \Illuminate\Http\Request $request The API request.
*
@@ -66,22 +78,11 @@
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
}
- $credentials = $request->only('email', 'password');
-
- if ($token = Auth::guard()->attempt($credentials)) {
- $user = Auth::guard()->user();
- $sf = new \App\Auth\SecondFactor($user);
-
- if ($response = $sf->requestHandler($request)) {
- return $response;
- }
-
- $response = V4\UsersController::userResponse($user);
-
- return $this->respondWithToken($token, $response);
+ $user = \App\User::where('email', $request->email)->first();
+ if (!$user) {
+ return response()->json(['status' => 'error', 'message' => __('auth.failed')], 401);
}
-
- return response()->json(['status' => 'error', 'message' => __('auth.failed')], 401);
+ return self::logonResponse($user, $request->password, $request->secondfactor);
}
/**
@@ -91,8 +92,16 @@
*/
public function logout()
{
- Auth::guard()->logout();
+ $tokenId = Auth::user()->token()->id;
+
+ $tokenRepository = app(TokenRepository::class);
+ $refreshTokenRepository = app(RefreshTokenRepository::class);
+ // Revoke an access token...
+ $tokenRepository->revokeAccessToken($tokenId);
+
+ // Revoke all of the token's refresh tokens...
+ $refreshTokenRepository->revokeRefreshTokensByAccessTokenId($tokenId);
return response()->json([
'status' => 'success',
'message' => __('auth.logoutsuccess')
@@ -104,26 +113,58 @@
*
* @return \Illuminate\Http\JsonResponse
*/
- public function refresh()
+ public function refresh(Request $request)
+ {
+ return self::refreshAndRespond($request);
+ }
+
+ /**
+ * Refresh the token and respond with it.
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ * @param array $response Additional response data
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ protected static function refreshAndRespond(Request $request, array $response = [])
{
- // @phpstan-ignore-next-line
- return $this->respondWithToken(Auth::guard()->refresh());
+ $proxyRequest = Request::create('/oauth/token', 'POST', [
+ 'grant_type' => 'refresh_token',
+ 'refresh_token' => $request->refresh_token,
+ 'client_id' => config('auth.proxy.client_id'),
+ 'client_secret' => config('auth.proxy.client_secret'),
+ ]);
+
+ $tokenResponse = app()->handle($proxyRequest);
+
+ return self::respondWithToken($tokenResponse, $response);
}
/**
* Get the token array structure.
*
- * @param string $token Respond with this token.
- * @param array $response Additional response data
+ * @param \Illuminate\Http\JsonResponse $tokenResponse The response containing the token.
+ * @param array $response Additional response data
*
* @return \Illuminate\Http\JsonResponse
*/
- protected static function respondWithToken($token, array $response = [])
+ protected static function respondWithToken($tokenResponse, array $response = [])
{
- $response['access_token'] = $token;
+ $data = json_decode($tokenResponse->getContent());
+
+ if ($tokenResponse->getStatusCode() != 200) {
+ if (isset($data->error) && $data->error == 'secondfactor') {
+ $errors = ['secondfactor' => $data['error_description']];
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
+
+ return response()->json(['status' => 'error', 'message' => __('auth.failed')], 401);
+ }
+
+ $response['access_token'] = $data->access_token;
+ $response['refresh_token'] = $data->refresh_token;
$response['token_type'] = 'bearer';
- // @phpstan-ignore-next-line
- $response['expires_in'] = Auth::guard()->factory()->getTTL() * 60;
+ $response['expires_in'] = $data->expires_in;
return response()->json($response);
}
diff --git a/src/app/Http/Controllers/API/PasswordResetController.php b/src/app/Http/Controllers/API/PasswordResetController.php
--- a/src/app/Http/Controllers/API/PasswordResetController.php
+++ b/src/app/Http/Controllers/API/PasswordResetController.php
@@ -138,6 +138,6 @@
// Remove the verification code
$this->code->delete();
- return AuthController::logonResponse($user);
+ return AuthController::logonResponse($user, $request->password);
}
}
diff --git a/src/app/Http/Controllers/API/SignupController.php b/src/app/Http/Controllers/API/SignupController.php
--- a/src/app/Http/Controllers/API/SignupController.php
+++ b/src/app/Http/Controllers/API/SignupController.php
@@ -332,7 +332,7 @@
DB::commit();
- return AuthController::logonResponse($user);
+ return AuthController::logonResponse($user, $request->password);
}
/**
diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php
--- a/src/app/Providers/AppServiceProvider.php
+++ b/src/app/Providers/AppServiceProvider.php
@@ -7,6 +7,7 @@
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
+use Laravel\Passport\Passport;
class AppServiceProvider extends ServiceProvider
{
@@ -17,7 +18,23 @@
*/
public function register()
{
- //
+ Passport::ignoreMigrations();
+ }
+
+ /**
+ * Serialize a bindings array to a string.
+ *
+ * @return string
+ */
+ private static function serializeSQLBindings(array $array): string
+ {
+ $serialized = array_map(function ($entry) {
+ if ($entry instanceof \DateTime) {
+ return $entry->format('Y-m-d h:i:s');
+ }
+ return $entry;
+ }, $array);
+ return implode(', ', $serialized);
}
/**
@@ -55,7 +72,7 @@
sprintf(
'[SQL] %s [%s]: %.4f sec.',
$query->sql,
- implode(', ', $query->bindings),
+ self::serializeSQLBindings($query->bindings),
$query->time / 1000
)
);
diff --git a/src/app/Providers/AuthServiceProvider.php b/src/app/Providers/AuthServiceProvider.php
--- a/src/app/Providers/AuthServiceProvider.php
+++ b/src/app/Providers/AuthServiceProvider.php
@@ -6,6 +6,7 @@
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
+use Laravel\Passport\Passport;
class AuthServiceProvider extends ServiceProvider
{
@@ -33,5 +34,24 @@
return new LDAPUserProvider($app['hash'], $config['model']);
}
);
+
+ // Hashes all secrets and thus makes them non-recoverable
+ /* Passport::hashClientSecrets(); */
+ // Only enable routes for access tokens
+ Passport::routes(
+ function ($router) {
+ $router->forAccessTokens();
+
+ // Override the default route to avoid rate-limiting.
+ \Route::post('/token', [
+ 'uses' => 'AccessTokenController@issueToken',
+ 'as' => 'passport.token',
+ ]);
+ }
+ );
+
+ Passport::tokensExpireIn(now()->addMinutes(\config('auth.token_expiry_minutes')));
+ Passport::refreshTokensExpireIn(now()->addMinutes(\config('auth.refresh_token_expiry_minutes')));
+ Passport::personalAccessTokensExpireIn(now()->addMonths(6));
}
}
diff --git a/src/app/Providers/PassportServiceProvider.php b/src/app/Providers/PassportServiceProvider.php
new file mode 100644
--- /dev/null
+++ b/src/app/Providers/PassportServiceProvider.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace App\Providers;
+
+use Defuse\Crypto\Key as EncryptionKey;
+use Defuse\Crypto\Encoding as EncryptionEncoding;
+use League\OAuth2\Server\AuthorizationServer;
+use Laravel\Passport\Passport;
+use Laravel\Passport\Bridge;
+
+class PassportServiceProvider extends \Laravel\Passport\PassportServiceProvider
+{
+
+ /**
+ * Make the authorization service instance.
+ *
+ * @return \League\OAuth2\Server\AuthorizationServer
+ */
+ public function makeAuthorizationServer()
+ {
+ return new AuthorizationServer(
+ $this->app->make(Bridge\ClientRepository::class),
+ $this->app->make(Bridge\AccessTokenRepository::class),
+ $this->app->make(Bridge\ScopeRepository::class),
+ $this->makeCryptKey('private'),
+ $this->makeEncryptionKey(app('encrypter')->getKey())
+ );
+ }
+
+
+ /**
+ * Create a Key instance for encrypting the refresh token
+ *
+ * Based on https://github.com/laravel/passport/pull/820
+ *
+ * @param string $keyBytes
+ * @return \Defuse\Crypto\Key
+ */
+ private function makeEncryptionKey($keyBytes)
+ {
+ // First, we will encode Laravel's encryption key into a format that the Defuse\Crypto\Key class can use,
+ // so we can instantiate a new Key object. We need to do this as the Key class has a private constructor method
+ // which means we cannot directly instantiate the class based on our Laravel encryption key.
+ $encryptionKeyAscii = EncryptionEncoding::saveBytesToChecksummedAsciiSafeString(
+ EncryptionKey::KEY_CURRENT_VERSION,
+ $keyBytes
+ );
+
+ // Instantiate a Key object so we can take advantage of significantly faster encryption/decryption
+ // from https://github.com/thephpleague/oauth2-server/pull/814. The improvement is 200x-300x faster.
+ return EncryptionKey::loadFromAsciiSafeString($encryptionKeyAscii);
+ }
+}
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -9,9 +9,11 @@
use App\Traits\SettingsTrait;
use App\Wallet;
use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Support\Facades\Hash;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Iatstuti\Database\Support\NullableFields;
-use Tymon\JWTAuth\Contracts\JWTSubject;
+use Laravel\Passport\HasApiTokens;
+use League\OAuth2\Server\Exception\OAuthServerException;
/**
* The eloquent definition of a User.
@@ -22,12 +24,13 @@
* @property int $status
* @property int $tenant_id
*/
-class User extends Authenticatable implements JWTSubject
+class User extends Authenticatable
{
use NullableFields;
use UserAliasesTrait;
use SettingsTrait;
use SoftDeletes;
+ use HasApiTokens;
// a new user, default on creation
public const STATUS_NEW = 1 << 0;
@@ -395,7 +398,7 @@
* @param string $email Email address
* @param bool $external Search also for an external email
*
- * @return \App\User User model object if found
+ * @return \App\User|null User model object if found
*/
public static function findByEmail(string $email, bool $external = false): ?User
{
@@ -422,15 +425,7 @@
return null;
}
- public function getJWTIdentifier()
- {
- return $this->getKey();
- }
- public function getJWTCustomClaims()
- {
- return [];
- }
/**
* Return groups controlled by the current user.
@@ -755,4 +750,123 @@
$this->attributes['status'] = $new_status;
}
+
+ /**
+ * Validate the user credentials
+ *
+ * @param string $username The username.
+ * @param string $password The password in plain text.
+ * @param bool $updatePassword Store the password if currently empty
+ *
+ * @return bool true on success
+ */
+ public function validateCredentials(string $username, string $password, bool $updatePassword = true): bool
+ {
+ $authenticated = false;
+
+ if ($this->email === \strtolower($username)) {
+ if (!empty($this->password)) {
+ if (Hash::check($password, $this->password)) {
+ $authenticated = true;
+ }
+ } elseif (!empty($this->password_ldap)) {
+ if (substr($this->password_ldap, 0, 6) == "{SSHA}") {
+ $salt = substr(base64_decode(substr($this->password_ldap, 6)), 20);
+
+ $hash = '{SSHA}' . base64_encode(
+ sha1($password . $salt, true) . $salt
+ );
+
+ if ($hash == $this->password_ldap) {
+ $authenticated = true;
+ }
+ } elseif (substr($this->password_ldap, 0, 9) == "{SSHA512}") {
+ $salt = substr(base64_decode(substr($this->password_ldap, 9)), 64);
+
+ $hash = '{SSHA512}' . base64_encode(
+ pack('H*', hash('sha512', $password . $salt)) . $salt
+ );
+
+ if ($hash == $this->password_ldap) {
+ $authenticated = true;
+ }
+ }
+ } else {
+ \Log::error("Incomplete credentials for {$this->email}");
+ }
+ }
+
+ if ($authenticated) {
+ \Log::info("Successful authentication for {$this->email}");
+
+ // TODO: update last login time
+ if ($updatePassword && (empty($this->password) || empty($this->password_ldap))) {
+ $this->password = $password;
+ $this->save();
+ }
+ } else {
+ // TODO: Try actual LDAP?
+ \Log::info("Authentication failed for {$this->email}");
+ }
+
+ return $authenticated;
+ }
+
+ /**
+ * Retrieve and authenticate a user
+ *
+ * @param string $username The username.
+ * @param string $password The password in plain text.
+ * @param string $secondFactor The second factor (secondfactor from current request is used as fallback).
+ *
+ * @return array ['user', 'reason', 'errorMessage']
+ */
+ public static function findAndAuthenticate($username, $password, $secondFactor = null): ?array
+ {
+ $user = User::where('email', $username)->first();
+ if (!$user) {
+ return ['reason' => 'notfound', 'errorMessage' => "User not found."];
+ }
+
+ if (!$user->validateCredentials($username, $password)) {
+ return ['reason' => 'credentials', 'errorMessage' => "Invalid password."];
+ }
+
+
+
+ if (!$secondFactor) {
+ // Check the request if there is a second factor provided
+ // as fallback.
+ $secondFactor = request()->secondfactor;
+ }
+
+ try {
+ (new \App\Auth\SecondFactor($user))->validate($secondFactor);
+ } catch (\Exception $e) {
+ return ['reason' => 'secondfactor', 'errorMessage' => $e->getMessage()];
+ }
+
+ return ['user' => $user];
+ }
+
+ /**
+ * Hook for passport
+ *
+ * @throws \Throwable
+ *
+ * @return \App\User User model object if found
+ */
+ public function findAndValidateForPassport($username, $password): User
+ {
+ $result = self::findAndAuthenticate($username, $password);
+
+ if (isset($result['reason'])) {
+ if ($result['reason'] == 'secondfactor') {
+ // This results in a json response of {'error': 'secondfactor', 'error_description': '$errorMessage'}
+ throw new OAuthServerException($result['errorMessage'], 6, 'secondfactor', 401);
+ }
+ throw OAuthServerException::invalidCredentials();
+ }
+ return $result['user'];
+ }
}
diff --git a/src/composer.json b/src/composer.json
--- a/src/composer.json
+++ b/src/composer.json
@@ -23,6 +23,7 @@
"kolab/net_ldap3": "dev-master",
"laravel/framework": "6.*",
"laravel/horizon": "^3",
+ "laravel/passport": "^9",
"laravel/tinker": "^2.4",
"mollie/laravel-mollie": "^2.9",
"morrislaptop/laravel-queue-clear": "^1.2",
@@ -30,8 +31,7 @@
"spatie/laravel-translatable": "^4.2",
"spomky-labs/otphp": "~4.0.0",
"stripe/stripe-php": "^7.29",
- "swooletw/laravel-swoole": "^2.6",
- "tymon/jwt-auth": "^1.0"
+ "swooletw/laravel-swoole": "^2.6"
},
"require-dev": {
"beyondcode/laravel-dump-server": "^1.0",
diff --git a/src/config/app.php b/src/config/app.php
--- a/src/config/app.php
+++ b/src/config/app.php
@@ -194,6 +194,7 @@
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\HorizonServiceProvider::class,
+ App\Providers\PassportServiceProvider::class,
App\Providers\RouteServiceProvider::class,
],
diff --git a/src/config/auth.php b/src/config/auth.php
--- a/src/config/auth.php
+++ b/src/config/auth.php
@@ -42,7 +42,7 @@
],
'api' => [
- 'driver' => 'jwt',
+ 'driver' => 'passport',
'provider' => 'users',
],
],
@@ -99,4 +99,25 @@
],
],
+
+ /*
+ |--------------------------------------------------------------------------
+ | OAuth Proxy Authentication
+ |--------------------------------------------------------------------------
+ |
+ | If you are planning to use your application to self-authenticate as a
+ | proxy, you can define the client and grant type to use here. This is
+ | sometimes the case when a trusted Single Page Application doesn't
+ | use a backend to send the authentication request, but instead
+ | relies on the API to handle proxying the request to itself.
+ |
+ */
+
+ 'proxy' => [
+ 'client_id' => env('PROXY_OAUTH_CLIENT_ID'),
+ 'client_secret' => env('PROXY_OAUTH_CLIENT_SECRET'),
+ ],
+
+ 'token_expiry_minutes' => env('OAUTH_TOKEN_EXPIRY', 60),
+ 'refresh_token_expiry_minutes' => env('OAUTH_REFRESH_TOKEN_EXPIRY', 30 * 24 * 60),
];
diff --git a/src/config/jwt.php b/src/config/jwt.php
deleted file mode 100644
--- a/src/config/jwt.php
+++ /dev/null
@@ -1,304 +0,0 @@
-<?php
-
-/*
- * This file is part of jwt-auth.
- *
- * (c) Sean Tymon <tymon148@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-return [
-
- /*
- |--------------------------------------------------------------------------
- | JWT Authentication Secret
- |--------------------------------------------------------------------------
- |
- | Don't forget to set this in your .env file, as it will be used to sign
- | your tokens. A helper command is provided for this:
- | `php artisan jwt:secret`
- |
- | Note: This will be used for Symmetric algorithms only (HMAC),
- | since RSA and ECDSA use a private/public key combo (See below).
- |
- */
-
- 'secret' => env('JWT_SECRET'),
-
- /*
- |--------------------------------------------------------------------------
- | JWT Authentication Keys
- |--------------------------------------------------------------------------
- |
- | The algorithm you are using, will determine whether your tokens are
- | signed with a random string (defined in `JWT_SECRET`) or using the
- | following public & private keys.
- |
- | Symmetric Algorithms:
- | HS256, HS384 & HS512 will use `JWT_SECRET`.
- |
- | Asymmetric Algorithms:
- | RS256, RS384 & RS512 / ES256, ES384 & ES512 will use the keys below.
- |
- */
-
- 'keys' => [
-
- /*
- |--------------------------------------------------------------------------
- | Public Key
- |--------------------------------------------------------------------------
- |
- | A path or resource to your public key.
- |
- | E.g. 'file://path/to/public/key'
- |
- */
-
- 'public' => env('JWT_PUBLIC_KEY'),
-
- /*
- |--------------------------------------------------------------------------
- | Private Key
- |--------------------------------------------------------------------------
- |
- | A path or resource to your private key.
- |
- | E.g. 'file://path/to/private/key'
- |
- */
-
- 'private' => env('JWT_PRIVATE_KEY'),
-
- /*
- |--------------------------------------------------------------------------
- | Passphrase
- |--------------------------------------------------------------------------
- |
- | The passphrase for your private key. Can be null if none set.
- |
- */
-
- 'passphrase' => env('JWT_PASSPHRASE'),
-
- ],
-
- /*
- |--------------------------------------------------------------------------
- | JWT time to live
- |--------------------------------------------------------------------------
- |
- | Specify the length of time (in minutes) that the token will be valid for.
- | Defaults to 1 hour.
- |
- | You can also set this to null, to yield a never expiring token.
- | Some people may want this behaviour for e.g. a mobile app.
- | This is not particularly recommended, so make sure you have appropriate
- | systems in place to revoke the token if necessary.
- | Notice: If you set this to null you should remove 'exp' element from 'required_claims' list.
- |
- */
-
- 'ttl' => env('JWT_TTL', 60),
-
- /*
- |--------------------------------------------------------------------------
- | Refresh time to live
- |--------------------------------------------------------------------------
- |
- | Specify the length of time (in minutes) that the token can be refreshed
- | within. I.E. The user can refresh their token within a 2 week window of
- | the original token being created until they must re-authenticate.
- | Defaults to 2 weeks.
- |
- | You can also set this to null, to yield an infinite refresh time.
- | Some may want this instead of never expiring tokens for e.g. a mobile app.
- | This is not particularly recommended, so make sure you have appropriate
- | systems in place to revoke the token if necessary.
- |
- */
-
- 'refresh_ttl' => env('JWT_REFRESH_TTL', 20160),
-
- /*
- |--------------------------------------------------------------------------
- | JWT hashing algorithm
- |--------------------------------------------------------------------------
- |
- | Specify the hashing algorithm that will be used to sign the token.
- |
- | See here: https://github.com/namshi/jose/tree/master/src/Namshi/JOSE/Signer/OpenSSL
- | for possible values.
- |
- */
-
- 'algo' => env('JWT_ALGO', 'HS256'),
-
- /*
- |--------------------------------------------------------------------------
- | Required Claims
- |--------------------------------------------------------------------------
- |
- | Specify the required claims that must exist in any token.
- | A TokenInvalidException will be thrown if any of these claims are not
- | present in the payload.
- |
- */
-
- 'required_claims' => [
- 'iss',
- 'iat',
- 'exp',
- 'nbf',
- 'sub',
- 'jti',
- ],
-
- /*
- |--------------------------------------------------------------------------
- | Persistent Claims
- |--------------------------------------------------------------------------
- |
- | Specify the claim keys to be persisted when refreshing a token.
- | `sub` and `iat` will automatically be persisted, in
- | addition to the these claims.
- |
- | Note: If a claim does not exist then it will be ignored.
- |
- */
-
- 'persistent_claims' => [
- // 'foo',
- // 'bar',
- ],
-
- /*
- |--------------------------------------------------------------------------
- | Lock Subject
- |--------------------------------------------------------------------------
- |
- | This will determine whether a `prv` claim is automatically added to
- | the token. The purpose of this is to ensure that if you have multiple
- | authentication models e.g. `App\User` & `App\OtherPerson`, then we
- | should prevent one authentication request from impersonating another,
- | if 2 tokens happen to have the same id across the 2 different models.
- |
- | Under specific circumstances, you may want to disable this behaviour
- | e.g. if you only have one authentication model, then you would save
- | a little on token size.
- |
- */
-
- 'lock_subject' => true,
-
- /*
- |--------------------------------------------------------------------------
- | Leeway
- |--------------------------------------------------------------------------
- |
- | This property gives the jwt timestamp claims some "leeway".
- | Meaning that if you have any unavoidable slight clock skew on
- | any of your servers then this will afford you some level of cushioning.
- |
- | This applies to the claims `iat`, `nbf` and `exp`.
- |
- | Specify in seconds - only if you know you need it.
- |
- */
-
- 'leeway' => env('JWT_LEEWAY', 0),
-
- /*
- |--------------------------------------------------------------------------
- | Blacklist Enabled
- |--------------------------------------------------------------------------
- |
- | In order to invalidate tokens, you must have the blacklist enabled.
- | If you do not want or need this functionality, then set this to false.
- |
- */
-
- 'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true),
-
- /*
- | -------------------------------------------------------------------------
- | Blacklist Grace Period
- | -------------------------------------------------------------------------
- |
- | When multiple concurrent requests are made with the same JWT,
- | it is possible that some of them fail, due to token regeneration
- | on every request.
- |
- | Set grace period in seconds to prevent parallel request failure.
- |
- */
-
- 'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 0),
-
- /*
- |--------------------------------------------------------------------------
- | Cookies encryption
- |--------------------------------------------------------------------------
- |
- | By default Laravel encrypt cookies for security reason.
- | If you decide to not decrypt cookies, you will have to configure Laravel
- | to not encrypt your cookie token by adding its name into the $except
- | array available in the middleware "EncryptCookies" provided by Laravel.
- | see https://laravel.com/docs/master/responses#cookies-and-encryption
- | for details.
- |
- | Set it to true if you want to decrypt cookies.
- |
- */
-
- 'decrypt_cookies' => false,
-
- /*
- |--------------------------------------------------------------------------
- | Providers
- |--------------------------------------------------------------------------
- |
- | Specify the various providers used throughout the package.
- |
- */
-
- 'providers' => [
-
- /*
- |--------------------------------------------------------------------------
- | JWT Provider
- |--------------------------------------------------------------------------
- |
- | Specify the provider that is used to create and decode the tokens.
- |
- */
-
- 'jwt' => Tymon\JWTAuth\Providers\JWT\Lcobucci::class,
-
- /*
- |--------------------------------------------------------------------------
- | Authentication Provider
- |--------------------------------------------------------------------------
- |
- | Specify the provider that is used to authenticate users.
- |
- */
-
- 'auth' => Tymon\JWTAuth\Providers\Auth\Illuminate::class,
-
- /*
- |--------------------------------------------------------------------------
- | Storage Provider
- |--------------------------------------------------------------------------
- |
- | Specify the provider that is used to store tokens in the blacklist.
- |
- */
-
- 'storage' => Tymon\JWTAuth\Providers\Storage\Illuminate::class,
-
- ],
-
-];
diff --git a/src/config/swoole_http.php b/src/config/swoole_http.php
--- a/src/config/swoole_http.php
+++ b/src/config/swoole_http.php
@@ -101,7 +101,9 @@
'providers' => [
Illuminate\Pagination\PaginationServiceProvider::class,
App\Providers\AuthServiceProvider::class,
- Tymon\JWTAuth\Providers\LaravelServiceProvider::class,
+ //Without this passport will sort of work,
+ //but PassportServiceProvider will not contain a valid app instance.
+ App\Providers\PassportServiceProvider::class,
],
/*
diff --git a/src/database/migrations/2021_04_28_090011_create_oauth_tables.php b/src/database/migrations/2021_04_28_090011_create_oauth_tables.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2021_04_28_090011_create_oauth_tables.php
@@ -0,0 +1,92 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+// phpcs:ignore
+class CreateOauthTables extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::create('oauth_clients', function (Blueprint $table) {
+ $table->bigIncrements('id');
+ $table->bigInteger('user_id')->nullable()->index();
+ $table->string('name');
+ $table->string('secret', 100)->nullable();
+ $table->string('provider')->nullable();
+ $table->text('redirect');
+ $table->boolean('personal_access_client');
+ $table->boolean('password_client');
+ $table->boolean('revoked');
+ $table->timestamps();
+
+ $table->foreign('user_id')
+ ->references('id')->on('users')
+ ->onDelete('cascade')
+ ->onUpdate('cascade');
+ });
+
+ Schema::create('oauth_personal_access_clients', function (Blueprint $table) {
+ $table->bigIncrements('id');
+ $table->bigInteger('client_id');
+ $table->timestamps();
+ });
+
+ Schema::create('oauth_auth_codes', function (Blueprint $table) {
+ $table->string('id', 100)->primary();
+ $table->bigInteger('user_id')->index();
+ $table->bigInteger('client_id');
+ $table->text('scopes')->nullable();
+ $table->boolean('revoked');
+ $table->dateTime('expires_at')->nullable();
+
+ $table->foreign('user_id')
+ ->references('id')->on('users')
+ ->onDelete('cascade')
+ ->onUpdate('cascade');
+ });
+
+ Schema::create('oauth_access_tokens', function (Blueprint $table) {
+ $table->string('id', 100)->primary();
+ $table->bigInteger('user_id')->nullable()->index();
+ $table->bigInteger('client_id');
+ $table->string('name')->nullable();
+ $table->text('scopes')->nullable();
+ $table->boolean('revoked');
+ $table->timestamps();
+ $table->dateTime('expires_at')->nullable();
+
+ $table->foreign('user_id')
+ ->references('id')->on('users')
+ ->onDelete('cascade')
+ ->onUpdate('cascade');
+ });
+
+ Schema::create('oauth_refresh_tokens', function (Blueprint $table) {
+ $table->string('id', 100)->primary();
+ $table->string('access_token_id', 100)->index();
+ $table->boolean('revoked');
+ $table->dateTime('expires_at')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('oauth_auth_codes');
+ Schema::dropIfExists('oauth_refresh_tokens');
+ Schema::dropIfExists('oauth_access_tokens');
+ Schema::dropIfExists('oauth_personal_access_clients');
+ Schema::dropIfExists('oauth_clients');
+ }
+}
diff --git a/src/database/seeds/DatabaseSeeder.php b/src/database/seeds/DatabaseSeeder.php
--- a/src/database/seeds/DatabaseSeeder.php
+++ b/src/database/seeds/DatabaseSeeder.php
@@ -22,6 +22,7 @@
'PlanSeeder',
'UserSeeder',
'OpenViduRoomSeeder',
+ 'OauthClientSeeder',
];
$env = ucfirst(App::environment());
diff --git a/src/database/seeds/local/OauthClientSeeder.php b/src/database/seeds/local/OauthClientSeeder.php
new file mode 100644
--- /dev/null
+++ b/src/database/seeds/local/OauthClientSeeder.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Database\Seeds\Local;
+
+use Laravel\Passport\Passport;
+use Illuminate\Database\Seeder;
+
+class OauthClientSeeder extends Seeder
+{
+ /**
+ * Run the database seeds.
+ *
+ * This emulates './artisan passport:client --password --name="Kolab Password Grant Client" --provider=users'
+ *
+ * @return void
+ */
+ public function run()
+ {
+ $client = Passport::client()->forceFill([
+ 'user_id' => null,
+ 'name' => "Kolab Password Grant Client",
+ 'secret' => 'JF4pL68ucLuMupaOviTeG8EJeQpjtZtcGLp4f0dq',
+ 'provider' => 'users',
+ 'redirect' => 'http://localhost',
+ 'personal_access_client' => 0,
+ 'password_client' => 1,
+ 'revoked' => false,
+ ]);
+
+ $client->save();
+ }
+}
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -143,6 +143,7 @@
}
localStorage.setItem('token', response.access_token)
+ localStorage.setItem('refreshToken', response.refresh_token)
axios.defaults.headers.common.Authorization = 'Bearer ' + response.access_token
if (response.email) {
@@ -168,7 +169,7 @@
// while the token is being refreshed
this.refreshTimeout = setTimeout(() => {
- axios.post('/api/auth/refresh').then(response => {
+ axios.post('/api/auth/refresh', {'refresh_token': response.refresh_token}).then(response => {
this.loginUser(response.data, false, true)
})
}, timeout * 1000)
@@ -177,6 +178,7 @@
logoutUser(redirect) {
store.commit('logoutUser')
localStorage.setItem('token', '')
+ localStorage.setItem('refreshToken', '')
delete axios.defaults.headers.common.Authorization
if (redirect !== false) {
diff --git a/src/resources/vue/App.vue b/src/resources/vue/App.vue
--- a/src/resources/vue/App.vue
+++ b/src/resources/vue/App.vue
@@ -32,7 +32,7 @@
this.$root.startLoading()
axios.defaults.headers.common.Authorization = 'Bearer ' + token
- axios.get('/api/auth/info?refresh_token=1')
+ axios.post('/api/auth/info?refresh=1', {refresh_token: localStorage.getItem("refreshToken")})
.then(response => {
this.$root.loginUser(response.data, false)
this.$root.stopLoading()
diff --git a/src/resources/vue/PasswordReset.vue b/src/resources/vue/PasswordReset.vue
--- a/src/resources/vue/PasswordReset.vue
+++ b/src/resources/vue/PasswordReset.vue
@@ -49,6 +49,16 @@
<label for="reset_confirm" class="sr-only">{{ $t('form.password-confirm') }}</label>
<input type="password" class="form-control" id="reset_confirm" :placeholder="$t('form.password-confirm')" required v-model="password_confirmation">
</div>
+ <div class="form-group pt-3">
+ <label for="secondfactor" class="sr-only">2FA</label>
+ <div class="input-group">
+ <span class="input-group-prepend">
+ <span class="input-group-text"><svg-icon icon="key"></svg-icon></span>
+ </span>
+ <input type="text" id="secondfactor" class="form-control rounded-right" placeholder="Second factor code" v-model="secondFactor">
+ </div>
+ <small class="form-text text-muted">Second factor code is optional for users with no 2-Factor Authentication setup.</small>
+ </div>
<button class="btn btn-secondary" type="button" @click="stepBack">{{ $t('btn.back') }}</button>
<button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}</button>
</form>
@@ -66,6 +76,7 @@
short_code: '',
password: '',
password_confirmation: '',
+ secondFactor: '',
fromEmail: window.config['mail.from.address']
}
},
@@ -127,7 +138,8 @@
code: this.code,
short_code: this.short_code,
password: this.password,
- password_confirmation: this.password_confirmation
+ password_confirmation: this.password_confirmation,
+ secondfactor: this.secondFactor
}).then(response => {
// auto-login and goto dashboard
this.$root.loginUser(response.data)
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -27,6 +27,7 @@
['middleware' => 'auth:api'],
function ($router) {
Route::get('info', 'API\AuthController@info');
+ Route::post('info', 'API\AuthController@info');
Route::post('logout', 'API\AuthController@logout');
Route::post('refresh', 'API\AuthController@refresh');
}
diff --git a/src/tests/Feature/Controller/AuthTest.php b/src/tests/Feature/Controller/AuthTest.php
--- a/src/tests/Feature/Controller/AuthTest.php
+++ b/src/tests/Feature/Controller/AuthTest.php
@@ -8,6 +8,28 @@
class AuthTest extends TestCase
{
+ private $expectedExpiry;
+
+ /**
+ * Reset all authentication guards to clear any cache users
+ */
+ protected function resetAuth()
+ {
+ $guards = array_keys(config('auth.guards'));
+
+ foreach ($guards as $guard) {
+ $guard = $this->app['auth']->guard($guard);
+
+ if ($guard instanceof \Illuminate\Auth\SessionGuard) {
+ $guard->logout();
+ }
+ }
+
+ $protectedProperty = new \ReflectionProperty($this->app['auth'], 'guards');
+ $protectedProperty->setAccessible(true);
+ $protectedProperty->setValue($this->app['auth'], []);
+ }
+
/**
* {@inheritDoc}
*/
@@ -17,6 +39,8 @@
$this->deleteTestUser('UsersControllerTest1@userscontroller.com');
$this->deleteTestDomain('userscontroller.com');
+
+ $this->expectedExpiry = \config('auth.token_expiry_minutes') * 60;
}
/**
@@ -57,12 +81,13 @@
// Note: Details of the content are tested in testUserResponse()
// Test token refresh via the info request
- // First we log in as we need the token (actingAs() will not work)
+ // First we log in to get the refresh token
$post = ['email' => 'john@kolab.org', 'password' => 'simple123'];
+ $user = $this->getTestUser('john@kolab.org');
$response = $this->post("api/auth/login", $post);
$json = $response->json();
- $response = $this->withHeaders(['Authorization' => 'Bearer ' . $json['access_token']])
- ->get("api/auth/info?refresh_token=1");
+ $response = $this->actingAs($user)
+ ->post("api/auth/info?refresh=1", ['refresh_token' => $json['refresh_token']]);
$response->assertStatus(200);
$json = $response->json();
@@ -108,7 +133,10 @@
$response->assertStatus(200);
$this->assertTrue(!empty($json['access_token']));
- $this->assertEquals(\config('jwt.ttl') * 60, $json['expires_in']);
+ $this->assertTrue(
+ ($this->expectedExpiry - 5) < $json['expires_in'] &&
+ $json['expires_in'] < ($this->expectedExpiry + 5)
+ );
$this->assertEquals('bearer', $json['token_type']);
$this->assertEquals($user->id, $json['id']);
$this->assertEquals($user->email, $json['email']);
@@ -123,7 +151,10 @@
$response->assertStatus(200);
$this->assertTrue(!empty($json['access_token']));
- $this->assertEquals(\config('jwt.ttl') * 60, $json['expires_in']);
+ $this->assertTrue(
+ ($this->expectedExpiry - 5) < $json['expires_in'] &&
+ $json['expires_in'] < ($this->expectedExpiry + 5)
+ );
$this->assertEquals('bearer', $json['token_type']);
// TODO: We have browser tests for 2FA but we should probably also test it here
@@ -146,6 +177,10 @@
$response = $this->json('POST', "api/auth/logout", []);
$response->assertStatus(401);
+ // Request with invalid token
+ $response = $this->withHeaders(['Authorization' => 'Bearer ' . "foobar"])->post("api/auth/logout");
+ $response->assertStatus(401);
+
// Request with valid token
$response = $this->withHeaders(['Authorization' => 'Bearer ' . $token])->post("api/auth/logout");
$response->assertStatus(200);
@@ -154,6 +189,7 @@
$this->assertEquals('success', $json['status']);
$this->assertEquals('Successfully logged out.', $json['message']);
+ $this->resetAuth();
// Check if it really destroyed the token?
$response = $this->withHeaders(['Authorization' => 'Bearer ' . $token])->get("api/auth/info");
@@ -180,15 +216,20 @@
$json = $response->json();
$token = $json['access_token'];
+ $user = $this->getTestUser('john@kolab.org');
+
// Request with a valid token
- $response = $this->withHeaders(['Authorization' => 'Bearer ' . $token])->post("api/auth/refresh");
+ $response = $this->actingAs($user)->post("api/auth/refresh", ['refresh_token' => $json['refresh_token']]);
$response->assertStatus(200);
$json = $response->json();
$this->assertTrue(!empty($json['access_token']));
$this->assertTrue($json['access_token'] != $token);
- $this->assertEquals(\config('jwt.ttl') * 60, $json['expires_in']);
+ $this->assertTrue(
+ ($this->expectedExpiry - 5) < $json['expires_in'] &&
+ $json['expires_in'] < ($this->expectedExpiry + 5)
+ );
$this->assertEquals('bearer', $json['token_type']);
$new_token = $json['access_token'];
diff --git a/src/tests/Unit/UserTest.php b/src/tests/Unit/UserTest.php
--- a/src/tests/Unit/UserTest.php
+++ b/src/tests/Unit/UserTest.php
@@ -39,6 +39,31 @@
$this->assertSame($ssh512, $user->password_ldap);
}
+ /**
+ * Test User password validation
+ */
+ public function testPasswordValidation(): void
+ {
+ $user = new User(['email' => 'user@email.com']);
+ $user->password = 'test';
+
+ $this->assertSame(true, $user->validateCredentials('user@email.com', 'test'));
+ $this->assertSame(false, $user->validateCredentials('user@email.com', 'wrong'));
+ $this->assertSame(true, $user->validateCredentials('User@Email.Com', 'test'));
+ $this->assertSame(false, $user->validateCredentials('wrong', 'test'));
+
+ // Ensure the fallback to the ldap_password works if the current password is empty
+ $ssh512 = "{SSHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ"
+ . "6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w==";
+ $ldapUser = new User(['email' => 'user2@email.com']);
+ $ldapUser->setRawAttributes(['password' => '', 'password_ldap' => $ssh512, 'email' => 'user2@email.com']);
+ $this->assertSame($ldapUser->password, '');
+ $this->assertSame($ldapUser->password_ldap, $ssh512);
+
+ $this->assertSame(true, $ldapUser->validateCredentials('user2@email.com', 'test', false));
+ $ldapUser->delete();
+ }
+
/**
* Test basic User funtionality
*/

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 3, 7:26 AM (13 h, 21 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18823010
Default Alt Text
D2494.1775201186.diff (52 KB)

Event Timeline