diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/AccountAuthenticator.java b/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/AccountAuthenticator.java index b73a9a683..213ab40b1 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/AccountAuthenticator.java +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/AccountAuthenticator.java @@ -54,6 +54,7 @@ import static eu.opencloud.android.data.authentication.AuthenticationConstantsKt.KEY_CLIENT_REGISTRATION_CLIENT_ID; import static eu.opencloud.android.data.authentication.AuthenticationConstantsKt.KEY_CLIENT_REGISTRATION_CLIENT_SECRET; import static eu.opencloud.android.data.authentication.AuthenticationConstantsKt.KEY_OAUTH2_REFRESH_TOKEN; +import static eu.opencloud.android.data.authentication.AuthenticationConstantsKt.KEY_OAUTH2_SCOPE; import static eu.opencloud.android.presentation.authentication.AuthenticatorConstants.KEY_AUTH_TOKEN_TYPE; import static org.koin.java.KoinJavaComponent.inject; @@ -386,7 +387,10 @@ private String refreshToken( clientAuth = OAuthUtils.Companion.getClientAuth(clientSecret, clientId); } - String scope = mContext.getResources().getString(R.string.oauth2_openid_scope); + String scope = accountManager.getUserData(account, KEY_OAUTH2_SCOPE); + if (scope == null) { + scope = mContext.getResources().getString(R.string.oauth2_openid_scope); + } TokenRequest oauthTokenRequest = new TokenRequest.RefreshToken( baseUrl, diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/LoginActivity.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/LoginActivity.kt index 7fd2800a2..da7c8cebe 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/LoginActivity.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/LoginActivity.kt @@ -48,8 +48,10 @@ import androidx.core.widget.doAfterTextChanged import eu.opencloud.android.BuildConfig import eu.opencloud.android.MainApp.Companion.accountType import eu.opencloud.android.R +import eu.opencloud.android.data.authentication.KEY_PREFERRED_USERNAME import eu.opencloud.android.data.authentication.KEY_USER_ID import eu.opencloud.android.databinding.AccountSetupBinding +import eu.opencloud.android.domain.authentication.oauth.model.ClientRegistrationInfo import eu.opencloud.android.domain.authentication.oauth.model.ResponseType import eu.opencloud.android.domain.authentication.oauth.model.TokenRequest import eu.opencloud.android.domain.exceptions.NoNetworkConnectionException @@ -98,6 +100,10 @@ private const val KEY_OIDC_SUPPORTED = "KEY_OIDC_SUPPORTED" private const val KEY_CODE_VERIFIER = "KEY_CODE_VERIFIER" private const val KEY_CODE_CHALLENGE = "KEY_CODE_CHALLENGE" private const val KEY_OIDC_STATE = "KEY_OIDC_STATE" +private const val KEY_AUTH_SERVER_BASE_URL = "KEY_AUTH_SERVER_BASE_URL" +private const val KEY_AUTH_OIDC_SUPPORTED = "KEY_AUTH_OIDC_SUPPORTED" +private const val KEY_AUTH_LOGIN_ACTION = "KEY_AUTH_LOGIN_ACTION" +private const val KEY_AUTH_USER_ACCOUNT = "KEY_AUTH_USER_ACCOUNT" class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrustedCertListener, SecurityEnforced { @@ -112,6 +118,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted private lateinit var serverBaseUrl: String private var oidcSupported = false + private var preferredUsername: String? = null private lateinit var binding: AccountSetupBinding @@ -172,10 +179,12 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted if (loginAction != ACTION_CREATE) { binding.accountUsername.isEnabled = false binding.accountUsername.isFocusable = false - userAccount?.name?.let { - username = getUsernameOfAccount(it) + userAccount?.let { account -> + // Prefer preferred_username from id_token (stored in AccountManager) for login_hint, + // fall back to the account name part (which may be a UUID) + username = AccountManager.get(this).getUserData(account, KEY_PREFERRED_USERNAME) + ?: getUsernameOfAccount(account.name) } - } if (savedInstanceState == null) { @@ -236,14 +245,19 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted if (savedInstanceState == null) { restoreAuthState() } - handleGetAuthorizationCodeResponse(intent) + if (authenticationViewModel.serverInfo.value?.peekContent()?.getStoredData() == null + && ::serverBaseUrl.isInitialized && serverBaseUrl.isNotEmpty()) { + // Process death: serverInfo is gone. Re-fetch it before processing the OAuth response. + // Store the intent as pending — getServerInfoIsSuccess will process it via checkServerType bypass. + pendingAuthorizationIntent = intent + authenticationViewModel.getServerInfo(serverBaseUrl) + } else { + handleGetAuthorizationCodeResponse(intent) + } } - // Process any pending intent that arrived before binding was ready - pendingAuthorizationIntent?.let { - handleGetAuthorizationCodeResponse(it) - pendingAuthorizationIntent = null - } + // Note: pendingAuthorizationIntent is processed in checkServerType() after + // getServerInfo() completes (process death recovery flow). } @@ -261,7 +275,10 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted private fun launchFileDisplayActivity() { val newIntent = Intent(this, FileDisplayActivity::class.java) - newIntent.data = intent.data + if (authenticationViewModel.launchedFromDeepLink) { + newIntent.data = intent.data + } + newIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) startActivity(newIntent) finish() } @@ -295,11 +312,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted authenticationViewModel.accountDiscovery.observe(this) { if (it.peekContent() is UIResult.Success) { notifyDocumentsProviderRoots(applicationContext) - if (authenticationViewModel.launchedFromDeepLink) { - launchFileDisplayActivity() - } else { - finish() - } + launchFileDisplayActivity() } else { binding.authStatusText.run { text = context.getString(R.string.login_account_preparing) @@ -424,6 +437,18 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted } private fun checkServerType(serverInfo: ServerInfo) { + // If we have a pending OAuth response (process death recovery), process it now + // that serverInfo has been re-fetched, instead of starting a new auth flow. + pendingAuthorizationIntent?.let { pendingIntent -> + pendingAuthorizationIntent = null + authTokenType = OAUTH_TOKEN_TYPE + if (serverInfo is ServerInfo.OIDCServer) { + oidcSupported = true + } + handleGetAuthorizationCodeResponse(pendingIntent) + return + } + when (serverInfo) { is ServerInfo.BasicServer -> { authTokenType = BASIC_TOKEN_TYPE @@ -447,14 +472,28 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted showOrHideBasicAuthFields(shouldBeVisible = false) authTokenType = OAUTH_TOKEN_TYPE oidcSupported = true - val registrationEndpoint = serverInfo.oidcServerConfiguration.registrationEndpoint - if (registrationEndpoint != null) { - registerClient( + val webFingerClientId = serverInfo.webFingerClientId + val webFingerScopes = serverInfo.webFingerScopes + if (webFingerClientId != null) { + performGetAuthorizationCodeRequest( authorizationEndpoint = serverInfo.oidcServerConfiguration.authorizationEndpoint.toUri(), - registrationEndpoint = registrationEndpoint + clientId = webFingerClientId, + webFingerScopes = webFingerScopes, ) } else { - performGetAuthorizationCodeRequest(serverInfo.oidcServerConfiguration.authorizationEndpoint.toUri()) + val registrationEndpoint = serverInfo.oidcServerConfiguration.registrationEndpoint + if (registrationEndpoint != null) { + registerClient( + authorizationEndpoint = serverInfo.oidcServerConfiguration.authorizationEndpoint.toUri(), + registrationEndpoint = registrationEndpoint, + webFingerScopes = webFingerScopes, + ) + } else { + performGetAuthorizationCodeRequest( + authorizationEndpoint = serverInfo.oidcServerConfiguration.authorizationEndpoint.toUri(), + webFingerScopes = webFingerScopes, + ) + } } } @@ -520,6 +559,12 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted resultBundle = intent.extras setResult(Activity.RESULT_OK, intent) + // Store preferred_username from id_token for login_hint on re-login + preferredUsername?.let { prefUsername -> + val account = Account(accountName, contextProvider.getString(R.string.account_type)) + AccountManager.get(this).setUserData(account, KEY_PREFERRED_USERNAME, prefUsername) + } + authenticationViewModel.discoverAccount(accountName = accountName, discoveryNeeded = loginAction == ACTION_CREATE) clearAuthState() } @@ -559,7 +604,8 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted */ private fun registerClient( authorizationEndpoint: Uri, - registrationEndpoint: String + registrationEndpoint: String, + webFingerScopes: List? = null, ) { authenticationViewModel.registerClient(registrationEndpoint) authenticationViewModel.registerClient.observe(this) { @@ -570,14 +616,15 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted uiResult.data?.let { clientRegistrationInfo -> performGetAuthorizationCodeRequest( authorizationEndpoint = authorizationEndpoint, - clientId = clientRegistrationInfo.clientId + clientId = clientRegistrationInfo.clientId, + webFingerScopes = webFingerScopes, ) } } is UIResult.Error -> { Timber.e(uiResult.error, "Client registration failed.") - performGetAuthorizationCodeRequest(authorizationEndpoint) + performGetAuthorizationCodeRequest(authorizationEndpoint, webFingerScopes = webFingerScopes) } } } @@ -585,7 +632,8 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted private fun performGetAuthorizationCodeRequest( authorizationEndpoint: Uri, - clientId: String = getString(R.string.oauth2_client_id) + clientId: String = getString(R.string.oauth2_client_id), + webFingerScopes: List? = null, ) { Timber.d("A browser should be opened now to authenticate this user.") @@ -597,12 +645,20 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted // which helps Firefox properly handle the OAuth redirect back to the app customTabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val scope = if (!oidcSupported) { + "" + } else if (webFingerScopes != null) { + webFingerScopes.joinToString(" ") + } else { + mdmProvider.getBrandingString(CONFIGURATION_OAUTH2_OPEN_ID_SCOPE, R.string.oauth2_openid_scope) + } + val authorizationEndpointUri = OAuthUtils.buildAuthorizationRequest( authorizationEndpoint = authorizationEndpoint, redirectUri = OAuthUtils.buildRedirectUri(applicationContext).toString(), clientId = clientId, responseType = ResponseType.CODE.string, - scope = if (oidcSupported) mdmProvider.getBrandingString(CONFIGURATION_OAUTH2_OPEN_ID_SCOPE, R.string.oauth2_openid_scope) else "", + scope = scope, prompt = if (oidcSupported) mdmProvider.getBrandingString(CONFIGURATION_OAUTH2_OPEN_ID_PROMPT, R.string.oauth2_openid_prompt) else "", codeChallenge = authenticationViewModel.codeChallenge, state = authenticationViewModel.oidcState, @@ -610,6 +666,8 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted sendLoginHintAndUser = mdmProvider.getBrandingBoolean(mdmKey = CONFIGURATION_SEND_LOGIN_HINT_AND_USER, booleanKey = R.bool.send_login_hint_and_user), ) + Timber.d("A browser should be opened now to authenticate this user is $authorizationEndpointUri ") + try { saveAuthState() @@ -673,6 +731,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted val (clientId, clientSecret) = if (clientRegistrationInfo?.clientId != null && clientRegistrationInfo.clientSecret != null) { Pair(clientRegistrationInfo.clientId, clientRegistrationInfo.clientSecret as String) } else { + // May be overridden below by serverInfo.webFingerClientId if available Pair(getString(R.string.oauth2_client_id), getString(R.string.oauth2_client_secret)) } @@ -684,18 +743,21 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted if (serverInfo is ServerInfo.OIDCServer) { tokenEndPoint = serverInfo.oidcServerConfiguration.tokenEndpoint + // Use webfinger-provided clientId if available, otherwise keep registration/default + val effectiveClientId = serverInfo.webFingerClientId ?: clientId + // RFC 7636: Public clients (token_endpoint_auth_method: none) must not send Authorization header if (serverInfo.oidcServerConfiguration.isTokenEndpointAuthMethodNone()) { clientAuth = "" - clientIdForRequest = clientId + clientIdForRequest = effectiveClientId } else if (serverInfo.oidcServerConfiguration.isTokenEndpointAuthMethodSupportedClientSecretPost()) { // For client_secret_post, credentials go in body, not Authorization header clientAuth = "" - clientIdForRequest = clientId + clientIdForRequest = effectiveClientId clientSecretForRequest = clientSecret } else { // For other methods (e.g., client_secret_basic), use Basic auth header - clientAuth = OAuthUtils.getClientAuth(clientSecret, clientId) + clientAuth = OAuthUtils.getClientAuth(clientSecret, effectiveClientId) } } else { tokenEndPoint = "$serverBaseUrl${File.separator}${contextProvider.getString(R.string.oauth2_url_endpoint_access)}" @@ -725,18 +787,45 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted Timber.d("Tokens received ${uiResult.data}, trying to login, creating account and adding it to account manager") val tokenResponse = uiResult.data ?: return@observe + // Extract preferred_username from id_token for login_hint on re-login + preferredUsername = extractPreferredUsernameFromIdToken(tokenResponse.idToken) + Timber.d("Preferred username from id_token: $preferredUsername") + + // When webfinger provides a client_id without dynamic registration, + // store it so AccountAuthenticator can use it for token refresh + val effectiveClientRegistrationInfo = clientRegistrationInfo + ?: (serverInfo as? ServerInfo.OIDCServer)?.webFingerClientId?.let { wfClientId -> + ClientRegistrationInfo( + clientId = wfClientId, + clientSecret = null, + clientIdIssuedAt = null, + clientSecretExpiration = 0, + ) + } + + // Scope priority: webfinger scopes > MDM/string-resource > token response + val webFingerScopes = if (serverInfo is ServerInfo.OIDCServer) { + serverInfo.webFingerScopes + } else { + null + } + val effectiveScope = if (!oidcSupported) { + tokenResponse.scope + } else if (webFingerScopes != null) { + webFingerScopes.joinToString(" ") + } else { + mdmProvider.getBrandingString(CONFIGURATION_OAUTH2_OPEN_ID_SCOPE, R.string.oauth2_openid_scope) + } + authenticationViewModel.loginOAuth( serverBaseUrl = serverBaseUrl, username = tokenResponse.additionalParameters?.get(KEY_USER_ID).orEmpty(), authTokenType = OAUTH_TOKEN_TYPE, accessToken = tokenResponse.accessToken, refreshToken = tokenResponse.refreshToken.orEmpty(), - scope = if (oidcSupported) mdmProvider.getBrandingString( - CONFIGURATION_OAUTH2_OPEN_ID_SCOPE, - R.string.oauth2_openid_scope, - ) else tokenResponse.scope, + scope = effectiveScope, updateAccountWithUsername = if (loginAction != ACTION_CREATE) userAccount?.name else null, - clientRegistrationInfo = clientRegistrationInfo + clientRegistrationInfo = effectiveClientRegistrationInfo ) } @@ -961,6 +1050,15 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted putString(KEY_CODE_VERIFIER, authenticationViewModel.codeVerifier) putString(KEY_CODE_CHALLENGE, authenticationViewModel.codeChallenge) putString(KEY_OIDC_STATE, authenticationViewModel.oidcState) + if (::serverBaseUrl.isInitialized) { + putString(KEY_AUTH_SERVER_BASE_URL, serverBaseUrl) + } + putBoolean(KEY_AUTH_OIDC_SUPPORTED, oidcSupported) + // The browser is also opened for re-login (ACTION_UPDATE_TOKEN / ACTION_UPDATE_EXPIRED_TOKEN), + // not just for new account creation. Persist these so the redirect back knows + // whether to update an existing account or create a new one. + putInt(KEY_AUTH_LOGIN_ACTION, loginAction.toInt()) + userAccount?.name?.let { putString(KEY_AUTH_USER_ACCOUNT, it) } apply() } } @@ -970,10 +1068,41 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted prefs.getString(KEY_CODE_VERIFIER, null)?.let { authenticationViewModel.codeVerifier = it } prefs.getString(KEY_CODE_CHALLENGE, null)?.let { authenticationViewModel.codeChallenge = it } prefs.getString(KEY_OIDC_STATE, null)?.let { authenticationViewModel.oidcState = it } + prefs.getString(KEY_AUTH_SERVER_BASE_URL, null)?.let { serverBaseUrl = it } + oidcSupported = prefs.getBoolean(KEY_AUTH_OIDC_SUPPORTED, false) + val savedLoginAction = prefs.getInt(KEY_AUTH_LOGIN_ACTION, -1) + if (savedLoginAction != -1) { + loginAction = savedLoginAction.toByte() + } + if (userAccount == null) { + prefs.getString(KEY_AUTH_USER_ACCOUNT, null)?.let { accountName -> + userAccount = Account(accountName, getString(R.string.account_type)) + } + } } private fun clearAuthState() { val prefs = getSharedPreferences("auth_state", android.content.Context.MODE_PRIVATE) prefs.edit().clear().apply() } + + /** + * Extract preferred_username from an OIDC id_token JWT. + * Fallback chain: preferred_username -> email -> sub + */ + private fun extractPreferredUsernameFromIdToken(idToken: String?): String? { + if (idToken == null) return null + return try { + val parts = idToken.split(".") + if (parts.size != 3) return null + val payload = String(android.util.Base64.decode(parts[1], android.util.Base64.URL_SAFE)) + val json = org.json.JSONObject(payload) + json.optString("preferred_username").ifBlank { null } + ?: json.optString("email").ifBlank { null } + ?: json.optString("sub").ifBlank { null } + } catch (e: Exception) { + Timber.e(e, "Failed to extract preferred_username from id_token") + null + } + } } diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/responses/TokenResponse.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/responses/TokenResponse.kt index 2dd479896..313d0c064 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/responses/TokenResponse.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/responses/TokenResponse.kt @@ -40,6 +40,8 @@ data class TokenResponse( @Json(name = "user_id") val userId: String?, val scope: String?, + @Json(name = "id_token") + val idToken: String? = null, @Json(name = "additional_parameters") val additionalParameters: Map? ) diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/webfinger/GetInstancesViaWebFingerOperation.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/webfinger/GetInstancesViaWebFingerOperation.kt index ba69ded9a..1db52524a 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/webfinger/GetInstancesViaWebFingerOperation.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/webfinger/GetInstancesViaWebFingerOperation.kt @@ -40,13 +40,15 @@ class GetInstancesViaWebFingerOperation( private val lockupServerDomain: String, private val rel: String, private val resource: String, -) : RemoteOperation>() { + private val platform: String? = null, +) : RemoteOperation() { private fun buildRequestUri() = Uri.parse(lockupServerDomain).buildUpon() .path(ENDPOINT_WEBFINGER_PATH) .appendQueryParameter("rel", rel) .appendQueryParameter("resource", resource) + .apply { platform?.let { appendQueryParameter("platform", it) } } .build() private fun isSuccess(status: Int): Boolean = status == HttpConstants.HTTP_OK @@ -61,26 +63,27 @@ class GetInstancesViaWebFingerOperation( method: HttpMethod, response: String?, status: Int - ): RemoteOperationResult> { + ): RemoteOperationResult { Timber.e("Failed requesting WebFinger info") if (response != null) { Timber.e("*** status code: $status; response message: $response") } else { Timber.e("*** status code: $status") } - return RemoteOperationResult>(method) + return RemoteOperationResult(method) } - private fun onRequestSuccessful(rawResponse: String): RemoteOperationResult> { + private fun onRequestSuccessful(rawResponse: String): RemoteOperationResult { val response = parseResponse(rawResponse) Timber.d("Successful WebFinger request: $response") - val operationResult = RemoteOperationResult>(RemoteOperationResult.ResultCode.OK) - operationResult.data = response.links?.map { it.href } ?: listOf() + val operationResult = RemoteOperationResult(RemoteOperationResult.ResultCode.OK) + operationResult.data = response return operationResult } - override fun run(client: OpenCloudClient): RemoteOperationResult> { + override fun run(client: OpenCloudClient): RemoteOperationResult { val requestUri = buildRequestUri() + Timber.d("Doing WebFinger request: $requestUri") val getMethod = GetMethod(URL(requestUri.toString())) // First iteration won't follow redirections. @@ -88,7 +91,7 @@ class GetInstancesViaWebFingerOperation( return try { val status = client.executeHttpMethod(getMethod) - val response = getMethod.getResponseBodyAsString()!! + val response = getMethod.getResponseBodyAsString() if (isSuccess(status)) { onRequestSuccessful(response) @@ -97,7 +100,7 @@ class GetInstancesViaWebFingerOperation( } } catch (e: Exception) { Timber.e(e, "Requesting WebFinger info failed") - RemoteOperationResult>(e) + RemoteOperationResult(e) } } diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/webfinger/responses/WebFingerResponse.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/webfinger/responses/WebFingerResponse.kt index b344bf0a7..1fb4efb11 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/webfinger/responses/WebFingerResponse.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/webfinger/responses/WebFingerResponse.kt @@ -24,12 +24,20 @@ package eu.opencloud.android.lib.resources.webfinger.responses +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) +data class WebFingerProperties( + @Json(name = "http://opencloud.eu/ns/oidc/client_id") val clientId: String?, + @Json(name = "http://opencloud.eu/ns/oidc/scopes") val scopes: List?, +) + @JsonClass(generateAdapter = true) data class WebFingerResponse( val subject: String, - val links: List? + val links: List?, + val properties: WebFingerProperties? = null, ) @JsonClass(generateAdapter = true) diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/webfinger/services/WebFingerService.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/webfinger/services/WebFingerService.kt index fe1639754..0af520b81 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/webfinger/services/WebFingerService.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/webfinger/services/WebFingerService.kt @@ -19,6 +19,7 @@ package eu.opencloud.android.lib.resources.webfinger.services import eu.opencloud.android.lib.common.OpenCloudClient import eu.opencloud.android.lib.common.operations.RemoteOperationResult +import eu.opencloud.android.lib.resources.webfinger.responses.WebFingerResponse interface WebFingerService { fun getInstancesFromWebFinger( @@ -27,4 +28,12 @@ interface WebFingerService { rel: String, client: OpenCloudClient, ): RemoteOperationResult> + + fun getOidcDiscoveryFromWebFinger( + lookupServer: String, + resource: String, + rel: String, + platform: String, + client: OpenCloudClient, + ): RemoteOperationResult } diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/webfinger/services/implementation/OCWebFingerService.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/webfinger/services/implementation/OCWebFingerService.kt index 6bf422787..fcb9e54e1 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/webfinger/services/implementation/OCWebFingerService.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/webfinger/services/implementation/OCWebFingerService.kt @@ -20,6 +20,7 @@ package eu.opencloud.android.lib.resources.webfinger.services.implementation import eu.opencloud.android.lib.common.OpenCloudClient import eu.opencloud.android.lib.common.operations.RemoteOperationResult import eu.opencloud.android.lib.resources.webfinger.GetInstancesViaWebFingerOperation +import eu.opencloud.android.lib.resources.webfinger.responses.WebFingerResponse import eu.opencloud.android.lib.resources.webfinger.services.WebFingerService class OCWebFingerService : WebFingerService { @@ -29,6 +30,23 @@ class OCWebFingerService : WebFingerService { resource: String, rel: String, client: OpenCloudClient, - ): RemoteOperationResult> = - GetInstancesViaWebFingerOperation(lookupServer, rel, resource).execute(client) + ): RemoteOperationResult> { + val result = GetInstancesViaWebFingerOperation(lockupServerDomain = lookupServer, rel = rel, resource = resource).execute(client) + if (!result.isSuccess) { + @Suppress("UNCHECKED_CAST") + return result as RemoteOperationResult> + } + val listResult = RemoteOperationResult>(RemoteOperationResult.ResultCode.OK) + listResult.data = result.data.links?.map { it.href } ?: listOf() + return listResult + } + + override fun getOidcDiscoveryFromWebFinger( + lookupServer: String, + resource: String, + rel: String, + platform: String, + client: OpenCloudClient, + ): RemoteOperationResult = + GetInstancesViaWebFingerOperation(lockupServerDomain = lookupServer, rel = rel, resource = resource, platform = platform).execute(client) } diff --git a/opencloudData/src/main/java/eu/opencloud/android/data/authentication/AuthenticationConstants.kt b/opencloudData/src/main/java/eu/opencloud/android/data/authentication/AuthenticationConstants.kt index 40e27ad8d..1700dc134 100644 --- a/opencloudData/src/main/java/eu/opencloud/android/data/authentication/AuthenticationConstants.kt +++ b/opencloudData/src/main/java/eu/opencloud/android/data/authentication/AuthenticationConstants.kt @@ -47,6 +47,11 @@ const val KEY_FEATURE_SPACES = "KEY_FEATURE_SPACES" /** * OIDC Client Registration */ +/** + * Preferred username from OIDC id_token, used for login_hint + */ +const val KEY_PREFERRED_USERNAME = "oc_preferred_username" + const val KEY_CLIENT_REGISTRATION_CLIENT_ID = "client_id" const val KEY_CLIENT_REGISTRATION_CLIENT_SECRET = "client_secret" const val KEY_CLIENT_REGISTRATION_CLIENT_EXPIRATION_DATE = "client_secret_expires_at" diff --git a/opencloudData/src/main/java/eu/opencloud/android/data/oauth/datasources/implementation/OCRemoteOAuthDataSource.kt b/opencloudData/src/main/java/eu/opencloud/android/data/oauth/datasources/implementation/OCRemoteOAuthDataSource.kt index f51614acb..d7cf5f7ca 100644 --- a/opencloudData/src/main/java/eu/opencloud/android/data/oauth/datasources/implementation/OCRemoteOAuthDataSource.kt +++ b/opencloudData/src/main/java/eu/opencloud/android/data/oauth/datasources/implementation/OCRemoteOAuthDataSource.kt @@ -131,6 +131,7 @@ class OCRemoteOAuthDataSource( tokenType = this.tokenType, userId = this.userId, scope = this.scope, + idToken = this.idToken, additionalParameters = this.additionalParameters ) diff --git a/opencloudData/src/main/java/eu/opencloud/android/data/server/repository/OCServerInfoRepository.kt b/opencloudData/src/main/java/eu/opencloud/android/data/server/repository/OCServerInfoRepository.kt index f2e267769..46d48f441 100644 --- a/opencloudData/src/main/java/eu/opencloud/android/data/server/repository/OCServerInfoRepository.kt +++ b/opencloudData/src/main/java/eu/opencloud/android/data/server/repository/OCServerInfoRepository.kt @@ -26,6 +26,7 @@ import eu.opencloud.android.data.server.datasources.RemoteServerInfoDataSource import eu.opencloud.android.data.webfinger.datasources.RemoteWebFingerDataSource import eu.opencloud.android.domain.server.ServerInfoRepository import eu.opencloud.android.domain.server.model.ServerInfo +import eu.opencloud.android.domain.webfinger.model.WebFingerOidcInfo import eu.opencloud.android.domain.webfinger.model.WebFingerRel import timber.log.Timber @@ -36,25 +37,23 @@ class OCServerInfoRepository( ) : ServerInfoRepository { override fun getServerInfo(path: String, creatingAccount: Boolean, enforceOIDC: Boolean): ServerInfo { - val oidcIssuerFromWebFinger: String? = if (creatingAccount) retrieveOIDCIssuerFromWebFinger(serverUrl = path) else null - - if (oidcIssuerFromWebFinger != null) { - val openIDConnectServerConfiguration = oidcRemoteOAuthDataSource.performOIDCDiscovery(oidcIssuerFromWebFinger) - return ServerInfo.OIDCServer( - openCloudVersion = "10.12", - baseUrl = path, - oidcServerConfiguration = openIDConnectServerConfiguration - ) - } + // Try webfinger first to get OIDC client config (client_id, scopes) and issuer. + // This is a lightweight call that returns null if webfinger is not available. + val oidcInfoFromWebFinger: WebFingerOidcInfo? = retrieveOidcInfoFromWebFinger(serverUrl = path) + // Always check server status to get the proper baseUrl and version. + // We must not skip this, because the server may normalize/redirect the URL, + // and the account name is built from baseUrl + username. val serverInfo = remoteServerInfoDataSource.getServerInfo(path, enforceOIDC) return if (serverInfo is ServerInfo.BasicServer) { serverInfo } else { - // Could be OAuth or OpenID Connect + // OIDC discovery: prefer the webfinger issuer if available (it may differ from baseUrl), + // otherwise discover from the server's baseUrl. + val oidcDiscoveryUrl = oidcInfoFromWebFinger?.issuer ?: serverInfo.baseUrl val openIDConnectServerConfiguration = try { - oidcRemoteOAuthDataSource.performOIDCDiscovery(serverInfo.baseUrl) + oidcRemoteOAuthDataSource.performOIDCDiscovery(oidcDiscoveryUrl) } catch (exception: Exception) { Timber.d(exception, "OIDC discovery not found") null @@ -64,7 +63,9 @@ class OCServerInfoRepository( ServerInfo.OIDCServer( openCloudVersion = serverInfo.openCloudVersion, baseUrl = serverInfo.baseUrl, - oidcServerConfiguration = openIDConnectServerConfiguration + oidcServerConfiguration = openIDConnectServerConfiguration, + webFingerClientId = oidcInfoFromWebFinger?.clientId, + webFingerScopes = oidcInfoFromWebFinger?.scopes, ) } else { ServerInfo.OAuth2Server( @@ -75,20 +76,16 @@ class OCServerInfoRepository( } } - private fun retrieveOIDCIssuerFromWebFinger( + private fun retrieveOidcInfoFromWebFinger( serverUrl: String, - ): String? { - val oidcIssuer = try { - webFingerDatasource.getInstancesFromWebFinger( - lookupServer = serverUrl, - rel = WebFingerRel.OIDC_ISSUER_DISCOVERY, - resource = serverUrl, - ).firstOrNull() - } catch (exception: Exception) { - Timber.d(exception, "Cant retrieve the oidc issuer from webfinger.") - null - } - - return oidcIssuer + ): WebFingerOidcInfo? = try { + webFingerDatasource.getOidcInfoFromWebFinger( + lookupServer = serverUrl, + rel = WebFingerRel.OIDC_ISSUER_DISCOVERY, + resource = serverUrl, + ) + } catch (exception: Exception) { + Timber.d(exception, "Cant retrieve the oidc info from webfinger.") + null } } diff --git a/opencloudData/src/main/java/eu/opencloud/android/data/webfinger/datasources/RemoteWebFingerDataSource.kt b/opencloudData/src/main/java/eu/opencloud/android/data/webfinger/datasources/RemoteWebFingerDataSource.kt index 75ab7d435..fabfb0687 100644 --- a/opencloudData/src/main/java/eu/opencloud/android/data/webfinger/datasources/RemoteWebFingerDataSource.kt +++ b/opencloudData/src/main/java/eu/opencloud/android/data/webfinger/datasources/RemoteWebFingerDataSource.kt @@ -17,9 +17,12 @@ */ package eu.opencloud.android.data.webfinger.datasources +import eu.opencloud.android.domain.webfinger.model.WebFingerOidcInfo import eu.opencloud.android.domain.webfinger.model.WebFingerRel interface RemoteWebFingerDataSource { + // Used with OPENCLOUD_INSTANCE rel: routes a user to the correct openCloud server instance + // when the user knows their username but not which server they are on (MDM-configured lookup server). fun getInstancesFromWebFinger( lookupServer: String, rel: WebFingerRel, @@ -33,4 +36,12 @@ interface RemoteWebFingerDataSource { username: String, accessToken: String, ): List + + // Used with OIDC_ISSUER_DISCOVERY rel: fetches the OIDC issuer URL and any server-advertised + // OIDC client config (client_id, scopes) so the app does not need hardcoded or MDM-configured values. + fun getOidcInfoFromWebFinger( + lookupServer: String, + rel: WebFingerRel, + resource: String, + ): WebFingerOidcInfo? } diff --git a/opencloudData/src/main/java/eu/opencloud/android/data/webfinger/datasources/implementation/OCRemoteWebFingerDataSource.kt b/opencloudData/src/main/java/eu/opencloud/android/data/webfinger/datasources/implementation/OCRemoteWebFingerDataSource.kt index d9466b778..67d7f5c84 100644 --- a/opencloudData/src/main/java/eu/opencloud/android/data/webfinger/datasources/implementation/OCRemoteWebFingerDataSource.kt +++ b/opencloudData/src/main/java/eu/opencloud/android/data/webfinger/datasources/implementation/OCRemoteWebFingerDataSource.kt @@ -20,6 +20,7 @@ package eu.opencloud.android.data.webfinger.datasources.implementation import eu.opencloud.android.data.ClientManager import eu.opencloud.android.data.executeRemoteOperation import eu.opencloud.android.data.webfinger.datasources.RemoteWebFingerDataSource +import eu.opencloud.android.domain.webfinger.model.WebFingerOidcInfo import eu.opencloud.android.domain.webfinger.model.WebFingerRel import eu.opencloud.android.lib.common.OpenCloudClient import eu.opencloud.android.lib.common.authentication.OpenCloudCredentialsFactory @@ -71,4 +72,27 @@ class OCRemoteWebFingerDataSource( ) } } + + override fun getOidcInfoFromWebFinger( + lookupServer: String, + rel: WebFingerRel, + resource: String, + ): WebFingerOidcInfo? { + val openCloudClient = clientManager.getClientForAnonymousCredentials(lookupServer, false) + val result = webFingerService.getOidcDiscoveryFromWebFinger( + lookupServer = lookupServer, + resource = resource, + rel = rel.uri, + platform = "android", + client = openCloudClient + ) + if (!result.isSuccess) return null + val response = result.data ?: return null + val issuer = response.links?.firstOrNull { it.rel == rel.uri }?.href ?: return null + return WebFingerOidcInfo( + issuer = issuer, + clientId = response.properties?.clientId, + scopes = response.properties?.scopes, + ) + } } diff --git a/opencloudData/src/test/java/eu/opencloud/android/data/server/repository/OCServerInfoRepositoryTest.kt b/opencloudData/src/test/java/eu/opencloud/android/data/server/repository/OCServerInfoRepositoryTest.kt index 349f261ca..8f765bb42 100644 --- a/opencloudData/src/test/java/eu/opencloud/android/data/server/repository/OCServerInfoRepositoryTest.kt +++ b/opencloudData/src/test/java/eu/opencloud/android/data/server/repository/OCServerInfoRepositoryTest.kt @@ -25,12 +25,17 @@ package eu.opencloud.android.data.server.repository import eu.opencloud.android.data.oauth.datasources.RemoteOAuthDataSource import eu.opencloud.android.data.server.datasources.RemoteServerInfoDataSource import eu.opencloud.android.data.webfinger.datasources.RemoteWebFingerDataSource +import eu.opencloud.android.domain.server.model.ServerInfo +import eu.opencloud.android.domain.webfinger.model.WebFingerOidcInfo import eu.opencloud.android.domain.webfinger.model.WebFingerRel import eu.opencloud.android.testutil.OC_SECURE_SERVER_INFO_BASIC_AUTH import eu.opencloud.android.testutil.OC_SECURE_SERVER_INFO_BEARER_AUTH import eu.opencloud.android.testutil.OC_SECURE_SERVER_INFO_OIDC_AUTH import eu.opencloud.android.testutil.OC_SECURE_SERVER_INFO_OIDC_AUTH_WEBFINGER_INSTANCE +import eu.opencloud.android.testutil.OC_SECURE_SERVER_INFO_OIDC_AUTH_WEBFINGER_INSTANCE_WITH_CLIENT +import eu.opencloud.android.testutil.OC_WEBFINGER_CLIENT_ID import eu.opencloud.android.testutil.OC_WEBFINGER_INSTANCE_URL +import eu.opencloud.android.testutil.OC_WEBFINGER_SCOPES import eu.opencloud.android.testutil.oauth.OC_OIDC_SERVER_CONFIGURATION import io.mockk.every import io.mockk.mockk @@ -66,7 +71,7 @@ class OCServerInfoRepositoryTest { @Test fun `getServerInfo returns a BasicServer when creatingAccount parameter is true and webfinger datasource throws an exception`() { every { - remoteWebFingerDataSource.getInstancesFromWebFinger( + remoteWebFingerDataSource.getOidcInfoFromWebFinger( lookupServer = OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl, rel = WebFingerRel.OIDC_ISSUER_DISCOVERY, resource = OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl @@ -85,7 +90,7 @@ class OCServerInfoRepositoryTest { assertEquals(OC_SECURE_SERVER_INFO_BASIC_AUTH, basicServer) verify(exactly = 1) { - remoteWebFingerDataSource.getInstancesFromWebFinger( + remoteWebFingerDataSource.getOidcInfoFromWebFinger( lookupServer = OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl, rel = WebFingerRel.OIDC_ISSUER_DISCOVERY, resource = OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl @@ -120,7 +125,7 @@ class OCServerInfoRepositoryTest { @Test fun `getServerInfo returns an OAuth2Server when creatingAccount parameter is true and webfinger datasource throws an exception`() { every { - remoteWebFingerDataSource.getInstancesFromWebFinger( + remoteWebFingerDataSource.getOidcInfoFromWebFinger( lookupServer = OC_SECURE_SERVER_INFO_BEARER_AUTH.baseUrl, rel = WebFingerRel.OIDC_ISSUER_DISCOVERY, resource = OC_SECURE_SERVER_INFO_BEARER_AUTH.baseUrl @@ -143,7 +148,7 @@ class OCServerInfoRepositoryTest { assertEquals(OC_SECURE_SERVER_INFO_BEARER_AUTH, oAuthServer) verify(exactly = 1) { - remoteWebFingerDataSource.getInstancesFromWebFinger( + remoteWebFingerDataSource.getOidcInfoFromWebFinger( lookupServer = OC_SECURE_SERVER_INFO_BEARER_AUTH.baseUrl, rel = WebFingerRel.OIDC_ISSUER_DISCOVERY, resource = OC_SECURE_SERVER_INFO_BEARER_AUTH.baseUrl @@ -179,7 +184,7 @@ class OCServerInfoRepositoryTest { @Test fun `getServerInfo returns an OIDCServer when creatingAccount parameter is true and webfinger datasource throws an exception`() { every { - remoteWebFingerDataSource.getInstancesFromWebFinger( + remoteWebFingerDataSource.getOidcInfoFromWebFinger( lookupServer = OC_SECURE_SERVER_INFO_OIDC_AUTH.baseUrl, rel = WebFingerRel.OIDC_ISSUER_DISCOVERY, resource = OC_SECURE_SERVER_INFO_OIDC_AUTH.baseUrl @@ -202,7 +207,7 @@ class OCServerInfoRepositoryTest { assertEquals(OC_SECURE_SERVER_INFO_OIDC_AUTH, oIDCServer) verify(exactly = 1) { - remoteWebFingerDataSource.getInstancesFromWebFinger( + remoteWebFingerDataSource.getOidcInfoFromWebFinger( lookupServer = OC_SECURE_SERVER_INFO_OIDC_AUTH.baseUrl, rel = WebFingerRel.OIDC_ISSUER_DISCOVERY, resource = OC_SECURE_SERVER_INFO_OIDC_AUTH.baseUrl @@ -215,12 +220,16 @@ class OCServerInfoRepositoryTest { @Test fun `getServerInfo returns an OIDCServer when creatingAccount is true and webfinger datasource returns an OIDC issuer`() { every { - remoteWebFingerDataSource.getInstancesFromWebFinger( + remoteWebFingerDataSource.getOidcInfoFromWebFinger( lookupServer = OC_SECURE_SERVER_INFO_OIDC_AUTH_WEBFINGER_INSTANCE.baseUrl, rel = WebFingerRel.OIDC_ISSUER_DISCOVERY, resource = OC_SECURE_SERVER_INFO_OIDC_AUTH_WEBFINGER_INSTANCE.baseUrl ) - } returns listOf(OC_WEBFINGER_INSTANCE_URL) + } returns WebFingerOidcInfo(issuer = OC_WEBFINGER_INSTANCE_URL, clientId = null, scopes = null) + + every { + remoteServerInfoDataSource.getServerInfo(OC_SECURE_SERVER_INFO_OIDC_AUTH_WEBFINGER_INSTANCE.baseUrl, false) + } returns ServerInfo.OAuth2Server(baseUrl = OC_SECURE_SERVER_INFO_OIDC_AUTH_WEBFINGER_INSTANCE.baseUrl, openCloudVersion = "10.12") every { remoteOAuthDataSource.performOIDCDiscovery(OC_WEBFINGER_INSTANCE_URL) @@ -234,7 +243,7 @@ class OCServerInfoRepositoryTest { assertEquals(OC_SECURE_SERVER_INFO_OIDC_AUTH_WEBFINGER_INSTANCE, oIDCServerWebfinger) verify(exactly = 1) { - remoteWebFingerDataSource.getInstancesFromWebFinger( + remoteWebFingerDataSource.getOidcInfoFromWebFinger( lookupServer = OC_SECURE_SERVER_INFO_OIDC_AUTH_WEBFINGER_INSTANCE.baseUrl, rel = WebFingerRel.OIDC_ISSUER_DISCOVERY, resource = OC_SECURE_SERVER_INFO_OIDC_AUTH_WEBFINGER_INSTANCE.baseUrl @@ -242,4 +251,42 @@ class OCServerInfoRepositoryTest { remoteOAuthDataSource.performOIDCDiscovery(OC_WEBFINGER_INSTANCE_URL) } } + + @Test + fun `getServerInfo returns an OIDCServer with webfinger client id and scopes when webfinger provides them`() { + every { + remoteWebFingerDataSource.getOidcInfoFromWebFinger( + lookupServer = OC_SECURE_SERVER_INFO_OIDC_AUTH_WEBFINGER_INSTANCE_WITH_CLIENT.baseUrl, + rel = WebFingerRel.OIDC_ISSUER_DISCOVERY, + resource = OC_SECURE_SERVER_INFO_OIDC_AUTH_WEBFINGER_INSTANCE_WITH_CLIENT.baseUrl + ) + } returns WebFingerOidcInfo(issuer = OC_WEBFINGER_INSTANCE_URL, clientId = OC_WEBFINGER_CLIENT_ID, scopes = OC_WEBFINGER_SCOPES) + + every { + remoteServerInfoDataSource.getServerInfo(OC_SECURE_SERVER_INFO_OIDC_AUTH_WEBFINGER_INSTANCE_WITH_CLIENT.baseUrl, false) + } returns ServerInfo.OAuth2Server( + baseUrl = OC_SECURE_SERVER_INFO_OIDC_AUTH_WEBFINGER_INSTANCE_WITH_CLIENT.baseUrl, + openCloudVersion = "10.12", + ) + + every { + remoteOAuthDataSource.performOIDCDiscovery(OC_WEBFINGER_INSTANCE_URL) + } returns OC_OIDC_SERVER_CONFIGURATION + + val result = ocServerInfoRepository.getServerInfo( + path = OC_SECURE_SERVER_INFO_OIDC_AUTH_WEBFINGER_INSTANCE_WITH_CLIENT.baseUrl, + creatingAccount = true, + enforceOIDC = false + ) + assertEquals(OC_SECURE_SERVER_INFO_OIDC_AUTH_WEBFINGER_INSTANCE_WITH_CLIENT, result) + + verify(exactly = 1) { + remoteWebFingerDataSource.getOidcInfoFromWebFinger( + lookupServer = OC_SECURE_SERVER_INFO_OIDC_AUTH_WEBFINGER_INSTANCE_WITH_CLIENT.baseUrl, + rel = WebFingerRel.OIDC_ISSUER_DISCOVERY, + resource = OC_SECURE_SERVER_INFO_OIDC_AUTH_WEBFINGER_INSTANCE_WITH_CLIENT.baseUrl + ) + remoteOAuthDataSource.performOIDCDiscovery(OC_WEBFINGER_INSTANCE_URL) + } + } } diff --git a/opencloudDomain/src/main/java/eu/opencloud/android/domain/authentication/oauth/model/TokenResponse.kt b/opencloudDomain/src/main/java/eu/opencloud/android/domain/authentication/oauth/model/TokenResponse.kt index 2103aa001..44dcad9fb 100644 --- a/opencloudDomain/src/main/java/eu/opencloud/android/domain/authentication/oauth/model/TokenResponse.kt +++ b/opencloudDomain/src/main/java/eu/opencloud/android/domain/authentication/oauth/model/TokenResponse.kt @@ -26,5 +26,6 @@ data class TokenResponse( val tokenType: String, val userId: String?, val scope: String?, + val idToken: String? = null, val additionalParameters: Map? ) diff --git a/opencloudDomain/src/main/java/eu/opencloud/android/domain/server/model/ServerInfo.kt b/opencloudDomain/src/main/java/eu/opencloud/android/domain/server/model/ServerInfo.kt index 90189498e..ea0f20160 100644 --- a/opencloudDomain/src/main/java/eu/opencloud/android/domain/server/model/ServerInfo.kt +++ b/opencloudDomain/src/main/java/eu/opencloud/android/domain/server/model/ServerInfo.kt @@ -43,6 +43,8 @@ sealed class ServerInfo( openCloudVersion: String, baseUrl: String, val oidcServerConfiguration: OIDCServerConfiguration, + val webFingerClientId: String? = null, + val webFingerScopes: List? = null, ) : ServerInfo(openCloudVersion = openCloudVersion, baseUrl = baseUrl) { override fun equals(other: Any?): Boolean { if (other !is OIDCServer) return false diff --git a/opencloudDomain/src/main/java/eu/opencloud/android/domain/webfinger/model/WebFingerOidcInfo.kt b/opencloudDomain/src/main/java/eu/opencloud/android/domain/webfinger/model/WebFingerOidcInfo.kt new file mode 100644 index 000000000..f4baae574 --- /dev/null +++ b/opencloudDomain/src/main/java/eu/opencloud/android/domain/webfinger/model/WebFingerOidcInfo.kt @@ -0,0 +1,26 @@ +/** + * openCloud Android client application + * + * @author Markus Goetz + * + * Copyright (C) 2026 OpenCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package eu.opencloud.android.domain.webfinger.model + +data class WebFingerOidcInfo( + val issuer: String, + val clientId: String?, + val scopes: List?, +) diff --git a/opencloudTestUtil/src/main/java/eu/opencloud/android/testutil/OCServerInfo.kt b/opencloudTestUtil/src/main/java/eu/opencloud/android/testutil/OCServerInfo.kt index d3b2455f4..47940eca3 100644 --- a/opencloudTestUtil/src/main/java/eu/opencloud/android/testutil/OCServerInfo.kt +++ b/opencloudTestUtil/src/main/java/eu/opencloud/android/testutil/OCServerInfo.kt @@ -66,3 +66,14 @@ val OC_SECURE_SERVER_INFO_OIDC_AUTH_WEBFINGER_INSTANCE = ServerInfo.OIDCServer( openCloudVersion = "10.12", oidcServerConfiguration = OC_OIDC_SERVER_CONFIGURATION ) + +const val OC_WEBFINGER_CLIENT_ID = "OpenCloudAndroid" +val OC_WEBFINGER_SCOPES = listOf("openid", "profile", "email", "offline_access") + +val OC_SECURE_SERVER_INFO_OIDC_AUTH_WEBFINGER_INSTANCE_WITH_CLIENT = ServerInfo.OIDCServer( + baseUrl = OC_SECURE_BASE_URL, + openCloudVersion = "10.12", + oidcServerConfiguration = OC_OIDC_SERVER_CONFIGURATION, + webFingerClientId = OC_WEBFINGER_CLIENT_ID, + webFingerScopes = OC_WEBFINGER_SCOPES, +)