diff --git a/authentication/src/main/kotlin/com/uber/sdk2/auth/internal/utils/NonceUtil.kt b/authentication/src/main/kotlin/com/uber/sdk2/auth/internal/utils/NonceUtil.kt new file mode 100644 index 0000000..338cf58 --- /dev/null +++ b/authentication/src/main/kotlin/com/uber/sdk2/auth/internal/utils/NonceUtil.kt @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2024 Uber Technologies, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.uber.sdk2.auth.internal.utils + +import android.util.Base64 +import org.json.JSONObject + +object NonceUtil { + + /** + * Extracts the `nonce` claim from a JWT id_token by decoding the payload segment (without + * verifying the signature). Returns null if the token is malformed or contains no nonce claim. + */ + @JvmStatic + fun extractNonceFromIdToken(idToken: String): String? { + val parts = idToken.split(".") + if (parts.size != 3) return null + return try { + val payload = Base64.decode(parts[1], Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING) + val json = JSONObject(String(payload, Charsets.UTF_8)) + json.optString("nonce", null) + } catch (_: Exception) { + null + } + } +} diff --git a/authentication/src/main/kotlin/com/uber/sdk2/auth/response/UberToken.kt b/authentication/src/main/kotlin/com/uber/sdk2/auth/response/UberToken.kt index c3a52e8..8d9b176 100644 --- a/authentication/src/main/kotlin/com/uber/sdk2/auth/response/UberToken.kt +++ b/authentication/src/main/kotlin/com/uber/sdk2/auth/response/UberToken.kt @@ -33,4 +33,5 @@ data class UberToken( @Json(name = "refresh_token") val refreshToken: String? = null, @Json(name = "expires_in") val expiresIn: Long? = null, @Json(name = "scope") val scope: String? = null, + @Json(name = "id_token") val idToken: String? = null, ) : Parcelable diff --git a/authentication/src/test/kotlin/com/uber/sdk2/auth/internal/utils/NonceUtilTest.kt b/authentication/src/test/kotlin/com/uber/sdk2/auth/internal/utils/NonceUtilTest.kt new file mode 100644 index 0000000..e163a7a --- /dev/null +++ b/authentication/src/test/kotlin/com/uber/sdk2/auth/internal/utils/NonceUtilTest.kt @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2024 Uber Technologies, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.uber.sdk2.auth.internal.utils + +import com.uber.sdk2.auth.RobolectricTestBase +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class NonceUtilTest : RobolectricTestBase() { + + 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" + } + + @Test + fun `extractNonceFromIdToken returns nonce when present`() { + val jwt = buildJwt("""{"sub":"user123","nonce":"my-nonce-value"}""") + assertEquals("my-nonce-value", NonceUtil.extractNonceFromIdToken(jwt)) + } + + @Test + fun `extractNonceFromIdToken returns null when nonce is absent`() { + val jwt = buildJwt("""{"sub":"user123"}""") + assertNull(NonceUtil.extractNonceFromIdToken(jwt)) + } + + @Test + fun `extractNonceFromIdToken returns null for malformed JWT`() { + assertNull(NonceUtil.extractNonceFromIdToken("not-a-jwt")) + } + + @Test + fun `extractNonceFromIdToken returns null for two-segment token`() { + assertNull(NonceUtil.extractNonceFromIdToken("header.payload")) + } + + @Test + fun `extractNonceFromIdToken returns null for invalid base64 payload`() { + assertNull(NonceUtil.extractNonceFromIdToken("header.!!!invalid!!!.sig")) + } +}