diff --git a/CHANGELOG.md b/CHANGELOG.md
index 176de57..78bb284 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,14 +1,20 @@
# Users change Log
+## 0.0.6
+
+- LoginResponseDTO as a standard response object from all endpoints
+-
+
## 0.0.5
- Port to Kotlin
+- webauthn (experimental)
## 0.0.4
- - Clean Architecture
- - Updated to Quarkus 3
- - Updated to Java 20 Temurin
+- Clean Architecture
+- Updated to Quarkus 3
+- Updated to Java 20 Temurin
## 0.0.3
diff --git a/docs/usecases/Autenticate/Authenticate.md b/docs/usecases/Autenticate/Authenticate.md
deleted file mode 100644
index ba6ad36..0000000
--- a/docs/usecases/Autenticate/Authenticate.md
+++ /dev/null
@@ -1,93 +0,0 @@
----
-layout: default
-title: Authenticate
-parent: Use Cases
-nav_order: 1
----
-
-## Authenticate
-
-This use case is responsible for authenticate a user in the system.
-
-### Normal flow
-
-* A client sends a e-mail and password.
-* The service validates the input data and verifies if the users exists in the
- system. If the users exists, the service returns a JSON with the user data
- and a signed JWT.
-
-## HTTPS endpoints
-
-* /users/login
- * Method: POST
- * Consumes: application/x-www-form-urlencoded
- * Produces: application/json
-
-* Request:
-
-```shell
-curl -X POST \
- 'http://localhost:8080/users/login' \
- --header 'Accept: */*' \
- --header 'User-Agent: Thunder Client (https://www.thunderclient.com)' \
- --header 'Content-Type: application/x-www-form-urlencoded' \
- --data-urlencode 'email=orion@services.dev' \
- --data-urlencode 'password=12345678'
-```
-
-* Response:
-
-```json
-{
-"user": {
- "hash": "53012a1a-b8ec-40f4-a81e-bc8b97ddab75",
- "name": "Orion",
- "email": "orion@services.dev",
- "emailValid": false,
- "secret2FA": null,
- "using2FA": false
-},
-"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJvcmlvbi11c2VycyIsInVwbiI6Im9yaW9uQHNlcnZpY2VzLmRldiIsImdyb3VwcyI6WyJ1c2VyIl0sImNfaGFzaCI6IjUzMDEyYTFhLWI4ZWMtNDBmNC1hODFlLWJjOGI5N2RkYWI3NSIsImVtYWlsIjoib3Jpb25Ac2VydmljZXMuZGV2IiwiaWF0IjoxNzE1Mzk0NzA0LCJleHAiOjE3MTUzOTUwMDQsImp0aSI6ImMzYjZkZmFkLTAyMDAtNDc3YS05MDJmLTU0ZDg5YjdiMTUzYyJ9.I93SpcxIm31wfMQeiFLuUuuWuwlG-C0aGascSEDseRueILn9Tf5shEyNDMLQr6QRNhQbNjRjnCwe_quenVfjBEF_BLgtDDq7maoqpzDdrnDoKxtxex0dIXmRg2ABZoktB-jBo8yJcflandp1FUe7hG1VduE2E8D6WqvUQiNrhhCiiEZ4d5Moc1H11S3YGg3X1U-QnWUGx70FYQG4Qo-1Ini7T6miC0xCxSJRxumXKKtBRLYMDizp5qPIVoVIatJUu4WgoVZWliStmE7wBu6X_La7z4rAddgIlGRiqLZPkaSruzO2PP3i_T1Ezupcw9ol6LP_nlPaOQHeAjJ7aSQMyA"
-}
-```
-
-## Social Authentication
-
-The system also supports authentication via social providers (Google).
-
-### Google Login
-
-* Endpoint: `/users/login/google`
-* Method: POST
-* Consumes: application/x-www-form-urlencoded
-* Produces: application/json
-
-* Request:
-
-```shell
-curl -X POST \
- 'http://localhost:8080/users/login/google' \
- --header 'Content-Type: application/x-www-form-urlencoded' \
- --data-urlencode 'idToken=GOOGLE_ID_TOKEN'
-```
-
-* Response: Same as normal login - AuthenticationDTO with user and token.
-
-### Social Authentication Flow
-
-1. User clicks "Login with Google" in the frontend
-2. Frontend initiates OAuth2 flow with the provider
-3. Provider returns an ID token (JWT)
-4. Frontend sends the ID token to the backend endpoint
-5. Backend validates the token and extracts user information (email, name)
-6. Backend searches for user by email
-7. If user doesn't exist, backend creates it automatically
-8. Backend generates a JWT token for the system
-9. Backend returns AuthenticationDTO with user and token
-
-## Exceptions
-
-RESTful Web Service layer will return a HTTP 401 (Unauthorized) if the user
-does not exist or the password is incorrect. If the request is invalid, for
-example, without the required parameters, the service will return a HTTP 400
-(Bad Request).
\ No newline at end of file
diff --git a/docs/usecases/Autenticate/login.md b/docs/usecases/Autenticate/login.md
new file mode 100644
index 0000000..2bc9fda
--- /dev/null
+++ b/docs/usecases/Autenticate/login.md
@@ -0,0 +1,137 @@
+---
+layout: default
+title: Login
+parent: Use Cases
+nav_order: 1
+---
+
+## Login
+
+This use case is responsible for authenticating a user in the system. The endpoint returns a `LoginResponseDTO` that may contain authentication credentials or indicate that two-factor authentication (2FA) is required.
+
+### Normal flow
+
+* A client sends an e-mail and password.
+* The service validates the input data and verifies if the user exists in the system.
+* If the user exists and has 2FA enabled with `require2FAForBasicLogin` set to `true`, the service returns a `LoginResponseDTO` indicating that 2FA is required.
+* If the user exists and 2FA is not required, the service returns a `LoginResponseDTO` containing the user data and a signed JWT token.
+
+### LoginResponseDTO
+
+The `/users/login` endpoint returns a `LoginResponseDTO` object that can represent different authentication states:
+
+**Structure:**
+
+```json
+{
+ "authentication": {
+ "user": {
+ "hash": "string",
+ "name": "string",
+ "email": "string",
+ "emailValid": boolean,
+ "secret2FA": "string | null",
+ "using2FA": boolean
+ },
+ "token": "string (JWT)"
+ },
+ "requires2FA": boolean,
+ "message": "string | null"
+}
+```
+
+**Fields:**
+
+- `authentication` (AuthenticationDTO | null): Contains the user data and JWT token when login is successful. This field is `null` when 2FA is required.
+- `requires2FA` (boolean): Indicates whether two-factor authentication is required to complete the login process.
+- `message` (string | null): Optional message providing additional information, typically "2FA code required" when 2FA is needed.
+
+**AuthenticationDTO Structure:**
+
+When `authentication` is present, it contains:
+
+- `user` (UserEntity): The authenticated user object containing:
+ - `hash`: Unique identifier for the user
+ - `name`: User's display name
+ - `email`: User's email address
+ - `emailValid`: Whether the email has been validated
+ - `secret2FA`: The 2FA secret (null if not configured)
+ - `using2FA`: Whether 2FA is enabled for this user
+- `token` (string): A signed JWT token that can be used for authenticated requests
+
+## HTTPS endpoints
+
+* /users/login
+ * Method: POST
+ * Consumes: application/x-www-form-urlencoded
+ * Produces: application/json
+
+### Request Example
+
+```shell
+curl -X POST \
+ 'http://localhost:8080/users/login' \
+ --header 'Content-Type: application/x-www-form-urlencoded' \
+ --data-urlencode 'email=orion@services.dev' \
+ --data-urlencode 'password=12345678'
+```
+
+### Response Examples
+
+#### Example 1: Successful Login (without 2FA requirement)
+
+When the user exists and 2FA is not required, the response contains the authentication data:
+
+```json
+{
+ "authentication": {
+ "user": {
+ "hash": "53012a1a-b8ec-40f4-a81e-bc8b97ddab75",
+ "name": "Orion",
+ "email": "orion@services.dev",
+ "emailValid": false,
+ "secret2FA": null,
+ "using2FA": false
+ },
+ "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJvcmlvbi11c2VycyIsInVwbiI6Im9yaW9uQHNlcnZpY2VzLmRldiIsImdyb3VwcyI6WyJ1c2VyIl0sImNfaGFzaCI6IjUzMDEyYTFhLWI4ZWMtNDBmNC1hODFlLWJjOGI5N2RkYWI3NSIsImVtYWlsIjoib3Jpb25Ac2VydmljZXMuZGV2IiwiaWF0IjoxNzE1Mzk0NzA0LCJleHAiOjE3MTUzOTUwMDQsImp0aSI6ImMzYjZkZmFkLTAyMDAtNDc3YS05MDJmLTU0ZDg5YjdiMTUzYyJ9.I93SpcxIm31wfMQeiFLuUuuWuwlG-C0aGascSEDseRueILn9Tf5shEyNDMLQr6QRNhQbNjRjnCwe_quenVfjBEF_BLgtDDq7maoqpzDdrnDoKxtxex0dIXmRg2ABZoktB-jBo8yJcflandp1FUe7hG1VduE2E8D6WqvUQiNrhhCiiEZ4d5Moc1H11S3YGg3X1U-QnWUGx70FYQG4Qo-1Ini7T6miC0xCxSJRxumXKKtBRLYMDizp5qPIVoVIatJUu4WgoVZWliStmE7wBu6X_La7z4rAddgIlGRiqLZPkaSruzO2PP3i_T1Ezupcw9ol6LP_nlPaOQHeAjJ7aSQMyA"
+ },
+ "requires2FA": false,
+ "message": null
+}
+```
+
+#### Example 2: Login Requiring 2FA
+
+When the user has 2FA enabled and `require2FAForBasicLogin` is set to `true`, the response indicates that a 2FA code is required:
+
+```json
+{
+ "authentication": null,
+ "requires2FA": true,
+ "message": "2FA code required"
+}
+```
+
+In this case, the client should:
+1. Extract the 2FA code from the user (typically from an authenticator app)
+2. Make a subsequent request to `/users/login/2fa` with the email and 2FA code
+3. The 2FA endpoint will return a complete `LoginResponseDTO` with authentication data upon successful validation
+
+### Handling the Response
+
+**When `requires2FA` is `false`:**
+- Use the `token` from `authentication.token` for subsequent authenticated requests
+- Store the token securely (e.g., in localStorage or httpOnly cookie)
+- Include the token in the `Authorization` header as `Bearer {token}`
+
+**When `requires2FA` is `true`:**
+- Prompt the user for their 2FA code
+- Make a POST request to `/users/login/2fa` with:
+ - `email`: The user's email
+ - `code`: The 2FA code from the authenticator app
+- The 2FA endpoint will return a `LoginResponseDTO` with `authentication` populated upon success
+
+## Exceptions
+
+RESTful Web Service layer will return a HTTP 401 (Unauthorized) if the user does not exist or the password is incorrect. If the request is invalid, for example, without the required parameters, the service will return a HTTP 400 (Bad Request).
+
diff --git a/docs/usecases/UseCases.puml b/docs/usecases/UseCases.puml
index afb3a42..8da7557 100644
--- a/docs/usecases/UseCases.puml
+++ b/docs/usecases/UseCases.puml
@@ -4,7 +4,7 @@ left to right direction
actor "Client" as client
rectangle Users{
- usecase "Authenticate" as UC1
+ usecase "Login" as UC1
usecase "Create and Authenticate" as UC2
usecase "Create User" as UC3
usecase "Validate E-mail" as UC4
diff --git a/pom.xml b/pom.xml
index 636d176..12eea31 100755
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
4.0.0
dev.orion
users
- 0.0.5
+ 0.0.6
3.12.1
21
diff --git a/src/main/kotlin/dev/orion/users/adapters/controllers/UserController.kt b/src/main/kotlin/dev/orion/users/adapters/controllers/UserController.kt
index e5c1429..8ab0a4b 100644
--- a/src/main/kotlin/dev/orion/users/adapters/controllers/UserController.kt
+++ b/src/main/kotlin/dev/orion/users/adapters/controllers/UserController.kt
@@ -175,20 +175,25 @@ class UserController : BasicController() {
/**
* Creates a user, generates a Json Web Token and returns a
- * AuthenticationDTO object.
+ * LoginResponseDTO object.
*
* @param name : The user name
* @param email : The user e-mail
* @param password : The user password
- * @return A Uni object
+ * @return A Uni object
*/
- fun createAuthenticate(name: String, email: String, password: String): Uni {
+ fun createAuthenticate(name: String, email: String, password: String): Uni {
return this.createUser(name, email, password)
.onItem().ifNotNull().transform { user ->
- val dto = AuthenticationDTO()
- dto.token = this.generateJWT(user)
- dto.user = user
- dto
+ val authDto = AuthenticationDTO()
+ authDto.token = this.generateJWT(user)
+ authDto.user = user
+
+ val response = LoginResponseDTO()
+ response.authentication = authDto
+ response.requires2FA = false
+
+ response
}
}
@@ -298,9 +303,9 @@ class UserController : BasicController() {
*
* @param email The email of the user
* @param code The TOTP code to validate
- * @return A Uni that emits an AuthenticationDTO with JWT if validation succeeds
+ * @return A Uni that emits a LoginResponseDTO with JWT if validation succeeds
*/
- fun validateSocialLogin2FA(email: String, code: String): Uni {
+ fun validateSocialLogin2FA(email: String, code: String): Uni {
// Validate code format using use case
val user: User = twoFactorAuthUC.validateCode(email, code)
@@ -332,10 +337,15 @@ class UserController : BasicController() {
}
// Generate JWT and return DTO
- val dto = AuthenticationDTO()
- dto.token = generateJWT(userEntity)
- dto.user = userEntity
- Uni.createFrom().item(dto)
+ val authDto = AuthenticationDTO()
+ authDto.token = generateJWT(userEntity)
+ authDto.user = userEntity
+
+ val response = LoginResponseDTO()
+ response.authentication = authDto
+ response.requires2FA = false
+
+ Uni.createFrom().item(response)
}
}
@@ -344,9 +354,9 @@ class UserController : BasicController() {
*
* @param email The email of the user
* @param code The TOTP code to validate
- * @return A Uni that emits an AuthenticationDTO with JWT if validation succeeds
+ * @return A Uni that emits a LoginResponseDTO with JWT if validation succeeds
*/
- fun validate2FACode(email: String, code: String): Uni {
+ fun validate2FACode(email: String, code: String): Uni {
// Validate code format using use case
val user: User = twoFactorAuthUC.validateCode(email, code)
@@ -373,10 +383,15 @@ class UserController : BasicController() {
}
// Generate JWT and return DTO
- val dto = AuthenticationDTO()
- dto.token = generateJWT(userEntity)
- dto.user = userEntity
- Uni.createFrom().item(dto)
+ val authDto = AuthenticationDTO()
+ authDto.token = generateJWT(userEntity)
+ authDto.user = userEntity
+
+ val response = LoginResponseDTO()
+ response.authentication = authDto
+ response.requires2FA = false
+
+ Uni.createFrom().item(response)
}
}
@@ -540,9 +555,9 @@ class UserController : BasicController() {
*
* @param email The email of the user
* @param response The authentication response from the client (JSON string)
- * @return An AuthenticationDTO with JWT if authentication succeeds
+ * @return A LoginResponseDTO with JWT if authentication succeeds
*/
- fun finishWebAuthnAuthentication(email: String, response: String): Uni {
+ fun finishWebAuthnAuthentication(email: String, response: String): Uni {
// Validate using use case
webAuthnUC.finishAuthentication(email, response)
@@ -566,10 +581,15 @@ class UserController : BasicController() {
webAuthnCredentialRepository.saveCredential(credential)
// Generate JWT and return DTO
- val dto = AuthenticationDTO()
- dto.token = generateJWT(user)
- dto.user = user
- dto
+ val authDto = AuthenticationDTO()
+ authDto.token = generateJWT(user)
+ authDto.user = user
+
+ val loginResponse = LoginResponseDTO()
+ loginResponse.authentication = authDto
+ loginResponse.requires2FA = false
+
+ loginResponse
}
}
}
diff --git a/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/AuthenticationWS.kt b/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/AuthenticationWS.kt
index 4b77a0b..257f94c 100644
--- a/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/AuthenticationWS.kt
+++ b/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/AuthenticationWS.kt
@@ -103,13 +103,8 @@ class AuthenticationWS {
return controller.login(email, password)
.onItem().ifNotNull()
.transform { response ->
- if (response.requires2FA) {
- // Return 200 OK but indicate 2FA is required
- Response.ok(response).status(Response.Status.OK).build()
- } else {
- // Normal login response
- Response.ok(response.authentication).build()
- }
+ // Always return LoginResponseDTO complete
+ Response.ok(response).build()
}
.onItem().ifNull()
.failWith(ServiceException("User not found", Response.Status.UNAUTHORIZED))
@@ -124,7 +119,7 @@ class AuthenticationWS {
*
* @param email The email of the user
* @param code The TOTP code
- * @return The AuthenticationDTO with JWT token
+ * @return The LoginResponseDTO with JWT token
* @throws A ServiceException if validation fails
*/
@POST
@@ -138,8 +133,8 @@ class AuthenticationWS {
@RestForm @NotEmpty code: String
): Uni {
return controller.validate2FACode(email, code)
- .onItem().transform { dto ->
- Response.ok(dto).build()
+ .onItem().transform { response ->
+ Response.ok(response).build()
}
.onFailure().transform { e ->
val message = e.message ?: "Invalid TOTP code"
@@ -153,7 +148,7 @@ class AuthenticationWS {
* @param name The name of the user
* @param email The email of the user
* @param password The password of the user
- * @return The Authentication DTO
+ * @return The LoginResponseDTO
* @throws A Bad Request if the service is unable to create the user
*/
@POST
@@ -168,7 +163,7 @@ class AuthenticationWS {
@FormParam("password") @NotEmpty password: String
): Uni {
return controller.createAuthenticate(name, email, password)
- .onItem().ifNotNull().transform { dto -> Response.ok(dto).build() }
+ .onItem().ifNotNull().transform { response -> Response.ok(response).build() }
.onFailure().transform { e ->
val message = e.message ?: "Unknown error"
throw ServiceException(message, Response.Status.BAD_REQUEST)
diff --git a/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/SocialAuthWS.kt b/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/SocialAuthWS.kt
index 00ac293..52b8356 100644
--- a/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/SocialAuthWS.kt
+++ b/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/SocialAuthWS.kt
@@ -17,7 +17,6 @@
package dev.orion.users.frameworks.rest.authentication
import dev.orion.users.adapters.controllers.UserController
-import dev.orion.users.adapters.presenters.AuthenticationDTO
import dev.orion.users.adapters.presenters.LoginResponseDTO
import dev.orion.users.frameworks.rest.ServiceException
import io.quarkus.hibernate.reactive.panache.common.WithSession
@@ -90,13 +89,8 @@ class SocialAuthWS {
}
.onItem().transformToUni { responseUni ->
responseUni.onItem().transform { response ->
- if (response.requires2FA) {
- // Return 200 OK but indicate 2FA is required
- Response.ok(response).status(Response.Status.OK).build()
- } else {
- // Normal login response
- Response.ok(response.authentication).build()
- }
+ // Always return LoginResponseDTO complete
+ Response.ok(response).build()
}
}
.onFailure().transform { e ->
@@ -110,7 +104,7 @@ class SocialAuthWS {
*
* @param email The email of the user
* @param code The TOTP code to validate
- * @return AuthenticationDTO with JWT token
+ * @return LoginResponseDTO with JWT token
* @throws ServiceException if validation fails
*/
@POST
@@ -124,8 +118,8 @@ class SocialAuthWS {
@RestForm @NotEmpty code: String
): Uni {
return controller.validateSocialLogin2FA(email, code)
- .onItem().transform { dto ->
- Response.ok(dto).build()
+ .onItem().transform { response ->
+ Response.ok(response).build()
}
.onFailure().transform { e ->
val message = e.message ?: "Invalid TOTP code"
diff --git a/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/TwoFactorAuth.kt b/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/TwoFactorAuth.kt
index e65bfe7..d5c2148 100644
--- a/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/TwoFactorAuth.kt
+++ b/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/TwoFactorAuth.kt
@@ -17,7 +17,7 @@
package dev.orion.users.frameworks.rest.authentication
import dev.orion.users.adapters.controllers.UserController
-import dev.orion.users.adapters.presenters.AuthenticationDTO
+import dev.orion.users.adapters.presenters.LoginResponseDTO
import dev.orion.users.frameworks.rest.ServiceException
import io.quarkus.hibernate.reactive.panache.common.WithSession
import io.smallrye.mutiny.Uni
@@ -85,7 +85,7 @@ class TwoFactorAuth {
*
* @param email The email of the user
* @param code The TOTP code to validate
- * @return The AuthenticationDTO with JWT token
+ * @return The LoginResponseDTO with JWT token
* @throws ServiceException if validation fails
*/
@POST
@@ -99,8 +99,8 @@ class TwoFactorAuth {
@RestForm @NotEmpty code: String
): Uni {
return controller.validate2FACode(email, code)
- .onItem().transform { dto ->
- Response.ok(dto).build()
+ .onItem().transform { response ->
+ Response.ok(response).build()
}
.onFailure().transform { e ->
val message = e.message ?: "Invalid TOTP code"
diff --git a/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/WebAuthnWS.kt b/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/WebAuthnWS.kt
index a1f631b..d2d52ca 100644
--- a/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/WebAuthnWS.kt
+++ b/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/WebAuthnWS.kt
@@ -17,7 +17,7 @@
package dev.orion.users.frameworks.rest.authentication
import dev.orion.users.adapters.controllers.UserController
-import dev.orion.users.adapters.presenters.AuthenticationDTO
+import dev.orion.users.adapters.presenters.LoginResponseDTO
import dev.orion.users.frameworks.rest.ServiceException
import io.quarkus.hibernate.reactive.panache.common.WithSession
import io.smallrye.mutiny.Uni
@@ -142,7 +142,7 @@ class WebAuthnWS {
*
* @param email The email of the user
* @param response The authentication response from the client (JSON string)
- * @return The AuthenticationDTO with JWT token
+ * @return The LoginResponseDTO with JWT token
* @throws ServiceException if authentication fails
*/
@POST
@@ -156,8 +156,8 @@ class WebAuthnWS {
@RestForm @NotEmpty response: String
): Uni {
return controller.finishWebAuthnAuthentication(email, response)
- .onItem().transform { dto ->
- Response.ok(dto).build()
+ .onItem().transform { loginResponse ->
+ Response.ok(loginResponse).build()
}
.onFailure().transform { e ->
val message = e.message ?: "Invalid WebAuthn authentication"