{"id":17956177,"url":"https://github.com/sleipnir/2fa-service-exercise","last_synced_at":"2025-04-03T17:18:45.507Z","repository":{"id":138952389,"uuid":"152084037","full_name":"sleipnir/2fa-service-exercise","owner":"sleipnir","description":"Create and validate TOTP: Time-Based One-Time Password Algorithm tokens - RFC6238","archived":false,"fork":false,"pushed_at":"2018-11-06T00:17:17.000Z","size":273,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-02-09T05:44:53.225Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sleipnir.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-10-08T13:23:14.000Z","updated_at":"2025-01-10T13:12:44.000Z","dependencies_parsed_at":null,"dependency_job_id":"dff91c93-c4ec-4fac-8a90-49c3885b5dec","html_url":"https://github.com/sleipnir/2fa-service-exercise","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sleipnir%2F2fa-service-exercise","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sleipnir%2F2fa-service-exercise/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sleipnir%2F2fa-service-exercise/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sleipnir%2F2fa-service-exercise/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sleipnir","download_url":"https://codeload.github.com/sleipnir/2fa-service-exercise/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247043352,"owners_count":20874087,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-29T10:35:15.136Z","updated_at":"2025-04-03T17:18:45.488Z","avatar_url":"https://github.com/sleipnir.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 2fa-service-exercise\nCreate and validate TOTP: Time-Based One-Time Password Algorithm tokens - RFC6238\n\nThis application creates and sends a TOTP token to a phone number. Based on the consumption of an external API (https://notifye.io)\n\n### Index\n\n  - Architectural Decisions\n  - Dependencies\n  - Build Application\n  - Running in Windows, Linux, and Docker environments\n  - Use application\n\n### Architectural Decisions\n\nI have chosen not to implement the algorithm for generating tokens *TOTP* or *HOTP* directly. \nSince there are many libraries and services that offer this functionality to me. And since these algorithms are specified by the IETF via **RFCs 4226 and 6238** I decided to use an external API that implements these specifications \nand provides an abstraction layer that facilitates my work.\nSince providers like Google Authenticator and Twillio Authy implement these specifications I believe they are adhering to the requested requirement.\nIn this case I chose to write a Java / Springboot Webflux application that bridges the server application to the authentication client \nand the tokens generation mechanism. \nThis application implements the external API responsible for generating and sending the token via cell phone, as well as the validation of the generated token. \nThis external API is called Notify-e (of which I am a founding member and therefore no cost to use. :-)).\n\nThe basic flow of interaction can be described as follows:\n\n1. The end user requests the login in the application of our client.\n2. The frontend application sends a POST to our API.\n3. Our API sends a request to the external API informing the data of the end user (telephone), as well as the time of expiration of the token that we wish.\n4. The external API generates the Token and sends it to the end user.\n5 The end user receives the message containing the token code and informs on our client's website.\n6. The site in turn sends us a GET request informing the code informed.\n7. We consume the external API that tells us whether the code is valid or not.\n8. We return the response to the caller.\n\nSimplified Request Token Diagram\n![Request Diagram](request-diagram.jpeg?raw=true \"Request Diagram\")\n\nSimplified Validation Token Diagram\n![Request Diagram](validation-diagram.jpeg?raw=true \"Validation Diagram\")\n\n\nI have defined that the application should provide some minimal security mechanism. And for simplicity I chose to work with a level of authentication \nand basic authorization.\nFor this reason, it is necessary to send in all requests, except for the Hook feature, the Authorization header with the Basic type and the encrypted user \nand password as defined in *RFC2617*.\nThe user and password will be informed in the contact email.\n\nIn summary, the solution should contain the following characteristics:\n\n1. Be self-contained (able to execute without external dependencies);\n2. Can be executed bare-metal or via container;\n3. Be scalable and stateless;\n4. Provide an authentication / authorization mechanism;\n5. Respecting the RFC6238 specification;\n6. Run on HTTP or HTTPS without changes in source code;\n7. Use of the benefits of reactive programming;\n\n\n#### Dependencies\n\n  - Java 8\n  - Maven\n  - Docker (Optional)\n  - NPM (Only for install the next dependency)\n  - Local Tunnel (https://localtunnel.github.io/www/) (Optional, for webhooks only)\n\n#### Build Application\n\nFirst of all you should clone this repository:\n```sh\n# clone\n$ git clone https://github.com/sleipnir/2fa-service-exercise.git\n\n# chance to directory\n$ cd 2fa-service-exercise\\2fa-service\n\n# compile code\n$ mvn install\n\n# On Linux Build docker container image\n$ cd ../; docker build -t com.creativesource/2fa-service .\n\n# if all tests passed then running....\n```\n\n### Running in Windows, Linux, and Docker environments\n\nWindows:\n```sh\n# install localtunnel and running for catch the webhooks events\nC:\\Windows\\system32\u003e npm install -g localtunnel\n\nC:\\Windows\\system32\u003e lt --port 8080\nyour url is: https://lazy-otter-12.localtunnel.me\n\n# And in other terminal and if you wish to see the logs of webhooks then you should set the external access url in an environment variable\n# Windows CMD Prompt (not powershell !)\n\nC:\\Windows\\system32\u003e set HOOK_URL \"https://lazy-otter-12.localtunnel.me/auth/tokens/events\"\nPS C:\\Windows\\system32\u003e echo %HOOK_URL%\nhttps://lazy-otter-12.localtunnel.me/auth/tokens/events\n\n# Enter in application directory\nC:\\Users\\adriano.pereira\\pocs\\2fa-service-exercise\u003e cd 2fa-service\n\n# Running application\nC:\\Users\\adriano.pereira\\pocs\\2fa-service-exercise\\2fa-service\u003e java -Xms256m -Xmx256m -XX:+UseG1GC -XX:+UseStringDeduplication -jar target/2fa-service-0.0.1-SNAPSHOT.jar\n# Logs looks like this...\n\nTwo Factor Authentication Service\n\n2018-10-09 12:16:51.548  INFO 30140 --- [           main] c.c.twofactor.service.Application        : Starting Application v0.0.1-SNAPSHOT on adriano-prog with PID 30140 (C:\\Users\\adriano.pereira\\app\\development\\workspaces\\notifye\\pocs\\2fa-service-exercise\\2fa-service\\target\\2fa-service-0.0.1-SNAPSHOT.jar started by adriano.pereira in C:\\Users\\adriano.pereira\\app\\development\\workspaces\\notifye\\pocs\\2fa-service-exercise\\2fa-service)\n2018-10-09 12:16:51.587 DEBUG 30140 --- [           main] c.c.twofactor.service.Application        : Running with Spring Boot v2.0.5.RELEASE, Spring v5.0.9.RELEASE\n2018-10-09 12:16:51.591  INFO 30140 --- [           main] c.c.twofactor.service.Application        : No active profile set, falling back to default profiles: default\n2018-10-09 12:16:51.711  INFO 30140 --- [           main] onfigReactiveWebServerApplicationContext : Refreshing org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext@4c75cab9: startup date [Tue Oct 09 12:16:51 BRT 2018]; root of context hierarchy\n2018-10-09 12:16:53.778  WARN 30140 --- [           main] o.s.security.core.userdetails.User       : User.withDefaultPasswordEncoder() is considered unsafe for production and is only intended for sample applications.\n2018-10-09 12:16:55.050  INFO 30140 --- [           main] s.w.r.r.m.a.RequestMappingHandlerMapping : Mapped \"{[/auth/tokens],methods=[POST]}\" onto public reactor.core.publisher.Mono\u003corg.springframework.http.ResponseEntity\u003ccom.creativesource.twofactor.service.model.TokenResponse\u003e\u003e com.creativesource.twofactor.service.web.TokenController.createToken(com.creativesource.twofactor.service.model.TokenRequest)\n2018-10-09 12:16:55.054  INFO 30140 --- [           main] s.w.r.r.m.a.RequestMappingHandlerMapping : Mapped \"{[/auth/tokens],methods=[GET]}\" onto public reactor.core.publisher.Mono\u003corg.springframework.http.ResponseEntity\u003ccom.creativesource.twofactor.service.model.TokenStatus\u003e\u003e com.creativesource.twofactor.service.web.TokenController.validateToken(java.lang.String)\n2018-10-09 12:16:55.057  INFO 30140 --- [           main] s.w.r.r.m.a.RequestMappingHandlerMapping : Mapped \"{[/auth/tokens/events],methods=[POST]}\" onto public reactor.core.publisher.Mono\u003corg.springframework.http.ResponseEntity\u003cjava.lang.Void\u003e\u003e com.creativesource.twofactor.service.web.WebHookController.event(java.lang.String)\n2018-10-09 12:16:55.231  INFO 30140 --- [           main] o.s.w.r.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.reactive.resource.ResourceWebHandler]\n2018-10-09 12:16:55.232  INFO 30140 --- [           main] o.s.w.r.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.reactive.resource.ResourceWebHandler]\n2018-10-09 12:16:55.390  INFO 30140 --- [           main] o.s.w.r.r.m.a.ControllerMethodResolver   : Looking for @ControllerAdvice: org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext@4c75cab9: startup date [Tue Oct 09 12:16:51 BRT 2018]; root of context hierarchy\n2018-10-09 12:16:56.403  INFO 30140 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup\n2018-10-09 12:16:58.117  INFO 30140 --- [ctor-http-nio-1] r.ipc.netty.tcp.BlockingNettyContext     : Started HttpServer on /0:0:0:0:0:0:0:0:8080\n2018-10-09 12:16:58.118  INFO 30140 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080\n2018-10-09 12:16:58.126  INFO 30140 --- [           main] c.c.twofactor.service.Application        : Started Application in 7.613 seconds (JVM running for 9.259)\n2\n``` \n\nLinux:\n```sh\n# install localtunnel and running for catch the webhooks events\n$ npm install -g localtunnel\n\n$ lt --port 8080\nyour url is: https://lazy-otter-12.localtunnel.me\n\n# And in other terminal and if you wish to see the logs of webhooks then you should set the external access url in an environment variable\n\n$ export HOOK_URL=\"https://lazy-otter-12.localtunnel.me/auth/tokens/events\"\n$ echo $HOOK_URL\nhttps://lazy-otter-12.localtunnel.me/auth/tokens/events\n\n# Enter in application directory\n$ cd 2fa-service\n\n# Running application\n$ java -Xms256m -Xmx256m -XX:+UseG1GC -XX:+UseStringDeduplication -jar target/2fa-service-0.0.1-SNAPSHOT.jar\n# Logs looks like this...\n\nTwo Factor Authentication Service\n\n2018-10-09 12:16:51.548  INFO 30140 --- [           main] c.c.twofactor.service.Application        : Starting Application v0.0.1-SNAPSHOT on adriano-prog with PID 30140 (C:\\Users\\adriano.pereira\\app\\development\\workspaces\\notifye\\pocs\\2fa-service-exercise\\2fa-service\\target\\2fa-service-0.0.1-SNAPSHOT.jar started by adriano.pereira in C:\\Users\\adriano.pereira\\app\\development\\workspaces\\notifye\\pocs\\2fa-service-exercise\\2fa-service)\n2018-10-09 12:16:51.587 DEBUG 30140 --- [           main] c.c.twofactor.service.Application        : Running with Spring Boot v2.0.5.RELEASE, Spring v5.0.9.RELEASE\n2018-10-09 12:16:51.591  INFO 30140 --- [           main] c.c.twofactor.service.Application        : No active profile set, falling back to default profiles: default\n2018-10-09 12:16:51.711  INFO 30140 --- [           main] onfigReactiveWebServerApplicationContext : Refreshing org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext@4c75cab9: startup date [Tue Oct 09 12:16:51 BRT 2018]; root of context hierarchy\n2018-10-09 12:16:53.778  WARN 30140 --- [           main] o.s.security.core.userdetails.User       : User.withDefaultPasswordEncoder() is considered unsafe for production and is only intended for sample applications.\n2018-10-09 12:16:55.050  INFO 30140 --- [           main] s.w.r.r.m.a.RequestMappingHandlerMapping : Mapped \"{[/auth/tokens],methods=[POST]}\" onto public reactor.core.publisher.Mono\u003corg.springframework.http.ResponseEntity\u003ccom.creativesource.twofactor.service.model.TokenResponse\u003e\u003e com.creativesource.twofactor.service.web.TokenController.createToken(com.creativesource.twofactor.service.model.TokenRequest)\n2018-10-09 12:16:55.054  INFO 30140 --- [           main] s.w.r.r.m.a.RequestMappingHandlerMapping : Mapped \"{[/auth/tokens],methods=[GET]}\" onto public reactor.core.publisher.Mono\u003corg.springframework.http.ResponseEntity\u003ccom.creativesource.twofactor.service.model.TokenStatus\u003e\u003e com.creativesource.twofactor.service.web.TokenController.validateToken(java.lang.String)\n2018-10-09 12:16:55.057  INFO 30140 --- [           main] s.w.r.r.m.a.RequestMappingHandlerMapping : Mapped \"{[/auth/tokens/events],methods=[POST]}\" onto public reactor.core.publisher.Mono\u003corg.springframework.http.ResponseEntity\u003cjava.lang.Void\u003e\u003e com.creativesource.twofactor.service.web.WebHookController.event(java.lang.String)\n2018-10-09 12:16:55.231  INFO 30140 --- [           main] o.s.w.r.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.reactive.resource.ResourceWebHandler]\n2018-10-09 12:16:55.232  INFO 30140 --- [           main] o.s.w.r.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.reactive.resource.ResourceWebHandler]\n2018-10-09 12:16:55.390  INFO 30140 --- [           main] o.s.w.r.r.m.a.ControllerMethodResolver   : Looking for @ControllerAdvice: org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext@4c75cab9: startup date [Tue Oct 09 12:16:51 BRT 2018]; root of context hierarchy\n2018-10-09 12:16:56.403  INFO 30140 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup\n2018-10-09 12:16:58.117  INFO 30140 --- [ctor-http-nio-1] r.ipc.netty.tcp.BlockingNettyContext     : Started HttpServer on /0:0:0:0:0:0:0:0:8080\n2018-10-09 12:16:58.118  INFO 30140 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080\n2018-10-09 12:16:58.126  INFO 30140 --- [           main] c.c.twofactor.service.Application        : Started Application in 7.613 seconds (JVM running for 9.259)\n2\n``` \n\nDocker:\n```sh\n# On host install localtunnel and running for catch the webhooks events\n$ npm install -g localtunnel\n\n$ lt --port 8080\nyour url is: https://lazy-otter-12.localtunnel.me\n\n# And another prompt instance running application. Don´t forget to set HOOK_URL with the correct URL (Or not passed -e parameter!This resource is optional)\n\n$ docker run -d \\\n    -p 8080:8080 \\\n    --name 2fa-service \\\n    -e HOOK_URL=https://ancient-wasp-35.localtunnel.me/auth/tokens/events \\\n\t--memory=512m \\\n   com.creativesource/2fa-service:latest\n   \n```\n\nLogs looks like this...\n```sh\n$ docker logs -t -f 2fa-service\n\nTwo Factor Authentication Service\n\n2018-10-09 12:16:51.548  INFO 30140 --- [           main] c.c.twofactor.service.Application        : Starting Application v0.0.1-SNAPSHOT on adriano-prog with PID 30140 (C:\\Users\\adriano.pereira\\app\\development\\workspaces\\notifye\\pocs\\2fa-service-exercise\\2fa-service\\target\\2fa-service-0.0.1-SNAPSHOT.jar started by adriano.pereira in C:\\Users\\adriano.pereira\\app\\development\\workspaces\\notifye\\pocs\\2fa-service-exercise\\2fa-service)\n2018-10-09 12:16:51.587 DEBUG 30140 --- [           main] c.c.twofactor.service.Application        : Running with Spring Boot v2.0.5.RELEASE, Spring v5.0.9.RELEASE\n2018-10-09 12:16:51.591  INFO 30140 --- [           main] c.c.twofactor.service.Application        : No active profile set, falling back to default profiles: default\n2018-10-09 12:16:51.711  INFO 30140 --- [           main] onfigReactiveWebServerApplicationContext : Refreshing org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext@4c75cab9: startup date [Tue Oct 09 12:16:51 BRT 2018]; root of context hierarchy\n2018-10-09 12:16:53.778  WARN 30140 --- [           main] o.s.security.core.userdetails.User       : User.withDefaultPasswordEncoder() is considered unsafe for production and is only intended for sample applications.\n2018-10-09 12:16:55.050  INFO 30140 --- [           main] s.w.r.r.m.a.RequestMappingHandlerMapping : Mapped \"{[/auth/tokens],methods=[POST]}\" onto public reactor.core.publisher.Mono\u003corg.springframework.http.ResponseEntity\u003ccom.creativesource.twofactor.service.model.TokenResponse\u003e\u003e com.creativesource.twofactor.service.web.TokenController.createToken(com.creativesource.twofactor.service.model.TokenRequest)\n2018-10-09 12:16:55.054  INFO 30140 --- [           main] s.w.r.r.m.a.RequestMappingHandlerMapping : Mapped \"{[/auth/tokens],methods=[GET]}\" onto public reactor.core.publisher.Mono\u003corg.springframework.http.ResponseEntity\u003ccom.creativesource.twofactor.service.model.TokenStatus\u003e\u003e com.creativesource.twofactor.service.web.TokenController.validateToken(java.lang.String)\n2018-10-09 12:16:55.057  INFO 30140 --- [           main] s.w.r.r.m.a.RequestMappingHandlerMapping : Mapped \"{[/auth/tokens/events],methods=[POST]}\" onto public reactor.core.publisher.Mono\u003corg.springframework.http.ResponseEntity\u003cjava.lang.Void\u003e\u003e com.creativesource.twofactor.service.web.WebHookController.event(java.lang.String)\n2018-10-09 12:16:55.231  INFO 30140 --- [           main] o.s.w.r.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.reactive.resource.ResourceWebHandler]\n2018-10-09 12:16:55.232  INFO 30140 --- [           main] o.s.w.r.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.reactive.resource.ResourceWebHandler]\n2018-10-09 12:16:55.390  INFO 30140 --- [           main] o.s.w.r.r.m.a.ControllerMethodResolver   : Looking for @ControllerAdvice: org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext@4c75cab9: startup date [Tue Oct 09 12:16:51 BRT 2018]; root of context hierarchy\n2018-10-09 12:16:56.403  INFO 30140 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup\n2018-10-09 12:16:58.117  INFO 30140 --- [ctor-http-nio-1] r.ipc.netty.tcp.BlockingNettyContext     : Started HttpServer on /0:0:0:0:0:0:0:0:8080\n2018-10-09 12:16:58.118  INFO 30140 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080\n2018-10-09 12:16:58.126  INFO 30140 --- [           main] c.c.twofactor.service.Application        : Started Application in 7.613 seconds (JVM running for 9.259)\n2\n``` \n\n### Use application\n\nYou can test the application in two ways, via Postman or via Curl directly from the command line.\nImport the 2fa-service-api.json file, which is located in the project root, into your Postman application if you wish to run via Postman Collections, or follow the examples below to test directly via CURL\n\n*Note: Before your try to run the requests in Postman application, please disable ssl verification!\n\nSend Request for create TOTP Token and send This Token to the client via phone number:\n\n```sh\n$ curl -X POST \\\n  http://localhost:8080/auth/tokens \\\n  -H 'Authorization: Basic c2Vuc2VkaWE6c2Vuc2VkaWEqMTIz' \\\n  -H 'Cache-Control: no-cache' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n\t\"countryCode\" : \"+55\",\n\t\"areaCode\" : \"11\",\n\t\"phoneNumber\" : \"959734939\",\n\t\"tokenTTL\" : 300\n}' \n```\n**In this request we define that a valid Token must be created, set to expire in 5 minutes and sent to the client whose phone number we enter in Json's phoneNumber attribute.\nDo not forget to enter a valid phone number that you have access to to receive the token.**\n\nSend Request for validate the received token:\n```sh\n$ curl -X GET \\\n  'http://localhost:8080/auth/tokens?code=274008' \\\n  -H 'Authorization: Basic c2Vuc2VkaWE6c2Vuc2VkaWEqMTIz' \\\n  -H 'Cache-Control: no-cache'\n```\n**Do not forget to replace the query string 'code' with the value of the token received in your phone.**\n\n### TODO\n\n  - Implement circuit-breaker for cascade failure treatment.\n  - Implement persistence and caching to avoid unnecessary network calls.\n  - Include new validations.\n  - Extract sensitive data such as passwords and tokens from property files and store them on a secure medium. Vault as an example.\n\n### Note\n\nIf the external API Token expires please request a new one via email.\n  \n# END\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsleipnir%2F2fa-service-exercise","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsleipnir%2F2fa-service-exercise","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsleipnir%2F2fa-service-exercise/lists"}