diff --git a/engine/src/main/java/com/google/android/fhir/FhirEngineProvider.kt b/engine/src/main/java/com/google/android/fhir/FhirEngineProvider.kt index ba9b0c2819..94552036bd 100644 --- a/engine/src/main/java/com/google/android/fhir/FhirEngineProvider.kt +++ b/engine/src/main/java/com/google/android/fhir/FhirEngineProvider.kt @@ -18,8 +18,8 @@ package com.google.android.fhir import android.content.Context import com.google.android.fhir.DatabaseErrorStrategy.UNSPECIFIED -import com.google.android.fhir.sync.Authenticator import com.google.android.fhir.sync.DataSource +import com.google.android.fhir.sync.HttpAuthenticator import com.google.android.fhir.sync.remote.HttpLogger import org.hl7.fhir.r4.model.SearchParameter @@ -145,11 +145,8 @@ data class ServerConfiguration( val baseUrl: String, /** A configuration to provide the network connection parameters. */ val networkConfiguration: NetworkConfiguration = NetworkConfiguration(), - /** - * An [Authenticator] for supplying any auth token that may be necessary to communicate with the - * server - */ - val authenticator: Authenticator? = null, + /** An [HttpAuthenticator] for providing HTTP authorization header. */ + val authenticator: HttpAuthenticator? = null, /** Logs the communication between the engine and the remote server. */ val httpLogger: HttpLogger = HttpLogger.NONE ) diff --git a/engine/src/main/java/com/google/android/fhir/sync/Authenticator.kt b/engine/src/main/java/com/google/android/fhir/sync/Authenticator.kt deleted file mode 100644 index 053b140d0e..0000000000 --- a/engine/src/main/java/com/google/android/fhir/sync/Authenticator.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2021 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.android.fhir.sync - -import androidx.annotation.WorkerThread - -/** - * [FhirEngine] depends on the developer app to handle user's authentication. The developer - * application may provide the implementation during the [FhirEngine] initial setup to obtain - * authToken to the engine for successful calls. - */ -interface Authenticator { - /** @return Access token for the engine to make requests on user's behalf. */ - @WorkerThread fun getAccessToken(): String -} diff --git a/engine/src/main/java/com/google/android/fhir/sync/HttpAuthenticator.kt b/engine/src/main/java/com/google/android/fhir/sync/HttpAuthenticator.kt new file mode 100644 index 0000000000..3636de70ab --- /dev/null +++ b/engine/src/main/java/com/google/android/fhir/sync/HttpAuthenticator.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.sync + +import androidx.annotation.WorkerThread +import okhttp3.Credentials + +/** + * Provides an authorization method for the HTTP requests FHIR Engine sends to the FHIR server. + * + * FHIR Engine does not handle user authentication. The application should handle user + * authentication and provide the appropriate authentication method so the HTTP requests FHIR Engine + * sends to the FHIR server contain the correct user information for the request to be + * authenticated. + * + * The implementation can provide different `HttpAuthenticationMethod`s at runtime. This is + * important if the authentication token expires or the user needs to re-authenticate. + */ +fun interface HttpAuthenticator { + fun getAuthenticationMethod(): HttpAuthenticationMethod +} + +/** + * The HTTP authentication method to be used for generating HTTP authorization header. + * + * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication. + */ +sealed interface HttpAuthenticationMethod { + /** @return The authorization header for the engine to make requests on user's behalf. */ + @WorkerThread fun getAuthorizationHeader(): String + + /** See https://datatracker.ietf.org/doc/html/rfc7617. */ + data class Basic(val username: String, val password: String) : HttpAuthenticationMethod { + override fun getAuthorizationHeader() = Credentials.basic(username, password) + } + + /** See https://datatracker.ietf.org/doc/html/rfc6750. */ + data class Bearer(val token: String) : HttpAuthenticationMethod { + override fun getAuthorizationHeader() = "Bearer: $token" + } +} diff --git a/engine/src/main/java/com/google/android/fhir/sync/remote/RetrofitHttpService.kt b/engine/src/main/java/com/google/android/fhir/sync/remote/RetrofitHttpService.kt index f4478b9e89..67b1bbe86b 100644 --- a/engine/src/main/java/com/google/android/fhir/sync/remote/RetrofitHttpService.kt +++ b/engine/src/main/java/com/google/android/fhir/sync/remote/RetrofitHttpService.kt @@ -17,7 +17,7 @@ package com.google.android.fhir.sync.remote import com.google.android.fhir.NetworkConfiguration -import com.google.android.fhir.sync.Authenticator +import com.google.android.fhir.sync.HttpAuthenticator import java.util.concurrent.TimeUnit import okhttp3.Interceptor import okhttp3.OkHttpClient @@ -44,10 +44,10 @@ internal interface RetrofitHttpService : FhirHttpService { private val baseUrl: String, private val networkConfiguration: NetworkConfiguration ) { - private var authenticator: Authenticator? = null + private var authenticator: HttpAuthenticator? = null private var httpLoggingInterceptor: HttpLoggingInterceptor? = null - fun setAuthenticator(authenticator: Authenticator?) = apply { + fun setAuthenticator(authenticator: HttpAuthenticator?) = apply { this.authenticator = authenticator } @@ -69,12 +69,14 @@ internal interface RetrofitHttpService : FhirHttpService { authenticator?.let { addInterceptor( Interceptor { chain: Interceptor.Chain -> - val accessToken = it.getAccessToken() val request = chain .request() .newBuilder() - .addHeader("Authorization", "Bearer $accessToken") + .addHeader( + "Authorization", + it.getAuthenticationMethod().getAuthorizationHeader() + ) .build() chain.proceed(request) } diff --git a/engine/src/test/java/com/google/android/fhir/sync/HttpAuthenticatorTest.kt b/engine/src/test/java/com/google/android/fhir/sync/HttpAuthenticatorTest.kt new file mode 100644 index 0000000000..20c294be3b --- /dev/null +++ b/engine/src/test/java/com/google/android/fhir/sync/HttpAuthenticatorTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.sync + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class HttpAuthenticatorTest { + @Test + fun `should generate basic authentication header`() { + assertThat(HttpAuthenticationMethod.Basic("username", "password").getAuthorizationHeader()) + .isEqualTo("Basic dXNlcm5hbWU6cGFzc3dvcmQ=") + } + + @Test + fun `should generate bearer authentication token`() { + assertThat(HttpAuthenticationMethod.Bearer("token").getAuthorizationHeader()) + .isEqualTo("Bearer: token") + } +}