Skip to content
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
41 changes: 40 additions & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,43 @@ jobs:
if: always()
with:
name: reports
path: guardian/build/reports/reports.html
path: guardian/build/reports/reports.html

sample:
name: Build sample app
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # pin@v7.0.1

- uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # pin@v6.1.0
with:
distribution: temurin
java-version-file: '.java-version'

- uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # pin@v6.4.0

- uses: ruby/setup-ruby@7f797815d8adceb1c412e7abefdd1ab4abc47a5f # pin@v1.335.0
with:
bundler: 'Gemfile.lock'

- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # pin@v6.1.0
with:
path: ~/vendor/bundle
key: bundle-v1-${{ hashFiles('fastlane/Gemfile.lock') }}
restore-keys: bundle-v1-

- name: Install Ruby dependencies
run: bundle check || bundle install --jobs 4 --retry 3
env:
BUNDLE_GEMFILE: ${{ github.workspace }}/fastlane/Gemfile
BUNDLE_PATH: ~/vendor/bundle

# Runs in parallel with the test job and reports its own result, so a lint or
# coverage failure can't hide a broken sample (and vice versa). Fails the PR if
# the sample stops compiling against the SDK sources under review.
- name: Build sample app
run: bundle exec fastlane android build_sample
env:
BUNDLE_GEMFILE: ${{ github.workspace }}/fastlane/Gemfile
BUNDLE_PATH: ~/vendor/bundle
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,12 @@ Auth0 helps you to:

## Development

### Sample app

The `app` module is a sample Guardian authenticator built against the local SDK. It is
compiled on every PR to `master` (the `sample` job in `pr.yml`). See [app/README.md](app/README.md) for how
to point it at your tenant and Firebase project.

### CI / Toolchain maintenance

Runtime versions are pinned in `.java-version` and `.ruby-version`. The
Expand Down
2 changes: 2 additions & 0 deletions app/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
/build
# Real Firebase config for a test project; overrides the placeholder app/google-services.json
/src/debug/google-services.json
138 changes: 133 additions & 5 deletions app/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,138 @@
# GuardianSDK sample app

## Running
A minimal Guardian authenticator built against the local `:guardian` module. It enrolls the device
by scanning a Guardian QR code, receives push notifications through FCM, and lets you allow or reject
login requests. It is also the host app for the SDK's integration tests.

Before running the sample app please configure your Firebase app and Guardian URL properly.
The sample is compiled on every PR to `master` and in the merge queue (the `sample` job in
`pr.yml`), so it can't silently break. It is never
published.

You should place the `google-services.json` file (required for FCM push notifications) inside your Android app module root. Typically at` at `MyApplication/app/google-services.json`.
## Prerequisites

The Guardian URL can be changed at `src/main/res/values/guardian.xml`. It should look like
`https://<YOUR_TENANT>.guardian.auth0.com`.
- JDK 17 or newer (see `.java-version`)
- Android SDK platforms 36 (the app) and 33 (the `:guardian` module). Gradle downloads them if you
accept the licences, otherwise install them with the SDK Manager. Point Gradle at the SDK with
`ANDROID_HOME` or a `local.properties` file (`sdk.dir=…`).
- A device or emulator running Android 7.0 (API 24) or newer **with Google Play services**. On an
emulator pick a "Google APIs" or "Google Play" system image, because FCM doesn't work without it.

## Configuration

### Guardian tenant

The tenant URL is compiled into `BuildConfig.TENANT_URL`. It defaults to
`https://guardian-demo.us.auth0.com`. Override it with any of these, listed by precedence:

1. A Gradle property on the command line:
`./gradlew :app:installDebug -PguardianTenantUrl=https://<your-tenant-domain>`
2. The same property in `~/.gradle/gradle.properties`, so it stays out of the repo:
`guardianTenantUrl=https://<your-tenant-domain>`
3. The `GUARDIAN_TENANT_URL` environment variable, which is handy in CI. Android Studio started from
the macOS Dock doesn't inherit shell variables; use the Gradle property there.

Any other way of setting the Gradle project property (for example the
`ORG_GRADLE_PROJECT_guardianTenantUrl` environment variable) also works and beats
`GUARDIAN_TENANT_URL`, which is only the fallback.

The value must be an `https://` URL. Because it is compiled in, **rebuild and reinstall after
changing it**. It must be the domain the enrollment link is issued from (your tenant's canonical or
custom domain), otherwise enrollment fails.

The tenant must have the **Push Notification using Auth0 Guardian** MFA factor enabled, with push
credentials for the Firebase project described below. In the Auth0 Dashboard open
**Security → Multi-factor Auth → Push Notification using Auth0 Guardian** and, under **Custom App**,
configure the Android push provider: either direct FCM, which needs a service-account key JSON
from the Firebase project (**Project settings → Service accounts**), or Amazon SNS. Labels may differ
slightly; see Auth0's "Configure Push Notifications for MFA" documentation.

### Firebase (`google-services.json`)

The committed `app/google-services.json` is a **placeholder with dummy data**. It stays in the repo
on purpose, so a clean clone and CI can build the app. Never commit a real config over it.

With the placeholder the app builds, installs and launches, but it can't obtain an FCM token. It
typically shows "Could not obtain FCM token" and closes when you dismiss the dialog, so the
**Enroll** button on the main screen is never reachable. Enrollment without a Firebase project is
only possible through the test hook below.

To receive real push notifications:

1. In the [Firebase console](https://console.firebase.google.com), create a test project, or use an
existing one, and add an Android app with package name **`com.auth0.guardian.sample`** exactly
(the build fails if the file has no client for that package).
2. Download its `google-services.json` and save it as **`app/src/debug/google-services.json`**.
The Google Services plugin prefers this file over the placeholder for debug builds, and the path is
git-ignored so the real config is never committed. Release builds still use the placeholder.
3. Give the tenant this project's FCM credentials (see above).

## Build and run

```sh
./gradlew :app:assembleDebug # build only
./gradlew :app:installDebug -PguardianTenantUrl=https://<your-tenant-domain>
```

or open the project in Android Studio and run the `app` configuration.

### Manual test flow

1. **Set up a tenant user that must enroll in push MFA.** Create a test user, then in
**Security → Multi-factor Auth** enable Push and set the policy to require MFA ("Always").
2. Launch the app. On Android 13 and newer, grant the notification permission. The FCM token is shown
while no account is enrolled.
3. Log in as the test user (for example with the database connection's **Try** button in the
Dashboard). Universal Login shows an enrollment QR code. Alternatively create an enrollment
ticket (`POST /api/v2/guardian/enrollments/ticket`, or "Send enrollment invitation" on the user's
page) and open the ticket URL.
4. In the app tap **Enroll**, grant the camera permission, and scan the QR code. If scanning isn't
practical, tap **Enter enrollment link**, paste the content of the QR code (an `otpauth://…`
link) and tap **Enroll** in the dialog. The button is also on the camera permission screen, so
you can enroll after denying the camera. On a device without a camera the dialog opens
automatically; emulators usually have a virtual camera, so tap the button there. Pasted links
take the same path as scanned codes.
5. Log in again as the same user to trigger an MFA push.
- With the app open (visible), the authentication request opens directly.
- With the app in the background or closed, a notification is shown: tap it. This needs the
notification permission; without it background pushes are dropped.
6. Tap **Allow** or **Deny**, then check the login result in the browser.
7. Tap **Unenroll** to delete the enrollment.

## Integration test hooks

- **Tenant URL**: also passed to instrumentation as the runner argument `tenantUrl`
(`InstrumentationRegistry.getArguments().getString("tenantUrl")`), and available as
`BuildConfig.TENANT_URL`. The module has no `androidTest` setup yet; the integration tests will
need to add `testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"` and the
`androidx.test` dependencies. Gradle only injects the argument for `connectedAndroidTest`; with a
raw `adb shell am instrument` pass `-e tenantUrl <url>`.
- **Skip QR scanning**: start `EnrollActivity` with the extra
`com.auth0.guardian.sample.EnrollActivity.ENROLLMENT_DATA` set to the enrollment URI, which is the
content of the QR code. Enrollment then starts immediately without the camera, the successful
enrollment is saved (so the next `MainActivity` launch is enrolled and handles pushes), and the
activity finishes with `RESULT_OK` and the enrollment as a result extra. On failure it shows the
error and finishes with `RESULT_CANCELED`. The device name and FCM token extras are still
required. Use `EnrollActivity.getStartIntent(context, deviceName, fcmToken, enrollmentData)` from
tests in the same package (the extra keys other than `ENROLLMENT_DATA` are private). For manual
testing use **Enter enrollment link** instead. `EnrollActivity` is **not exported**, so
`adb shell am start` can't launch it as a normal user; this hook is for instrumentation only.
- **Views**: each control has an ID, and the interactive ones also a content description. Prefer the
IDs as the locator; the descriptions are for tests that locate views by accessibility label. Each
description contains the visible label of its button (for example "Deny authentication request"
for **Deny**), so they also work with Voice Access.

| Screen | Control | ID | Content description |
| --- | --- | --- | --- |
| Main | Enroll button | `enrollButton` | Enroll |
| Main | Unenroll button | `unenrollButton` | Unenroll |
| Main | FCM token | `fcmTokenText` | (none, uses its text) |
| Main | TOTP code | `otpView` | One-time code |
| Enroll | QR scanner | `scanner` | Guardian QR code scanner |
| Enroll | Scan hint (also the no-camera message) | `scanHintText` | (none, uses its text) |
| Enroll | Enter enrollment link button | `enterLinkButton` | Enter enrollment link |
| Enroll | Same button on the camera permission screen | `enterLinkPermissionButton` | Enter enrollment link |
| Enroll | Enrollment link input (dialog) | `enrollmentLinkInput` | Enrollment link |
| Enroll | Camera permission explanation | `permissionMessageText` | (none, uses its text) |
| Enroll | Request camera permission | `requestPermissionButton` | Request permission to use the camera |
| Notification | Allow button (label "Allow") | `allowButton` | Allow authentication request |
| Notification | Reject button (label "Deny") | `rejectButton` | Deny authentication request |
49 changes: 39 additions & 10 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
apply plugin: 'com.android.application'
apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'com.google.gms.google-services'

// Guardian tenant the sample talks to. Override per machine or per CI run with
// -PguardianTenantUrl=https://<tenant>.<region>.auth0.com, an entry in
// ~/.gradle/gradle.properties, or the GUARDIAN_TENANT_URL environment variable.
def guardianTenantUrl = project.findProperty('guardianTenantUrl') ?:
System.getenv('GUARDIAN_TENANT_URL') ?:
'https://guardian-demo.us.auth0.com'
// It is compiled into a Java string literal, so reject anything that would break it.
if (!(guardianTenantUrl ==~ /^https:\/\/[^"\\\s]+$/)) {
throw new GradleException("guardianTenantUrl must be an https URL, got: '${guardianTenantUrl}'")
}

android {
compileSdkVersion 33
compileSdk 36

buildFeatures {
// Explicit: gradle.properties' android.defaults.buildfeatures.buildconfig is deprecated
buildConfig true
}

defaultConfig {
applicationId "com.auth0.guardian.sample"
minSdkVersion 22
targetSdkVersion 33
minSdkVersion 24
targetSdkVersion 36
versionCode 1
versionName "1.0"

buildConfigField "String", "TENANT_URL", "\"${guardianTenantUrl}\""
// Exposed to instrumentation tests via InstrumentationRegistry.getArguments().
testInstrumentationRunnerArguments tenantUrl: guardianTenantUrl
}
buildTypes {
release {
Expand All @@ -18,28 +39,36 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
namespace 'com.auth0.guardian.sample'
}

kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}

dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation project(':guardian')

implementation platform('com.google.firebase:firebase-bom:22.1.0')
implementation platform('com.google.firebase:firebase-bom:35.1.0')
// FCM
implementation 'com.google.firebase:firebase-core'
implementation 'com.google.firebase:firebase-messaging'

implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.appcompat:appcompat:1.8.0'
implementation 'androidx.activity:activity:1.13.0'
implementation 'androidx.core:core:1.18.0'
implementation 'com.google.android.material:material:1.14.0'
// GSON
implementation 'com.google.code.gson:gson:2.9.1'
implementation 'com.google.code.gson:gson:2.15.0'
// EventBus
implementation 'org.greenrobot:eventbus:3.3.1'
// ZXing QR decoder deps
implementation 'com.google.zxing:core:3.5.0'
implementation 'com.google.zxing:core:3.5.4'

implementation 'org.bouncycastle:bcprov-jdk18on:1.85'
}
6 changes: 3 additions & 3 deletions app/google-services.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/*
** THIS IS A SAMPLE CONFIGURATION FILE WITH DUMMY DATA
**
** Replace this file with a valid google-services.json
** downloaded from the Firebase Console with your project details.
** See: https://console.firebase.google.com
** Kept on purpose so a clean clone and CI can build the app. Do not replace or
** commit a real config here: put yours in app/src/debug/google-services.json
** (git-ignored). See app/README.md.
*/

{
Expand Down
1 change: 0 additions & 1 deletion app/src/debug/res/values/.gitignore

This file was deleted.

6 changes: 5 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

<!-- Backup and device transfer are off: the enrollment, including its private key, must
stay on this device. -->
<application
android:allowBackup="true"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
Expand Down
Loading
Loading