Skip to content

Commit

Permalink
feat(monkeys): Add support for 2FA during login [WPB-6289]
Browse files Browse the repository at this point in the history
  • Loading branch information
Augusto César Dias committed Feb 2, 2024
1 parent f11edf5 commit 96c6a2f
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 20 deletions.
12 changes: 9 additions & 3 deletions monkeys/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@
],
"properties": {
"startCommand": {
"description": "The command to start the process. For example kubectl to start a pod or docker to start a container. It can be templated with the following fields: teamName, email, userId, teamId, monkeyIndex, monkeyClientId",
"description": "The command to start the process. For example kubectl to start a pod or docker to start a container. It can be templated with the following fields: teamName, email, userId, teamId, monkeyIndex, monkeyClientId, internalUser, internalPassword",
"type": "string",
"examples": [
"/opt/app/monkeys/bin/monkey-server -p 80{{monkeyIndex}}",
"kubectl run monkey --image monkeys --overrides='{\"spec\": {\"hostname\": \"monkey-{{monkeyIndex}}\"}}' --command -- /opt/app/monkeys/bin/monkey-server"
]
},
"addressTemplate": {
"description": "The template to resolve the address of the individual monkey. It can be templated with the following fields: teamName, email, userId, teamId, monkeyIndex, monkeyClientId",
"description": "The template to resolve the address of the individual monkey. It can be templated with the following fields: teamName, email, userId, teamId, monkeyIndex, monkeyClientId, internalUser, internalPassword",
"type": "string",
"examples": [
"http://localhost:80{{monkeyIndex}}",
Expand Down Expand Up @@ -147,7 +147,8 @@
"domain",
"authUser",
"authPassword",
"userCount"
"userCount",
"2FAEnabled"
],
"properties": {
"api": {
Expand Down Expand Up @@ -198,6 +199,11 @@
"description": "How many users should be created in this team",
"type": "integer"
},
"2FAEnabled": {
"description": "Does this server required 2FA authentication?",
"type": "boolean",
"default": false
},
"dumpUsers": {
"description": "Should the application dump the users created into a file",
"type": "boolean"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,14 @@ class LocalMonkey(monkeyType: MonkeyType, internalId: MonkeyId) : Monkey(monkeyT
* Logs user in and register client (if not registered)
*/
override suspend fun login(coreLogic: CoreLogic, callback: (Monkey) -> Unit) {
val authScope = getAuthScope(coreLogic, this.monkeyType.userData().team.backend)
val email = this.monkeyType.userData().email
val password = this.monkeyType.userData().password
val loginResult = authScope.login(email, password, false)
val userData = this.monkeyType.userData()
val secondFactor = userData.request2FA()
val authScope = getAuthScope(coreLogic, userData.team.backend)
val email = userData.email
val password = userData.password
val loginResult = authScope.login(
userIdentifier = email, password = password, shouldPersistClient = false, secondFactorVerificationCode = secondFactor
)
if (loginResult !is AuthenticationResult.Success) {
error("User creds didn't work ($email, $password)")
}
Expand All @@ -86,7 +90,10 @@ class LocalMonkey(monkeyType: MonkeyType, internalId: MonkeyId) : Monkey(monkeyT
}
val sessionScope = coreLogic.getSessionScope(loginResult.authData.userId)
val registerClientParam = RegisterClientUseCase.RegisterClientParam(
password = this.monkeyType.userData().password, capabilities = emptyList(), clientType = ClientType.Temporary
password = userData.password,
capabilities = emptyList(),
clientType = ClientType.Temporary,
secondFactorVerificationCode = secondFactor
)
val registerResult = sessionScope.client.getOrRegister(registerClientParam)
if (registerResult is RegisterClientResult.Failure) {
Expand Down Expand Up @@ -185,10 +192,7 @@ class LocalMonkey(monkeyType: MonkeyType, internalId: MonkeyId) : Monkey(monkeyT
}

override suspend fun createConversation(
name: String,
monkeyList: List<Monkey>,
protocol: ConversationOptions.Protocol,
isDestroyable: Boolean
name: String, monkeyList: List<Monkey>, protocol: ConversationOptions.Protocol, isDestroyable: Boolean
): MonkeyConversation {
val self = this
return this.monkeyState.readyThen {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ data class BackendConfig(
@SerialName("authUser") val authUser: String,
@SerialName("authPassword") val authPassword: String,
@SerialName("userCount") val userCount: ULong,
@SerialName("2FAEnabled") val secondFactorAuth : Boolean = false,
@SerialName("dumpUsers") val dumpUsers: Boolean = false,
@SerialName("presetTeam") val presetTeam: TeamConfig? = null
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
Expand Down Expand Up @@ -162,7 +163,7 @@ private suspend fun HttpClient.createTeam(backendConfig: BackendConfig): Team {
ownerId = UserId(userId, backend.domain),
client = this
)
login(team.owner.email, team.owner.password)
login(team.owner.email, team.owner.password, team.owner.request2FA())

put("self/handle") { setBody(mapOf("handle" to ownerId).toJsonObject()) }
logger.i("Owner $email (id $userId) of team ${backendConfig.teamName} (id: $teamId) in backend ${backendConfig.domain}")
Expand All @@ -172,7 +173,8 @@ private suspend fun HttpClient.createTeam(backendConfig: BackendConfig): Team {
private suspend fun setUserHandle(backendConfig: BackendConfig, userData: UserData) {
lateinit var token: BearerTokens
val httpClient = httpClient(backendConfig) { token }
httpClient.login(userData.email, userData.password) { accessToken ->
val secondFactor = httpClient.request2FA(userData.email, userData.userId)
httpClient.login(userData.email, userData.password, secondFactor) { accessToken ->
token = BearerTokens(accessToken, "")
}
httpClient.put("self/handle") { setBody(mapOf("handle" to "monkey-${userData.userId.value}").toJsonObject()) }
Expand All @@ -198,14 +200,15 @@ private suspend fun HttpClient.createUser(i: Int, team: Team, userPassword: Stri
private suspend fun HttpClient.login(
email: String,
password: String,
secondFactor: String?,
tokenProvider: (accessToken: String) -> Unit = { accessToken ->
token = BearerTokens(accessToken, "")
}
) {
val response = post("login") {
setBody(
mapOf(
"email" to email, "password" to password, "label" to ""
"email" to email, "password" to password, "label" to "", "verification_code" to secondFactor
).toJsonObject()
)
}.body<JsonObject>()
Expand All @@ -214,13 +217,24 @@ private suspend fun HttpClient.login(
}

internal suspend fun HttpClient.teamParticipants(team: Team): List<UserId> {
this.login(team.owner.email, team.owner.password)
this.login(team.owner.email, team.owner.password, team.owner.request2FA())
val members = get("teams/${team.id}/members").body<JsonObject>()
return members["members"]!!.jsonArray.map { member ->
UserId(member.jsonObject["user"]!!.jsonPrimitive.content, team.backend.domain)
}
}

suspend fun HttpClient.request2FA(email: String, userId: UserId): String {
this.post("v3/verification-code/send") {
setBody(
mapOf(
"action" to "login", "email" to email
).toJsonObject()
)
}
return this.get("i/users/${userId.value}/verification-code/login").body<JsonPrimitive>().content
}

private suspend fun HttpClient.invite(teamId: String, email: String, name: String): String {
val invitationId = post("teams/$teamId/invitations") {
setBody(
Expand All @@ -234,7 +248,7 @@ private suspend fun HttpClient.invite(teamId: String, email: String, name: Strin
}

internal fun httpClient(backendConfig: BackendConfig, tokenProvider: () -> BearerTokens? = { token }) = HttpClient(OkHttp.create()) {
val excludedPaths = listOf("register", "login", "activate")
val excludedPaths = listOf("v3", "register", "login", "activate")
defaultRequest {
url(backendConfig.api)
contentType(ContentType.Application.Json)
Expand Down
16 changes: 13 additions & 3 deletions monkeys/src/main/kotlin/com/wire/kalium/monkeys/model/UserData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,20 @@ data class UserData(
this.password,
this.team.backend.domain,
this.team.name,
"",
"",
this.team.backend.authUser,
this.team.backend.authPassword,
1u,
this.team.backend.secondFactorAuthEnabled,
presetTeam = TeamConfig(
this.team.id,
UserAccount(this.team.owner.email, this.team.owner.userId.value),
users = listOf(UserAccount(this.email, this.userId.value))
)
)

suspend fun request2FA(): String? {
return if (this.team.backend.secondFactorAuthEnabled) this.team.request2FA(this.email, this.userId) else null
}
}

@Suppress("LongParameterList")
Expand All @@ -66,6 +71,8 @@ class Team(

suspend fun usersFromTeam(): List<UserId> = this.client.teamParticipants(this)

internal suspend fun request2FA(email: String, userId: UserId) = this.client.request2FA(email, userId)

override fun equals(other: Any?): Boolean {
return other != null && other is Team && other.id == this.id
}
Expand All @@ -84,10 +91,13 @@ data class Backend(
val website: String,
val title: String,
val domain: String,
val secondFactorAuthEnabled: Boolean,
val authUser: String,
val authPassword: String,
) {
companion object {
fun fromConfig(config: BackendConfig): Backend = with(config) {
Backend(api, accounts, webSocket, blackList, teams, website, title, domain)
Backend(api, accounts, webSocket, blackList, teams, website, title, domain, secondFactorAuth, authUser, authPassword)
}
}
}

0 comments on commit 96c6a2f

Please sign in to comment.