Handling Authentication Errors
When we log in with an invalid email and password, it looks like the json_login
system sends back some nice JSON with an error
key set to "Invalid credentials". If we wanted to customize this, we could create a class that implements AuthenticationFailureHandlerInterface
:
class AppAuthFailureHandler implements AuthenticationFailureHandlerInterface
{
public function onAuthenticationFailure($request, $exception)
{
return new JsonResponse(
['something' => 'went wrong'],
401
);
}
}
And then set its service ID onto the failure_handler
option under json_login
:
json_login:
failure_handler: App\Security\AppAuthFailureHandler
Showing the Error on the Form
But, this is plenty good for us. So let's use it over in our /assets/vue/LoginForm.vue
. We won't go too deeply into Vue, but I already have state called error
, and if we set that, it will show up on the form:
// ... lines 1 - 48 | |
<script setup> | |
// ... lines 50 - 54 | |
const error = ref(''); | |
// ... lines 56 - 65 | |
const handleSubmit = async () => { | |
// ... line 67 | |
error.value = ''; | |
// ... lines 69 - 82 | |
if (!response.ok) { | |
const data = await response.json(); | |
console.log(data); | |
// TODO: set error | |
return; | |
} | |
// ... lines 90 - 93 | |
} | |
</script> |
After making the request, if the response is not okay, we're already decoding the JSON. Now let's say error.value = data.error
:
// ... lines 1 - 48 | |
<script setup> | |
// ... lines 50 - 65 | |
const handleSubmit = async () => { | |
// ... lines 67 - 82 | |
if (!response.ok) { | |
const data = await response.json(); | |
error.value = data.error; | |
return; | |
} | |
// ... lines 89 - 92 | |
} | |
</script> |
To see if this works, make sure you have Webpack Encore running in the background so it recompiles our JavaScript. Refresh. And... you can click this little link to cheat and enter a valid email. But then type in a ridiculous password and... I love it! We see "Invalid credentials" on top with some red boxes!
json_login Requires Content-Type: application/json
So the AJAX call is working great. Though, there is one gotcha with the json_login
security mechanism: it requires you to send a Content-Type
header set to application/json
. We are setting this on our Ajax call and you should to:
// ... lines 1 - 48 | |
<script setup> | |
// ... lines 50 - 65 | |
const handleSubmit = async () => { | |
// ... lines 67 - 69 | |
const response = await fetch('/login', { | |
// ... line 71 | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
// ... lines 75 - 78 | |
}); | |
// ... lines 80 - 92 | |
} | |
</script> |
But... if someone forgets, we want to make sure that things don't go completely crazy.
Comment out that Content-Type
header so we can see what happens:
// ... lines 1 - 48 | |
<script setup> | |
// ... lines 50 - 65 | |
const handleSubmit = async () => { | |
// ... lines 67 - 69 | |
const response = await fetch('/login', { | |
// ... line 71 | |
headers: { | |
//'Content-Type': 'application/json' | |
}, | |
// ... lines 75 - 78 | |
}); | |
// ... lines 80 - 92 | |
} | |
</script> |
Then move over, refresh the page... type a ridiculous password and... it clears the form? Look down at the Network call. The endpoint returned a 200 status code with a user
key set to null
!
And... that makes sense! Because we're missing the header, the json_login
mechanism did nothing. Instead, the request continued to our SecurityController
... except that this time the user is not logged in. So, we return user: null
... with a 200 status code.
That's a problem because it make it look like the Ajax call was successful. To fix this, if, for any reason the json_login
mechanism was skipped... but the user is hitting our login endpoint, let's return a 401 status code that says:
Hey! You need to log in!
So, if not $user
, then return $this->json()
... and this could look like anything. Let's include an error
key explaining what probably went wrong: this matches the error
key that json_login
returns when the credentials fail, so our JavaScript will like this. Heck. I'll even fix my typo!
// ... lines 1 - 9 | |
class SecurityController extends AbstractController | |
{ | |
// ... line 12 | |
public function login(#[CurrentUser] $user = null): Response | |
{ | |
if (!$user) { | |
return $this->json([ | |
'error' => 'Invalid login request: check that the Content-Type header is "application/json".', | |
], 401); | |
} | |
// ... lines 20 - 23 | |
} | |
} |
Most importantly, for the second argument, pass a 401 for the status code.
Below, we can simplify... because now we know that there will be a user:
// ... lines 1 - 9 | |
class SecurityController extends AbstractController | |
{ | |
// ... line 12 | |
public function login(#[CurrentUser] $user = null): Response | |
{ | |
if (!$user) { | |
return $this->json([ | |
'error' => 'Invalid login request: check that the Content-Type header is "application/json".', | |
], 401); | |
} | |
return $this->json([ | |
'user' => $user->getId(), | |
]); | |
} | |
} |
Beautiful! Spin over and submit another bad password. Oh, gorgeous! The 401 status code triggers our error handling code, which displays the error on top. So awesome.
Go back to LoginForm.vue
and put the Content-Type
header back:
// ... lines 1 - 48 | |
<script setup> | |
// ... lines 50 - 65 | |
const handleSubmit = async () => { | |
// ... lines 67 - 69 | |
const response = await fetch('/login', { | |
// ... line 71 | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
// ... lines 75 - 78 | |
}); | |
// ... lines 80 - 92 | |
} | |
</script> |
Next: let's login successfully and... figure out what we want to do when that happens! We're also going to talk about the session and how that authenticates our API requests.
i think you put the coding-challenge before this chapter by mistake.
btw. Ryan ... my cats missing the cat-lang of the courses. they still stuck on Api-Platform 2.6.