go to part...
An authentication system is an important part of our SPA. Before we start though, let's alter our API\AuthController@login
method from the first part of this tutorial. I didn't realize it right away, but the way things are done now is a bit inconvenient. You're probably going to need information about the user we just logged in, not just an access token alone. Below is the modified method's code, I added comments so no additional explanations are needed.
public function login()
{
// Check if a user with the specified email exists
$user = User::whereEmail(request('username'))->first();
if (!$user) {
return response()->json([
'message' => 'Wrong email or password',
'status' => 422
], 422);
}
// If a user with the email was found - check if the specified password
// belongs to this user
if (!Hash::check(request('password'), $user->password)) {
return response()->json([
'message' => 'Wrong email or password',
'status' => 422
], 422);
}
// Send an internal API request to get an access token
$client = DB::table('oauth_clients')
->where('password_client', true)
->first();
// Make sure a Password Client exists in the DB
if (!$client) {
return response()->json([
'message' => 'Laravel Passport is not setup properly.',
'status' => 500
], 500);
}
$data = [
'grant_type' => 'password',
'client_id' => $client->id,
'client_secret' => $client->secret,
'username' => request('username'),
'password' => request('password'),
];
$request = Request::create('/oauth/token', 'POST', $data);
$response = app()->handle($request);
// Check if the request was successful
if ($response->getStatusCode() != 200) {
return response()->json([
'message' => 'Wrong email or password',
'status' => 422
], 422);
}
// Get the data from the response
$data = json_decode($response->getContent());
// Format the final response in a desirable format
return response()->json([
'token' => $data->access_token,
'user' => $user,
'status' => 200
]);
}
Now let's take care of the login page. Edit the Login.vue
file like this:
<template>
<div>
<h1>Login</h1>
<p>
<label for="username">Email</label>
<input type="text" name="username" v-model="username">
</p>
<p>
<label for="password">Password</label>
<input type="password" name="password" v-model="password">
</p>
<button @click="login">Login</button>
</div>
</template>
<script>
export default {
data() {
return {
username: '',
password: '',
};
},
methods: {
login() {
let data = {
username: this.username,
password: this.password
};
axios.post('/api/login', data)
.then(({data}) => {
// TODO: store data
// data.token
// data.user
})
.catch(({response}) => {
alert(response.data.message);
});
}
}
}
</script>
This is just a simple form that sends an AJAX request using axios
(which comes with Laravel by default) to our /api/login
endpoint. There's nothing to elaborate on here if you know some basic Vue.js.
So we're getting a token and information about the user in the response. We need to persist this somewhere, so that this data is not lost when user reloads the page or closes the browser. We're going to store everything in HTML5 Local Storage. This method makes your data vulnerable to an XSS attack and to prevent and XSS attack you should always sanitize any user submitted data on the back-end. There's another way to store the token but it is also not entirely safe. This article describes the problem in detail.
Let's create an ES6 class Auth
which will take care of authentication related things, kind of like the Laravel's Auth class. Create a new file resources/assets/js/auth.js
which will be a singleton:
class Auth {
constructor() {
}
}
export default new Auth();
Make the instance of the class available globally by adding the following code at the top of the resources/assets/js/app.js
file:
import auth from './auth.js';
window.auth = auth;
Next, let's go to auth.js
and create a login
method, which will simply store the received data.
class Auth {
constructor() {
}
login (token, user) {
window.localStorage.setItem('token', token);
window.localStorage.setItem('user', JSON.stringify(user));
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
}
}
Working with HTML5 Local Storage is extremely easy. It is a simple key-value storage that can only store strings as values. Since user
is an object we need to "stringify" it first. After storing the data we're setting the Authorization
header for axios to include our token with all the further AJAX requests.
Finally, we need to call this new method in the Login.vue
file after a successful response from our API. Also, it makes sense to redirect our user to the "authenticated users only" part of the app, in our case it is the /dashboard
page. The redirect is performed using the push
method on the router, and the router itself can be accessed inside any of Vue files via this.$router
.
axios.post('/api/login', data)
.then(({data}) => {
auth.login(data.token, data.user);
this.$router.push('/dashboard');
})
.catch(({response}) => {
alert(response.data.message);
});
Alright, now we should update the user interface to indicate that a user has logged in. The least we can do is change the Login
link to a Logout
link and, maybe, show the user's name somewhere. Our auth
object serves as a global storage, but unfortunately Vue can't watch for changes on it. To update Vue on any changes we'll have to create a global event manager, which is going to be a different Vue instance. Let's do this inside the app.js
file.
Vue.use(VueRouter);
window.Event = new Vue;
Now we're going to add some stuff to the auth.js
class. First, we're going to add two properties to store data. Modify the constructor like this:
constructor () {
this.token = null;
this.user = null;
}
We're going to set values to these properties at the end of the login
method. After that we're going to emit a global event.
...
login (token, user) {
window.localStorage.setItem('token', token);
window.localStorage.setItem('user', JSON.stringify(user));
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
this.token = token;
this.user = user;
Event.$emit('userLoggedIn');
}
Finally, let's add a helper method to detect wether anyone is authenticated. We're going to simply check if the this.token
property is set and convert it into boolean.
check () {
return !! this.token;
}
By the way, you might think that the whole system looks unsafe - anyone can just run auth.login('token', {});
in their browser's console and the interface will change to that of an authenticated user and all the protected pages will be accessible. That is somewhat true, but remember that our server will return '401' after the first AJAX request with a fake token, so no real changes will take place on the back-end. As for the changing the UI back to the unathenticated state, will have to take care of this later.
Alright, Layout.vue
is the only file left to change. First, alter the <script>
part:
<script>
export default {
data() {
return {
authenticated: auth.check(),
user: auth.user
};
},
mounted() {
Event.$on('userLoggedIn', () => {
this.authenticated = true;
this.user = auth.user;
});
},
}
</script>
We're populating the data with the initial authentication state. Then in the mounted()
method we're subscribing to the global userLoggedIn
event and re-reading the data from the global auth
object when the event triggers. Ok, now put this template code in place of the old Login
link:
<div v-if="authenticated && user">
<p>Hello, {{ user.name }}</p>
<router-link to="/logout">Logout</router-link>
</div>
<router-link to="/login" v-else>Login</router-link>
If the authenticated
property equals to true
and the user
object is not null
or undefined
- show the block with the user's name and the Logout
link. Otherwise dont' show the name, show the Login
link.
We have more stuff to take care of. For now, why don't you implement the logout functionality as a homework? This should be as easy as undoing all the login()
steps. Just set all the data to empty strings or nulls and redirect user to the home page. See you in the next part!
All the materials at voerro are absolutely free and are worked on in the author's spare time. If you found any of the tutorials helpful, please consider supporting the project. Thank you!