|
| 1 | +Two-Factor Authentication in an API |
| 2 | +=================================== |
| 3 | + |
| 4 | +This guide describes how to set-up two-factor authentication in a Symfony application that doesn't generate a frontend, |
| 5 | +but provides API endpoints instead. |
| 6 | + |
| 7 | +## Prerequisites |
| 8 | + |
| 9 | +To make two-factor authentication work in an API, your **API has to be stateful**. That means the API is starting a |
| 10 | +session which is passed by the client on every call. The session is necessary for two-factor authentication to store the |
| 11 | +state of the login - if the user has already completed two-factor authentication or not. |
| 12 | + |
| 13 | +## Setup |
| 14 | + |
| 15 | +ℹ️ For simplicity of this guide, it is assumed that you're building a JSON API and you're using the `json_login` |
| 16 | +authentication mechanism, which comes with Symfony. For any other authentication mechanism it should work the same or at |
| 17 | +least similar, as long as it lets you configure a custom success handler. |
| 18 | + |
| 19 | +You need to implement 4 classes: |
| 20 | + |
| 21 | +1) A custom success handler for the authentication mechanism |
| 22 | +2) A custom "two-factor authentication required" handler for the two-factor authentication |
| 23 | +3) A custom success handler for the two-factor authentication |
| 24 | +4) A custom failure handler for the two-factor authentication |
| 25 | + |
| 26 | +### 1) Response on login |
| 27 | + |
| 28 | +This first response is returned after the user logged in. Without two-factor authentication, it would either return |
| 29 | +a "login success" or "login failure" response. With two-factor authentication, you eventually need to return a third |
| 30 | +type of response to tell the client that authentication hasn't completed yet and two-factor authentication is required. |
| 31 | +The client should show the two-factor authentication form then. |
| 32 | + |
| 33 | +If you provide multiple authentication mechanisms for the user to identify themselves, you have to do this for each one |
| 34 | +of them. |
| 35 | + |
| 36 | +To implement such a response you need to a custom success handler: |
| 37 | + |
| 38 | +```php |
| 39 | +<?php |
| 40 | + |
| 41 | +namespace App\Security; |
| 42 | + |
| 43 | +use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenInterface; |
| 44 | +use Symfony\Component\HttpFoundation\Request; |
| 45 | +use Symfony\Component\HttpFoundation\Response; |
| 46 | +use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; |
| 47 | +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; |
| 48 | + |
| 49 | +class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface |
| 50 | +{ |
| 51 | + public function onAuthenticationSuccess(Request $request, TokenInterface $token): Response |
| 52 | + { |
| 53 | + if ($token instanceof TwoFactorTokenInterface) { |
| 54 | + // Return the response to tell the client two-factor authentication is required. |
| 55 | + return new Response('{"login": "success": "2fa_complete": false}'); |
| 56 | + } |
| 57 | + |
| 58 | + // Otherwise return the default response for successful login. You could do this by decorating |
| 59 | + // the original authentication success handler and calling it here. |
| 60 | + } |
| 61 | +} |
| 62 | +``` |
| 63 | + |
| 64 | +Register it as a service and configure it as a custom `success_handler` for the authentication method: |
| 65 | + |
| 66 | +```yaml |
| 67 | +# app/config/security.yml |
| 68 | +security: |
| 69 | + firewalls: |
| 70 | + your_firewall_name: |
| 71 | + json_login: # The authentication mechanism you're using |
| 72 | + success_handler: your_api_success_handler |
| 73 | +``` |
| 74 | +
|
| 75 | +### 2) Response to require two-factor authentication |
| 76 | +
|
| 77 | +Configure a response that is returned when the user requests a path, but it is not accessible (yet), because the user |
| 78 | +has to complete two-factor authentication first. This could be the same as your "access denied" response. |
| 79 | +
|
| 80 | +Create a class which implements `Scheb\TwoFactorBundle\Security\Http\Authentication\AuthenticationRequiredHandlerInterface` |
| 81 | +to return the response. |
| 82 | + |
| 83 | +```php |
| 84 | +<?php |
| 85 | +
|
| 86 | +namespace App\Security; |
| 87 | +
|
| 88 | +use Scheb\TwoFactorBundle\Security\Http\Authentication\AuthenticationRequiredHandlerInterface; |
| 89 | +use Symfony\Component\HttpFoundation\Request; |
| 90 | +use Symfony\Component\HttpFoundation\Response; |
| 91 | +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; |
| 92 | +
|
| 93 | +class TwoFactorAuthenticationRequiredHandler implements AuthenticationRequiredHandlerInterface |
| 94 | +{ |
| 95 | + public function onAuthenticationRequired(Request $request, TokenInterface $token): Response |
| 96 | + { |
| 97 | + // Return the response to tell the client that authentication hasn't completed yet and |
| 98 | + // two-factor authentication is required. |
| 99 | + return new Response('{"error": "access_denied", "2fa_complete": false}'); |
| 100 | + } |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +Register it as a service and configure it as the `required_handler` of the `two_factor` authentication method: |
| 105 | + |
| 106 | +```yaml |
| 107 | +# app/config/security.yml |
| 108 | +security: |
| 109 | + firewalls: |
| 110 | + your_firewall_name: |
| 111 | + two_factor: |
| 112 | + required_handler: your_api_2fa_required_handler |
| 113 | +``` |
| 114 | + |
| 115 | +### 3) Response when two-factor authentication was successful |
| 116 | + |
| 117 | +Configure a response that is returned when two-factor authentication was completed successfully and the user is now |
| 118 | +fully authentication. Implement another success handler for it: |
| 119 | + |
| 120 | +```php |
| 121 | +<?php |
| 122 | +
|
| 123 | +namespace App\Security; |
| 124 | +
|
| 125 | +use Symfony\Component\HttpFoundation\Request; |
| 126 | +use Symfony\Component\HttpFoundation\Response; |
| 127 | +use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; |
| 128 | +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; |
| 129 | +
|
| 130 | +class TwoFactorAuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface |
| 131 | +{ |
| 132 | + public function onAuthenticationSuccess(Request $request, TokenInterface $token): Response |
| 133 | + { |
| 134 | + // Return the response to tell the client that authentication including two-factor |
| 135 | + // authentication is complete now. |
| 136 | + return new Response('{"login": "success", "2fa_complete": true}'); |
| 137 | + } |
| 138 | +} |
| 139 | +``` |
| 140 | + |
| 141 | +Register it as a service and configure it as the `success_handler` of the `two_factor` authentication method: |
| 142 | + |
| 143 | +```yaml |
| 144 | +# app/config/security.yml |
| 145 | +security: |
| 146 | + firewalls: |
| 147 | + your_firewall_name: |
| 148 | + two_factor: |
| 149 | + success_handler: your_api_2fa_success_handler |
| 150 | +``` |
| 151 | + |
| 152 | +### 4) Response when two-factor authentication failed |
| 153 | + |
| 154 | +Configure a response that is returned when two-factor authentication was tried, but authentication failed for some |
| 155 | +reason. Implement a failure handler for it: |
| 156 | + |
| 157 | +```php |
| 158 | +<?php |
| 159 | +
|
| 160 | +namespace App\Security; |
| 161 | +
|
| 162 | +use Symfony\Component\HttpFoundation\Request; |
| 163 | +use Symfony\Component\HttpFoundation\Response; |
| 164 | +use Symfony\Component\Security\Core\Exception\AuthenticationException; |
| 165 | +use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; |
| 166 | +
|
| 167 | +class TwoFactorAuthenticationSuccessHandler implements AuthenticationFailureHandlerInterface |
| 168 | +{ |
| 169 | + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response |
| 170 | + { |
| 171 | + // Return the response to tell the client that 2fa failed. You may want to add more details |
| 172 | + // from the $exception. |
| 173 | + return new Response('{"error": "2fa_failed", "2fa_complete": false}'); |
| 174 | + } |
| 175 | +} |
| 176 | +``` |
| 177 | + |
| 178 | +Register it as a service and configure it as the `failure_handler` of the `two_factor` authentication method: |
| 179 | + |
| 180 | +```yaml |
| 181 | +# app/config/security.yml |
| 182 | +security: |
| 183 | + firewalls: |
| 184 | + your_firewall_name: |
| 185 | + two_factor: |
| 186 | + failure_handler: your_api_2fa_failure_handler |
| 187 | +``` |
0 commit comments