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 @@ -41,7 +41,7 @@ $count = $entries->count(); if ($count == 1) { - $user = $entries->select(['id', 'email', 'password', 'password_ldap'])->first(); + $user = $entries->first(); if (!$this->validateCredentials($user, $credentials)) { return null; 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/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); } }