Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -171,19 +171,24 @@ class CredentialManagerUtils @Inject constructor(
*
* @param requestResult The result of the passkey creation request.
* @param context The activity context from the Composable, to be used in Credential Manager APIs
* @param isConditional Whether the passkey creation is conditional.
* @return The [CreatePublicKeyCredentialResponse] object containing the passkey, or null if an error occurred.
*/
@SuppressLint("PublicKeyCredential")
suspend fun createPasskey(
requestResult: JSONObject,
context: Context,
isConditional: Boolean = false,
): GenericCredentialManagerResponse {
val passkeysEligibility = PasskeysEligibility.isPasskeySupported(context)
if (!passkeysEligibility.isEligible) {
return GenericCredentialManagerResponse.Error(errorMessage = passkeysEligibility.reason)
}

val credentialRequest = CreatePublicKeyCredentialRequest(requestResult.toString())
val credentialRequest = CreatePublicKeyCredentialRequest(
requestJson = requestResult.toString(),
isConditional = isConditional
)
val credentialResponse: CreatePublicKeyCredentialResponse
try {
credentialResponse = credentialManager.createCredential(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@
*/
package com.authentication.shrine.api

import com.authentication.shrine.model.EditUsernameRequest
import com.authentication.shrine.model.FederationOptionsRequest
import com.authentication.shrine.model.GenericAuthResponse
import com.authentication.shrine.model.LoginUsernamePasswordRequest
import com.authentication.shrine.model.PasskeysList
import com.authentication.shrine.model.PasswordRequest
import com.authentication.shrine.model.RegisterRequestRequestBody
import com.authentication.shrine.model.RegisterRequestResponse
import com.authentication.shrine.model.RegisterResponseRequestBody
Expand All @@ -40,41 +39,17 @@ import retrofit2.http.Query
interface AuthApiService {

/**
* Sets or updates the username for the current session.
* Logs in with the username and password
*
* @param username The request body containing the new username.
* @param usernamePasswordRequest The request body containing the username and password.
* @return A Retrofit {@link Response} wrapping a {@link GenericAuthResponse},
* indicating the success or failure of the operation.
*/
@POST("auth/username")
suspend fun setUsername(
@Body username: EditUsernameRequest,
@POST("auth/username-password")
suspend fun loginWithUsernamePassword(
@Body usernamePasswordRequest: LoginUsernamePasswordRequest,
): Response<GenericAuthResponse>

/**
* Sets or updates the password for the authenticated user.
*
* @param cookie The session cookie for authentication.
* @param password The request body containing the new password information.
* @return A Retrofit {@link Response} wrapping a {@link GenericAuthResponse},
* indicating the success or failure of the operation.
*/
@POST("auth/password")
suspend fun setPassword(
@Header("Cookie") cookie: String,
@Body password: PasswordRequest,
): Response<GenericAuthResponse>

/**
* Initiates a WebAuthn registration ceremony by requesting registration options
* from the server.
*
* @param cookie The session cookie for authentication.
* @param requestBody The request body, potentially containing user information
* or relying party details for the registration request.
* @return A Retrofit {@link Response} wrapping a {@link RegisterRequestResponse},
* which contains the challenge and options for the WebAuthn registration.
*/
@POST("webauthn/registerRequest")
suspend fun registerRequest(
@Header("Cookie") cookie: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@
package com.authentication.shrine.model

/**
* Represents the request body for setting or updating an existing username.
* This data class is typically used for serialization (e.g., with Gson or Kotlinx Serialization)
* when making API calls related to username management.
* Represents the request body for logging in with a username and password.
*
* @property username The username to be set or updated.
* @property username The username to log in with.
* @property password The password to log in with.
*/
data class EditUsernameRequest(

data class LoginUsernamePasswordRequest(
val username: String,
val password: String,
)

/**
Expand All @@ -38,14 +39,3 @@ data class RegisterUsernameRequest(
val username: String,
val displayName: String,
)

/**
* Represents the request body for setting or updating a password.
* This data class is typically used for serialization (e.g., with Gson or Kotlinx Serialization)
* when making API calls related to password management.
*
* @property password The new password.
*/
data class PasswordRequest(
val password: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,13 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import com.authentication.shrine.api.ApiException
import com.authentication.shrine.api.AuthApiService
import com.authentication.shrine.model.AuthError
import com.authentication.shrine.model.AuthResult
import com.authentication.shrine.model.CredmanResponse
import com.authentication.shrine.model.EditUsernameRequest
import com.authentication.shrine.model.FederationOptionsRequest
import com.authentication.shrine.model.LoginUsernamePasswordRequest
import com.authentication.shrine.model.PasskeysList
import com.authentication.shrine.model.PasswordRequest
import com.authentication.shrine.model.RegisterRequestRequestBody
import com.authentication.shrine.model.RegisterResponseRequestBody
import com.authentication.shrine.model.RegisterUsernameRequest
Expand Down Expand Up @@ -140,7 +138,7 @@ class AuthRepository @Inject constructor(
*/
suspend fun login(username: String, password: String): AuthResult<Unit> {
return try {
val response = authApiService.setUsername(EditUsernameRequest(username = username))
val response = authApiService.loginWithUsernamePassword(LoginUsernamePasswordRequest(username, password))
if (response.isSuccessful) {
dataStore.edit { prefs ->
prefs[USERNAME] = response.body()?.username.orEmpty()
Expand All @@ -149,7 +147,6 @@ class AuthRepository @Inject constructor(
prefs[SESSION_ID] = it
}
}
setSessionWithPassword(password)
AuthResult.Success(Unit)
} else {
if (response.code() == 401) {
Expand All @@ -166,45 +163,6 @@ class AuthRepository @Inject constructor(
}
}

/**
* Signs in with a password.
*
* @param password The password to use.
* @return True if the sign-in was successful, false otherwise.
*/
private suspend fun setSessionWithPassword(password: String): Boolean {
val username = dataStore.read(USERNAME)
val sessionId = dataStore.read(SESSION_ID)
if (!username.isNullOrEmpty() && !sessionId.isNullOrEmpty()) {
try {
val response = authApiService.setPassword(
cookie = sessionId.createCookieHeader(),
password = PasswordRequest(password = password),
)
if (response.isSuccessful) {
dataStore.edit { prefs ->
prefs[USERNAME] = response.body()?.username.orEmpty()
prefs[DISPLAYNAME] = response.body()?.displayName.orEmpty()
response.getSessionId()?.also {
prefs[SESSION_ID] = it
}
}
return true
} else if (response.code() == 401) {
signOut()
}
} catch (e: ApiException) {
Log.e(TAG, "Invalid login credentials", e)

// Remove previously stored credentials and start login over again
signOut()
}
} else {
Log.e(TAG, "Please check if username and session id is present in your datastore")
}
return false
}

/**
* Clears all the sign-in information.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import com.authentication.shrine.ui.common.ShrineLoader
import com.authentication.shrine.ui.theme.ShrineTheme
import com.authentication.shrine.ui.viewmodel.AuthenticationUiState
import com.authentication.shrine.ui.viewmodel.AuthenticationViewModel
import org.json.JSONObject

/**
* Stateful composable function for Authentication screen.
Expand All @@ -73,9 +74,9 @@ fun AuthenticationScreen(

// Passing in the lambda / context to the VM
val context = LocalContext.current
val createRestoreKey = {
val createRestoreKey: suspend () -> Unit = {
viewModel.createRestoreKey(
createRestoreKeyOnCredMan = { createRestoreCredObject ->
createRestoreKey = { createRestoreCredObject ->
credentialManagerUtils.createRestoreKey(
context = context,
requestResult = createRestoreCredObject,
Expand All @@ -86,9 +87,24 @@ fun AuthenticationScreen(

val onSignInWithPasskeyOrPasswordRequest = {
viewModel.signInWithPasskeyOrPasswordRequest(
onSuccess = { flag ->
createRestoreKey()
navigateToHome(flag)
onSuccess = { isPasswordCredential ->
var skipPasskeyPrompt = !isPasswordCredential
if (isPasswordCredential) {
val conditionalSuccess = viewModel.conditionalCreatePasskey(
createPasskey = { createPasskeyRequestObj: JSONObject ->
credentialManagerUtils.createPasskey(
requestResult = createPasskeyRequestObj,
context = context,
isConditional = true,
)
}
)
createRestoreKey()
if (conditionalSuccess) {
skipPasskeyPrompt = true
}
}
navigateToHome(skipPasskeyPrompt)
},
getCredential = { jsonObject ->
credentialManagerUtils.getPasskeyOrPasswordCredential(
Expand All @@ -102,9 +118,8 @@ fun AuthenticationScreen(
val onSignInWithSignInWithGoogleRequest = {
viewModel.signInWithGoogleRequest(
onSuccess = {
createRestoreKey()
// Don't suggest passkeys if user signs in with Google.
navigateToHome(false)
navigateToHome(true)
},
getCredential = {
credentialManagerUtils.getSignInWithGoogleCredential(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,23 +85,19 @@ fun RegisterPasswordScreen(
var passwordVisible by remember { mutableStateOf(false) }

val context = LocalContext.current
val createRestoreKey = {
viewModel.createRestoreKey(
createRestoreKeyOnCredMan = { createRestoreCredObject ->
credentialManagerUtils.createRestoreKey(
context = context,
requestResult = createRestoreCredObject,
)
},
)
}

val onRegister = { emailAddress: String, registrationPassword: String ->
viewModel.onPasswordRegister(
username = emailAddress,
password = registrationPassword,
onSuccess = { flag ->
createRestoreKey()
viewModel.createRestoreKey(
createRestoreKeyOnCredMan = { createRestoreCredObject ->
credentialManagerUtils.createRestoreKey(
context = context,
requestResult = createRestoreCredObject,
)
},
)
navigateToHome(flag)
},
createPassword = { username: String, password: String ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ fun RegisterScreen(
var email by remember { mutableStateOf("") }

val context = LocalContext.current
val createRestoreKey = {
val createRestoreKey: suspend () -> Unit = {
viewModel.createRestoreKey(
createRestoreKeyOnCredMan = { createRestoreCredObject ->
credentialManagerUtils.createRestoreKey(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ class ShrineNavActions(navController: NavHostController) {
// Takes user to Home flow.
val navigateToHome: (isSignInThroughPasskeys: Boolean) -> Unit = {
if (it) {
navController.navigate(ShrineAppDestinations.CreatePasskeyRoute.name) {
navController.navigate(ShrineAppDestinations.MainMenuRoute.name) {
popUpTo(ShrineAppDestinations.NavHostRoute.name)
launchSingleTop = true
}
} else {
navController.navigate(ShrineAppDestinations.MainMenuRoute.name) {
navController.navigate(ShrineAppDestinations.CreatePasskeyRoute.name) {
popUpTo(ShrineAppDestinations.NavHostRoute.name)
launchSingleTop = true
}
Expand Down
Loading