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 @@ -60,7 +60,8 @@
/**
* Authenticator for openCloud accounts.
*
* Controller class accessed from the system AccountManager, providing integration of openCloud accounts with the
* Controller class accessed from the system AccountManager, providing
* integration of openCloud accounts with the
* Android system.
*/
public class AccountAuthenticator extends AbstractAccountAuthenticator {
Expand All @@ -84,8 +85,8 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator {
*/
@Override
public Bundle addAccount(AccountAuthenticatorResponse response,
String accountType, String authTokenType,
String[] requiredFeatures, Bundle options) {
String accountType, String authTokenType,
String[] requiredFeatures, Bundle options) {
Timber.i("Adding account with type " + accountType + " and auth token " + authTokenType);

final Bundle bundle = new Bundle();
Expand Down Expand Up @@ -120,7 +121,7 @@ public Bundle addAccount(AccountAuthenticatorResponse response,
*/
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse response,
Account account, Bundle options) {
Account account, Bundle options) {
try {
validateAccountType(account.type);
} catch (AuthenticatorException e) {
Expand All @@ -141,7 +142,7 @@ public Bundle confirmCredentials(AccountAuthenticatorResponse response,

@Override
public Bundle editProperties(AccountAuthenticatorResponse response,
String accountType) {
String accountType) {
return null;
}

Expand All @@ -150,7 +151,7 @@ public Bundle editProperties(AccountAuthenticatorResponse response,
*/
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse accountAuthenticatorResponse,
Account account, String authTokenType, Bundle options) {
Account account, String authTokenType, Bundle options) {
/// validate parameters
try {
validateAccountType(account.type);
Expand All @@ -168,7 +169,8 @@ public Bundle getAuthToken(AccountAuthenticatorResponse accountAuthenticatorResp
// Basic
accessToken = accountManager.getPassword(account);
} else {
// OAuth, gets an auth token from the AccountManager's cache. If no auth token is cached for
// OAuth, gets an auth token from the AccountManager's cache. If no auth token
// is cached for
// this account, null will be returned
accessToken = accountManager.peekAuthToken(account, authTokenType);
if (accessToken == null && canBeRefreshed(authTokenType) && clientSecretIsValid(accountManager, account)) {
Expand All @@ -184,13 +186,15 @@ public Bundle getAuthToken(AccountAuthenticatorResponse accountAuthenticatorResp
return result;
}

/// if not stored, return Intent to access the LoginActivity and UPDATE the token for the account
/// if not stored, return Intent to access the LoginActivity and UPDATE the
/// token for the account
return prepareBundleToAccessLoginActivity(accountAuthenticatorResponse, account, authTokenType, options);
}

/**
* Check if the client has expired or not.
* If the client has expired, we can not refresh the token and user needs to re-authenticate.
* If the client has expired, we can not refresh the token and user needs to
* re-authenticate.
*
* @return true if the client is still valid
*/
Expand Down Expand Up @@ -220,15 +224,15 @@ public String getAuthTokenLabel(String authTokenType) {

@Override
public Bundle hasFeatures(AccountAuthenticatorResponse response,
Account account, String[] features) {
Account account, String[] features) {
final Bundle result = new Bundle();
result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true);
return result;
}

@Override
public Bundle updateCredentials(AccountAuthenticatorResponse response,
Account account, String authTokenType, Bundle options) {
Account account, String authTokenType, Bundle options) {
final Intent intent = new Intent(mContext, LoginActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,
response);
Expand Down Expand Up @@ -265,9 +269,10 @@ private void validateAuthTokenType(String authTokenType)
throws UnsupportedAuthTokenTypeException {
if (!authTokenType.equals(MainApp.Companion.getAuthTokenType()) &&
!authTokenType.equals(AccountTypeUtils.getAuthTokenTypePass(MainApp.Companion.getAccountType())) &&
!authTokenType.equals(AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.Companion.getAccountType())) &&
!authTokenType.equals(AccountTypeUtils.getAuthTokenTypeRefreshToken(MainApp.Companion.getAccountType()))
) {
!authTokenType.equals(AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.Companion.getAccountType()))
&&
!authTokenType
.equals(AccountTypeUtils.getAuthTokenTypeRefreshToken(MainApp.Companion.getAccountType()))) {
throw new UnsupportedAuthTokenTypeException();
}
}
Expand Down Expand Up @@ -309,15 +314,13 @@ public static class UnsupportedAuthTokenTypeException extends
}

private boolean canBeRefreshed(String authTokenType) {
return (authTokenType.equals(AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.Companion.
getAccountType())));
return (authTokenType.equals(AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.Companion.getAccountType())));
}

private String refreshToken(
Account account,
String authTokenType,
AccountManager accountManager
) {
AccountManager accountManager) {

// Prepare everything to perform the token request
String refreshToken = accountManager.getUserData(account, KEY_OAUTH2_REFRESH_TOKEN);
Expand All @@ -333,10 +336,11 @@ private String refreshToken(
String baseUrl = accountManager.getUserData(account, AccountUtils.Constants.KEY_OC_BASE_URL);

// OIDC Discovery
@NotNull Lazy<OIDCDiscoveryUseCase> oidcDiscoveryUseCase = inject(OIDCDiscoveryUseCase.class);
@NotNull
Lazy<OIDCDiscoveryUseCase> oidcDiscoveryUseCase = inject(OIDCDiscoveryUseCase.class);
OIDCDiscoveryUseCase.Params oidcDiscoveryUseCaseParams = new OIDCDiscoveryUseCase.Params(baseUrl);
UseCaseResult<OIDCServerConfiguration> oidcServerConfigurationUseCaseResult =
oidcDiscoveryUseCase.getValue().invoke(oidcDiscoveryUseCaseParams);
UseCaseResult<OIDCServerConfiguration> oidcServerConfigurationUseCaseResult = oidcDiscoveryUseCase.getValue()
.invoke(oidcDiscoveryUseCaseParams);

String tokenEndpoint;

Expand All @@ -363,7 +367,8 @@ private String refreshToken(
tokenEndpoint = oidcServerConfigurationUseCaseResult.getDataOrNull().getTokenEndpoint();

if (oidcServerConfigurationUseCaseResult.getDataOrNull() != null &&
oidcServerConfigurationUseCaseResult.getDataOrNull().isTokenEndpointAuthMethodSupportedClientSecretPost()) {
oidcServerConfigurationUseCaseResult.getDataOrNull()
.isTokenEndpointAuthMethodSupportedClientSecretPost()) {
clientIdForRequest = clientId;
clientSecretForRequest = clientSecret;
}
Expand All @@ -374,7 +379,12 @@ private String refreshToken(
tokenEndpoint = baseUrl + File.separator + mContext.getString(R.string.oauth2_url_endpoint_access);
}

String clientAuth = OAuthUtils.Companion.getClientAuth(clientSecret, clientId);
String clientAuth;
if (clientSecret.isEmpty()) {
clientAuth = "";
} else {
clientAuth = OAuthUtils.Companion.getClientAuth(clientSecret, clientId);
}

String scope = mContext.getResources().getString(R.string.oauth2_openid_scope);

Expand All @@ -385,11 +395,11 @@ private String refreshToken(
scope,
clientIdForRequest,
clientSecretForRequest,
refreshToken
);
refreshToken);

// Token exchange
@NotNull Lazy<RequestTokenUseCase> requestTokenUseCase = inject(RequestTokenUseCase.class);
@NotNull
Lazy<RequestTokenUseCase> requestTokenUseCase = inject(RequestTokenUseCase.class);
RequestTokenUseCase.Params requestTokenParams = new RequestTokenUseCase.Params(oauthTokenRequest);
UseCaseResult<TokenResponse> tokenResponseResult = requestTokenUseCase.getValue().invoke(requestTokenParams);

Expand All @@ -398,7 +408,8 @@ private String refreshToken(
return handleSuccessfulRefreshToken(safeTokenResponse,
account, authTokenType, accountManager, refreshToken);
} else {
Timber.e(tokenResponseResult.getThrowableOrNull(), "OAuth request to refresh access token failed. Preparing to access Login Activity");
Timber.e(tokenResponseResult.getThrowableOrNull(),
"OAuth request to refresh access token failed. Preparing to access Login Activity");
return null;
}
}
Expand All @@ -408,43 +419,41 @@ private String handleSuccessfulRefreshToken(
Account account,
String authTokenType,
AccountManager accountManager,
String oldRefreshToken
) {
String newAccessToken = tokenResponse.getAccessToken();
accountManager.setAuthToken(account, authTokenType, newAccessToken);

String refreshTokenToUseFromNowOn;
if (tokenResponse.getRefreshToken() != null) {
refreshTokenToUseFromNowOn = tokenResponse.getRefreshToken();
} else {
refreshTokenToUseFromNowOn = oldRefreshToken;
}
accountManager.setUserData(account, KEY_OAUTH2_REFRESH_TOKEN, refreshTokenToUseFromNowOn);

Timber.d("Token refreshed successfully. New access token: [ %s ]. New refresh token: [ %s ]",
newAccessToken, refreshTokenToUseFromNowOn);
String oldRefreshToken) {
String newAccessToken = tokenResponse.getAccessToken();
accountManager.setAuthToken(account, authTokenType, newAccessToken);

return newAccessToken;
String refreshTokenToUseFromNowOn;
if (tokenResponse.getRefreshToken() != null) {
refreshTokenToUseFromNowOn = tokenResponse.getRefreshToken();
} else {
refreshTokenToUseFromNowOn = oldRefreshToken;
}
accountManager.setUserData(account, KEY_OAUTH2_REFRESH_TOKEN, refreshTokenToUseFromNowOn);

Timber.d("Token refreshed successfully. New access token: [ %s ]. New refresh token: [ %s ]",
newAccessToken, refreshTokenToUseFromNowOn);

return newAccessToken;
}

/**
* Return bundle with intent to access LoginActivity and UPDATE the token for the account
* Return bundle with intent to access LoginActivity and UPDATE the token for
* the account
*/
private Bundle prepareBundleToAccessLoginActivity(
AccountAuthenticatorResponse accountAuthenticatorResponse,
Account account,
String authTokenType,
Bundle options
) {
Bundle options) {
final Intent intent = new Intent(mContext, LoginActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,
accountAuthenticatorResponse);
intent.putExtra(KEY_AUTH_TOKEN_TYPE, authTokenType);
intent.putExtra(AuthenticatorConstants.EXTRA_ACCOUNT, account);
intent.putExtra(
AuthenticatorConstants.EXTRA_ACTION,
AuthenticatorConstants.ACTION_UPDATE_EXPIRED_TOKEN
);
AuthenticatorConstants.ACTION_UPDATE_EXPIRED_TOKEN);

final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -666,25 +666,31 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted

val clientRegistrationInfo = authenticationViewModel.registerClient.value?.peekContent()?.getStoredData()

val clientAuth = if (clientRegistrationInfo?.clientId != null && clientRegistrationInfo.clientSecret != null) {
OAuthUtils.getClientAuth(clientRegistrationInfo.clientSecret as String, clientRegistrationInfo.clientId)
// Determine clientId and clientSecret
val (clientId, clientSecret) = if (clientRegistrationInfo?.clientId != null && clientRegistrationInfo.clientSecret != null) {
Pair(clientRegistrationInfo.clientId, clientRegistrationInfo.clientSecret as String)
} else {
Pair(getString(R.string.oauth2_client_id), getString(R.string.oauth2_client_secret))
}

val clientAuth = if (clientSecret.isEmpty()) {
""
} else {
OAuthUtils.getClientAuth(getString(R.string.oauth2_client_secret), getString(R.string.oauth2_client_id))
OAuthUtils.getClientAuth(clientSecret, clientId)
}

// Use oidc discovery one, or build an oauth endpoint using serverBaseUrl + Setup string.
val tokenEndPoint: String

var clientId: String? = null
var clientSecret: String? = null
var clientIdForRequest: String? = null
var clientSecretForRequest: String? = null

val serverInfo = authenticationViewModel.serverInfo.value?.peekContent()?.getStoredData()
if (serverInfo is ServerInfo.OIDCServer) {
tokenEndPoint = serverInfo.oidcServerConfiguration.tokenEndpoint
if (serverInfo.oidcServerConfiguration.isTokenEndpointAuthMethodSupportedClientSecretPost()) {
clientId = clientRegistrationInfo?.clientId ?: contextProvider.getString(R.string.oauth2_client_id)
clientSecret = clientRegistrationInfo?.clientSecret ?: contextProvider.getString(R.string.oauth2_client_secret)
clientIdForRequest = clientRegistrationInfo?.clientId ?: contextProvider.getString(R.string.oauth2_client_id)
clientSecretForRequest = clientRegistrationInfo?.clientSecret ?: contextProvider.getString(R.string.oauth2_client_secret)
}
} else {
tokenEndPoint = "$serverBaseUrl${File.separator}${contextProvider.getString(R.string.oauth2_url_endpoint_access)}"
Expand All @@ -697,8 +703,8 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
tokenEndpoint = tokenEndPoint,
clientAuth = clientAuth,
scope = scope,
clientId = clientId,
clientSecret = clientSecret,
clientId = clientIdForRequest,
clientSecret = clientSecretForRequest,
authorizationCode = authorizationCode,
redirectUri = OAuthUtils.buildRedirectUri(applicationContext).toString(),
codeVerifier = authenticationViewModel.codeVerifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ class TokenRequestRemoteOperation(

val postMethod = PostMethod(URL(tokenRequestParams.tokenEndpoint), requestBody)

postMethod.addRequestHeader(AUTHORIZATION_HEADER, tokenRequestParams.clientAuth)
if (tokenRequestParams.clientAuth.isNotEmpty()) {
postMethod.addRequestHeader(AUTHORIZATION_HEADER, tokenRequestParams.clientAuth)
}

val status = client.executeHttpMethod(postMethod)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ import eu.opencloud.android.testutil.oauth.OC_CLIENT_REGISTRATION
import eu.opencloud.android.testutil.oauth.OC_CLIENT_REGISTRATION_REQUEST
import eu.opencloud.android.testutil.oauth.OC_OIDC_SERVER_CONFIGURATION
import eu.opencloud.android.testutil.oauth.OC_TOKEN_REQUEST_ACCESS
import eu.opencloud.android.testutil.oauth.OC_TOKEN_REQUEST_ACCESS_PUBLIC_CLIENT
import eu.opencloud.android.testutil.oauth.OC_TOKEN_REQUEST_REFRESH
import eu.opencloud.android.testutil.oauth.OC_TOKEN_REQUEST_REFRESH_PUBLIC_CLIENT
import eu.opencloud.android.testutil.oauth.OC_TOKEN_RESPONSE

val OC_REMOTE_OIDC_DISCOVERY_RESPONSE = OIDCDiscoveryResponse(
Expand Down Expand Up @@ -65,6 +67,32 @@ val OC_REMOTE_TOKEN_REQUEST_PARAMS_REFRESH = TokenRequestParams.RefreshToken(
refreshToken = OC_TOKEN_REQUEST_REFRESH.refreshToken
)

/**
* Test fixtures for public PKCE clients (RFC 7636).
* Public clients MUST NOT send Authorization header during token exchange.
*/
val OC_REMOTE_TOKEN_REQUEST_PARAMS_ACCESS_PUBLIC_CLIENT = TokenRequestParams.Authorization(
tokenEndpoint = OC_TOKEN_REQUEST_ACCESS_PUBLIC_CLIENT.tokenEndpoint,
clientAuth = OC_TOKEN_REQUEST_ACCESS_PUBLIC_CLIENT.clientAuth, // Empty string
grantType = OC_TOKEN_REQUEST_ACCESS_PUBLIC_CLIENT.grantType,
scope = OC_TOKEN_REQUEST_ACCESS_PUBLIC_CLIENT.scope,
clientId = null,
clientSecret = null,
authorizationCode = OC_TOKEN_REQUEST_ACCESS_PUBLIC_CLIENT.authorizationCode,
redirectUri = OC_TOKEN_REQUEST_ACCESS_PUBLIC_CLIENT.redirectUri,
codeVerifier = OC_TOKEN_REQUEST_ACCESS_PUBLIC_CLIENT.codeVerifier
)

val OC_REMOTE_TOKEN_REQUEST_PARAMS_REFRESH_PUBLIC_CLIENT = TokenRequestParams.RefreshToken(
tokenEndpoint = OC_TOKEN_REQUEST_REFRESH_PUBLIC_CLIENT.tokenEndpoint,
clientAuth = OC_TOKEN_REQUEST_REFRESH_PUBLIC_CLIENT.clientAuth, // Empty string
grantType = OC_TOKEN_REQUEST_REFRESH_PUBLIC_CLIENT.grantType,
scope = OC_TOKEN_REQUEST_REFRESH_PUBLIC_CLIENT.scope,
clientId = null,
clientSecret = null,
refreshToken = OC_TOKEN_REQUEST_REFRESH_PUBLIC_CLIENT.refreshToken
)

val OC_REMOTE_TOKEN_RESPONSE = TokenResponse(
accessToken = OC_TOKEN_RESPONSE.accessToken,
expiresIn = OC_TOKEN_RESPONSE.expiresIn,
Expand Down
Loading
Loading