diff --git a/src/.env.example b/src/.env.example --- a/src/.env.example +++ b/src/.env.example @@ -98,6 +98,7 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" JWT_SECRET= +JWT_TTL=60 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 @@ -14,21 +14,7 @@ class LDAPUserProvider extends EloquentUserProvider implements UserProvider { /** - * Retrieve the user by its ID. - * - * @param string $identifier The unique ID for the user to attempt to retrieve. - * - * @return \Illuminate\Contracts\Auth\Authenticatable|null - */ - public function retrieveById($identifier) - { - return parent::retrieveById($identifier); - } - - /** - * Retrieve the user by its credentials. - * - * Please note that this function also validates the password. + * Retrieve the user by its credentials (email). * * @param array $credentials An array containing the email and password. * @@ -36,18 +22,12 @@ */ public function retrieveByCredentials(array $credentials) { - $entries = User::where('email', '=', $credentials['email']); + $entries = User::where('email', '=', $credentials['email'])->get(); $count = $entries->count(); if ($count == 1) { - $user = $entries->select(['id', 'email', 'password', 'password_ldap'])->first(); - - if (!$this->validateCredentials($user, $credentials)) { - return null; - } - - return $user; + return $entries->first(); } if ($count > 1) { @@ -103,12 +83,10 @@ } } - // TODO: update last login time - // TODO: Update password if necessary, examine whether writing to - // user->password is sufficient? 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(); diff --git a/src/app/Http/Kernel.php b/src/app/Http/Kernel.php --- a/src/app/Http/Kernel.php +++ b/src/app/Http/Kernel.php @@ -30,13 +30,13 @@ */ protected $middlewareGroups = [ 'web' => [ - \App\Http\Middleware\EncryptCookies::class, - \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, - \Illuminate\Session\Middleware\StartSession::class, + // \App\Http\Middleware\EncryptCookies::class, + // \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + // \Illuminate\Session\Middleware\StartSession::class, // \Illuminate\Session\Middleware\AuthenticateSession::class, - \Illuminate\View\Middleware\ShareErrorsFromSession::class, - \App\Http\Middleware\VerifyCsrfToken::class, - \Illuminate\Routing\Middleware\SubstituteBindings::class, + // \Illuminate\View\Middleware\ShareErrorsFromSession::class, + // \App\Http\Middleware\VerifyCsrfToken::class, + // \Illuminate\Routing\Middleware\SubstituteBindings::class, ], 'api' => [ diff --git a/src/app/User.php b/src/app/User.php --- a/src/app/User.php +++ b/src/app/User.php @@ -653,12 +653,7 @@ */ public function setPasswordLdapAttribute($password) { - if (!empty($password)) { - $this->attributes['password'] = bcrypt($password, [ "rounds" => 12 ]); - $this->attributes['password_ldap'] = '{SSHA512}' . base64_encode( - pack('H*', hash('sha512', $password)) - ); - } + $this->setPasswordAttribute($password); } /** 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 @@ -50,17 +50,40 @@ return false }, // Set user state to "logged in" - loginUser(token, dashboard) { - store.commit('logoutUser') // destroy old state data - store.commit('loginUser') - localStorage.setItem('token', token) - axios.defaults.headers.common.Authorization = 'Bearer ' + token + loginUser(response, dashboard, update) { + if (!update) { + store.commit('logoutUser') // destroy old state data + store.commit('loginUser') + } + + localStorage.setItem('token', response.access_token) + axios.defaults.headers.common.Authorization = 'Bearer ' + response.access_token if (dashboard !== false) { this.$router.push(store.state.afterLogin || { name: 'dashboard' }) } store.state.afterLogin = null + + // Refresh the token before it expires + let timeout = response.expires_in || 0 + + // We'll refresh 60 seconds before the token expires + // or immediately when we have no expiration time (on token re-use) + if (timeout > 60) { + timeout -= 60 + } + + // TODO: We probably should try a few times in case of an error + // TODO: We probably should prevent axios from doing any requests + // while the token is being refreshed + + this.refreshTimeout = setTimeout(() => { + axios.post('/api/auth/refresh').then(response => { + this.loginUser(response.data, false, true) + }) + + }, timeout * 1000) }, // Set user state to "not logged in" logoutUser() { @@ -68,6 +91,7 @@ localStorage.setItem('token', '') delete axios.defaults.headers.common.Authorization this.$router.push({ name: 'login' }) + clearTimeout(this.refreshTimeout) }, // Display "loading" overlay inside of the specified element addLoader(elem) { 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 @@ -21,7 +21,7 @@ .then(response => { this.isLoading = false this.$root.stopLoading() - this.$root.loginUser(token, false) + this.$root.loginUser({ access_token: token }, false) this.$store.state.authInfo = response.data }) .catch(error => { diff --git a/src/resources/vue/Login.vue b/src/resources/vue/Login.vue --- a/src/resources/vue/Login.vue +++ b/src/resources/vue/Login.vue @@ -68,7 +68,7 @@ secondfactor: this.secondFactor }).then(response => { // login user and redirect to dashboard - this.$root.loginUser(response.data.access_token) + this.$root.loginUser(response.data) }) } } 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 @@ -129,7 +129,7 @@ password_confirmation: this.password_confirmation }).then(response => { // auto-login and goto dashboard - this.$root.loginUser(response.data.access_token) + this.$root.loginUser(response.data) }) }, // Moves the user a step back in registration form diff --git a/src/resources/vue/Signup.vue b/src/resources/vue/Signup.vue --- a/src/resources/vue/Signup.vue +++ b/src/resources/vue/Signup.vue @@ -233,7 +233,7 @@ voucher: this.voucher }).then(response => { // auto-login and goto dashboard - this.$root.loginUser(response.data.access_token) + this.$root.loginUser(response.data) }) }, // Moves the user a step back in registration form 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 @@ -125,9 +125,42 @@ $response->assertStatus(401); } + /** + * Test /api/auth/refresh + */ public function testRefresh(): void { - // TODO - $this->markTestIncomplete(); + // Request with no token, testing that it requires auth + $response = $this->post("api/auth/refresh"); + $response->assertStatus(401); + + // Test the same using JSON mode + $response = $this->json('POST', "api/auth/refresh", []); + $response->assertStatus(401); + + // Login the user to get a valid token + $post = ['email' => 'john@kolab.org', 'password' => 'simple123']; + $response = $this->post("api/auth/login", $post); + $response->assertStatus(200); + $json = $response->json(); + $token = $json['access_token']; + + // Request with a valid token + $response = $this->withHeaders(['Authorization' => 'Bearer ' . $token])->post("api/auth/refresh"); + $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('bearer', $json['token_type']); + $new_token = $json['access_token']; + + // TODO: Shall we invalidate the old token? + + // And if the new token is working + $response = $this->withHeaders(['Authorization' => 'Bearer ' . $new_token])->get("api/auth/info"); + $response->assertStatus(200); } } 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 @@ -8,9 +8,41 @@ class UserTest extends TestCase { /** + * Test User password mutator + */ + public function testSetPasswordAttribute(): void + { + $user = new User(['email' => 'user@email.com']); + + $user->password = 'test'; + + $ssh512 = "{SSHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ" + . "6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="; + + $this->assertRegExp('/^\$2y\$12\$[0-9a-zA-Z\/.]{53}$/', $user->password); + $this->assertSame($ssh512, $user->password_ldap); + } + + /** + * Test User password mutator + */ + public function testSetPasswordLdapAttribute(): void + { + $user = new User(['email' => 'user@email.com']); + + $user->password_ldap = 'test'; + + $ssh512 = "{SSHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ" + . "6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="; + + $this->assertRegExp('/^\$2y\$12\$[0-9a-zA-Z\/.]{53}$/', $user->password); + $this->assertSame($ssh512, $user->password_ldap); + } + + /** * Test basic User funtionality */ - public function testUserStatus() + public function testStatus(): void { $statuses = [ User::STATUS_NEW, @@ -43,7 +75,7 @@ /** * Test setStatusAttribute exception */ - public function testUserStatusInvalid(): void + public function testStatusInvalid(): void { $this->expectException(\Exception::class);