Skip to content

Supporting App2App OAuth Flows #622

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
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
Prev Previous commit
fixed formatting issues
Signed-off-by: Fabian Hauck <[email protected]>
  • Loading branch information
fabian-hk committed Nov 18, 2020
commit 300a91d24c2f085889cdd336a95031d71acd1257
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
/*
* Copyright 2016 The AppAuth for Android Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.openid.appauth.app2app;

import android.util.Base64;

import androidx.annotation.NonNull;

import org.json.JSONArray;
@@ -12,22 +25,25 @@

final class CertificateFingerprintEncoding {

private CertificateFingerprintEncoding() {
}
private static final int DECIMAL = 10;
private static final int HEXADECIMAL = 16;
private static final int HALF_BYTE = 4;

private CertificateFingerprintEncoding() {}

/**
* This method takes the certificate fingerprints from the '/.well-known/assetlinks.json' file
* and decodes it in the correct way to compare the hashes with the ones found on the device.
*/
@NonNull
protected static Set<String> certFingerprintsToDecodedString(
@NonNull JSONArray certFingerprints) {
@NonNull JSONArray certFingerprints) {
Set<String> hashes = new HashSet<>();

for (int i = 0; i < certFingerprints.length(); i++) {
try {
byte[] byteArray = hexStringToByteArray(certFingerprints.get(i).toString());
String str = Base64.encodeToString(byteArray, 10);
String str = Base64.encodeToString(byteArray, DECIMAL);
hashes.add(str);
} catch (JSONException e) {
e.printStackTrace();
@@ -39,33 +55,31 @@ protected static Set<String> certFingerprintsToDecodedString(

/**
* This method converts a hex string that is separated by colons into a ByteArray.
* <p>
* Example hexString: 4F:69:88:01:...
*
* <p>Example hexString: 4F:69:88:01:...
*/
@NonNull
private static byte[] hexStringToByteArray(@NonNull String hexString) {
String[] hexValues = hexString.split(":");
byte[] byteArray = new byte[hexValues.length];
String str;
int b = 0;
int tmp = 0;

for (int i = 0; i < hexValues.length; ++i) {
str = hexValues[i];
b = 0;
b = hexValue(str.charAt(0));
b <<= 4;
b |= hexValue(str.charAt(1));
byteArray[i] = (byte) b;
tmp = 0;
tmp = hexValue(str.charAt(0));
tmp <<= HALF_BYTE;
tmp |= hexValue(str.charAt(1));
byteArray[i] = (byte) tmp;
}

return byteArray;
}

/**
* Converts a single hex digit into its decimal value.
*/
/** Converts a single hex digit into its decimal value. */
private static int hexValue(char hexChar) {
int digit = Character.digit(hexChar, 16);
int digit = Character.digit(hexChar, HEXADECIMAL);
if (digit < 0) {
throw new IllegalArgumentException("Invalid hex char " + hexChar);
} else {
36 changes: 24 additions & 12 deletions library/java/net/openid/appauth/app2app/RedirectSession.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
/*
* Copyright 2016 The AppAuth for Android Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.openid.appauth.app2app;

import android.content.Context;
import android.net.Uri;

import androidx.annotation.NonNull;

import java.util.Set;
import org.json.JSONArray;

/**
* Class to hold all important information to perform a secure redirection.
*/
import java.util.Set;

/** Class to hold all important information to perform a secure redirection. */
class RedirectSession {

private Context mContext;
@@ -19,9 +31,9 @@ class RedirectSession {
private Set<String> mBaseCertFingerprints;
private JSONArray mAssetLinksFile = null;

protected RedirectSession(@NonNull Context mContext, @NonNull Uri mUri) {
this.mContext = mContext;
this.mUri = mUri;
protected RedirectSession(@NonNull Context context, @NonNull Uri uri) {
this.mContext = context;
this.mUri = uri;
}

@NonNull
@@ -55,15 +67,15 @@ protected Set<String> getBaseCertFingerprints() {
return mBaseCertFingerprints;
}

protected void setBaseCertFingerprints(Set<String> mBaseCertFingerprints) {
this.mBaseCertFingerprints = mBaseCertFingerprints;
protected void setBaseCertFingerprints(Set<String> baseCertFingerprints) {
this.mBaseCertFingerprints = baseCertFingerprints;
}

public JSONArray getAssetLinksFile() {
return mAssetLinksFile;
}

public void setAssetLinksFile(JSONArray mAssetLinksFile) {
this.mAssetLinksFile = mAssetLinksFile;
public void setAssetLinksFile(JSONArray assetLinksFile) {
this.mAssetLinksFile = assetLinksFile;
}
}
132 changes: 70 additions & 62 deletions library/java/net/openid/appauth/app2app/SecureRedirection.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
/*
* Copyright 2016 The AppAuth for Android Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.openid.appauth.app2app;

import android.content.Context;
@@ -11,27 +25,23 @@
import android.os.AsyncTask;
import android.util.Pair;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.browser.customtabs.CustomTabsIntent;

import java.io.IOException;
import java.io.InputStream;

import java.net.HttpURLConnection;

import net.openid.appauth.Utils;
import net.openid.appauth.browser.BrowserAllowList;
import net.openid.appauth.browser.BrowserDescriptor;
import net.openid.appauth.browser.BrowserSelector;
import net.openid.appauth.browser.VersionedBrowserMatcher;
import net.openid.appauth.connectivity.DefaultConnectionBuilder;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -40,8 +50,7 @@

public final class SecureRedirection {

private SecureRedirection() {
}
private SecureRedirection() {}

/**
* This method redirects an user securely from one app to another with a given URL. For this to
@@ -52,25 +61,25 @@ public static void secureRedirection(@NonNull Context context, @NonNull Uri uri)
getAssetLinksFile(new RedirectSession(context, uri));
}

/**
* This function retrieves the '/.well-known/assetlinks.json' file from the given domain.
*/
/** This function retrieves the '/.well-known/assetlinks.json' file from the given domain. */
private static void getAssetLinksFile(@NonNull final RedirectSession redirectSession) {
new DownloadAssetLinksFile().execute(redirectSession);
}

private static class DownloadAssetLinksFile extends
AsyncTask<RedirectSession, Void, RedirectSession> {
private static class DownloadAssetLinksFile
extends AsyncTask<RedirectSession, Void, RedirectSession> {

@Override
protected RedirectSession doInBackground(RedirectSession... redirectSessions) {
RedirectSession redirectSession = redirectSessions[0];
Uri uri = Uri.parse(redirectSession.getUri().getScheme()
+ "://"
+ redirectSession.getUri().getHost()
+ ":"
+ redirectSession.getUri().getPort()
+ "/.well-known/assetlinks.json");
Uri uri =
Uri.parse(
redirectSession.getUri().getScheme()
+ "://"
+ redirectSession.getUri().getHost()
+ ":"
+ redirectSession.getUri().getPort()
+ "/.well-known/assetlinks.json");

InputStream is = null;
try {
@@ -97,18 +106,17 @@ protected RedirectSession doInBackground(RedirectSession... redirectSessions) {
protected void onPostExecute(RedirectSession redirectSession) {
if (redirectSession.getAssetLinksFile() != null) {
JSONArray baseCertFingerprints =
findInstalledApp(redirectSession, redirectSession.getAssetLinksFile());
findInstalledApp(redirectSession, redirectSession.getAssetLinksFile());

redirectSession.setBaseCertFingerprints(
CertificateFingerprintEncoding
.certFingerprintsToDecodedString(
baseCertFingerprints));
CertificateFingerprintEncoding.certFingerprintsToDecodedString(
baseCertFingerprints));

doRedirection(redirectSession);
} else {
System.err.println(
"Failed to fetch '/.well-known/assetlinks.json' from domain "
+ "'${redirectSession.uri.host}'\nError: ${error}");
"Failed to fetch '/.well-known/assetlinks.json' from domain "
+ "'${redirectSession.uri.host}'\nError: ${error}");
redirectToWeb(redirectSession.getContext(), redirectSession.getUri());
}
}
@@ -125,9 +133,9 @@ protected void onPostExecute(RedirectSession redirectSession) {
*/
@NonNull
private static JSONArray findInstalledApp(
@NonNull RedirectSession redirectSession, @NonNull JSONArray assetLinks) {
@NonNull RedirectSession redirectSession, @NonNull JSONArray assetLinks) {
Pair<Set<String>, Map<String, JSONArray>> basePair =
getBaseValuesFromAssetLinksFile(assetLinks);
getBaseValuesFromAssetLinksFile(assetLinks);
Set<String> foundPackageNames = getPackageNamesForIntent(redirectSession);

// Intersect the set of installed apps with the set of apps
@@ -156,7 +164,7 @@ private static JSONArray findInstalledApp(
*/
@NonNull
private static Pair<Set<String>, Map<String, JSONArray>> getBaseValuesFromAssetLinksFile(
@NonNull JSONArray assetLinks) {
@NonNull JSONArray assetLinks) {
Set<String> basePackageNames = new HashSet<>();
Map<String, JSONArray> baseCertFingerprints = new HashMap<>();
try {
@@ -193,10 +201,10 @@ private static Set<String> getPackageNamesForIntent(@NonNull RedirectSession red
intent.addCategory(Intent.CATEGORY_BROWSABLE);

List<ResolveInfo> infos =
redirectSession
.getContext()
.getPackageManager()
.queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER);
redirectSession
.getContext()
.getPackageManager()
.queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER);

Set<String> packageNames = new HashSet<>();
for (ResolveInfo info : infos) {
@@ -240,26 +248,26 @@ private static Set<String> getSigningCertificates(@NonNull RedirectSession redir
Signature[] signatures;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
SigningInfo signingInfo =
redirectSession
.getContext()
.getPackageManager()
.getPackageInfo(
redirectSession.getBasePackageName(),
PackageManager.GET_SIGNING_CERTIFICATES)
.signingInfo;
redirectSession
.getContext()
.getPackageManager()
.getPackageInfo(
redirectSession.getBasePackageName(),
PackageManager.GET_SIGNING_CERTIFICATES)
.signingInfo;
signatures = signingInfo.getSigningCertificateHistory();
} else {
signatures =
redirectSession
.getContext()
.getPackageManager()
.getPackageInfo(
redirectSession.getBasePackageName(),
PackageManager.GET_SIGNATURES)
.signatures;
redirectSession
.getContext()
.getPackageManager()
.getPackageInfo(
redirectSession.getBasePackageName(),
PackageManager.GET_SIGNATURES)
.signatures;
}
return BrowserDescriptor.generateSignatureHashes(
signatures, BrowserDescriptor.DIGEST_SHA_256);
signatures, BrowserDescriptor.DIGEST_SHA_256);
} catch (PackageManager.NameNotFoundException excepetion) {
return null;
}
@@ -271,7 +279,7 @@ private static Set<String> getSigningCertificates(@NonNull RedirectSession redir
*/
@VisibleForTesting
public static boolean matchHashes(
@NonNull Set<String> certHashes0, @NonNull Set<String> certHashes1) {
@NonNull Set<String> certHashes0, @NonNull Set<String> certHashes1) {
return certHashes0.containsAll(certHashes1) && certHashes0.size() == certHashes1.size();
}

@@ -288,27 +296,27 @@ public static void redirectToWeb(@NonNull Context context, @NonNull Uri uri) {
* the integrity of this browser. It then opens the given uri in an Android Custom Tab.
*/
public static void redirectToWeb(
@NonNull Context context, @NonNull Uri uri, int additionalFlags, int toolbarColor) {
@NonNull Context context, @NonNull Uri uri, int additionalFlags, int toolbarColor) {
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setToolbarColor(toolbarColor);
CustomTabsIntent customTabsIntent = builder.build();

BrowserDescriptor browserDescriptor =
BrowserSelector.select(
context,
new BrowserAllowList(
VersionedBrowserMatcher.CHROME_CUSTOM_TAB,
VersionedBrowserMatcher.CHROME_BROWSER,
VersionedBrowserMatcher.FIREFOX_CUSTOM_TAB,
VersionedBrowserMatcher.FIREFOX_BROWSER,
VersionedBrowserMatcher.SAMSUNG_CUSTOM_TAB,
VersionedBrowserMatcher.SAMSUNG_BROWSER));
BrowserSelector.select(
context,
new BrowserAllowList(
VersionedBrowserMatcher.CHROME_CUSTOM_TAB,
VersionedBrowserMatcher.CHROME_BROWSER,
VersionedBrowserMatcher.FIREFOX_CUSTOM_TAB,
VersionedBrowserMatcher.FIREFOX_BROWSER,
VersionedBrowserMatcher.SAMSUNG_CUSTOM_TAB,
VersionedBrowserMatcher.SAMSUNG_BROWSER));

if (browserDescriptor != null) {
customTabsIntent
.intent
.setPackage(browserDescriptor.packageName)
.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | additionalFlags);
.intent
.setPackage(browserDescriptor.packageName)
.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | additionalFlags);
customTabsIntent.launchUrl(context, uri);
} else {
Toast.makeText(context, "Could not find a browser", Toast.LENGTH_SHORT).show();
19 changes: 19 additions & 0 deletions library/java/net/openid/appauth/app2app/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2016 The AppAuth for Android Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* This package provides methods to securely redirect a user from one app to another
* in an app2app OAuth 2.0 flow.
*/
package net.openid.appauth.app2app;
86 changes: 35 additions & 51 deletions library/java/net/openid/appauth/browser/BrowserDescriptor.java
Original file line number Diff line number Diff line change
@@ -24,9 +24,7 @@
import java.util.HashSet;
import java.util.Set;

/**
* Represents a browser that may be used for an authorization flow.
*/
/** Represents a browser that may be used for an authorization flow. */
public class BrowserDescriptor {

// See: http://stackoverflow.com/a/2816747
@@ -35,33 +33,27 @@ public class BrowserDescriptor {
public static final String DIGEST_SHA_256 = "SHA-256";
public static final String DIGEST_SHA_512 = "SHA-512";

/**
* The package name of the browser app.
*/
/** The package name of the browser app. */
public final String packageName;

/**
* The set of {@link android.content.pm.Signature signatures} of the browser app,
* which have been hashed with SHA-512, and Base-64 URL-safe encoded.
* The set of {@link android.content.pm.Signature signatures} of the browser app, which have
* been hashed with SHA-512, and Base-64 URL-safe encoded.
*/
public final Set<String> signatureHashes;

/**
* The version string of the browser app.
*/
/** The version string of the browser app. */
public final String version;

/**
* Whether it is intended that the browser will be used via a custom tab.
*/
/** Whether it is intended that the browser will be used via a custom tab. */
public final Boolean useCustomTab;

/**
* Creates a description of a browser from a {@link PackageInfo} object returned from the
* {@link android.content.pm.PackageManager}. The object is expected to include the
* signatures of the app, which can be retrieved with the
* {@link android.content.pm.PackageManager#GET_SIGNATURES GET_SIGNATURES} flag when
* calling {@link android.content.pm.PackageManager#getPackageInfo(String, int)}.
* Creates a description of a browser from a {@link PackageInfo} object returned from the {@link
* android.content.pm.PackageManager}. The object is expected to include the signatures of the
* app, which can be retrieved with the {@link android.content.pm.PackageManager#GET_SIGNATURES
* GET_SIGNATURES} flag when calling {@link
* android.content.pm.PackageManager#getPackageInfo(String, int)}.
*/
public BrowserDescriptor(@NonNull PackageInfo packageInfo, boolean useCustomTab) {
this(
@@ -73,19 +65,16 @@ public BrowserDescriptor(@NonNull PackageInfo packageInfo, boolean useCustomTab)

/**
* Creates a description of a browser from the core properties that are frequently used to
* decide whether a browser can be used for an authorization flow. In most cases, it is
* more convenient to use the other variant of the constructor that consumes a
* {@link PackageInfo} object provided by the package manager.
* decide whether a browser can be used for an authorization flow. In most cases, it is more
* convenient to use the other variant of the constructor that consumes a {@link PackageInfo}
* object provided by the package manager.
*
* @param packageName
* The Android package name of the browser.
* @param signatureHashes
* The set of SHA-512, Base64 url safe encoded signatures for the app. This can be
* generated for a signature by calling {@link #generateSignatureHash(Signature)}.
* @param version
* The version name of the browser.
* @param useCustomTab
* Whether it is intended to use the browser as a custom tab.
* @param packageName The Android package name of the browser.
* @param signatureHashes The set of SHA-512, Base64 url safe encoded signatures for the app.
* This can be generated for a signature by calling {@link
* #generateSignatureHash(Signature)}.
* @param version The version name of the browser.
* @param useCustomTab Whether it is intended to use the browser as a custom tab.
*/
public BrowserDescriptor(
@NonNull String packageName,
@@ -99,16 +88,12 @@ public BrowserDescriptor(
}

/**
* Creates a copy of this browser descriptor, changing the intention to use it as a custom
* tab to the specified value.
* Creates a copy of this browser descriptor, changing the intention to use it as a custom tab
* to the specified value.
*/
@NonNull
public BrowserDescriptor changeUseCustomTab(boolean newUseCustomTabValue) {
return new BrowserDescriptor(
packageName,
signatureHashes,
version,
newUseCustomTabValue);
return new BrowserDescriptor(packageName, signatureHashes, version, newUseCustomTabValue);
}

@Override
@@ -142,39 +127,38 @@ public int hashCode() {
return hash;
}

/**
* Generates a SHA-* hash, Base64 url-safe encoded, from a {@link Signature}.
*/
/** Generates a SHA-* hash, Base64 url-safe encoded, from a {@link Signature}. */
@NonNull
public static String generateSignatureHash(@NonNull Signature signature, @NonNull String digestSHA) {
public static String generateSignatureHash(
@NonNull Signature signature, @NonNull String digestSha) {
try {
MessageDigest digest = MessageDigest.getInstance(digestSHA);
MessageDigest digest = MessageDigest.getInstance(digestSha);
byte[] hashBytes = digest.digest(signature.toByteArray());
return Base64.encodeToString(hashBytes, Base64.URL_SAFE | Base64.NO_WRAP);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(
"Platform does not support" + digestSHA + " hashing");
throw new IllegalStateException("Platform does not support" + digestSha + " hashing");
}
}

/**
* Generates a set of SHA-512, Base64 url-safe encoded signature hashes from the provided
* array of signatures.
* Generates a set of SHA-512, Base64 url-safe encoded signature hashes from the provided array
* of signatures.
*/
@NonNull
public static Set<String> generateSignatureHashes(@NonNull Signature[] signatures) {
return generateSignatureHashes(signatures, DIGEST_SHA_512);
}

/**
* Generates a set of SHA-*, Base64 url-safe encoded signature hashes from the provided
* array of signatures.
* Generates a set of SHA-*, Base64 url-safe encoded signature hashes from the provided array of
* signatures.
*/
@NonNull
public static Set<String> generateSignatureHashes(@NonNull Signature[] signatures, @NonNull String digestSHA) {
public static Set<String> generateSignatureHashes(
@NonNull Signature[] signatures, @NonNull String digestSha) {
Set<String> signatureHashes = new HashSet<>();
for (Signature signature : signatures) {
signatureHashes.add(generateSignatureHash(signature, digestSHA));
signatureHashes.add(generateSignatureHash(signature, digestSha));
}

return signatureHashes;