Skip to content

Commit 0748b87

Browse files
committed
Added Saptcha inmplementation
1 parent d27f78a commit 0748b87

File tree

11 files changed

+192
-13
lines changed

11 files changed

+192
-13
lines changed

app/src/main/java/com/sap/cdc/bitsnbytes/cdc/IdentityServiceRepository.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,10 @@ class IdentityServiceRepository private constructor(context: Context) {
264264
.clearPasskey(passkeysAuthenticationProvider)
265265
}
266266

267+
suspend fun getSaptchaToken(): IAuthResponse {
268+
return authenticationService.authenticate().getSaptchaToken()
269+
}
270+
267271
//endregion
268272

269273
//region PUSH

app/src/main/java/com/sap/cdc/bitsnbytes/ui/view/screens/EmailSignIn.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
package com.sap.cdc.bitsnbytes.ui.view.screens
44

5+
import android.widget.Toast
56
import androidx.compose.foundation.layout.Box
67
import androidx.compose.foundation.layout.Column
78
import androidx.compose.foundation.layout.fillMaxWidth
@@ -19,6 +20,7 @@ import androidx.compose.ui.Alignment
1920
import androidx.compose.ui.ExperimentalComposeUiApi
2021
import androidx.compose.ui.Modifier
2122
import androidx.compose.ui.autofill.AutofillType
23+
import androidx.compose.ui.platform.LocalContext
2224
import androidx.compose.ui.platform.LocalFocusManager
2325
import androidx.compose.ui.text.font.FontWeight
2426
import androidx.compose.ui.tooling.preview.Preview
@@ -62,6 +64,11 @@ fun EmailSignInView(viewModel: IEmailSignInViewModel) {
6264
var passwordVisible: Boolean by remember { mutableStateOf(false) }
6365
val focusManager = LocalFocusManager.current
6466

67+
val context = LocalContext.current
68+
69+
var captchaRequired by remember { mutableStateOf(false) }
70+
var isSwitchChecked by remember { mutableStateOf(false) }
71+
6572
//UI elements
6673
LoadingStateColumn(
6774
loading
@@ -167,11 +174,38 @@ fun EmailSignInView(viewModel: IEmailSignInViewModel) {
167174
authResponse?.resolvable()?.toJson()
168175
}"
169176
)
177+
},
178+
onCaptchaRequired = {
179+
loading = false
180+
captchaRequired = true
181+
signInError = "Captcha required"
170182
}
171183
)
172184
}
173185
)
174186

187+
if (captchaRequired) {
188+
SmallVerticalSpacer()
189+
androidx.compose.material3.Switch(
190+
checked = isSwitchChecked,
191+
onCheckedChange = { isChecked ->
192+
isSwitchChecked = isChecked
193+
if (isChecked) {
194+
viewModel.getSaptchaToken(
195+
token = { token ->
196+
loading = false
197+
Toast.makeText(context, token, Toast.LENGTH_SHORT).show()
198+
},
199+
onFailedWith = { error ->
200+
loading = false
201+
signInError = error?.errorDescription!!
202+
}
203+
)
204+
}
205+
}
206+
)
207+
}
208+
175209
if (signInError.isNotEmpty()) {
176210
SimpleErrorMessages(
177211
text = signInError

app/src/main/java/com/sap/cdc/bitsnbytes/ui/viewmodel/EmailRegisterViewModel.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,11 @@ class EmailRegisterViewModel(context: Context) : BaseViewModel(context), IEmailR
6969

7070
AuthState.INTERRUPTED -> {
7171
when (authResponse.cdcResponse().errorCode()) {
72-
ResolvableContext.ERR_ERROR_PENDING_TWO_FACTOR_REGISTRATION -> {
72+
ResolvableContext.ERR_PENDING_TWO_FACTOR_REGISTRATION -> {
7373
onPendingTwoFactorRegistration(authResponse)
7474
}
7575

76-
ResolvableContext.ERR_ERROR_PENDING_TWO_FACTOR_VERIFICATION -> {
76+
ResolvableContext.ERR_PENDING_TWO_FACTOR_VERIFICATION -> {
7777
onPendingTwoFactorVerification(authResponse)
7878
}
7979
}

app/src/main/java/com/sap/cdc/bitsnbytes/ui/viewmodel/EmailSignInViewModel.kt

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,18 @@ interface IEmailSignInViewModel {
2222
onLoginIdentifierExists: () -> Unit,
2323
onPendingTwoFactorRegistration: (IAuthResponse?) -> Unit,
2424
onPendingTwoFactorVerification: (IAuthResponse?) -> Unit,
25+
onCaptchaRequired: () -> Unit,
2526
onFailedWith: (CDCError?) -> Unit
2627
) {
2728
//Stub
2829
}
30+
31+
fun getSaptchaToken(
32+
token: (String) -> Unit,
33+
onFailedWith: (CDCError?) -> Unit,
34+
) {
35+
//Stub
36+
}
2937
}
3038

3139
// Mock preview class for the EmailSignInViewModel
@@ -45,6 +53,7 @@ class EmailSignInViewModel(context: Context) : BaseViewModel(context),
4553
onLoginIdentifierExists: () -> Unit,
4654
onPendingTwoFactorRegistration: (IAuthResponse?) -> Unit,
4755
onPendingTwoFactorVerification: (IAuthResponse?) -> Unit,
56+
onCaptchaRequired: () -> Unit,
4857
onFailedWith: (CDCError?) -> Unit
4958
) {
5059
viewModelScope.launch {
@@ -64,16 +73,36 @@ class EmailSignInViewModel(context: Context) : BaseViewModel(context),
6473
onLoginIdentifierExists()
6574
}
6675

67-
ResolvableContext.ERR_ERROR_PENDING_TWO_FACTOR_REGISTRATION -> {
76+
ResolvableContext.ERR_PENDING_TWO_FACTOR_REGISTRATION -> {
6877
onPendingTwoFactorRegistration(authResponse)
6978
}
7079

71-
ResolvableContext.ERR_ERROR_PENDING_TWO_FACTOR_VERIFICATION -> {
80+
ResolvableContext.ERR_PENDING_TWO_FACTOR_VERIFICATION -> {
7281
onPendingTwoFactorVerification(authResponse)
7382
}
83+
84+
ResolvableContext.ERR_CAPTCHA_REQUIRED -> {
85+
onCaptchaRequired()
86+
}
7487
}
7588
}
7689
}
7790
}
7891
}
92+
93+
override fun getSaptchaToken(token: (String) -> Unit,
94+
onFailedWith: (CDCError?) -> Unit) {
95+
viewModelScope.launch {
96+
val authResponse = identityService.getSaptchaToken()
97+
when (authResponse.state()) {
98+
AuthState.SUCCESS -> {
99+
token(authResponse.cdcResponse().stringField("saptchaToken") as String)
100+
}
101+
102+
else -> {
103+
onFailedWith(authResponse.toDisplayError())
104+
}
105+
}
106+
}
107+
}
79108
}

library/src/main/java/com/sap/cdc/android/sdk/auth/Auth.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.sap.cdc.android.sdk.auth
22

33
import androidx.activity.ComponentActivity
44
import com.sap.cdc.android.sdk.auth.flow.AccountAuthFlow
5+
import com.sap.cdc.android.sdk.auth.flow.CaptchaAuthFlow
56
import com.sap.cdc.android.sdk.auth.flow.LoginAuthFlow
67
import com.sap.cdc.android.sdk.auth.flow.LogoutAuthFlow
78
import com.sap.cdc.android.sdk.auth.flow.PasskeysAuthFlow
@@ -209,6 +210,9 @@ interface IAuthApis {
209210
suspend fun registerForAuthPushNotifications(): IAuthResponse
210211

211212
suspend fun verifyAuthPushNotification(vToken: String): IAuthResponse
213+
214+
215+
suspend fun getSaptchaToken(): IAuthResponse
212216
}
213217

214218
/**
@@ -311,6 +315,11 @@ internal class AuthApis(
311315
return flow.verifyAuthPush(vToken)
312316
}
313317

318+
override suspend fun getSaptchaToken(): IAuthResponse {
319+
val flow = CaptchaAuthFlow(coreClient, sessionService)
320+
return flow.getSaptchaToken()
321+
}
322+
314323
}
315324

316325
//endregion

library/src/main/java/com/sap/cdc/android/sdk/auth/AuthEndpoints.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ class AuthEndpoints {
5858
const val EP_TFA_TOTP_REGISTER = "accounts.tfa.totp.register"
5959
const val EP_TFA_TOTP_VERIFY = "accounts.tfa.totp.verify"
6060

61+
const val EP_RISK_SAPTCHA_GET_CHALLENGE = "accounts.risk.saptcha.getChallenge"
62+
const val EP_RISK_SAPTCHA_VERIFY = "accounts.risk.saptcha.verify"
63+
6164
// May be redundant cause connection can be done using notifySocialLogin with
6265
// loginMode = connect.
6366
const val EP_SOCIALIZE_ADD_CONNECTION = "socialize.addConnection"

library/src/main/java/com/sap/cdc/android/sdk/auth/ResolvableContext.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,16 @@ class ResolvableContext(
3131
const val ERR_ACCOUNT_PENDING_REGISTRATION = 206001
3232
const val ERR_ACCOUNT_PENDING_VERIFICATION = 206002
3333
const val ERR_ENTITY_EXIST_CONFLICT = 403043
34-
const val ERR_ERROR_PENDING_TWO_FACTOR_REGISTRATION = 403102
35-
const val ERR_ERROR_PENDING_TWO_FACTOR_VERIFICATION = 403101
34+
const val ERR_PENDING_TWO_FACTOR_REGISTRATION = 403102
35+
const val ERR_PENDING_TWO_FACTOR_VERIFICATION = 403101
36+
const val ERR_CAPTCHA_REQUIRED = 401020
3637

3738
val resolvables = mapOf(
3839
ERR_ACCOUNT_PENDING_REGISTRATION to "Account pending registration",
3940
ERR_ENTITY_EXIST_CONFLICT to "Entity exist conflict",
40-
ERR_ERROR_PENDING_TWO_FACTOR_REGISTRATION to "Pending two factor registration",
41-
ERR_ERROR_PENDING_TWO_FACTOR_VERIFICATION to "Pending two factor verification",
41+
ERR_PENDING_TWO_FACTOR_REGISTRATION to "Pending two factor registration",
42+
ERR_PENDING_TWO_FACTOR_VERIFICATION to "Pending two factor verification",
43+
ERR_CAPTCHA_REQUIRED to "Captcha required"
4244
)
4345
}
4446

library/src/main/java/com/sap/cdc/android/sdk/auth/flow/AuthFlow.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ open class AuthFlow(val coreClient: CoreClient, val sessionService: SessionServi
7777
ResolvableOtp(cdcResponse.stringField("vToken"))
7878
}
7979

80+
81+
ResolvableContext.ERR_CAPTCHA_REQUIRED -> {
82+
CDCDebuggable.log(LOG_TAG, "ERR_SAPTCHA_REQUIRED")
83+
}
84+
8085
// REGISTRATION
8186
ResolvableContext.ERR_ACCOUNT_PENDING_REGISTRATION -> {
8287
CDCDebuggable.log(LOG_TAG, "ERR_ACCOUNT_PENDING_REGISTRATION")
@@ -104,8 +109,8 @@ open class AuthFlow(val coreClient: CoreClient, val sessionService: SessionServi
104109
}
105110

106111
// TFA
107-
ResolvableContext.ERR_ERROR_PENDING_TWO_FACTOR_REGISTRATION,
108-
ResolvableContext.ERR_ERROR_PENDING_TWO_FACTOR_VERIFICATION -> {
112+
ResolvableContext.ERR_PENDING_TWO_FACTOR_REGISTRATION,
113+
ResolvableContext.ERR_PENDING_TWO_FACTOR_VERIFICATION -> {
109114
CDCDebuggable.log(LOG_TAG, "ERR_ERROR_PENDING_TWO_FACTOR_REGISTRATION")
110115
// Get providers
111116
val tfaResolve = AuthTFA(coreClient, sessionService)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.sap.cdc.android.sdk.auth.flow
2+
3+
import com.sap.cdc.android.sdk.CDCDebuggable
4+
import com.sap.cdc.android.sdk.auth.AuthEndpoints.Companion.EP_RISK_SAPTCHA_GET_CHALLENGE
5+
import com.sap.cdc.android.sdk.auth.AuthEndpoints.Companion.EP_RISK_SAPTCHA_VERIFY
6+
import com.sap.cdc.android.sdk.auth.AuthResponse
7+
import com.sap.cdc.android.sdk.auth.AuthenticationApi
8+
import com.sap.cdc.android.sdk.auth.IAuthResponse
9+
import com.sap.cdc.android.sdk.auth.session.SessionService
10+
import com.sap.cdc.android.sdk.core.CoreClient
11+
import com.sap.cdc.android.sdk.extensions.encodeWith
12+
import com.sap.cdc.android.sdk.extensions.jwtDecode
13+
14+
class CaptchaAuthFlow(coreClient: CoreClient, sessionService: SessionService) :
15+
AuthFlow(coreClient, sessionService) {
16+
17+
companion object {
18+
const val LOG_TAG = "AuthFlowCaptcha"
19+
}
20+
21+
suspend fun getSaptchaToken(): IAuthResponse {
22+
CDCDebuggable.log(TFAAuthFlow.LOG_TAG, "startChallenge")
23+
val getJwt =
24+
AuthenticationApi(coreClient, sessionService).genericSend(
25+
EP_RISK_SAPTCHA_GET_CHALLENGE,
26+
parameters = mutableMapOf()
27+
)
28+
val authResponse = AuthResponse(getJwt)
29+
if (authResponse.isError()) {
30+
// Flow ends with error.
31+
return authResponse
32+
}
33+
34+
val token = getJwt.stringField("saptchaToken") as String
35+
CDCDebuggable.log(TFAAuthFlow.LOG_TAG, "token: $token")
36+
37+
val jwtObject = token.jwtDecode()
38+
val jti = jwtObject.getString("jti")
39+
val pattern = jwtObject.getString("pattern")
40+
41+
// Verify challenge
42+
var i = 0
43+
var isFinished = false
44+
while (!isFinished) {
45+
i += 1
46+
isFinished = verifyChallenge(jti, pattern, i)
47+
}
48+
49+
// Verify token
50+
val params = mutableMapOf("token" to "$token|$i")
51+
val verifyJwt =
52+
AuthenticationApi(coreClient, sessionService).genericSend(
53+
EP_RISK_SAPTCHA_VERIFY,
54+
parameters = params
55+
)
56+
57+
val verifyResponse = AuthResponse(verifyJwt)
58+
return verifyResponse
59+
}
60+
61+
private fun verifyChallenge(challengeId: String, pattern: String, i: Int): Boolean {
62+
val value = "$challengeId.$i".encodeWith("SHA-512")
63+
val regex = pattern.toRegex()
64+
return regex.containsMatchIn(value)
65+
}
66+
67+
}

library/src/main/java/com/sap/cdc/android/sdk/extensions/StringExt.kt

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ package com.sap.cdc.android.sdk.extensions
22

33
import android.util.Base64
44
import com.sap.cdc.android.sdk.core.SiteConfig
5+
import org.json.JSONObject
6+
import java.math.BigInteger
57
import java.net.URLDecoder
8+
import java.nio.charset.StandardCharsets
9+
import java.security.MessageDigest
610
import java.security.SecureRandom
711
import java.util.Locale
812

@@ -66,4 +70,26 @@ fun String.parseQueryStringParams(): Map<String, String> =
6670
value.isNotBlank()
6771
}
6872
}
69-
.toMap()
73+
.toMap()
74+
75+
fun String.jwtDecode(): JSONObject {
76+
val parts = this.split(".")
77+
val base64EncodedData = parts[1]
78+
val data = Base64.decode(
79+
base64EncodedData.toByteArray(charset = StandardCharsets.UTF_8),
80+
Base64.DEFAULT
81+
)
82+
return JSONObject(String(data, StandardCharsets.UTF_8))
83+
}
84+
85+
fun String.encodeWith(algorithm: String): String {
86+
val md: MessageDigest = MessageDigest.getInstance(algorithm)
87+
val messageDigest = md.digest(this.toByteArray())
88+
val no = BigInteger(1, messageDigest)
89+
var hash: String = no.toString(16)
90+
// Add preceding 0s to make it 128 chars long
91+
while (hash.length < 128) {
92+
hash = "0$hash"
93+
}
94+
return hash
95+
}

0 commit comments

Comments
 (0)