311 lines
9.0 KiB
Java
311 lines
9.0 KiB
Java
package com.jc.jcfw.appauth;
|
|
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.SharedPreferences;
|
|
import android.content.res.Resources;
|
|
import android.net.Uri;
|
|
import android.text.TextUtils;
|
|
|
|
import com.fitchgc.jcwallet.R;
|
|
|
|
import net.openid.appauth.connectivity.ConnectionBuilder;
|
|
import net.openid.appauth.connectivity.DefaultConnectionBuilder;
|
|
|
|
import org.json.JSONException;
|
|
import org.json.JSONObject;
|
|
|
|
import java.io.IOException;
|
|
import java.lang.ref.WeakReference;
|
|
import java.nio.charset.Charset;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import okio.Buffer;
|
|
import okio.BufferedSource;
|
|
import okio.Okio;
|
|
|
|
public final class JConfiguration {
|
|
|
|
private static final String TAG = "Configuration";
|
|
|
|
private static final String PREFS_NAME = "config";
|
|
private static final String KEY_LAST_HASH = "lastHash";
|
|
|
|
private static WeakReference<JConfiguration> sInstance = new WeakReference<>(null);
|
|
|
|
private final Context mContext;
|
|
private final SharedPreferences mPrefs;
|
|
private final Resources mResources;
|
|
|
|
private JSONObject mConfigJson;
|
|
private String mConfigHash;
|
|
private String mConfigError;
|
|
|
|
private String mClientId;
|
|
private String mScope;
|
|
private Uri mRedirectUri;
|
|
private Uri mEndSessionRedirectUri;
|
|
private Uri mDiscoveryUri;
|
|
private Uri mAuthEndpointUri;
|
|
private Uri mTokenEndpointUri;
|
|
private Uri mEndSessionEndpoint;
|
|
private Uri mRegistrationEndpointUri;
|
|
private Uri mUserInfoEndpointUri;
|
|
private boolean mHttpsRequired;
|
|
|
|
public static JConfiguration getInstance(Context context) {
|
|
JConfiguration config = sInstance.get();
|
|
if (config == null) {
|
|
config = new JConfiguration(context);
|
|
sInstance = new WeakReference<JConfiguration>(config);
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
public JConfiguration(Context context) {
|
|
mContext = context;
|
|
mPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
mResources = context.getResources();
|
|
|
|
try {
|
|
readConfiguration();
|
|
} catch (InvalidConfigurationException ex) {
|
|
mConfigError = ex.getMessage();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Indicates whether the configuration has changed from the last known valid state.
|
|
*/
|
|
public boolean hasConfigurationChanged() {
|
|
String lastHash = getLastKnownConfigHash();
|
|
return !mConfigHash.equals(lastHash);
|
|
}
|
|
|
|
/**
|
|
* Indicates whether the current configuration is valid.
|
|
*/
|
|
public boolean isValid() {
|
|
return mConfigError == null;
|
|
}
|
|
|
|
/**
|
|
* Returns a description of the configuration error, if the configuration is invalid.
|
|
*/
|
|
@Nullable
|
|
public String getConfigurationError() {
|
|
return mConfigError;
|
|
}
|
|
|
|
/**
|
|
* Indicates that the current configuration should be accepted as the "last known valid"
|
|
* configuration.
|
|
*/
|
|
public void acceptConfiguration() {
|
|
mPrefs.edit().putString(KEY_LAST_HASH, mConfigHash).apply();
|
|
}
|
|
|
|
@Nullable
|
|
public String getClientId() {
|
|
return mClientId;
|
|
}
|
|
|
|
@NonNull
|
|
public String getScope() {
|
|
return mScope;
|
|
}
|
|
|
|
@NonNull
|
|
public Uri getRedirectUri() {
|
|
return mRedirectUri;
|
|
}
|
|
|
|
@Nullable
|
|
public Uri getDiscoveryUri() {
|
|
return mDiscoveryUri;
|
|
}
|
|
|
|
@Nullable
|
|
public Uri getEndSessionRedirectUri() { return mEndSessionRedirectUri; }
|
|
|
|
@Nullable
|
|
public Uri getAuthEndpointUri() {
|
|
return mAuthEndpointUri;
|
|
}
|
|
|
|
@Nullable
|
|
public Uri getTokenEndpointUri() {
|
|
return mTokenEndpointUri;
|
|
}
|
|
|
|
@Nullable
|
|
public Uri getEndSessionEndpoint() {
|
|
return mEndSessionEndpoint;
|
|
}
|
|
|
|
@Nullable
|
|
public Uri getRegistrationEndpointUri() {
|
|
return mRegistrationEndpointUri;
|
|
}
|
|
|
|
@Nullable
|
|
public Uri getUserInfoEndpointUri() {
|
|
return mUserInfoEndpointUri;
|
|
}
|
|
|
|
public boolean isHttpsRequired() {
|
|
return mHttpsRequired;
|
|
}
|
|
|
|
public ConnectionBuilder getConnectionBuilder() {
|
|
if (isHttpsRequired()) {
|
|
return DefaultConnectionBuilder.INSTANCE;
|
|
}
|
|
return ConnectionBuilderForTesting.INSTANCE;
|
|
}
|
|
|
|
private String getLastKnownConfigHash() {
|
|
return mPrefs.getString(KEY_LAST_HASH, null);
|
|
}
|
|
|
|
private void readConfiguration() throws InvalidConfigurationException {
|
|
BufferedSource configSource =
|
|
Okio.buffer(Okio.source(mResources.openRawResource(R.raw.auth_config)));
|
|
Buffer configData = new Buffer();
|
|
try {
|
|
configSource.readAll(configData);
|
|
mConfigJson = new JSONObject(configData.readString(Charset.forName("UTF-8")));
|
|
} catch (IOException ex) {
|
|
throw new InvalidConfigurationException(
|
|
"Failed to read configuration: " + ex.getMessage());
|
|
} catch (JSONException ex) {
|
|
throw new InvalidConfigurationException(
|
|
"Unable to parse configuration: " + ex.getMessage());
|
|
}
|
|
|
|
mConfigHash = configData.sha256().base64();
|
|
mClientId = getConfigString("client_id");
|
|
mScope = getRequiredConfigString("authorization_scope");
|
|
mRedirectUri = getRequiredConfigUri("redirect_uri");
|
|
mEndSessionRedirectUri = getRequiredConfigUri("end_session_redirect_uri");
|
|
|
|
if (!isRedirectUriRegistered()) {
|
|
throw new InvalidConfigurationException(
|
|
"redirect_uri is not handled by any activity in this app! "
|
|
+ "Ensure that the appAuthRedirectScheme in your build.gradle file "
|
|
+ "is correctly configured, or that an appropriate intent filter "
|
|
+ "exists in your app manifest.");
|
|
}
|
|
|
|
if (getConfigString("discovery_uri") == null) {
|
|
mAuthEndpointUri = getRequiredConfigWebUri("authorization_endpoint_uri");
|
|
|
|
mTokenEndpointUri = getRequiredConfigWebUri("token_endpoint_uri");
|
|
mUserInfoEndpointUri = getRequiredConfigWebUri("user_info_endpoint_uri");
|
|
mEndSessionEndpoint = getRequiredConfigUri("end_session_endpoint");
|
|
|
|
if (mClientId == null) {
|
|
mRegistrationEndpointUri = getRequiredConfigWebUri("registration_endpoint_uri");
|
|
}
|
|
} else {
|
|
mDiscoveryUri = getRequiredConfigWebUri("discovery_uri");
|
|
}
|
|
|
|
mHttpsRequired = mConfigJson.optBoolean("https_required", true);
|
|
}
|
|
|
|
@Nullable
|
|
String getConfigString(String propName) {
|
|
String value = mConfigJson.optString(propName);
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
|
|
value = value.trim();
|
|
if (TextUtils.isEmpty(value)) {
|
|
return null;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
@NonNull
|
|
private String getRequiredConfigString(String propName)
|
|
throws InvalidConfigurationException {
|
|
String value = getConfigString(propName);
|
|
if (value == null) {
|
|
throw new InvalidConfigurationException(
|
|
propName + " is required but not specified in the configuration");
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
@NonNull
|
|
Uri getRequiredConfigUri(String propName)
|
|
throws InvalidConfigurationException {
|
|
String uriStr = getRequiredConfigString(propName);
|
|
Uri uri;
|
|
try {
|
|
uri = Uri.parse(uriStr);
|
|
} catch (Throwable ex) {
|
|
throw new InvalidConfigurationException(propName + " could not be parsed", ex);
|
|
}
|
|
|
|
if (!uri.isHierarchical() || !uri.isAbsolute()) {
|
|
throw new InvalidConfigurationException(
|
|
propName + " must be hierarchical and absolute");
|
|
}
|
|
|
|
if (!TextUtils.isEmpty(uri.getEncodedUserInfo())) {
|
|
throw new InvalidConfigurationException(propName + " must not have user info");
|
|
}
|
|
|
|
if (!TextUtils.isEmpty(uri.getEncodedQuery())) {
|
|
throw new InvalidConfigurationException(propName + " must not have query parameters");
|
|
}
|
|
|
|
if (!TextUtils.isEmpty(uri.getEncodedFragment())) {
|
|
throw new InvalidConfigurationException(propName + " must not have a fragment");
|
|
}
|
|
|
|
return uri;
|
|
}
|
|
|
|
Uri getRequiredConfigWebUri(String propName)
|
|
throws InvalidConfigurationException {
|
|
Uri uri = getRequiredConfigUri(propName);
|
|
String scheme = uri.getScheme();
|
|
if (TextUtils.isEmpty(scheme) || !("http".equals(scheme) || "https".equals(scheme))) {
|
|
throw new InvalidConfigurationException(
|
|
propName + " must have an http or https scheme");
|
|
}
|
|
|
|
return uri;
|
|
}
|
|
|
|
private boolean isRedirectUriRegistered() {
|
|
// ensure that the redirect URI declared in the configuration is handled by some activity
|
|
// in the app, by querying the package manager speculatively
|
|
Intent redirectIntent = new Intent();
|
|
redirectIntent.setPackage(mContext.getPackageName());
|
|
redirectIntent.setAction(Intent.ACTION_VIEW);
|
|
redirectIntent.addCategory(Intent.CATEGORY_BROWSABLE);
|
|
redirectIntent.setData(mRedirectUri);
|
|
|
|
return !mContext.getPackageManager().queryIntentActivities(redirectIntent, 0).isEmpty();
|
|
}
|
|
|
|
public static final class InvalidConfigurationException extends Exception {
|
|
InvalidConfigurationException(String reason) {
|
|
super(reason);
|
|
}
|
|
|
|
InvalidConfigurationException(String reason, Throwable cause) {
|
|
super(reason, cause);
|
|
}
|
|
}
|
|
}
|