diff --git a/demos/jans-chip/android/README.md b/demos/jans-chip/android/README.md index f119e878fdd..5dd2b8f1983 100644 --- a/demos/jans-chip/android/README.md +++ b/demos/jans-chip/android/README.md @@ -1,6 +1,7 @@ # Jans Chip -## A first party android mobile application that leverages dynamic client registration (DCR), DPoP access tokens. +- A first party android mobile application that leverages dynamic client registration (DCR), DPoP access tokens. +- Passkey authentication [Demo Video](https://www.loom.com/embed/66e145e3bba4406ebda53715168ca8f9?sid=e946f580-587e-4c55-8ea8-3845d6ae4ce9) @@ -11,10 +12,35 @@ ![](./docs/enrolment.png) +##### Mobile application loading + +1. The mobile application distribution includes the [SSA](https://docs.Janssen.io/v1.1.3/admin/auth-server/endpoints/ssa/#software-statement-assertion-ssa) (Software Statement Assertion) generated from the Janssen Auth Server. The application reads OpenID and FIDO issuers from the SSA. +2. & 3. The application fetches OpenID and FIDO configurations to the mobile database. +4. To mitigate the risk of app tampering and the use of fraudulent devices, the integrity of the app and device, for Android applications can be verified using the [Play Integrity API](https://developer.android.com/google/play/integrity). +5. The application performs [DCR](https://docs.Janssen.io/v1.1.3/admin/auth-server/endpoints/client-registration/#dynamic-client-registration-dcr) (Dynamic Client Registration) against the Auth Server using the SSA. To [use attestation in OAuth 2.0 Dynamic Client Registration](https://www.ietf.org/id/draft-tschofenig-oauth-attested-dclient-reg-00.html), the app generates evidence in JWT format containing verifiable claims like app_id and app-checksum. The integrity verdict from the Play Integrity API can be added as a claim in the evidence JWT for attested DCR. This claim is then verified by the designated verifier to ensure the trustworthiness of both the app and the Android device. Based on this verification, the DCR is either approved or rejected. + +##### Passkey Enrolment + +6. Once the application is loaded, it shows the enrollment screen where the user can submit their username/password (for users already registered on the auth server). The app requests the Janssen Auth server's Authorization Challenge Endpoint to exchange an authorization code for the correct username/password. The Janssen auth server provides an AuthorizationChallengeType custom script which is used to control Authorization Challenge Endpoint behavior. During user enrollment in the app, username and password verification is expertly guided by the Authorization Challenge script. +7. After verifying the username/password, the app calls the Janssen FIDO Server's [/attestation/options](https://github.com/JanssensenProject/Janssen/blob/main/Janssen-fido2/docs/JanssenFido2Swagger.yaml) endpoint with the username, displayname, and attestation (none) request parameters. The Janssen FIDO Server returns information about the user, the RP, and the type of credential desired. +8. To perform user verification, the mobile app requests the user's fingerprint impression. +9. On successful fingerprint verification, the authenticator library inside the app creates a new asymmetric key pair and safely stores the private key. The public key, a credential ID, and other attestation data are converted to an attestationObject by the authenticator. +10. The app calls the Janssen FIDO Server's [/attestation/reult](https://github.com/JanssensenProject/Janssen/blob/main/Janssen-fido2/docs/JanssenFido2Swagger.yaml) endpoint with the attestationObject and clientDataJSON (a byte array containing the challenge sent by the RP, the origin of the domain observed by the client, and the type of operation performed) request parameters. The Janssen FIDO Server verifies the request and sends the success/failure response. +11. Using the authorization code obtained in step #6, the app requests an access token from the Janssen Auth Server using the token endpoint. The token is of type [DpoP](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop-16). +12. Using the access token, user info is fetched from the Janssen Auth Server, and the user details page is shown on the app with the user info. + #### Authentication ![](./docs/authentication.png) +1. The mobile application displays already enrolled passkeys with it. On selecting a passkey it makes request to [/assertion/options](https://github.com/JanssenProject/jans/blob/main/jans-fido2/docs/jansFido2Swagger.yaml) of Janssen FIDO server. +2. The response of `/assertion/options` contains the challenge and the allowCredentials field which contains a list of previously registered credentials. +3. The app requests the user for biometric input (thumb impression). +4. One the user is verified, the authenticator finds a credential that matches the Relying Party ID and creates a new assertion by signing over the clientDataHash and authenticatorData with the private key generated for this account during enrolment. +5. The app then make request to [/assertion/result](https://github.com/JanssenProject/jans/blob/main/jans-fido2/docs/jansFido2Swagger.yaml) of Janssen FIDO server with authenticatorData and assertion signature. +6. The FIDO server performs validation and responds with success or failure response. This request is made through Authorization Challenge Endpoint script so that authorization code is sent on success of `/assertion/result` request. +7. & 8. Using the authorization code, the app exchanges an access token and then user-info from Janssen Auth server. + ### Prerequisite 1. A Running Janssen Auth server and Janssen FIDO server. @@ -45,4 +71,5 @@ Add following [Auth Challenge Script](./docs/authChallengeScript.java) in Jans S **Reference:** - https://github.com/JanssenProject/jans/wiki/Mobile-DPoP-FIDO-Authn -- https://github.com/JanssenProject/jans/wiki/DPoP-Mobile-App-POC \ No newline at end of file +- https://github.com/JanssenProject/jans/wiki/DPoP-Mobile-App-POC +- Authenticator code from https://github.com/duo-labs/android-webauthn-authenticator/tree/master \ No newline at end of file diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/MainActivity.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/MainActivity.kt index 6a12ac8d4ee..e19b9129e3e 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/MainActivity.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/MainActivity.kt @@ -29,6 +29,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -41,31 +42,27 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController -import com.example.compose.AppTheme -import com.nimbusds.jwt.JWTClaimsSet +import io.jans.chip.theme.AppTheme import com.spr.jetpack_loading.components.indicators.lineScaleIndicator.LineScaleIndicator import com.spr.jetpack_loading.enums.PunchType -import io.jans.chip.factories.DPoPProofFactory import io.jans.chip.model.OIDCClient import io.jans.chip.model.OPConfiguration import io.jans.chip.model.UserInfoResponse import io.jans.chip.model.appIntegrity.AppIntegrityResponse +import io.jans.chip.model.fido.config.FidoConfiguration import io.jans.chip.ui.screens.NavigationRoutes import io.jans.chip.ui.screens.authenticatedGraph import io.jans.chip.ui.screens.unauthenticatedGraph -import io.jans.chip.utils.AppConfig import io.jans.chip.viewmodel.MainViewModel import io.jans.jans_chip.R import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async -import kotlinx.coroutines.delay import kotlinx.coroutines.launch class MainActivity : AppCompatActivity() { @@ -74,103 +71,7 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val mainViewModel = MainViewModel.getInstance(this) - var loading = true - CoroutineScope(Dispatchers.IO).launch { - - mainViewModel.opConfigurationPresent = false - mainViewModel.fidoConfigurationPresent = false - mainViewModel.attestationOptionSuccess = false - mainViewModel.attestationOptionResponse = false - mainViewModel.clientRegistered = false - mainViewModel.userIsAuthenticated = false - - mainViewModel.errorInLoading = false - - //get openid configuration - try { - val jwtClaimsSet: JWTClaimsSet = DPoPProofFactory.getClaimsFromSSA() - var opConfiguration: OPConfiguration? = - async { mainViewModel.getOPConfigurationInDatabase() }.await() - if (opConfiguration != null) { - mainViewModel.opConfigurationPresent = true - } else { - val issuer: String = jwtClaimsSet.getClaim("iss").toString() - mainViewModel.setOpConfigUrl(issuer + AppConfig.OP_CONFIG_URL) - opConfiguration = async { mainViewModel.fetchOPConfiguration() }.await() - if (opConfiguration == null || opConfiguration.isSuccessful == false) { - mainViewModel.errorInLoading = true - mainViewModel.loadingErrorMessage = "Error in fetching OP Configuration" - throw Exception("Error in fetching OP Configuration") - } - } - - //get FIDO configuration - val fidoConfiguration = async { mainViewModel.getFidoConfigInDatabase() }.await() - if (fidoConfiguration != null) { - mainViewModel.fidoConfigurationPresent = true - } else { - val issuer: String = jwtClaimsSet.getClaim("iss").toString() - mainViewModel.setFidoConfigUrl(issuer + AppConfig.FIDO_CONFIG_URL) - val fidoConfigurationResponse = - async { mainViewModel.fetchFidoConfiguration() }.await() - - if (fidoConfigurationResponse == null || fidoConfigurationResponse.isSuccessful == false) { - mainViewModel.errorInLoading = true - mainViewModel.loadingErrorMessage = "Error in fetching FIDO Configuration" - throw Exception("Error in fetching FIDO Configuration") - } - } - //check OIDC client - var oidcClient: OIDCClient? = async { mainViewModel.getClientInDatabase() }.await() - - if (oidcClient != null) { - mainViewModel.clientRegistered = true - } else { - oidcClient = mainViewModel.doDCRUsingSSA( - AppConfig.SSA, - AppConfig.ALLOWED_REGISTRATION_SCOPES - ) - mainViewModel.clientRegistered = oidcClient != null - if (oidcClient == null || oidcClient.isSuccessful == false) { - mainViewModel.errorInLoading = true - mainViewModel.loadingErrorMessage = "Error in registering OIDC Client" - throw Exception("Error in registering OIDC Client") - } - } - val userInfoResponse: UserInfoResponse? = - mainViewModel.getUserInfoWithAccessToken(oidcClient.recentGeneratedAccessToken) - if (userInfoResponse?.isSuccessful == true) { - mainViewModel.setUserInfoResponse(userInfoResponse) - mainViewModel.userIsAuthenticated = true - } - - - var appIntegrityEntity: String? = - async { mainViewModel.checkAppIntegrityFromDatabase() }.await() - if (appIntegrityEntity == null) { - val appIntegrityResponse: AppIntegrityResponse? = - async { mainViewModel.checkAppIntegrity() }.await() - if (appIntegrityResponse != null) { - mainViewModel.errorInLoading = true - mainViewModel.loadingErrorMessage = - appIntegrityResponse.appIntegrity?.appRecognitionVerdict - ?: "Unable to fetch App Integrity from Google Play Integrity" - } - } else { - mainViewModel.errorInLoading = true - mainViewModel.loadingErrorMessage = "App Integrity: ${appIntegrityEntity}" - } - loading = false - } catch (e: Exception) { - //catching exception - loading = false - mainViewModel.errorInLoading = true - mainViewModel.loadingErrorMessage = "Error in loading app: ${e.message}" - e.printStackTrace() - } - - } setContent { AppTheme { // A surface container using the 'background' color from the theme @@ -178,17 +79,14 @@ class MainActivity : AppCompatActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - val loadingApp = remember { mutableStateOf(loading) } val shouldShowDialog = remember { mutableStateOf(false) } val dialogContent = remember { mutableStateOf("") } - - shouldShowDialog.value = mainViewModel.errorInLoading - dialogContent.value = mainViewModel.loadingErrorMessage + LoadingAppTasks(shouldShowDialog, dialogContent, mainViewModel) AppAlertDialog( shouldShowDialog = shouldShowDialog, content = dialogContent ) - if (!loading) { + if (!mainViewModel.mainState.isLoading) { MainApp() } else { Column( @@ -221,6 +119,81 @@ class MainActivity : AppCompatActivity() { } } +@Composable +fun LoadingAppTasks( + shouldShowDialog: MutableState, + dialogContent: MutableState, + mainViewModel: MainViewModel +) { + LaunchedEffect(true) { + CoroutineScope(Dispatchers.IO).launch { + //get openid configuration + try { + mainViewModel.mainState = mainViewModel.mainState.copy(isLoading = true) + val opConfiguration: OPConfiguration? = + async { mainViewModel.getOPConfiguration() }.await() + if (opConfiguration?.isSuccessful == false) { + mainViewModel.mainState = mainViewModel.mainState.copy(errorInLoading = true) + mainViewModel.mainState = mainViewModel.mainState.copy(loadingErrorMessage = "Error in fetching OP Configuration") + //throw Exception("Error in fetching OP Configuration") + } + + //get FIDO configuration + val fidoConfiguration: FidoConfiguration? = + async { mainViewModel.getFIDOConfiguration() }.await() + if (fidoConfiguration?.isSuccessful == false) { + mainViewModel.mainState = mainViewModel.mainState.copy(errorInLoading = true) + mainViewModel.mainState = mainViewModel.mainState.copy(loadingErrorMessage = "Error in fetching FIDO Configuration") + //throw Exception("Error in fetching FIDO Configuration") + } + + //check OIDC client + val oidcClient: OIDCClient? = async { mainViewModel.getOIDCClient() }.await() + if (oidcClient?.isSuccessful == false) { + mainViewModel.mainState = mainViewModel.mainState.copy(errorInLoading = true) + mainViewModel.mainState = mainViewModel.mainState.copy(loadingErrorMessage = "Error in registering OIDC Client") + //throw Exception("Error in registering OIDC Client") + } + //setting user-info + val userInfoResponse: UserInfoResponse? = + mainViewModel.getUserInfo(oidcClient?.recentGeneratedAccessToken) + if (userInfoResponse?.isSuccessful == true) { + mainViewModel.setUserInfoResponse(userInfoResponse) + } + //checking app integrity + val appIntegrityEntity: String? = + async { mainViewModel.checkAppIntegrityFromDatabase() }.await() + if (appIntegrityEntity == null) { + val appIntegrityResponse: AppIntegrityResponse? = + async { mainViewModel.checkAppIntegrity() }.await() + if (appIntegrityResponse != null) { + mainViewModel.mainState = mainViewModel.mainState.copy(errorInLoading = true) + mainViewModel.mainState = mainViewModel.mainState.copy( + loadingErrorMessage = appIntegrityResponse.appIntegrity?.appRecognitionVerdict + ?: "Unable to fetch App Integrity from Google Play Integrity" + ) + } + } else { + mainViewModel.mainState = mainViewModel.mainState.copy(errorInLoading = true) + mainViewModel.mainState = mainViewModel.mainState.copy(loadingErrorMessage = "App Integrity: $appIntegrityEntity") + } + shouldShowDialog.value = mainViewModel.mainState.errorInLoading + dialogContent.value = mainViewModel.mainState.loadingErrorMessage + + mainViewModel.mainState = mainViewModel.mainState.copy(isLoading = false) + } catch (e: Exception) { + //catching exception + mainViewModel.mainState = mainViewModel.mainState.copy(isLoading = false) + mainViewModel.mainState = mainViewModel.mainState.copy(errorInLoading = true) + mainViewModel.mainState = mainViewModel.mainState.copy(loadingErrorMessage = "Error in loading app: ${e.message}") + e.printStackTrace() + } + } + } + + /* Landing screen content */ +} + @Composable fun MainApp() { Surface( @@ -277,52 +250,6 @@ fun AppAlertDialog(shouldShowDialog: MutableState, content: MutableStat } } -@Composable -fun AppLoaderDialog(shouldShowDialog: MutableState) { - if (shouldShowDialog.value) { - Column( - modifier = Modifier - .fillMaxSize() - .navigationBarsPadding() - .imePadding() - .height(400.dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Spacer(modifier = Modifier.height(40.dp)) - LineScaleIndicator( - color = Color(0xFF134520), - rectCount = 5, - distanceOnXAxis = 30f, - lineHeight = 100, - animationDuration = 500, - minScale = 0.3f, - maxScale = 1.5f, - punchType = PunchType.RANDOM_PUNCH, - penThickness = 15f - ) - } - } -} - -@Composable -fun Title( - // 1 - text: String, - fontFamily: FontFamily, - fontWeight: FontWeight, - fontSize: TextUnit, -) { - Text( // 2 - text = text, - style = TextStyle( - fontFamily = fontFamily, - fontWeight = fontWeight, - fontSize = fontSize, // 3 - ) - ) -} @Composable fun LogButton( diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/PasswordTextField.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/PasswordTextField.kt deleted file mode 100644 index 08c74e5d95f..00000000000 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/PasswordTextField.kt +++ /dev/null @@ -1,116 +0,0 @@ -package io.jans.chip - - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Visibility -import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.input.VisualTransformation -import java.util.regex.Pattern - -/** - * Jetpack Compose component for a password text field with validation and visibility toggle. - * - * @param password The current value of the password text field. - * @param onPasswordChange The callback to be invoked when the password value changes. - * @param errorColor The color to be used for displaying error messages. - * @param textFieldLabel The label for the password text field. - * @param errorText The error message to be displayed when the password is not valid. - */ - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun PasswordTextField( - password: TextFieldValue, - onPasswordChange: (TextFieldValue) -> Unit, - errorColor: Color = MaterialTheme.colorScheme.error, - textFieldLabel: String = "Password", - errorText: String = "Password not valid" -) { - // State variables to manage password visibility and validity - var showPassword by remember { mutableStateOf(false) } - var isPasswordError by remember { mutableStateOf(true) } - - // TextField for entering user password - TextField( - value = password, - onValueChange = { - onPasswordChange(it) - isPasswordError = it.isValidPassword() - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Done - ), - visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), - trailingIcon = { - // Password visibility toggle icon - PasswordVisibilityToggleIcon( - showPassword = showPassword, - onTogglePasswordVisibility = { showPassword = !showPassword }) - }, - isError = !isPasswordError, - supportingText = { - // Display error text if the password is not valid - if (!isPasswordError) { - Text( - modifier = Modifier.fillMaxWidth(), - text = errorText, - color = errorColor - ) - } - }, - label = { Text(textFieldLabel) }, - //modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp) - ) -} - -/** - * Jetpack Compose component for a password visibility toggle icon. - * - * @param showPassword Whether the password is currently visible. - * @param onTogglePasswordVisibility The callback to toggle password visibility. - */ -@Composable -fun PasswordVisibilityToggleIcon( - showPassword: Boolean, - onTogglePasswordVisibility: () -> Unit -) { - // Determine the icon based on password visibility - val image = if (showPassword) Icons.Filled.Visibility else Icons.Filled.VisibilityOff - val contentDescription = if (showPassword) "Hide password icon" else "Show password icon" - - // IconButton to toggle password visibility - IconButton(onClick = onTogglePasswordVisibility) { - Icon(imageVector = image, contentDescription = contentDescription) - } -} - -/** - * Extension function to check if the [TextFieldValue] represents a valid password. - */ -fun TextFieldValue.isValidPassword(): Boolean { - val password = text - val passwordRegex = - Pattern.compile("[a-zA-Z0-9\\-#\\.\\(\\)\\/%&\\s]{0,19}") - - return password.matches((passwordRegex).toRegex()) -} \ No newline at end of file diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/OIDCClient.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/OIDCClient.kt index 9fbcd22fcaf..4d2f647e226 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/OIDCClient.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/OIDCClient.kt @@ -40,8 +40,8 @@ data class OIDCClient( ) { @Ignore - var isSuccessful: Boolean? = true + var isSuccessful: Boolean = false @Ignore - var errorMessage: String? = null + var errorMessage: String = "" } \ No newline at end of file diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/OPConfiguration.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/OPConfiguration.kt index f23c7d70146..a9166acfe94 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/OPConfiguration.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/OPConfiguration.kt @@ -37,10 +37,10 @@ data class OPConfiguration( ) { @Ignore - var isSuccessful: Boolean? = true + var isSuccessful: Boolean = false @Ignore - var errorMessage: String? = null + var errorMessage: String = "" @ColumnInfo(name = "FIDO_URL") var fidoUrl: String? = null diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/UserInfoResponse.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/UserInfoResponse.kt index 890b1532a21..57a75ff58dc 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/UserInfoResponse.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/UserInfoResponse.kt @@ -6,7 +6,7 @@ class UserInfoResponse ( var response: Any? = null ) { @Ignore - var isSuccessful: Boolean? = true + var isSuccessful: Boolean? = false @Ignore var errorMessage: String? = null diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/appIntegrity/AppIntegrityResponse.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/appIntegrity/AppIntegrityResponse.kt index 27778d75b00..c9ea4ede9d0 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/appIntegrity/AppIntegrityResponse.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/appIntegrity/AppIntegrityResponse.kt @@ -20,7 +20,7 @@ data class AppIntegrityResponse( var error: String? = null ) { @Ignore - var isSuccessful: Boolean? = true + var isSuccessful: Boolean? = false @Ignore var errorMessage: String? = null diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/fido/config/FidoConfiguration.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/fido/config/FidoConfiguration.kt index 1f683901bc7..395c86822df 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/fido/config/FidoConfiguration.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/model/fido/config/FidoConfiguration.kt @@ -2,6 +2,7 @@ package io.jans.chip.model.fido.config import androidx.room.ColumnInfo import androidx.room.Entity +import androidx.room.Ignore import androidx.room.PrimaryKey import com.google.gson.annotations.SerializedName @@ -26,4 +27,10 @@ data class FidoConfiguration( @ColumnInfo(name = "ASSERTION_RESULT_ENDPOINT") var assertionResultEndpoint: String? = null, -) \ No newline at end of file +) { + @Ignore + var isSuccessful: Boolean = false + + @Ignore + var errorMessage: String = "" +} \ No newline at end of file diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/DCRRepository.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/DCRRepository.kt index 26e77ae7953..ae0a565ae07 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/DCRRepository.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/DCRRepository.kt @@ -105,7 +105,7 @@ class DCRRepository(context: Context) { dcrRequest.jwks = jwks.toJsonString() Log.d(TAG, "Inside doDCR :: jwks :: " + jwks.toJsonString()) - var response: Response = + val response: Response = ApiAdapter.getInstance(issuer).doDCR(dcrRequest, registrationUrl) if (response.code() != 200 && response.code() != 201) { @@ -273,19 +273,15 @@ class DCRRepository(context: Context) { } - suspend fun isClientInDatabase(): Boolean { - var oidcClients: List? = appDatabase?.oidcClientDao()?.getAll() - var oidcClient: OIDCClient? = null - oidcClient = oidcClients?.let { it -> it[0] } - return oidcClient != null - } - - suspend fun getClientInDatabase(): OIDCClient? { + suspend fun getOIDCClient(): OIDCClient? { val oidcClients: List = appDatabase.oidcClientDao().getAll() var oidcClient: OIDCClient? = null - if(oidcClients.isNotEmpty()) { - oidcClient = oidcClients?.let { it -> it.get(0) } + if(!oidcClients.isNullOrEmpty()) { + oidcClient = oidcClients[0] + oidcClient.isSuccessful = true + return oidcClient } + oidcClient = doDCRUsingSSA(AppConfig.SSA, AppConfig.ALLOWED_REGISTRATION_SCOPES) return oidcClient } diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/FidoConfigurationRepository.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/FidoConfigurationRepository.kt index 1e783e40082..b07a573507a 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/FidoConfigurationRepository.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/FidoConfigurationRepository.kt @@ -2,9 +2,11 @@ package io.jans.chip.repository import android.content.Context import android.util.Log +import com.nimbusds.jwt.JWTClaimsSet import io.jans.chip.retrofit.ApiAdapter import io.jans.chip.utils.AppConfig import io.jans.chip.AppDatabase +import io.jans.chip.factories.DPoPProofFactory import io.jans.chip.model.OPConfiguration import io.jans.chip.model.fido.config.FidoConfiguration import io.jans.chip.model.fido.config.FidoConfigurationResponse @@ -13,19 +15,18 @@ import retrofit2.Response class FidoConfigurationRepository(context: Context) { private val TAG = "FidoConfigurationRepository" private val appDatabase = AppDatabase.getInstance(context); - private var fidoConfigurationResponse: FidoConfigurationResponse? = - FidoConfigurationResponse(null, null, null) + private var fidoConfiguration: FidoConfiguration = FidoConfiguration("", null, null, null, null, null) var obtainedContext: Context = context - suspend fun fetchFidoConfiguration(configurationUrl: String): FidoConfigurationResponse? { + private suspend fun fetchFidoConfiguration(configurationUrl: String): FidoConfiguration? { val issuer: String = configurationUrl.replace(AppConfig.FIDO_CONFIG_URL, "") Log.d(TAG, "Inside fetchFIDOConfiguration :: configurationUrl ::$configurationUrl") try { val opConfigurationList: List = appDatabase.opConfigurationDao().getAll() if (opConfigurationList.isEmpty()) { - fidoConfigurationResponse?.isSuccessful = false - fidoConfigurationResponse?.errorMessage = "OpenID configuration not found in database." - return fidoConfigurationResponse + fidoConfiguration.isSuccessful = false + fidoConfiguration.errorMessage = "OpenID configuration not found in database." + return fidoConfiguration } val opConfiguration: OPConfiguration = opConfigurationList[0] @@ -33,55 +34,58 @@ class FidoConfigurationRepository(context: Context) { ApiAdapter.getInstance(issuer).getFidoConfiguration(configurationUrl) if (response.code() != 200) { - fidoConfigurationResponse?.isSuccessful = false - fidoConfigurationResponse?.errorMessage = + fidoConfiguration.isSuccessful = false + fidoConfiguration.errorMessage = "Error in fetching FIDO Configuration. Error message: ${response.message()}" - return fidoConfigurationResponse + return fidoConfiguration } - fidoConfigurationResponse = response.body() + val fidoConfigurationResponse: FidoConfigurationResponse? = response.body() if (!response.isSuccessful || fidoConfigurationResponse == null) { - fidoConfigurationResponse?.isSuccessful = false - fidoConfigurationResponse?.errorMessage = + fidoConfiguration.isSuccessful = false + fidoConfiguration.errorMessage = "Error in fetching FIDO Configuration. Error message: ${response.message()}" - return fidoConfigurationResponse + return fidoConfiguration } - fidoConfigurationResponse?.isSuccessful = true - val fidoConfigDB = FidoConfiguration( + fidoConfiguration = FidoConfiguration( AppConfig.DEFAULT_S_NO, - fidoConfigurationResponse?.issuer, - fidoConfigurationResponse?.attestation?.optionsEndpoint, - fidoConfigurationResponse?.attestation?.resultEndpoint, - fidoConfigurationResponse?.assertion?.optionsEndpoint, - fidoConfigurationResponse?.assertion?.resultEndpoint + fidoConfigurationResponse.issuer, + fidoConfigurationResponse.attestation?.optionsEndpoint, + fidoConfigurationResponse.attestation?.resultEndpoint, + fidoConfigurationResponse.assertion?.optionsEndpoint, + fidoConfigurationResponse.assertion?.resultEndpoint ) - + fidoConfiguration.isSuccessful = true Log.d( TAG, - "Inside fetchOPConfiguration :: opConfiguration :: ${fidoConfigurationResponse?.issuer}" + "Inside fetchOPConfiguration :: ${fidoConfigurationResponse.issuer}" ) appDatabase.fidoConfigurationDao().deleteAll() - appDatabase.fidoConfigurationDao().insert(fidoConfigDB) + appDatabase.fidoConfigurationDao().insert(fidoConfiguration) opConfiguration.fidoUrl = configurationUrl appDatabase.opConfigurationDao().update(opConfiguration) - return fidoConfigurationResponse + return fidoConfiguration } catch (e: Exception) { Log.e(TAG, "Error in fetching OP Configuration. ${e.message}".trimIndent()) - fidoConfigurationResponse?.isSuccessful = false - fidoConfigurationResponse?.errorMessage = + fidoConfiguration.isSuccessful = false + fidoConfiguration.errorMessage = "Error in fetching FIDO Configuration. Error message: ${e.message}" - return fidoConfigurationResponse + return fidoConfiguration } } - suspend fun getFidoConfigInDatabase(): FidoConfiguration? { - val fidoConfigurationList: List = appDatabase.fidoConfigurationDao() - .getAll() + suspend fun getFidoConfig(): FidoConfiguration? { + val fidoConfigurationList: List = appDatabase.fidoConfigurationDao().getAll() var fidoConfiguration: FidoConfiguration? = null - if(fidoConfigurationList.isNotEmpty()) { - fidoConfiguration = fidoConfigurationList.let { it -> it[0] } + if(!fidoConfigurationList.isNullOrEmpty()) { + fidoConfiguration = fidoConfigurationList[0] + fidoConfiguration.isSuccessful = true + return fidoConfiguration } + val jwtClaimsSet: JWTClaimsSet = DPoPProofFactory.getClaimsFromSSA() + val issuer: String = jwtClaimsSet.getClaim("iss").toString() + fidoConfiguration = fetchFidoConfiguration(issuer + AppConfig.FIDO_CONFIG_URL) return fidoConfiguration } diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/LoginResponseRepository.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/LoginResponseRepository.kt index 1c8dfe834be..983ed1bda0a 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/LoginResponseRepository.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/LoginResponseRepository.kt @@ -15,7 +15,7 @@ class LoginResponseRepository(context: Context) { private val appDatabase = AppDatabase.getInstance(context); var obtainedContext: Context = context private var loginResponse: LoginResponse? = LoginResponse(null) - suspend fun processlogin( + suspend fun processLogin( usernameText: String, passwordText: String?, authMethod: String, diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/OPConfigurationRepository.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/OPConfigurationRepository.kt index 349f108e5c4..5d7e340c04c 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/OPConfigurationRepository.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/OPConfigurationRepository.kt @@ -2,10 +2,13 @@ package io.jans.chip.repository import android.content.Context import android.util.Log +import com.nimbusds.jwt.JWTClaimsSet import io.jans.chip.model.OPConfiguration import io.jans.chip.retrofit.ApiAdapter import io.jans.chip.utils.AppConfig import io.jans.chip.AppDatabase +import io.jans.chip.factories.DPoPProofFactory +import io.jans.chip.model.OIDCClient import retrofit2.Response class OPConfigurationRepository (context: Context){ @@ -13,10 +16,10 @@ class OPConfigurationRepository (context: Context){ private val appDatabase = AppDatabase.getInstance(context); private var opConfiguration: OPConfiguration? = OPConfiguration("", null, null, null, null, null, null) - suspend fun fetchOPConfiguration(configurationUrl: String): OPConfiguration? { + private suspend fun fetchOPConfiguration(configurationUrl: String): OPConfiguration? { try { val issuer: String = configurationUrl.replace(AppConfig.OP_CONFIG_URL, "") - var response: Response = + val response: Response = ApiAdapter.getInstance(issuer).getOPConfiguration(configurationUrl) if (response.code() != 200) { opConfiguration?.isSuccessful = false @@ -57,12 +60,17 @@ class OPConfigurationRepository (context: Context){ return false } - suspend fun getOPConfigurationInDatabase(): OPConfiguration? { - var opConfigurations: List? = appDatabase.opConfigurationDao().getAll() + suspend fun getOPConfiguration(): OPConfiguration? { + val opConfigurations: List? = appDatabase.opConfigurationDao().getAll() var opConfiguration: OPConfiguration? = null - if(opConfigurations != null && !opConfigurations.isEmpty()) { - opConfiguration = opConfigurations?.let { it -> it.get(0) } + if(!opConfigurations.isNullOrEmpty()) { + opConfiguration = opConfigurations[0] + opConfiguration.isSuccessful = true + return opConfiguration } + val jwtClaimsSet: JWTClaimsSet = DPoPProofFactory.getClaimsFromSSA() + val issuer: String = jwtClaimsSet.getClaim("iss").toString() + opConfiguration = fetchOPConfiguration(issuer + AppConfig.OP_CONFIG_URL) return opConfiguration } diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/PlayIntegrityRepository.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/PlayIntegrityRepository.kt index 5df44d66f8f..889fec69c7d 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/PlayIntegrityRepository.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/repository/PlayIntegrityRepository.kt @@ -231,8 +231,8 @@ class PlayIntegrityRepository(context: Context) { suspend fun getAppIntegrityEntityInDatabase(): AppIntegrityEntity? { var appIntegrityEntities: List? = appDatabase.appIntegrityDao().getAll() var appIntegrityEntity: AppIntegrityEntity? = null - if(appIntegrityEntities != null && !appIntegrityEntities.isEmpty()) { - appIntegrityEntity = appIntegrityEntities?.let { it -> it.get(0) } + if(!appIntegrityEntities.isNullOrEmpty()) { + appIntegrityEntity = appIntegrityEntities[0] } return appIntegrityEntity } diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/retrofit/APIInterface.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/retrofit/APIInterface.kt index 6abf39502ec..8fc4e40db5c 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/retrofit/APIInterface.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/retrofit/APIInterface.kt @@ -97,79 +97,4 @@ interface APIInterface { @GET suspend fun verifyIntegrityTokenOnAppServer(@Url url: String?): Response? - /*@GET - fun getFidoConfiguration(@Url url: String?): Call? - - @POST - fun doDCR(@Body dcrRequest: DCRequest?, @Url url: String?): Call? - - @FormUrlEncoded - @POST - fun getAuthorizationChallenge( - @Field("client_id") clientId: String?, - @Field("username") username: String?, - @Field("password") password: String?, - @Field("state") state: String?, - @Field("nonce") nonce: String?, - @Field("use_device_session") useDeviceSession: Boolean, - @Url url: String? - ): Call? - - @FormUrlEncoded - @POST - fun getToken( - @Field("client_id") clientId: String?, - @Field("code") code: String?, - @Field("grant_type") grantType: String?, - @Field("redirect_uri") redirectUri: String?, - @Field("scope") scope: String?, - @Header("Authorization") authHeader: String?, - @Header("DPoP") dpopJwt: String?, - @Url url: String? - ): Call? - - @FormUrlEncoded - @POST - fun getUserInfo( - @Field("access_token") accessToken: String?, - @Header("Authorization") authHeader: String?, - @Url url: String? - ): Call? - - @FormUrlEncoded - @POST - fun logout( - @Field("token") token: String?, - @Field("token_type_hint") tokenTypeHint: String?, - @Header("Authorization") authHeader: String?, - @Url url: String? - ): Call? - - // This API for verifying integrity token - @GET - fun verifyIntegrityTokenOnAppServer(@Url url: String?): Call? - - @POST - fun attestationOption( - @Body request: AttestationOptionRequest?, - @Url url: String? - ): Call? - - @POST - fun attestationResult( - @Body request: AttestationResultRequest?, - @Url url: String? - ): POST?>? - - @POST - fun assertionOption( - @Body request: AssertionOptionRequest?, - @Url url: String? - ): Call? - - @POST - fun assertionResult( - @Body request: AssertionResultRequest?, - @Url url: String? - ): Call?>?*/ } \ No newline at end of file diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/theme/Color.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/theme/Color.kt index 0c0000d8c08..5534a0e5be6 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/theme/Color.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/theme/Color.kt @@ -1,4 +1,4 @@ -package com.example.compose +package io.jans.chip.theme import androidx.compose.ui.graphics.Color val md_theme_light_primary = Color(0xFF006D3D) diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/theme/Theme.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/theme/Theme.kt index 473f7588267..2a002b6fb04 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/theme/Theme.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/theme/Theme.kt @@ -1,4 +1,4 @@ -package com.example.compose +package io.jans.chip.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/theme/Type.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/theme/Type.kt index 2ca555e7947..4713fe163df 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/theme/Type.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/theme/Type.kt @@ -1,4 +1,4 @@ -package io.jans.fidokot.ui.theme +package io.jans.chip.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/screens/dashboard/DashboardScreen.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/screens/dashboard/DashboardScreen.kt index bbe1f7b32c4..9aebf085c47 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/screens/dashboard/DashboardScreen.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/screens/dashboard/DashboardScreen.kt @@ -58,8 +58,8 @@ fun DashboardScreen( verticalArrangement = Arrangement.Center, ) { - if (mainViewModel.clientRegistered && (mainViewModel.attestationOptionResponse || mainViewModel.assertionOptionResponse)) { - if (mainViewModel.userIsAuthenticated) { + if (mainViewModel.mainState.isClientRegistered && (mainViewModel.mainState.attestationResultSuccess || mainViewModel.mainState.assertionResultSuccess)) { + if (mainViewModel.mainState.isUserIsAuthenticated) { TitleText(text = stringResource(id = R.string.dashboard_title_welcome) + " " + mainViewModel.getUsername()) if (mainViewModel.getUserInfoResponse().response != null) { val userInfo = JSONObject(mainViewModel.getUserInfoResponse().response.toString()) @@ -75,8 +75,7 @@ fun DashboardScreen( onClick = { CoroutineScope(Dispatchers.Main).launch { loading = true - val logoutResponse: LogoutResponse = - async { mainViewModel.logout() }.await() + val logoutResponse: LogoutResponse = async { mainViewModel.logout() }.await() if (logoutResponse.isSuccessful != true) { shouldShowDialog.value = true dialogContent.value = logoutResponse.errorMessage.toString() diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/screens/unauthenticated/login/LoginScreen.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/screens/unauthenticated/login/LoginScreen.kt index 572716bcd02..2dde5717cd8 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/screens/unauthenticated/login/LoginScreen.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/screens/unauthenticated/login/LoginScreen.kt @@ -1,11 +1,9 @@ package io.jans.chip.ui.screens.unauthenticated.login -import android.widget.Toast import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -74,8 +72,7 @@ fun LoginScreen( val authAdaptor = AuthAdaptor(context) val shouldShowDialog = remember { mutableStateOf(false) } val dialogContent = remember { mutableStateOf("") } - val creds: List? = - authAdaptor.getAllCredentials() + val creds: List? = authAdaptor.getAllCredentials() var loginState by remember { loginViewModel.loginState } @@ -184,7 +181,7 @@ fun LoginScreen( ) // Heading Login // Login Inputs Composable - if (creds == null || creds.isEmpty()) { + if (creds.isNullOrEmpty()) { MediumTitleText( modifier = Modifier.padding(top = AppTheme.dimens.paddingLarge), text = stringResource(id = R.string.no_passkey_enrolled) @@ -211,27 +208,29 @@ fun LoginScreen( if (isBiometricAvailable) { CoroutineScope(Dispatchers.Main).launch { + //show the loading screen loginState = loginState.copy(isLoading = true) + //fetch FIDO configuration val fidoConfiguration = - async { mainViewModel.fetchFidoConfiguration() }.await() + async { mainViewModel.getFIDOConfiguration() }.await() if (fidoConfiguration?.isSuccessful == false) { shouldShowDialog.value = true - dialogContent.value = - fidoConfiguration.errorMessage.toString() + dialogContent.value = fidoConfiguration.errorMessage.toString() loginState = loginState.copy(isLoading = false) return@launch } + //call /assertion/option val assertionOptionResponse: AssertionOptionResponse? = async { mainViewModel.assertionOption(ele.userDisplayName) }.await() if (assertionOptionResponse?.isSuccessful == false) { shouldShowDialog.value = true - dialogContent.value = - assertionOptionResponse.errorMessage.toString() + dialogContent.value = assertionOptionResponse.errorMessage.toString() loginState = loginState.copy(isLoading = false) return@launch } + //get authenticator wrapper instance val authAdaptor = AuthAdaptor(context) - + //select public key credential val selectedPublicKeyCredentialSource = async { authAdaptor.selectPublicKeyCredentialSource( LocalCredentialSelector(), @@ -239,109 +238,104 @@ fun LoginScreen( fidoConfiguration?.issuer, ) }.await() - - val signature = - async { + //Generate a signature object + val signature = async { authAdaptor.generateSignature( selectedPublicKeyCredentialSource ) }.await() - + //show biometric prompt BiometricHelper.authenticateUser(context, signature!!, onSuccess = { plainText -> CoroutineScope(Dispatchers.Main).launch { mainViewModel.setUsername(ele.userDisplayName) - if (assertionOptionResponse != null) { - val assertionResultRequest: AssertionResultRequest = - async { - authAdaptor.authenticate( - assertionOptionResponse, - fidoConfiguration?.issuer, - selectedPublicKeyCredentialSource - ) - }.await() - if (assertionResultRequest.isSuccessful == false) { - shouldShowDialog.value = - true - dialogContent.value = - assertionResultRequest.errorMessage.toString() - loginState = loginState.copy(isLoading = false) - return@launch - } - + //call authenticator authenticate method to get authenticatorData and assertion signature + val assertionResultRequest: AssertionResultRequest = + async { + authAdaptor.authenticate( + assertionOptionResponse!!, + fidoConfiguration?.issuer, + selectedPublicKeyCredentialSource + ) + }.await() + if (assertionResultRequest.isSuccessful == false) { + shouldShowDialog.value = true + dialogContent.value = assertionResultRequest.errorMessage.toString() + loginState = loginState.copy(isLoading = false) + return@launch + } - val loginResponse: LoginResponse? = - async { - mainViewModel.processlogin( - ele.userDisplayName, - null, - "authenticate", - Gson().toJson( - assertionResultRequest - ) + //process authentication to get authorization code + val loginResponse: LoginResponse? = + async { + mainViewModel.processLogin( + ele.userDisplayName, + null, + "authenticate", + Gson().toJson( + assertionResultRequest ) - }.await() + ) + }.await() - if (loginResponse?.isSuccessful == false) { - shouldShowDialog.value = - true - dialogContent.value = - loginResponse.errorMessage.toString() - loginState = loginState.copy(isLoading = false) - return@launch - } - val tokenResponse: TokenResponse? = - async { - mainViewModel.getToken( - loginResponse?.authorizationCode, - ) - }.await() - if (tokenResponse?.isSuccessful == false) { - shouldShowDialog.value = - true - dialogContent.value = - tokenResponse.errorMessage.toString() - loginState = loginState.copy(isLoading = false) - return@launch - } - val userInfoResponse: UserInfoResponse? = - async { - mainViewModel.getUserInfo( - tokenResponse?.accessToken - ) - }.await() - if (userInfoResponse != null) { - mainViewModel.setUserInfoResponse( - userInfoResponse + if (loginResponse?.isSuccessful == false) { + shouldShowDialog.value = true + dialogContent.value = loginResponse.errorMessage.toString() + loginState = loginState.copy(isLoading = false) + return@launch + } + //exchange token for code + val tokenResponse: TokenResponse? = + async { + mainViewModel.getToken( + loginResponse?.authorizationCode, ) - } - if (userInfoResponse?.isSuccessful == false) { - shouldShowDialog.value = - true - dialogContent.value = - userInfoResponse.errorMessage.toString() - loginState = loginState.copy(isLoading = false) - return@launch - } + }.await() + if (tokenResponse?.isSuccessful == false) { + shouldShowDialog.value = true + dialogContent.value = tokenResponse.errorMessage.toString() + loginState = loginState.copy(isLoading = false) + return@launch + } + //exchange user-info for token + val userInfoResponse: UserInfoResponse? = + async { + mainViewModel.getUserInfo( + tokenResponse?.accessToken + ) + }.await() + if (userInfoResponse != null) { + mainViewModel.setUserInfoResponse( + userInfoResponse + ) + } + if (userInfoResponse?.isSuccessful == false) { + shouldShowDialog.value = true + dialogContent.value = userInfoResponse.errorMessage.toString() + loginState = loginState.copy(isLoading = false) + return@launch } - mainViewModel.assertionOptionResponse = - true - loginViewModel.onUiEvent( - loginUiEvent = LoginUiEvent.Submit - ) - loginState = loginState.copy(isLoading = false) - //Toast.makeText(context,"Biometric authentication successful!$plainText", Toast.LENGTH_SHORT).show() } + mainViewModel.mainState = + mainViewModel.mainState.copy( + assertionOptionSuccess = true + ) + mainViewModel.mainState = + mainViewModel.mainState.copy( + assertionResultSuccess = true + ) + loginViewModel.onUiEvent( + loginUiEvent = LoginUiEvent.Submit + ) + loginState = loginState.copy(isLoading = false) + //Toast.makeText(context,"Biometric authentication successful!$plainText", Toast.LENGTH_SHORT).show() }) } //end } else { - Toast.makeText( - context, - "Biometric authentication is not available!", - Toast.LENGTH_SHORT - ).show() + shouldShowDialog.value = true + dialogContent.value = "Biometric authentication is not available!" loginState = loginState.copy(isLoading = false) } }) diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/screens/unauthenticated/login/LoginViewModel.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/screens/unauthenticated/login/LoginViewModel.kt index 92e21089444..7ef84f00ed3 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/screens/unauthenticated/login/LoginViewModel.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/screens/unauthenticated/login/LoginViewModel.kt @@ -22,32 +22,6 @@ class LoginViewModel : ViewModel() { fun onUiEvent(loginUiEvent: LoginUiEvent) { when (loginUiEvent) { - // Email/Mobile changed - /*is LoginUiEvent.EmailOrMobileChanged -> { - loginState.value = loginState.value.copy( - emailOrMobile = loginUiEvent.inputValue, - errorState = loginState.value.errorState.copy( - emailOrMobileErrorState = if (loginUiEvent.inputValue.trim().isNotEmpty()) - ErrorState() - else - emailOrMobileEmptyErrorState - ) - ) - } - - // Password changed - is LoginUiEvent.PasswordChanged -> { - loginState.value = loginState.value.copy( - password = loginUiEvent.inputValue, - errorState = loginState.value.errorState.copy( - passwordErrorState = if (loginUiEvent.inputValue.trim().isNotEmpty()) - ErrorState() - else - passwordEmptyErrorState - ) - ) - }*/ - // Submit Login is LoginUiEvent.Submit -> { //val inputsValidated = validateInputs() diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/screens/unauthenticated/registration/RegistrationScreen.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/screens/unauthenticated/registration/RegistrationScreen.kt index 19cfcfe272f..c8f218e3013 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/screens/unauthenticated/registration/RegistrationScreen.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/screens/unauthenticated/registration/RegistrationScreen.kt @@ -47,6 +47,7 @@ import io.jans.chip.model.TokenResponse import io.jans.chip.model.UserInfoResponse import io.jans.chip.model.fido.attestation.option.AttestationOptionResponse import io.jans.chip.model.fido.attestation.result.AttestationResultRequest +import io.jans.chip.model.fido.config.FidoConfiguration import io.jans.chip.model.fido.config.FidoConfigurationResponse import io.jans.chip.ui.common.customComposableViews.MediumTitleText import io.jans.chip.ui.common.customComposableViews.SmallClickableWithIconAndText @@ -200,7 +201,9 @@ fun RegistrationScreen( }, onSubmit = { + //create authenticator instance val authAdaptor = AuthAdaptor(context) + //check if selected enrolled credential present in database if(authAdaptor.isCredentialsPresent(mainViewModel.getUsername())) { shouldShowDialog.value = true dialogContent.value = "Username already enrolled!" @@ -209,8 +212,9 @@ fun RegistrationScreen( if (isBiometricAvailable) { CoroutineScope(Dispatchers.Main).launch { registrationState = registrationState.copy(isLoading = true) + //authenticate to get authorization code val loginResponse: LoginResponse? = async { - mainViewModel.processlogin( + mainViewModel.processLogin( mainViewModel.getUsername(), mainViewModel.getPassword(), "enroll", @@ -220,12 +224,11 @@ fun RegistrationScreen( if (loginResponse?.isSuccessful == false) { shouldShowDialog.value = true - dialogContent.value = - loginResponse.errorMessage.toString() - registrationState = - registrationState.copy(isLoading = false) + dialogContent.value = loginResponse.errorMessage.toString() + registrationState = registrationState.copy(isLoading = false) return@launch } + //exchange token for code val tokenResponse: TokenResponse? = async { mainViewModel.getToken( loginResponse?.authorizationCode, @@ -233,13 +236,12 @@ fun RegistrationScreen( }.await() if (tokenResponse?.isSuccessful == false) { shouldShowDialog.value = true - dialogContent.value = - tokenResponse.errorMessage.toString() + dialogContent.value = tokenResponse.errorMessage.toString() - registrationState = - registrationState.copy(isLoading = false) + registrationState = registrationState.copy(isLoading = false) return@launch } + //exchange user-info for token val userInfoResponse: UserInfoResponse? = async { mainViewModel.getUserInfo(tokenResponse?.accessToken) }.await() if (userInfoResponse != null) { @@ -249,25 +251,22 @@ fun RegistrationScreen( } if (userInfoResponse?.isSuccessful == false) { shouldShowDialog.value = true - dialogContent.value = - userInfoResponse.errorMessage.toString() + dialogContent.value = userInfoResponse.errorMessage.toString() - registrationState = - registrationState.copy(isLoading = false) + registrationState = registrationState.copy(isLoading = false) return@launch } - - val fidoConfiguration: FidoConfigurationResponse? = - async { mainViewModel.fetchFidoConfiguration() }.await() + //get fido configuration + val fidoConfiguration: FidoConfiguration? = + async { mainViewModel.getFIDOConfiguration() }.await() if (fidoConfiguration?.isSuccessful == false) { shouldShowDialog.value = true - dialogContent.value = - fidoConfiguration?.errorMessage.toString() + dialogContent.value = fidoConfiguration?.errorMessage.toString() - registrationState = - registrationState.copy(isLoading = false) + registrationState = registrationState.copy(isLoading = false) return@launch } + //call /attestation/option val attestationOptionResponse: AttestationOptionResponse? = async { mainViewModel.attestationOption( @@ -276,32 +275,31 @@ fun RegistrationScreen( }.await() if (attestationOptionResponse?.isSuccessful == false) { shouldShowDialog.value = true - dialogContent.value = - attestationOptionResponse?.errorMessage.toString() + dialogContent.value = attestationOptionResponse?.errorMessage.toString() - registrationState = - registrationState.copy(isLoading = false) + registrationState = registrationState.copy(isLoading = false) return@launch } - + // Generate a new credential val publicKeyCredentialSource = async { authAdaptor.getPublicKeyCredentialSource( attestationOptionResponse, fidoConfiguration?.issuer ) }.await() + //Generate a signature object val signature = async { authAdaptor.generateSignature( publicKeyCredentialSource ) }.await() - + //show biometric prompt BiometricHelper.registerUserBiometrics(context, signature!!, onSuccess = { plainText -> CoroutineScope(Dispatchers.Main).launch { - + //call authenticator register method to get attestationObject val attestationResultRequest: AttestationResultRequest? = async { authAdaptor.register( @@ -312,14 +310,12 @@ fun RegistrationScreen( }.await() if (attestationResultRequest?.isSuccessful == false) { shouldShowDialog.value = true - dialogContent.value = - attestationResultRequest.errorMessage.toString() + dialogContent.value = attestationResultRequest.errorMessage.toString() - registrationState = - registrationState.copy(isLoading = false) + registrationState = registrationState.copy(isLoading = false) return@launch } - + //call /attestation/result val attestationResultResponse = async { mainViewModel.attestationResult( attestationResultRequest @@ -327,21 +323,17 @@ fun RegistrationScreen( }.await() if (attestationResultResponse?.isSuccessful == false) { shouldShowDialog.value = true - dialogContent.value = - attestationResultResponse.errorMessage.toString() + dialogContent.value = attestationResultResponse.errorMessage.toString() - registrationState = - registrationState.copy(isLoading = false) + registrationState = registrationState.copy(isLoading = false) return@launch } - mainViewModel.attestationOptionResponse = true + mainViewModel.mainState = mainViewModel.mainState.copy(attestationOptionSuccess = true) + mainViewModel.mainState = mainViewModel.mainState.copy(attestationResultSuccess = true) - registrationViewModel.onUiEvent( - registrationUiEvent = RegistrationUiEvent.Submit - ) + registrationViewModel.onUiEvent(registrationUiEvent = RegistrationUiEvent.Submit) - registrationState = - registrationState.copy(isLoading = false) + registrationState = registrationState.copy(isLoading = false) } }) } @@ -351,8 +343,7 @@ fun RegistrationScreen( "Biometric authentication is not available!", Toast.LENGTH_SHORT ).show() - registrationState = - registrationState.copy(isLoading = false) + registrationState = registrationState.copy(isLoading = false) } } ) diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/theme/Theme.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/theme/Theme.kt index fbf71c637e7..ffb24d495cd 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/theme/Theme.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/ui/theme/Theme.kt @@ -13,7 +13,58 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.core.view.ViewCompat -import com.example.compose.* +import io.jans.chip.theme.md_theme_dark_background +import io.jans.chip.theme.md_theme_dark_error +import io.jans.chip.theme.md_theme_dark_errorContainer +import io.jans.chip.theme.md_theme_dark_inverseOnSurface +import io.jans.chip.theme.md_theme_dark_inversePrimary +import io.jans.chip.theme.md_theme_dark_inverseSurface +import io.jans.chip.theme.md_theme_dark_onBackground +import io.jans.chip.theme.md_theme_dark_onError +import io.jans.chip.theme.md_theme_dark_onErrorContainer +import io.jans.chip.theme.md_theme_dark_onPrimary +import io.jans.chip.theme.md_theme_dark_onPrimaryContainer +import io.jans.chip.theme.md_theme_dark_onSecondary +import io.jans.chip.theme.md_theme_dark_onSecondaryContainer +import io.jans.chip.theme.md_theme_dark_onSurface +import io.jans.chip.theme.md_theme_dark_onSurfaceVariant +import io.jans.chip.theme.md_theme_dark_onTertiary +import io.jans.chip.theme.md_theme_dark_onTertiaryContainer +import io.jans.chip.theme.md_theme_dark_outline +import io.jans.chip.theme.md_theme_dark_primary +import io.jans.chip.theme.md_theme_dark_primaryContainer +import io.jans.chip.theme.md_theme_dark_secondary +import io.jans.chip.theme.md_theme_dark_secondaryContainer +import io.jans.chip.theme.md_theme_dark_surface +import io.jans.chip.theme.md_theme_dark_surfaceVariant +import io.jans.chip.theme.md_theme_dark_tertiary +import io.jans.chip.theme.md_theme_dark_tertiaryContainer +import io.jans.chip.theme.md_theme_light_background +import io.jans.chip.theme.md_theme_light_error +import io.jans.chip.theme.md_theme_light_errorContainer +import io.jans.chip.theme.md_theme_light_inverseOnSurface +import io.jans.chip.theme.md_theme_light_inversePrimary +import io.jans.chip.theme.md_theme_light_inverseSurface +import io.jans.chip.theme.md_theme_light_onBackground +import io.jans.chip.theme.md_theme_light_onError +import io.jans.chip.theme.md_theme_light_onErrorContainer +import io.jans.chip.theme.md_theme_light_onPrimary +import io.jans.chip.theme.md_theme_light_onPrimaryContainer +import io.jans.chip.theme.md_theme_light_onSecondary +import io.jans.chip.theme.md_theme_light_onSecondaryContainer +import io.jans.chip.theme.md_theme_light_onSurface +import io.jans.chip.theme.md_theme_light_onSurfaceVariant +import io.jans.chip.theme.md_theme_light_onTertiary +import io.jans.chip.theme.md_theme_light_onTertiaryContainer +import io.jans.chip.theme.md_theme_light_outline +import io.jans.chip.theme.md_theme_light_primary +import io.jans.chip.theme.md_theme_light_primaryContainer +import io.jans.chip.theme.md_theme_light_secondary +import io.jans.chip.theme.md_theme_light_secondaryContainer +import io.jans.chip.theme.md_theme_light_surface +import io.jans.chip.theme.md_theme_light_surfaceVariant +import io.jans.chip.theme.md_theme_light_tertiary +import io.jans.chip.theme.md_theme_light_tertiaryContainer private val LightColors = lightColorScheme( primary = md_theme_light_primary, diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/utils/biometric/BiometricHelper.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/utils/biometric/BiometricHelper.kt index fd59b0d6007..a4325ee0b88 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/utils/biometric/BiometricHelper.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/utils/biometric/BiometricHelper.kt @@ -8,6 +8,7 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity import io.jans.jans_chip.R import java.security.Signature + /* * BiometricHelper is a utility object that simplifies the implementation of biometric authentication * functionalities in Android apps. It provides methods to check biometric availability, register user @@ -81,51 +82,13 @@ object BiometricHelper { .build() } - // Register user biometrics by encrypting a randomly generated token - /*fun registerUserBiometrics( - context: FragmentActivity, - prefBiometric: String, - onSuccess: (authResult: BiometricPrompt.AuthenticationResult) -> Unit = {}, - ) { - val cryptoManager = CryptoManager() - val cipher = cryptoManager.initEncryptionCipher(SECRET_KEY) - val biometricPrompt = getBiometricPrompt(context) { authResult -> - authResult.cryptoObject?.cipher?.let { cipher -> - // Dummy token for now(in production app, generate a unique and genuine token - // for each user registration or consider using token received from authentication server) - val token = UUID.randomUUID().toString() - val encryptedToken = cryptoManager.encrypt(token, cipher) - cryptoManager.saveToPrefs( - encryptedToken, - context, - ENCRYPTED_FILE_NAME, - Context.MODE_PRIVATE, - prefBiometric, - *//*PREF_BIOMETRIC*//* - ) - // Execute custom action on successful registration - onSuccess(authResult) - } - } - biometricPrompt.authenticate( - getPromptInfo( - context, - "Fido Enrolment", - "Enrol using your biometric credential", - "Touch the fingerprint sensor" - ), - BiometricPrompt.CryptoObject(cipher) - ) - }*/ fun registerUserBiometrics( context: FragmentActivity, signature: Signature, onSuccess: (authResult: BiometricPrompt.AuthenticationResult) -> Unit = {}, ) { - //val cryptoManager = CryptoManager() - //val cipher = cryptoManager.initEncryptionCipher(SECRET_KEY) - val biometricPrompt = getBiometricPrompt(context) { authResult -> + val biometricPrompt = getBiometricPrompt(context) { authResult -> onSuccess(authResult) } biometricPrompt.authenticate( @@ -145,31 +108,15 @@ object BiometricHelper { signature: Signature, onSuccess: (authResult: BiometricPrompt.AuthenticationResult) -> Unit, ) { - /*val cryptoManager = CryptoManager() - val encryptedData = cryptoManager.getFromPrefs( + val biometricPrompt = getBiometricPrompt(context) { authResult -> + onSuccess(authResult) + } + val promptInfo = getPromptInfo( context, - ENCRYPTED_FILE_NAME, - Context.MODE_PRIVATE, - prefBiometric*/ - /*PREF_BIOMETRIC*/ - //) - //encryptedData?.let { data -> - // val cipher = cryptoManager.initDecryptionCipher(SECRET_KEY, data.initializationVector) - val biometricPrompt = getBiometricPrompt(context) { authResult -> - //authResult.cryptoObject?.cipher?.let { cipher -> - //val plainText = cryptoManager.decrypt(data.ciphertext, cipher) - // Execute custom action on successful authentication - - //} - onSuccess(authResult) - } - val promptInfo = getPromptInfo( - context, - "Fido Authentication", - "Authenticate using your biometric credential", - "Touch the fingerprint sensor" - ) - biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(signature)) - //} + "Fido Authentication", + "Authenticate using your biometric credential", + "Touch the fingerprint sensor" + ) + biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(signature)) } } \ No newline at end of file diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/utils/biometric/Constants.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/utils/biometric/Constants.kt deleted file mode 100644 index 7e4a74891e6..00000000000 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/utils/biometric/Constants.kt +++ /dev/null @@ -1,10 +0,0 @@ -package io.jans.chip.utils.biometric - -const val PREF_BIOMETRIC = "biometric_preferences" -const val ENCRYPTED_FILE_NAME = "encrypted_data_store" -const val SECRET_KEY = "biometric_secret_key" - -object NavigationRoutes { - const val SIGN_IN = "SignIn" - const val SIGN_UP = "SignUp" -} \ No newline at end of file diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/utils/biometric/CryptoManager.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/utils/biometric/CryptoManager.kt deleted file mode 100644 index f85a12600d5..00000000000 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/utils/biometric/CryptoManager.kt +++ /dev/null @@ -1,141 +0,0 @@ -package io.jans.chip.utils.biometric - -import android.content.Context -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyProperties -import com.google.gson.Gson -import java.nio.charset.Charset -import java.security.KeyStore -import javax.crypto.Cipher -import javax.crypto.KeyGenerator -import javax.crypto.SecretKey -import javax.crypto.spec.GCMParameterSpec - -interface CryptoManager { - - fun initEncryptionCipher(keyName: String): Cipher - - fun initDecryptionCipher(keyName: String, initializationVector: ByteArray): Cipher - - fun encrypt(plaintext: String, cipher: Cipher): EncryptedData - - fun decrypt(ciphertext: ByteArray, cipher: Cipher): String - - fun saveToPrefs( - encryptedData: EncryptedData, - context: Context, - filename: String, - mode: Int, - prefKey: String - ) - - fun getFromPrefs( - context: Context, - filename: String, - mode: Int, - prefKey: String - ): EncryptedData? -} - -fun CryptoManager(): CryptoManager = CryptoManagerImpl() - -class CryptoManagerImpl : CryptoManager { - - private val ENCRYPTION_TRANSFORMATION = "AES/GCM/NoPadding" - private val ANDROID_KEYSTORE = "AndroidKeyStore" - private val KEY_ALIAS = "MyKeyAlias" - - private val keyStore: KeyStore = KeyStore.getInstance(ANDROID_KEYSTORE) - - init { - keyStore.load(null) - if (!keyStore.containsAlias(KEY_ALIAS)) { - createSecretKey() - } - } - - override fun initEncryptionCipher(keyName: String): Cipher { - val cipher = Cipher.getInstance(ENCRYPTION_TRANSFORMATION) - cipher.init(Cipher.ENCRYPT_MODE, getSecretKey()) - return cipher - } - - override fun initDecryptionCipher(keyName: String, initializationVector: ByteArray): Cipher { - val cipher = Cipher.getInstance(ENCRYPTION_TRANSFORMATION) - val spec = GCMParameterSpec(128, initializationVector) - cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec) - return cipher - } - - override fun encrypt(plaintext: String, cipher: Cipher): EncryptedData { - val encryptedBytes = cipher.doFinal(plaintext.toByteArray(Charset.forName("UTF-8"))) - return EncryptedData(encryptedBytes, cipher.iv) - } - - override fun decrypt(ciphertext: ByteArray, cipher: Cipher): String { - val decryptedBytes = cipher.doFinal(ciphertext) - return String(decryptedBytes, Charset.forName("UTF-8")) - } - - override fun saveToPrefs( - encryptedData: EncryptedData, - context: Context, - filename: String, - mode: Int, - prefKey: String - ) { - val json = Gson().toJson(encryptedData) - with(context.getSharedPreferences(filename, mode).edit()) { - putString(prefKey, json) - apply() - } - } - - override fun getFromPrefs( - context: Context, - filename: String, - mode: Int, - prefKey: String - ): EncryptedData? { - val json = context.getSharedPreferences(filename, mode).getString(prefKey, null) - return Gson().fromJson(json, EncryptedData::class.java) - } - - private fun createSecretKey() { - val keyGenParams = KeyGenParameterSpec.Builder( - KEY_ALIAS, - KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT - ).apply { - setBlockModes(KeyProperties.BLOCK_MODE_GCM) - setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) - setUserAuthenticationRequired(true) - }.build() - - val keyGenerator = - KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE) - keyGenerator.init(keyGenParams) - keyGenerator.generateKey() - } - - private fun getSecretKey(): SecretKey { - return keyStore.getKey(KEY_ALIAS, null) as SecretKey - } -} - -data class EncryptedData(val ciphertext: ByteArray, val initializationVector: ByteArray) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as EncryptedData - - if (!ciphertext.contentEquals(other.ciphertext)) return false - return initializationVector.contentEquals(other.initializationVector) - } - - override fun hashCode(): Int { - var result = ciphertext.contentHashCode() - result = 31 * result + initializationVector.contentHashCode() - return result - } -} \ No newline at end of file diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/utils/biometric/EnableBiometricDialog.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/utils/biometric/EnableBiometricDialog.kt deleted file mode 100644 index 5d0ba6f851d..00000000000 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/utils/biometric/EnableBiometricDialog.kt +++ /dev/null @@ -1,34 +0,0 @@ -package io.jans.chip.utils.biometric - -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import io.jans.jans_chip.R - -@Composable -fun EnableBiometricDialog( - onEnable: () -> Unit, - onDismiss: () -> Unit -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { }, - text = { - Text(text = stringResource(R.string.enable_biometric_dialog_title_text)) - }, - confirmButton = { - Button(onClick = { - onEnable() - }) { - Text(text = stringResource(R.string.enable_biometric_dialog_confirm_btn_text)) - } - }, - dismissButton = { - Button(onClick = onDismiss) { - Text(text = stringResource(R.string.enable_biometric_dialog_dismiss_btn_text)) - } - } - ) -} \ No newline at end of file diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/viewmodel/MainViewModel.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/viewmodel/MainViewModel.kt index 6cd0151b0c2..89a04611434 100644 --- a/demos/jans-chip/android/app/src/main/java/io/jans/chip/viewmodel/MainViewModel.kt +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/viewmodel/MainViewModel.kt @@ -22,7 +22,6 @@ import io.jans.chip.model.fido.attestation.option.AttestationOptionResponse import io.jans.chip.model.fido.attestation.result.AttestationResultRequest import io.jans.chip.model.fido.attestation.result.AttestationResultResponse import io.jans.chip.model.fido.config.FidoConfiguration -import io.jans.chip.model.fido.config.FidoConfigurationResponse import io.jans.chip.repository.DCRRepository import io.jans.chip.repository.FidoAssertionRepository import io.jans.chip.repository.FidoAttestationRepository @@ -33,6 +32,7 @@ import io.jans.chip.repository.OPConfigurationRepository import io.jans.chip.repository.PlayIntegrityRepository import io.jans.chip.repository.TokenResponseRepository import io.jans.chip.repository.UserInfoResponseRepository +import io.jans.chip.viewmodel.state.MainState class MainViewModel : ViewModel() { @@ -44,27 +44,18 @@ class MainViewModel : ViewModel() { private lateinit var password: String private lateinit var userInfoResponse: UserInfoResponse - var opConfigurationPresent by mutableStateOf(false) - var fidoConfigurationPresent by mutableStateOf(false) - var attestationOptionSuccess by mutableStateOf(false) - var attestationOptionResponse by mutableStateOf(false) - var clientRegistered by mutableStateOf(false) - var userIsAuthenticated by mutableStateOf(false) - var assertionOptionResponse by mutableStateOf(false) - var errorInLoading by mutableStateOf(false) - var loadingErrorMessage by mutableStateOf("") - - - lateinit var opConfigurationRepository: OPConfigurationRepository - lateinit var dcrRepository: DCRRepository - lateinit var fidoConfigurationRepository: FidoConfigurationRepository - lateinit var loginResponseRepository: LoginResponseRepository - lateinit var tokenResponseRepository: TokenResponseRepository - lateinit var userInfoResponseRepository: UserInfoResponseRepository - lateinit var logoutRepository: LogoutRepository - lateinit var fidoAttestationRepository: FidoAttestationRepository - lateinit var fidoAssertionRepository: FidoAssertionRepository - lateinit var playIntegrityRepository: PlayIntegrityRepository + var mainState by mutableStateOf(MainState()) + + private lateinit var opConfigurationRepository: OPConfigurationRepository + private lateinit var dcrRepository: DCRRepository + private lateinit var fidoConfigurationRepository: FidoConfigurationRepository + private lateinit var loginResponseRepository: LoginResponseRepository + private lateinit var tokenResponseRepository: TokenResponseRepository + private lateinit var userInfoResponseRepository: UserInfoResponseRepository + private lateinit var logoutRepository: LogoutRepository + private lateinit var fidoAttestationRepository: FidoAttestationRepository + private lateinit var fidoAssertionRepository: FidoAssertionRepository + private lateinit var playIntegrityRepository: PlayIntegrityRepository companion object { @@ -146,70 +137,11 @@ class MainViewModel : ViewModel() { return userInfoResponse } - suspend fun fetchOPConfiguration(): OPConfiguration? { - - val opConfiguration: OPConfiguration? = - opConfigurationRepository.fetchOPConfiguration(opConfigUrl) - if (opConfiguration?.isSuccessful == true) { - opConfigurationPresent = true - } - return opConfiguration - } - - suspend fun doDCR(scopeText: String): OIDCClient? { - val oidcClient: OIDCClient? = dcrRepository.doDCR(scopeText) - clientRegistered = true - return oidcClient - } - - suspend fun doDCRUsingSSA(ssa: String, scopeText: String): OIDCClient? { - val oidcClient: OIDCClient? = dcrRepository.doDCRUsingSSA(ssa, scopeText) - clientRegistered = true - return oidcClient - } - - suspend fun fetchFidoConfiguration(): FidoConfigurationResponse? { - if (!(this::fidoConfigUrl.isInitialized)) { - val opConfiguration: OPConfiguration? = - opConfigurationRepository.getOPConfigurationInDatabase() - opConfiguration?.fidoUrl?.let { setFidoConfigUrl(it) } - } - - val fidoConfigurationResponse: FidoConfigurationResponse? = - fidoConfigurationRepository.fetchFidoConfiguration(fidoConfigUrl) - if (fidoConfigurationResponse?.isSuccessful == true) { - fidoConfigurationPresent = true - } - return fidoConfigurationResponse - } - - suspend fun getFidoConfigInDatabase(): FidoConfiguration? { - return fidoConfigurationRepository.getFidoConfigInDatabase() - } - - suspend fun deleteOPConfigurationInDatabase() { - opConfigurationRepository.deleteOPConfigurationInDatabase() - opConfigurationPresent = false - } - - suspend fun deleteClientInDatabase() { - dcrRepository.deleteClientInDatabase() - clientRegistered = false - userIsAuthenticated = false - } - - suspend fun deleteFidoConfigurationInDatabase() { - fidoConfigurationRepository.deleteFidoConfigurationInDatabase() - fidoConfigurationPresent = false - attestationOptionSuccess = false - attestationOptionResponse = false - } - suspend fun attestationOption(username: String): AttestationOptionResponse? { val attestationOptionResponse: AttestationOptionResponse? = fidoAttestationRepository.attestationOption(username) if (attestationOptionResponse?.isSuccessful == true) { - attestationOptionSuccess = true + mainState = mainState.copy(attestationOptionSuccess = true) } return attestationOptionResponse } @@ -218,7 +150,7 @@ class MainViewModel : ViewModel() { val attestationResultResponse: AttestationResultResponse? = fidoAttestationRepository.attestationResult(attestationResultRequest) if (attestationResultResponse?.isSuccessful == true) { - attestationOptionResponse = true + mainState = mainState.copy(attestationResultSuccess = true) } return attestationResultResponse } @@ -228,8 +160,9 @@ class MainViewModel : ViewModel() { } suspend fun checkAppIntegrityFromDatabase(): String? { - val appIntegrityEntity: AppIntegrityEntity = playIntegrityRepository.getAppIntegrityEntityInDatabase() - ?: return null + val appIntegrityEntity: AppIntegrityEntity = + playIntegrityRepository.getAppIntegrityEntityInDatabase() + ?: return null if (appIntegrityEntity.error != null) { return appIntegrityEntity.error @@ -244,7 +177,6 @@ class MainViewModel : ViewModel() { } suspend fun assertionOption(username: String): AssertionOptionResponse? { - return fidoAssertionRepository.assertionOption(username) } @@ -252,19 +184,20 @@ class MainViewModel : ViewModel() { val assertionResultResponse: AssertionResultResponse = fidoAssertionRepository.assertionResult(assertionResultRequest) if (assertionResultResponse.isSuccessful == true) { - assertionOptionResponse = true + mainState = mainState.copy(attestationOptionSuccess = true) + mainState = mainState.copy(attestationResultSuccess = true) } return assertionResultResponse } - suspend fun processlogin( + suspend fun processLogin( usernameText: String, passwordText: String?, authMethod: String, assertionResultRequest: String? ): LoginResponse? { //userIsAuthenticated = true - return loginResponseRepository.processlogin( + return loginResponseRepository.processLogin( usernameText, passwordText, authMethod, @@ -278,44 +211,43 @@ class MainViewModel : ViewModel() { return tokenResponseRepository.getToken(authorizationCode) } - suspend fun getUserInfo(accessToken: String?): UserInfoResponse { - val userInfoResponse: UserInfoResponse = - userInfoResponseRepository.getUserInfo(accessToken) - if (userInfoResponse?.isSuccessful == true) { - userIsAuthenticated = true - } - return userInfoResponse - } - suspend fun logout(): LogoutResponse { val logoutResponse: LogoutResponse = logoutRepository.logout() if (logoutResponse.isSuccessful == true) { - userIsAuthenticated = false + mainState = mainState.copy(isUserIsAuthenticated = false) } return logoutResponse } - suspend fun isOPConfigurationInDatabase(): Boolean { - return opConfigurationRepository.isOPConfigurationInDatabase() - } - - suspend fun getOPConfigurationInDatabase(): OPConfiguration? { - return opConfigurationRepository.getOPConfigurationInDatabase() - } - - suspend fun isClientInDatabase(): Boolean { - return dcrRepository.isClientInDatabase() + suspend fun getOIDCClient(): OIDCClient? { + val oidcClient: OIDCClient? = dcrRepository.getOIDCClient() + if (oidcClient?.isSuccessful == true) { + mainState = mainState.copy(isClientRegistered = true) + } + return oidcClient } - suspend fun getClientInDatabase(): OIDCClient? { - return dcrRepository.getClientInDatabase() + suspend fun getOPConfiguration(): OPConfiguration? { + val opConfiguration: OPConfiguration? = opConfigurationRepository.getOPConfiguration() + if (opConfiguration?.isSuccessful == true) { + mainState = mainState.copy(opConfigurationPresent = true) + } + return opConfiguration } - suspend fun isAuthenticated(accessToken: String?): Boolean { - return loginResponseRepository.isAuthenticated(accessToken) + suspend fun getFIDOConfiguration(): FidoConfiguration? { + val fidoConfiguration: FidoConfiguration? = fidoConfigurationRepository.getFidoConfig() + if (fidoConfiguration?.isSuccessful == true) { + mainState = mainState.copy(fidoConfigurationPresent = true) + } + return fidoConfiguration } - suspend fun getUserInfoWithAccessToken(accessToken: String?): UserInfoResponse? { - return userInfoResponseRepository.getUserInfo(accessToken) + suspend fun getUserInfo(accessToken: String?): UserInfoResponse? { + val userInfoResponse: UserInfoResponse? = userInfoResponseRepository.getUserInfo(accessToken) + if (userInfoResponse?.isSuccessful == true) { + mainState = mainState.copy(isUserIsAuthenticated = true) + } + return userInfoResponse } } \ No newline at end of file diff --git a/demos/jans-chip/android/app/src/main/java/io/jans/chip/viewmodel/state/MainState.kt b/demos/jans-chip/android/app/src/main/java/io/jans/chip/viewmodel/state/MainState.kt new file mode 100644 index 00000000000..7c6b21469de --- /dev/null +++ b/demos/jans-chip/android/app/src/main/java/io/jans/chip/viewmodel/state/MainState.kt @@ -0,0 +1,15 @@ +package io.jans.chip.viewmodel.state + +data class MainState ( + val opConfigurationPresent: Boolean = false, + val fidoConfigurationPresent: Boolean = false, + val attestationOptionSuccess: Boolean = false, + val attestationResultSuccess: Boolean = false, + val isClientRegistered: Boolean = false, + val isUserIsAuthenticated: Boolean = false, + val assertionOptionSuccess: Boolean = false, + val assertionResultSuccess: Boolean = false, + val errorInLoading: Boolean = false, + val loadingErrorMessage: String = "", + val isLoading: Boolean = false +) \ No newline at end of file diff --git a/demos/jans-chip/android/docs/authentication.png b/demos/jans-chip/android/docs/authentication.png index 5e9aba10e86..61f147332a5 100644 Binary files a/demos/jans-chip/android/docs/authentication.png and b/demos/jans-chip/android/docs/authentication.png differ diff --git a/demos/jans-chip/android/docs/enrolment.png b/demos/jans-chip/android/docs/enrolment.png index bb52aa9fe1e..25013de5cad 100644 Binary files a/demos/jans-chip/android/docs/enrolment.png and b/demos/jans-chip/android/docs/enrolment.png differ diff --git a/demos/jans-chip/android/webauthn/src/main/java/io/jans/webauthn/Authenticator.java b/demos/jans-chip/android/webauthn/src/main/java/io/jans/webauthn/Authenticator.java index c2040b29a5e..f443407b497 100644 --- a/demos/jans-chip/android/webauthn/src/main/java/io/jans/webauthn/Authenticator.java +++ b/demos/jans-chip/android/webauthn/src/main/java/io/jans/webauthn/Authenticator.java @@ -201,7 +201,7 @@ public AttestationObject makeCredential(AuthenticatorMakeCredentialOptions optio } return attestationObject; }*/ - + // 7. Generate a new credential public PublicKeyCredentialSource getPublicKeyCredentialSource(AuthenticatorMakeCredentialOptions options) { PublicKeyCredentialSource credentialSource; try { @@ -217,6 +217,10 @@ public PublicKeyCredentialSource getPublicKeyCredentialSource(AuthenticatorMakeC } } + /** + * Generate a signature object to be unlocked via biometric prompt + * This signature object should be passed down to performSignature + **/ public Signature generateSignature(PublicKeyCredentialSource credentialSource) throws VirgilException { PrivateKey privateKey = credentialSafe.getKeyPairByAlias(credentialSource.keyPairAlias).getPrivate(); return WebAuthnCryptography.generateSignatureObject(privateKey); diff --git a/demos/jans-chip/android/webauthn/src/main/java/io/jans/webauthn/util/SelectCredentialDialogFragment.java b/demos/jans-chip/android/webauthn/src/main/java/io/jans/webauthn/util/SelectCredentialDialogFragment.java index 8a8979fbec0..92a76bd3039 100644 --- a/demos/jans-chip/android/webauthn/src/main/java/io/jans/webauthn/util/SelectCredentialDialogFragment.java +++ b/demos/jans-chip/android/webauthn/src/main/java/io/jans/webauthn/util/SelectCredentialDialogFragment.java @@ -14,7 +14,6 @@ import java.util.List; import java.util.concurrent.Exchanger; -import io.jans.webauthn.R; import io.jans.webauthn.models.PublicKeyCredentialSource; public class SelectCredentialDialogFragment extends DialogFragment implements CredentialSelector {