|
| 1 | +## Contents |
| 2 | +- [Intro](#intro) |
| 3 | +- [Flowchart](#flowchart) |
| 4 | +- [Models](#models) |
| 5 | + - [User](#user) |
| 6 | + - [Device](#device) |
| 7 | + - [Backup Tokens](#backup-tokens) |
| 8 | +- [Endpoints](#endpoints) |
| 9 | + - [Auth](#auth) |
| 10 | + - [Two Factor Authentication](#two-factor-authentication) |
| 11 | + - [Users](#users) |
| 12 | + - [Tasks](#tasks) |
| 13 | +- [Crud](#crud) |
| 14 | +- [Core](#core) |
| 15 | +## Run |
| 16 | +- [Run from docker-compose](#run-from-docker-compose) |
| 17 | + |
| 18 | +## Todo |
| 19 | +- [Todos](#todos) |
| 20 | + |
| 21 | + |
| 22 | +__________ |
| 23 | +__________ |
| 24 | +### **[Intro](#intro)** |
| 25 | +The application is an authentication microservice, developed with FastAPI framework, for login access via JWT token and possibility to enable two factor authentication (TFA). |
| 26 | + |
| 27 | +**Two Factor Authentication** |
| 28 | + |
| 29 | +TFA can be enabled choosing by two kind of devices: |
| 30 | +* email (OTP token will be send via email) |
| 31 | +* code generator (on activation a qr code is send to the user that, since than, will have the OTP token in any authenticator app like Microsoft or Google Authenticator apps) |
| 32 | + |
| 33 | +It is only available the TOTP version of OTP validation protocol, based on timestamp and a private key for each user. |
| 34 | + |
| 35 | +**Backup Tokens** |
| 36 | + |
| 37 | +On TFA activation, independently from the chosen device, an email will be sent to the user with 5 backup tokens that can be used in case qr code was lost or it's somehow not possible to receive the token, these backup tokens are consumed every time one is used. |
| 38 | + |
| 39 | +**Email service** |
| 40 | + |
| 41 | +Email sending is faked by printing on stout the sent email text. It is anyway handled by a Celery consumer listening on a RabbitMQ message broker. |
| 42 | + |
| 43 | +**Services** |
| 44 | + |
| 45 | +The app run inside Docker, orchestrated by docker-compose. Database is Postgres, RabbitMq is used as message broker for Celery tasks and Redis is used as a backend for saving tasks results. It is also available a Flower instance to check task status via web dashboard. |
| 46 | + |
| 47 | +| service | container name | listening port | |
| 48 | +|---|---|---| |
| 49 | +| **FastAPI** | fastapi_2fa | 5555 | |
| 50 | +| **PostgresDB** | fastapi_2fa-db | 5454 | |
| 51 | +| **Redis** | fastapi_2fa-cache | 6389 | |
| 52 | +| **RabbitMQ** | fastapi_2fa-rabbitmq | 15672 | |
| 53 | +| **Celery worker** | fastapi_2fa-celery | | |
| 54 | +| **Flower** | fastapi_2fa-flower | 5557 | |
| 55 | + |
| 56 | + |
| 57 | + |
| 58 | + |
| 59 | +### **[Flowchart](#flowchart)** |
| 60 | + |
| 61 | +<img src="https://raw.githubusercontent.com/dfm88/fastapi-two-factor-authentication/main/docs/two_factor_flowchart.drawio.png" alt="image" style="width:600px;"/> |
| 62 | + |
| 63 | + |
| 64 | +### **[Models](#models)** |
| 65 | + |
| 66 | +User model has a One to One relationship with Device model. Device model has a One to Many relationship with BackupTokens model. |
| 67 | + |
| 68 | +#### [User](#user) |
| 69 | + |
| 70 | +* `full_name` - user name |
| 71 | +* `email` - used for login |
| 72 | +* `hashed_password` - user hashed password |
| 73 | +* `tfa_enabled` - boolean field to determine if user has TFA enabled |
| 74 | + |
| 75 | +#### [Device](#device) |
| 76 | + |
| 77 | +* `user_id` - associated user |
| 78 | +* `key` - here is saved the encrypted version of the user OTP key |
| 79 | +* `device_type` - string value to determine if TFA device is of type `email` or `code_generator` |
| 80 | + |
| 81 | +#### [Backup Tokens](#backup-tokens) |
| 82 | + |
| 83 | +* `device_id` - associated device |
| 84 | +* `token` - random TOTP backup token |
| 85 | + |
| 86 | +### **[Endpoints](#endpoints)** |
| 87 | + |
| 88 | +Endpoints can be easily tested via FastAPI Swagger on route `/docs`. here are proposed the `curl` version to easily set JWT tokens |
| 89 | + |
| 90 | +#### [Auth](#auth) |
| 91 | + |
| 92 | +* `/api/v1/auth/signup` |
| 93 | + |
| 94 | + **POST** - Creates a new user. |
| 95 | + |
| 96 | + `device` key is required only if `tfa_enabled` is `true`; |
| 97 | + |
| 98 | + `device_type` is one between `email` or `code_generator` |
| 99 | + |
| 100 | + **body** |
| 101 | + |
| 102 | + ```json |
| 103 | + { |
| 104 | + |
| 105 | + "tfa_enabled": true, |
| 106 | + "full_name": "string", |
| 107 | + "password": "123456", |
| 108 | + "device": { |
| 109 | + "device_type": "email" |
| 110 | + } |
| 111 | + } |
| 112 | + ``` |
| 113 | + |
| 114 | + **curl** (the `--output` option will save on your filesystem a valid qr-code image only if device type is `code_generator`) |
| 115 | + |
| 116 | + ```shell |
| 117 | + curl -X 'POST' \ |
| 118 | + 'http://localhost:5555/api/v1/auth/signup' \ |
| 119 | + -H 'accept: application/json' \ |
| 120 | + -H 'Content-Type: application/json' \ |
| 121 | + -d '{ |
| 122 | + |
| 123 | + "tfa_enabled": true, |
| 124 | + "full_name": "string", |
| 125 | + "password": "123456", |
| 126 | + "device": { |
| 127 | + "device_type": "email" |
| 128 | + } |
| 129 | + }' --output my_qrcode.png |
| 130 | + ``` |
| 131 | + |
| 132 | +* `/api/v1/auth/login` |
| 133 | + |
| 134 | + **POST** - Authenticates new user |
| 135 | + |
| 136 | + **form data** |
| 137 | + |
| 138 | + ``` |
| 139 | + Email |
| 140 | + Password |
| 141 | + ``` |
| 142 | + |
| 143 | + **curl** |
| 144 | + |
| 145 | + ```shell |
| 146 | + curl -X 'POST' \ |
| 147 | + 'http://localhost:5555/api/v1/auth/login' \ |
| 148 | + -H 'accept: application/json' \ |
| 149 | + -H 'Content-Type: application/x-www-form-urlencoded' \ |
| 150 | + -d 'grant_type=&username={{ USERNAME }}&password={{ PASSWORD }}&scope=&client_id=&client_secret=' |
| 151 | + ``` |
| 152 | + |
| 153 | +* `/api/v1/auth/test-token` |
| 154 | + |
| 155 | + **GET** - Test authenticated endpoint to check if user is authenticated, JWT token must be set in `Authorization` header |
| 156 | + |
| 157 | + **curl** |
| 158 | + |
| 159 | + ```shell |
| 160 | + curl -X 'GET' \ |
| 161 | + 'http://localhost:5555/api/v1/auth/test-token' \ |
| 162 | + -H 'accept: application/json' \ |
| 163 | + -H 'Authorization: Bearer {{ MY_ACCESS_JWT_TOKEN }}' |
| 164 | + ``` |
| 165 | + |
| 166 | +* `/api/v1/auth/refresh` |
| 167 | + |
| 168 | + **POST** - given a `REFRESH_JWT_TOKEN` returns a new `ACCESS_JWT_TOKEN` |
| 169 | + |
| 170 | + **body** |
| 171 | + |
| 172 | + ```json |
| 173 | + { |
| 174 | + "refresh_token": "{{ MY_JWT_REFRESH_TOKEN }}" |
| 175 | + } |
| 176 | + ``` |
| 177 | + |
| 178 | + **curl** |
| 179 | + |
| 180 | + ```shell |
| 181 | + curl -X 'POST' \ |
| 182 | + 'http://localhost:5555/api/v1/auth/refresh' \ |
| 183 | + -H 'accept: application/json' \ |
| 184 | + -H 'Content-Type: application/json' \ |
| 185 | + -d '{ |
| 186 | + "refresh_token": "{{ MY_JWT_REFRESH_TOKEN }}" |
| 187 | + }' |
| 188 | + ``` |
| 189 | + |
| 190 | + |
| 191 | + |
| 192 | +#### [Two Factor Authentication](#two-factor-authentication) |
| 193 | + |
| 194 | +* `/api/v1/tfa/login_tfa?tfa_token=` |
| 195 | + |
| 196 | + **POST** - it's the second step after login for users with TFA enabled, it is necessary to have the TOTP token and to have the temporary access token in `Authorization` header returned by `login` endpoint for users with TFA enabled |
| 197 | + |
| 198 | + **curl** |
| 199 | + |
| 200 | + ```shell |
| 201 | + curl -X 'POST' \ |
| 202 | + 'http://localhost:5555/api/v1/tfa/login_tfa?tfa_token={{ MY_TOTP_TOKEN }}' \ |
| 203 | + -H 'accept: application/json' \ |
| 204 | + -H 'Authorization: Bearer {{ MY_PRE_TFA_JWT_ACCESS_TOKEN }}' \ |
| 205 | + -d '' |
| 206 | + ``` |
| 207 | + |
| 208 | +* `/api/v1/tfa/recover_tfa?tfa_backup_token=` |
| 209 | + |
| 210 | + **POST** - it's the second step after login for users with TFA enabled that can't receive/recover their TOTP token, so they can use one of the backup tokens sent in signup step. It is necessary to have the temporary access token in `Authorization` header returned by `login` endpoint for users with TFA enabled |
| 211 | + |
| 212 | + **curl** |
| 213 | + |
| 214 | + ```shell |
| 215 | + curl -X 'POST' \ |
| 216 | + 'http://localhost:5555/api/v1/tfa/recover_tfa?tfa_backup_token={{ MY_BACKUP_TOTP_TOKEN }}' \ |
| 217 | + -H 'accept: application/json' \ |
| 218 | + -H 'Authorization: Bearer {{ MY_PRE_TFA_JWT_ACCESS_TOKEN }}' \ |
| 219 | + -d '' |
| 220 | + ``` |
| 221 | + |
| 222 | +* `/api/v1/tfa/get_my_qrcode` |
| 223 | + |
| 224 | + **GET** - authenticated endpoint to receive again its own qr code (only for user with `code_generator` device) |
| 225 | + |
| 226 | + **curl** |
| 227 | + |
| 228 | + ```shell |
| 229 | + curl -X 'GET' \ |
| 230 | + 'http://localhost:5555/api/v1/tfa/get_my_qrcode' \ |
| 231 | + -H 'accept: application/json' \ |
| 232 | + -H 'Authorization: Bearer {{ MY_JWT_ACCESS_TOKEN }}' --output my_recovered_qr_code.png |
| 233 | + ``` |
| 234 | + |
| 235 | +* `/api/v1/tfa/enable_tfa` |
| 236 | + |
| 237 | + **PUT** - enables TFA for authenticated users that didn't enable it on signup step. `device_type` is one between `email` or `code_generator` |
| 238 | + |
| 239 | + **body** |
| 240 | + |
| 241 | + ```json |
| 242 | + { |
| 243 | + "device_type": "code_generator" |
| 244 | + } |
| 245 | + ``` |
| 246 | + |
| 247 | + **curl** (the `--output` option will save on your filesystem a valid qr-code image only if device type is `code_generator`) |
| 248 | + |
| 249 | + ```shell |
| 250 | + curl -X 'PUT' \ |
| 251 | + 'http://localhost:5555/api/v1/tfa/enable_tfa' \ |
| 252 | + -H 'accept: application/json' \ |
| 253 | + -H 'Authorization: Bearer {{ MY_JWT_ACCESS_TOKEN }}' \ |
| 254 | + -H 'Content-Type: application/json' \ |
| 255 | + -d '{ |
| 256 | + "device_type": "code_generator" |
| 257 | + }' --output my_new_qr_code.png |
| 258 | + ``` |
| 259 | + |
| 260 | + |
| 261 | +#### [Users](#users) |
| 262 | + |
| 263 | +* `/api/v1/users/users` |
| 264 | + |
| 265 | + **GET** - test non-authenticated endpoint to get all users in DB |
| 266 | + |
| 267 | + **curl** |
| 268 | + |
| 269 | + ```shell |
| 270 | + curl -X 'GET' \ |
| 271 | + 'http://localhost:5555/api/v1/users/users' \ |
| 272 | + -H 'accept: application/json' |
| 273 | + ``` |
| 274 | + |
| 275 | +#### [Tasks](#tasks) |
| 276 | + |
| 277 | +* `/api/v1/tasks/test-celery` |
| 278 | + |
| 279 | + **GET** - endpoint to test celery send mail function |
| 280 | + |
| 281 | + **curl** |
| 282 | + |
| 283 | + ```shell |
| 284 | + curl -X 'GET' \ |
| 285 | + 'http://localhost:5555/api/v1/tasks/test-celery' \ |
| 286 | + -H 'accept: application/json' |
| 287 | + ``` |
| 288 | + |
| 289 | +* `/api/v1/tasks/taskstatus?task_id=` |
| 290 | + |
| 291 | + **GET** - endpoint to retrieve task status. `?task_id` is returned by ``/api/v1/tasks/test-celery` endpoint |
| 292 | + |
| 293 | + **curl** |
| 294 | + |
| 295 | + ```shell |
| 296 | + curl -X 'GET' \ |
| 297 | + 'http://localhost:5555/api/v1/tasks/taskstatus?task_id={{ TASK_ID }}' \ |
| 298 | + -H 'accept: application/json' |
| 299 | + ``` |
| 300 | + |
| 301 | +### **[Crud](#crud)** |
| 302 | +The modules inside this package are responsible to query the DB |
| 303 | + |
| 304 | +### **[Core](#core)** |
| 305 | +The modules inside this package are responsible to handle the core business logic of the application |
| 306 | + |
| 307 | +__________ |
| 308 | +__________ |
| 309 | + |
| 310 | +## [Run from docker-compose](#run-from-docker-compose) |
| 311 | + |
| 312 | +To run all the services, from the application root run: |
| 313 | + |
| 314 | +```shell |
| 315 | +docker-compose -f docker/docker-compose.yaml up |
| 316 | +``` |
| 317 | + |
| 318 | +The FastAPI server is exposed on port `5555`, FastAPI swagger is available at http://localhost:5555/docs#/ |
| 319 | + |
| 320 | +--- |
| 321 | + |
| 322 | +To follow only FastAPI logs, from another terminal, run: |
| 323 | +```shell |
| 324 | +docker logs --tail 200 -f fastapi_2fa |
| 325 | +``` |
| 326 | + |
| 327 | +--- |
| 328 | + |
| 329 | +To run tests, from another terminal, run: |
| 330 | +```shell |
| 331 | +docker exec fastapi_2fa pytest --cov-report term --cov=fastapi_2fa tests/ |
| 332 | +``` |
| 333 | + |
| 334 | +__________ |
| 335 | +__________ |
| 336 | + |
| 337 | +## [Todos](#todos) |
| 338 | + |
| 339 | +* reset / recover password |
| 340 | +* encrypt backup tokens |
| 341 | +* throttling for failed login |
| 342 | +* complete tests |
| 343 | +* logging |
0 commit comments