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 @@ -128,8 +128,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/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); @@ -36,12 +37,21 @@ */ public static function logonResponse(User $user) { - // @phpstan-ignore-next-line - $token = Auth::guard()->login($user); + $proxyRequest = Request::create('/oauth/token', 'POST', [ + 'username' => $user->email, + 'password' => $user->password, + 'grant_type' => 'password', + 'client_id' => config('auth.proxy.client_id'), + 'client_secret' => config('auth.proxy.client_secret'), + 'scopes' => '[*]' + ]); + + $tokenResponse = app()->handle($proxyRequest); + $response = V4\UsersController::userResponse($user); $response['status'] = 'success'; - return self::respondWithToken($token, $response); + return self::respondWithToken($tokenResponse, $response); } /** @@ -66,19 +76,33 @@ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); } - $credentials = $request->only('email', 'password'); + $proxyRequest = Request::create('/oauth/token', 'POST', [ + 'username' => $request->email, + 'password' => $request->password, + 'grant_type' => 'password', + 'client_id' => config('auth.proxy.client_id'), + 'client_secret' => config('auth.proxy.client_secret'), + 'scopes' => '[*]' + ]); + + $tokenResponse = app()->handle($proxyRequest); + + if ($tokenResponse->getStatusCode() === 200) { + $user = \App\User::where('email', $request->email)->first(); + if (!$user) { + throw new \Exception("Authentication required."); + } - if ($token = Auth::guard()->attempt($credentials)) { - $user = Auth::guard()->user(); $sf = new \App\Auth\SecondFactor($user); + // Returns null on success if ($response = $sf->requestHandler($request)) { return $response; } $response = V4\UsersController::userResponse($user); - return $this->respondWithToken($token, $response); + return $this->respondWithToken($tokenResponse, $response); } return response()->json(['status' => 'error', 'message' => __('auth.failed')], 401); @@ -91,8 +115,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 +136,42 @@ * * @return \Illuminate\Http\JsonResponse */ - public function refresh() + public function refresh(Request $request) { - // @phpstan-ignore-next-line - return $this->respondWithToken(Auth::guard()->refresh()); + return self::refreshAndRespond($request); + } + + + protected static function refreshAndRespond($request, array $response = []) + { + $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()); + + $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/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php --- a/src/app/Providers/AppServiceProvider.php +++ b/src/app/Providers/AppServiceProvider.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; use Illuminate\Support\ServiceProvider; +use Laravel\Passport\Passport; class AppServiceProvider extends ServiceProvider { @@ -43,6 +44,8 @@ \App\VerificationCode::observe(\App\Observers\VerificationCodeObserver::class); \App\Wallet::observe(\App\Observers\WalletObserver::class); + Passport::ignoreMigrations(); + Schema::defaultStringLength(191); // Log SQL queries in debug mode 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,11 @@ return new LDAPUserProvider($app['hash'], $config['model']); } ); + //Hashes all secrets and thus makes them non-recoverable + /* Passport::hashClientSecrets(); */ + Passport::routes(); + 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/User.php b/src/app/User.php --- a/src/app/User.php +++ b/src/app/User.php @@ -11,7 +11,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Iatstuti\Database\Support\NullableFields; -use Tymon\JWTAuth\Contracts\JWTSubject; +use Laravel\Passport\HasApiTokens; /** * The eloquent definition of a User. @@ -21,12 +21,13 @@ * @property string $password * @property int $status */ -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; @@ -401,15 +402,7 @@ return null; } - public function getJWTIdentifier() - { - return $this->getKey(); - } - public function getJWTCustomClaims() - { - return []; - } /** * Return groups controlled by the current user. diff --git a/src/composer.json b/src/composer.json --- a/src/composer.json +++ b/src/composer.json @@ -22,6 +22,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", @@ -29,8 +30,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 @@ -183,6 +183,7 @@ * Package Service Providers... */ Barryvdh\DomPDF\ServiceProvider::class, + Laravel\Passport\PassportServiceProvider::class, /* * Application Service Providers... 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' => 1 * 24 * 60, + 'refresh_token_expiry_minutes' => 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 @@ - - * - * 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. + Laravel\Passport\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 @@ +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 @@ -21,6 +21,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 @@ +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 @@ -137,6 +137,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) { @@ -171,6 +172,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 @@ -24,7 +24,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/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,27 @@ class AuthTest extends TestCase { + + /** + * 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} */ @@ -57,12 +78,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 +130,7 @@ $response->assertStatus(200); $this->assertTrue(!empty($json['access_token'])); - $this->assertEquals(\config('jwt.ttl') * 60, $json['expires_in']); + $this->assertEquals(\config('auth.token_expiry_minutes') * 60, $json['expires_in']); $this->assertEquals('bearer', $json['token_type']); $this->assertEquals($user->id, $json['id']); $this->assertEquals($user->email, $json['email']); @@ -123,7 +145,7 @@ $response->assertStatus(200); $this->assertTrue(!empty($json['access_token'])); - $this->assertEquals(\config('jwt.ttl') * 60, $json['expires_in']); + $this->assertEquals(\config('auth.token_expiry_minutes') * 60, $json['expires_in']); $this->assertEquals('bearer', $json['token_type']); // TODO: We have browser tests for 2FA but we should probably also test it here @@ -146,6 +168,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 +180,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 +207,17 @@ $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->assertEquals(\config('auth.token_expiry_minutes') * 60, $json['expires_in']); $this->assertEquals('bearer', $json['token_type']); $new_token = $json['access_token'];