Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MFA-2681] Add advanced telemetry to SDK #113

Merged
merged 4 commits into from
Mar 15, 2024
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
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand All @@ -60,6 +67,7 @@ public class DeviceAPIClient {
public GuardianAPIRequest<Void> delete() {
return requestFactory
.<Void>newRequest("DELETE", url, Void.class)
.setHeader("Auth0-Client", this.clientInfo.toBase64())
.setBearer(token);
}

Expand All @@ -80,6 +88,7 @@ public GuardianAPIRequest<Map<String, Object>> update(@Nullable String identifie
@Nullable String gcmToken) {
Type type = new TypeToken<Map<String, Object>>() {}.getType();
return requestFactory.<Map<String, Object>>newRequest("PATCH", url, type)
.setHeader("Auth0-Client", this.clientInfo.toBase64())
.setBearer(token)
.setParameter("identifier", identifier)
.setParameter("name", name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import android.net.Uri;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

Expand Down Expand Up @@ -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
*
Expand All @@ -271,6 +279,10 @@ public Guardian build() {
builder.enableLogging();
}

if (telemetryInfo != null) {
builder.setTelemetryInfo(telemetryInfo.appName, telemetryInfo.appVersion);
oleksandrozerov-okta marked this conversation as resolved.
Show resolved Hide resolved
}

return new Guardian(builder.build());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -106,6 +116,7 @@ public GuardianAPIRequest<Map<String, Object>> enroll(@NonNull String enrollment
return requestFactory
.<Map<String, Object>>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))
Expand Down Expand Up @@ -162,6 +173,7 @@ public GuardianAPIRequest<Void> allow(@NonNull String txToken,
final String jwt = createAccessApprovalJWT(privateKey, url.toString(), deviceIdentifier, challenge, true, null);
return requestFactory
.<Void>newRequest("POST", url, Void.class)
.setHeader("Auth0-Client", this.clientInfo.toBase64())
.setBearer(txToken)
.setParameter("challenge_response", jwt);
}
Expand Down Expand Up @@ -189,6 +201,7 @@ public GuardianAPIRequest<Void> reject(@NonNull String txToken,
final String jwt = createAccessApprovalJWT(privateKey, url.toString(), deviceIdentifier, challenge, false, reason);
return requestFactory
.<Void>newRequest("POST", url, Void.class)
.setHeader("Auth0-Client", this.clientInfo.toBase64())
.setBearer(txToken)
.setParameter("challenge_response", jwt);
}
Expand Down Expand Up @@ -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
*
Expand All @@ -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
Expand All @@ -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);
}
Expand All @@ -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);
}
}
Expand Down
25 changes: 25 additions & 0 deletions guardian/src/test/java/android/util/Base64.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -234,22 +234,77 @@ 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<Map<String, Object>> 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()));

byte[] auth0ClientDecoded = Base64.decode(
auth0ClientEncoded, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP);

Type type = new TypeToken<Map<String, String>>() {
}.getType();
Gson gson = new GsonBuilder().create();
Map<String, String> 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<Map<String, Object>> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading