From 2f2c9d00c12ad496cae5645874442288e1d37ad1 Mon Sep 17 00:00:00 2001 From: Navid Date: Fri, 17 Nov 2023 12:12:10 -0500 Subject: [PATCH 1/3] Don't offer initial calibration to G7 --- .../java/com/eveningoutpost/dexdrip/NavDrawerBuilder.java | 2 +- .../dexdrip/g5model/FirmwareCapability.java | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/NavDrawerBuilder.java b/app/src/main/java/com/eveningoutpost/dexdrip/NavDrawerBuilder.java index 11f290f10e..4a378041e3 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/NavDrawerBuilder.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/NavDrawerBuilder.java @@ -90,7 +90,7 @@ public NavDrawerBuilder(final Context context) { } } else { //If there haven't been two initial calibrations if (BgReading.isDataSuitableForDoubleCalibration() || Ob1G5CollectionService.isG5WantingInitialCalibration()) { - if (FirmwareCapability.isTransmitterRawIncapable(getTransmitterID()) && last_two_bgReadings.size() > 1) { //A Firefly G6 after third reading + if ((FirmwareCapability.isTransmitterRawIncapable(getTransmitterID()) && last_two_bgReadings.size() > 1) || FirmwareCapability.isDeviceG7(getTransmitterID()) ) { //A Firefly G6 after third reading this.nav_drawer_options.add(context.getString(R.string.add_calibration)); this.nav_drawer_intents.add(new Intent(context, AddCalibration.class)); } else { //G5 or non-Firefly G6 or Firefly G6 in no-code mode, after warm-up before initial calibration diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/g5model/FirmwareCapability.java b/app/src/main/java/com/eveningoutpost/dexdrip/g5model/FirmwareCapability.java index a33a1aa2ed..6665a1b8b4 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/g5model/FirmwareCapability.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/g5model/FirmwareCapability.java @@ -79,6 +79,10 @@ static boolean isFirmwarePreemptiveRestartCapable(final String version) { return isFirmwareRawCapable(version); // hang off this for now as they are currently the same } + static boolean isG7Firmware(final String version) { + return KNOWN_ALT_FIRMWARES.contains(version); + } + public static boolean isTransmitterPredictiveCapable(final String tx_id) { return isG6Firmware(getRawFirmwareVersionString(tx_id)); } @@ -98,6 +102,10 @@ public static boolean isTransmitterStandardFirefly(final String tx_id) { // Fire return false; } + public static boolean isDeviceG7(final String tx_id) { + return isG7Firmware(getRawFirmwareVersionString(tx_id)); + } + public static boolean isTransmitterG5(final String tx_id) { return isG5Firmware(getRawFirmwareVersionString(tx_id)); } From 63dd2022f0be81353996b27596c8884f3ab40737 Mon Sep 17 00:00:00 2001 From: Navid Date: Fri, 17 Nov 2023 12:14:27 -0500 Subject: [PATCH 2/3] Comment --- .../main/java/com/eveningoutpost/dexdrip/NavDrawerBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/NavDrawerBuilder.java b/app/src/main/java/com/eveningoutpost/dexdrip/NavDrawerBuilder.java index 4a378041e3..42a6bcf17b 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/NavDrawerBuilder.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/NavDrawerBuilder.java @@ -90,7 +90,7 @@ public NavDrawerBuilder(final Context context) { } } else { //If there haven't been two initial calibrations if (BgReading.isDataSuitableForDoubleCalibration() || Ob1G5CollectionService.isG5WantingInitialCalibration()) { - if ((FirmwareCapability.isTransmitterRawIncapable(getTransmitterID()) && last_two_bgReadings.size() > 1) || FirmwareCapability.isDeviceG7(getTransmitterID()) ) { //A Firefly G6 after third reading + if ((FirmwareCapability.isTransmitterRawIncapable(getTransmitterID()) && last_two_bgReadings.size() > 1) || FirmwareCapability.isDeviceG7(getTransmitterID()) ) { //A Firefly G6 after third reading or a G7 this.nav_drawer_options.add(context.getString(R.string.add_calibration)); this.nav_drawer_intents.add(new Intent(context, AddCalibration.class)); } else { //G5 or non-Firefly G6 or Firefly G6 in no-code mode, after warm-up before initial calibration From 84efa6a65a129a51c2add162a413fb2bedee2ac4 Mon Sep 17 00:00:00 2001 From: benceszasz Date: Sun, 26 Nov 2023 13:58:28 +0100 Subject: [PATCH 3/3] CarePartner app authentication --- app/build.gradle | 3 + .../CareLinkFollowDownloader.java | 35 +- .../carelinkfollow/CareLinkFollowService.java | 48 +- .../carelinkfollow/auth/CareLinkAuthType.java | 6 + .../auth/CareLinkAuthentication.java | 20 + .../auth/CareLinkAuthenticator.java | 608 ++++++++++++++++-- .../auth/CareLinkCredential.java | 34 +- .../auth/CareLinkCredentialStore.java | 69 +- .../auth/CarePartnerAppConfig.java | 92 +++ .../carelinkfollow/client/CareLinkClient.java | 63 +- .../dexdrip/utils/Preferences.java | 3 +- 11 files changed, 857 insertions(+), 124 deletions(-) create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthType.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthentication.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CarePartnerAppConfig.java diff --git a/app/build.gradle b/app/build.gradle index 50050ee502..e9caef7917 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -164,6 +164,8 @@ android { //exclude 'META-INF/androidx.core_core.version' exclude 'META-INF/com.android.tools/proguard/coroutines.pro' exclude 'META-INF/ASL2.0' + pickFirst 'org/bouncycastle/x509/CertPathReviewerMessages.properties' + pickFirst 'org/bouncycastle/x509/CertPathReviewerMessages_de.properties' } compileOptions { @@ -340,6 +342,7 @@ dependencies { //implementation 'com.google.dagger:dagger-android-support:2.x' // if you use the support libraries annotationProcessor 'com.google.dagger:dagger-compiler:2.25.4' implementation 'net.danlew:android.joda:2.10.6.1' + implementation 'org.bouncycastle:bcpkix-jdk15to18:1.68' testImplementation 'joda-time:joda-time:2.10.7' testImplementation 'junit:junit:4.13.2' diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/CareLinkFollowDownloader.java b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/CareLinkFollowDownloader.java index 13bdbd6058..9b990ec132 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/CareLinkFollowDownloader.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/CareLinkFollowDownloader.java @@ -9,7 +9,6 @@ import com.eveningoutpost.dexdrip.utilitymodels.CollectionServiceStarter; import com.eveningoutpost.dexdrip.utilitymodels.Inevitable; import com.eveningoutpost.dexdrip.cgm.carelinkfollow.client.CareLinkClient; -import com.eveningoutpost.dexdrip.cgm.carelinkfollow.client.CountryUtils; import com.eveningoutpost.dexdrip.cgm.carelinkfollow.message.RecentData; import static com.eveningoutpost.dexdrip.models.JoH.emptyString; @@ -66,7 +65,7 @@ public void doEverything(boolean refreshToken, boolean downloadData) { private void downloadData() { msg("Start download"); - if (checkCredentials()) { + if (checkCredentials(true, true, true)) { try { if (getCareLinkClient() != null) { extendWakeLock(30_000); @@ -85,35 +84,37 @@ private void downloadData() { private void refreshToken() { msg("Start refreshing token"); - if (checkCredentials()) { + if (checkCredentials(true, false, true)) { try { if (new CareLinkAuthenticator(CareLinkCredentialStore.getInstance().getCredential().country, CareLinkCredentialStore.getInstance()).refreshToken()) { - UserError.Log.d(TAG, "Login token renewed!"); + UserError.Log.d(TAG, "Access token renewed!"); msg(null); } else { - UserError.Log.e(TAG, "Error renewing login token!"); - msg("Login refresh failed! Will try again!"); + UserError.Log.e(TAG, "Error renewing access token!"); + msg("Access refresh failed! Will try again!"); } } catch (Exception e) { - UserError.Log.e(TAG, "Error renewing login token: " + e.getMessage()); - msg("Login refresh failed! Will try again!"); + UserError.Log.e(TAG, "Error renewing access token: " + e.getMessage()); + msg("Access refresh failed! Will try again!"); } } } - private boolean checkCredentials() { + private boolean checkCredentials(boolean checkAuthenticated, boolean checkAccessExpired, boolean checkRefreshExpired) { // Not authenticated - if (CareLinkCredentialStore.getInstance().getAuthStatus() != CareLinkCredentialStore.AUTHENTICATED) { + if (checkAuthenticated && CareLinkCredentialStore.getInstance().getAuthStatus() != CareLinkCredentialStore.AUTHENTICATED) { msg("Not logged in! Please log in!"); return false; - // Token expired - } else if (CareLinkCredentialStore.getInstance().getExpiresIn() <= 0) { - msg("Login refresh expired! Please log in!"); + } + if (checkAccessExpired && CareLinkCredentialStore.getInstance().getAccessExpiresIn() <= 0) { + msg("Access expired!"); + return false; + } + if (checkRefreshExpired && CareLinkCredentialStore.getInstance().getRefreshExpiresIn() <= 0) { + msg("Login expired! Please log in!"); return false; - // Credentials are all ok! - } else { - return true; } + return true; } private void msg(final String msg) { @@ -214,4 +215,4 @@ protected static synchronized void releaseWakeLock() { JoH.releaseWakeLock(wl); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/CareLinkFollowService.java b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/CareLinkFollowService.java index 4667501148..7f8db2745b 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/CareLinkFollowService.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/CareLinkFollowService.java @@ -127,20 +127,20 @@ static void scheduleWakeUp() { final BgReading lastBg = BgReading.lastNoSenssor(); final long last = lastBg != null ? lastBg.timestamp : 0; - final long nextTokenRefresh = anticipateNextTokenRefresh(JoH.tsl(), CareLinkCredentialStore.getInstance().getExpiresOn(), getRenewBeforeMillis(), getRenewIntervalMillis()); + final long nextTokenRefresh = anticipateNextTokenRefresh(JoH.tsl(), CareLinkCredentialStore.getInstance().getAccessExpiresOn(), getRenewBeforeMillis(), getRenewIntervalMillis()); final long nextDataPoll = anticipateNextDataPoll(JoH.tsl(), last, SAMPLE_PERIOD, getGraceMillis(), getMissedIntervalMillis()); // Token needs to refreshed sooner - if(nextTokenRefresh <= nextDataPoll){ + if (nextTokenRefresh <= nextDataPoll) { next = nextTokenRefresh; - scheduleReason = " as login expires: "; + scheduleReason = " as access expires: "; // Data is required sooner } else { next = nextDataPoll; scheduleReason = " as last BG timestamp: "; } - if(JoH.msTill(next) < (RATE_LIMIT_SECONDS * Constants.SECOND_IN_MS)) + if (JoH.msTill(next) < (RATE_LIMIT_SECONDS * Constants.SECOND_IN_MS)) next = JoH.tsl() + (RATE_LIMIT_SECONDS * Constants.SECOND_IN_MS); wakeup_time = next; @@ -149,7 +149,7 @@ static void scheduleWakeUp() { JoH.wakeUpIntent(xdrip.getAppContext(), JoH.msTill(next), WakeLockTrampoline.getPendingIntent(CareLinkFollowService.class, Constants.CARELINK_SERVICE_FAILOVER_ID)); } - private static long anticipateNextTokenRefresh(long now, final long expiry, final long before, final long interval){ + private static long anticipateNextTokenRefresh(long now, final long expiry, final long before, final long interval) { long next; @@ -188,7 +188,7 @@ public static long anticipateNextDataPoll(long now, final long last, final long } - private static CareLinkFollowDownloader getDownloader(){ + private static CareLinkFollowDownloader getDownloader() { if (downloader == null) { downloader = new CareLinkFollowDownloader( Pref.getString("clfollow_user", ""), @@ -223,16 +223,16 @@ public int onStartCommand(Intent intent, int flags, int startId) { gracePeriod = Pref.getStringToInt("clfollow_grace_period", 30); if (missedPollInterval == 0) missedPollInterval = Pref.getStringToInt("clfollow_missed_poll_interval", 5); - if(renewBefore == 0) + if (renewBefore == 0) renewBefore = 10; - if(renewInterval == 0) + if (renewInterval == 0) renewInterval = 1; lastBg = BgReading.lastNoSenssor(); if (lastBg != null) { lastBgTime = lastBg.timestamp; } // Check if downloader needs to be started (last BG old or token needs to be renewed) - final boolean refreshToken = (JoH.msTill(CareLinkCredentialStore.getInstance().getExpiresOn()) < getRenewBeforeMillis()) ? true : false; + final boolean refreshToken = (JoH.msTill(CareLinkCredentialStore.getInstance().getAccessExpiresOn()) < getRenewBeforeMillis()) ? true : false; final boolean downloadData = (lastBg == null || msSince(lastBg.timestamp) > SAMPLE_PERIOD + getGraceMillis()) ? true : false; if (refreshToken || downloadData) { //Only start if rate limit is not exceeded @@ -262,7 +262,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { public static List megaStatus() { final BgReading lastBg = BgReading.lastNoSenssor(); - long hightlightGrace = Constants.SECOND_IN_MS * getGraceMillis() + Constants.SECOND_IN_MS * 10; //garce + 20 seconds for processing + long hightlightGrace = Constants.SECOND_IN_MS * getGraceMillis() + Constants.SECOND_IN_MS * 10; //garce + 20 seconds for processing // Status for BG receive delay (time from bg was recorded till received in xdrip) String ageOfBgLastPoll = "n/a"; @@ -300,18 +300,36 @@ public static List megaStatus() { authHighlight = StatusItem.Highlight.GOOD; authStatus = "AUTHENTICATED"; break; - case CareLinkCredentialStore.TOKEN_EXPIRED: + case CareLinkCredentialStore.ACCESS_EXPIRED: + authHighlight = StatusItem.Highlight.NOTICE; + authStatus = "ACCESS EXPIRED"; + break; + case CareLinkCredentialStore.REFRESH_EXPIRED: authHighlight = StatusItem.Highlight.BAD; - authStatus = "TOKEN EXPIRED"; + authStatus = "REFRESH EXPIRED"; break; } + //Client type + String clientType = "Unkown"; + try { + switch (CareLinkCredentialStore.getInstance().getCredential().authType) { + case Browser: + clientType = "Browser"; + break; + case MobileApp: + clientType = "CarePartner app"; + break; + } + } catch (Exception ex) { + } //Build status screeen List megaStatus = new ArrayList<>(); - + megaStatus.add(new StatusItem("Client type", clientType)); megaStatus.add(new StatusItem("Authentication status", authStatus, authHighlight)); - megaStatus.add(new StatusItem("Login expires in", JoH.niceTimeScalar(CareLinkCredentialStore.getInstance().getExpiresIn()))); + megaStatus.add(new StatusItem("Access expires in", JoH.niceTimeScalar(CareLinkCredentialStore.getInstance().getAccessExpiresIn()))); + megaStatus.add(new StatusItem("Login expires in", JoH.niceTimeScalar(CareLinkCredentialStore.getInstance().getRefreshExpiresIn()))); megaStatus.add(new StatusItem()); megaStatus.add(new StatusItem("Latest BG", ageLastBg + (lastBg != null ? " ago" : ""), bgAgeHighlight)); megaStatus.add(new StatusItem("BG receive delay", ageOfBgLastPoll, ageOfLastBgPollHighlight)); @@ -374,4 +392,4 @@ public static SpannableString nanoStatus() { return emptyString(current_state) ? null : new SpannableString(current_state); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthType.java b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthType.java new file mode 100644 index 0000000000..b4f09a6176 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthType.java @@ -0,0 +1,6 @@ +package com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth; + +public enum CareLinkAuthType { + Browser, + MobileApp +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthentication.java b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthentication.java new file mode 100644 index 0000000000..d7d87e1fb3 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthentication.java @@ -0,0 +1,20 @@ +package com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth; + +import okhttp3.Headers; + +public class CareLinkAuthentication { + + public CareLinkAuthType authType; + private Headers.Builder builder; + + public CareLinkAuthentication(Headers headers, CareLinkAuthType authType) { + this.builder = new Headers.Builder(); + this.builder.addAll(headers); + this.authType = authType; + } + + public Headers getHeaders() { + return builder.build(); + } + +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthenticator.java b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthenticator.java index 69dcaf5bc8..e38e5bca38 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthenticator.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthenticator.java @@ -2,8 +2,10 @@ import android.app.Activity; import android.app.Dialog; +import android.app.AlertDialog; import android.content.DialogInterface; import android.graphics.Bitmap; +import android.net.UrlQuerySanitizer; import android.os.Handler; import android.os.Looper; import android.support.v7.widget.LinearLayoutCompat; @@ -12,15 +14,49 @@ import android.webkit.WebView; import android.webkit.WebViewClient; +import com.eveningoutpost.dexdrip.models.UserError; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.KeyPair; +import java.security.SecureRandom; import java.text.SimpleDateFormat; import java.util.ArrayList; + +import android.util.Base64; +import android.widget.LinearLayout; +import android.widget.ProgressBar; + +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; + +import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Locale; +import java.util.Map; +import java.util.Random; +import java.util.UUID; import java.util.concurrent.Semaphore; +import javax.security.auth.x500.X500Principal; + +import okhttp3.ConnectionPool; import okhttp3.Cookie; +import okhttp3.FormBody; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -29,12 +65,22 @@ public class CareLinkAuthenticator { + private static final String TAG = "CareLinkAuthenticator"; + + protected static final String CAREPARTNER_APP_DISCO_URL = "https://clcloud.minimed.com/connect/carepartner/v6/discover/android/3.1"; protected static final String CARELINK_CONNECT_SERVER_EU = "carelink.minimed.eu"; protected static final String CARELINK_CONNECT_SERVER_US = "carelink.minimed.com"; protected static final String CARELINK_LANGUAGE_EN = "en"; protected static final String CARELINK_AUTH_TOKEN_COOKIE_NAME = "auth_tmp_token"; protected static final String CARELINK_TOKEN_VALIDTO_COOKIE_NAME = "c_token_valid_to"; + protected static final String[] ANDROID_MODELS = { + "SM-G973F", + "SM-G988U1", + "SM-G981W", + "SM-G9600" + }; + protected static final SimpleDateFormat[] VALIDTO_DATE_FORMATS = { new SimpleDateFormat("EEE MMM d HH:mm:ss zzz yyyy", Locale.ENGLISH), new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.ENGLISH), @@ -52,10 +98,22 @@ public class CareLinkAuthenticator { //new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX") }; + private static final int PKCE_BASE64_ENCODE_SETTINGS = + Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE; + private final Semaphore available = new Semaphore(0, true); + private AlertDialog progressDialog; private String carelinkCountry; private CareLinkCredentialStore credentialStore; + private CarePartnerAppConfig carepartnerAppConfig; + private String deviceId = null; + private String androidModel; + private String clientId = null; + private String clientSecret = null; + private String magIdentifier = null; + private String authCode = null; + private OkHttpClient httpClient = null; public CareLinkAuthenticator(String carelinkCountry, CareLinkCredentialStore credentialStore) { @@ -63,44 +121,387 @@ public CareLinkAuthenticator(String carelinkCountry, CareLinkCredentialStore cre this.credentialStore = credentialStore; } - /* - public synchronized CareLinkCredential getCreditential() throws InterruptedException { - if(Looper.myLooper() == Looper.getMainLooper()) - throw new RuntimeException("don't call getAccessToken() from the main thread."); + public boolean authenticate(Activity context, CareLinkAuthType authType) throws InterruptedException { - switch (credentialStore.getAuthStatus()) { - case CareLinkCredentialStore.NOT_AUTHENTICATED: - authenticate(); - available.acquire(); - break; - case CareLinkCredentialStore.TOKEN_EXPIRED: - refreshToken(); - available.acquire(); + if (Looper.myLooper() == Looper.getMainLooper()) + throw new RuntimeException("don't call authenticate() from the main thread."); + + switch (authType) { + case Browser: + this.authenticateAsBrowser(context); break; - case CareLinkCredentialStore.AUTHENTICATED: + case MobileApp: + this.authenticateAsCpApp(context); break; } - return credentialStore.getCredential(); + return (credentialStore.getAuthStatus() == CareLinkCredentialStore.AUTHENTICATED); } - */ - public boolean authenticate(Activity context) throws InterruptedException { - if (Looper.myLooper() == Looper.getMainLooper()) - throw new RuntimeException("don't call authenticate() from the main thread."); + public boolean refreshToken() { + //Have credential, authType is known, already authenticated + if (credentialStore.getCredential() != null && credentialStore.getCredential().authType != null && credentialStore.getAuthStatus() != CareLinkCredentialStore.NOT_AUTHENTICATED) { + switch (credentialStore.getCredential().authType) { + case Browser: + return this.refreshBrowserToken(); + case MobileApp: + return this.refreshCpAppToken(); + default: + return false; + } + } else { + return false; + } + } + + private void authenticateAsCpApp(Activity context) throws InterruptedException { + + JsonObject clientCredential; + String codeVerifier = null; + String authUrl; + String idToken = null; + String idTokenType = null; + + try { + + //Show progress dialog + this.showProgressDialog(context); - Handler handler = new Handler(Looper.getMainLooper()); - handler.post(new Runnable() { + //Generate IDs, models + deviceId = generateDeviceId(); + androidModel = this.generateAndroidModel(); + + //Load App config + this.loadAppConfig(); + + //Create client credential + clientCredential = this.createClientCredential(deviceId); + clientId = clientCredential.get("client_id").getAsString(); + clientSecret = clientCredential.get("client_secret").getAsString(); + + //Prepare authentication + UserError.Log.d(TAG, "Prepare authentication"); + codeVerifier = generateRandomDataBase64url(32); + authUrl = this.prepareAuth(clientId, codeVerifier); + + //Hide progress dialog + this.hideProgressDialog(); + + //Authenticate in browser + UserError.Log.d(TAG, "Start browser login"); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + CareLinkAuthenticator.this.showCpAppAuthPage(context, authUrl); + } + }); + available.acquire(); + + //Show progress dialog + this.showProgressDialog(context); + + //Register device + UserError.Log.d(TAG, "Register device"); + Response registerResp = this.registerDevice(deviceId, androidModel, clientId, clientSecret, authCode, codeVerifier); + magIdentifier = registerResp.header("mag-identifier"); + idToken = registerResp.header("id-token"); + idTokenType = registerResp.header("id-token-type"); + + //Get access token + UserError.Log.d(TAG, "Get access token"); + JsonObject tokenObject = this.getAccessToken(clientId, clientSecret, magIdentifier, idToken, idTokenType); + + //Store credentials + UserError.Log.d(TAG, "Store credentials"); + this.credentialStore.setMobileAppCredential(this.carelinkCountry, + this.deviceId, this.androidModel, this.clientId, this.clientSecret, this.magIdentifier, + tokenObject.get("access_token").getAsString(), tokenObject.get("refresh_token").getAsString(), + //new Date(Calendar.getInstance().getTime().getTime() + 15 * 60000), + //new Date(Calendar.getInstance().getTime().getTime() + 30 * 60000)); + new Date(Calendar.getInstance().getTime().getTime() + (tokenObject.get("expires_in").getAsInt() * 1000)), + new Date(Calendar.getInstance().getTime().getTime() + (this.carepartnerAppConfig.getRefreshLifetimeSec() * 1000))); + + //Hide progress dialog + this.hideProgressDialog(); + + } catch (Exception ex) { + UserError.Log.e(TAG, "Error authenticating as CpApp. Details: " + ex.getMessage()); + this.hideProgressDialog(); + } + + } + + private void authenticateAsBrowser(Activity context) throws InterruptedException { + + //Authenticate in browser + new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { - showDialog(context); + CareLinkAuthenticator.this.showBrowserAuthPage(context, ""); } }); available.acquire(); - return (credentialStore.getAuthStatus() == CareLinkCredentialStore.AUTHENTICATED); } - public boolean refreshToken() { + private OkHttpClient getHttpClient() { + return new OkHttpClient.Builder().build(); + } + + private boolean loadAppConfig() { + try { + if (carepartnerAppConfig == null) { + carepartnerAppConfig = new CarePartnerAppConfig(); + UserError.Log.d(TAG, "Get region config"); + carepartnerAppConfig.regionConfig = this.getCpAppRegionConfig(); + UserError.Log.d(TAG, "Get SSO config"); + carepartnerAppConfig.ssoConfig = this.getCpAppSSOConfig(carepartnerAppConfig.getSSOConfigUrl()); + } + return true; + } catch (Exception ex) { + UserError.Log.e(TAG, "Error getting AppConfig. Details: " + ex.getMessage()); + return false; + } + } + + private JsonObject getAccessToken(String clientId, String clientSecret, String magIdentifier, String idToken, String idTokenType) throws IOException { + return this.getToken(clientId, clientSecret, magIdentifier, idToken, idTokenType, null); + } + + private JsonObject refreshToken(String clientId, String clientSecret, String magIdentifier, String refreshToken) throws IOException { + return this.getToken(clientId, clientSecret, magIdentifier, null, null, refreshToken); + } + + private JsonObject getToken(String clientId, String clientSecret, String magIdentifier, String idToken, String idTokenType, String refreshToken) throws IOException { + + Request.Builder requestBuilder = null; + FormBody.Builder form = null; + + //Common token request params + form = new FormBody.Builder() + .add("client_id", clientId) + .add("client_secret", clientSecret); + //Authentication token request params + if (idToken != null) { + form.add("assertion", idToken) + .add("grant_type", idTokenType) + .add("scope", this.carepartnerAppConfig.getOAuthScope()); + //Refresh token request params + } else { + form.add("refresh_token", refreshToken) + .add("grant_type", "refresh_token"); + } + + requestBuilder = new Request.Builder() + .post(form.build()) + .addHeader("mag-identifier", magIdentifier) + .header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + + return this.callSsoRestApi(requestBuilder, carepartnerAppConfig.getOAuthTokenEndpoint(), null); + + } + + private Response registerDevice(String deviceId, String androidModel, String clientId, String clientSecret, String authCode, String codeVerifier) { + + String trimmedCsr = null; + Response response = null; + String cert = null; + + try { + //Create RSA2048 keypair and CSR + KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA"); + keygen.initialize(2048); + KeyPair keypair = keygen.genKeyPair(); + trimmedCsr = createTrimmedCsr(keypair, "SHA256withRSA", "socialLogin", deviceId, androidModel, "Medtronic"); + + + RequestBody body; + Request.Builder requestBuilder; + + body = RequestBody.create(null, trimmedCsr); + + requestBuilder = new Request.Builder() + .post(body) + .addHeader("device-id", Base64.encodeToString(deviceId.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP)) + .addHeader("device-name", Base64.encodeToString(androidModel.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP)) + .addHeader("authorization", "Bearer " + authCode) + .addHeader("client-authorization", "Basic " + Base64.encodeToString((clientId + ":" + clientSecret).getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP)) + .addHeader("cert-format", "pem") + .addHeader("create-session", "true") + .addHeader("code-verifier", codeVerifier) + .addHeader("redirect-uri", carepartnerAppConfig.getOAuthRedirectUri()); + + return response = this.callSsoApi(requestBuilder, carepartnerAppConfig.getMagDeviceRegisterEndpoint(), null); + + } catch (Exception ex) { + ex.getMessage(); + return null; + } + + } + + private String createTrimmedCsr(KeyPair keypair, String signAlgo, String cn, String ou, String dc, String o) throws IOException, OperatorCreationException { + + PKCS10CertificationRequestBuilder p10Builder = new JcaPKCS10CertificationRequestBuilder( + new X500Principal( + "CN=" + cn + + ", OU=" + ou + + ", DC=" + dc + + ", O=" + o), keypair.getPublic()); + JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder(signAlgo); + ContentSigner signer = csBuilder.build(keypair.getPrivate()); + PKCS10CertificationRequest csr = p10Builder.build(signer); + StringWriter writer = new StringWriter(); + JcaPEMWriter jcaPEMWriter = new JcaPEMWriter(writer); + jcaPEMWriter.writeObject(csr); + jcaPEMWriter.close(); + + return writer.toString().replaceAll("-----.*-----", "").replaceAll("\\r", "").replaceAll("\\n", ""); + + } + + private String prepareAuth(String clientId, String codeVerifier) throws IOException { + + Request.Builder requestBuilder = null; + Map queryParams = null; + String codeChallenge = null; + JsonObject providers = null; + + //Generate SHA-256 code challenge + try { + codeChallenge = Base64.encodeToString( + MessageDigest.getInstance("SHA-256").digest(codeVerifier.getBytes("ISO_8859_1")), + PKCE_BASE64_ENCODE_SETTINGS); + } catch (Exception ex) { + } + + //Set query params + queryParams = new HashMap(); + queryParams.put("client_id", clientId); + queryParams.put("response_type", "code"); + queryParams.put("display", "social_login"); + queryParams.put("scope", this.carepartnerAppConfig.getOAuthScope()); + queryParams.put("code_challenge", codeChallenge); + queryParams.put("code_challenge_method", "S256"); + queryParams.put("redirect_uri", this.carepartnerAppConfig.getOAuthRedirectUri()); + queryParams.put("state", generateRandomDataBase64url(32)); + + requestBuilder = new Request.Builder() + .get(); + + providers = this.callSsoRestApi(requestBuilder, carepartnerAppConfig.getOAuthAuthEndpoint(), queryParams); + + //Get auth url of enterprise login provider + for (JsonElement provider : providers.get("providers").getAsJsonArray()) { + if (provider.getAsJsonObject().get("provider").getAsJsonObject().get("id").getAsString().contentEquals("enterprise")) + return (provider.getAsJsonObject().get("provider").getAsJsonObject().get("auth_url").getAsString()); + } + + return null; + + } + + private JsonObject createClientCredential(String deviceId) throws IOException { + + RequestBody form; + Request.Builder requestBuilder; + + form = new FormBody.Builder() + .add("client_id", carepartnerAppConfig.getClientId()) + .add("nonce", UUID.randomUUID().toString()) + .build(); + + requestBuilder = new Request.Builder() + .post(form) + .addHeader("device-id", Base64.encodeToString(deviceId.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP | Base64.URL_SAFE)); + + return this.callSsoRestApi(requestBuilder, carepartnerAppConfig.getMagCredentialInitEndpoint(), null); + + } + + private String generateDeviceId() { + + try { + byte[] bytes = MessageDigest.getInstance("SHA-256").digest(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)); + StringBuilder stringBuilder = new StringBuilder(bytes.length); + for (byte byteChar : bytes) + stringBuilder.append(String.format("%02x", byteChar)); + return stringBuilder.toString(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + + return null; + + } + + private String generateAndroidModel() { + return ANDROID_MODELS[new Random().nextInt(ANDROID_MODELS.length)]; + } + + private String generateRandomDataBase64url(int length) { + SecureRandom secureRandom = new SecureRandom(); + byte[] codeVerifier = new byte[length]; + secureRandom.nextBytes(codeVerifier); + return Base64.encodeToString(codeVerifier, PKCE_BASE64_ENCODE_SETTINGS); + } + + private JsonObject callSsoRestApi(Request.Builder requestBuilder, String endpoint, Map queryParams) throws IOException { + + Response response = this.callSsoApi(requestBuilder, endpoint, queryParams); + if (response.isSuccessful()) { + return JsonParser.parseString(response.body().string()).getAsJsonObject(); + } else { + return null; + } + + } + + private Response callSsoApi(Request.Builder requestBuilder, String endpoint, Map queryParams) throws IOException { + + HttpUrl.Builder url = null; + + //Build URL + url = new HttpUrl.Builder() + .scheme("https") + .host(carepartnerAppConfig.getSSOServerHost()) + .addPathSegments(carepartnerAppConfig.getSSOServerPrefix()) + .addPathSegments(endpoint); + //Add query params + if (queryParams != null) { + for (Map.Entry param : queryParams.entrySet()) { + url.addQueryParameter(param.getKey(), param.getValue()); + } + } + requestBuilder.url(url.build()); + //Send request + return this.getHttpClient().newCall(requestBuilder.build()).execute(); + + } + + private boolean refreshCpAppToken() { + JsonObject tokenRefreshResult; + + try { + this.loadAppConfig(); + tokenRefreshResult = this.refreshToken( + credentialStore.getCredential().clientId, credentialStore.getCredential().clientSecret, + credentialStore.getCredential().magIdentifier, credentialStore.getCredential().refreshToken); + credentialStore.updateMobileAppCredential( + tokenRefreshResult.get("access_token").getAsString(), + //new Date(Calendar.getInstance().getTime().getTime() + 15 * 60000), + //new Date(Calendar.getInstance().getTime().getTime() + 30 * 60000), + new Date(Calendar.getInstance().getTime().getTime() + (tokenRefreshResult.get("expires_in").getAsInt() * 1000)), + new Date(Calendar.getInstance().getTime().getTime() + (this.carepartnerAppConfig.getRefreshLifetimeSec() * 1000)), + tokenRefreshResult.get("refresh_token").getAsString()); + return true; + } catch (Exception ex) { + UserError.Log.e(TAG, "Error refreshing CpApp token! Details: " + ex.getMessage()); + return false; + } + } + + private boolean refreshBrowserToken() { + //If not authenticated => unable to refresh if (credentialStore.getAuthStatus() == CareLinkCredentialStore.NOT_AUTHENTICATED) return false; @@ -143,10 +544,10 @@ public boolean refreshToken() { //New authentication cookies found if (cookieJar.contains(CARELINK_AUTH_TOKEN_COOKIE_NAME) && cookieJar.contains(CARELINK_TOKEN_VALIDTO_COOKIE_NAME)) { //Update credentials - this.credentialStore.setCredential( - this.carelinkCountry, + this.credentialStore.updateBrowserCredential( cookieJar.getCookies(CARELINK_AUTH_TOKEN_COOKIE_NAME).get(0).value(), this.parseValidTo(cookieJar.getCookies(CARELINK_TOKEN_VALIDTO_COOKIE_NAME).get(0).value()), + this.parseValidTo(cookieJar.getCookies(CARELINK_TOKEN_VALIDTO_COOKIE_NAME).get(0).value()), cookieJar.getAllCookies().toArray(new Cookie[0])); } else { return false; @@ -164,13 +565,72 @@ public boolean refreshToken() { } return (credentialStore.getAuthStatus() == CareLinkCredentialStore.AUTHENTICATED); + } + + private void showProgressDialog(Activity context) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + CareLinkAuthenticator.this.getProgressDialog(context).show(); + } + }); + } + + private void hideProgressDialog() { + + if (this.progressDialog != null && this.progressDialog.isShowing()) { + this.progressDialog.dismiss(); + } + + } + + private AlertDialog getProgressDialog(Activity context) { + if (this.progressDialog == null) { + AlertDialog.Builder builder; + builder = new AlertDialog.Builder(context); + builder.setTitle("Login in progress..."); + final ProgressBar progressBar = new ProgressBar(context); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + progressBar.setLayoutParams(lp); + builder.setView(progressBar); + this.progressDialog = builder.create(); + } + //return builder; + return this.progressDialog; + } + + private void showBrowserAuthPage(Activity context, String url) { + final Dialog authDialog = new Dialog(context); + this.showAuthWebView(authDialog, url, new WebViewClient() { + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + if (CareLinkAuthenticator.this.extractBrowserLoginCookies(url)) + authDialog.dismiss(); + } + }); } - private void showDialog(Activity context) { + private void showCpAppAuthPage(Activity context, String url) { + + //CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder().build(); + //customTabsIntent.launchUrl( + // context, Uri.parse(url)); - //Create dialog final Dialog authDialog = new Dialog(context); + this.showAuthWebView(authDialog, url, new WebViewClient() { + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + if (CareLinkAuthenticator.this.extractCpAppAuthCode(url)) + authDialog.dismiss(); + } + }); + } + + private void showAuthWebView(Dialog authDialog, String url, WebViewClient webViewClient) { + LinearLayoutCompat layout = new LinearLayoutCompat(authDialog.getContext()); WebView webView = new WebView(authDialog.getContext()); webView.setLayoutParams(new LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); @@ -187,28 +647,81 @@ public void onDismiss(DialogInterface dialog) { //Configure Webview webView.getSettings().setJavaScriptEnabled(true); webView.getSettings().setUserAgentString("Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36"); - HashMap headers = new HashMap<>(); - headers.put("Accept-Language", "en;q=0.9, *;q=0.8"); - headers.put("Sec-Ch-Ua", "\"Google Chrome\";v=\"117\", \"Not;A=Brand\";v=\"8\", \"Chromium\";v=\"117\""); - webView.loadUrl(this.getLoginUrl(), headers); - webView.setWebViewClient(new WebViewClient() { - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - if (CareLinkAuthenticator.this.extractCookies(url)) - authDialog.dismiss(); - } - - }); - + webView.loadUrl(url); + webView.setWebViewClient(webViewClient); //Set dialog display infos and show it authDialog.setCancelable(true); authDialog.getWindow().setLayout(LinearLayoutCompat.LayoutParams.MATCH_PARENT, LinearLayoutCompat.LayoutParams.MATCH_PARENT); authDialog.show(); + + } + + private boolean extractCpAppAuthCode(String url) { + + //When directed to redirect uri => extract code + if (url.contains(this.carepartnerAppConfig.getOAuthRedirectUri())) { + try { + UrlQuerySanitizer sanitizer = new UrlQuerySanitizer(); + sanitizer.setAllowUnregisteredParamaters(true); + sanitizer.parseUrl(url); + authCode = sanitizer.getValue("code"); + } catch (Exception ex) { + } + return true; + } else + return false; + + } + + private JsonObject getCpAppRegionConfig() { + + + //Get CarePartner app discover + JsonObject endpointConfig = this.getConfigJson(CAREPARTNER_APP_DISCO_URL); + //Extract region config of selected country + JsonArray countries = endpointConfig.getAsJsonArray("supportedCountries"); + JsonArray regions = endpointConfig.getAsJsonArray("CP"); + for (JsonElement country : countries) { + if (country.getAsJsonObject().has(this.carelinkCountry.toUpperCase(Locale.ROOT))) { + String regionCode = country.getAsJsonObject().get(this.carelinkCountry.toUpperCase(Locale.ROOT)).getAsJsonObject().get("region").getAsString(); + for (JsonElement region : regions) { + if (region.getAsJsonObject().get("region").getAsString().contentEquals(regionCode)) { + return region.getAsJsonObject(); + } + } + } + } + + return null; + + } + + private JsonObject getCpAppSSOConfig(String url) { + return this.getConfigJson(url); + } + + private JsonObject getConfigJson(String url) { + + Request request = null; + + request = new Request.Builder() + .url(url) + .get() + .build(); + try { + Response response = this.getHttpClient().newCall(request).execute(); + if (response.isSuccessful()) { + return JsonParser.parseString(response.body().string()).getAsJsonObject(); + } + } catch (Exception ex) { + } + + return null; + } - protected String getLoginUrl() { + private String getWebAppLoginUrl() { HttpUrl url = null; @@ -224,14 +737,15 @@ protected String getLoginUrl() { } - protected String careLinkServer() { + private String careLinkServer() { if (this.carelinkCountry.equals("us")) return CARELINK_CONNECT_SERVER_US; else return CARELINK_CONNECT_SERVER_EU; } - protected Boolean extractCookies(String url) { + private Boolean extractBrowserLoginCookies(String url) { + String cookies = null; String authToken = null; String host = null; @@ -277,7 +791,7 @@ protected Boolean extractCookies(String url) { return false; //Update credentials - this.credentialStore.setCredential(this.carelinkCountry, authToken, validToDate, cookieList.toArray(new Cookie[0])); + this.credentialStore.setBrowserCredential(this.carelinkCountry, authToken, validToDate, validToDate, cookieList.toArray(new Cookie[0])); //success return true; } else @@ -285,12 +799,12 @@ protected Boolean extractCookies(String url) { return false; } - protected void unlock() { + private void unlock() { if (available.availablePermits() <= 0) available.release(); } - protected Date parseValidTo(String validToDateString) { + private Date parseValidTo(String validToDateString) { for (SimpleDateFormat zonedFormat : VALIDTO_DATE_FORMATS) { try { return zonedFormat.parse(validToDateString); diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkCredential.java b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkCredential.java index 352374f257..89705d1bcb 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkCredential.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkCredential.java @@ -4,13 +4,39 @@ import java.util.Date; import okhttp3.Cookie; +import okhttp3.Headers; public class CareLinkCredential { + public String country = null; public String accessToken = null; public Cookie[] cookies = null; - public Date tokenValidTo = null; + public Date accessValidTo = null; + public Date refreshValidTo = null; + public CareLinkAuthType authType = null; + public String androidModel = null; + public String deviceId = null; + public String clientId = null; + public String clientSecret = null; + public String magIdentifier = null; + public String refreshToken = null; + + + public CareLinkAuthentication getAuthentication() { + + //Not authenticated + if (this.authType == null || this.getAuthorizationFieldValue() == null) + return null; + + //Build authentication + Headers.Builder headers = new Headers.Builder(); + headers.add("Authorization", this.getAuthorizationFieldValue()); + if (this.authType == CareLinkAuthType.MobileApp) + headers.add("mag-identifier", this.magIdentifier); + return new CareLinkAuthentication(headers.build(), this.authType); + + } public String getToken() { return accessToken; @@ -23,11 +49,11 @@ public String getAuthorizationFieldValue() { return "Bearer " + this.getToken(); } - public long getExpiresIn() { - if (this.tokenValidTo == null) + public long getAccessExpiresIn() { + if (this.accessValidTo == null) return -1; else - return this.tokenValidTo.getTime() - Calendar.getInstance().getTime().getTime(); + return this.accessValidTo.getTime() - Calendar.getInstance().getTime().getTime(); } } diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkCredentialStore.java b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkCredentialStore.java index ceadeabb97..62cf74b7d5 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkCredentialStore.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkCredentialStore.java @@ -15,8 +15,9 @@ public class CareLinkCredentialStore { private static final String TAG = "CareLinkCredentialStore"; public final static int NOT_AUTHENTICATED = 0; - public final static int TOKEN_EXPIRED = 1; + public final static int ACCESS_EXPIRED = 1; public final static int AUTHENTICATED = 2; + public final static int REFRESH_EXPIRED = 3; private CareLinkCredential credential = null; private static CareLinkCredentialStore instance = null; @@ -37,7 +38,8 @@ public static CareLinkCredentialStore getInstance() { if (!credJson.equals("")) { try { CareLinkCredential savedCred = new GsonBuilder().create().fromJson(credJson, CareLinkCredential.class); - instance.setCredential(savedCred.country, savedCred.accessToken, savedCred.tokenValidTo, savedCred.cookies, false); + instance.setCredential(savedCred.country, savedCred.authType, savedCred.accessToken, savedCred.accessValidTo, savedCred.refreshValidTo, savedCred.cookies, + savedCred.androidModel, savedCred.deviceId, savedCred.clientId, savedCred.clientSecret, savedCred.magIdentifier, savedCred.refreshToken, false); } catch (Exception e) { UserError.Log.d(TAG, "Error when restoring saved Credential: " + e.getMessage()); } @@ -49,17 +51,38 @@ public static CareLinkCredentialStore getInstance() { return instance; } - synchronized void setCredential(String country, String accessToken, Date tokenValidTo, Cookie[] cookies) { - this.setCredential(country, accessToken, tokenValidTo, cookies, true); + synchronized void setMobileAppCredential(String country, String deviceId, String androidModel, String clientId, String clientSecret, String magIdentifier, String accessToken, String refreshToken, Date accessValidTo, Date refreshValidTo) { + this.setCredential(country, CareLinkAuthType.MobileApp, accessToken, accessValidTo, refreshValidTo, null, androidModel, deviceId, clientId, clientSecret, magIdentifier, refreshToken, true); } - protected synchronized void setCredential(String country, String accessToken, Date tokenValidTo, Cookie[] cookies, boolean save) { + synchronized void updateMobileAppCredential(String accessToken, Date accessValidTo, Date refreshValidTo, String refreshToken) { + this.setCredential(credential.country, CareLinkAuthType.MobileApp, accessToken, accessValidTo, refreshValidTo, null, credential.androidModel, credential.deviceId, credential.clientId, credential.clientSecret, credential.magIdentifier, refreshToken, true); + } + + synchronized void updateBrowserCredential(String accessToken, Date accessValidTo, Date refreshValidTo, Cookie[] cookies) { + this.setCredential(credential.country, CareLinkAuthType.Browser, accessToken, accessValidTo, refreshValidTo, cookies, null, null, null, null, null, null, true); + } + + synchronized void setBrowserCredential(String country, String accessToken, Date accessValidTo, Date refreshValidTo, Cookie[] cookies) { + this.setCredential(country, CareLinkAuthType.Browser, accessToken, accessValidTo, refreshValidTo, cookies, null, null, null, null, null, null, true); + } + + protected synchronized void setCredential(String country, CareLinkAuthType authType, String accessToken, Date accessValidTo, Date refreshValidTo, Cookie[] cookies, String androidModel, String deviceId, String clientId, String clientSecret, String magIdentifier, String refreshToken, boolean save) { + credential = new CareLinkCredential(); + credential.authType = authType; credential.country = country; credential.accessToken = accessToken; + credential.accessValidTo = accessValidTo; credential.cookies = cookies; - credential.tokenValidTo = tokenValidTo; - if (credential.accessToken == null || credential.tokenValidTo == null) + credential.androidModel = androidModel; + credential.deviceId = deviceId; + credential.clientId = clientId; + credential.clientSecret = clientSecret; + credential.magIdentifier = magIdentifier; + credential.refreshToken = refreshToken; + credential.refreshValidTo = refreshValidTo; + if (credential.accessToken == null || credential.accessValidTo == null) authStatus = NOT_AUTHENTICATED; else evaluateExpiration(); @@ -82,18 +105,32 @@ public int getAuthStatus() { return authStatus; } - public long getExpiresIn() { - if (credential == null || credential.tokenValidTo == null) + public long getAccessExpiresIn() { + if (credential == null || credential.accessValidTo == null) + return -1; + else + return credential.accessValidTo.getTime() - Calendar.getInstance().getTime().getTime(); + } + + public long getAccessExpiresOn() { + if (credential == null || credential.accessValidTo == null) + return -1; + else + return credential.accessValidTo.getTime(); + } + + public long getRefreshExpiresIn() { + if (credential == null || credential.refreshValidTo == null) return -1; else - return credential.tokenValidTo.getTime() - Calendar.getInstance().getTime().getTime(); + return credential.refreshValidTo.getTime() - Calendar.getInstance().getTime().getTime(); } - public long getExpiresOn() { - if (credential == null || credential.tokenValidTo == null) + public long getRefreshExpiresOn() { + if (credential == null || credential.refreshValidTo == null) return -1; else - return credential.tokenValidTo.getTime(); + return credential.refreshValidTo.getTime(); } synchronized void clear() { @@ -104,8 +141,10 @@ synchronized void clear() { } protected void evaluateExpiration() { - if (this.getExpiresIn() < 0) - authStatus = TOKEN_EXPIRED; + if (this.getRefreshExpiresIn() < 0) + authStatus = REFRESH_EXPIRED; + else if (this.getAccessExpiresIn() < 0) + authStatus = ACCESS_EXPIRED; else authStatus = AUTHENTICATED; } diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CarePartnerAppConfig.java b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CarePartnerAppConfig.java new file mode 100644 index 0000000000..939594da5b --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CarePartnerAppConfig.java @@ -0,0 +1,92 @@ +package com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +public class CarePartnerAppConfig { + + public JsonObject regionConfig = null; + public JsonObject ssoConfig = null; + + public String getRegion() { + return regionConfig.get("region").getAsString(); + } + + public String getSSOConfigUrl() { + return regionConfig.get("SSOConfiguration").getAsString(); + } + + public String getCloudBaseUrl() { + return regionConfig.get("baseUrlCumulus").getAsString(); + } + + public String getCareLinkBaseUrl() { + return regionConfig.get("baseUrlCareLink").getAsString(); + } + + public String getSSOServerHost() { + return this.getChildJsonString(ssoConfig, "server.hostname"); + } + + public String getSSOServerPrefix() { + return this.getChildJsonString(ssoConfig, "server.prefix"); + } + + public int getSSOServerPort() { + return this.getChildJsonElement(ssoConfig, "server.port").getAsInt(); + } + + public String getOAuthAuthEndpoint() { + return this.getChildJsonString(ssoConfig, "oauth.system_endpoints.authorization_endpoint_path").substring(1); + } + + public String getOAuthTokenEndpoint() { + return this.getChildJsonString(ssoConfig, "oauth.system_endpoints.token_endpoint_path").substring(1); + } + + public String getMagCredentialInitEndpoint() { + return this.getChildJsonString(ssoConfig, "mag.system_endpoints.client_credential_init_endpoint_path").substring(1); + } + + public String getMagDeviceRegisterEndpoint() { + return this.getChildJsonString(ssoConfig, "mag.system_endpoints.device_register_endpoint_path").substring(1); + } + + public String getClientId() { + return getClientMemberString("client_id"); + } + + public String getOAuthScope() { + return getClientMemberString("scope"); + } + + public String getOAuthRedirectUri() { + return getClientMemberString("redirect_uri"); + } + + public int getRefreshLifetimeSec() { + return Integer.parseInt(getClientMemberString("client_key_custom.lifetimes.oauth2_refresh_token_lifetime_sec")); + } + + private String getClientMemberString(String clientMember) { + return this.getChildJsonString(this.getChildJsonElement(ssoConfig, "oauth.client.client_ids").getAsJsonArray().get(0) + .getAsJsonObject(), clientMember); + } + + private String getChildJsonString(JsonObject parent, String path) { + return getChildJsonElement(parent, path).getAsString(); + } + + private JsonElement getChildJsonElement(JsonObject parent, String path) { + + JsonElement obj = parent; + + for (String member : path.split("\\.")) { + obj = obj.getAsJsonObject().get(member); + } + + return obj; + + } + +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/client/CareLinkClient.java b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/client/CareLinkClient.java index 56764025bc..8703b07c90 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/client/CareLinkClient.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/client/CareLinkClient.java @@ -1,5 +1,7 @@ package com.eveningoutpost.dexdrip.cgm.carelinkfollow.client; +import com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth.CareLinkAuthType; +import com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth.CareLinkAuthentication; import com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth.CareLinkCredentialStore; import com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth.EditableCookieJar; import com.eveningoutpost.dexdrip.cgm.carelinkfollow.message.ActiveNotification; @@ -32,6 +34,7 @@ import okhttp3.ConnectionPool; import okhttp3.FormBody; +import okhttp3.Headers; import okhttp3.HttpUrl; import okhttp3.MediaType; import okhttp3.OkHttpClient; @@ -176,7 +179,10 @@ private void createHttpClient(){ EditableCookieJar cookieJar = null; cookieJar = new EditableCookieJar(); - cookieJar.AddCookies(this.credentialStore.getCredential().cookies); + //Add cookies if there are any + if(this.credentialStore.getCredential().cookies != null && this.credentialStore.getCredential().cookies.length > 0) { + cookieJar.AddCookies(this.credentialStore.getCredential().cookies); + } this.httpClient = new OkHttpClient.Builder() .cookieJar(cookieJar) @@ -204,7 +210,7 @@ public RecentData getRecentData() { public RecentData getRecentData(String patientUsername) { // Force login to get basic info - if (getAuthorizationToken() == null) + if (getAuthentication() == null) return null; // 7xxG @@ -224,7 +230,7 @@ else if (this.sessionM2MEnabled) public String getDefaultPatientUsername() { // Force login to get basic info - if (getAuthorizationToken() == null) + if (getAuthentication() == null) return null; // Care Partner + multi follow => first patient @@ -249,7 +255,7 @@ public boolean isBleDevice(String patientUsername){ return sessionDeviceIsBle; // Force login to get basic info - if(getAuthorizationToken() == null) + if(getAuthentication() == null) return false; // Patient: device from recent uploads if possible @@ -407,7 +413,7 @@ protected Response getLoginSession() throws IOException { requestBuilder = new Request.Builder() .url(url); - this.addHttpHeaders(requestBuilder, RequestType.HtmlGet); + this.addHttpHeaders(requestBuilder, RequestType.HtmlGet, true); return this.httpClient.newCall(requestBuilder.build()).execute(); @@ -441,7 +447,7 @@ protected Response doLogin(Response loginSessionResponse) throws IOException { .url(url) .post(form); - this.addHttpHeaders(requestBuilder, RequestType.HtmlGet); + this.addHttpHeaders(requestBuilder, RequestType.HtmlGet, true); return this.httpClient.newCall(requestBuilder.build()).execute(); @@ -473,7 +479,7 @@ protected Response doConsent(Response doLoginResponse) throws IOException { .url(consentUrl) .post(form); - this.addHttpHeaders(requestBuilder, RequestType.HtmlPost); + this.addHttpHeaders(requestBuilder, RequestType.HtmlPost, true); return this.httpClient.newCall(requestBuilder.build()).execute(); @@ -494,7 +500,7 @@ protected String extractResponseData(String respBody, String groupRegex, int gro } - protected String getAuthorizationToken() { + protected CareLinkAuthentication getAuthentication() { // CredentialStore is used if(this.credentialStore != null){ @@ -505,7 +511,7 @@ protected String getAuthorizationToken() { if(!this.collectingSessionInfos && !this.sessionInfosLoaded) return null; else - return this.credentialStore.getCredential().getAuthorizationFieldValue(); + return this.credentialStore.getCredential().getAuthentication(); // New token is needed: // a) no token or about to expire => execute authentication // b) last response 401 @@ -524,8 +530,11 @@ protected String getAuthorizationToken() { return null; } - //there can be only one - return "Bearer" + " " + ((SimpleOkHttpCookieJar) httpClient.cookieJar()).getCookies(CARELINK_AUTH_TOKEN_COOKIE_NAME).get(0).value(); + //there can be only one auth cookie + return new CareLinkAuthentication( + new Headers.Builder().add("Authorization", "Bearer" + " " + ((SimpleOkHttpCookieJar) httpClient.cookieJar()).getCookies(CARELINK_AUTH_TOKEN_COOKIE_NAME).get(0).value()).build(), + CareLinkAuthType.Browser); + //return "Bearer" + " " + ((SimpleOkHttpCookieJar) httpClient.cookieJar()).getCookies(CARELINK_AUTH_TOKEN_COOKIE_NAME).get(0).value(); } @@ -663,28 +672,30 @@ protected T getData(HttpUrl url, RequestBody requestBody, Class dataClass Request.Builder requestBuilder = null; HttpUrl.Builder urlBuilder = null; - String authToken = null; + CareLinkAuthentication authentication = null; String responseString = null; Response response = null; Object data = null; + boolean isBrowserClient = true; this.lastDataSuccess = false; this.lastErrorMessage = ""; - // Get auth token - authToken = this.getAuthorizationToken(); + // Get authentication + authentication = this.getAuthentication(); - if (authToken != null) { + if (authentication != null) { // Create request for URL with authToken - requestBuilder = new Request.Builder().url(url).addHeader("Authorization", authToken); + //requestBuilder = new Request.Builder().url(url).addHeader("Authorization", authToken); + requestBuilder = new Request.Builder().url(url).headers(authentication.getHeaders()); - // Add header + // Add additional headers if (requestBody == null) { - this.addHttpHeaders(requestBuilder, RequestType.Json); + this.addHttpHeaders(requestBuilder, RequestType.Json, authentication.authType == CareLinkAuthType.Browser); } else { requestBuilder.post(requestBody); - this.addHttpHeaders(requestBuilder, RequestType.HtmlPost); + this.addHttpHeaders(requestBuilder, RequestType.HtmlPost, authentication.authType == CareLinkAuthType.Browser); } // Send request @@ -736,14 +747,16 @@ protected T getData(String host, String path, Map queryParam } // Http header builder for requests - protected void addHttpHeaders(Request.Builder requestBuilder, RequestType type) { + protected void addHttpHeaders(Request.Builder requestBuilder, RequestType type, boolean isBrowserClient) { //Add common browser headers - requestBuilder - .addHeader("Accept-Language", "en;q=0.9, *;q=0.8") - .addHeader("Connection", "keep-alive") - .addHeader("Sec-Ch-Ua", "\"Google Chrome\";v=\"117\", \"Not;A=Brand\";v=\"8\", \"Chromium\";v=\"117\"") - .addHeader("User-Agent", "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36"); + if(isBrowserClient) { + requestBuilder + .addHeader("Accept-Language", "en;q=0.9, *;q=0.8") + .addHeader("Connection", "keep-alive") + .addHeader("Sec-Ch-Ua", "\"Google Chrome\";v=\"117\", \"Not;A=Brand\";v=\"8\", \"Chromium\";v=\"117\"") + .addHeader("User-Agent", "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36"); + } //Set media type based on request type switch (type) { diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/utils/Preferences.java b/app/src/main/java/com/eveningoutpost/dexdrip/utils/Preferences.java index 493ecf412d..0ef2ca1809 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/utils/Preferences.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/utils/Preferences.java @@ -58,6 +58,7 @@ import com.eveningoutpost.dexdrip.alert.Registry; import com.eveningoutpost.dexdrip.calibrations.PluggableCalibration; import com.eveningoutpost.dexdrip.cgm.carelinkfollow.CareLinkFollowService; +import com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth.CareLinkAuthType; import com.eveningoutpost.dexdrip.cgm.nsfollow.NightscoutFollow; import com.eveningoutpost.dexdrip.cgm.sharefollow.ShareFollowService; import com.eveningoutpost.dexdrip.cgm.webfollow.Cpref; @@ -1353,7 +1354,7 @@ public void run() { JoH.static_toast(preference.getContext(), "Country is required!", Toast.LENGTH_LONG); else { CareLinkAuthenticator authenticator = new CareLinkAuthenticator(country, CareLinkCredentialStore.getInstance()); - if (authenticator.authenticate(getActivity())) { + if (authenticator.authenticate(getActivity(), CareLinkAuthType.MobileApp)) { JoH.static_toast(preference.getContext(), "Login completed!", Toast.LENGTH_LONG); CareLinkFollowService.resetInstanceAndInvalidateSession(); CollectionServiceStarter.restartCollectionServiceBackground();