Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: optimization of db and rest calls on App start #8947 #8950

Merged
merged 8 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions demos/jans-chip/android/README.md
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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.
Expand Down Expand Up @@ -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
- https://github.com/JanssenProject/jans/wiki/DPoP-Mobile-App-POC
- Authenticator code from https://github.com/duo-labs/android-webauthn-authenticator/tree/master
233 changes: 80 additions & 153 deletions demos/jans-chip/android/app/src/main/java/io/jans/chip/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() {
Expand All @@ -74,121 +71,22 @@ 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
Surface(
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(
Expand Down Expand Up @@ -221,6 +119,81 @@ class MainActivity : AppCompatActivity() {
}
}

@Composable
fun LoadingAppTasks(
shouldShowDialog: MutableState<Boolean>,
dialogContent: MutableState<String>,
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(
Expand Down Expand Up @@ -277,52 +250,6 @@ fun AppAlertDialog(shouldShowDialog: MutableState<Boolean>, content: MutableStat
}
}

@Composable
fun AppLoaderDialog(shouldShowDialog: MutableState<Boolean>) {
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(
Expand Down
Loading