From ce99f2c5a0cea75bef38a131b8d720d9acb90ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Klawikowski?= Date: Thu, 19 Sep 2024 11:39:58 +0200 Subject: [PATCH] chore: Add ktlint, fix ktlint errors, fix deprecations --- .../schibsted/account/example/ClientConfig.kt | 10 +- .../schibsted/account/example/ExampleApp.kt | 85 ++++---- .../schibsted/account/example/HttpClient.kt | 11 +- .../account/example/LoggedInActivity.kt | 44 ++-- .../schibsted/account/example/MainActivity.kt | 24 ++- .../account/example/ManualLoginActivity.kt | 2 +- .../account/example/SimpleService.kt | 4 +- .../com/schibsted/account/ExampleUnitTest.kt | 5 +- build.gradle | 4 + .../webflows/activities/AuthResultLiveData.kt | 124 +++++------ .../AuthorizationManagementActivity.kt | 13 +- .../activities/RedirectUriReceiverActivity.kt | 2 +- .../account/webflows/api/ApiResultCallback.kt | 17 +- .../account/webflows/api/HttpError.kt | 1 + .../webflows/api/JWKSetDeserializer.kt | 2 +- .../api/SDKUserAgentHeaderInterceptor.kt | 10 +- .../webflows/api/SchibstedAccountApi.kt | 84 ++++---- .../webflows/api/SchibstedAccountService.kt | 12 +- .../account/webflows/api/TokenRequest.kt | 16 +- .../webflows/api/UserProfileResponse.kt | 27 ++- .../account/webflows/client/AuthRequest.kt | 12 +- .../account/webflows/client/AuthState.kt | 8 +- .../account/webflows/client/Client.kt | 135 +++++++----- .../webflows/client/ClientConfiguration.kt | 2 +- .../account/webflows/client/Environment.kt | 2 +- .../account/webflows/client/UrlBuilder.kt | 30 +-- .../loginPrompt/LoginPromptContentProvider.kt | 21 +- .../loginPrompt/LoginPromptFragment.kt | 21 +- .../loginPrompt/LoginPromptManager.kt | 18 +- .../loginPrompt/SessionInfoDatabase.kt | 27 +-- .../loginPrompt/SessionInfoManager.kt | 39 ++-- .../persistence/ObfuscatedSessionFinder.kt | 50 +++-- .../webflows/persistence/SessionStorage.kt | 56 +++-- .../webflows/persistence/StateStorage.kt | 10 +- .../webflows/persistence/StorageError.kt | 2 +- .../account/webflows/token/IdTokenClaims.kt | 4 +- .../webflows/token/IdTokenValidator.kt | 45 ++-- .../account/webflows/token/TokenHandler.kt | 75 ++++--- .../account/webflows/token/UserTokens.kt | 12 +- .../tracking/SchibstedAccountTracker.kt | 2 +- .../tracking/SchibstedAccountTrackingEvent.kt | 3 +- .../tracking/SchibstedAccountTrackingStore.kt | 3 +- .../webflows/user/AuthenticatedRequests.kt | 32 +-- .../webflows/user/StoredUserSession.kt | 4 +- .../schibsted/account/webflows/user/User.kt | 135 +++++++----- .../webflows/util/BestEffortRunOnceTask.kt | 2 +- .../schibsted/account/webflows/util/Either.kt | 1 + .../schibsted/account/testutil/Fixtures.kt | 37 ++-- .../schibsted/account/testutil/TestUtil.kt | 25 ++- .../activities/AuthResultLiveDataTest.kt | 34 +-- .../AuthorizationManagementActivityTest.kt | 47 ++-- .../RedirectUriReceiverActivityTest.kt | 5 +- .../webflows/api/SchibstedAccountApiTest.kt | 203 ++++++++++-------- .../webflows/api/UserProfileResponseTest.kt | 115 +++++----- .../account/webflows/client/ClientTest.kt | 188 +++++++++------- .../account/webflows/client/UrlBuilderTest.kt | 8 +- ...StorageTest.kt => MigratingStorageTest.kt} | 22 +- .../storage/ObfuscatedSessionFinderTest.kt | 74 ++++--- .../webflows/token/IdTokenValidatorTest.kt | 133 ++++++------ .../account/webflows/user/UserTest.kt | 190 ++++++++-------- .../util/BestEffortRunOnceTaskTest.kt | 68 +++--- .../account/webflows/util/EitherTest.kt | 9 +- .../account/webflows/util/TestRetrofitApi.kt | 4 +- 63 files changed, 1368 insertions(+), 1042 deletions(-) rename webflows/src/test/java/com/schibsted/account/webflows/storage/{StorageTest.kt => MigratingStorageTest.kt} (81%) diff --git a/app/src/main/java/com/schibsted/account/example/ClientConfig.kt b/app/src/main/java/com/schibsted/account/example/ClientConfig.kt index 20f4a5ab..589f7b81 100644 --- a/app/src/main/java/com/schibsted/account/example/ClientConfig.kt +++ b/app/src/main/java/com/schibsted/account/example/ClientConfig.kt @@ -5,9 +5,9 @@ import com.schibsted.account.webflows.client.Environment object ClientConfig { @JvmStatic val environment = Environment.PRE - const val clientId = "602525f2b41fa31789a95aa8" - const val loginRedirectUri = "com.sdk-example.pre.602525f2b41fa31789a95aa8:///login" - const val manualLoginRedirectUri = "com.sdk-example.pre.602525f2b41fa31789a95aa8:///manual-login" - const val webClientId = "599fd705ed21dc0d55011d2a" - const val webClientRedirectUri = "https://pre.sdk-example.com/safepage" + const val CLIENT_ID = "602525f2b41fa31789a95aa8" + const val LOGIN_REDIRECT_URI = "com.sdk-example.pre.602525f2b41fa31789a95aa8:///login" + const val MANUAL_LOGIN_REDIRECT_URI = "com.sdk-example.pre.602525f2b41fa31789a95aa8:///manual-login" + const val WEB_CLIENT_ID = "599fd705ed21dc0d55011d2a" + const val WEB_CLIENT_REDIRECT_URI = "https://pre.sdk-example.com/safepage" } diff --git a/app/src/main/java/com/schibsted/account/example/ExampleApp.kt b/app/src/main/java/com/schibsted/account/example/ExampleApp.kt index c83165b5..dfdd37d6 100644 --- a/app/src/main/java/com/schibsted/account/example/ExampleApp.kt +++ b/app/src/main/java/com/schibsted/account/example/ExampleApp.kt @@ -17,7 +17,6 @@ import com.schibsted.account.webflows.tracking.SchibstedAccountTrackingListener import timber.log.Timber class ExampleApp : Application() { - override fun onCreate() { super.onCreate() @@ -29,11 +28,12 @@ class ExampleApp : Application() { } private fun initTracking() { - val listener = object : SchibstedAccountTrackingListener { - override fun onEvent(event: SchibstedAccountTrackingEvent) { - Timber.d("Tracked event ${event::class.simpleName.toString()}") + val listener = + object : SchibstedAccountTrackingListener { + override fun onEvent(event: SchibstedAccountTrackingEvent) { + Timber.d("Tracked event ${event::class.simpleName}") + } } - } SchibstedAccountTrackerStore.addTrackingListener(listener) } @@ -45,31 +45,36 @@ class ExampleApp : Application() { } private fun initClient() { - val clientConfig = ClientConfiguration( - env = environment, - clientId = ClientConfig.clientId, - redirectUri = ClientConfig.loginRedirectUri - ) - client = Client( - context = applicationContext, - configuration = clientConfig, - httpClient = instance - ) + val clientConfig = + ClientConfiguration( + env = environment, + clientId = ClientConfig.CLIENT_ID, + redirectUri = ClientConfig.LOGIN_REDIRECT_URI, + ) + client = + Client( + context = applicationContext, + configuration = clientConfig, + httpClient = instance, + ) } private fun initManualClient() { - val clientConfig = ClientConfiguration( - env = environment, - clientId = ClientConfig.clientId, - redirectUri = ClientConfig.manualLoginRedirectUri - ) - manualClient = Client( - context = applicationContext, - configuration = clientConfig, - httpClient = instance, - logoutCallback = { - Timber.i("Received a logout event from client") - }) + val clientConfig = + ClientConfiguration( + env = environment, + clientId = ClientConfig.CLIENT_ID, + redirectUri = ClientConfig.MANUAL_LOGIN_REDIRECT_URI, + ) + manualClient = + Client( + context = applicationContext, + configuration = clientConfig, + httpClient = instance, + logoutCallback = { + Timber.i("Received a logout event from client") + }, + ) } private fun initAuthorizationManagement() { @@ -79,18 +84,20 @@ class ExampleApp : Application() { cancelIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP AuthorizationManagementActivity.setup( client = client, - completionIntent = PendingIntent.getActivity( - this, - 0, - completionIntent, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0 - ), - cancelIntent = PendingIntent.getActivity( - this, - 1, - cancelIntent, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0 - ) + completionIntent = + PendingIntent.getActivity( + this, + 0, + completionIntent, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0, + ), + cancelIntent = + PendingIntent.getActivity( + this, + 1, + cancelIntent, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0, + ), ) } diff --git a/app/src/main/java/com/schibsted/account/example/HttpClient.kt b/app/src/main/java/com/schibsted/account/example/HttpClient.kt index 5fbaec2b..334f9a48 100644 --- a/app/src/main/java/com/schibsted/account/example/HttpClient.kt +++ b/app/src/main/java/com/schibsted/account/example/HttpClient.kt @@ -5,9 +5,10 @@ import okhttp3.logging.HttpLoggingInterceptor object HttpClient { @JvmStatic - val instance: OkHttpClient = run { - val logging = HttpLoggingInterceptor() - logging.setLevel(HttpLoggingInterceptor.Level.BODY) - OkHttpClient.Builder().addNetworkInterceptor(logging).build() - } + val instance: OkHttpClient = + run { + val logging = HttpLoggingInterceptor() + logging.setLevel(HttpLoggingInterceptor.Level.BODY) + OkHttpClient.Builder().addNetworkInterceptor(logging).build() + } } diff --git a/app/src/main/java/com/schibsted/account/example/LoggedInActivity.kt b/app/src/main/java/com/schibsted/account/example/LoggedInActivity.kt index 7deb003d..48b01ee0 100644 --- a/app/src/main/java/com/schibsted/account/example/LoggedInActivity.kt +++ b/app/src/main/java/com/schibsted/account/example/LoggedInActivity.kt @@ -94,10 +94,9 @@ class LoggedInActivity : AppCompatActivity() { binding.sessionExchangeButton.setOnClickListener { if (isUserLoggedIn) { user?.webSessionUrl( - clientId = ClientConfig.webClientId, - redirectUri = ClientConfig.webClientRedirectUri, - ) - { result: Either -> + clientId = ClientConfig.WEB_CLIENT_ID, + redirectUri = ClientConfig.WEB_CLIENT_REDIRECT_URI, + ) { result: Either -> result .onSuccess { value: URL -> Timber.i("Session exchange URL: $value") @@ -121,11 +120,12 @@ class LoggedInActivity : AppCompatActivity() { } private fun evaluateAndUpdateUserSession() { - client = when (intent.getSerializableExtra(FLOW_EXTRA)) { - Flow.AUTOMATIC -> ExampleApp.client - Flow.MANUAL -> ExampleApp.manualClient - else -> throw RuntimeException("Must provide a flow enum") - } + client = + when (intent.getSerializableExtra(FLOW_EXTRA)) { + Flow.AUTOMATIC -> ExampleApp.client + Flow.MANUAL -> ExampleApp.manualClient + else -> throw RuntimeException("Must provide a flow enum") + } val userSession: UserSession? = intent.getParcelableExtra(USER_SESSION_EXTRA) client?.let { client -> @@ -136,14 +136,15 @@ class LoggedInActivity : AppCompatActivity() { private fun initMakeAuthenticatedRequestButton() { binding.testRetrofitAuthenticatedRequest.setOnClickListener { - val myService: SimpleService = HttpClient.instance.newBuilder().let { - user?.bind(it) - Retrofit.Builder() - .baseUrl(ClientConfig.environment.url) - .client(it.build()) - .build() - .create(SimpleService::class.java) - } + val myService: SimpleService = + HttpClient.instance.newBuilder().let { + user?.bind(it) + Retrofit.Builder() + .baseUrl(ClientConfig.environment.url) + .client(it.build()) + .build() + .create(SimpleService::class.java) + } CoroutineScope(Dispatchers.IO).launch { val profileData = myService.userProfile(user?.userId.toString()).await() @@ -164,10 +165,15 @@ class LoggedInActivity : AppCompatActivity() { private const val FLOW_EXTRA = "com.schibsted.account.FLOW" enum class Flow { - AUTOMATIC, MANUAL + AUTOMATIC, + MANUAL, } - fun intentWithUser(context: Context?, user: User, flow: Flow): Intent { + fun intentWithUser( + context: Context?, + user: User, + flow: Flow, + ): Intent { val intent = Intent(context, LoggedInActivity::class.java) intent.putExtra(USER_SESSION_EXTRA, user.session) intent.putExtra(FLOW_EXTRA, flow) diff --git a/app/src/main/java/com/schibsted/account/example/MainActivity.kt b/app/src/main/java/com/schibsted/account/example/MainActivity.kt index e61921ab..3264a1e1 100644 --- a/app/src/main/java/com/schibsted/account/example/MainActivity.kt +++ b/app/src/main/java/com/schibsted/account/example/MainActivity.kt @@ -40,7 +40,6 @@ class MainActivity : AppCompatActivity() { } } - private fun initializeButtons() { binding.loginButton.setOnClickListener { startActivity(ExampleApp.client.getAuthenticationIntent(this, "customState")) @@ -51,13 +50,16 @@ class MainActivity : AppCompatActivity() { } private fun observeAuthResultLiveData() { - AuthResultLiveData.get(ExampleApp.client).observe(this, Observer { result: Either -> - result - .onSuccess { user: User -> startLoggedInActivity(user) } - .onFailure { state: NotAuthed -> - handleNotAuthedState(state) - } - } as Observer>) + AuthResultLiveData.get(ExampleApp.client).observe( + this, + Observer { result: Either -> + result + .onSuccess { user: User -> startLoggedInActivity(user) } + .onFailure { state: NotAuthed -> + handleNotAuthedState(state) + } + } as Observer>, + ) } private fun handleNotAuthedState(state: NotAuthed) { @@ -86,12 +88,12 @@ class MainActivity : AppCompatActivity() { LoggedInActivity.intentWithUser( this, user, - LoggedInActivity.Companion.Flow.AUTOMATIC - ) + LoggedInActivity.Companion.Flow.AUTOMATIC, + ), ) } companion object { - var LOGIN_FAILED_EXTRA = "com.schibsted.account.LOGIN_FAILED" + const val LOGIN_FAILED_EXTRA = "com.schibsted.account.LOGIN_FAILED" } } diff --git a/app/src/main/java/com/schibsted/account/example/ManualLoginActivity.kt b/app/src/main/java/com/schibsted/account/example/ManualLoginActivity.kt index 83e96256..dbea2117 100644 --- a/app/src/main/java/com/schibsted/account/example/ManualLoginActivity.kt +++ b/app/src/main/java/com/schibsted/account/example/ManualLoginActivity.kt @@ -77,7 +77,7 @@ class ManualLoginActivity : AppCompatActivity() { Toast.makeText( this, "User could not be resumed, error: ${it.cause.message} ", - Toast.LENGTH_SHORT + Toast.LENGTH_SHORT, ) .show() } diff --git a/app/src/main/java/com/schibsted/account/example/SimpleService.kt b/app/src/main/java/com/schibsted/account/example/SimpleService.kt index b87bbd33..3079f44a 100644 --- a/app/src/main/java/com/schibsted/account/example/SimpleService.kt +++ b/app/src/main/java/com/schibsted/account/example/SimpleService.kt @@ -7,5 +7,7 @@ import retrofit2.http.Path interface SimpleService { @GET("/api/2/user/{userId}") - fun userProfile(@Path("userId") userId: String): Call + fun userProfile( + @Path("userId") userId: String, + ): Call } diff --git a/app/src/test/java/com/schibsted/account/ExampleUnitTest.kt b/app/src/test/java/com/schibsted/account/ExampleUnitTest.kt index ec779ff3..571e264d 100644 --- a/app/src/test/java/com/schibsted/account/ExampleUnitTest.kt +++ b/app/src/test/java/com/schibsted/account/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package com.schibsted.account +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * @@ -14,4 +13,4 @@ class ExampleUnitTest { fun addition_isCorrect() { assertEquals(4, 2 + 2) } -} \ No newline at end of file +} diff --git a/build.gradle b/build.gradle index 7477ebd9..38d5749f 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,8 @@ buildscript { classpath 'com.android.tools.build:gradle:8.5.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jlleitschuh.gradle:ktlint-gradle:12.1.1" + // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } @@ -24,6 +26,8 @@ plugins { def gitVersion = ['sh', '-c', 'git describe --tag 2> /dev/null || git rev-parse --short HEAD'].execute().text.trim() allprojects { + apply plugin: "org.jlleitschuh.gradle.ktlint" // Version should be inherited from parent + repositories { google() mavenCentral() diff --git a/webflows/src/main/java/com/schibsted/account/webflows/activities/AuthResultLiveData.kt b/webflows/src/main/java/com/schibsted/account/webflows/activities/AuthResultLiveData.kt index 24449ab6..ec17a00e 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/activities/AuthResultLiveData.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/activities/AuthResultLiveData.kt @@ -14,12 +14,14 @@ import com.schibsted.account.webflows.util.Either.Right sealed class NotAuthed { object CancelledByUser : NotAuthed() + object NoLoggedInUser : NotAuthed() + object AuthInProgress : NotAuthed() + data class LoginFailed(val error: LoginError) : NotAuthed() } - typealias AuthResult = Either /** @@ -35,79 +37,79 @@ typealias AuthResult = Either */ class AuthResultLiveData private constructor(private val client: Client) : LiveData() { + internal fun update(result: AuthResult) { + value = result + } - internal fun update(result: AuthResult) { - value = result - } - - init { - client.resumeLastLoggedInUser { result -> - result - .onSuccess { resumedUser -> - value = if (resumedUser != null) { - Right(resumedUser) - } else { - Left(NotAuthed.NoLoggedInUser) + init { + client.resumeLastLoggedInUser { result -> + result + .onSuccess { resumedUser -> + value = + if (resumedUser != null) { + Right(resumedUser) + } else { + Left(NotAuthed.NoLoggedInUser) + } } - } - .onFailure { - value = Left(NotAuthed.NoLoggedInUser) - } + .onFailure { + value = Left(NotAuthed.NoLoggedInUser) + } + } } - } - internal fun update(intent: Intent) { - client.handleAuthenticationResponse(intent) { result -> - when (result) { - is Right -> update(result) - is Left -> update( - Left( - when (result.value) { - is LoginError.CancelledByUser -> { - SchibstedAccountTracker.track(SchibstedAccountTrackingEvent.UserLoginCanceled) - NotAuthed.CancelledByUser - } + internal fun update(intent: Intent) { + client.handleAuthenticationResponse(intent) { result -> + when (result) { + is Right -> update(result) + is Left -> + update( + Left( + when (result.value) { + is LoginError.CancelledByUser -> { + SchibstedAccountTracker.track(SchibstedAccountTrackingEvent.UserLoginCanceled) + NotAuthed.CancelledByUser + } - else -> NotAuthed.LoginFailed(result.value) - } - ) - ) + else -> NotAuthed.LoginFailed(result.value) + }, + ), + ) + } } } - } - /** - * Change state to [NotAuthed.NoLoggedInUser]. - * - * Internally uses [LiveData.postValue] so can safely be called from background threads. - */ - internal fun logout() { - postValue(Left(NotAuthed.NoLoggedInUser)) - } + /** + * Change state to [NotAuthed.NoLoggedInUser]. + * + * Internally uses [LiveData.postValue] so can safely be called from background threads. + */ + internal fun logout() { + postValue(Left(NotAuthed.NoLoggedInUser)) + } - companion object { - private lateinit var instance: AuthResultLiveData + companion object { + private lateinit var instance: AuthResultLiveData - internal fun getIfInitialised(): AuthResultLiveData? = - if (::instance.isInitialized) instance else null + internal fun getIfInitialised(): AuthResultLiveData? = if (::instance.isInitialized) instance else null - @JvmStatic - fun get(client: Client): AuthResultLiveData { - if (!::instance.isInitialized) { - instance = create(client) + @JvmStatic + fun get(client: Client): AuthResultLiveData { + if (!::instance.isInitialized) { + instance = create(client) + } + return instance } - return instance - } - @JvmStatic - @MainThread - internal fun create(client: Client): AuthResultLiveData { - if (::instance.isInitialized) { - throw IllegalStateException("Already initialized") - } + @JvmStatic + @MainThread + internal fun create(client: Client): AuthResultLiveData { + if (::instance.isInitialized) { + throw IllegalStateException("Already initialized") + } - instance = AuthResultLiveData(client) - return instance + instance = AuthResultLiveData(client) + return instance + } } } -} diff --git a/webflows/src/main/java/com/schibsted/account/webflows/activities/AuthorizationManagementActivity.kt b/webflows/src/main/java/com/schibsted/account/webflows/activities/AuthorizationManagementActivity.kt index bee6fc3b..9d1d426f 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/activities/AuthorizationManagementActivity.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/activities/AuthorizationManagementActivity.kt @@ -24,7 +24,6 @@ * * Removal of usage of custom types for compatibility. */ - package com.schibsted.account.webflows.activities import android.app.Activity @@ -202,7 +201,7 @@ class AuthorizationManagementActivity : Activity() { fun setup( client: Client, completionIntent: PendingIntent? = null, - cancelIntent: PendingIntent? = null + cancelIntent: PendingIntent? = null, ) { Companion.client = client AuthResultLiveData.create(client) @@ -217,7 +216,10 @@ class AuthorizationManagementActivity : Activity() { * @throws IllegalStateException if {@link AuthorizationManagementActivity#setup) has not * been called before this */ - internal fun createStartIntent(context: Context, authIntent: Intent): Intent { + internal fun createStartIntent( + context: Context, + authIntent: Intent, + ): Intent { if (AuthResultLiveData.getIfInitialised() == null) { throw IllegalStateException("AuthorizationManagementActivity.setup must be called before this") } @@ -232,7 +234,10 @@ class AuthorizationManagementActivity : Activity() { * @param context the package context for the app. * @param responseUri the response URI, which carries the parameters describing the response. */ - internal fun createResponseHandlingIntent(context: Context, responseUri: Uri?): Intent { + internal fun createResponseHandlingIntent( + context: Context, + responseUri: Uri?, + ): Intent { return createBaseIntent(context).apply { data = responseUri addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) diff --git a/webflows/src/main/java/com/schibsted/account/webflows/activities/RedirectUriReceiverActivity.kt b/webflows/src/main/java/com/schibsted/account/webflows/activities/RedirectUriReceiverActivity.kt index c01dab84..fe4c0fac 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/activities/RedirectUriReceiverActivity.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/activities/RedirectUriReceiverActivity.kt @@ -54,7 +54,7 @@ class RedirectUriReceiverActivity : Activity() { // ensures that we can remove the browser tab from the back stack. See the documentation // on AuthorizationManagementActivity for more details. startActivity( - AuthorizationManagementActivity.createResponseHandlingIntent(this, intent.data) + AuthorizationManagementActivity.createResponseHandlingIntent(this, intent.data), ) finish() } diff --git a/webflows/src/main/java/com/schibsted/account/webflows/api/ApiResultCallback.kt b/webflows/src/main/java/com/schibsted/account/webflows/api/ApiResultCallback.kt index b6338252..ee36624e 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/api/ApiResultCallback.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/api/ApiResultCallback.kt @@ -7,18 +7,25 @@ import retrofit2.Callback import retrofit2.Response internal fun responseToResult(response: Response): ApiResult { - val body = response.body() ?: return Left( - HttpError.ErrorResponse(response.code(), response.errorBody()?.string()) - ) + val body = + response.body() ?: return Left( + HttpError.ErrorResponse(response.code(), response.errorBody()?.string()), + ) return Right(body) } internal class ApiResultCallback(private val callback: (ApiResult) -> Unit) : Callback { - override fun onFailure(call: Call, t: Throwable) { + override fun onFailure( + call: Call, + t: Throwable, + ) { callback(Left(HttpError.UnexpectedError(t))) } - override fun onResponse(call: Call, response: Response) { + override fun onResponse( + call: Call, + response: Response, + ) { callback(responseToResult(response)) } } diff --git a/webflows/src/main/java/com/schibsted/account/webflows/api/HttpError.kt b/webflows/src/main/java/com/schibsted/account/webflows/api/HttpError.kt index 5f57cd7b..090b02e0 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/api/HttpError.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/api/HttpError.kt @@ -2,5 +2,6 @@ package com.schibsted.account.webflows.api sealed class HttpError { data class ErrorResponse(val code: Int, val body: String?) : HttpError() + data class UnexpectedError(val cause: Throwable) : HttpError() } diff --git a/webflows/src/main/java/com/schibsted/account/webflows/api/JWKSetDeserializer.kt b/webflows/src/main/java/com/schibsted/account/webflows/api/JWKSetDeserializer.kt index 0dbaf7f4..9db5aa03 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/api/JWKSetDeserializer.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/api/JWKSetDeserializer.kt @@ -12,7 +12,7 @@ internal class JWKSetDeserializer : JsonDeserializer { override fun deserialize( json: JsonElement, typeOfT: Type, - context: JsonDeserializationContext + context: JsonDeserializationContext, ): JWKSet { try { return JWKSet.parse(json.toString()) diff --git a/webflows/src/main/java/com/schibsted/account/webflows/api/SDKUserAgentHeaderInterceptor.kt b/webflows/src/main/java/com/schibsted/account/webflows/api/SDKUserAgentHeaderInterceptor.kt index c1e64198..55f45f21 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/api/SDKUserAgentHeaderInterceptor.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/api/SDKUserAgentHeaderInterceptor.kt @@ -5,14 +5,16 @@ import com.schibsted.account.webflows.BuildConfig import okhttp3.Interceptor internal class SDKUserAgentHeaderInterceptor : Interceptor { - val userAgentHeaderValue: String = "AccountSDKAndroidWeb/${BuildConfig.VERSION_NAME} " + + val userAgentHeaderValue: String = + "AccountSDKAndroidWeb/${BuildConfig.VERSION_NAME} " + "(Linux; Android ${Build.VERSION.RELEASE}; API ${Build.VERSION.SDK_INT}; " + "${Build.MANUFACTURER}; ${Build.MODEL})" override fun intercept(chain: Interceptor.Chain): okhttp3.Response { - val request = chain.request().newBuilder() - .header("User-Agent", userAgentHeaderValue) - .build() + val request = + chain.request().newBuilder() + .header("User-Agent", userAgentHeaderValue) + .build() return chain.proceed(request) } } diff --git a/webflows/src/main/java/com/schibsted/account/webflows/api/SchibstedAccountApi.kt b/webflows/src/main/java/com/schibsted/account/webflows/api/SchibstedAccountApi.kt index be6b70aa..7d889474 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/api/SchibstedAccountApi.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/api/SchibstedAccountApi.kt @@ -19,16 +19,18 @@ private fun ApiResult>.unpack(): ApiResult } internal class SchibstedAccountApi(baseUrl: HttpUrl, okHttpClient: OkHttpClient) { - private val retrofit: Retrofit = Retrofit.Builder() - .baseUrl(baseUrl.toString()) - .addConverterFactory(createGsonConverterFactory()) - .client(apiHttpClient(okHttpClient.newBuilder())) - .build() + private val retrofit: Retrofit = + Retrofit.Builder() + .baseUrl(baseUrl.toString()) + .addConverterFactory(createGsonConverterFactory()) + .client(apiHttpClient(okHttpClient.newBuilder())) + .build() private fun createGsonConverterFactory(): GsonConverterFactory { - val gson = GsonBuilder() - .registerTypeAdapter(JWKSet::class.java, JWKSetDeserializer()) - .create() + val gson = + GsonBuilder() + .registerTypeAdapter(JWKSet::class.java, JWKSetDeserializer()) + .create() return GsonConverterFactory.create(gson) } @@ -43,14 +45,15 @@ internal class SchibstedAccountApi(baseUrl: HttpUrl, okHttpClient: OkHttpClient) fun makeTokenRequest( tokenRequest: UserTokenRequest, - callback: (ApiResult) -> Unit + callback: (ApiResult) -> Unit, ) { - val params = mutableMapOf( - "client_id" to tokenRequest.clientId, - "grant_type" to "authorization_code", - "code" to tokenRequest.authCode, - "redirect_uri" to tokenRequest.redirectUri - ) + val params = + mutableMapOf( + "client_id" to tokenRequest.clientId, + "grant_type" to "authorization_code", + "code" to tokenRequest.authCode, + "redirect_uri" to tokenRequest.redirectUri, + ) tokenRequest.codeVerifier?.let { codeVerifier -> params["code_verifier"] = codeVerifier } @@ -59,11 +62,12 @@ internal class SchibstedAccountApi(baseUrl: HttpUrl, okHttpClient: OkHttpClient) } fun makeTokenRequest(tokenRequest: RefreshTokenRequest): ApiResult { - val params = mutableMapOf( - "client_id" to tokenRequest.clientId, - "grant_type" to "refresh_token", - "refresh_token" to tokenRequest.refreshToken, - ) + val params = + mutableMapOf( + "client_id" to tokenRequest.clientId, + "grant_type" to "refresh_token", + "refresh_token" to tokenRequest.refreshToken, + ) tokenRequest.scope?.let { scope -> params["scope"] = scope } @@ -79,7 +83,10 @@ internal class SchibstedAccountApi(baseUrl: HttpUrl, okHttpClient: OkHttpClient) schaccService.jwks().enqueue(ApiResultCallback(callback)) } - fun userProfile(user: User, callback: (ApiResult) -> Unit) { + fun userProfile( + user: User, + callback: (ApiResult) -> Unit, + ) { proctectedSchaccApi(user) { service -> service.userProfile(user.uuid) .enqueue(ApiResultCallback { callback(it.unpack()) }) @@ -91,13 +98,14 @@ internal class SchibstedAccountApi(baseUrl: HttpUrl, okHttpClient: OkHttpClient) clientId: String, redirectUri: String, state: String?, - callback: (ApiResult) -> Unit + callback: (ApiResult) -> Unit, ) { - val params = mutableMapOf( - "type" to "session", - "clientId" to clientId, - "redirectUri" to redirectUri - ) + val params = + mutableMapOf( + "type" to "session", + "clientId" to clientId, + "redirectUri" to redirectUri, + ) state?.let { params["state"] = it } @@ -111,7 +119,7 @@ internal class SchibstedAccountApi(baseUrl: HttpUrl, okHttpClient: OkHttpClient) fun codeExchange( user: User, clientId: String, - callback: (ApiResult) -> Unit + callback: (ApiResult) -> Unit, ) { proctectedSchaccApi(user) { service -> service.codeExchange(clientId) @@ -121,16 +129,18 @@ internal class SchibstedAccountApi(baseUrl: HttpUrl, okHttpClient: OkHttpClient) private fun proctectedSchaccApi( user: User, - block: (SchibstedAccountTokenProtectedService) -> Unit + block: (SchibstedAccountTokenProtectedService) -> Unit, ) { - val httpClient = user.httpClient.newBuilder() - .addInterceptor(SDKUserAgentHeaderInterceptor()) - .build() - - val protectedSchaccService = retrofit.newBuilder() - .client(httpClient) - .build() - .create(SchibstedAccountTokenProtectedService::class.java) + val httpClient = + user.httpClient.newBuilder() + .addInterceptor(SDKUserAgentHeaderInterceptor()) + .build() + + val protectedSchaccService = + retrofit.newBuilder() + .client(httpClient) + .build() + .create(SchibstedAccountTokenProtectedService::class.java) block(protectedSchaccService) } } diff --git a/webflows/src/main/java/com/schibsted/account/webflows/api/SchibstedAccountService.kt b/webflows/src/main/java/com/schibsted/account/webflows/api/SchibstedAccountService.kt index a3d0af4c..e919181f 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/api/SchibstedAccountService.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/api/SchibstedAccountService.kt @@ -8,7 +8,9 @@ internal interface SchibstedAccountService { @Headers("X-OIDC: v1") @FormUrlEncoded @POST("/oauth/token") - fun tokenRequest(@FieldMap params: Map): Call + fun tokenRequest( + @FieldMap params: Map, + ): Call @GET("/oauth/jwks") fun jwks(): Call @@ -18,11 +20,15 @@ internal interface SchibstedAccountTokenProtectedService { data class SchibstedAccountApiResponse(val data: T) @GET("/api/2/user/{userId}") - fun userProfile(@Path("userId") userId: String): Call> + fun userProfile( + @Path("userId") userId: String, + ): Call> @FormUrlEncoded @POST("/api/2/oauth/exchange") - fun sessionExchange(@FieldMap params: Map): Call> + fun sessionExchange( + @FieldMap params: Map, + ): Call> @FormUrlEncoded @POST("/api/2/oauth/exchange") diff --git a/webflows/src/main/java/com/schibsted/account/webflows/api/TokenRequest.kt b/webflows/src/main/java/com/schibsted/account/webflows/api/TokenRequest.kt index d7e22b36..774a23a5 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/api/TokenRequest.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/api/TokenRequest.kt @@ -7,15 +7,15 @@ internal data class UserTokenResponse( val refresh_token: String?, val id_token: String?, val scope: String?, - val expires_in: Int + val expires_in: Int, ) { override fun toString(): String { return "UserTokenResponse(\n" + - "access_token: ${Util.removeJwtSignature(access_token)},\n" + - "refresh_token: ${Util.removeJwtSignature(refresh_token)}, \n" + - "id_token: ${Util.removeJwtSignature(id_token)},\n" + - "scope: ${scope ?: ""},\n" + - "expires_in: ${expires_in})" + "access_token: ${Util.removeJwtSignature(access_token)},\n" + + "refresh_token: ${Util.removeJwtSignature(refresh_token)}, \n" + + "id_token: ${Util.removeJwtSignature(id_token)},\n" + + "scope: ${scope ?: ""},\n" + + "expires_in: ${expires_in})" } } @@ -23,11 +23,11 @@ internal data class UserTokenRequest( val authCode: String, val codeVerifier: String?, val clientId: String, - val redirectUri: String + val redirectUri: String, ) internal data class RefreshTokenRequest( val refreshToken: String, val scope: String?, - val clientId: String + val clientId: String, ) diff --git a/webflows/src/main/java/com/schibsted/account/webflows/api/UserProfileResponse.kt b/webflows/src/main/java/com/schibsted/account/webflows/api/UserProfileResponse.kt index 31d68d8c..d9642828 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/api/UserProfileResponse.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/api/UserProfileResponse.kt @@ -36,7 +36,7 @@ data class UserProfileResponse( val locale: String? = null, val utcOffset: String? = null, val pairId: String? = null, - val sdrn: String? = null + val sdrn: String? = null, ) interface Identifier { @@ -52,7 +52,7 @@ data class Email( override val type: String? = null, override val primary: Boolean? = null, override val verified: Boolean? = null, - override val verifiedTime: String? = null + override val verifiedTime: String? = null, ) : Identifier data class PhoneNumber( @@ -60,20 +60,20 @@ data class PhoneNumber( override val type: String? = null, override val primary: Boolean? = null, override val verified: Boolean? = null, - override val verifiedTime: String? = null + override val verifiedTime: String? = null, ) : Identifier data class Name( val givenName: String? = null, val familyName: String? = null, - val formatted: String? = null + val formatted: String? = null, ) data class Account( val id: String? = null, val accountName: String? = null, val domain: String? = null, - val connected: String? = null + val connected: String? = null, ) data class Address( @@ -83,9 +83,8 @@ data class Address( val locality: String? = null, val region: String? = null, val country: String? = null, - val type: AddressType? = null + val type: AddressType? = null, ) { - enum class AddressType { @SerializedName("home") HOME, @@ -94,14 +93,19 @@ data class Address( DELIVERY, @SerializedName("invoice") - INVOICE; + INVOICE, + + ; override fun toString(): String = super.toString().lowercase(Locale.ROOT) } } private class BirthdayTypeAdapter : TypeAdapter() { - override fun write(out: JsonWriter, value: String?) { + override fun write( + out: JsonWriter, + value: String?, + ) { if (value != null) { out.value(value) } @@ -119,7 +123,10 @@ private class BirthdayTypeAdapter : TypeAdapter() { } private class StringOrIgnoreTypeAdapter : TypeAdapter() { - override fun write(out: JsonWriter, value: String?) { + override fun write( + out: JsonWriter, + value: String?, + ) { if (value != null) { out.value(value) } diff --git a/webflows/src/main/java/com/schibsted/account/webflows/client/AuthRequest.kt b/webflows/src/main/java/com/schibsted/account/webflows/client/AuthRequest.kt index 44241999..47e3a834 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/client/AuthRequest.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/client/AuthRequest.kt @@ -9,8 +9,10 @@ package com.schibsted.account.webflows.client * @property mfa Optional MFA verification to prompt the user with. * @property loginHint User identifier to be prefilled in the login flow. */ -data class AuthRequest @JvmOverloads constructor( - val extraScopeValues: Set = setOf(), - val mfa: MfaType? = null, - val loginHint: String? = null -) +data class AuthRequest + @JvmOverloads + constructor( + val extraScopeValues: Set = setOf(), + val mfa: MfaType? = null, + val loginHint: String? = null, + ) diff --git a/webflows/src/main/java/com/schibsted/account/webflows/client/AuthState.kt b/webflows/src/main/java/com/schibsted/account/webflows/client/AuthState.kt index d0011c4d..764aa1b2 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/client/AuthState.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/client/AuthState.kt @@ -10,15 +10,15 @@ internal data class AuthState( val state: String, val nonce: String, val codeVerifier: String, - val mfa: MfaType? + val mfa: MfaType?, ) enum class MfaType(val value: String) { PASSWORD("password"), OTP("otp"), SMS("sms"), - EID_NO("eid-no"), //Only used for PRE environment - EID_SE("eid-se"), //Only used for PRE environment - EID_FI("eid-fi"), //Only used for PRE environment + EID_NO("eid-no"), // Only used for PRE environment + EID_SE("eid-se"), // Only used for PRE environment + EID_FI("eid-fi"), // Only used for PRE environment EID("eid"), // For Production } diff --git a/webflows/src/main/java/com/schibsted/account/webflows/client/Client.kt b/webflows/src/main/java/com/schibsted/account/webflows/client/Client.kt index d3204e16..099cc6f4 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/client/Client.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/client/Client.kt @@ -3,7 +3,6 @@ package com.schibsted.account.webflows.client import android.content.Context import android.content.Intent import android.net.Uri -import android.util.Log import androidx.browser.customtabs.CustomTabsIntent import androidx.fragment.app.FragmentManager import com.schibsted.account.webflows.activities.AuthorizationManagementActivity @@ -14,7 +13,6 @@ import com.schibsted.account.webflows.loginPrompt.LoginPromptManager import com.schibsted.account.webflows.loginPrompt.SessionInfoManager import com.schibsted.account.webflows.persistence.EncryptedSharedPrefsStorage import com.schibsted.account.webflows.persistence.MigratingSessionStorage -import com.schibsted.account.webflows.persistence.ObfuscatedSessionFinder import com.schibsted.account.webflows.persistence.SessionStorage import com.schibsted.account.webflows.persistence.SharedPrefsStorage import com.schibsted.account.webflows.persistence.StateStorage @@ -59,7 +57,7 @@ class Client { context: Context, configuration: ClientConfiguration, httpClient: OkHttpClient, - logoutCallback: (() -> Unit)? = null + logoutCallback: (() -> Unit)? = null, ) { this.configuration = configuration stateStorage = StateStorage(context.applicationContext) @@ -68,10 +66,11 @@ class Client { val sharedPrefsStorage = SharedPrefsStorage(context.applicationContext, configuration.serverUrl.toString()) - sessionStorage = MigratingSessionStorage( - newStorage = sharedPrefsStorage, - previousStorage = encryptedStorage - ) + sessionStorage = + MigratingSessionStorage( + newStorage = sharedPrefsStorage, + previousStorage = encryptedStorage, + ) schibstedAccountApi = SchibstedAccountApi(configuration.serverUrl.toString().toHttpUrl(), httpClient) @@ -87,7 +86,7 @@ class Client { sessionStorage: SessionStorage, httpClient: OkHttpClient, tokenHandler: TokenHandler, - schibstedAccountApi: SchibstedAccountApi + schibstedAccountApi: SchibstedAccountApi, ) { this.configuration = configuration this.stateStorage = stateStorage @@ -114,14 +113,15 @@ class Client { authRequest: AuthRequest = AuthRequest(), ): Intent { val loginUrl = generateLoginUrl(authRequest, state) - val intent: Intent = if (isCustomTabsSupported(context)) { - buildCustomTabsIntent() - .apply { - intent.data = loginUrl - }.intent - } else { - Intent(Intent.ACTION_VIEW, loginUrl).addCategory(Intent.CATEGORY_BROWSABLE) - } + val intent: Intent = + if (isCustomTabsSupported(context)) { + buildCustomTabsIntent() + .apply { + intent.data = loginUrl + }.intent + } else { + Intent(Intent.ACTION_VIEW, loginUrl).addCategory(Intent.CATEGORY_BROWSABLE) + } return AuthorizationManagementActivity.createStartIntent(context, intent) } @@ -133,7 +133,11 @@ class Client { * @param authRequest Optional [AuthRequest] parameter. */ @JvmOverloads - fun launchAuth(context: Context, state: String?, authRequest: AuthRequest = AuthRequest()) { + fun launchAuth( + context: Context, + state: String?, + authRequest: AuthRequest = AuthRequest(), + ) { val loginUrl = generateLoginUrl(authRequest, state) if (isCustomTabsSupported(context)) { buildCustomTabsIntent().launchUrl(context, loginUrl) @@ -152,13 +156,18 @@ class Client { * @return External id. */ @JvmOverloads - fun getExternalId(pairId: String, externalParty: String, optionalSuffix: String = ""): String? { + fun getExternalId( + pairId: String, + externalParty: String, + optionalSuffix: String = "", + ): String? { return try { val stringToHash = - if (optionalSuffix.isEmpty()) + if (optionalSuffix.isEmpty()) { "$pairId:$externalParty" - else + } else { "$pairId:$externalParty:$optionalSuffix" + } MessageDigest .getInstance(SHA256) .digest(stringToHash.toByteArray()) @@ -189,7 +198,10 @@ class Client { * @param authRequest Authentication request parameters. * @param state State. */ - private fun generateLoginUrl(authRequest: AuthRequest, state: String?): Uri { + private fun generateLoginUrl( + authRequest: AuthRequest, + state: String?, + ): Uri { val loginUrl = urlBuilder.loginUrl(authRequest, state) return Uri.parse(loginUrl) } @@ -200,25 +212,31 @@ class Client { * This only needs to be used if manually starting the login flow using [launchAuth]. * If using [getAuthenticationIntent] this step will be handled for you. */ - fun handleAuthenticationResponse(intent: Intent, callback: LoginResultHandler) { - val authResponse = intent.data?.query ?: return callback( - Left(LoginError.UnexpectedError("No authentication response")) - ) + fun handleAuthenticationResponse( + intent: Intent, + callback: LoginResultHandler, + ) { + val authResponse = + intent.data?.query ?: return callback( + Left(LoginError.UnexpectedError("No authentication response")), + ) handleAuthenticationResponse(authResponse, callback) } private fun handleAuthenticationResponse( authResponseParameters: String, - callback: LoginResultHandler + callback: LoginResultHandler, ) { val authResponse = Util.parseQueryParameters(authResponseParameters) - val stored = stateStorage.getValue(AUTH_STATE_KEY, AuthState::class) - ?: return callback(Left(LoginError.AuthStateReadError)) - - val eidUserCancelError = mapOf( - "error" to "access_denied", - "error_description" to "EID authentication was canceled by the user" - ) + val stored = + stateStorage.getValue(AUTH_STATE_KEY, AuthState::class) + ?: return callback(Left(LoginError.AuthStateReadError)) + + val eidUserCancelError = + mapOf( + "error" to "access_denied", + "error_description" to "EID authentication was canceled by the user", + ) val error = authResponse["error"] val errorDescription = authResponse["error_description"] if (error != null && error == eidUserCancelError["error"] && errorDescription == eidUserCancelError["error_description"]) { @@ -233,7 +251,7 @@ class Client { return } - //stateStorage.removeValue(AUTH_STATE_KEY) + // stateStorage.removeValue(AUTH_STATE_KEY) if (error != null) { val oauthError = OAuthError(error, errorDescription) @@ -241,8 +259,9 @@ class Client { return } - val authCode = authResponse["code"] - ?: return callback(Left(LoginError.UnexpectedError("Missing authorization code in authentication response"))) + val authCode = + authResponse["code"] + ?: return callback(Left(LoginError.UnexpectedError("Missing authorization code in authentication response"))) makeTokenRequest(authCode, stored) { storedUserSession -> storedUserSession .onSuccess { session -> @@ -266,16 +285,17 @@ class Client { internal fun makeTokenRequest( authCode: String, authState: AuthState?, - callback: (Either) -> Unit + callback: (Either) -> Unit, ) { tokenHandler.makeTokenRequest(authCode, authState) { result -> - val session: Either = result.map { tokenResponse -> - StoredUserSession( - configuration.clientId, - tokenResponse.userTokens, - Date() - ) - } + val session: Either = + result.map { tokenResponse -> + StoredUserSession( + configuration.clientId, + tokenResponse.userTokens, + Date(), + ) + } callback(session) } } @@ -314,10 +334,11 @@ class Client { if (tokens != null) { val newAccessToken = result.value.access_token val newRefreshToken = result.value.refresh_token ?: refreshToken - val userTokens = tokens.copy( - accessToken = newAccessToken, - refreshToken = newRefreshToken - ) + val userTokens = + tokens.copy( + accessToken = newAccessToken, + refreshToken = newRefreshToken, + ) user.tokens = userTokens val userSession = StoredUserSession(configuration.clientId, userTokens, Date()) @@ -348,17 +369,19 @@ class Client { suspend fun requestLoginPrompt( context: Context, supportFragmentManager: FragmentManager, - isCancelable: Boolean = true + isCancelable: Boolean = true, ): Boolean { val internalSessionFound = hasSessionStorage(configuration.clientId) return if (!internalSessionFound && userHasSessionOnDevice(context.applicationContext)) { LoginPromptManager( LoginPromptConfig( this.getAuthenticationIntent(context, null), - isCancelable - ) + isCancelable, + ), ).showLoginPromptIfAbsent(supportFragmentManager) - } else false + } else { + false + } } private suspend fun hasSessionStorage(clientId: String) = @@ -373,7 +396,7 @@ class Client { private suspend fun userHasSessionOnDevice(context: Context): Boolean { return SessionInfoManager( context, - configuration.serverUrl.toString() + configuration.serverUrl.toString(), ).isUserLoggedInOnTheDevice() } @@ -390,7 +413,7 @@ data class OAuthError(val error: String, val errorDescription: String?) { val parsed = JSONObject(json) OAuthError( parsed.getString("error"), - parsed.optString("error_description") + parsed.optString("error_description"), ) } catch (e: JSONException) { null @@ -438,9 +461,13 @@ sealed class LoginError { sealed class RefreshTokenError { object NoRefreshToken : RefreshTokenError() + object ConcurrentRefreshFailure : RefreshTokenError() + object UserWasLoggedOut : RefreshTokenError() + data class RefreshRequestFailed(val error: HttpError) : RefreshTokenError() + data class UnexpectedError(val message: String) : RefreshTokenError() } diff --git a/webflows/src/main/java/com/schibsted/account/webflows/client/ClientConfiguration.kt b/webflows/src/main/java/com/schibsted/account/webflows/client/ClientConfiguration.kt index 6154a380..cfe3a422 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/client/ClientConfiguration.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/client/ClientConfiguration.kt @@ -8,6 +8,6 @@ data class ClientConfiguration(val serverUrl: URL, val clientId: String, val red constructor(env: Environment, clientId: String, redirectUri: String) : this( env.url, clientId, - redirectUri + redirectUri, ) } diff --git a/webflows/src/main/java/com/schibsted/account/webflows/client/Environment.kt b/webflows/src/main/java/com/schibsted/account/webflows/client/Environment.kt index 072132e9..a594cf68 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/client/Environment.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/client/Environment.kt @@ -7,5 +7,5 @@ enum class Environment(val url: URL) { PRO_NO(URL("https://payment.schibsted.no")), PRO_FI(URL("https://login.schibsted.fi")), PRO_DK(URL("https://login.schibsted.dk")), - PRE(URL("https://identity-pre.schibsted.com")) + PRE(URL("https://identity-pre.schibsted.com")), } diff --git a/webflows/src/main/java/com/schibsted/account/webflows/client/UrlBuilder.kt b/webflows/src/main/java/com/schibsted/account/webflows/client/UrlBuilder.kt index 26b2af69..a6ab7474 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/client/UrlBuilder.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/client/UrlBuilder.kt @@ -8,34 +8,38 @@ import java.security.MessageDigest internal class UrlBuilder( private val clientConfig: ClientConfiguration, private val stateStorage: StateStorage, - private val authStateKey: String + private val authStateKey: String, ) { private val defaultScopeValues = setOf("openid", "offline_access") - fun loginUrl(authRequest: AuthRequest, state: String? = null): String { + fun loginUrl( + authRequest: AuthRequest, + state: String? = null, + ): String { val stateValue = state?.let { state } ?: Util.randomString(10) val nonce = Util.randomString(10) val codeVerifier = Util.randomString(60) stateStorage.setValue( authStateKey, - AuthState(stateValue, nonce, codeVerifier, authRequest.mfa) + AuthState(stateValue, nonce, codeVerifier, authRequest.mfa), ) val scopes = authRequest.extraScopeValues.union(defaultScopeValues) val scopeString = scopes.joinToString(" ") val codeChallenge = computeCodeChallenge(codeVerifier) - val authParams: MutableMap = mutableMapOf( - "client_id" to clientConfig.clientId, - "redirect_uri" to clientConfig.redirectUri, - "response_type" to "code", - "state" to stateValue, - "scope" to scopeString, - "nonce" to nonce, - "code_challenge" to codeChallenge, - "code_challenge_method" to "S256" - ) + val authParams: MutableMap = + mutableMapOf( + "client_id" to clientConfig.clientId, + "redirect_uri" to clientConfig.redirectUri, + "response_type" to "code", + "state" to stateValue, + "scope" to scopeString, + "nonce" to nonce, + "code_challenge" to codeChallenge, + "code_challenge_method" to "S256", + ) if (authRequest.loginHint != null) { authParams["login_hint"] = authRequest.loginHint diff --git a/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/LoginPromptContentProvider.kt b/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/LoginPromptContentProvider.kt index 63a9b5b0..f2dc826d 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/LoginPromptContentProvider.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/LoginPromptContentProvider.kt @@ -11,7 +11,6 @@ import com.schibsted.account.webflows.tracking.SchibstedAccountTracker import com.schibsted.account.webflows.tracking.SchibstedAccountTrackingEvent internal class LoginPromptContentProvider : ContentProvider() { - private lateinit var uriMatcher: UriMatcher private lateinit var db: SessionInfoDatabase private lateinit var contentURI: Uri @@ -33,7 +32,9 @@ internal class LoginPromptContentProvider : ContentProvider() { contentURI = Uri.parse(providerUrl) uriMatcher = UriMatcher(UriMatcher.NO_MATCH) uriMatcher.addURI( - providerName, "sessions", uriCode + providerName, + "sessions", + uriCode, ) return db != null @@ -44,7 +45,7 @@ internal class LoginPromptContentProvider : ContentProvider() { projection: Array?, selection: String?, selectionArgs: Array?, - sortOrder: String? + sortOrder: String?, ): Cursor? { if (uriMatcher.match(uri) != uriCode) { throw IllegalArgumentException("Unknown URI $uri") @@ -52,7 +53,10 @@ internal class LoginPromptContentProvider : ContentProvider() { return db?.getSessions(selectionArgs?.first() as String) } - override fun insert(uri: Uri, values: ContentValues?): Uri { + override fun insert( + uri: Uri, + values: ContentValues?, + ): Uri { val rowId = db?.saveSessionTimestamp(values?.get("packageName") as String, values?.get("serverUrl") as String) if (rowId != null) { val uri: Uri = ContentUris.withAppendedId(contentURI, rowId) @@ -64,13 +68,18 @@ internal class LoginPromptContentProvider : ContentProvider() { } override fun update( - uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array? + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array?, ): Int { return 0 } override fun delete( - uri: Uri, selection: String?, selectionArgs: Array? + uri: Uri, + selection: String?, + selectionArgs: Array?, ): Int { val rowsAffected = db?.clearSessionsForPackage(selectionArgs?.first() as String) SchibstedAccountTracker.track(SchibstedAccountTrackingEvent.LoginPromptContentProviderDelete) diff --git a/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/LoginPromptFragment.kt b/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/LoginPromptFragment.kt index 7b1bb74a..fb703d18 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/LoginPromptFragment.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/LoginPromptFragment.kt @@ -41,15 +41,19 @@ internal class LoginPromptFragment : BottomSheetDialogFragment() { savedInstanceState: Bundle?, ): View { super.onCreateView(inflater, container, savedInstanceState) - view = LayoutInflater.from(requireContext()).inflate( - R.layout.login_prompt, - container, - false - ) + view = + LayoutInflater.from(requireContext()).inflate( + R.layout.login_prompt, + container, + false, + ) return view } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { super.onViewCreated(view, savedInstanceState) initializeButtons() SchibstedAccountTracker.track(SchibstedAccountTrackingEvent.LoginPromptCreated) @@ -95,8 +99,8 @@ internal class LoginPromptFragment : BottomSheetDialogFragment() { startActivity( Intent( Intent.ACTION_VIEW, - uri - ).addCategory(Intent.CATEGORY_BROWSABLE) + uri, + ).addCategory(Intent.CATEGORY_BROWSABLE), ) } } @@ -105,5 +109,4 @@ internal class LoginPromptFragment : BottomSheetDialogFragment() { companion object { const val ARG_CONFIG = "LOGIN_PROMPT_CONFIG_ARG" } - } diff --git a/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/LoginPromptManager.kt b/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/LoginPromptManager.kt index dcd4883d..9f954f6b 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/LoginPromptManager.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/LoginPromptManager.kt @@ -6,11 +6,10 @@ import android.os.Parcelable import androidx.fragment.app.FragmentManager import kotlinx.parcelize.Parcelize - @Parcelize internal data class LoginPromptConfig( val authIntent: Intent, - val isCancelable: Boolean = true + val isCancelable: Boolean = true, ) : Parcelable internal class LoginPromptManager(private val loginPromptConfig: LoginPromptConfig) { @@ -22,20 +21,23 @@ internal class LoginPromptManager(private val loginPromptConfig: LoginPromptConf * @param supportFragmentManager Calling entity's fragment manager. * @return true if login prompt is shown, false otherwise. */ - fun showLoginPromptIfAbsent(supportFragmentManager: FragmentManager) : Boolean{ + fun showLoginPromptIfAbsent(supportFragmentManager: FragmentManager): Boolean { val loginPromptFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as? LoginPromptFragment return if (loginPromptFragment == null) { initializeLoginPrompt(loginPromptConfig).show(supportFragmentManager, fragmentTag) true - } else false + } else { + false + } } private fun initializeLoginPrompt(config: LoginPromptConfig): LoginPromptFragment = LoginPromptFragment().apply { - arguments = Bundle().apply { - putParcelable(LoginPromptFragment.ARG_CONFIG, config) - } + arguments = + Bundle().apply { + putParcelable(LoginPromptFragment.ARG_CONFIG, config) + } } -} \ No newline at end of file +} diff --git a/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/SessionInfoDatabase.kt b/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/SessionInfoDatabase.kt index 00865d3a..6322289d 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/SessionInfoDatabase.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/SessionInfoDatabase.kt @@ -7,12 +7,11 @@ import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper import java.util.Date - internal class SessionInfoDatabase(context: Context) : SQLiteOpenHelper( context, DATABASE_NAME, null, - DATABASE_VERSION + DATABASE_VERSION, ) { companion object { const val DATABASE_NAME = "SessionInfoDB" @@ -33,18 +32,22 @@ internal class SessionInfoDatabase(context: Context) : SQLiteOpenHelper( override fun onUpgrade( db: SQLiteDatabase, oldVersion: Int, - newVersion: Int + newVersion: Int, ) { db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME") onCreate(db) } - fun saveSessionTimestamp(packageName: String, serverUrl: String): Long { - val values = ContentValues().apply { - put("packageName", packageName) - put("timestamp", Date().time) - put("serverUrl", serverUrl) - } + fun saveSessionTimestamp( + packageName: String, + serverUrl: String, + ): Long { + val values = + ContentValues().apply { + put("packageName", packageName) + put("timestamp", Date().time) + put("serverUrl", serverUrl) + } return this.writableDatabase.insertWithOnConflict( TABLE_NAME, null, @@ -56,9 +59,9 @@ internal class SessionInfoDatabase(context: Context) : SQLiteOpenHelper( fun getSessions(serverUrl: String): Cursor? { val projection = arrayOf("packageName", "timestamp", "serverUrl") val selection = "timestamp > ? AND serverUrl = ?" - //get sessions the last year period + // get sessions the last year period val oneYearInMilliseconds = 365 * 24 * 60 * 60 * 1000 - val arguments = arrayOf("${Date().time - (oneYearInMilliseconds)}", "${serverUrl}") + val arguments = arrayOf("${Date().time - (oneYearInMilliseconds)}", "$serverUrl") val sortOrder = "timestamp DESC" return this.readableDatabase.query( TABLE_NAME, @@ -67,7 +70,7 @@ internal class SessionInfoDatabase(context: Context) : SQLiteOpenHelper( arguments, null, null, - sortOrder + sortOrder, ) } diff --git a/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/SessionInfoManager.kt b/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/SessionInfoManager.kt index c658d7de..ac07cc06 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/SessionInfoManager.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/loginPrompt/SessionInfoManager.kt @@ -8,7 +8,6 @@ import android.content.pm.ResolveInfo import android.net.Uri import android.os.Build - internal class SessionInfoManager(context: Context, serverUrl: String) { private val contentResolver = context.contentResolver private val packageName = context.packageName @@ -17,28 +16,29 @@ internal class SessionInfoManager(context: Context, serverUrl: String) { fun save() { contentResolver.insert( - Uri.parse("content://${packageName}.contentprovider/sessions"), + Uri.parse("content://$packageName.contentprovider/sessions"), ContentValues().apply { put("packageName", packageName) put("serverUrl", serverUrl) - }) + }, + ) } fun clear() { contentResolver.delete( - Uri.parse("content://${packageName}.contentprovider/sessions"), + Uri.parse("content://$packageName.contentprovider/sessions"), null, - arrayOf(packageName) + arrayOf(packageName), ) } private fun isSessionPresent(authority: String): Boolean { return contentResolver.query( - Uri.parse("content://${authority}/sessions"), + Uri.parse("content://$authority/sessions"), null, null, arrayOf(serverUrl), - null + null, )?.use { it.count > 0 } ?: false @@ -47,19 +47,22 @@ internal class SessionInfoManager(context: Context, serverUrl: String) { suspend fun isUserLoggedInOnTheDevice(): Boolean { val contentProviders: List val intent = Intent("com.schibsted.account.LOGIN_PROMPT_CONTENT_PROVIDER") - contentProviders = when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> packageManager.queryIntentContentProviders( - intent, - PackageManager.ResolveInfoFlags.of(0) - ) + contentProviders = + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> + packageManager.queryIntentContentProviders( + intent, + PackageManager.ResolveInfoFlags.of(0), + ) - Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> packageManager.queryIntentContentProviders( - intent, - PackageManager.MATCH_ALL - ) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> + packageManager.queryIntentContentProviders( + intent, + PackageManager.MATCH_ALL, + ) - else -> packageManager.queryIntentContentProviders(intent, 0) - } + else -> packageManager.queryIntentContentProviders(intent, 0) + } for (contentProvider in contentProviders) { if (isSessionPresent(contentProvider.providerInfo.authority)) { return true diff --git a/webflows/src/main/java/com/schibsted/account/webflows/persistence/ObfuscatedSessionFinder.kt b/webflows/src/main/java/com/schibsted/account/webflows/persistence/ObfuscatedSessionFinder.kt index 6e468bf6..013c5210 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/persistence/ObfuscatedSessionFinder.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/persistence/ObfuscatedSessionFinder.kt @@ -1,6 +1,5 @@ package com.schibsted.account.webflows.persistence -import android.util.Log import com.google.gson.Gson import com.google.gson.JsonParser import com.nimbusds.jwt.JWTParser @@ -14,7 +13,7 @@ import java.util.Date * Crawls through the obfuscated session JSON to find the access token to enable auto log-in. */ -//TODO: remove Logs and printlns after RC build is accepted. +// TODO: remove Logs and printlns after RC build is accepted. internal object ObfuscatedSessionFinder { private const val USER_TOKENS_KEY = "userTokens" private const val REFRESH_TOKEN_KEY = "refreshToken" @@ -22,7 +21,6 @@ internal object ObfuscatedSessionFinder { private const val REFRESH_TOKEN_RECOGNIZE_KEY = "sid" private const val ID_TOKEN_RECOGNIZE_KEY = "nonce" - /** * Determines if the session is obfuscated. * @@ -30,7 +28,7 @@ internal object ObfuscatedSessionFinder { * @return true if the session is obfuscated, false otherwise. */ private fun isSessionObfuscated(storedUserSessionJson: String): Boolean { - val sessionJsonObject = JsonParser().parse(storedUserSessionJson).getAsJsonObject() + val sessionJsonObject = JsonParser.parseString(storedUserSessionJson).getAsJsonObject() if (sessionJsonObject.has(USER_TOKENS_KEY)) { val userTokenJsonObject = sessionJsonObject.getAsJsonObject(USER_TOKENS_KEY) // If the userTokens key and the refreshToken key is present @@ -54,14 +52,14 @@ internal object ObfuscatedSessionFinder { fun getDeobfuscatedStoredUserSessionIfViable( gson: Gson, clientId: String, - storedUserSessionJson: String? + storedUserSessionJson: String?, ): StorageReadResult { try { // based on the storedUserSessionJson, determine if the session is obfuscated storedUserSessionJson?.let { if (isSessionObfuscated(storedUserSessionJson)) { val sessionJsonObject = - JsonParser().parse(storedUserSessionJson).asJsonObject + JsonParser.parseString(storedUserSessionJson).asJsonObject var refreshToken = "" var idToken = "" var accessToken = "" @@ -81,7 +79,7 @@ internal object ObfuscatedSessionFinder { JWTParser.parse(innerValue.asString).let { jwtValue -> val payload = jwtValue.jwtClaimsSet.toPayload() val payloadJson = - JsonParser().parse(payload.toString()).asJsonObject + JsonParser.parseString(payload.toString()).asJsonObject // recognize the token based on the payload // some tokens have specific keys that can be used to recognize them val refreshTokenRecognized = @@ -95,7 +93,7 @@ internal object ObfuscatedSessionFinder { accessToken = innerValue.asString } else if (refreshTokenRecognized) { refreshToken = innerValue.asString - } else if (idTokenRecognized) { + } else { idToken = innerValue.asString } } @@ -106,11 +104,12 @@ internal object ObfuscatedSessionFinder { } } // Create the IdTokenClaims object based on the tokens - val idTokenClaims = createIdTokenClaimsBasedOnTokenJsons( - refreshToken, - accessToken, - idToken - ) + val idTokenClaims = + createIdTokenClaimsBasedOnTokenJsons( + refreshToken, + accessToken, + idToken, + ) // Create the UserTokens object based on the idTokenClaims and rest of data val userTokens = UserTokens(accessToken, refreshToken, idToken, idTokenClaims) val result = StoredUserSession(clientId, userTokens, Date()) @@ -138,25 +137,32 @@ internal object ObfuscatedSessionFinder { fun createIdTokenClaimsBasedOnTokenJsons( refreshToken: String?, accessToken: String?, - idToken: String? + idToken: String?, ): IdTokenClaims { val refreshTokenClaims = refreshToken.let { if (refreshToken.isNullOrEmpty().not()) { JWTParser.parse(refreshToken).jwtClaimsSet - } else null + } else { + null + } } val accessTokenClaims = accessToken.let { if (accessToken.isNullOrEmpty().not()) { JWTParser.parse(accessToken).jwtClaimsSet - } else null + } else { + null + } + } + val idTokenClaims = + idToken.let { + if (idToken.isNullOrEmpty().not()) { + JWTParser.parse(idToken).jwtClaimsSet + } else { + null + } } - val idTokenClaims = idToken.let { - if (idToken.isNullOrEmpty().not()) { - JWTParser.parse(idToken).jwtClaimsSet - } else null - } return IdTokenClaims( idTokenClaims?.getStringClaim("iss") ?: "", idTokenClaims?.getStringClaim("sub") ?: "", @@ -164,7 +170,7 @@ internal object ObfuscatedSessionFinder { idTokenClaims?.getStringListClaim("aud") ?: emptyList(), idTokenClaims?.getDateClaim("exp")?.time?.div(1000) ?: 0L, idTokenClaims?.getStringClaim("nonce"), - idTokenClaims?.getStringListClaim("amr") + idTokenClaims?.getStringListClaim("amr"), ) } } diff --git a/webflows/src/main/java/com/schibsted/account/webflows/persistence/SessionStorage.kt b/webflows/src/main/java/com/schibsted/account/webflows/persistence/SessionStorage.kt index 719ae712..47aef768 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/persistence/SessionStorage.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/persistence/SessionStorage.kt @@ -2,7 +2,6 @@ package com.schibsted.account.webflows.persistence import android.content.Context import android.content.SharedPreferences -import android.util.Log import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey import com.google.gson.Gson @@ -25,7 +24,12 @@ internal typealias StorageReadCallback = (StorageReadResult) -> Unit */ internal interface SessionStorage { fun save(session: StoredUserSession) - fun get(clientId: String, callback: StorageReadCallback) + + fun get( + clientId: String, + callback: StorageReadCallback, + ) + fun remove(clientId: String) } @@ -33,12 +37,14 @@ internal class MigratingSessionStorage( private val newStorage: SharedPrefsStorage, private val previousStorage: EncryptedSharedPrefsStorage, ) : SessionStorage { - override fun save(session: StoredUserSession) { newStorage.save(session) } - override fun get(clientId: String, callback: StorageReadCallback) { + override fun get( + clientId: String, + callback: StorageReadCallback, + ) { newStorage.get(clientId) { result -> result .onSuccess { newSession -> @@ -53,7 +59,10 @@ internal class MigratingSessionStorage( } } - private fun lookupPreviousStorage(clientId: String, callback: StorageReadCallback) { + private fun lookupPreviousStorage( + clientId: String, + callback: StorageReadCallback, + ) { previousStorage.get(clientId) { result -> result.onSuccess { it?.let { @@ -74,11 +83,11 @@ internal class MigratingSessionStorage( internal class EncryptedSharedPrefsStorage(context: Context) : SessionStorage { private val gson = GsonBuilder().setDateFormat("MM dd, yyyy HH:mm:ss").create() - private val prefs: SharedPreferences? by lazy { - val masterKey = MasterKey.Builder(context.applicationContext) - .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) - .build() + val masterKey = + MasterKey.Builder(context.applicationContext) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() try { EncryptedSharedPreferences.create( @@ -86,12 +95,12 @@ internal class EncryptedSharedPrefsStorage(context: Context) : SessionStorage { PREFERENCE_FILENAME, masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, ) } catch (e: GeneralSecurityException) { Timber.e( "Error occurred while trying to build encrypted shared preferences", - e + e, ) null } @@ -106,19 +115,22 @@ internal class EncryptedSharedPrefsStorage(context: Context) : SessionStorage { } catch (e: SecurityException) { Timber.e( "Error occurred while trying to write to encrypted shared preferences", - e + e, ) } } - override fun get(clientId: String, callback: StorageReadCallback) { + override fun get( + clientId: String, + callback: StorageReadCallback, + ) { try { val json = prefs?.getString(clientId, null) ?: return callback(Either.Right(null)) callback(gson.getStoredUserSession(clientId, json)) } catch (e: SecurityException) { Timber.e( "Error occurred while trying to read from encrypted shared preferences", - e + e, ) callback(Either.Left(StorageError.UnexpectedError(e))) } @@ -132,7 +144,7 @@ internal class EncryptedSharedPrefsStorage(context: Context) : SessionStorage { } catch (e: SecurityException) { Timber.e( "Error occurred while trying to delete from encrypted shared preferences", - e + e, ) } } @@ -143,7 +155,6 @@ internal class EncryptedSharedPrefsStorage(context: Context) : SessionStorage { } internal class SharedPrefsStorage(context: Context, serverUrl: String) : SessionStorage { - private val gson = GsonBuilder().setDateFormat("MM dd, yyyy HH:mm:ss").create() private val prefs = context.getSharedPreferences(PREFERENCE_FILENAME, Context.MODE_PRIVATE) private val sessionInfoManager = SessionInfoManager(context, serverUrl) @@ -155,7 +166,10 @@ internal class SharedPrefsStorage(context: Context, serverUrl: String) : Session sessionInfoManager.save() } - override fun get(clientId: String, callback: StorageReadCallback) { + override fun get( + clientId: String, + callback: StorageReadCallback, + ) { val json = prefs.getString(clientId, null) callback(gson.getStoredUserSession(clientId, json)) } @@ -172,13 +186,15 @@ internal class SharedPrefsStorage(context: Context, serverUrl: String) : Session } } - -private fun Gson.getStoredUserSession(clientId: String, json: String?): StorageReadResult { +private fun Gson.getStoredUserSession( + clientId: String, + json: String?, +): StorageReadResult { return try { ObfuscatedSessionFinder.getDeobfuscatedStoredUserSessionIfViable( this, clientId, - json + json, ) } catch (e: JsonSyntaxException) { Either.Left(StorageError.UnexpectedError(e)) diff --git a/webflows/src/main/java/com/schibsted/account/webflows/persistence/StateStorage.kt b/webflows/src/main/java/com/schibsted/account/webflows/persistence/StateStorage.kt index 6d573e8d..b9b519f4 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/persistence/StateStorage.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/persistence/StateStorage.kt @@ -17,14 +17,20 @@ internal class StateStorage(context: Context) { context.applicationContext.getSharedPreferences(PREFERENCE_FILENAME, Context.MODE_PRIVATE) } - fun setValue(key: String, value: T) { + fun setValue( + key: String, + value: T, + ) { val editor = prefs.edit() val json = gson.toJson(value) editor.putString(key, json) editor.apply() } - fun getValue(key: String, c: KClass): T? { + fun getValue( + key: String, + c: KClass, + ): T? { val json = prefs.getString(key, null) ?: return null return gson.fromJson(json, c.java) } diff --git a/webflows/src/main/java/com/schibsted/account/webflows/persistence/StorageError.kt b/webflows/src/main/java/com/schibsted/account/webflows/persistence/StorageError.kt index 028a6cbf..56385547 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/persistence/StorageError.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/persistence/StorageError.kt @@ -1,5 +1,5 @@ package com.schibsted.account.webflows.persistence sealed class StorageError { - data class UnexpectedError(val cause: Throwable): StorageError() + data class UnexpectedError(val cause: Throwable) : StorageError() } diff --git a/webflows/src/main/java/com/schibsted/account/webflows/token/IdTokenClaims.kt b/webflows/src/main/java/com/schibsted/account/webflows/token/IdTokenClaims.kt index 545dcf54..cd6ef75c 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/token/IdTokenClaims.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/token/IdTokenClaims.kt @@ -21,5 +21,5 @@ internal data class IdTokenClaims( @SerializedName("nonce") val nonce: String?, @SerializedName("amr") - val amr: List? -) : Parcelable \ No newline at end of file + val amr: List?, +) : Parcelable diff --git a/webflows/src/main/java/com/schibsted/account/webflows/token/IdTokenValidator.kt b/webflows/src/main/java/com/schibsted/account/webflows/token/IdTokenValidator.kt index 08a77f55..f55d6aef 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/token/IdTokenValidator.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/token/IdTokenValidator.kt @@ -19,6 +19,7 @@ internal sealed class IdTokenValidationError { abstract val message: String data class FailedValidation(override val message: String) : IdTokenValidationError() + data class UnexpectedError(override val message: String) : IdTokenValidationError() } @@ -27,7 +28,7 @@ internal object IdTokenValidator { idToken: String, jwks: AsyncJwks, validationContext: IdTokenValidationContext, - callback: (Either) -> Unit + callback: (Either) -> Unit, ) { // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation jwks.fetch { fetchedJwks -> @@ -42,13 +43,14 @@ internal object IdTokenValidator { private fun validate( idToken: String, jwks: JWKSet, - validationContext: IdTokenValidationContext + validationContext: IdTokenValidationContext, ): Either { val jwtProcessor = DefaultJWTProcessor() - val keySelector = JWSVerificationKeySelector( - JWSAlgorithm.RS256, - ImmutableJWKSet(jwks) - ) + val keySelector = + JWSVerificationKeySelector( + JWSAlgorithm.RS256, + ImmutableJWKSet(jwks), + ) jwtProcessor.jwsKeySelector = keySelector val expectedClaims = JWTClaimsSet.Builder() @@ -56,18 +58,19 @@ internal object IdTokenValidator { expectedClaims.claim("nonce", validationContext.nonce) } - jwtProcessor.jwtClaimsSetVerifier = IdTokenClaimsVerifier( - validationContext.clientId, - expectedClaims.build(), - setOf("sub", "exp") - ) + jwtProcessor.jwtClaimsSetVerifier = + IdTokenClaimsVerifier( + validationContext.clientId, + expectedClaims.build(), + setOf("sub", "exp"), + ) val claims: JWTClaimsSet try { claims = jwtProcessor.process(idToken, validationContext) } catch (e: BadJWTException) { return Left( - IdTokenValidationError.FailedValidation(e.message ?: "Failed to verify ID Token") + IdTokenValidationError.FailedValidation(e.message ?: "Failed to verify ID Token"), ) } @@ -79,8 +82,8 @@ internal object IdTokenValidator { claims.audience, (claims.expirationTime.time / 1000), claims.getStringClaim("nonce"), - claims.getStringListClaim("amr") - ) + claims.getStringListClaim("amr"), + ), ) } } @@ -88,9 +91,12 @@ internal object IdTokenValidator { internal class IdTokenClaimsVerifier( clientId: String, exactMatchClaims: JWTClaimsSet, - requiredClaims: Set + requiredClaims: Set, ) : DefaultJWTClaimsVerifier(clientId, exactMatchClaims, requiredClaims) { - override fun verify(claims: JWTClaimsSet?, context: IdTokenValidationContext?) { + override fun verify( + claims: JWTClaimsSet?, + context: IdTokenValidationContext?, + ) { super.verify(claims, context) // verify issuer, allowing trailing slash @@ -104,7 +110,10 @@ internal class IdTokenClaimsVerifier( } } - private fun contains(values: List?, value: String?): Boolean { + private fun contains( + values: List?, + value: String?, + ): Boolean { var needle = value ?: return true // no value to search for val haystack = values ?: return false // no values to search among @@ -123,5 +132,5 @@ internal data class IdTokenValidationContext( val issuer: String, val clientId: String, val nonce: String?, - val expectedAmr: String? + val expectedAmr: String?, ) : SecurityContext diff --git a/webflows/src/main/java/com/schibsted/account/webflows/token/TokenHandler.kt b/webflows/src/main/java/com/schibsted/account/webflows/token/TokenHandler.kt index 1e39ce2c..01d8c1f7 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/token/TokenHandler.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/token/TokenHandler.kt @@ -13,20 +13,22 @@ import timber.log.Timber internal sealed class TokenError { data class TokenRequestError(val cause: HttpError) : TokenError() + data class IdTokenNotValid(val cause: IdTokenValidationError) : TokenError() + object NoIdTokenReceived : TokenError() } internal data class UserTokensResult( val userTokens: UserTokens, val scope: String?, - val expiresIn: Int + val expiresIn: Int, ) { override fun toString(): String { return "UserTokensResult(\n" + - "userTokens: ${userTokens}\n" + - "scope: ${scope ?: ""},\n" + - "expires_in: ${expiresIn})" + "userTokens: ${userTokens}\n" + + "scope: ${scope ?: ""},\n" + + "expires_in: $expiresIn)" } } @@ -34,7 +36,7 @@ internal typealias TokenRequestResult = Either internal class TokenHandler( private val clientConfiguration: ClientConfiguration, - private val schibstedAccountApi: SchibstedAccountApi + private val schibstedAccountApi: SchibstedAccountApi, ) { private val jwks: AsyncJwks @@ -45,14 +47,15 @@ internal class TokenHandler( fun makeTokenRequest( authCode: String, authState: AuthState?, - callback: (TokenRequestResult) -> Unit + callback: (TokenRequestResult) -> Unit, ) { - val tokenRequest = UserTokenRequest( - authCode, - authState?.codeVerifier, - clientConfiguration.clientId, - clientConfiguration.redirectUri - ) + val tokenRequest = + UserTokenRequest( + authCode, + authState?.codeVerifier, + clientConfiguration.clientId, + clientConfiguration.redirectUri, + ) schibstedAccountApi.makeTokenRequest(tokenRequest) { result -> result .onSuccess { handleTokenResponse(it, authState, callback) } @@ -65,13 +68,14 @@ internal class TokenHandler( fun makeTokenRequest( refreshToken: String, - scope: String? = null + scope: String? = null, ): Either { - val tokenRequest = RefreshTokenRequest( - refreshToken, - scope, - clientConfiguration.clientId - ) + val tokenRequest = + RefreshTokenRequest( + refreshToken, + scope, + clientConfiguration.clientId, + ) return when (val result = schibstedAccountApi.makeTokenRequest(tokenRequest)) { is Right -> result is Left -> { @@ -84,9 +88,8 @@ internal class TokenHandler( private fun handleTokenResponse( tokenResponse: UserTokenResponse, authState: AuthState?, - callback: (TokenRequestResult) -> Unit + callback: (TokenRequestResult) -> Unit, ) { - val idToken = tokenResponse.id_token if (idToken == null) { Timber.e("Missing ID Token") @@ -94,30 +97,32 @@ internal class TokenHandler( return } - val idTokenValidationContext = IdTokenValidationContext( - clientConfiguration.issuer, - clientConfiguration.clientId, - authState?.nonce, - authState?.mfa?.value - ) + val idTokenValidationContext = + IdTokenValidationContext( + clientConfiguration.issuer, + clientConfiguration.clientId, + authState?.nonce, + authState?.mfa?.value, + ) IdTokenValidator.validate(idToken, jwks, idTokenValidationContext) { result -> result .onSuccess { - val tokens = UserTokens( - tokenResponse.access_token, - tokenResponse.refresh_token, - tokenResponse.id_token, - it - ) + val tokens = + UserTokens( + tokenResponse.access_token, + tokenResponse.refresh_token, + tokenResponse.id_token, + it, + ) callback( Right( UserTokensResult( tokens, tokenResponse.scope, - tokenResponse.expires_in - ) - ) + tokenResponse.expires_in, + ), + ), ) } .onFailure { callback(Left(IdTokenNotValid(it))) } diff --git a/webflows/src/main/java/com/schibsted/account/webflows/token/UserTokens.kt b/webflows/src/main/java/com/schibsted/account/webflows/token/UserTokens.kt index a26e4ad1..f0e86cbe 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/token/UserTokens.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/token/UserTokens.kt @@ -16,13 +16,13 @@ internal data class UserTokens( @SerializedName("idToken") val idToken: String, @SerializedName("idTokenClaims") - val idTokenClaims: IdTokenClaims + val idTokenClaims: IdTokenClaims, ) : Parcelable { override fun toString(): String { return "UserTokens(\n" + - "accessToken: ${Util.removeJwtSignature(accessToken)},\n" + - "refreshToken: ${Util.removeJwtSignature(refreshToken)}, \n" + - "idToken: ${Util.removeJwtSignature(idToken)},\n" + - "idTokenClaims: ${idTokenClaims})" + "accessToken: ${Util.removeJwtSignature(accessToken)},\n" + + "refreshToken: ${Util.removeJwtSignature(refreshToken)}, \n" + + "idToken: ${Util.removeJwtSignature(idToken)},\n" + + "idTokenClaims: $idTokenClaims)" } -} \ No newline at end of file +} diff --git a/webflows/src/main/java/com/schibsted/account/webflows/tracking/SchibstedAccountTracker.kt b/webflows/src/main/java/com/schibsted/account/webflows/tracking/SchibstedAccountTracker.kt index c25f8ef1..a41e3047 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/tracking/SchibstedAccountTracker.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/tracking/SchibstedAccountTracker.kt @@ -4,4 +4,4 @@ internal object SchibstedAccountTracker { internal fun track(event: SchibstedAccountTrackingEvent) { SchibstedAccountTrackerStore.notifyListeners(event) } -} \ No newline at end of file +} diff --git a/webflows/src/main/java/com/schibsted/account/webflows/tracking/SchibstedAccountTrackingEvent.kt b/webflows/src/main/java/com/schibsted/account/webflows/tracking/SchibstedAccountTrackingEvent.kt index 67fc190d..3c71a28e 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/tracking/SchibstedAccountTrackingEvent.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/tracking/SchibstedAccountTrackingEvent.kt @@ -2,7 +2,6 @@ package com.schibsted.account.webflows.tracking import com.schibsted.account.webflows.BuildConfig - const val PACKAGE_NAME = "account-sdk-android-web" const val PROVIDER_COMPONENT = "schibsted-account" @@ -40,4 +39,4 @@ sealed class SchibstedAccountTrackingEvent { object UserLoginFailed : SchibstedAccountTrackingEvent() object UserLoginCanceled : SchibstedAccountTrackingEvent() -} \ No newline at end of file +} diff --git a/webflows/src/main/java/com/schibsted/account/webflows/tracking/SchibstedAccountTrackingStore.kt b/webflows/src/main/java/com/schibsted/account/webflows/tracking/SchibstedAccountTrackingStore.kt index bebc077a..49efab65 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/tracking/SchibstedAccountTrackingStore.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/tracking/SchibstedAccountTrackingStore.kt @@ -2,6 +2,7 @@ package com.schibsted.account.webflows.tracking interface SchibstedAccountTrackerStore { fun addTrackingListener(trackingListener: SchibstedAccountTrackingListener) + fun removeTrackingListener(trackingListener: SchibstedAccountTrackingListener) companion object : SchibstedAccountTrackerStore { @@ -40,4 +41,4 @@ interface SchibstedAccountTrackerStore { interface SchibstedAccountTrackingListener { fun onEvent(event: SchibstedAccountTrackingEvent) -} \ No newline at end of file +} diff --git a/webflows/src/main/java/com/schibsted/account/webflows/user/AuthenticatedRequests.kt b/webflows/src/main/java/com/schibsted/account/webflows/user/AuthenticatedRequests.kt index 0c2d0b8e..0b6adb6d 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/user/AuthenticatedRequests.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/user/AuthenticatedRequests.kt @@ -11,9 +11,10 @@ import timber.log.Timber */ internal class AuthenticatedRequestInterceptor(private val user: User) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { - val requestWithToken = chain.request().newBuilder() - .withBearerToken(user.tokens?.accessToken) - .build() + val requestWithToken = + chain.request().newBuilder() + .withBearerToken(user.tokens?.accessToken) + .build() return chain.proceed(requestWithToken) } } @@ -26,7 +27,10 @@ internal class AuthenticatedRequestInterceptor(private val user: User) : Interce * token is still not accepted by the server. */ internal class AccessTokenAuthenticator(private val user: User) : Authenticator { - override fun authenticate(route: Route?, response: Response): Request? { + override fun authenticate( + route: Route?, + response: Response, + ): Request? { if (response.code != 401 || response.retryCount >= 1) { return null } @@ -34,9 +38,10 @@ internal class AccessTokenAuthenticator(private val user: User) : Authenticator return when (val tokenRefreshResult = user.refreshTokens()) { is Right -> { // retry request with fresh access token - val request = response.request.newBuilder() - .withBearerToken(user.tokens?.accessToken) - .build() + val request = + response.request.newBuilder() + .withBearerToken(user.tokens?.accessToken) + .build() request } is Left -> { @@ -47,13 +52,14 @@ internal class AccessTokenAuthenticator(private val user: User) : Authenticator } } -private fun Request.Builder.withBearerToken(token: String?): Request.Builder = apply { - if (token != null) { - header("Authorization", "Bearer $token") - } else { - Timber.e("No access token to include in request") +private fun Request.Builder.withBearerToken(token: String?): Request.Builder = + apply { + if (token != null) { + header("Authorization", "Bearer $token") + } else { + Timber.e("No access token to include in request") + } } -} private val Response.retryCount: Int get() { diff --git a/webflows/src/main/java/com/schibsted/account/webflows/user/StoredUserSession.kt b/webflows/src/main/java/com/schibsted/account/webflows/user/StoredUserSession.kt index 87e9bbba..6c24cd11 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/user/StoredUserSession.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/user/StoredUserSession.kt @@ -12,5 +12,5 @@ internal data class StoredUserSession( @SerializedName("userTokens") val userTokens: UserTokens, @SerializedName("updatedAt") - val updatedAt: Date -) \ No newline at end of file + val updatedAt: Date, +) diff --git a/webflows/src/main/java/com/schibsted/account/webflows/user/User.kt b/webflows/src/main/java/com/schibsted/account/webflows/user/User.kt index 624becdb..17da66d3 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/user/User.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/user/User.kt @@ -31,42 +31,48 @@ class User { private val tokenRefreshTask: BestEffortRunOnceTask val session: UserSession - get() = onlyIfLoggedIn { tokens -> - UserSession(tokens) - } + get() = + onlyIfLoggedIn { tokens -> + UserSession(tokens) + } /** User integer id (as string) */ val userId: String - get() = onlyIfLoggedIn { tokens -> - tokens.idTokenClaims.userId - } + get() = + onlyIfLoggedIn { tokens -> + tokens.idTokenClaims.userId + } /** User UUID */ val uuid: String - get() = onlyIfLoggedIn { tokens -> - tokens.idTokenClaims.sub - } + get() = + onlyIfLoggedIn { tokens -> + tokens.idTokenClaims.sub + } /** * ID Token */ val idToken: String - get() = onlyIfLoggedIn { tokens -> - tokens.idToken - } + get() = + onlyIfLoggedIn { tokens -> + tokens.idToken + } constructor(client: Client, session: UserSession) : this(client, session.tokens) internal constructor(client: Client, tokens: UserTokens) { this.client = client this.tokens = tokens - this.httpClient = client.httpClient.newBuilder() - .addInterceptor(AuthenticatedRequestInterceptor(this)) - .authenticator(AccessTokenAuthenticator(this)) - .build() - this.tokenRefreshTask = BestEffortRunOnceTask(5000) { - client.refreshTokensForUser(this) - } + this.httpClient = + client.httpClient.newBuilder() + .addInterceptor(AuthenticatedRequestInterceptor(this)) + .authenticator(AccessTokenAuthenticator(this)) + .build() + this.tokenRefreshTask = + BestEffortRunOnceTask(5000) { + client.refreshTokensForUser(this) + } } /** @@ -90,9 +96,10 @@ class User { fun isLoggedIn(): Boolean = tokens != null /** Fetch user profile data. */ - fun fetchProfileData(callback: (ApiResult) -> Unit) = onlyIfLoggedIn { - client.schibstedAccountApi.userProfile(this, callback) - } + fun fetchProfileData(callback: (ApiResult) -> Unit) = + onlyIfLoggedIn { + client.schibstedAccountApi.userProfile(this, callback) + } /** * Generate URL with embedded one-time code for creating a web session for the current user. @@ -106,19 +113,18 @@ class User { clientId: String, redirectUri: String, state: String? = null, - callback: (ApiResult) -> Unit - ) = - onlyIfLoggedIn { - client.schibstedAccountApi.sessionExchange( - user = this, - clientId = clientId, - redirectUri = redirectUri, - state = state - ) { - val result = it.map { schibstedAccountUrl("/session/${it.code}") } - callback(result) - } + callback: (ApiResult) -> Unit, + ) = onlyIfLoggedIn { + client.schibstedAccountApi.sessionExchange( + user = this, + clientId = clientId, + redirectUri = redirectUri, + state = state, + ) { + val result = it.map { schibstedAccountUrl("/session/${it.code}") } + callback(result) } + } /** * Requests a OAuth authorization code for the current user. @@ -127,19 +133,22 @@ class User { * @param clientId which client to get the code on behalf of, e.g. client id for associated web application * @param callback callback that receives the one time code */ - fun oneTimeCode(clientId: String, callback: (ApiResult) -> Unit) = - onlyIfLoggedIn { - client.schibstedAccountApi.codeExchange(this, clientId) { - callback(it.map { it.code }) - } + fun oneTimeCode( + clientId: String, + callback: (ApiResult) -> Unit, + ) = onlyIfLoggedIn { + client.schibstedAccountApi.codeExchange(this, clientId) { + callback(it.map { it.code }) } + } /** * Generate URL for Schibsted account pages. */ - fun accountPagesUrl(): URL = onlyIfLoggedIn { - schibstedAccountUrl("/profile-pages") - } + fun accountPagesUrl(): URL = + onlyIfLoggedIn { + schibstedAccountUrl("/profile-pages") + } /** * Perform the given [request] with user access token as Bearer token in Authorization header. @@ -156,17 +165,25 @@ class User { */ fun makeAuthenticatedRequest( request: Request, - callback: (Either) -> Unit + callback: (Either) -> Unit, ) = onlyIfLoggedIn { - httpClient.newCall(request).enqueue(object : Callback { - override fun onFailure(call: Call, e: IOException) { - callback(Left(e)) - } + httpClient.newCall(request).enqueue( + object : Callback { + override fun onFailure( + call: Call, + e: IOException, + ) { + callback(Left(e)) + } - override fun onResponse(call: Call, response: Response) { - callback(Right(response)) - } - }) + override fun onResponse( + call: Call, + response: Response, + ) { + callback(Right(response)) + } + }, + ) } /** @@ -192,12 +209,13 @@ class User { internal fun refreshTokens(): TokenRefreshResult { val result = tokenRefreshTask.run() + fun shouldLogout(result: TokenRefreshResult?): Boolean { return result is Left && - result.value is RefreshTokenError.RefreshRequestFailed && - result.value.error is HttpError.ErrorResponse && - result.value.error.body != null && - OAuthError.fromJson(result.value.error.body)?.error == "invalid_grant" + result.value is RefreshTokenError.RefreshRequestFailed && + result.value.error is HttpError.ErrorResponse && + result.value.error.body != null && + OAuthError.fromJson(result.value.error.body)?.error == "invalid_grant" } if (shouldLogout(result)) { @@ -222,8 +240,9 @@ class User { } private fun onlyIfLoggedIn(block: (UserTokens) -> T): T { - val currentTokens = tokens - ?: throw IllegalStateException("Can not use tokens of logged-out user!") + val currentTokens = + tokens + ?: throw IllegalStateException("Can not use tokens of logged-out user!") return block(currentTokens) } @@ -238,7 +257,7 @@ class User { @Parcelize data class UserSession internal constructor( - internal val tokens: UserTokens + internal val tokens: UserTokens, ) : Parcelable private typealias TokenRefreshResult = Either diff --git a/webflows/src/main/java/com/schibsted/account/webflows/util/BestEffortRunOnceTask.kt b/webflows/src/main/java/com/schibsted/account/webflows/util/BestEffortRunOnceTask.kt index 1f168bce..269c44c1 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/util/BestEffortRunOnceTask.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/util/BestEffortRunOnceTask.kt @@ -16,7 +16,7 @@ import java.util.concurrent.atomic.AtomicBoolean */ internal class BestEffortRunOnceTask( private val timeoutMilliSeconds: Long = 1000, - private val block: () -> T + private val block: () -> T, ) { private val lock = ConditionVariable() private val inProgress = AtomicBoolean(false) diff --git a/webflows/src/main/java/com/schibsted/account/webflows/util/Either.kt b/webflows/src/main/java/com/schibsted/account/webflows/util/Either.kt index 1e3f65cb..b732b92c 100644 --- a/webflows/src/main/java/com/schibsted/account/webflows/util/Either.kt +++ b/webflows/src/main/java/com/schibsted/account/webflows/util/Either.kt @@ -9,6 +9,7 @@ package com.schibsted.account.webflows.util sealed class Either { data class Right(val value: R) : Either() + data class Left(val value: L) : Either() fun onSuccess(fn: (success: R) -> Unit): Either { diff --git a/webflows/src/test/java/com/schibsted/account/testutil/Fixtures.kt b/webflows/src/test/java/com/schibsted/account/testutil/Fixtures.kt index 1437802d..f244a9d8 100644 --- a/webflows/src/test/java/com/schibsted/account/testutil/Fixtures.kt +++ b/webflows/src/test/java/com/schibsted/account/testutil/Fixtures.kt @@ -8,28 +8,27 @@ import com.schibsted.account.webflows.persistence.StateStorage import com.schibsted.account.webflows.token.IdTokenClaims import com.schibsted.account.webflows.token.TokenHandler import com.schibsted.account.webflows.token.UserTokens -import com.schibsted.account.webflows.util.TestRetrofitApi import io.mockk.mockk import okhttp3.OkHttpClient -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory import java.net.URL internal object Fixtures { - val clientConfig = ClientConfiguration( - URL("https://issuer.example.com"), - "client1", - "com.example.client://login" - ) - val idTokenClaims = IdTokenClaims( - clientConfig.issuer, - "userUuid", - "12345", - listOf(clientConfig.clientId), - 10, - "testNonce", - null - ) + val clientConfig = + ClientConfiguration( + URL("https://issuer.example.com"), + "client1", + "com.example.client://login", + ) + val idTokenClaims = + IdTokenClaims( + clientConfig.issuer, + "userUuid", + "12345", + listOf(clientConfig.clientId), + 10, + "testNonce", + null, + ) val userTokens = UserTokens("accessToken", "refreshToken", "idToken", idTokenClaims) fun getClient( @@ -38,7 +37,7 @@ internal object Fixtures { sessionStorage: SessionStorage = mockk(relaxed = true), httpClient: OkHttpClient = Fixtures.httpClient, tokenHandler: TokenHandler = mockk(relaxed = true), - schibstedAccountApi: SchibstedAccountApi = mockk(relaxed = true) + schibstedAccountApi: SchibstedAccountApi = mockk(relaxed = true), ): Client { return Client( clientConfiguration, @@ -46,7 +45,7 @@ internal object Fixtures { sessionStorage, httpClient, tokenHandler, - schibstedAccountApi + schibstedAccountApi, ) } diff --git a/webflows/src/test/java/com/schibsted/account/testutil/TestUtil.kt b/webflows/src/test/java/com/schibsted/account/testutil/TestUtil.kt index 6301ee03..44dd4b49 100644 --- a/webflows/src/test/java/com/schibsted/account/testutil/TestUtil.kt +++ b/webflows/src/test/java/com/schibsted/account/testutil/TestUtil.kt @@ -5,9 +5,7 @@ import com.nimbusds.jose.JWSHeader import com.nimbusds.jose.JWSObject import com.nimbusds.jose.Payload import com.nimbusds.jose.crypto.RSASSASigner -import com.nimbusds.jose.jwk.JWK import com.nimbusds.jose.jwk.RSAKey -import com.schibsted.account.webflows.token.IdTokenValidatorTest import com.schibsted.account.webflows.util.Either import com.schibsted.account.webflows.util.Either.Left import com.schibsted.account.webflows.util.Either.Right @@ -17,7 +15,10 @@ import org.junit.Assert import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit -fun await(timeoutSeconds: Long = 1, func: (() -> Unit) -> Unit) { +fun await( + timeoutSeconds: Long = 1, + func: (() -> Unit) -> Unit, +) { val latch = CountDownLatch(1) try { @@ -40,7 +41,10 @@ fun Either.assertLeft(func: (L) -> Unit) { func((this as Left).value) } -fun withServer(vararg responses: MockResponse, func: (MockWebServer) -> Unit) { +fun withServer( + vararg responses: MockResponse, + func: (MockWebServer) -> Unit, +) { val server = MockWebServer() for (r in responses) { @@ -55,10 +59,15 @@ fun withServer(vararg responses: MockResponse, func: (MockWebServer) -> Unit) { } } -fun createJws(key: RSAKey, keyId: String, payload: Payload): String { - val header = JWSHeader.Builder(JWSAlgorithm.RS256) - .keyID(keyId) - .build() +fun createJws( + key: RSAKey, + keyId: String, + payload: Payload, +): String { + val header = + JWSHeader.Builder(JWSAlgorithm.RS256) + .keyID(keyId) + .build() val jwsObject = JWSObject(header, payload) jwsObject.sign(RSASSASigner(key)) diff --git a/webflows/src/test/java/com/schibsted/account/webflows/activities/AuthResultLiveDataTest.kt b/webflows/src/test/java/com/schibsted/account/webflows/activities/AuthResultLiveDataTest.kt index d8f3443d..267bad1f 100644 --- a/webflows/src/test/java/com/schibsted/account/webflows/activities/AuthResultLiveDataTest.kt +++ b/webflows/src/test/java/com/schibsted/account/webflows/activities/AuthResultLiveDataTest.kt @@ -18,7 +18,10 @@ import com.schibsted.account.webflows.util.Either.Right import io.mockk.every import io.mockk.mockk import org.junit.After -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.fail import org.junit.Test import org.junit.runner.RunWith import org.robolectric.Shadows.shadowOf @@ -107,26 +110,31 @@ class AuthResultLiveDataTest { } AuthResultLiveData.create(client) - AuthResultLiveData.get(client).update(Intent().apply { - data = Uri.parse("https://client.example.com/redirect?code=12345&state=test") - }) + AuthResultLiveData.get(client).update( + Intent().apply { + data = Uri.parse("https://client.example.com/redirect?code=12345&state=test") + }, + ) AuthResultLiveData.get(client).value!!.assertRight { assertEquals(user, it) } } @Test fun updateHandlesFailureResult() { val errorResult = LoginError.UnexpectedError("Test error") - val client = mockk(relaxed = true) { - every { handleAuthenticationResponse(any(), any()) } answers { - val callback = secondArg() - callback(Left(errorResult)) + val client = + mockk(relaxed = true) { + every { handleAuthenticationResponse(any(), any()) } answers { + val callback = secondArg() + callback(Left(errorResult)) + } } - } AuthResultLiveData.create(client) - AuthResultLiveData.get(client).update(Intent().apply { - data = Uri.parse("https://client.example.com/redirect?code=12345&state=test") - }) + AuthResultLiveData.get(client).update( + Intent().apply { + data = Uri.parse("https://client.example.com/redirect?code=12345&state=test") + }, + ) AuthResultLiveData.get(client).value!!.assertLeft { when (it) { is NotAuthed.LoginFailed -> assertEquals(errorResult, it.error) @@ -153,7 +161,7 @@ class AuthResultLiveDataTest { AuthResultLiveData.get(client).value!!.assertLeft { assertEquals( NotAuthed.NoLoggedInUser, - it + it, ) } } diff --git a/webflows/src/test/java/com/schibsted/account/webflows/activities/AuthorizationManagementActivityTest.kt b/webflows/src/test/java/com/schibsted/account/webflows/activities/AuthorizationManagementActivityTest.kt index ebb81618..503877a1 100644 --- a/webflows/src/test/java/com/schibsted/account/webflows/activities/AuthorizationManagementActivityTest.kt +++ b/webflows/src/test/java/com/schibsted/account/webflows/activities/AuthorizationManagementActivityTest.kt @@ -35,10 +35,11 @@ import org.robolectric.Robolectric @RunWith(AndroidJUnit4::class) class AuthorizationManagementActivityTest { - private val testActivityIntent = Intent( - getApplicationContext(), - TestActivity::class.java - ) + private val testActivityIntent = + Intent( + getApplicationContext(), + TestActivity::class.java, + ) private val client: Client = mockk(relaxed = true) @@ -65,9 +66,10 @@ class AuthorizationManagementActivityTest { @Test fun testShouldStartAuthIfNotStarted() { - val authIntent = Intent().apply { - putExtra("AUTH", true) - } + val authIntent = + Intent().apply { + putExtra("AUTH", true) + } val intent = AuthorizationManagementActivity.createStartIntent(getApplicationContext(), authIntent) @@ -77,7 +79,7 @@ class AuthorizationManagementActivityTest { AuthResultLiveData.get(client).value!!.assertLeft { assertEquals( NotAuthed.AuthInProgress, - it + it, ) } } @@ -88,9 +90,10 @@ class AuthorizationManagementActivityTest { Uri.parse("https://client.example.com/redirect?code=12345&state=test") val ctx = getApplicationContext() - val intent = Intent(ctx, AuthorizationManagementActivity::class.java).apply { - data = authResponse - } + val intent = + Intent(ctx, AuthorizationManagementActivity::class.java).apply { + data = authResponse + } AuthResultLiveDataTest.resetInstance() val client = mockk(relaxed = true) @@ -105,9 +108,12 @@ class AuthorizationManagementActivityTest { AuthResultLiveData.get(client).value!!.assertRight { assertEquals(user, it) } verify(exactly = 1) { - client.handleAuthenticationResponse(withArg { intent -> - assertEquals(authResponse, intent.data) - }, any()) + client.handleAuthenticationResponse( + withArg { intent -> + assertEquals(authResponse, intent.data) + }, + any(), + ) } verify(exactly = 1) { AuthorizationManagementActivity.completionIntent?.send() } verify(exactly = 0) { AuthorizationManagementActivity.cancelIntent?.send() } @@ -122,7 +128,7 @@ class AuthorizationManagementActivityTest { AuthResultLiveData.get(client).value!!.assertLeft { assertEquals( NotAuthed.CancelledByUser, - it + it, ) } verify(exactly = 1) { AuthorizationManagementActivity.cancelIntent?.send() } @@ -131,9 +137,10 @@ class AuthorizationManagementActivityTest { @Test fun testActivityWithStressOnResumeTest() { - val authIntent = Intent().apply { - putExtra("AUTH", true) - } + val authIntent = + Intent().apply { + putExtra("AUTH", true) + } val startIntent = AuthorizationManagementActivity.createStartIntent(getApplicationContext(), authIntent) @@ -158,7 +165,7 @@ class AuthorizationManagementActivityTest { AuthResultLiveData.get(client).value!!.assertLeft { assertEquals( NotAuthed.CancelledByUser, - it + it, ) } @@ -170,7 +177,7 @@ class AuthorizationManagementActivityTest { AuthResultLiveData.get(client).value!!.assertLeft { assertEquals( NotAuthed.AuthInProgress, - it + it, ) } } diff --git a/webflows/src/test/java/com/schibsted/account/webflows/activities/RedirectUriReceiverActivityTest.kt b/webflows/src/test/java/com/schibsted/account/webflows/activities/RedirectUriReceiverActivityTest.kt index 645a3261..00c6a544 100644 --- a/webflows/src/test/java/com/schibsted/account/webflows/activities/RedirectUriReceiverActivityTest.kt +++ b/webflows/src/test/java/com/schibsted/account/webflows/activities/RedirectUriReceiverActivityTest.kt @@ -18,7 +18,6 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith - @RunWith(AndroidJUnit4::class) class RedirectUriReceiverActivityTest { @Test @@ -32,8 +31,8 @@ class RedirectUriReceiverActivityTest { intending( Matchers.allOf( IntentMatchers.hasComponent(AuthorizationManagementActivity::class.java.name), - IntentMatchers.hasData(redirectUri) - ) + IntentMatchers.hasData(redirectUri), + ), ).respondWith(ActivityResult(Activity.RESULT_OK, Intent())) val scenario = launch(redirectIntent) diff --git a/webflows/src/test/java/com/schibsted/account/webflows/api/SchibstedAccountApiTest.kt b/webflows/src/test/java/com/schibsted/account/webflows/api/SchibstedAccountApiTest.kt index c180496f..2796ea1e 100644 --- a/webflows/src/test/java/com/schibsted/account/webflows/api/SchibstedAccountApiTest.kt +++ b/webflows/src/test/java/com/schibsted/account/webflows/api/SchibstedAccountApiTest.kt @@ -1,7 +1,11 @@ package com.schibsted.account.webflows.api import com.nimbusds.jose.jwk.gen.RSAKeyGenerator -import com.schibsted.account.testutil.* +import com.schibsted.account.testutil.Fixtures +import com.schibsted.account.testutil.assertLeft +import com.schibsted.account.testutil.assertRight +import com.schibsted.account.testutil.await +import com.schibsted.account.testutil.withServer import com.schibsted.account.webflows.user.User import com.schibsted.account.webflows.util.Util import okhttp3.HttpUrl.Companion.toHttpUrl @@ -11,64 +15,67 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test - class SchibstedAccountApiTest { - private val tokenRequest = UserTokenRequest( - "authCode", - "12345", - "client1", - "redirectUri" - ) + private val tokenRequest = + UserTokenRequest( + "authCode", + "12345", + "client1", + "redirectUri", + ) private fun assertAuthCodeTokenRequest( expectedTokenRequest: UserTokenRequest, - actualRequest: RecordedRequest + actualRequest: RecordedRequest, ) { assertEquals("/oauth/token", actualRequest.path) val tokenRequestParams = Util.parseQueryParameters(actualRequest.body.readUtf8()) - val expectTokenRequest = mapOf( - "grant_type" to "authorization_code", - "code" to expectedTokenRequest.authCode, - "code_verifier" to expectedTokenRequest.codeVerifier, - "client_id" to expectedTokenRequest.clientId, - "redirect_uri" to expectedTokenRequest.redirectUri - ) + val expectTokenRequest = + mapOf( + "grant_type" to "authorization_code", + "code" to expectedTokenRequest.authCode, + "code_verifier" to expectedTokenRequest.codeVerifier, + "client_id" to expectedTokenRequest.clientId, + "redirect_uri" to expectedTokenRequest.redirectUri, + ) assertEquals(expectTokenRequest, tokenRequestParams) } private fun withApiResponseWrapper(responseData: String): String { return """ - { - "name": "SPP Container", - "version": "0.2", - "api": 2, - "code": 200, - "data": $responseData - } - """.trimIndent() + { + "name": "SPP Container", + "version": "0.2", + "api": 2, + "code": 200, + "data": $responseData + } + """.trimIndent() } @Test fun makeAuthCodeTokenRequestSuccess() { - val tokenResponse = UserTokenResponse( - "accessToken1", - "refreshToken1", - "idToken1", - "openid offline_access", - 3600 - ) - val httpResponse = MockResponse().setBody( - """ - { - "access_token": "${tokenResponse.access_token}", - "refresh_token": "${tokenResponse.refresh_token}", - "id_token": "${tokenResponse.id_token}", - "scope": "${tokenResponse.scope}", - "expires_in": "${tokenResponse.expires_in}" - } - """.trimIndent() - ) + val tokenResponse = + UserTokenResponse( + "accessToken1", + "refreshToken1", + "idToken1", + "openid offline_access", + 3600, + ) + val httpResponse = + MockResponse().setBody( + """ + { + "access_token": "${tokenResponse.access_token}", + "refresh_token": "${tokenResponse.refresh_token}", + "id_token": "${tokenResponse.id_token}", + "scope": "${tokenResponse.scope}", + "expires_in": "${tokenResponse.expires_in}" + } + """.trimIndent(), + ) withServer(httpResponse) { server -> val schaccApi = SchibstedAccountApi(server.url("/"), Fixtures.httpClient) @@ -84,28 +91,31 @@ class SchibstedAccountApiTest { @Test fun makeRefreshTokenRequestSuccess() { - val tokenResponse = UserTokenResponse( - "accessToken1", - "refreshToken1", - null, - null, - 3600 - ) - val httpResponse = MockResponse().setBody( - """ - { - "access_token": "${tokenResponse.access_token}", - "refresh_token": "${tokenResponse.refresh_token}", - "expires_in": "${tokenResponse.expires_in}" - } - """.trimIndent() - ) + val tokenResponse = + UserTokenResponse( + "accessToken1", + "refreshToken1", + null, + null, + 3600, + ) + val httpResponse = + MockResponse().setBody( + """ + { + "access_token": "${tokenResponse.access_token}", + "refresh_token": "${tokenResponse.refresh_token}", + "expires_in": "${tokenResponse.expires_in}" + } + """.trimIndent(), + ) - val tokenRequest = RefreshTokenRequest( - "refreshToken", - null, - Fixtures.clientConfig.clientId - ) + val tokenRequest = + RefreshTokenRequest( + "refreshToken", + null, + Fixtures.clientConfig.clientId, + ) withServer(httpResponse) { server -> val schaccApi = SchibstedAccountApi(server.url("/"), Fixtures.httpClient) await { done -> @@ -114,11 +124,12 @@ class SchibstedAccountApiTest { val tokenRequestParams = Util.parseQueryParameters(server.takeRequest().body.readUtf8()) - val expectedParams = mapOf( - "client_id" to Fixtures.clientConfig.clientId, - "grant_type" to "refresh_token", - "refresh_token" to tokenRequest.refreshToken - ) + val expectedParams = + mapOf( + "client_id" to Fixtures.clientConfig.clientId, + "grant_type" to "refresh_token", + "refresh_token" to tokenRequest.refreshToken, + ) assertEquals(expectedParams, tokenRequestParams) done() } @@ -159,13 +170,14 @@ class SchibstedAccountApiTest { @Test fun jwksSuccess() { val jwk = RSAKeyGenerator(2048).generate().toPublicJWK() - val jwksResponse = MockResponse().setBody( - """ - { - "keys": [$jwk] - } - """.trimIndent() - ) + val jwksResponse = + MockResponse().setBody( + """ + { + "keys": [$jwk] + } + """.trimIndent(), + ) withServer(jwksResponse) { server -> val schaccApi = SchibstedAccountApi(server.url("/"), Fixtures.httpClient) await { done -> @@ -225,24 +237,26 @@ class SchibstedAccountApiTest { @Test fun userProfileSuccess() { - val userProfileResponse = MockResponse().setBody( - withApiResponseWrapper( - """ - { - "uuid": "96085e85-349b-4dbf-9809-fa721e7bae46" - } - """.trimIndent() + val userProfileResponse = + MockResponse().setBody( + withApiResponseWrapper( + """ + { + "uuid": "96085e85-349b-4dbf-9809-fa721e7bae46" + } + """.trimIndent(), + ), ) - ) withServer(userProfileResponse) { server -> val schaccApi = SchibstedAccountApi(server.url("/"), Fixtures.httpClient) await { done -> val user = User(Fixtures.getClient(), Fixtures.userTokens) schaccApi.userProfile(user) { result -> result.assertRight { - val expectedProfileResponse = UserProfileResponse( - uuid = "96085e85-349b-4dbf-9809-fa721e7bae46", - ) + val expectedProfileResponse = + UserProfileResponse( + uuid = "96085e85-349b-4dbf-9809-fa721e7bae46", + ) assertEquals(expectedProfileResponse, it) } @@ -260,11 +274,12 @@ class SchibstedAccountApiTest { @Test fun sessionExchangeSuccess() { - val sessionExchangeResponse = MockResponse().setBody( - withApiResponseWrapper( - """{"code": "12345"}""" + val sessionExchangeResponse = + MockResponse().setBody( + withApiResponseWrapper( + """{"code": "12345"}""", + ), ) - ) withServer(sessionExchangeResponse) { server -> val schaccApi = SchibstedAccountApi(server.url("/"), Fixtures.httpClient) await { done -> @@ -276,14 +291,13 @@ class SchibstedAccountApiTest { user = user, state = state, clientId = clientId, - redirectUri = redirectUri + redirectUri = redirectUri, ) { result -> result.assertRight { val expectedSessionExchangeResponse = SessionExchangeResponse("12345") assertEquals(expectedSessionExchangeResponse, it) } - val request = server.takeRequest() // request should contain user access token val authHeader = request.getHeader("Authorization") @@ -295,8 +309,9 @@ class SchibstedAccountApiTest { "clientId" to clientId, "redirectUri" to redirectUri, "type" to "session", - "state" to state - ), requestParams + "state" to state, + ), + requestParams, ) done() } diff --git a/webflows/src/test/java/com/schibsted/account/webflows/api/UserProfileResponseTest.kt b/webflows/src/test/java/com/schibsted/account/webflows/api/UserProfileResponseTest.kt index 0b1607be..28e78cb4 100644 --- a/webflows/src/test/java/com/schibsted/account/webflows/api/UserProfileResponseTest.kt +++ b/webflows/src/test/java/com/schibsted/account/webflows/api/UserProfileResponseTest.kt @@ -1,7 +1,8 @@ package com.schibsted.account.webflows.api import com.google.gson.Gson -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Test class UserProfileResponseTest { @@ -37,59 +38,65 @@ class UserProfileResponseTest { val jsonString = javaClass.getResource("/user-profile-response.json")!!.readText() val parsed = Gson().fromJson(jsonString, UserProfileResponse::class.java) - val expectedProfileResponse = UserProfileResponse( - uuid = "96085e85-349b-4dbf-9809-fa721e7bae46", - userId = "12345", - status = 1, - email = "test@example.com", - emailVerified = "1970-01-01 00:00:00", - emails = listOf( - Email( - "test@example.com", - "other", - true, - true, - "1970-01-01 00:00:00" - ) - ), - phoneNumber = "+46123456", - phoneNumberVerified = null, - phoneNumbers = listOf(PhoneNumber("+46123456", "other", false, false)), - displayName = "Unit test", - name = Name("Unit", "Test", "Unit Test"), - addresses = mapOf( - Address.AddressType.HOME to Address( - "12345 Test, Sverige", - "Test", - "12345", - "Test locality", - "Test region", - "Sverige", - Address.AddressType.HOME - ) - ), - gender = "withheld", - birthday = "1970-01-01 00:00:00", - accounts = mapOf( - "client1" to Account( - "client1", - "Example", - "example.com", - "1970-01-01 00:00:00" - ) - ), - merchants = listOf(12345, 54321), - published = "1970-01-01 00:00:00", - verified = "1970-01-01 00:00:00", - updated = "1971-01-01 00:00:00", - passwordChanged = "1970-01-01 00:00:00", - lastAuthenticated = "1970-01-01 00:00:00", - lastLoggedIn = "1970-01-01 00:00:00", - locale = "sv_SE", - utcOffset = "+02:00", - pairId = "12345", - sdrn = "sdrn:schibsted:user:12345" - ) + val expectedProfileResponse = + UserProfileResponse( + uuid = "96085e85-349b-4dbf-9809-fa721e7bae46", + userId = "12345", + status = 1, + email = "test@example.com", + emailVerified = "1970-01-01 00:00:00", + emails = + listOf( + Email( + "test@example.com", + "other", + true, + true, + "1970-01-01 00:00:00", + ), + ), + phoneNumber = "+46123456", + phoneNumberVerified = null, + phoneNumbers = listOf(PhoneNumber("+46123456", "other", false, false)), + displayName = "Unit test", + name = Name("Unit", "Test", "Unit Test"), + addresses = + mapOf( + Address.AddressType.HOME to + Address( + "12345 Test, Sverige", + "Test", + "12345", + "Test locality", + "Test region", + "Sverige", + Address.AddressType.HOME, + ), + ), + gender = "withheld", + birthday = "1970-01-01 00:00:00", + accounts = + mapOf( + "client1" to + Account( + "client1", + "Example", + "example.com", + "1970-01-01 00:00:00", + ), + ), + merchants = listOf(12345, 54321), + published = "1970-01-01 00:00:00", + verified = "1970-01-01 00:00:00", + updated = "1971-01-01 00:00:00", + passwordChanged = "1970-01-01 00:00:00", + lastAuthenticated = "1970-01-01 00:00:00", + lastLoggedIn = "1970-01-01 00:00:00", + locale = "sv_SE", + utcOffset = "+02:00", + pairId = "12345", + sdrn = "sdrn:schibsted:user:12345", + ) assertEquals(expectedProfileResponse, parsed) } } diff --git a/webflows/src/test/java/com/schibsted/account/webflows/client/ClientTest.kt b/webflows/src/test/java/com/schibsted/account/webflows/client/ClientTest.kt index 6fe7e013..f19848a1 100644 --- a/webflows/src/test/java/com/schibsted/account/webflows/client/ClientTest.kt +++ b/webflows/src/test/java/com/schibsted/account/webflows/client/ClientTest.kt @@ -3,7 +3,6 @@ package com.schibsted.account.webflows.client import android.content.Intent import android.os.Build import android.os.ConditionVariable -import androidx.annotation.RequiresApi import androidx.test.filters.SdkSuppress import com.schibsted.account.testutil.Fixtures import com.schibsted.account.testutil.Fixtures.clientConfig @@ -26,20 +25,24 @@ import com.schibsted.account.webflows.user.UserSession import com.schibsted.account.webflows.util.Either import com.schibsted.account.webflows.util.Either.Left import com.schibsted.account.webflows.util.Either.Right -import io.mockk.* +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.verify import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test -import java.util.* +import java.util.Date import java.util.concurrent.CompletableFuture class ClientTest { - private fun authResultIntent(authResponseParameters: String?): Intent { return mockk { - every { data } returns mockk { - every { query } returns authResponseParameters - } + every { data } returns + mockk { + every { query } returns authResponseParameters + } } } @@ -49,25 +52,28 @@ class ClientTest { val nonce = "testNonce" val sessionStorageMock: SessionStorage = mockk(relaxUnitFun = true) val authState = AuthState(state, nonce, "codeVerifier", null) - val stateStorageMock: StateStorage = mockk(relaxUnitFun = true) { - every { getValue(Client.AUTH_STATE_KEY, AuthState::class) } returns authState - } + val stateStorageMock: StateStorage = + mockk(relaxUnitFun = true) { + every { getValue(Client.AUTH_STATE_KEY, AuthState::class) } returns authState + } val authCode = "testAuthCode" - val tokenHandler: TokenHandler = mockk(relaxed = true) { - every { makeTokenRequest(authCode, authState, any()) } answers { - val callback = thirdArg<(TokenRequestResult) -> Unit>() - val tokensResult = - UserTokensResult(Fixtures.userTokens, "openid offline_access", 10) - callback(Right(tokensResult)) + val tokenHandler: TokenHandler = + mockk(relaxed = true) { + every { makeTokenRequest(authCode, authState, any()) } answers { + val callback = thirdArg<(TokenRequestResult) -> Unit>() + val tokensResult = + UserTokensResult(Fixtures.userTokens, "openid offline_access", 10) + callback(Right(tokensResult)) + } } - } - val client = getClient( - sessionStorage = sessionStorageMock, - stateStorage = stateStorageMock, - tokenHandler = tokenHandler - ) + val client = + getClient( + sessionStorage = sessionStorageMock, + stateStorage = stateStorageMock, + tokenHandler = tokenHandler, + ) client.handleAuthenticationResponse(authResultIntent("code=$authCode&state=$state")) { it.assertRight { user -> @@ -76,12 +82,14 @@ class ClientTest { } verify { - sessionStorageMock.save(withArg { storedSession -> - assertEquals(clientConfig.clientId, storedSession.clientId) - assertEquals(Fixtures.userTokens, storedSession.userTokens) - val secondsSinceSessionCreated = (Date().time - storedSession.updatedAt.time) / 1000 - assertTrue(secondsSinceSessionCreated < 1) // created within last second - }) + sessionStorageMock.save( + withArg { storedSession -> + assertEquals(clientConfig.clientId, storedSession.clientId) + assertEquals(Fixtures.userTokens, storedSession.userTokens) + val secondsSinceSessionCreated = (Date().time - storedSession.updatedAt.time) / 1000 + assertTrue(secondsSinceSessionCreated < 1) // created within last second + }, + ) } } @@ -96,24 +104,28 @@ class ClientTest { @Test fun handleAuthenticationResponseShouldParseTokenErrorResponse() { - val tokenHandler: TokenHandler = mockk(relaxed = true) { - every { makeTokenRequest(any(), any(), any()) } answers { - val callback = thirdArg<(TokenRequestResult) -> Unit>() - val errorResponse = HttpError.ErrorResponse( - 400, - """{"error": "test", "error_description": "Something went wrong"}""" - ) - callback(Left(TokenError.TokenRequestError(errorResponse))) + val tokenHandler: TokenHandler = + mockk(relaxed = true) { + every { makeTokenRequest(any(), any(), any()) } answers { + val callback = thirdArg<(TokenRequestResult) -> Unit>() + val errorResponse = + HttpError.ErrorResponse( + 400, + """{"error": "test", "error_description": "Something went wrong"}""", + ) + callback(Left(TokenError.TokenRequestError(errorResponse))) + } } - } val authState = AuthState("testState", "testNonce", "codeVerifier", null) - val stateStorageMock: StateStorage = mockk(relaxUnitFun = true) { - every { getValue(Client.AUTH_STATE_KEY, AuthState::class) } returns authState - } - val client = getClient( - tokenHandler = tokenHandler, - stateStorage = stateStorageMock - ) + val stateStorageMock: StateStorage = + mockk(relaxUnitFun = true) { + every { getValue(Client.AUTH_STATE_KEY, AuthState::class) } returns authState + } + val client = + getClient( + tokenHandler = tokenHandler, + stateStorage = stateStorageMock, + ) client.handleAuthenticationResponse(authResultIntent("code=authCode&state=${authState.state}")) { it.assertLeft { error -> @@ -126,28 +138,35 @@ class ClientTest { @Test fun handleAuthenticationResponseCanParseHtmlErrorResponse() { - val tokenHandler: TokenHandler = mockk(relaxed = true) { - every { makeTokenRequest(any(), any(), any()) } answers { - val callback = thirdArg<(TokenRequestResult) -> Unit>() - val errorResponse = HttpError.ErrorResponse( - 503, - """503""" - ) - callback(Left(TokenError.TokenRequestError(errorResponse))) + val tokenHandler: TokenHandler = + mockk(relaxed = true) { + every { makeTokenRequest(any(), any(), any()) } answers { + val callback = thirdArg<(TokenRequestResult) -> Unit>() + val errorResponse = + HttpError.ErrorResponse( + 503, + """503""", + ) + callback(Left(TokenError.TokenRequestError(errorResponse))) + } } - } val authState = AuthState("testState", "testNonce", "codeVerifier", null) - val stateStorageMock: StateStorage = mockk(relaxUnitFun = true) { - every { getValue(Client.AUTH_STATE_KEY, AuthState::class) } returns authState - } - val client = getClient( - tokenHandler = tokenHandler, - stateStorage = stateStorageMock - ) + val stateStorageMock: StateStorage = + mockk(relaxUnitFun = true) { + every { getValue(Client.AUTH_STATE_KEY, AuthState::class) } returns authState + } + val client = + getClient( + tokenHandler = tokenHandler, + stateStorage = stateStorageMock, + ) client.handleAuthenticationResponse(authResultIntent("code=authCode&state=${authState.state}")) { it.assertLeft { error -> - val expected = LoginError.UnexpectedError("TokenRequestError(cause=ErrorResponse(code=503, body=503))") + val expected = + LoginError.UnexpectedError( + "TokenRequestError(cause=ErrorResponse(code=503, body=503))", + ) assertEquals(expected, error) } } @@ -167,7 +186,7 @@ class ClientTest { result.assertRight { assertEquals( User(client, UserSession(Fixtures.userTokens)), - it + it, ) } } @@ -194,12 +213,13 @@ class ClientTest { @Test fun refreshTokensHandlesConcurrentLogout() { val lock = ConditionVariable(false) - val tokenHandler = mockk(relaxed = true) { - every { makeTokenRequest(any(), any()) } answers { - lock.block(10) // wait until after logout is complete - Right(UserTokenResponse("", "", "", "", 0)) + val tokenHandler = + mockk(relaxed = true) { + every { makeTokenRequest(any(), any()) } answers { + lock.block(10) // wait until after logout is complete + Right(UserTokenResponse("", "", "", "", 0)) + } } - } val client = getClient(tokenHandler = tokenHandler) val user = User(client, Fixtures.userTokens) @@ -207,35 +227,37 @@ class ClientTest { * Run token refresh operation in separate thread, manually forcing the operation to block * until the user has been logged out */ - val refreshTask = CompletableFuture.supplyAsync { - val result = client.refreshTokensForUser(user) - result.assertLeft { - assertEquals( - RefreshTokenError.UnexpectedError("User has logged-out during token refresh"), - it - ) + val refreshTask = + CompletableFuture.supplyAsync { + val result = client.refreshTokensForUser(user) + result.assertLeft { + assertEquals( + RefreshTokenError.UnexpectedError("User has logged-out during token refresh"), + it, + ) + } + } + val logoutTask = + CompletableFuture.supplyAsync { + user.logout() + lock.open() // unblock refresh token response } - } - val logoutTask = CompletableFuture.supplyAsync { - user.logout() - lock.open() // unblock refresh token response - } CompletableFuture.allOf(refreshTask, logoutTask).join() } @Test fun testExternalId() { val client = getClient() - val externalIdAllParameters = client.getExternalId("pairId","externalParty","optionalSuffix") + val externalIdAllParameters = client.getExternalId("pairId", "externalParty", "optionalSuffix") // values generated via : https://emn178.github.io/online-tools/sha256.html // pairId:externalParty:optionalSuffix // e0b2b31df36848059b44ac0ee6784607b003a3688ac6bbdb196d8465bbc8b281 - assertEquals(externalIdAllParameters, "e0b2b31df36848059b44ac0ee6784607b003a3688ac6bbdb196d8465bbc8b281" ) + assertEquals(externalIdAllParameters, "e0b2b31df36848059b44ac0ee6784607b003a3688ac6bbdb196d8465bbc8b281") // values generated via : https://emn178.github.io/online-tools/sha256.html // pairId:externalParty // 386eb5f9c3e56843ff83e43fa3d69fc4c2b2072f8e8036332baefb04e96f28b9 - val externalIdWithoutOptionalParameters = client.getExternalId("pairId","externalParty") - assertEquals(externalIdWithoutOptionalParameters, "386eb5f9c3e56843ff83e43fa3d69fc4c2b2072f8e8036332baefb04e96f28b9" ) + val externalIdWithoutOptionalParameters = client.getExternalId("pairId", "externalParty") + assertEquals(externalIdWithoutOptionalParameters, "386eb5f9c3e56843ff83e43fa3d69fc4c2b2072f8e8036332baefb04e96f28b9") } } diff --git a/webflows/src/test/java/com/schibsted/account/webflows/client/UrlBuilderTest.kt b/webflows/src/test/java/com/schibsted/account/webflows/client/UrlBuilderTest.kt index 96f2d645..e9ec9a63 100644 --- a/webflows/src/test/java/com/schibsted/account/webflows/client/UrlBuilderTest.kt +++ b/webflows/src/test/java/com/schibsted/account/webflows/client/UrlBuilderTest.kt @@ -3,7 +3,9 @@ package com.schibsted.account.webflows.client import com.schibsted.account.testutil.Fixtures import com.schibsted.account.webflows.util.Util import io.mockk.mockk -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull import org.junit.Test import java.net.URL @@ -23,7 +25,7 @@ class UrlBuilderTest { assertEquals("select_account", queryParams["prompt"]) assertEquals( setOf("openid", "offline_access"), - queryParams.getValue("scope").split(" ").toSet() + queryParams.getValue("scope").split(" ").toSet(), ) assertNotNull(queryParams["state"]) assertNotNull(queryParams["nonce"]) @@ -46,7 +48,7 @@ class UrlBuilderTest { assertEquals( setOf("openid", "offline_access", "scope1", "scope2"), - queryParams.getValue("scope").split(" ").toSet() + queryParams.getValue("scope").split(" ").toSet(), ) } diff --git a/webflows/src/test/java/com/schibsted/account/webflows/storage/StorageTest.kt b/webflows/src/test/java/com/schibsted/account/webflows/storage/MigratingStorageTest.kt similarity index 81% rename from webflows/src/test/java/com/schibsted/account/webflows/storage/StorageTest.kt rename to webflows/src/test/java/com/schibsted/account/webflows/storage/MigratingStorageTest.kt index a1da24dd..1aac537e 100644 --- a/webflows/src/test/java/com/schibsted/account/webflows/storage/StorageTest.kt +++ b/webflows/src/test/java/com/schibsted/account/webflows/storage/MigratingStorageTest.kt @@ -11,9 +11,9 @@ import com.schibsted.account.webflows.util.Either import io.mockk.every import io.mockk.mockk import io.mockk.verify -import junit.framework.Assert.assertEquals +import org.junit.Assert.assertEquals import org.junit.Test -import java.util.* +import java.util.Date class MigratingStorageTest { private val userSession = @@ -21,18 +21,18 @@ class MigratingStorageTest { @Test fun testMigratingStorageReadsEncryptedStorage() { - val encryptedStorage: EncryptedSharedPrefsStorage = mockk { - every { - get(any(), any()) - } answers { - val callback = secondArg() - callback(Either.Right(userSession)) + val encryptedStorage: EncryptedSharedPrefsStorage = + mockk { + every { + get(any(), any()) + } answers { + val callback = secondArg() + callback(Either.Right(userSession)) + } } - } val sharedPrefsStorage: SharedPrefsStorage = mockk(relaxed = true) val migratingStorage = MigratingSessionStorage(sharedPrefsStorage, encryptedStorage) - migratingStorage.get(Fixtures.clientConfig.clientId) { it.assertRight { storedUserSession -> assertEquals(userSession.userTokens.idToken, storedUserSession?.userTokens?.idToken) @@ -50,7 +50,7 @@ class MigratingStorageTest { it.assertRight { storedUserSession -> assertEquals( userSession.userTokens.idToken, - storedUserSession?.userTokens?.idToken + storedUserSession?.userTokens?.idToken, ) } verify(exactly = 0) { diff --git a/webflows/src/test/java/com/schibsted/account/webflows/storage/ObfuscatedSessionFinderTest.kt b/webflows/src/test/java/com/schibsted/account/webflows/storage/ObfuscatedSessionFinderTest.kt index d0ca62fd..512f73f8 100644 --- a/webflows/src/test/java/com/schibsted/account/webflows/storage/ObfuscatedSessionFinderTest.kt +++ b/webflows/src/test/java/com/schibsted/account/webflows/storage/ObfuscatedSessionFinderTest.kt @@ -11,7 +11,6 @@ import org.junit.Before import org.junit.Test class ObfuscatedSessionFinderTest { - // Gson is used to serialize and deserialize objects // We mimick the Gson object used in the SessionStorage class private val gson = GsonBuilder().setDateFormat("MM dd, yyyy HH:mm:ss").create() @@ -32,7 +31,7 @@ class ObfuscatedSessionFinderTest { ObfuscatedSessionFinder.getDeobfuscatedStoredUserSessionIfViable( gson, clientId, - userSessionJson + userSessionJson, ) // then @@ -54,7 +53,7 @@ class ObfuscatedSessionFinderTest { ObfuscatedSessionFinder.getDeobfuscatedStoredUserSessionIfViable( gson, clientId, - userSessionJson + userSessionJson, ) // then @@ -67,7 +66,7 @@ class ObfuscatedSessionFinderTest { @Test fun `given obfuscated user session return deobfuscated user session object`() { - //given + // given val clientId = CLIENT_ID val userSessionJson = OBFUSCATED_USER_SESSION_JSON @@ -76,7 +75,7 @@ class ObfuscatedSessionFinderTest { ObfuscatedSessionFinder.getDeobfuscatedStoredUserSessionIfViable( gson, clientId, - userSessionJson + userSessionJson, ) // then @@ -102,7 +101,7 @@ class ObfuscatedSessionFinderTest { ObfuscatedSessionFinder.getDeobfuscatedStoredUserSessionIfViable( gson, clientId, - userSessionJson + userSessionJson, ) // then @@ -124,11 +123,12 @@ class ObfuscatedSessionFinderTest { val userSessionJson = MALFORMED_NOT_OBFUSCATED_USER_SESSION_JSON // when - val storedUserSession = ObfuscatedSessionFinder.getDeobfuscatedStoredUserSessionIfViable( - gson, - clientId, - userSessionJson - ) + val storedUserSession = + ObfuscatedSessionFinder.getDeobfuscatedStoredUserSessionIfViable( + gson, + clientId, + userSessionJson, + ) // then storedUserSession.onSuccess { @@ -145,11 +145,12 @@ class ObfuscatedSessionFinderTest { val userSessionJson = MALFORMED_OBFUSCATED_USER_SESSION_JSON // when - val storedUserSession = ObfuscatedSessionFinder.getDeobfuscatedStoredUserSessionIfViable( - gson, - clientId, - userSessionJson - ) + val storedUserSession = + ObfuscatedSessionFinder.getDeobfuscatedStoredUserSessionIfViable( + gson, + clientId, + userSessionJson, + ) // then storedUserSession.onSuccess { @@ -159,8 +160,6 @@ class ObfuscatedSessionFinderTest { } } - - /***** * DEBUG VALUES FOR TOKENS: * Use https://jwt.io/ to decode the tokens @@ -238,17 +237,24 @@ class ObfuscatedSessionFinderTest { assertEquals("pwd", idTokenClaims.amr?.get(0) ?: "") } - companion object { private const val CLIENT_ID = "123" + + @Suppress("ktlint:standard:max-line-length") private const val ACCESS_TOKEN_OBFUSCATED = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJzdWIiOiJiY2Q2MmY2OS1hYWViLTUxNzktYjgxYS0zZTI0NWNkNzc5YWYiLCJ1c2VyX2lkIjoiMTIwMDcxMDciLCJzY29wZSI6Im9wZW5pZCBvZmZsaW5lX2FjY2VzcyIsImlzcyI6Imh0dHBzOlwvXC9pZGVudGl0eS1wcmUuc2NoaWJzdGVkLmNvbVwvIiwiZXhwIjoxNzEyMTMzODc3LCJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNzEyMTMzODE3LCJjbGllbnRfaWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJqdGkiOiJlZDQzYTMwNi1mOTBkLTRjYmUtYTU1ZC1lMzM3ZjM3MzhlZjEifQ.sckisjhfp-BoB379HI2cnPeDEH1XJMBghcBBRtDC4vY" + + @Suppress("ktlint:standard:max-line-length") private const val ID_TOKEN_OBFUSCATED = "eyJraWQiOiJjN2Y2MDM2OS04MDMyLTQ1MWEtODcxZC1iNDhkMzA0YTJiMzUiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJiY2Q2MmY2OS1hYWViLTUxNzktYjgxYS0zZTI0NWNkNzc5YWYiLCJhdWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJhY3IiOiIwIiwibGVnYWN5X3VzZXJfaWQiOiIxMjAwNzEwNyIsImFtciI6WyJwd2QiXSwiYXV0aF90aW1lIjoxNzEyMTMzODA4LCJpc3MiOiJodHRwczpcL1wvaWRlbnRpdHktcHJlLnNjaGlic3RlZC5jb21cLyIsImV4cCI6MTcxMjEzNzQxNywiaWF0IjoxNzEyMTMzODE3LCJub25jZSI6IjRlTEhtWG81RVQifQ.NXIQw4lWp3FXc8MzQNmE5frV-KsmOG9JeKQBt9bCYcrBAHMvhw2bQQwm9bpjM-y36GGxtFanZGz6hyitZl_YGwU_FuM2XXWG1r5L1J2v2rFCfMnZdYfj-to28lK4JiYDU2-rc_eywdbzSbmQtps7qRxHWYTjf3gbkU5kEguYNMopncpk77pzRT3jjaBuzIPGoLBjLFG54FTKCFeeVZf-H8lSEz8-8x-p7WczLjroDHrOYdGznm9MytllN5sO1hR5d4_j6AiWfV_cyo2DadCoVcyeuI7lOoZMsP5B_HTiemj6c_C5Zl6ePKoc_8dvKGnmG6ArbQNdx1bcUbZ5m5KHwg" + + @Suppress("ktlint:standard:max-line-length") private const val REFRESH_TOKEN_OBFUSCATED = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJzdWIiOiJiY2Q2MmY2OS1hYWViLTUxNzktYjgxYS0zZTI0NWNkNzc5YWYiLCJ1c2VyX2lkIjoiMTIwMDcxMDciLCJzY29wZSI6Im9wZW5pZCBvZmZsaW5lX2FjY2VzcyIsImlzcyI6Imh0dHBzOlwvXC9pZGVudGl0eS1wcmUuc2NoaWJzdGVkLmNvbVwvIiwiZXhwIjoxNzEyNzM4NjE3LCJ0eXBlIjoicmVmcmVzaCIsImlhdCI6MTcxMjEzMzgxNywiY2xpZW50X2lkIjoiNjAyNTI1ZjJiNDFmYTMxNzg5YTk1YWE4IiwianRpIjoiYWNkNWNkNmUtMTNkNS00NTYxLTg5NjYtMzhlYTY3NjNmNGQ3Iiwic2lkIjoiYnA4bWsifQ.N1nyl_9C0-ClPb_eSytXxi_cOrRVZEWjYKU6kz8klVU" - private const val OBFUSCATED_USER_SESSION_JSON = "{\n" + + @Suppress("ktlint:standard:max-line-length") + private const val OBFUSCATED_USER_SESSION_JSON = + "{\n" + " \"a\":\"123\",\n" + " \"b\":\"04 03, 2024 10:43:37\",\n" + " \"d\":{\n" + @@ -271,14 +277,21 @@ class ObfuscatedSessionFinderTest { " }\n" + "}" + @Suppress("ktlint:standard:max-line-length") private const val ACCESS_TOKEN_NOT_OBFUSCATED = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJzdWIiOiJiY2Q2MmY2OS1hYWViLTUxNzktYjgxYS0zZTI0NWNkNzc5YWYiLCJ1c2VyX2lkIjoiMTIwMDcxMDciLCJzY29wZSI6Im9wZW5pZCBvZmZsaW5lX2FjY2VzcyIsImlzcyI6Imh0dHBzOlwvXC9pZGVudGl0eS1wcmUuc2NoaWJzdGVkLmNvbVwvIiwiZXhwIjoxNzEyMTMzODc3LCJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNzEyMTMzODE3LCJjbGllbnRfaWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJqdGkiOiJlZDQzYTMwNi1mOTBkLTRjYmUtYTU1ZC1lMzM3ZjM3MzhlZjEifQ.sckisjhfp-BoB379HI2cnPeDEH1XJMBghcBBRtDC4vY" + + @Suppress("ktlint:standard:max-line-length") private const val ID_TOKEN_NOT_OBFUSCATED = "eyJraWQiOiJjN2Y2MDM2OS04MDMyLTQ1MWEtODcxZC1iNDhkMzA0YTJiMzUiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJiY2Q2MmY2OS1hYWViLTUxNzktYjgxYS0zZTI0NWNkNzc5YWYiLCJhdWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJhY3IiOiIwIiwibGVnYWN5X3VzZXJfaWQiOiIxMjAwNzEwNyIsImFtciI6WyJwd2QiXSwiYXV0aF90aW1lIjoxNzEyMTMzODA4LCJpc3MiOiJodHRwczpcL1wvaWRlbnRpdHktcHJlLnNjaGlic3RlZC5jb21cLyIsImV4cCI6MTcxMjEzNzQxNywiaWF0IjoxNzEyMTMzODE3LCJub25jZSI6IjRlTEhtWG81RVQifQ.NXIQw4lWp3FXc8MzQNmE5frV-KsmOG9JeKQBt9bCYcrBAHMvhw2bQQwm9bpjM-y36GGxtFanZGz6hyitZl_YGwU_FuM2XXWG1r5L1J2v2rFCfMnZdYfj-to28lK4JiYDU2-rc_eywdbzSbmQtps7qRxHWYTjf3gbkU5kEguYNMopncpk77pzRT3jjaBuzIPGoLBjLFG54FTKCFeeVZf-H8lSEz8-8x-p7WczLjroDHrOYdGznm9MytllN5sO1hR5d4_j6AiWfV_cyo2DadCoVcyeuI7lOoZMsP5B_HTiemj6c_C5Zl6ePKoc_8dvKGnmG6ArbQNdx1bcUbZ5m5KHwg" + + @Suppress("ktlint:standard:max-line-length") private const val REFRESH_TOKEN_NOT_OBFUSCATED = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJzdWIiOiJiY2Q2MmY2OS1hYWViLTUxNzktYjgxYS0zZTI0NWNkNzc5YWYiLCJ1c2VyX2lkIjoiMTIwMDcxMDciLCJzY29wZSI6Im9wZW5pZCBvZmZsaW5lX2FjY2VzcyIsImlzcyI6Imh0dHBzOlwvXC9pZGVudGl0eS1wcmUuc2NoaWJzdGVkLmNvbVwvIiwiZXhwIjoxNzEyNzM4NjE3LCJ0eXBlIjoicmVmcmVzaCIsImlhdCI6MTcxMjEzMzgxNywiY2xpZW50X2lkIjoiNjAyNTI1ZjJiNDFmYTMxNzg5YTk1YWE4IiwianRpIjoiYWNkNWNkNmUtMTNkNS00NTYxLTg5NjYtMzhlYTY3NjNmNGQ3Iiwic2lkIjoiYnA4bWsifQ.N1nyl_9C0-ClPb_eSytXxi_cOrRVZEWjYKU6kz8klVU" - private const val NOT_OBFUSCATED_USER_SESSION_JSON = "{\n" + + @Suppress("ktlint:standard:max-line-length") + private const val NOT_OBFUSCATED_USER_SESSION_JSON = + "{\n" + " \"clientId\":\"123\",\n" + " \"updatedAt\":\"04 03, 2024 10:43:37\",\n" + " \"userTokens\":{\n" + @@ -302,12 +315,14 @@ class ObfuscatedSessionFinderTest { "}" // Simply comment any line to check the test case - private const val MALFORMED_NOT_OBFUSCATED_USER_SESSION_JSON = "{\n" + + @Suppress("ktlint:standard:max-line-length") + private const val MALFORMED_NOT_OBFUSCATED_USER_SESSION_JSON = + "{\n" + " \"a\":\"123\",\n" + " \"b\":\"04 03, 2024 10:43:37\",\n" + " \"d\":{\n" + " \"e\":\"eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJzdWIiOiJiY2Q2MmY2OS1hYWViLTUxNzktYjgxYS0zZTI0NWNkNzc5YWYiLCJ1c2VyX2lkIjoiMTIwMDcxMDciLCJzY29wZSI6Im9wZW5pZCBvZmZsaW5lX2FjY2VzcyIsImlzcyI6Imh0dHBzOlwvXC9pZGVudGl0eS1wcmUuc2NoaWJzdGVkLmNvbVwvIiwiZXhwIjoxNzEyMTMzODc3LCJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNzEyMTMzODE3LCJjbGllbnRfaWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJqdGkiOiJlZDQzYTMwNi1mOTBkLTRjYmUtYTU1ZC1lMzM3ZjM3MzhlZjEifQ.sckisjhfp-BoB379HI2cnPeDEH1XJMBghcBBRtDC4vY\",\n" + - // " \"f\":\"eyJraWQiOiJjN2Y2MDM2OS04MDMyLTQ1MWEtODcxZC1iNDhkMzA0YTJiMzUiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJiY2Q2MmY2OS1hYWViLTUxNzktYjgxYS0zZTI0NWNkNzc5YWYiLCJhdWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJhY3IiOiIwIiwibGVnYWN5X3VzZXJfaWQiOiIxMjAwNzEwNyIsImFtciI6WyJwd2QiXSwiYXV0aF90aW1lIjoxNzEyMTMzODA4LCJpc3MiOiJodHRwczpcL1wvaWRlbnRpdHktcHJlLnNjaGlic3RlZC5jb21cLyIsImV4cCI6MTcxMjEzNzQxNywiaWF0IjoxNzEyMTMzODE3LCJub25jZSI6IjRlTEhtWG81RVQifQ.NXIQw4lWp3FXc8MzQNmE5frV-KsmOG9JeKQBt9bCYcrBAHMvhw2bQQwm9bpjM-y36GGxtFanZGz6hyitZl_YGwU_FuM2XXWG1r5L1J2v2rFCfMnZdYfj-to28lK4JiYDU2-rc_eywdbzSbmQtps7qRxHWYTjf3gbkU5kEguYNMopncpk77pzRT3jjaBuzIPGoLBjLFG54FTKCFeeVZf-H8lSEz8-8x-p7WczLjroDHrOYdGznm9MytllN5sO1hR5d4_j6AiWfV_cyo2DadCoVcyeuI7lOoZMsP5B_HTiemj6c_C5Zl6ePKoc_8dvKGnmG6ArbQNdx1bcUbZ5m5KHwg\",\n" + + // " \"f\":\"eyJraWQiOiJjN2Y2MDM2OS04MDMyLTQ1MWEtODcxZC1iNDhkMzA0YTJiMzUiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJiY2Q2MmY2OS1hYWViLTUxNzktYjgxYS0zZTI0NWNkNzc5YWYiLCJhdWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJhY3IiOiIwIiwibGVnYWN5X3VzZXJfaWQiOiIxMjAwNzEwNyIsImFtciI6WyJwd2QiXSwiYXV0aF90aW1lIjoxNzEyMTMzODA4LCJpc3MiOiJodHRwczpcL1wvaWRlbnRpdHktcHJlLnNjaGlic3RlZC5jb21cLyIsImV4cCI6MTcxMjEzNzQxNywiaWF0IjoxNzEyMTMzODE3LCJub25jZSI6IjRlTEhtWG81RVQifQ.NXIQw4lWp3FXc8MzQNmE5frV-KsmOG9JeKQBt9bCYcrBAHMvhw2bQQwm9bpjM-y36GGxtFanZGz6hyitZl_YGwU_FuM2XXWG1r5L1J2v2rFCfMnZdYfj-to28lK4JiYDU2-rc_eywdbzSbmQtps7qRxHWYTjf3gbkU5kEguYNMopncpk77pzRT3jjaBuzIPGoLBjLFG54FTKCFeeVZf-H8lSEz8-8x-p7WczLjroDHrOYdGznm9MytllN5sO1hR5d4_j6AiWfV_cyo2DadCoVcyeuI7lOoZMsP5B_HTiemj6c_C5Zl6ePKoc_8dvKGnmG6ArbQNdx1bcUbZ5m5KHwg\",\n" + " \"g\":{\n" + " \"h\":[\n" + " \"pwd\"\n" + @@ -326,10 +341,12 @@ class ObfuscatedSessionFinderTest { "}" // Simply comment any line to check the test case - private const val MALFORMED_OBFUSCATED_USER_SESSION_JSON = "{\n" + + @Suppress("ktlint:standard:max-line-length") + private const val MALFORMED_OBFUSCATED_USER_SESSION_JSON = + "{\n" + " \"a\":\"123\",\n" + " \"b\":{\n" + - // " \"d\":\"eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJzdWIiOiJiY2Q2MmY2OS1hYWViLTUxNzktYjgxYS0zZTI0NWNkNzc5YWYiLCJ1c2VyX2lkIjoiMTIwMDcxMDciLCJzY29wZSI6Im9wZW5pZCBvZmZsaW5lX2FjY2VzcyIsImlzcyI6Imh0dHBzOlwvXC9pZGVudGl0eS1wcmUuc2NoaWJzdGVkLmNvbVwvIiwiZXhwIjoxNzEyNTYyMDc5LCJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNzEyNTYyMDE5LCJjbGllbnRfaWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJqdGkiOiI2NjYwNzA0NS0xMzQwLTQ1OTktOTM2My1jM2M0MjdjMWRiMWIifQ.ABPt3TbnD2v_5zDaPK5X5djnV_sXqDkq9u802Q2BN-c\",\n" + + // " \"d\":\"eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJzdWIiOiJiY2Q2MmY2OS1hYWViLTUxNzktYjgxYS0zZTI0NWNkNzc5YWYiLCJ1c2VyX2lkIjoiMTIwMDcxMDciLCJzY29wZSI6Im9wZW5pZCBvZmZsaW5lX2FjY2VzcyIsImlzcyI6Imh0dHBzOlwvXC9pZGVudGl0eS1wcmUuc2NoaWJzdGVkLmNvbVwvIiwiZXhwIjoxNzEyNTYyMDc5LCJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNzEyNTYyMDE5LCJjbGllbnRfaWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJqdGkiOiI2NjYwNzA0NS0xMzQwLTQ1OTktOTM2My1jM2M0MjdjMWRiMWIifQ.ABPt3TbnD2v_5zDaPK5X5djnV_sXqDkq9u802Q2BN-c\",\n" + " \"e\":\"eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJzdWIiOiJiY2Q2MmY2OS1hYWViLTUxNzktYjgxYS0zZTI0NWNkNzc5YWYiLCJ1c2VyX2lkIjoiMTIwMDcxMDciLCJzY29wZSI6Im9wZW5pZCBvZmZsaW5lX2FjY2VzcyIsImlzcyI6Imh0dHBzOlwvXC9pZGVudGl0eS1wcmUuc2NoaWJzdGVkLmNvbVwvIiwiZXhwIjoxNzEzMTY2ODE5LCJ0eXBlIjoicmVmcmVzaCIsImlhdCI6MTcxMjU2MjAxOSwiY2xpZW50X2lkIjoiNjAyNTI1ZjJiNDFmYTMxNzg5YTk1YWE4IiwianRpIjoiMTYyYzkwMmQtMjdhYi00MGYwLWJjMjktM2U2NjczZWRiMTExIiwic2lkIjoiUms2bGsifQ.ym_eEVNrPgfb10wP1O8-tuJlUzwhjgMz2JgposKHzL8\",\n" + " \"f\":\"eyJraWQiOiJjN2Y2MDM2OS04MDMyLTQ1MWEtODcxZC1iNDhkMzA0YTJiMzUiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJiY2Q2MmY2OS1hYWViLTUxNzktYjgxYS0zZTI0NWNkNzc5YWYiLCJhdWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJhY3IiOiIwIiwibGVnYWN5X3VzZXJfaWQiOiIxMjAwNzEwNyIsImFtciI6WyJja2UiXSwiYXV0aF90aW1lIjoxNzEyNTYyMDE5LCJpc3MiOiJodHRwczpcL1wvaWRlbnRpdHktcHJlLnNjaGlic3RlZC5jb21cLyIsImV4cCI6MTcxMjU2NTYxOSwiaWF0IjoxNzEyNTYyMDE5LCJub25jZSI6IlBCNXVyME1HbG4ifQ.m_cZBMtj7SlXmaAfVZNWK_wv8WQufpVRUX6a8pNtBppVZ8sQ0J0KswWjIC7uPsPoaP6jBQTfyy7w3JL9SCs00DK3AAX7spYDssGLac-YO_5sEzeX4bGsPwY5xH5oRgL1JspCTSxPqk-oArduzZYffHVq4g50_MweU6vNZ_6qRtFaDQ8DzTtM6qS3lr03zy1ckptn3eemL5PbZix-ZSVinKFIOsTeQaS9b7xVMbgupP6FRtRTuwU2248OW2b_2lxWlv2mX51tiFy1oyYcbbOu_TY94p0QHDrD85CclpMwDBNG4bc2rJ9hBcm4QiLLi6TMdeYdb-7KfTdvH6_xWVxdLQ\",\n" + " \"g\":{\n" + @@ -349,8 +366,13 @@ class ObfuscatedSessionFinderTest { " \"c\":\"04 08, 2024 09:40:19\"\n" + "}" + @Suppress("ktlint:standard:max-line-length") private const val REFRESH_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJzdWIiOiJiY2Q2MmY2OS1hYWViLTUxNzktYjgxYS0zZTI0NWNkNzc5YWYiLCJ1c2VyX2lkIjoiMTIwMDcxMDciLCJzY29wZSI6Im9wZW5pZCBvZmZsaW5lX2FjY2VzcyIsImlzcyI6Imh0dHBzOlwvXC9pZGVudGl0eS1wcmUuc2NoaWJzdGVkLmNvbVwvIiwiZXhwIjoxNzEyNzM4NjE3LCJ0eXBlIjoicmVmcmVzaCIsImlhdCI6MTcxMjEzMzgxNywiY2xpZW50X2lkIjoiNjAyNTI1ZjJiNDFmYTMxNzg5YTk1YWE4IiwianRpIjoiYWNkNWNkNmUtMTNkNS00NTYxLTg5NjYtMzhlYTY3NjNmNGQ3Iiwic2lkIjoiYnA4bWsifQ.N1nyl_9C0-ClPb_eSytXxi_cOrRVZEWjYKU6kz8klVU" + + @Suppress("ktlint:standard:max-line-length") private const val ACCESS_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJzdWIiOiJiY2Q2MmY2OS1hYWViLTUxNzktYjgxYS0zZTI0NWNkNzc5YWYiLCJ1c2VyX2lkIjoiMTIwMDcxMDciLCJzY29wZSI6Im9wZW5pZCBvZmZsaW5lX2FjY2VzcyIsImlzcyI6Imh0dHBzOlwvXC9pZGVudGl0eS1wcmUuc2NoaWJzdGVkLmNvbVwvIiwiZXhwIjoxNzEyMTMzODc3LCJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNzEyMTMzODE3LCJjbGllbnRfaWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJqdGkiOiJlZDQzYTMwNi1mOTBkLTRjYmUtYTU1ZC1lMzM3ZjM3MzhlZjEifQ.sckisjhfp-BoB379HI2cnPeDEH1XJMBghcBBRtDC4vY" + + @Suppress("ktlint:standard:max-line-length") private const val ID_TOKEN = "eyJraWQiOiJjN2Y2MDM2OS04MDMyLTQ1MWEtODcxZC1iNDhkMzA0YTJiMzUiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJiY2Q2MmY2OS1hYWViLTUxNzktYjgxYS0zZTI0NWNkNzc5YWYiLCJhdWQiOiI2MDI1MjVmMmI0MWZhMzE3ODlhOTVhYTgiLCJhY3IiOiIwIiwibGVnYWN5X3VzZXJfaWQiOiIxMjAwNzEwNyIsImFtciI6WyJwd2QiXSwiYXV0aF90aW1lIjoxNzEyMTMzODA4LCJpc3MiOiJodHRwczpcL1wvaWRlbnRpdHktcHJlLnNjaGlic3RlZC5jb21cLyIsImV4cCI6MTcxMjEzNzQxNywiaWF0IjoxNzEyMTMzODE3LCJub25jZSI6IjRlTEhtWG81RVQifQ.NXIQw4lWp3FXc8MzQNmE5frV-KsmOG9JeKQBt9bCYcrBAHMvhw2bQQwm9bpjM-y36GGxtFanZGz6hyitZl_YGwU_FuM2XXWG1r5L1J2v2rFCfMnZdYfj-to28lK4JiYDU2-rc_eywdbzSbmQtps7qRxHWYTjf3gbkU5kEguYNMopncpk77pzRT3jjaBuzIPGoLBjLFG54FTKCFeeVZf-H8lSEz8-8x-p7WczLjroDHrOYdGznm9MytllN5sO1hR5d4_j6AiWfV_cyo2DadCoVcyeuI7lOoZMsP5B_HTiemj6c_C5Zl6ePKoc_8dvKGnmG6ArbQNdx1bcUbZ5m5KHwg" } -} \ No newline at end of file +} diff --git a/webflows/src/test/java/com/schibsted/account/webflows/token/IdTokenValidatorTest.kt b/webflows/src/test/java/com/schibsted/account/webflows/token/IdTokenValidatorTest.kt index 7ebe6ce0..11c2354b 100644 --- a/webflows/src/test/java/com/schibsted/account/webflows/token/IdTokenValidatorTest.kt +++ b/webflows/src/test/java/com/schibsted/account/webflows/token/IdTokenValidatorTest.kt @@ -12,7 +12,7 @@ import com.schibsted.account.webflows.jose.AsyncJwks import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test -import java.util.* +import java.util.Date private class TestJwks(private val jwks: JWKSet?) : AsyncJwks { override fun fetch(callback: (JWKSet?) -> Unit) { @@ -25,24 +25,25 @@ class IdTokenValidatorTest { private val jwks: JWKSet init { - jwk = RSAKeyGenerator(2048) - .keyID(idTokenKeyId) - .generate() + jwk = + RSAKeyGenerator(2048) + .keyID(ID_TOKEN_KEY_ID) + .generate() jwks = JWKSet(jwk) } private fun defaultIdTokenClaims(): JWTClaimsSet.Builder { return JWTClaimsSet.Builder() - .issuer(issuer) - .audience(clientId) + .issuer(ISSUER) + .audience(CLIENT_ID) .expirationTime(Date(System.currentTimeMillis() + 5000)) - .subject(userUuid) - .claim("legacy_user_id", userId) - .claim("nonce", nonce) + .subject(USER_UUID) + .claim("legacy_user_id", USER_ID) + .claim("nonce", NONCE) } private fun createIdToken(claims: JWTClaimsSet): String { - return createJws(jwk.toRSAKey(), idTokenKeyId, Payload(claims.toJSONObject())) + return createJws(jwk.toRSAKey(), ID_TOKEN_KEY_ID, Payload(claims.toJSONObject())) } private fun idTokenClaims(claims: JWTClaimsSet): IdTokenClaims { @@ -53,20 +54,23 @@ class IdTokenValidatorTest { claims.audience, (claims.expirationTime.time / 1000), claims.getStringClaim("nonce"), - claims.getStringListClaim("amr") + claims.getStringListClaim("amr"), ) } - private fun assertErrorMessage(expectedSubstring: String, actualMessage: String) { + private fun assertErrorMessage( + expectedSubstring: String, + actualMessage: String, + ) { assertTrue( - "'${expectedSubstring}' not in '${actualMessage}'", - actualMessage.contains(expectedSubstring) + "'$expectedSubstring' not in '$actualMessage'", + actualMessage.contains(expectedSubstring), ) } @Test fun testAcceptsValidWithoutAMR() { - val context = IdTokenValidationContext(issuer, clientId, nonce, null) + val context = IdTokenValidationContext(ISSUER, CLIENT_ID, NONCE, null) val claims = defaultIdTokenClaims().build() val idToken = createIdToken(claims) IdTokenValidator.validate(idToken, TestJwks(jwks), context) { result -> @@ -77,10 +81,11 @@ class IdTokenValidatorTest { @Test fun testAcceptsValidWithExpectedAMR() { val expectedAmrValue = "testValue" - val context = IdTokenValidationContext(issuer, clientId, nonce, expectedAmrValue) - val claims = defaultIdTokenClaims() - .claim("amr", listOf(expectedAmrValue, "otherValue")) - .build() + val context = IdTokenValidationContext(ISSUER, CLIENT_ID, NONCE, expectedAmrValue) + val claims = + defaultIdTokenClaims() + .claim("amr", listOf(expectedAmrValue, "otherValue")) + .build() val idToken = createIdToken(claims) IdTokenValidator.validate(idToken, TestJwks(jwks), context) { result -> result.assertRight { assertEquals(idTokenClaims(claims), it) } @@ -90,10 +95,11 @@ class IdTokenValidatorTest { @Test fun testAcceptsEidAMRWithoutCountryPrefix() { val expectedAmrValue = "eid-se" - val context = IdTokenValidationContext(issuer, clientId, nonce, expectedAmrValue) - val claims = defaultIdTokenClaims() - .claim("amr", listOf("eid", "otherValue")) - .build() + val context = IdTokenValidationContext(ISSUER, CLIENT_ID, NONCE, expectedAmrValue) + val claims = + defaultIdTokenClaims() + .claim("amr", listOf("eid", "otherValue")) + .build() val idToken = createIdToken(claims) IdTokenValidator.validate(idToken, TestJwks(jwks), context) { result -> result.assertRight { assertEquals(idTokenClaims(claims), it) } @@ -102,12 +108,13 @@ class IdTokenValidatorTest { @Test fun testRejectMissingExpectedAMRInIdTokenWithoutAMR() { - val context = IdTokenValidationContext(issuer, clientId, nonce, "testValue") + val context = IdTokenValidationContext(ISSUER, CLIENT_ID, NONCE, "testValue") for (amr in listOf(null, emptyList(), listOf("otherValue1", "otherValue2"))) { - val claims = defaultIdTokenClaims() - .claim("amr", amr) - .build() + val claims = + defaultIdTokenClaims() + .claim("amr", amr) + .build() val idToken = createIdToken(claims) IdTokenValidator.validate(idToken, TestJwks(jwks), context) { result -> result.assertLeft { assertErrorMessage("Missing expected AMR value", it.message) } @@ -117,12 +124,13 @@ class IdTokenValidatorTest { @Test fun testRejectsMismatchingNonce() { - val context = IdTokenValidationContext(issuer, clientId, nonce, null) + val context = IdTokenValidationContext(ISSUER, CLIENT_ID, NONCE, null) for (nonce in listOf(null, "otherNonce")) { - val claims = defaultIdTokenClaims() - .claim("nonce", nonce) - .build() + val claims = + defaultIdTokenClaims() + .claim("nonce", nonce) + .build() val idToken = createIdToken(claims) IdTokenValidator.validate(idToken, TestJwks(jwks), context) { result -> result.assertLeft { assertErrorMessage("nonce", it.message) } @@ -132,11 +140,12 @@ class IdTokenValidatorTest { @Test fun testRejectsMismatchingIssuer() { - val context = IdTokenValidationContext(issuer, clientId, nonce, null) + val context = IdTokenValidationContext(ISSUER, CLIENT_ID, NONCE, null) - val claims = defaultIdTokenClaims() - .issuer("https://other.example.com") - .build() + val claims = + defaultIdTokenClaims() + .issuer("https://other.example.com") + .build() val idToken = createIdToken(claims) IdTokenValidator.validate(idToken, TestJwks(jwks), context) { result -> result.assertLeft { assertErrorMessage("Invalid issuer", it.message) } @@ -145,19 +154,21 @@ class IdTokenValidatorTest { @Test fun testAcceptsIssuerWithTrailingSlash() { - val issuerWithSlash = "$issuer/" - val testData = listOf( - issuer to issuer, - issuer to issuerWithSlash, - issuerWithSlash to issuer, - issuerWithSlash to issuerWithSlash - ) + val issuerWithSlash = "$ISSUER/" + val testData = + listOf( + ISSUER to ISSUER, + ISSUER to issuerWithSlash, + issuerWithSlash to ISSUER, + issuerWithSlash to issuerWithSlash, + ) for ((idTokenIssuer, expectedIssuer) in testData) { - val context = IdTokenValidationContext(expectedIssuer, clientId, nonce, null) - val claims = defaultIdTokenClaims() - .issuer(idTokenIssuer) - .build() + val context = IdTokenValidationContext(expectedIssuer, CLIENT_ID, NONCE, null) + val claims = + defaultIdTokenClaims() + .issuer(idTokenIssuer) + .build() val idToken = createIdToken(claims) IdTokenValidator.validate(idToken, TestJwks(jwks), context) { result -> result.assertRight { assertEquals(idTokenClaims(claims), it) } @@ -167,11 +178,12 @@ class IdTokenValidatorTest { @Test fun testRejectsAudienceClaimWithoutExpectedClientId() { - val context = IdTokenValidationContext(issuer, clientId, nonce, null) + val context = IdTokenValidationContext(ISSUER, CLIENT_ID, NONCE, null) - val claims = defaultIdTokenClaims() - .audience("otherClient") - .build() + val claims = + defaultIdTokenClaims() + .audience("otherClient") + .build() val idToken = createIdToken(claims) IdTokenValidator.validate(idToken, TestJwks(jwks), context) { result -> result.assertLeft { assertErrorMessage("audience", it.message) } @@ -180,12 +192,13 @@ class IdTokenValidatorTest { @Test fun testRejectsExpiredIdToken() { - val context = IdTokenValidationContext(issuer, clientId, nonce, null) + val context = IdTokenValidationContext(ISSUER, CLIENT_ID, NONCE, null) val hourAgo = System.currentTimeMillis() - (60 * 60) * 1000 - val claims = defaultIdTokenClaims() - .expirationTime(Date(hourAgo)) - .build() + val claims = + defaultIdTokenClaims() + .expirationTime(Date(hourAgo)) + .build() val idToken = createIdToken(claims) IdTokenValidator.validate(idToken, TestJwks(jwks), context) { result -> result.assertLeft { assertErrorMessage("Expired JWT", it.message) } @@ -193,11 +206,11 @@ class IdTokenValidatorTest { } companion object { - const val idTokenKeyId = "testKey" - const val issuer = "https://issuer.example.com" - const val clientId = "client1" - const val userUuid = "userUuid" - const val userId = "12345" - const val nonce = "nonce1234" + const val ID_TOKEN_KEY_ID = "testKey" + const val ISSUER = "https://issuer.example.com" + const val CLIENT_ID = "client1" + const val USER_UUID = "userUuid" + const val USER_ID = "12345" + const val NONCE = "nonce1234" } } diff --git a/webflows/src/test/java/com/schibsted/account/webflows/user/UserTest.kt b/webflows/src/test/java/com/schibsted/account/webflows/user/UserTest.kt index bb3686e3..a7454179 100644 --- a/webflows/src/test/java/com/schibsted/account/webflows/user/UserTest.kt +++ b/webflows/src/test/java/com/schibsted/account/webflows/user/UserTest.kt @@ -2,10 +2,13 @@ package com.schibsted.account.webflows.user import android.os.Build import android.os.Looper -import androidx.annotation.RequiresApi import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SdkSuppress -import com.schibsted.account.testutil.* +import com.schibsted.account.testutil.Fixtures +import com.schibsted.account.testutil.assertLeft +import com.schibsted.account.testutil.assertRight +import com.schibsted.account.testutil.await +import com.schibsted.account.testutil.withServer import com.schibsted.account.webflows.activities.AuthResultLiveData import com.schibsted.account.webflows.activities.AuthResultLiveDataTest import com.schibsted.account.webflows.activities.NotAuthed @@ -27,17 +30,22 @@ import io.mockk.verify import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.mockwebserver.MockResponse -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith import org.robolectric.Shadows.shadowOf import java.net.ConnectException -import java.util.* +import java.util.Date import java.util.concurrent.CompletableFuture @RunWith(AndroidJUnit4::class) class UserTest { - @Test fun `Equals fun should return true if token content is the same`() { val userTokens1 = @@ -89,7 +97,7 @@ class UserTest { result.assertRight { assertEquals(responseData, it.body!!.string()) } assertEquals( "Bearer ${Fixtures.userTokens.accessToken}", - server.takeRequest().getHeader("Authorization") + server.takeRequest().getHeader("Authorization"), ) done() } @@ -113,35 +121,40 @@ class UserTest { @Test fun makeAuthenticatedRequestRefreshesTokenWhen401Response() { - val tokenHandler: TokenHandler = mockk(relaxed = true) { - every { makeTokenRequest(any(), null) } returns run { - val tokensResult = UserTokenResponse( - "newAccessToken", - "newRefreshToken", - null, - "openid offline_access", - 10 - ) - Right(tokensResult) + val tokenHandler: TokenHandler = + mockk(relaxed = true) { + every { makeTokenRequest(any(), null) } returns + run { + val tokensResult = + UserTokenResponse( + "newAccessToken", + "newRefreshToken", + null, + "openid offline_access", + 10, + ) + Right(tokensResult) + } + } + val sessionStorageMock: SessionStorage = + mockk(relaxed = true) { + every { save(any()) } returns Unit } - } - val sessionStorageMock: SessionStorage = mockk(relaxed = true) { - every { save(any()) } returns Unit - } val user = User( Fixtures.getClient( tokenHandler = tokenHandler, - sessionStorage = sessionStorageMock + sessionStorage = sessionStorageMock, ), - Fixtures.userTokens + Fixtures.userTokens, ) val responseData = "Test data" - val responses = arrayOf( - MockResponse().setResponseCode(401).setBody("Unauthorized"), - MockResponse().setResponseCode(200).setBody(responseData) - ) + val responses = + arrayOf( + MockResponse().setResponseCode(401).setBody("Unauthorized"), + MockResponse().setResponseCode(200).setBody(responseData), + ) withServer(*responses) { server -> val request = Request.Builder().url(server.url("/")).build() @@ -151,26 +164,29 @@ class UserTest { assertEquals( "Bearer ${Fixtures.userTokens.accessToken}", - server.takeRequest().getHeader("Authorization") + server.takeRequest().getHeader("Authorization"), ) assertEquals( "Bearer newAccessToken", - server.takeRequest().getHeader("Authorization") + server.takeRequest().getHeader("Authorization"), ) verify { - sessionStorageMock.save(withArg { storedSession -> - assertEquals(Fixtures.clientConfig.clientId, storedSession.clientId) - assertEquals( - Fixtures.userTokens.copy( - accessToken = "newAccessToken", - "newRefreshToken" - ), storedSession.userTokens - ) - val secondsSinceSessionCreated = - (Date().time - storedSession.updatedAt.time) / 1000 - assertTrue(secondsSinceSessionCreated < 1) // created within last second - }) + sessionStorageMock.save( + withArg { storedSession -> + assertEquals(Fixtures.clientConfig.clientId, storedSession.clientId) + assertEquals( + Fixtures.userTokens.copy( + accessToken = "newAccessToken", + "newRefreshToken", + ), + storedSession.userTokens, + ) + val secondsSinceSessionCreated = + (Date().time - storedSession.updatedAt.time) / 1000 + assertTrue(secondsSinceSessionCreated < 1) // created within last second + }, + ) } done() } @@ -178,27 +194,30 @@ class UserTest { } } - @Test fun makeAuthenticatedRequestDoesntRefreshOnRepeated401() { - val tokenHandler: TokenHandler = mockk(relaxed = true) { - every { makeTokenRequest(any(), null) } returns run { - val tokensResult = UserTokenResponse( - "newAccessToken", - "newRefreshToken", - null, - "openid offline_access", - 10 - ) - Right(tokensResult) + val tokenHandler: TokenHandler = + mockk(relaxed = true) { + every { makeTokenRequest(any(), null) } returns + run { + val tokensResult = + UserTokenResponse( + "newAccessToken", + "newRefreshToken", + null, + "openid offline_access", + 10, + ) + Right(tokensResult) + } } - } val user = User(Fixtures.getClient(tokenHandler = tokenHandler), Fixtures.userTokens) - val responses = arrayOf( - MockResponse().setResponseCode(401).setBody("Unauthorized"), - MockResponse().setResponseCode(401).setBody("Still unauthorized"), - ) + val responses = + arrayOf( + MockResponse().setResponseCode(401).setBody("Unauthorized"), + MockResponse().setResponseCode(401).setBody("Still unauthorized"), + ) withServer(*responses) { server -> val request = Request.Builder().url(server.url("/")).build() @@ -208,18 +227,18 @@ class UserTest { assertEquals( "Bearer ${Fixtures.userTokens.accessToken}", - server.takeRequest().getHeader("Authorization") + server.takeRequest().getHeader("Authorization"), ) assertEquals( "Bearer newAccessToken", - server.takeRequest().getHeader("Authorization") + server.takeRequest().getHeader("Authorization"), ) // only tries to refresh once verify(exactly = 1) { tokenHandler.makeTokenRequest( Fixtures.userTokens.refreshToken!!, - null + null, ) } done() @@ -240,7 +259,7 @@ class UserTest { result.assertRight { assertEquals("Unauthorized", it.body!!.string()) } assertEquals( "Bearer ${Fixtures.userTokens.accessToken}", - server.takeRequest().getHeader("Authorization") + server.takeRequest().getHeader("Authorization"), ) done() @@ -249,16 +268,17 @@ class UserTest { } } - @Test fun makeAuthenticatedRequestForwardsOriginalResponseWhenTokenRefreshFails() { - val tokenHandler: TokenHandler = mockk(relaxed = true) { - every { makeTokenRequest(any(), null) } returns run { - val error = - TokenError.TokenRequestError(HttpError.UnexpectedError(Error("Refresh token request failed"))) - Left(error) + val tokenHandler: TokenHandler = + mockk(relaxed = true) { + every { makeTokenRequest(any(), null) } returns + run { + val error = + TokenError.TokenRequestError(HttpError.UnexpectedError(Error("Refresh token request failed"))) + Left(error) + } } - } val user = User(Fixtures.getClient(tokenHandler = tokenHandler), Fixtures.userTokens) withServer(MockResponse().setResponseCode(401).setBody("Unauthorized")) { server -> @@ -269,7 +289,7 @@ class UserTest { result.assertRight { assertEquals("Unauthorized", it.body!!.string()) } assertEquals( "Bearer ${Fixtures.userTokens.accessToken}", - server.takeRequest().getHeader("Authorization") + server.takeRequest().getHeader("Authorization"), ) verify(exactly = 1) { @@ -307,7 +327,7 @@ class UserTest { every { client.refreshTokensForUser(user) } answers { Thread.sleep(20) // artificial delay to simulate network request Right(Fixtures.userTokens.copy(accessToken = "accessToken1")) - } andThenAnswer { + } andThenAnswer { Right(Fixtures.userTokens.copy(accessToken = "accessToken2")) } @@ -326,14 +346,15 @@ class UserTest { fun refreshTokensLogsOutOnInvalidGrantResponse() { val client: Client = mockk(relaxed = true) val user = User(client, Fixtures.userTokens) - val tokenRefreshResponse = Left( - RefreshTokenError.RefreshRequestFailed( - HttpError.ErrorResponse( - 400, - """{"error": "invalid_grant", "error_description": "Invalid refresh token"}""" - ) + val tokenRefreshResponse = + Left( + RefreshTokenError.RefreshRequestFailed( + HttpError.ErrorResponse( + 400, + """{"error": "invalid_grant", "error_description": "Invalid refresh token"}""", + ), + ), ) - ) every { client.refreshTokensForUser(user) } returns tokenRefreshResponse assertEquals(Left(RefreshTokenError.UserWasLoggedOut), user.refreshTokens()) @@ -344,14 +365,15 @@ class UserTest { fun refreshTokensDoesNotCauseLogoutOn500Response() { val client: Client = mockk(relaxed = true) val user = User(client, Fixtures.userTokens) - val tokenRefreshResponse = Left( - RefreshTokenError.RefreshRequestFailed( - HttpError.ErrorResponse( - 500, - """Error""" - ) + val tokenRefreshResponse = + Left( + RefreshTokenError.RefreshRequestFailed( + HttpError.ErrorResponse( + 500, + """Error""", + ), + ), ) - ) every { client.refreshTokensForUser(user) } returns tokenRefreshResponse assertEquals(tokenRefreshResponse, user.refreshTokens()) @@ -394,7 +416,7 @@ class UserTest { val user = User(Fixtures.getClient(), Fixtures.userTokens) assertEquals( "${Fixtures.clientConfig.serverUrl}/profile-pages", - user.accountPagesUrl().toString() + user.accountPagesUrl().toString(), ) } } diff --git a/webflows/src/test/java/com/schibsted/account/webflows/util/BestEffortRunOnceTaskTest.kt b/webflows/src/test/java/com/schibsted/account/webflows/util/BestEffortRunOnceTaskTest.kt index 04d5e1b0..30c0d325 100644 --- a/webflows/src/test/java/com/schibsted/account/webflows/util/BestEffortRunOnceTaskTest.kt +++ b/webflows/src/test/java/com/schibsted/account/webflows/util/BestEffortRunOnceTaskTest.kt @@ -3,7 +3,8 @@ package com.schibsted.account.webflows.util import io.mockk.every import io.mockk.mockk import io.mockk.verify -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Ignore import org.junit.Test import kotlin.concurrent.thread @@ -13,14 +14,18 @@ private interface TestOperation { } class BestEffortRunOnceTaskTest { - private fun runInParallel(numThreads: Int, task: BestEffortRunOnceTask): Map { + private fun runInParallel( + numThreads: Int, + task: BestEffortRunOnceTask, + ): Map { val results = mutableMapOf() - val threads = (0 until numThreads).map { i -> - thread { - results[i] = task.run() + val threads = + (0 until numThreads).map { i -> + thread { + results[i] = task.run() + } } - } for (t in threads) { t.join() @@ -32,38 +37,49 @@ class BestEffortRunOnceTaskTest { @Test @Ignore("This test is flaky") fun runOnlyExecutesOperationOnce() { - val opMock = mockk> { - every { doWork() } returnsMany listOf("First result", "Second result") - } + val opMock = + mockk> { + every { doWork() } returnsMany listOf("First result", "Second result") + } - val results = runInParallel(3, BestEffortRunOnceTask { - Thread.sleep(20) // artificial delay to force subsequent threads to wait for the first one - opMock.doWork() - }) + val results = + runInParallel( + 3, + BestEffortRunOnceTask { + Thread.sleep(20) // artificial delay to force subsequent threads to wait for the first one + opMock.doWork() + }, + ) verify(exactly = 1) { opMock.doWork() } // All three threads got the same result assertEquals( results.values.toList(), - listOf("First result", "First result", "First result") + listOf("First result", "First result", "First result"), ) } @Test @Ignore("This test is flaky") fun runDoesNotRepeatOperationIfLockTimesOut() { - val results = listOf( - "First result", - "Second result", - "Third result" - ) - val opMock = mockk> { - every { doWork() } returnsMany results - } - val actualResults = runInParallel(3, BestEffortRunOnceTask(10) { - Thread.sleep(20) - opMock.doWork() - }) + val results = + listOf( + "First result", + "Second result", + "Third result", + ) + val opMock = + mockk> { + every { doWork() } returnsMany results + } + val actualResults = + runInParallel( + 3, + BestEffortRunOnceTask(10) { + Thread.sleep(20) + opMock.doWork() + }, + ) verify(exactly = 1) { opMock.doWork() } assertEquals(results[0], actualResults[0]) // first thread should always get first result diff --git a/webflows/src/test/java/com/schibsted/account/webflows/util/EitherTest.kt b/webflows/src/test/java/com/schibsted/account/webflows/util/EitherTest.kt index 0a8c3885..e96c7ffc 100644 --- a/webflows/src/test/java/com/schibsted/account/webflows/util/EitherTest.kt +++ b/webflows/src/test/java/com/schibsted/account/webflows/util/EitherTest.kt @@ -1,7 +1,10 @@ package com.schibsted.account.webflows.util -import com.schibsted.account.webflows.util.Either.* -import org.junit.Assert.* +import com.schibsted.account.webflows.util.Either.Left +import com.schibsted.account.webflows.util.Either.Right +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test class EitherTest { @@ -45,7 +48,7 @@ class EitherTest { fun onFailureShouldNotApplyToRightValue() { var called = false val value = Right(1) - assertEquals(value, value.onFailure { called = true }) + assertEquals(value, value.onFailure { called = true }) assertFalse(called) } } diff --git a/webflows/src/test/java/com/schibsted/account/webflows/util/TestRetrofitApi.kt b/webflows/src/test/java/com/schibsted/account/webflows/util/TestRetrofitApi.kt index 9241a90f..a3206e73 100644 --- a/webflows/src/test/java/com/schibsted/account/webflows/util/TestRetrofitApi.kt +++ b/webflows/src/test/java/com/schibsted/account/webflows/util/TestRetrofitApi.kt @@ -9,5 +9,5 @@ interface TestRetrofitApi { } data class TestUser( - val username: String? -) \ No newline at end of file + val username: String?, +)