Skip to content
Open
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 @@ -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;

Expand Down Expand Up @@ -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,
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String>?
)
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ class GetInstancesViaWebFingerOperation(
private val lockupServerDomain: String,
private val rel: String,
private val resource: String,
) : RemoteOperation<List<String>>() {
private val platform: String? = null,
) : RemoteOperation<WebFingerResponse>() {

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
Expand All @@ -61,34 +63,35 @@ class GetInstancesViaWebFingerOperation(
method: HttpMethod,
response: String?,
status: Int
): RemoteOperationResult<List<String>> {
): RemoteOperationResult<WebFingerResponse> {
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<List<String>>(method)
return RemoteOperationResult<WebFingerResponse>(method)
}

private fun onRequestSuccessful(rawResponse: String): RemoteOperationResult<List<String>> {
private fun onRequestSuccessful(rawResponse: String): RemoteOperationResult<WebFingerResponse> {
val response = parseResponse(rawResponse)
Timber.d("Successful WebFinger request: $response")
val operationResult = RemoteOperationResult<List<String>>(RemoteOperationResult.ResultCode.OK)
operationResult.data = response.links?.map { it.href } ?: listOf()
val operationResult = RemoteOperationResult<WebFingerResponse>(RemoteOperationResult.ResultCode.OK)
operationResult.data = response
return operationResult
}

override fun run(client: OpenCloudClient): RemoteOperationResult<List<String>> {
override fun run(client: OpenCloudClient): RemoteOperationResult<WebFingerResponse> {
val requestUri = buildRequestUri()
Timber.d("Doing WebFinger request: $requestUri")
val getMethod = GetMethod(URL(requestUri.toString()))

// First iteration won't follow redirections.
getMethod.followRedirects = false

return try {
val status = client.executeHttpMethod(getMethod)
val response = getMethod.getResponseBodyAsString()!!
val response = getMethod.getResponseBodyAsString()

if (isSuccess(status)) {
onRequestSuccessful(response)
Expand All @@ -97,7 +100,7 @@ class GetInstancesViaWebFingerOperation(
}
} catch (e: Exception) {
Timber.e(e, "Requesting WebFinger info failed")
RemoteOperationResult<List<String>>(e)
RemoteOperationResult<WebFingerResponse>(e)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>?,
)

@JsonClass(generateAdapter = true)
data class WebFingerResponse(
val subject: String,
val links: List<LinkItem>?
val links: List<LinkItem>?,
val properties: WebFingerProperties? = null,
)

@JsonClass(generateAdapter = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -27,4 +28,12 @@ interface WebFingerService {
rel: String,
client: OpenCloudClient,
): RemoteOperationResult<List<String>>

fun getOidcDiscoveryFromWebFinger(
lookupServer: String,
resource: String,
rel: String,
platform: String,
client: OpenCloudClient,
): RemoteOperationResult<WebFingerResponse>
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -29,6 +30,23 @@ class OCWebFingerService : WebFingerService {
resource: String,
rel: String,
client: OpenCloudClient,
): RemoteOperationResult<List<String>> =
GetInstancesViaWebFingerOperation(lookupServer, rel, resource).execute(client)
): RemoteOperationResult<List<String>> {
val result = GetInstancesViaWebFingerOperation(lockupServerDomain = lookupServer, rel = rel, resource = resource).execute(client)
if (!result.isSuccess) {
@Suppress("UNCHECKED_CAST")
return result as RemoteOperationResult<List<String>>
}
val listResult = RemoteOperationResult<List<String>>(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<WebFingerResponse> =
GetInstancesViaWebFingerOperation(lockupServerDomain = lookupServer, rel = rel, resource = resource, platform = platform).execute(client)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class OCRemoteOAuthDataSource(
tokenType = this.tokenType,
userId = this.userId,
scope = this.scope,
idToken = this.idToken,
additionalParameters = this.additionalParameters
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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(
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -33,4 +36,12 @@ interface RemoteWebFingerDataSource {
username: String,
accessToken: String,
): List<String>

// 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?
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
}
}
Loading
Loading