Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,7 @@ sealed class AuthException(override val message: String) : RuntimeException(mess
internal const val UNKNOWN = "Unknown error occurred"

internal const val INVALID_STATE = "State parameter mismatch possible CSRF attack"

internal const val INVALID_NONCE = "Nonce claim in id_token does not match the sent nonce"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import com.uber.sdk2.auth.internal.service.AuthService
import com.uber.sdk2.auth.internal.sso.SsoLinkFactory
import com.uber.sdk2.auth.internal.sso.UniversalSsoLink.Companion.RESPONSE_TYPE
import com.uber.sdk2.auth.internal.utils.Base64Util
import com.uber.sdk2.auth.internal.utils.NonceUtil
import com.uber.sdk2.auth.request.AuthContext
import com.uber.sdk2.auth.request.AuthType
import com.uber.sdk2.auth.request.SsoConfig
Expand Down Expand Up @@ -89,8 +90,13 @@ class AuthProvider(
)

return if (tokenResponse.isSuccessful) {
tokenResponse.body()?.let { AuthResult.Success(it) }
?: AuthResult.Error(AuthException.ClientError("Token request failed with empty response"))
tokenResponse.body()?.let { token ->
val claimNonce = token.idToken?.let { NonceUtil.extractNonceFromIdToken(it) }
if (claimNonce != effectiveNonce) {
return AuthResult.Error(AuthException.ClientError(AuthException.INVALID_NONCE))
}
AuthResult.Success(token)
} ?: AuthResult.Error(AuthException.ClientError("Token request failed with empty response"))
} else {
AuthResult.Error(
AuthException.ClientError("Token request failed with code: ${tokenResponse.code()}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,12 @@ class AuthProviderTest : RobolectricTestBase() {
.thenReturn(Response.success(PARResponse("requestUri", "codeVerifier")))
whenever(codeVerifierGenerator.generateCodeVerifier()).thenReturn("verifier")
whenever(codeVerifierGenerator.generateCodeChallenge("verifier")).thenReturn("challenge")
whenever(authService.token(any(), any(), any(), any(), any()))
.thenReturn(Response.success(UberToken(accessToken = "accessToken")))
val authContext =
AuthContext(AuthDestination.CrossAppSso(listOf(CrossApp.Rider)), AuthType.PKCE(), null)
val authProvider = AuthProvider(activity, authContext, authService, codeVerifierGenerator)
val idToken = buildJwt("""{"sub":"user","nonce":"${authProvider.effectiveNonce}"}""")
whenever(authService.token(any(), any(), any(), any(), any()))
.thenReturn(Response.success(UberToken(accessToken = "accessToken", idToken = idToken)))
val result = authProvider.authenticate()
verify(ssoLink).execute(any())
verify(authService, never()).loginParRequest(any(), any(), any(), any())
Expand All @@ -104,12 +105,13 @@ class AuthProviderTest : RobolectricTestBase() {
.thenReturn(Response.success(PARResponse("requestUri", "codeVerifier")))
whenever(codeVerifierGenerator.generateCodeVerifier()).thenReturn("verifier")
whenever(codeVerifierGenerator.generateCodeChallenge("verifier")).thenReturn("challenge")
whenever(authService.token(any(), any(), any(), any(), any()))
.thenReturn(Response.success(UberToken(accessToken = "accessToken")))
val prefillInfo = PrefillInfo("email", "firstName", "lastName", "phoneNumber")
val authContext =
AuthContext(AuthDestination.CrossAppSso(listOf(CrossApp.Rider)), AuthType.PKCE(), prefillInfo)
val authProvider = AuthProvider(activity, authContext, authService, codeVerifierGenerator)
val idToken = buildJwt("""{"sub":"user","nonce":"${authProvider.effectiveNonce}"}""")
whenever(authService.token(any(), any(), any(), any(), any()))
.thenReturn(Response.success(UberToken(accessToken = "accessToken", idToken = idToken)))
val argumentCaptor = argumentCaptor<Map<String, String>>()
val result = authProvider.authenticate()
verify(authService)
Expand Down Expand Up @@ -371,15 +373,16 @@ class AuthProviderTest : RobolectricTestBase() {
whenever(ssoLink.execute(any())).thenReturn("authCode")
whenever(codeVerifierGenerator.generateCodeVerifier()).thenReturn("verifier")
whenever(codeVerifierGenerator.generateCodeChallenge("verifier")).thenReturn("challenge")
whenever(authService.token(any(), any(), any(), any(), any()))
.thenReturn(Response.success(UberToken(accessToken = "accessToken")))
// Mock PAR request to fail
val errorResponse: Response<PARResponse> = Response.error(500, mock())
whenever(authService.loginParRequest(any(), any(), any(), any())).thenReturn(errorResponse)
val prefillInfo = PrefillInfo("email", "firstName", "lastName", "phoneNumber")
val authContext =
AuthContext(AuthDestination.CrossAppSso(listOf(CrossApp.Rider)), AuthType.PKCE(), prefillInfo)
val authProvider = AuthProvider(activity, authContext, authService, codeVerifierGenerator)
val idToken = buildJwt("""{"sub":"user","nonce":"${authProvider.effectiveNonce}"}""")
whenever(authService.token(any(), any(), any(), any(), any()))
.thenReturn(Response.success(UberToken(accessToken = "accessToken", idToken = idToken)))
val argumentCaptor = argumentCaptor<Map<String, String>>()
val result = authProvider.authenticate()
// Verify PAR request was attempted
Expand Down Expand Up @@ -515,4 +518,115 @@ class AuthProviderTest : RobolectricTestBase() {
assertEquals(AuthOptionalConfig(), authContext.options)
assertEquals(UriConfig.UberEnvironment.PRODUCTION, authContext.environment)
}

// ---- Nonce validation tests ----

@Test
fun `test PKCE with nonce validates id_token nonce claim`() = runTest {
val idToken = buildJwt("""{"sub":"user","nonce":"my-nonce"}""")
whenever(ssoLink.execute(any())).thenReturn("code")
whenever(codeVerifierGenerator.generateCodeVerifier()).thenReturn("verifier")
whenever(codeVerifierGenerator.generateCodeChallenge("verifier")).thenReturn("challenge")
whenever(authService.token(any(), any(), any(), any(), any()))
.thenReturn(Response.success(UberToken(accessToken = "accessToken", idToken = idToken)))
val authContext =
AuthContext(
AuthDestination.CrossAppSso(listOf(CrossApp.Rider)),
AuthType.PKCE(),
nonce = "my-nonce",
)
val authProvider = AuthProvider(activity, authContext, authService, codeVerifierGenerator)
val result = authProvider.authenticate()
assert(result is AuthResult.Success)
assert((result as AuthResult.Success).uberToken.accessToken == "accessToken")
}

@Test
fun `test PKCE with nonce mismatch returns error`() = runTest {
val idToken = buildJwt("""{"sub":"user","nonce":"wrong-nonce"}""")
whenever(ssoLink.execute(any())).thenReturn("code")
whenever(codeVerifierGenerator.generateCodeVerifier()).thenReturn("verifier")
whenever(codeVerifierGenerator.generateCodeChallenge("verifier")).thenReturn("challenge")
whenever(authService.token(any(), any(), any(), any(), any()))
.thenReturn(Response.success(UberToken(accessToken = "accessToken", idToken = idToken)))
val authContext =
AuthContext(
AuthDestination.CrossAppSso(listOf(CrossApp.Rider)),
AuthType.PKCE(),
nonce = "expected-nonce",
)
val authProvider = AuthProvider(activity, authContext, authService, codeVerifierGenerator)
val result = authProvider.authenticate()
assert(result is AuthResult.Error)
assertEquals(AuthException.INVALID_NONCE, (result as AuthResult.Error).authException.message)
}

@Test
fun `test PKCE with nonce but missing id_token returns error`() = runTest {
whenever(ssoLink.execute(any())).thenReturn("code")
whenever(codeVerifierGenerator.generateCodeVerifier()).thenReturn("verifier")
whenever(codeVerifierGenerator.generateCodeChallenge("verifier")).thenReturn("challenge")
whenever(authService.token(any(), any(), any(), any(), any()))
.thenReturn(Response.success(UberToken(accessToken = "accessToken")))
val authContext =
AuthContext(
AuthDestination.CrossAppSso(listOf(CrossApp.Rider)),
AuthType.PKCE(),
nonce = "my-nonce",
)
val authProvider = AuthProvider(activity, authContext, authService, codeVerifierGenerator)
val result = authProvider.authenticate()
assert(result is AuthResult.Error)
assertEquals(AuthException.INVALID_NONCE, (result as AuthResult.Error).authException.message)
}

@Test
fun `test PKCE always validates id_token nonce even when caller provides no nonce`() = runTest {
whenever(ssoLink.execute(any())).thenReturn("code")
whenever(codeVerifierGenerator.generateCodeVerifier()).thenReturn("verifier")
whenever(codeVerifierGenerator.generateCodeChallenge("verifier")).thenReturn("challenge")
whenever(authService.token(any(), any(), any(), any(), any()))
.thenReturn(Response.success(UberToken(accessToken = "accessToken")))
val authContext =
AuthContext(AuthDestination.CrossAppSso(listOf(CrossApp.Rider)), AuthType.PKCE(), null)
val authProvider = AuthProvider(activity, authContext, authService, codeVerifierGenerator)
val result = authProvider.authenticate()
// Auto-generated nonce always set; missing id_token cannot satisfy validation
assert(result is AuthResult.Error)
assertEquals(AuthException.INVALID_NONCE, (result as AuthResult.Error).authException.message)
}

@Test
fun `test PKCE with auto-generated nonce succeeds when id_token nonce matches`() = runTest {
whenever(ssoLink.execute(any())).thenReturn("code")
whenever(codeVerifierGenerator.generateCodeVerifier()).thenReturn("verifier")
whenever(codeVerifierGenerator.generateCodeChallenge("verifier")).thenReturn("challenge")
val authContext =
AuthContext(AuthDestination.CrossAppSso(listOf(CrossApp.Rider)), AuthType.PKCE(), null)
val authProvider = AuthProvider(activity, authContext, authService, codeVerifierGenerator)
val idToken = buildJwt("""{"sub":"user","nonce":"${authProvider.effectiveNonce}"}""")
whenever(authService.token(any(), any(), any(), any(), any()))
.thenReturn(Response.success(UberToken(accessToken = "accessToken", idToken = idToken)))
val result = authProvider.authenticate()
assert(result is AuthResult.Success)
assert((result as AuthResult.Success).uberToken.accessToken == "accessToken")
}

private fun buildJwt(payloadJson: String): String {
val header =
android.util.Base64.encodeToString(
"""{"alg":"RS256","typ":"JWT"}""".toByteArray(),
android.util.Base64.URL_SAFE or
android.util.Base64.NO_WRAP or
android.util.Base64.NO_PADDING,
)
val payload =
android.util.Base64.encodeToString(
payloadJson.toByteArray(),
android.util.Base64.URL_SAFE or
android.util.Base64.NO_WRAP or
android.util.Base64.NO_PADDING,
)
return "$header.$payload.fake-signature"
}
}
Loading