Skip to content

Commit

Permalink
feat: Add SIM Swap API (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
SMadani authored Jul 29, 2024
1 parent 64f96d0 commit bc703e9
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 9 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [0.6.0] - 2024-07-31

### Added
- SIM Swap API

## [0.5.0] - 2024-07-25

### Added
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Contributors

This is the vonage-java-sdk hall of fame! All contributors of source code, or
This is the vonage-kotlin-sdk hall of fame! All contributors of source code, or
documentation, or tests are eligible to be added to this list.

- Sina Madani ([@SMadani](https://github.com/SMadani))
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ You'll need to have [created a Vonage account](https://dashboard.nexmo.com/sign-
- [Messages](https://developer.vonage.com/en/messages/overview)
- [Verify](https://developer.vonage.com/en/verify/overview)
- [Voice](https://developer.vonage.com/en/voice/voice-api/overview)
- [SIM Swap](https://developer.vonage.com/en/sim-swap/overview)
- [Number Insight](https://developer.vonage.com/en/number-insight/overview)
- [SMS](https://developer.vonage.com/en/messaging/sms/overview)
- [Conversion](https://developer.vonage.com/en/messaging/conversion-api/overview)
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>com.vonage</groupId>
<artifactId>server-sdk-kotlin</artifactId>
<version>0.5.0</version>
<version>0.6.0</version>

<name>Vonage Kotlin Server SDK</name>
<description>Kotlin client for Vonage APIs</description>
Expand Down
27 changes: 27 additions & 0 deletions src/main/kotlin/com/vonage/client/kt/SimSwap.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2024 Vonage
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.vonage.client.kt

import com.vonage.client.camara.simswap.*

class SimSwap(private val simSwapClient: SimSwapClient) {

fun checkSimSwap(phoneNumber: String, maxAgeHours: Int = 240): Boolean =
simSwapClient.checkSimSwap(phoneNumber, maxAgeHours)

fun retrieveSimSwapDate(phoneNumber: String) =
simSwapClient.retrieveSimSwapDate(phoneNumber)
}
1 change: 1 addition & 0 deletions src/main/kotlin/com/vonage/client/kt/Vonage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Vonage(init: VonageClient.Builder.() -> Unit) {
val redact = Redact(vonageClient.redactClient)
val verifyLegacy = VerifyLegacy(vonageClient.verifyClient)
val numberInsight = NumberInsight(vonageClient.insightClient)
val simSwap = SimSwap(vonageClient.simSwapClient)
}

fun VonageClient.Builder.authFromEnv(): VonageClient.Builder {
Expand Down
30 changes: 23 additions & 7 deletions src/test/kotlin/com/vonage/client/kt/AbstractTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,17 @@ import kotlin.test.assertEquals
abstract class AbstractTest {
protected val apiKey = "a1b2c3d4"
protected val applicationId = "00000000-0000-4000-8000-000000000000"
protected val accessToken = "abc123456def"
private val apiSecret = "1234567890abcdef"
private val apiKeySecretEncoded = "YTFiMmMzZDQ6MTIzNDU2Nzg5MGFiY2RlZg=="
private val privateKeyPath = "src/test/resources/com/vonage/client/kt/application_key"
private val signatureSecretName = "sig"
private val apiSecretName = "api_secret"
private val apiKeyName = "api_key"
private val authHeaderName = "Authorization"
private val basicSecretEncodedHeader = "Basic $apiKeySecretEncoded"
private val jwtBearerPattern = "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9(\\..+){2}"
private val accessTokenBearer = "Bearer $accessToken"
protected val testUuidStr = "aaaaaaaa-bbbb-4ccc-8ddd-0123456789ab"
protected val testUuid: UUID = UUID.fromString(testUuidStr)
protected val toNumber = "447712345689"
Expand Down Expand Up @@ -106,7 +111,7 @@ abstract class AbstractTest {
}

protected enum class AuthType {
JWT, API_KEY_SECRET_HEADER, API_KEY_SECRET_QUERY_PARAMS, API_KEY_SIGNATURE_SECRET
JWT, API_KEY_SECRET_HEADER, API_KEY_SECRET_QUERY_PARAMS, API_KEY_SIGNATURE_SECRET, ACCESS_TOKEN
}

private fun HttpMethod.toWireMockMethod(): Method = when (this) {
Expand All @@ -128,11 +133,21 @@ abstract class AbstractTest {
private fun Map<String, Any>.toJson(): String = ObjectMapper().writeValueAsString(this)

protected fun mockPostQueryParams(expectedUrl: String, expectedRequestParams: Map<String, Any>,
authType: AuthType? = AuthType.API_KEY_SECRET_QUERY_PARAMS,
status: Int = 200, expectedResponseParams: Map<String, Any>? = null) {

val stub = post(urlPathEqualTo(expectedUrl))
.withFormParam(apiKeyName, equalTo(apiKey))
.withFormParam(apiSecretName, equalTo(apiSecret))
when (authType) {
AuthType.API_KEY_SECRET_QUERY_PARAMS -> {
stub.withFormParam(apiKeyName, equalTo(apiKey))
.withFormParam(apiSecretName, equalTo(apiSecret))
}
AuthType.JWT -> stub.withHeader(authHeaderName, matching(jwtBearerPattern))
AuthType.ACCESS_TOKEN -> stub.withHeader(authHeaderName, equalTo(accessTokenBearer))
AuthType.API_KEY_SECRET_HEADER -> stub.withHeader(authHeaderName, equalTo(basicSecretEncodedHeader))
AuthType.API_KEY_SIGNATURE_SECRET -> stub.withFormParam(apiKeyName, equalTo(apiKey))
null -> Unit
}

expectedRequestParams.forEach {(k, v) -> stub.withFormParam(k, equalTo(v.toString()))}

Expand Down Expand Up @@ -162,13 +177,14 @@ abstract class AbstractTest {
}

if (authType != null) {
val authHeaderName = "Authorization"
when (authType) {
AuthType.JWT -> headers contains authHeaderName like
"Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9(\\..+){2}"
AuthType.JWT -> headers contains authHeaderName like jwtBearerPattern

AuthType.ACCESS_TOKEN ->
headers contains authHeaderName equalTo accessTokenBearer

AuthType.API_KEY_SECRET_HEADER ->
headers contains authHeaderName equalTo "Basic $apiKeySecretEncoded"
headers contains authHeaderName equalTo basicSecretEncodedHeader

AuthType.API_KEY_SECRET_QUERY_PARAMS -> {
queryParams contains apiKeyName equalTo apiKey
Expand Down
108 changes: 108 additions & 0 deletions src/test/kotlin/com/vonage/client/kt/SimSwapTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright 2024 Vonage
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.vonage.client.kt

import com.vonage.client.auth.camara.FraudPreventionDetectionScope
import kotlin.test.*

class SimSwapTest : AbstractTest() {
private val simSwapClient = vonage.simSwap
private val baseSimSwapUrl = "/camara/sim-swap/v040"
private val checkSimSwapUrl = "$baseSimSwapUrl/check"
private val retrieveSimSwapDateUrl = "$baseSimSwapUrl/retrieve-date"
private val simSwapNumber = toNumber
private val phoneNumberMap = mapOf("phoneNumber" to simSwapNumber)
private val authReqId = "arid/0dadaeb4-7c79-4d39-b4b0-5a6cc08bf537"

private fun mockBackendAuth(scope: FraudPreventionDetectionScope) {
mockPostQueryParams(
expectedUrl = "/oauth2/bc-authorize",
authType = AuthType.JWT,
expectedRequestParams = mapOf(
"login_hint" to "tel:+$simSwapNumber",
"scope" to "openid dpv:FraudPreventionAndDetection#$scope"
),
expectedResponseParams = mapOf(
"auth_req_id" to authReqId,
"expires_in" to 120,
"interval" to 3
)
)
mockPostQueryParams(
expectedUrl = "/oauth2/token",
authType = AuthType.JWT,
expectedRequestParams = mapOf(
"grant_type" to "urn:openid:params:grant-type:ciba",
"auth_req_id" to authReqId
),
expectedResponseParams = mapOf(
"access_token" to accessToken,
"refresh_token" to "xyz789012ghi",
"token_type" to "Bearer",
"expires" to 3600
),
)
}

private fun assertCheckSimSwap(maxAge: Int = 240, invocation: SimSwap.() -> Boolean) {
mockBackendAuth(FraudPreventionDetectionScope.CHECK_SIM_SWAP)
for (result in listOf(true, false, null)) {
mockPost(
expectedUrl = checkSimSwapUrl,
authType = AuthType.ACCESS_TOKEN,
expectedRequestParams = phoneNumberMap + mapOf("maxAge" to maxAge),
expectedResponseParams = if (result != null) mapOf("swapped" to result) else mapOf()
)
assertEquals(result ?: false, invocation.invoke(simSwapClient))
}
}

private fun assertRetrieveSimSwapDate(includeResponse: Boolean) {
mockBackendAuth(FraudPreventionDetectionScope.RETRIEVE_SIM_SWAP_DATE)
mockPost(
expectedUrl = retrieveSimSwapDateUrl,
authType = AuthType.ACCESS_TOKEN,
expectedRequestParams = phoneNumberMap,
expectedResponseParams = if (includeResponse) mapOf("latestSimChange" to timestampStr) else mapOf()
)
assertEquals(if (includeResponse) timestamp else null, simSwapClient.retrieveSimSwapDate(simSwapNumber))
}

@Test
fun `check sim swap number only`() {
assertCheckSimSwap {
checkSimSwap(simSwapNumber)
}
}

@Test
fun `check sim swap with maxAge`() {
val maxAge = 1200
assertCheckSimSwap(maxAge) {
checkSimSwap(simSwapNumber, maxAge)
}
}

@Test
fun `retrieve sim swap date success`() {
assertRetrieveSimSwapDate(true)
}

@Test
fun `retrieve sim swap date unknown`() {
assertRetrieveSimSwapDate(false)
}
}

0 comments on commit bc703e9

Please sign in to comment.