From d2af7972769b968aef32ac39a991eeade133a01b Mon Sep 17 00:00:00 2001 From: lalwani Date: Mon, 1 Jul 2024 17:16:10 -0700 Subject: [PATCH] Add code challenge param only with pkce authType --- .../uber/sdk2/auth/internal/AuthProvider.kt | 101 ++++++++++-------- .../sdk2/auth/internal/AuthProviderTest.kt | 44 +++++++- .../com/uber/sdk2/core/config/UriConfig.kt | 1 - .../com/uber/sdk2/core/UriConfigTest.kt | 3 - 4 files changed, 102 insertions(+), 47 deletions(-) diff --git a/authentication/src/main/kotlin/com/uber/sdk2/auth/internal/AuthProvider.kt b/authentication/src/main/kotlin/com/uber/sdk2/auth/internal/AuthProvider.kt index cc7b02bf..fa750990 100644 --- a/authentication/src/main/kotlin/com/uber/sdk2/auth/internal/AuthProvider.kt +++ b/authentication/src/main/kotlin/com/uber/sdk2/auth/internal/AuthProvider.kt @@ -25,10 +25,14 @@ import com.uber.sdk2.auth.internal.sso.UniversalSsoLink.Companion.RESPONSE_TYPE import com.uber.sdk2.auth.internal.utils.Base64Util import com.uber.sdk2.auth.request.AuthContext import com.uber.sdk2.auth.request.AuthType +import com.uber.sdk2.auth.request.SsoConfig import com.uber.sdk2.auth.request.SsoConfigProvider import com.uber.sdk2.auth.response.AuthResult import com.uber.sdk2.auth.response.PARResponse import com.uber.sdk2.auth.response.UberToken +import com.uber.sdk2.core.config.UriConfig +import com.uber.sdk2.core.config.UriConfig.CODE_CHALLENGE_PARAM +import com.uber.sdk2.core.config.UriConfig.REQUEST_URI import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -38,63 +42,76 @@ class AuthProvider( private val authService: AuthService = AuthService.create(), private val codeVerifierGenerator: PKCEGenerator = PKCEGeneratorImpl, ) : AuthProviding { - private val verifier: String = codeVerifierGenerator.generateCodeVerifier() private val ssoLink = SsoLinkFactory.generateSsoLink(activity, authContext) override suspend fun authenticate(): AuthResult { val ssoConfig = withContext(Dispatchers.IO) { SsoConfigProvider.getSsoConfig(activity) } return try { - val parResponse = - authContext.prefillInfo?.let { - val response = - authService.loginParRequest( - ssoConfig.clientId, - RESPONSE_TYPE, - Base64Util.encodePrefillInfoToString(it), - ssoConfig.scope ?: "profile", - ) - val body = response.body() - body?.takeIf { response.isSuccessful } - ?: throw AuthException.ServerError("Bad response ${response.code()}") - } ?: PARResponse("", "") - - val queryParams = - mapOf( - "request_uri" to parResponse.requestUri, - "code_challenge" to codeVerifierGenerator.generateCodeChallenge(verifier), - ) - + val parResponse = sendPushedAuthorizationRequest(ssoConfig) + val queryParams = getQueryParams(parResponse) val authCode = ssoLink.execute(queryParams) when (authContext.authType) { AuthType.AuthCode -> AuthResult.Success(UberToken(authCode = authCode)) - is AuthType.PKCE -> { - val tokenResponse = - authService.token( - ssoConfig.clientId, - verifier, - authContext.authType.grantType, - ssoConfig.redirectUri, - authCode, - ) - - if (tokenResponse.isSuccessful) { - tokenResponse.body()?.let { AuthResult.Success(it) } - ?: AuthResult.Error( - AuthException.ClientError("Token request failed with empty response") - ) - } else { - AuthResult.Error( - AuthException.ClientError("Token request failed with code: ${tokenResponse.code()}") - ) - } - } + is AuthType.PKCE -> performPkce(ssoConfig, authContext.authType, authCode) } } catch (e: AuthException) { AuthResult.Error(e) } } + private suspend fun performPkce( + ssoConfig: SsoConfig, + authType: AuthType.PKCE, + authCode: String, + ): AuthResult { + val tokenResponse = + authService.token( + ssoConfig.clientId, + verifier, + authType.grantType, + ssoConfig.redirectUri, + authCode, + ) + + return if (tokenResponse.isSuccessful) { + tokenResponse.body()?.let { AuthResult.Success(it) } + ?: AuthResult.Error(AuthException.ClientError("Token request failed with empty response")) + } else { + AuthResult.Error( + AuthException.ClientError("Token request failed with code: ${tokenResponse.code()}") + ) + } + } + + private suspend fun sendPushedAuthorizationRequest(ssoConfig: SsoConfig) = + authContext.prefillInfo?.let { + val response = + authService.loginParRequest( + ssoConfig.clientId, + RESPONSE_TYPE, + Base64Util.encodePrefillInfoToString(it), + ssoConfig.scope ?: "profile", + ) + val body = response.body() + body?.takeIf { response.isSuccessful } + ?: throw AuthException.ServerError("Bad response ${response.code()}") + } ?: PARResponse("", "") + + private fun getQueryParams(parResponse: PARResponse) = buildMap { + when (authContext.authType) { + AuthType.AuthCode -> { + parResponse.requestUri.takeIf { it.isNotEmpty() }?.let { put(REQUEST_URI, it) } + } + is AuthType.PKCE -> { + val codeChallenge = codeVerifierGenerator.generateCodeChallenge(verifier) + parResponse.requestUri.takeIf { it.isNotEmpty() }?.let { put(REQUEST_URI, it) } + put(CODE_CHALLENGE_PARAM, codeChallenge) + put(UriConfig.CODE_CHALLENGE_METHOD, UriConfig.CODE_CHALLENGE_METHOD_VAL) + } + } + } + override fun handleAuthCode(authCode: String) { ssoLink.handleAuthCode(authCode) } diff --git a/authentication/src/test/kotlin/com/uber/sdk2/auth/internal/AuthProviderTest.kt b/authentication/src/test/kotlin/com/uber/sdk2/auth/internal/AuthProviderTest.kt index 5d4db66e..fffb3843 100644 --- a/authentication/src/test/kotlin/com/uber/sdk2/auth/internal/AuthProviderTest.kt +++ b/authentication/src/test/kotlin/com/uber/sdk2/auth/internal/AuthProviderTest.kt @@ -33,12 +33,18 @@ import com.uber.sdk2.auth.response.AuthResult import com.uber.sdk2.auth.response.PARResponse import com.uber.sdk2.auth.response.UberToken import com.uber.sdk2.auth.sso.SsoLink +import com.uber.sdk2.core.config.UriConfig.CODE_CHALLENGE_METHOD +import com.uber.sdk2.core.config.UriConfig.CODE_CHALLENGE_METHOD_VAL +import com.uber.sdk2.core.config.UriConfig.CODE_CHALLENGE_PARAM +import com.uber.sdk2.core.config.UriConfig.REQUEST_URI import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.mock import org.mockito.kotlin.never +import org.mockito.kotlin.reset import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.annotation.Config @@ -56,6 +62,7 @@ class AuthProviderTest : RobolectricTestBase() { @Before fun setUp() { ssoLink = Shadow.extract(SsoLinkFactory).ssoLink + reset(ssoLink) } @Test @@ -71,6 +78,7 @@ class AuthProviderTest : RobolectricTestBase() { AuthContext(AuthDestination.CrossAppSso(listOf(CrossApp.Rider)), AuthType.PKCE(), null) val authProvider = AuthProvider(activity, authContext, authService, codeVerifierGenerator) val result = authProvider.authenticate() + verify(ssoLink).execute(any()) verify(authService, never()).loginParRequest(any(), any(), any(), any()) verify(authService).token("clientId", "verifier", "authorization_code", "redirectUri", "code") assert(result is AuthResult.Success) @@ -90,6 +98,7 @@ class AuthProviderTest : RobolectricTestBase() { val authContext = AuthContext(AuthDestination.CrossAppSso(listOf(CrossApp.Rider)), AuthType.PKCE(), prefillInfo) val authProvider = AuthProvider(activity, authContext, authService, codeVerifierGenerator) + val argumentCaptor = argumentCaptor>() val result = authProvider.authenticate() verify(authService) .loginParRequest( @@ -100,6 +109,11 @@ class AuthProviderTest : RobolectricTestBase() { ) verify(authService) .token("clientId", "verifier", "authorization_code", "redirectUri", "authCode") + verify(ssoLink).execute(argumentCaptor.capture()) + assert(argumentCaptor.firstValue[REQUEST_URI] == "requestUri") + assert(argumentCaptor.firstValue[CODE_CHALLENGE_PARAM] == "challenge") + assert(argumentCaptor.firstValue[CODE_CHALLENGE_METHOD] == CODE_CHALLENGE_METHOD_VAL) + assert(argumentCaptor.firstValue.size == 3) assert(result is AuthResult.Success) assert((result as AuthResult.Success).uberToken.accessToken == "accessToken") } @@ -109,15 +123,42 @@ class AuthProviderTest : RobolectricTestBase() { whenever(ssoLink.execute(any())).thenReturn("authCode") whenever(authService.loginParRequest(any(), any(), any(), any())) .thenReturn(Response.success(PARResponse("requestUri", "codeVerifier"))) + val prefillInfo = PrefillInfo("email", "firstName", "lastName", "phoneNumber") val authContext = - AuthContext(AuthDestination.CrossAppSso(listOf(CrossApp.Rider)), AuthType.AuthCode, null) + AuthContext( + AuthDestination.CrossAppSso(listOf(CrossApp.Rider)), + AuthType.AuthCode, + prefillInfo, + ) val authProvider = AuthProvider(activity, authContext, authService, codeVerifierGenerator) + val argumentCaptor = argumentCaptor>() val result = authProvider.authenticate() verify(authService, never()).token(any(), any(), any(), any(), any()) + verify(ssoLink).execute(argumentCaptor.capture()) + assert(argumentCaptor.lastValue[REQUEST_URI] == "requestUri") + assert(argumentCaptor.lastValue.size == 1) assert(result is AuthResult.Success) assert((result as AuthResult.Success).uberToken.authCode == "authCode") } + @Test + fun `test authenticate when AuthCode flow and prefillInfo is Null should return only AuthCode`() = + runTest { + whenever(ssoLink.execute(any())).thenReturn("authCode") + whenever(authService.loginParRequest(any(), any(), any(), any())) + .thenReturn(Response.success(PARResponse("requestUri", "codeVerifier"))) + val authContext = + AuthContext(AuthDestination.CrossAppSso(listOf(CrossApp.Rider)), AuthType.AuthCode, null) + val authProvider = AuthProvider(activity, authContext, authService, codeVerifierGenerator) + val argumentCaptor = argumentCaptor>() + val result = authProvider.authenticate() + verify(authService, never()).token(any(), any(), any(), any(), any()) + verify(ssoLink).execute(argumentCaptor.capture()) + assert(argumentCaptor.lastValue.isEmpty()) + assert(result is AuthResult.Success) + assert((result as AuthResult.Success).uberToken.authCode == "authCode") + } + @Test fun `test authenticate when authException should return error result`() = runTest { whenever(ssoLink.execute(any())).thenThrow(AuthException.ClientError("error")) @@ -125,6 +166,7 @@ class AuthProviderTest : RobolectricTestBase() { AuthContext(AuthDestination.CrossAppSso(listOf(CrossApp.Rider)), AuthType.AuthCode, null) val authProvider = AuthProvider(activity, authContext, authService, codeVerifierGenerator) val result = authProvider.authenticate() + verify(ssoLink).execute(any()) assert(result is AuthResult.Error && result.authException.message == "error") } } diff --git a/core/src/main/kotlin/com/uber/sdk2/core/config/UriConfig.kt b/core/src/main/kotlin/com/uber/sdk2/core/config/UriConfig.kt index 1f8af838..3a397ebf 100644 --- a/core/src/main/kotlin/com/uber/sdk2/core/config/UriConfig.kt +++ b/core/src/main/kotlin/com/uber/sdk2/core/config/UriConfig.kt @@ -65,7 +65,6 @@ object UriConfig { .appendQueryParameter(SCOPE_PARAM, scopes) .appendQueryParameter(SDK_VERSION_PARAM, BuildConfig.VERSION_NAME) .appendQueryParameter(PLATFORM_PARAM, "android") - .appendQueryParameter(CODE_CHALLENGE_METHOD, CODE_CHALLENGE_METHOD_VAL) return builder.build() } diff --git a/core/src/test/kotlin/com/uber/sdk2/core/UriConfigTest.kt b/core/src/test/kotlin/com/uber/sdk2/core/UriConfigTest.kt index 52b79ea5..99ae588b 100644 --- a/core/src/test/kotlin/com/uber/sdk2/core/UriConfigTest.kt +++ b/core/src/test/kotlin/com/uber/sdk2/core/UriConfigTest.kt @@ -17,8 +17,6 @@ package com.uber.sdk2.core import com.uber.sdk2.core.config.UriConfig import com.uber.sdk2.core.config.UriConfig.CLIENT_ID_PARAM -import com.uber.sdk2.core.config.UriConfig.CODE_CHALLENGE_METHOD -import com.uber.sdk2.core.config.UriConfig.CODE_CHALLENGE_METHOD_VAL import com.uber.sdk2.core.config.UriConfig.PLATFORM_PARAM import com.uber.sdk2.core.config.UriConfig.REDIRECT_PARAM import com.uber.sdk2.core.config.UriConfig.RESPONSE_TYPE_PARAM @@ -47,7 +45,6 @@ class UriConfigTest : RobolectricTestBase() { assertEquals(scopes, uri.getQueryParameter(SCOPE_PARAM)) assertEquals(BuildConfig.VERSION_NAME, uri.getQueryParameter(SDK_VERSION_PARAM)) assertEquals("android", uri.getQueryParameter(PLATFORM_PARAM)) - assertEquals(CODE_CHALLENGE_METHOD_VAL, uri.getQueryParameter(CODE_CHALLENGE_METHOD)) } @Test