diff --git a/guardian/src/main/java/com/auth0/android/guardian/sdk/ClientInfo.java b/guardian/src/main/java/com/auth0/android/guardian/sdk/ClientInfo.java new file mode 100644 index 0000000..5c16a4d --- /dev/null +++ b/guardian/src/main/java/com/auth0/android/guardian/sdk/ClientInfo.java @@ -0,0 +1,46 @@ +package com.auth0.android.guardian.sdk; + +import android.util.Base64; + +import androidx.annotation.Nullable; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.SerializedName; + +public class ClientInfo { + final String name = "Guardian.Android"; + final String version = BuildConfig.VERSION_NAME; + + @SerializedName("env") + @Nullable + TelemetryInfo telemetryInfo; + + public ClientInfo (TelemetryInfo telemetryInfo) { + this.telemetryInfo = telemetryInfo; + } + + public ClientInfo () { + this.telemetryInfo = null; + } + + String toJson() { + Gson gson = new GsonBuilder().create(); + return gson.toJson(this); + } + + String toBase64() { + byte[] bytes = this.toJson().getBytes(); + return Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); + } + + public static class TelemetryInfo { + String appName; + String appVersion; + + public TelemetryInfo(String appName, String appVersion) { + this.appName = appName; + this.appVersion = appVersion; + } + } +} diff --git a/guardian/src/main/java/com/auth0/android/guardian/sdk/DeviceAPIClient.java b/guardian/src/main/java/com/auth0/android/guardian/sdk/DeviceAPIClient.java index e1a5f6e..5795a64 100644 --- a/guardian/src/main/java/com/auth0/android/guardian/sdk/DeviceAPIClient.java +++ b/guardian/src/main/java/com/auth0/android/guardian/sdk/DeviceAPIClient.java @@ -43,13 +43,20 @@ public class DeviceAPIClient { private final HttpUrl url; private final String token; + private final ClientInfo clientInfo; + DeviceAPIClient(RequestFactory requestFactory, HttpUrl baseUrl, String id, String token) { + this(requestFactory, baseUrl, id, token, null); + } + + DeviceAPIClient(RequestFactory requestFactory, HttpUrl baseUrl, String id, String token, @Nullable ClientInfo.TelemetryInfo telemetryInfo) { this.requestFactory = requestFactory; this.url = baseUrl.newBuilder() .addPathSegments("api/device-accounts") .addPathSegment(id) .build(); this.token = token; + this.clientInfo = new ClientInfo(telemetryInfo); } /** @@ -60,6 +67,7 @@ public class DeviceAPIClient { public GuardianAPIRequest delete() { return requestFactory .newRequest("DELETE", url, Void.class) + .setHeader("Auth0-Client", this.clientInfo.toBase64()) .setBearer(token); } @@ -80,6 +88,7 @@ public GuardianAPIRequest> update(@Nullable String identifie @Nullable String gcmToken) { Type type = new TypeToken>() {}.getType(); return requestFactory.>newRequest("PATCH", url, type) + .setHeader("Auth0-Client", this.clientInfo.toBase64()) .setBearer(token) .setParameter("identifier", identifier) .setParameter("name", name) diff --git a/guardian/src/main/java/com/auth0/android/guardian/sdk/Guardian.java b/guardian/src/main/java/com/auth0/android/guardian/sdk/Guardian.java index d3eb8de..9d8e8e0 100644 --- a/guardian/src/main/java/com/auth0/android/guardian/sdk/Guardian.java +++ b/guardian/src/main/java/com/auth0/android/guardian/sdk/Guardian.java @@ -24,6 +24,7 @@ import android.net.Uri; import android.os.Bundle; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -253,6 +254,13 @@ public Builder enableLogging() { return this; } + private ClientInfo.TelemetryInfo telemetryInfo; + + public Builder setTelemetryInfo(String appName, String appVersion) { + this.telemetryInfo = new ClientInfo.TelemetryInfo(appName, appVersion); + return this; + } + /** * Builds and returns the Guardian instance * @@ -271,6 +279,10 @@ public Guardian build() { builder.enableLogging(); } + if (telemetryInfo != null) { + builder.setTelemetryInfo(telemetryInfo.appName, telemetryInfo.appVersion); + } + return new Guardian(builder.build()); } } diff --git a/guardian/src/main/java/com/auth0/android/guardian/sdk/GuardianAPIClient.java b/guardian/src/main/java/com/auth0/android/guardian/sdk/GuardianAPIClient.java index 8aa1a9a..922702d 100644 --- a/guardian/src/main/java/com/auth0/android/guardian/sdk/GuardianAPIClient.java +++ b/guardian/src/main/java/com/auth0/android/guardian/sdk/GuardianAPIClient.java @@ -67,9 +67,19 @@ public class GuardianAPIClient { private final RequestFactory requestFactory; private final HttpUrl baseUrl; + private final ClientInfo clientInfo; + + GuardianAPIClient(RequestFactory requestFactory, HttpUrl baseUrl) { this.requestFactory = requestFactory; this.baseUrl = baseUrl; + this.clientInfo = new ClientInfo(null); + } + + GuardianAPIClient(RequestFactory requestFactory, HttpUrl baseUrl, ClientInfo.TelemetryInfo telemetryInfo) { + this.requestFactory = requestFactory; + this.baseUrl = baseUrl; + this.clientInfo = new ClientInfo(telemetryInfo); } String getUrl() { @@ -106,6 +116,7 @@ public GuardianAPIRequest> enroll(@NonNull String enrollment return requestFactory .>newRequest("POST", url, type) .setHeader("Authorization", String.format("Ticket id=\"%s\"", enrollmentTicket)) + .setHeader("Auth0-Client", this.clientInfo.toBase64()) .setParameter("identifier", deviceIdentifier) .setParameter("name", deviceName) .setParameter("push_credentials", createPushCredentials(gcmToken)) @@ -162,6 +173,7 @@ public GuardianAPIRequest allow(@NonNull String txToken, final String jwt = createAccessApprovalJWT(privateKey, url.toString(), deviceIdentifier, challenge, true, null); return requestFactory .newRequest("POST", url, Void.class) + .setHeader("Auth0-Client", this.clientInfo.toBase64()) .setBearer(txToken) .setParameter("challenge_response", jwt); } @@ -189,6 +201,7 @@ public GuardianAPIRequest reject(@NonNull String txToken, final String jwt = createAccessApprovalJWT(privateKey, url.toString(), deviceIdentifier, challenge, false, reason); return requestFactory .newRequest("POST", url, Void.class) + .setHeader("Auth0-Client", this.clientInfo.toBase64()) .setBearer(txToken) .setParameter("challenge_response", jwt); } @@ -351,6 +364,13 @@ public Builder enableLogging() { return this; } + ClientInfo clientInfo = new ClientInfo(); + + public Builder setTelemetryInfo(String appName, String appVersion) { + this.clientInfo.telemetryInfo = new ClientInfo.TelemetryInfo(appName, appVersion); + return this; + } + /** * Builds and returns the GuardianAPIClient instance * @@ -364,10 +384,7 @@ public GuardianAPIClient build() { final OkHttpClient.Builder builder = new OkHttpClient.Builder(); - final String clientInfo = Base64.encodeToString( - String.format("{\"name\":\"Guardian.Android\",\"version\":\"%s\"}", - BuildConfig.VERSION_NAME).getBytes(), - Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); + final String encodedClientInfo = this.clientInfo.toBase64(); builder.addInterceptor(new Interceptor() { @Override @@ -380,7 +397,7 @@ public Response intercept(Chain chain) throws IOException { String.format("GuardianSDK/%s Android %s", BuildConfig.VERSION_NAME, Build.VERSION.RELEASE)) - .header("Auth0-Client", clientInfo) + .header("Auth0-Client", encodedClientInfo) .build(); return chain.proceed(requestWithUserAgent); } @@ -398,6 +415,10 @@ public Response intercept(Chain chain) throws IOException { RequestFactory requestFactory = new RequestFactory(gson, client); + if(clientInfo.telemetryInfo != null) { + return new GuardianAPIClient(requestFactory, url, clientInfo.telemetryInfo); + } + return new GuardianAPIClient(requestFactory, url); } } diff --git a/guardian/src/test/java/android/util/Base64.java b/guardian/src/test/java/android/util/Base64.java new file mode 100644 index 0000000..87be80f --- /dev/null +++ b/guardian/src/test/java/android/util/Base64.java @@ -0,0 +1,25 @@ +package android.util; + +import android.os.Build; + +import java.nio.charset.StandardCharsets; + +public class Base64 { + public static final int NO_PADDING = 1; + public static final int NO_WRAP = 2; + public static final int URL_SAFE = 8; + public static String encodeToString(byte[] input, int flags) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return java.util.Base64.getEncoder().encodeToString(input); + } + return "eh?"; + } + + public static byte[] decode(String str, int flags) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return java.util.Base64.getDecoder().decode(str); + } + + return "eh?".getBytes(StandardCharsets.UTF_8); + } +} \ No newline at end of file diff --git a/guardian/src/test/java/com/auth0/android/guardian/sdk/GuardianAPIClientTest.java b/guardian/src/test/java/com/auth0/android/guardian/sdk/GuardianAPIClientTest.java index 29781b7..d5a5f7d 100644 --- a/guardian/src/test/java/com/auth0/android/guardian/sdk/GuardianAPIClientTest.java +++ b/guardian/src/test/java/com/auth0/android/guardian/sdk/GuardianAPIClientTest.java @@ -73,7 +73,6 @@ import static com.auth0.android.guardian.sdk.utils.CallbackMatcher.hasNoError; import static org.hamcrest.Matchers.blankOrNullString; import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.emptyOrNullString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.hasEntry; @@ -82,6 +81,7 @@ import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.Mockito.mock; @@ -234,6 +234,27 @@ public void shouldHaveCustomUserAgentAndLanguageHeader() throws Exception { assertThat(request.getHeader("Accept-Language"), is(equalTo(Locale.getDefault().toString()))); + } + + @Test + public void shouldCorrectlySetClientHeader() throws Exception { + mockAPI.willReturnEnrollment(ENROLLMENT_ID, ENROLLMENT_URL, ENROLLMENT_ISSUER, ENROLLMENT_USER, + DEVICE_ACCOUNT_TOKEN, RECOVERY_CODE, TOTP_SECRET, TOTP_ALGORITHM, TOTP_DIGITS, TOTP_PERIOD); + + final MockCallback> callback = new MockCallback<>(); + + final String STUB_APP_NAME = "SomeCoolApp"; + final String STUB_APP_VERSION = "1.2.3.4"; + + GuardianAPIClient testApiClient = new GuardianAPIClient.Builder() + .url(Uri.parse(this.mockAPI.getDomain())) + .setTelemetryInfo(STUB_APP_NAME, STUB_APP_VERSION) + .build(); + + testApiClient.enroll(ENROLLMENT_TICKET, DEVICE_IDENTIFIER, DEVICE_NAME, GCM_TOKEN, publicKey) + .start(callback); + + RecordedRequest request = mockAPI.takeRequest(); String auth0ClientEncoded = request.getHeader("Auth0-Client"); assertThat(auth0ClientEncoded, is(notNullValue())); @@ -241,15 +262,49 @@ public void shouldHaveCustomUserAgentAndLanguageHeader() throws Exception { byte[] auth0ClientDecoded = Base64.decode( auth0ClientEncoded, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP); - Type type = new TypeToken>() { - }.getType(); Gson gson = new GsonBuilder().create(); - Map auth0Client = gson.fromJson( + + ClientInfo auth0Client = gson.fromJson( new InputStreamReader( - new ByteArrayInputStream(auth0ClientDecoded)), type); + new ByteArrayInputStream(auth0ClientDecoded)), ClientInfo.class); + + assertThat(auth0Client, is(notNullValue())); + + assertThat(auth0Client.telemetryInfo, is(notNullValue())); + assertThat(auth0Client.telemetryInfo.appName, equalTo(STUB_APP_NAME)); + assertThat(auth0Client.telemetryInfo.appVersion, equalTo(STUB_APP_VERSION)); + } + + @Test + public void shouldNotSetTelemetryInfoIfNoneIsProvided() throws Exception { + mockAPI.willReturnEnrollment(ENROLLMENT_ID, ENROLLMENT_URL, ENROLLMENT_ISSUER, ENROLLMENT_USER, + DEVICE_ACCOUNT_TOKEN, RECOVERY_CODE, TOTP_SECRET, TOTP_ALGORITHM, TOTP_DIGITS, TOTP_PERIOD); + + final MockCallback> callback = new MockCallback<>(); + + GuardianAPIClient testApiClient = new GuardianAPIClient.Builder() + .url(Uri.parse(this.mockAPI.getDomain())) + .build(); + + testApiClient.enroll(ENROLLMENT_TICKET, DEVICE_IDENTIFIER, DEVICE_NAME, GCM_TOKEN, publicKey) + .start(callback); + + RecordedRequest request = mockAPI.takeRequest(); + + String auth0ClientEncoded = request.getHeader("Auth0-Client"); + assertThat(auth0ClientEncoded, is(notNullValue())); + + byte[] auth0ClientDecoded = Base64.decode( + auth0ClientEncoded, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP); + + Gson gson = new GsonBuilder().create(); + + ClientInfo auth0Client = gson.fromJson( + new InputStreamReader( + new ByteArrayInputStream(auth0ClientDecoded)), ClientInfo.class); + assertThat(auth0Client, is(notNullValue())); - assertThat(auth0Client, hasEntry("name", "Guardian.Android")); - assertThat(auth0Client, hasEntry("version", BuildConfig.VERSION_NAME)); + assertThat(auth0Client.telemetryInfo, is(nullValue())); } @Test diff --git a/guardian/src/test/java/com/auth0/android/guardian/sdk/GuardianTest.java b/guardian/src/test/java/com/auth0/android/guardian/sdk/GuardianTest.java index d571e96..8cc90b2 100644 --- a/guardian/src/test/java/com/auth0/android/guardian/sdk/GuardianTest.java +++ b/guardian/src/test/java/com/auth0/android/guardian/sdk/GuardianTest.java @@ -244,6 +244,17 @@ public void shouldBuildWithDomain() throws Exception { is(equalTo("https://example.guardian.auth0.com/"))); } + @Test + public void shouldBuildWithTelemetryInfo() throws Exception { + Guardian guardian = new Guardian.Builder() + .domain("example.guardian.auth0.com") + .setTelemetryInfo("SomeAppName", "1.2.3") + .build(); + + assertThat(guardian.getAPIClient().getUrl(), + is(equalTo("https://example.guardian.auth0.com/"))); + } + @Test public void shouldFailIfDomainWasAlreadySet() throws Exception { exception.expect(IllegalArgumentException.class);