This repository has been archived by the owner on Mar 9, 2022. It is now read-only.
Permalink
Show file tree
Hide file tree
5 changes: 1 addition & 4 deletions
5
app/src/main/java/org/mozilla/firefox/vpn/GuardianComponent.kt
5 changes: 1 addition & 4 deletions
5
app/src/main/java/org/mozilla/firefox/vpn/MockedGuardianComponent.kt
2 changes: 1 addition & 1 deletion
2
app/src/main/java/org/mozilla/firefox/vpn/main/settings/domain/SignOutUseCase.kt
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
New non-polling authentication flow.
- Fixes intermittent hanging during auth - Implements PKCE
- Loading branch information
1 parent
40c281d
commit 981c840
Showing
39 changed files
with
985 additions
and
518 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
78 changes: 78 additions & 0 deletions
78
app/src/androidTest/java/org/mozilla/firefox/vpn/PkceRegressionTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ | ||
|
|
||
| package org.mozilla.firefox.vpn | ||
|
|
||
| import android.content.Intent | ||
| import android.net.Uri | ||
| import androidx.test.espresso.Espresso.onView | ||
| import androidx.test.espresso.assertion.ViewAssertions.matches | ||
| import androidx.test.espresso.matcher.ViewMatchers.isDisplayed | ||
| import androidx.test.espresso.matcher.ViewMatchers.withId | ||
| import androidx.test.ext.junit.runners.AndroidJUnit4 | ||
| import androidx.test.filters.FlakyTest | ||
| import androidx.test.filters.LargeTest | ||
| import androidx.test.rule.ActivityTestRule | ||
| import kotlinx.coroutines.CompletableDeferred | ||
| import kotlinx.coroutines.delay | ||
| import kotlinx.coroutines.runBlocking | ||
| import kotlinx.coroutines.withTimeout | ||
| import org.junit.Assert.assertNotNull | ||
| import org.junit.Rule | ||
| import org.junit.Test | ||
| import org.junit.runner.RunWith | ||
| import org.mozilla.firefox.vpn.splash.SplashActivity | ||
|
|
||
| @LargeTest | ||
| @RunWith(AndroidJUnit4::class) | ||
| class PkceRegressionTest { | ||
|
|
||
| @Rule | ||
| @JvmField | ||
| val splashActivityTestRule = ActivityTestRule(SplashActivity::class.java) | ||
|
|
||
| @Rule | ||
| @JvmField | ||
| val intentReceiverActivityTestRule = ActivityTestRule(IntentReceiverActivity::class.java) | ||
|
|
||
| @Test | ||
| @FlakyTest | ||
| /** | ||
| * Flaky for two reasons. 1) this needs to touch the network, 2) Espresso tests are just flaky. | ||
| */ | ||
| fun pkce_regression_test() { | ||
| // Simulate response from user logging in at: | ||
| // "https://stage-vpn.guardian.nonprod.cloudops.mozgcp.net/api/v2/vpn/login/android?code_challenge=fx-O4_N_sfGrXxLgDkByfVNgZUPCI1s5PqWp8k1fG8M=&code_challenge_method=S256" | ||
| val authCode = "d60b4de6f4a8a6e2228e82b328729d9cc1666b96a1f7a5202fdc563c925bb7a3ea3f4efa1ef3c37d" | ||
| val intentUri = Uri.parse( | ||
| "https://stage-vpn.guardian.nonprod.cloudops.mozgcp.net" + | ||
| "/vpn/client/login/success?" + | ||
| "code=$authCode" + | ||
| "#Intent;category=android.intent.category.BROWSABLE;" + | ||
| "launchFlags=0x14000000;" + | ||
| "component=org.mozilla.firefox.vpn.debug/org.mozilla.firefox.vpn.IntentReceiverActivity;" + | ||
| "i.org.chromium.chrome.browser.referrer_id=18;" + | ||
| "S.com.android.browser.application_id=com.android.chrome;end" | ||
| ) | ||
| val intent = Intent("android.intent.action.VIEW", intentUri) | ||
|
|
||
| val receivedCode = CompletableDeferred<AuthCode>() | ||
| IntentReceiverActivity.setAuthCodeReceivedDeferred(receivedCode) | ||
|
|
||
| // IntentReceiverActivity launched with the above auth code | ||
| intentReceiverActivityTestRule.launchActivity(intent) | ||
|
|
||
| // assert an auth code was received | ||
| runBlocking { | ||
| withTimeout(5_000) { | ||
| assertNotNull(receivedCode.await()) | ||
| } | ||
| } | ||
|
|
||
| runBlocking { delay(1_000) } | ||
|
|
||
| // assert onboarding screen still shown, login did not proceed | ||
| onView(withId(R.id.auth_btn)).check(matches(isDisplayed())) | ||
| } | ||
| } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
49 changes: 49 additions & 0 deletions
49
app/src/main/java/org/mozilla/firefox/vpn/IntentReceiverActivity.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
|
||
| package org.mozilla.firefox.vpn | ||
|
|
||
| import android.content.Intent | ||
| import android.os.Bundle | ||
| import androidx.appcompat.app.AppCompatActivity | ||
| import kotlinx.coroutines.CompletableDeferred | ||
| import org.mozilla.firefox.vpn.ext.toCode | ||
| import org.mozilla.firefox.vpn.user.domain.AuthToken | ||
|
|
||
| /** | ||
| * Code that is included in the `verify` request that is used to retrieve an [AuthToken]. | ||
| */ | ||
| typealias AuthCode = String | ||
|
|
||
| /** | ||
| * Abstraction that handles incoming [Intent]s for use elsewhere in the app. After | ||
| * processing an [Intent], this activity will finish itself. | ||
| */ | ||
| open class IntentReceiverActivity : AppCompatActivity() { | ||
|
|
||
| companion object { | ||
|
|
||
| /** | ||
| * Set a [CompletableDeferred] that will be completed with an [AuthCode] the next | ||
| * time one is received. Note that this is not guaranteed to ever complete. | ||
| * | ||
| * This is static because the alternative was a similarly bad practice: weaving | ||
| * the reference through many layers of Android framework code. | ||
| */ | ||
| fun setAuthCodeReceivedDeferred(authCodeReceived: CompletableDeferred<AuthCode>) { | ||
| _authCodeReceived = authCodeReceived | ||
| } | ||
| private var _authCodeReceived: CompletableDeferred<AuthCode> = CompletableDeferred() | ||
| } | ||
|
|
||
| override fun onCreate(savedInstanceState: Bundle?) { | ||
| super.onCreate(savedInstanceState) | ||
|
|
||
| intent?.data?.toCode()?.let { authCode -> | ||
| _authCodeReceived.complete(authCode) | ||
| } | ||
|
|
||
| finish() | ||
| } | ||
| } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
57 changes: 57 additions & 0 deletions
57
app/src/main/java/org/mozilla/firefox/vpn/crypto/AuthCodeHelper.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
|
||
| package org.mozilla.firefox.vpn.crypto | ||
|
|
||
| import android.util.Base64 | ||
| import java.security.MessageDigest | ||
| import java.security.SecureRandom | ||
| import kotlin.random.Random | ||
| import kotlin.random.asKotlinRandom | ||
|
|
||
| /** | ||
| * PKCE code verifier. | ||
| */ | ||
| typealias CodeVerifier = String | ||
| /** | ||
| * PKCE code challenge. | ||
| */ | ||
| typealias CodeChallenge = String | ||
|
|
||
| private const val CHALLENGE_FLAGS = Base64.URL_SAFE or Base64.NO_WRAP | ||
|
|
||
| /** | ||
| * Contains utility functions for generating secure codes. | ||
| */ | ||
| object AuthCodeHelper { | ||
|
|
||
| /** | ||
| * Returns a cryptographically random key. Sent during token request as part of PKCE. | ||
| */ | ||
| fun generateCodeVerifier(random: Random = SecureRandom().asKotlinRandom()): CodeVerifier { | ||
| val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') + listOf('-', '_', '.', '~') | ||
|
|
||
| val size = (43..128).random(random) | ||
|
|
||
| val sb = StringBuilder() | ||
| for (i in 1..size) { | ||
| sb.append(allowedChars.random(random)) | ||
| } | ||
| return sb.toString() | ||
| } | ||
|
|
||
| /** | ||
| * Returns a SHA-256 encoded hash based on [verifier]. Used to retrieve a token as | ||
| * part of PKCE. | ||
| */ | ||
| @Synchronized // MessageDigest is not thread-safe | ||
| fun generateCodeChallenge(verifier: CodeVerifier): CodeChallenge { | ||
| val bytes = verifier.toByteArray(Charsets.US_ASCII) | ||
| val messageDigest = MessageDigest.getInstance("SHA-256") | ||
| messageDigest.update(bytes, 0, bytes.size) | ||
| val digest = messageDigest.digest() | ||
|
|
||
| return Base64.encodeToString(digest, CHALLENGE_FLAGS) | ||
| } | ||
| } |
47 changes: 47 additions & 0 deletions
47
app/src/main/java/org/mozilla/firefox/vpn/ext/LiveEvent.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
|
||
| package org.mozilla.firefox.vpn.ext | ||
|
|
||
| import com.hadilq.liveevent.LiveEvent | ||
|
|
||
| /** | ||
| * Adds an event on to the [LiveEvent]. This makes usage more idiomatic by letting | ||
| * [LiveEvent]s be called as functions. | ||
| * | ||
| * EXAMPLE | ||
| * | ||
| * With this extension: | ||
| * ``` | ||
| * promptLogin(info.loginUrl) | ||
| * ``` | ||
| * | ||
| * Without this extension: | ||
| * ``` | ||
| * promptLogin.value = info.loginUrl | ||
| * ``` | ||
| */ | ||
| operator fun <T> LiveEvent<T>.invoke(value: T) { | ||
| this.value = value | ||
| } | ||
|
|
||
| /** | ||
| * Adds an event on to the [LiveEvent]. This makes usage more idiomatic by letting | ||
| * [LiveEvent]s be called as functions. | ||
| * | ||
| * EXAMPLE | ||
| * | ||
| * With this extension: | ||
| * ``` | ||
| * promptLogin() | ||
| * ``` | ||
| * | ||
| * Without this extension: | ||
| * ``` | ||
| * promptLogin.value = Unit | ||
| * ``` | ||
| */ | ||
| operator fun LiveEvent<Unit>.invoke() { | ||
| this.value = Unit | ||
| } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
|
||
| package org.mozilla.firefox.vpn.ext | ||
|
|
||
| import android.net.Uri | ||
| import org.mozilla.firefox.vpn.AuthCode | ||
|
|
||
| private const val CODE_QUERY_PARAM = "code" | ||
|
|
||
| private val ALLOWED_CODE_CHARS = (('0'..'9') + ('a'..'f')).toSet() | ||
|
|
||
| fun Uri.toCode(): AuthCode? { | ||
| val code = getQueryParameter(CODE_QUERY_PARAM) | ||
|
|
||
| return if ( | ||
| code == null || | ||
| code.length != 80 || | ||
| code.any { char -> !ALLOWED_CODE_CHARS.contains(char) } | ||
| ) { | ||
| null | ||
| } else { | ||
| code | ||
| } | ||
| } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.