diff --git a/.gitignore b/.gitignore index 39fb081..6259ca0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,9 @@ -*.iml -.gradle +.gradle/ /local.properties -/.idea/workspace.xml -/.idea/libraries + +.idea/ + .DS_Store -/build -/captures +/build/ +/captures/ .externalNativeBuild diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 5b05804..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 3963879..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index ba113a8..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 7f68460..0000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index e619035..bc60760 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ MobileAuthentication supports Android API 23 and above so AES encryption can be ## Getting Started project build.gradle: -``` +```groovy allprojects { repositories { ... @@ -16,7 +16,7 @@ allprojects { ``` app build.gradle: -``` +```groovy dependencies { implementation "com.github.bcgov:mobile-authentication-android:" } @@ -25,20 +25,34 @@ dependencies { ### Prerequisites A Red Hat Single Sign-On server component that is setup to handle an OAuth2 authorization code flow. -### Android Manifest -You will need to add this to your AndroidManifest and specify what custom schema you're using in your redirectUri. -```xml - - - - - - - - +### Handling Custom Scheme Redirects + +Specify the redirect value by setting the manifest placeholder value `appAuthRedirectScheme` in your project's build.gradle file: + +```groovy +android { + defaultConfig { + ... + + manifestPlaceholders = [ + 'appAuthRedirectScheme': '' + ] + + ... + } +} ``` +Only the scheme is required: + +> 'appAuthRedirectScheme': 'custom-scheme' <- happiness + +Adding any more isn't valid: + +> 'appAuthRedirectScheme': 'custom-scheme://somePath' <- runtime issues + +***Note:*** you were previously required to add RedirectActivity with an intent filter to your application's manifest. This is no longer required because the functionality is handled by this manifest placeholder value. + ## Usage There are four main commands for handling tokens that will be called using the MobileAuthenticationClient class. 1. Authenticate @@ -62,7 +76,7 @@ The parameters needed for the client can all be found on your Red Hat Single Sig The hint param is optional and can be used to directly send the user to the specified login. -We recommened you use a custom application schema for your redirectUri such as ://android +We recommend you use a custom application schema for your redirectUri such as ://android Please use the context of the activity in which you are going to call authenticate to create the client. @@ -100,7 +114,7 @@ If the token is expired and the refresh token is NOT expired the token will auto Exceptions: 1. If there is no token locally a `TokenNotFoundException` will be thrown in the onError of the TokenCallback. This means the user has not yet been authenticated so no token exists locally. -2. If the token's refresh token is expired a `RefreshExpiredException` will be thrown in the onError of the TokenCallback. In this case the user will need to reauthenticate. +2. If the token's refresh token is expired a `RefreshExpiredException` will be thrown in the onError of the TokenCallback. In this case the user will need to re-authenticate. 3. If the token does not have a refresh token then a `NoRefreshTokenException` will be thrown in the onError of the TokenCallback. This means the Token data being returned does not contain the required refreshToken for this lib to work. Calling getToken: @@ -130,7 +144,7 @@ client?.getToken(object: MobileAuthenticationClient.TokenCallback { Refresh token will refresh the locally stored token. Exceptions: -1. If the token's refresh token is expired a `RefreshExpiredException` will be thrown in the onError of the TokenCallback. In this case the user will need to reauthenticate. +1. If the token's refresh token is expired a `RefreshExpiredException` will be thrown in the onError of the TokenCallback. In this case the user will need to re-authenticate. 2. If the token does not have a refresh token then a `NoRefreshTokenException` will be thrown in the onError of the TokenCallback. This means the Token data being returned does not contain the required refreshToken for this lib to work. Calling refreshToken: diff --git a/app/build.gradle b/app/build.gradle index 39c173c..c15782b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,14 +5,29 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { - compileSdkVersion 26 + defaultConfig { + compileSdkVersion rootProject.ext.compileSdkVersion applicationId "ca.bc.gov.mobileauthenticationandroidexample" - minSdkVersion 23 - targetSdkVersion 26 + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + + manifestPlaceholders = [ + /* + Instead of an in AndroidManifest.xml the redirectUri scheme is + defined here. Only give the scheme itself: + + 'appAuthRedirectScheme': 'custom-scheme' <- happiness + + Adding any more isn't valid: + + 'appAuthRedirectScheme': 'custom-scheme://somePath' <- runtime issues + */ + 'appAuthRedirectScheme': 'secure-image' + ] } buildTypes { release { @@ -20,17 +35,18 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + namespace 'ca.bc.gov.mobileauthenticationandroidexample' } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':mobileauthentication') - implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" - implementation "com.android.support:appcompat-v7:26.1.0" - implementation "com.android.support.constraint:constraint-layout:1.0.2" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" + implementation 'androidx.appcompat:appcompat:1.0.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation "junit:junit:4.12" - androidTestImplementation "com.android.support.test:runner:1.0.1" - androidTestImplementation "com.android.support.test.espresso:espresso-core:3.0.1" + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6656cb8..fdd0973 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + @@ -14,24 +13,14 @@ android:supportsRtl="true" android:theme="@style/AppTheme"> - + - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/java/ca/bc/gov/mobileauthenticationandroidexample/MainActivity.kt b/app/src/main/java/ca/bc/gov/mobileauthenticationandroidexample/MainActivity.kt index 398cd7a..ec9d5d5 100644 --- a/app/src/main/java/ca/bc/gov/mobileauthenticationandroidexample/MainActivity.kt +++ b/app/src/main/java/ca/bc/gov/mobileauthenticationandroidexample/MainActivity.kt @@ -1,7 +1,7 @@ package ca.bc.gov.mobileauthenticationandroidexample import android.content.Intent -import android.support.v7.app.AppCompatActivity +import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.util.Log import ca.bc.gov.mobileauthentication.MobileAuthenticationClient diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 092ef3c..70be574 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,9 +1,8 @@ - - + diff --git a/build.gradle b/build.gradle index 2354353..33fd42b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,19 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.2.21' + ext.kotlinVersion = '1.7.10' + + ext.compileSdkVersion = 28 + ext.minSdkVersion = 23 + ext.targetSdkVersion = 26 + repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.1' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'com.android.tools.build:gradle:7.2.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle.properties b/gradle.properties index aac7c9b..9e6fce1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,6 +9,8 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. +android.enableJetifier=true +android.useAndroidX=true org.gradle.jvmargs=-Xmx1536m # When configured, Gradle will run in incubating parallel mode. diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index eae11e6..081467e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Feb 07 13:43:17 PST 2018 +#Mon Sep 19 11:37:24 PDT 2022 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip +zipStoreBase=GRADLE_USER_HOME diff --git a/mobileauthentication/build.gradle b/mobileauthentication/build.gradle index b95f3e8..f2725e0 100644 --- a/mobileauthentication/build.gradle +++ b/mobileauthentication/build.gradle @@ -6,17 +6,14 @@ apply plugin: 'kotlin-android-extensions' android { - compileSdkVersion 26 defaultConfig { - minSdkVersion 23 - targetSdkVersion 26 - compileSdkVersion 26 + compileSdkVersion rootProject.ext.compileSdkVersion + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion buildToolsVersion '26.0.2' - versionCode 1 - versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } @@ -31,25 +28,25 @@ android { } } } + namespace 'ca.bc.gov.mobileauthentication' } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "com.android.support:appcompat-v7:26.1.0" - implementation "com.android.support:customtabs:26.1.0" - implementation "com.android.support.constraint:constraint-layout:1.0.2" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.21" + implementation 'net.openid:appauth:0.11.1' + + implementation 'androidx.appcompat:appcompat:1.0.0' + implementation 'androidx.browser:browser:1.0.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" implementation "io.reactivex.rxjava2:rxandroid:2.0.1" implementation "io.reactivex.rxjava2:rxjava:2.1.7" implementation "io.reactivex.rxjava2:rxkotlin:2.2.0" - implementation "com.squareup.retrofit2:retrofit:2.3.0" implementation "com.squareup.retrofit2:converter-gson:2.3.0" - implementation "com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0" - implementation "com.squareup.okhttp3:logging-interceptor:3.9.1" testImplementation "junit:junit:4.12" testImplementation "com.nhaarman:mockito-kotlin:1.5.0" diff --git a/mobileauthentication/src/main/AndroidManifest.xml b/mobileauthentication/src/main/AndroidManifest.xml index 02b8573..22a4fd5 100644 --- a/mobileauthentication/src/main/AndroidManifest.xml +++ b/mobileauthentication/src/main/AndroidManifest.xml @@ -1,12 +1,12 @@ - + - + android:theme="@style/AppTheme" + android:launchMode="singleInstance" /> + diff --git a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/MobileAuthenticationClient.kt b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/MobileAuthenticationClient.kt index 985bedc..af79231 100644 --- a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/MobileAuthenticationClient.kt +++ b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/MobileAuthenticationClient.kt @@ -4,10 +4,8 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.preference.PreferenceManager -import ca.bc.gov.mobileauthentication.common.Constants import ca.bc.gov.mobileauthentication.common.exceptions.TokenNotFoundException -import ca.bc.gov.mobileauthentication.common.utils.UrlUtils -import ca.bc.gov.mobileauthentication.data.AuthApi +import ca.bc.gov.mobileauthentication.data.AppAuthApi import ca.bc.gov.mobileauthentication.data.models.Token import ca.bc.gov.mobileauthentication.data.repos.token.TokenRepo import ca.bc.gov.mobileauthentication.di.Injection @@ -51,11 +49,9 @@ class MobileAuthenticationClient( private val disposables = CompositeDisposable() private val gson: Gson = Injection.provideGson() - private val grantType: String = Constants.GRANT_TYPE_AUTH_CODE - private val authApi: AuthApi = InjectionUtils.getAuthApi(UrlUtils.cleanBaseUrl(baseUrl)) + private val appauthApi: AppAuthApi = AppAuthApi(context, baseUrl, realmName, authEndpoint, redirectUri, clientId, hint) private val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) - private val tokenRepo: TokenRepo = InjectionUtils.getTokenRepo( - authApi, realmName, grantType, redirectUri, clientId, sharedPrefs) + private val tokenRepo: TokenRepo = InjectionUtils.getTokenRepo(appauthApi, sharedPrefs) private var passedRequestCode: Int = DEFAULT_REQUEST_CODE @@ -64,6 +60,7 @@ class MobileAuthenticationClient( */ override fun authenticate(requestCode: Int) { this.passedRequestCode = requestCode + Intent(context, RedirectActivity::class.java) .putExtra(RedirectActivity.BASE_URL, baseUrl) .putExtra(RedirectActivity.REALM_NAME, realmName) @@ -78,21 +75,34 @@ class MobileAuthenticationClient( * Handles on activity result and determines if the authentication was successful * or an error occurred. */ - override fun handleAuthResult( - requestCode: Int, resultCode: Int, data: Intent?, - tokenCallback: TokenCallback) { - - if (resultCode == Activity.RESULT_OK && requestCode == passedRequestCode && data != null) { - val success = data.getBooleanExtra(MobileAuthenticationClient.SUCCESS, false) - if (success) { - val tokenJson = data.getStringExtra(MobileAuthenticationClient.TOKEN_JSON) - val token: Token = gson.fromJson(tokenJson, Token::class.java) - tokenCallback.onSuccess(token) - } - else { - val errorMessage = data.getStringExtra(MobileAuthenticationClient.ERROR_MESSAGE) - tokenCallback.onError(Throwable(errorMessage)) - } + override fun handleAuthResult(requestCode: Int, resultCode: Int, data: Intent?, + tokenCallback: TokenCallback) { + if (requestCode != passedRequestCode) + return + + if (resultCode != Activity.RESULT_OK) { + val message = data?.getStringExtra(ERROR_MESSAGE) ?: "Unknown authentication error" + tokenCallback.onError(Throwable(message)) + return + } + + if (data == null) { + tokenCallback.onError(Throwable("Result OK but authentication response is malformed")) + return + } + + handleConsumerResult(data, tokenCallback) + } + + private fun handleConsumerResult(data: Intent, tokenCallback: TokenCallback) { + val success = data.getBooleanExtra(SUCCESS, false) + if (success) { + val tokenJson = data.getStringExtra(TOKEN_JSON) + val token: Token = gson.fromJson(tokenJson, Token::class.java) + tokenCallback.onSuccess(token) + } else { + val errorMessage = data.getStringExtra(ERROR_MESSAGE) + tokenCallback.onError(Throwable(errorMessage)) } } @@ -104,7 +114,7 @@ class MobileAuthenticationClient( * If a token does not exist a @see ca.bc.gov.mobileauthentication.common.exceptions.TokenNotFoundException will be thrown */ override fun getToken(tokenCallback: TokenCallback) { - tokenRepo.getToken() + tokenRepo.getToken(null) .firstElement() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()).subscribeBy( @@ -123,7 +133,7 @@ class MobileAuthenticationClient( /** * Gets Token from local storage as a RxJava2 Observable */ - override fun getTokenAsObservable(): Observable = tokenRepo.getToken() + override fun getTokenAsObservable(): Observable = tokenRepo.getToken(null) /** * Refreshes token @@ -148,7 +158,7 @@ class MobileAuthenticationClient( /** * Refreshes Token that is stored in local storage as a RxJava2 Observable */ - override fun refreshTokenAsObservable(): Observable = tokenRepo.getToken() + override fun refreshTokenAsObservable(): Observable = tokenRepo.getToken(null) .flatMap { token -> tokenRepo.refreshToken(token) } /** @@ -190,6 +200,7 @@ class MobileAuthenticationClient( companion object { const val DEFAULT_REQUEST_CODE = 1012 + const val APPAUTH_REQUEST_CODE = 2024 const val SUCCESS = "SUCCESS" const val ERROR_MESSAGE = "ERROR_MESSAGE" const val TOKEN_JSON = "TOKEN_JSON" diff --git a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/Constants.kt b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/Constants.kt index 4d997c4..be96932 100644 --- a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/Constants.kt +++ b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/Constants.kt @@ -18,9 +18,6 @@ package ca.bc.gov.mobileauthentication.common * */ object Constants { - const val READ_TIME_OUT = 20L - const val CONNECT_TIME_OUT = 20L - const val GRANT_TYPE_AUTH_CODE = "authorization_code" const val RESPONSE_TYPE_CODE = "code" } \ No newline at end of file diff --git a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/InvalidOperationException.kt b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/InvalidOperationException.kt index 48894b7..4dca56a 100644 --- a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/InvalidOperationException.kt +++ b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/InvalidOperationException.kt @@ -19,6 +19,6 @@ package ca.bc.gov.mobileauthentication.common.exceptions */ class InvalidOperationException: Throwable() { - override val message: String? get() = "Invalid operation" + override val message: String get() = "Invalid operation" } \ No newline at end of file diff --git a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/NoCodeException.kt b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/NoCodeException.kt index 00432f7..f081934 100644 --- a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/NoCodeException.kt +++ b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/NoCodeException.kt @@ -18,6 +18,6 @@ package ca.bc.gov.mobileauthentication.common.exceptions * */ class NoCodeException : Throwable() { - override val message: String? + override val message: String get() = "Code is required for getting token" } \ No newline at end of file diff --git a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/NoRefreshTokenException.kt b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/NoRefreshTokenException.kt index 7f8fbab..f050acd 100644 --- a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/NoRefreshTokenException.kt +++ b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/NoRefreshTokenException.kt @@ -18,6 +18,6 @@ package ca.bc.gov.mobileauthentication.common.exceptions * */ class NoRefreshTokenException : Throwable() { - override val message: String? + override val message: String get() = "No refresh token" } \ No newline at end of file diff --git a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/RefreshExpiredException.kt b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/RefreshExpiredException.kt index 177e8fb..fa252a1 100644 --- a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/RefreshExpiredException.kt +++ b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/RefreshExpiredException.kt @@ -18,6 +18,6 @@ package ca.bc.gov.mobileauthentication.common.exceptions * */ class RefreshExpiredException : Throwable() { - override val message: String? + override val message: String get() = "Refresh token has expired. Please get new token using authenticate." } \ No newline at end of file diff --git a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/TokenNotFoundException.kt b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/TokenNotFoundException.kt index 1f02363..9837936 100644 --- a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/TokenNotFoundException.kt +++ b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/common/exceptions/TokenNotFoundException.kt @@ -18,6 +18,6 @@ package ca.bc.gov.mobileauthentication.common.exceptions * */ class TokenNotFoundException : Throwable() { - override val message: String? + override val message: String get() = "No token found. Please call authenticate before trying to retrieve a token locally." } \ No newline at end of file diff --git a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/AppAuthApi.kt b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/AppAuthApi.kt new file mode 100644 index 0000000..f648b6f --- /dev/null +++ b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/AppAuthApi.kt @@ -0,0 +1,123 @@ +package ca.bc.gov.mobileauthentication.data + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.browser.customtabs.CustomTabsIntent +import ca.bc.gov.mobileauthentication.common.utils.UrlUtils +import ca.bc.gov.mobileauthentication.data.models.Token +import io.reactivex.Observable +import io.reactivex.schedulers.Schedulers +import net.openid.appauth.* + + +class AppAuthApi(private val context: Context, + baseUrl: String, + private val realmName: String, + authEndpoint: String, + redirectUri: String, + private val clientId: String, + hint: String = "") { + + companion object { + const val REFRESH_EXPIRES_IN = "refresh_expires_in" + const val NOT_BEFORE_POLICY = "not-before-policy" + const val SESSION_STATE = "session_state" + } + + private fun buildTokenUrl(baseUrl: String): String { + return UrlUtils.cleanBaseUrl(baseUrl) + "auth/realms/$realmName/protocol/openid-connect/token" + } + + private val authorizationConfig: AuthorizationServiceConfiguration + private val authorizationRequest: AuthorizationRequest + + init { + authorizationConfig = AuthorizationServiceConfiguration( + Uri.parse(authEndpoint), + Uri.parse(buildTokenUrl(baseUrl))) + + val reqBuilder = AuthorizationRequest.Builder( + authorizationConfig, + clientId, + ResponseTypeValues.CODE, + Uri.parse(redirectUri)) + + if (hint.isNotEmpty()) + reqBuilder.setLoginHint(hint) + + authorizationRequest = reqBuilder.build() + } + + private fun buildRefreshTokenRequest(token: Token): TokenRequest { + return TokenRequest.Builder(authorizationConfig, clientId) + .setGrantType(GrantTypeValues.REFRESH_TOKEN) + .setRefreshToken(token.refreshToken) + .build() + } + + private fun convertToToken(tokenResponse: TokenResponse): Token { + val current = System.currentTimeMillis() + val currentUnixTime = current / 1000 + + val accessExpirationUnixTimeMillis = tokenResponse.accessTokenExpirationTime + val accessExpirationUnixTime = accessExpirationUnixTimeMillis?.div(1000) + val accessExpiresInSeconds = accessExpirationUnixTime?.minus(currentUnixTime) + val accessExpiresInMillis = accessExpiresInSeconds?.times(1000) + + val refreshExpiresInSeconds = tokenResponse.additionalParameters[REFRESH_EXPIRES_IN]?.toLong() + val refreshExpirationUnixTime = refreshExpiresInSeconds?.plus(currentUnixTime) + val refreshExpirationUnixTimeMillis = refreshExpirationUnixTime?.times(1000) + + return Token( + tokenResponse.accessToken, + accessExpiresInMillis, + refreshExpiresInSeconds, + tokenResponse.refreshToken, + tokenResponse.tokenType, + tokenResponse.idToken, + tokenResponse.additionalParameters[NOT_BEFORE_POLICY]?.toLong(), + tokenResponse.additionalParameters[SESSION_STATE], + accessExpirationUnixTimeMillis, + refreshExpirationUnixTimeMillis + ) + } + + /** + * Using configuration provided to the Constructor: builds an Intent that will be used to + * initiate the OAuth authentication flow (using startActivityForResult) + */ + fun getAuthRequestIntent(customTabsIntent: CustomTabsIntent? = null): Intent { + val service = AuthorizationService(context) + + return if (customTabsIntent != null) + service.getAuthorizationRequestIntent(authorizationRequest, customTabsIntent) + else + service.getAuthorizationRequestIntent(authorizationRequest) + } + + private fun performTokenRequest(tokenReq: TokenRequest): Observable { + return Observable.create { + AuthorizationService(context).performTokenRequest(tokenReq) { response, ex -> + if (response != null) + it.onNext(convertToToken(response)) + else if (ex != null) + it.onError(ex) + } + }.subscribeOn(Schedulers.io()) + } + + /** + * Makes an OAuth request to exchange an Authorization code for an access token. + */ + fun getToken(authResponse: AuthorizationResponse): Observable { + return performTokenRequest(authResponse.createTokenExchangeRequest()) + } + + /** + * Using the provided refresh token: makes an OAuth request to exchange for a new access token. + */ + fun refreshToken(token: Token): Observable { + return performTokenRequest(buildRefreshTokenRequest(token)) + } +} \ No newline at end of file diff --git a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/AuthApi.kt b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/AuthApi.kt deleted file mode 100644 index 90e8446..0000000 --- a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/AuthApi.kt +++ /dev/null @@ -1,55 +0,0 @@ -package ca.bc.gov.mobileauthentication.data - -import ca.bc.gov.mobileauthentication.data.models.Token -import io.reactivex.Observable -import retrofit2.http.Field -import retrofit2.http.FormUrlEncoded -import retrofit2.http.POST -import retrofit2.http.Path - -/** - * - * 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. - * - * Created by Aidan Laing on 2017-12-12. - * - */ -interface AuthApi { - - /** - * OAuth2 get Token call - */ - @POST("/auth/realms/{realm_name}/protocol/openid-connect/token") - @FormUrlEncoded - fun getToken( - @Path("realm_name") realmName: String, - @Field("grant_type") grantType: String, - @Field("redirect_uri") redirectUri: String, - @Field("client_id") client_id: String, - @Field("code") code: String - ): Observable - - /** - * OAuth2 refresh Token call - */ - @POST("/auth/realms/{realm_name}/protocol/openid-connect/token") - @FormUrlEncoded - fun refreshToken( - @Path("realm_name") realmName: String, - @Field("redirect_uri") redirectUri: String, - @Field("client_id") client_id: String, - @Field("refresh_token") refreshToken: String, - @Field("grant_type") grantType: String = "refresh_token" - ): Observable - -} \ No newline at end of file diff --git a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/SecureSharedPrefs.kt b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/SecureSharedPrefs.kt index 887c526..7eb2410 100644 --- a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/SecureSharedPrefs.kt +++ b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/SecureSharedPrefs.kt @@ -154,7 +154,7 @@ class SecureSharedPrefs( } companion object { - private val AES_TRANSFORMATION = "AES/GCM/NoPadding" + private const val AES_TRANSFORMATION = "AES/GCM/NoPadding" } } \ No newline at end of file diff --git a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/TokenDataSource.kt b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/TokenDataSource.kt index 1876049..4d754ed 100644 --- a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/TokenDataSource.kt +++ b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/TokenDataSource.kt @@ -2,6 +2,7 @@ package ca.bc.gov.mobileauthentication.data.repos.token import ca.bc.gov.mobileauthentication.data.models.Token import io.reactivex.Observable +import net.openid.appauth.AuthorizationResponse /** * @@ -22,7 +23,7 @@ import io.reactivex.Observable */ interface TokenDataSource { - fun getToken(code: String? = null): Observable + fun getToken(authResponse: AuthorizationResponse? = null): Observable fun saveToken(token: Token): Observable diff --git a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/TokenLocalDataSource.kt b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/TokenLocalDataSource.kt index aec6933..fcf1d61 100644 --- a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/TokenLocalDataSource.kt +++ b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/TokenLocalDataSource.kt @@ -4,6 +4,7 @@ import ca.bc.gov.mobileauthentication.common.exceptions.InvalidOperationExceptio import ca.bc.gov.mobileauthentication.data.models.Token import com.google.gson.Gson import io.reactivex.Observable +import net.openid.appauth.AuthorizationResponse /** * @@ -45,7 +46,7 @@ private constructor( /** * Gets token from local db and returns */ - override fun getToken(code: String?): Observable { + override fun getToken(authResponse: AuthorizationResponse?): Observable { return Observable.create { emitter -> val tokenJson = secureSharedPrefs.getString(TOKEN_KEY) if (tokenJson.isNotBlank()) { diff --git a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/TokenRemoteDataSource.kt b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/TokenRemoteDataSource.kt index 1aaf13e..16fff34 100644 --- a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/TokenRemoteDataSource.kt +++ b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/TokenRemoteDataSource.kt @@ -3,9 +3,10 @@ package ca.bc.gov.mobileauthentication.data.repos.token import ca.bc.gov.mobileauthentication.common.exceptions.InvalidOperationException import ca.bc.gov.mobileauthentication.common.exceptions.NoCodeException import ca.bc.gov.mobileauthentication.common.exceptions.NoRefreshTokenException -import ca.bc.gov.mobileauthentication.data.AuthApi +import ca.bc.gov.mobileauthentication.data.AppAuthApi import ca.bc.gov.mobileauthentication.data.models.Token import io.reactivex.Observable +import net.openid.appauth.AuthorizationResponse /** * @@ -25,27 +26,15 @@ import io.reactivex.Observable * */ class TokenRemoteDataSource -private constructor( - private val authApi: AuthApi, - private val realmName: String, - private val grantType: String, - private val redirectUri: String, - private val clientId: String -) : TokenDataSource { +private constructor(private val authApi: AppAuthApi) : TokenDataSource { companion object { @Volatile private var INSTANCE: TokenRemoteDataSource? = null - fun getInstance( - authApi: AuthApi, - realmName: String, - grantType: String, - redirectUri: String, - clientId: String): TokenRemoteDataSource = INSTANCE ?: synchronized(this) { - INSTANCE ?: TokenRemoteDataSource( - authApi, realmName, grantType, redirectUri, clientId).also { INSTANCE = it } + fun getInstance(authApi: AppAuthApi): TokenRemoteDataSource = INSTANCE ?: synchronized(this) { + INSTANCE ?: TokenRemoteDataSource(authApi).also { INSTANCE = it } } } @@ -53,14 +42,11 @@ private constructor( * Exchanges code for token using authentication api * Returns error if code is null */ - override fun getToken(code: String?): Observable { - if (code == null) return Observable.error(NoCodeException()) - return authApi.getToken(realmName, grantType, redirectUri, clientId, code) - .map { token -> - token.expiresAt = System.currentTimeMillis() + ((token.expiresIn ?: 0) * 1000) - token.refreshExpiresAt = System.currentTimeMillis() + ((token.refreshExpiresIn ?: 0) * 1000) - token - } + override fun getToken(authResponse: AuthorizationResponse?): Observable { + if (authResponse == null) + return Observable.error(NoCodeException()) + + return authApi.getToken(authResponse) } /** @@ -75,13 +61,10 @@ private constructor( * Returns error if there is no refresh token */ override fun refreshToken(token: Token): Observable { - val refreshToken = token.refreshToken ?: return Observable.error(NoRefreshTokenException()) - return authApi.refreshToken(realmName, redirectUri, clientId, refreshToken) - .map { refreshedToken -> - refreshedToken.expiresAt = System.currentTimeMillis() + ((refreshedToken.expiresIn ?: 0) * 1000) - refreshedToken.refreshExpiresAt = System.currentTimeMillis() + ((refreshedToken.refreshExpiresIn ?: 0) * 1000) - refreshedToken - } + if (token.refreshToken == null) + return Observable.error(NoRefreshTokenException()) + + return authApi.refreshToken(token) } /** * Invalid operation for remote data source diff --git a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/TokenRepo.kt b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/TokenRepo.kt index ef6c902..e40683a 100644 --- a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/TokenRepo.kt +++ b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/data/repos/token/TokenRepo.kt @@ -3,6 +3,7 @@ package ca.bc.gov.mobileauthentication.data.repos.token import ca.bc.gov.mobileauthentication.common.exceptions.RefreshExpiredException import ca.bc.gov.mobileauthentication.data.models.Token import io.reactivex.Observable +import net.openid.appauth.AuthorizationResponse /** * @@ -52,12 +53,12 @@ private constructor( * If token from local db refresh is expired then @see ca.bc.gov.mobileauthentication.common.exceptions.RefreshExpiredException will be thrown. * If refresh token is not expired and token is expired then token will be refreshed and returned */ - override fun getToken(code: String?): Observable { - return if (code != null) { - remoteDataSource.getToken(code) + override fun getToken(authResponse: AuthorizationResponse?): Observable { + return if (authResponse != null) { + remoteDataSource.getToken(authResponse) .flatMap { localDataSource.saveToken(it) } } else { - localDataSource.getToken() + localDataSource.getToken(null) .flatMap { when { it.isRefreshExpired() -> Observable.error(RefreshExpiredException()) diff --git a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/di/Injection.kt b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/di/Injection.kt index b0d47c2..9cbd201 100644 --- a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/di/Injection.kt +++ b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/di/Injection.kt @@ -1,22 +1,10 @@ package ca.bc.gov.mobileauthentication.di import android.content.SharedPreferences -import ca.bc.gov.mobileauthentication.data.AuthApi import ca.bc.gov.mobileauthentication.data.repos.token.SecureSharedPrefs -import ca.bc.gov.mobileauthentication.data.repos.token.TokenLocalDataSource -import ca.bc.gov.mobileauthentication.data.repos.token.TokenRemoteDataSource -import ca.bc.gov.mobileauthentication.data.repos.token.TokenRepo import com.google.gson.Gson import com.google.gson.GsonBuilder -import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.CallAdapter -import retrofit2.Converter -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory import java.security.KeyStore -import java.util.concurrent.TimeUnit /** * @@ -37,25 +25,6 @@ import java.util.concurrent.TimeUnit */ object Injection { - // OkHttpClient - @JvmStatic - fun provideOkHttpClient( - readTimeOut: Long, - connectTimeOut: Long, - interceptor: HttpLoggingInterceptor - ): OkHttpClient = OkHttpClient.Builder() - .readTimeout(readTimeOut, TimeUnit.SECONDS) - .connectTimeout(connectTimeOut, TimeUnit.SECONDS) - .addInterceptor(interceptor) - .build() - - // Logging interceptor - @JvmStatic - fun provideHttpLoggingInterceptor( - loggingLevel: HttpLoggingInterceptor.Level - ): HttpLoggingInterceptor = HttpLoggingInterceptor() - .apply { level = loggingLevel } - // Gson private var cachedGson: Gson? = null @@ -65,51 +34,6 @@ object Injection { .create() .also { cachedGson = it } - // Converter Factory - @JvmStatic - fun provideConverterFactory(gson: Gson): Converter.Factory = GsonConverterFactory.create(gson) - - // Call Adapter Factory - private var cachedCallAdapterFactory: CallAdapter.Factory? = null - - @JvmStatic - fun provideCallAdapterFactory(): CallAdapter.Factory = cachedCallAdapterFactory - ?: - RxJava2CallAdapterFactory.create() - .also { cachedCallAdapterFactory = it } - - // Retrofit - @JvmStatic - fun provideRetrofit( - apiDomain: String, - okHttpClient: OkHttpClient, - converterFactory: Converter.Factory, - callAdapterFactory: CallAdapter.Factory - ): Retrofit = Retrofit.Builder() - .baseUrl(apiDomain) - .client(okHttpClient) - .addConverterFactory(converterFactory) - .addCallAdapterFactory(callAdapterFactory) - .build() - - // Auth Api - @JvmStatic - fun provideAuthApi(retrofit: Retrofit): AuthApi = retrofit.create(AuthApi::class.java) - - // Token Repo - @JvmStatic - fun provideTokenRepo( - authApi: AuthApi, - realmName: String, - grantType: String, - redirectUri: String, - clientId: String, - gson: Gson, - secureSharedPrefs: SecureSharedPrefs - ): TokenRepo = TokenRepo.getInstance( - TokenRemoteDataSource.getInstance(authApi, realmName, grantType, redirectUri, clientId), - TokenLocalDataSource.getInstance(gson, secureSharedPrefs)) - // Secure shared prefs @JvmStatic fun provideSecureSharedPrefs( diff --git a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/di/InjectionUtils.kt b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/di/InjectionUtils.kt index 8eb5904..4607886 100644 --- a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/di/InjectionUtils.kt +++ b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/di/InjectionUtils.kt @@ -1,18 +1,12 @@ package ca.bc.gov.mobileauthentication.di import android.content.SharedPreferences -import ca.bc.gov.mobileauthentication.data.AuthApi -import ca.bc.gov.mobileauthentication.common.Constants +import ca.bc.gov.mobileauthentication.data.AppAuthApi import ca.bc.gov.mobileauthentication.data.repos.token.SecureSharedPrefs import ca.bc.gov.mobileauthentication.data.repos.token.TokenLocalDataSource import ca.bc.gov.mobileauthentication.data.repos.token.TokenRemoteDataSource import ca.bc.gov.mobileauthentication.data.repos.token.TokenRepo import com.google.gson.Gson -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.CallAdapter -import retrofit2.Converter -import retrofit2.Retrofit import java.security.KeyStore /** @@ -34,41 +28,17 @@ import java.security.KeyStore */ object InjectionUtils { - /** - * Gets Auth Api with standard params - */ - fun getAuthApi( - apiDomain: String, - gson: Gson = Injection.provideGson(), - converterFactory: Converter.Factory = Injection.provideConverterFactory(gson), - callAdapterFactory : CallAdapter.Factory = Injection.provideCallAdapterFactory(), - readTimeOut: Long = Constants.READ_TIME_OUT, - connectTimeOut: Long = Constants.CONNECT_TIME_OUT, - loggingLevel: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.BODY, - httpLoggingInterceptor: HttpLoggingInterceptor = Injection.provideHttpLoggingInterceptor( - loggingLevel), - okHttpClient: OkHttpClient = Injection.provideOkHttpClient( - readTimeOut, connectTimeOut, httpLoggingInterceptor), - retrofit: Retrofit = Injection.provideRetrofit( - apiDomain, okHttpClient, converterFactory, callAdapterFactory) - ): AuthApi = Injection.provideAuthApi(retrofit) - /** * Gets Token Repo with standard params */ fun getTokenRepo( - authApi: AuthApi, - realmName: String, - grantType: String, - redirectUri: String, - clientId: String, + authApi: AppAuthApi, sharedPreferences: SharedPreferences, gson: Gson = Injection.provideGson(), keyStore: KeyStore = Injection.provideKeyStore(), secureSharedPrefs: SecureSharedPrefs = Injection.provideSecureSharedPrefs(keyStore, sharedPreferences) ): TokenRepo = TokenRepo.getInstance( - TokenRemoteDataSource.getInstance(authApi, realmName, grantType, redirectUri, clientId), + TokenRemoteDataSource.getInstance(authApi), TokenLocalDataSource.getInstance(gson, secureSharedPrefs) ) - } \ No newline at end of file diff --git a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/screens/redirect/RedirectActivity.kt b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/screens/redirect/RedirectActivity.kt index af1fdb4..4ecedb7 100644 --- a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/screens/redirect/RedirectActivity.kt +++ b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/screens/redirect/RedirectActivity.kt @@ -2,20 +2,20 @@ package ca.bc.gov.mobileauthentication.screens.redirect import android.app.Activity import android.content.Intent -import android.net.Uri import android.os.Bundle import android.preference.PreferenceManager -import android.widget.Toast -import ca.bc.gov.mobileauthentication.R -import ca.bc.gov.mobileauthentication.di.InjectionUtils -import android.support.customtabs.CustomTabsIntent -import android.support.v4.content.ContextCompat -import android.support.v7.app.AppCompatActivity import android.view.View +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.browser.customtabs.CustomTabColorSchemeParams +import androidx.browser.customtabs.CustomTabsIntent +import androidx.core.content.ContextCompat import ca.bc.gov.mobileauthentication.MobileAuthenticationClient +import ca.bc.gov.mobileauthentication.R import ca.bc.gov.mobileauthentication.common.Constants -import ca.bc.gov.mobileauthentication.common.utils.UrlUtils +import ca.bc.gov.mobileauthentication.data.AppAuthApi import ca.bc.gov.mobileauthentication.di.Injection +import ca.bc.gov.mobileauthentication.di.InjectionUtils import kotlinx.android.synthetic.main.activity_login.* /** @@ -41,6 +41,8 @@ class RedirectActivity : AppCompatActivity(), RedirectContract.View { override var loading: Boolean = false + private lateinit var appauthApi: AppAuthApi + companion object { const val BASE_URL = "BASE_URL" const val REALM_NAME = "REALM_NAME" @@ -94,13 +96,11 @@ class RedirectActivity : AppCompatActivity(), RedirectContract.View { } // Building presenter params - val grantType = Constants.GRANT_TYPE_AUTH_CODE val responseType = Constants.RESPONSE_TYPE_CODE - val authApi = InjectionUtils.getAuthApi(UrlUtils.cleanBaseUrl(baseUrl)) + appauthApi = AppAuthApi(this, baseUrl, realmName, authEndpoint, redirectUri, clientId, hint) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) - val tokenRepo = InjectionUtils.getTokenRepo( - authApi, realmName, grantType, redirectUri, clientId, sharedPreferences) + val tokenRepo = InjectionUtils.getTokenRepo(appauthApi, sharedPreferences) val gson = Injection.provideGson() @@ -128,10 +128,15 @@ class RedirectActivity : AppCompatActivity(), RedirectContract.View { checkIntentForRedirect(intent) } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (requestCode == MobileAuthenticationClient.APPAUTH_REQUEST_CODE) + checkIntentForRedirect(data) + } + private fun checkIntentForRedirect(intent: Intent?) { - if (intent != null && intent.action == Intent.ACTION_VIEW && intent.data != null) { - presenter?.redirectReceived(intent.data.toString()) - } + presenter?.redirectReceived(intent) } // Loading @@ -162,12 +167,18 @@ class RedirectActivity : AppCompatActivity(), RedirectContract.View { * Goes to Chrome custom tab */ override fun loadWithChrome(url: String) { - CustomTabsIntent.Builder() - .addDefaultShareMenuItem() + val colorScheme = CustomTabColorSchemeParams.Builder() .setToolbarColor(ContextCompat.getColor(this, R.color.colorPrimary)) + .build() + + val customTabsIntent = CustomTabsIntent.Builder() + .setShareState(CustomTabsIntent.SHARE_STATE_OFF) + .setDefaultColorSchemeParams(colorScheme) .setShowTitle(true) .build() - .launchUrl(this, Uri.parse(url)) + + val authIntent = appauthApi.getAuthRequestIntent(customTabsIntent) + startActivityForResult(authIntent, MobileAuthenticationClient.APPAUTH_REQUEST_CODE) } // Results @@ -184,4 +195,9 @@ class RedirectActivity : AppCompatActivity(), RedirectContract.View { data.putExtra(MobileAuthenticationClient.TOKEN_JSON, tokenJson) setResult(Activity.RESULT_OK, data) } + + override fun onBackPressed() { + setResultError("Login cancelled by user") + super.onBackPressed() + } } diff --git a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/screens/redirect/RedirectContract.kt b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/screens/redirect/RedirectContract.kt index 5ae4acf..a151248 100644 --- a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/screens/redirect/RedirectContract.kt +++ b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/screens/redirect/RedirectContract.kt @@ -1,5 +1,6 @@ package ca.bc.gov.mobileauthentication.screens.redirect +import android.content.Intent import ca.bc.gov.mobileauthentication.common.BasePresenter import ca.bc.gov.mobileauthentication.common.BaseView @@ -43,7 +44,7 @@ interface RedirectContract { interface Presenter: BasePresenter { fun loginClicked() - fun redirectReceived(redirectUrl: String) + fun redirectReceived(redirectIntent: Intent?) } } \ No newline at end of file diff --git a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/screens/redirect/RedirectPresenter.kt b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/screens/redirect/RedirectPresenter.kt index 276fcf1..a90323a 100644 --- a/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/screens/redirect/RedirectPresenter.kt +++ b/mobileauthentication/src/main/java/ca/bc/gov/mobileauthentication/screens/redirect/RedirectPresenter.kt @@ -1,6 +1,6 @@ package ca.bc.gov.mobileauthentication.screens.redirect -import ca.bc.gov.mobileauthentication.common.utils.UrlUtils +import android.content.Intent import ca.bc.gov.mobileauthentication.data.repos.token.TokenRepo import com.google.gson.Gson import io.reactivex.android.schedulers.AndroidSchedulers @@ -8,6 +8,7 @@ import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.addTo import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers +import net.openid.appauth.AuthorizationResponse /** * @@ -61,20 +62,23 @@ class RedirectPresenter( "$authEndpoint?response_type=$responseType&client_id=$clientId&redirect_uri=$redirectUri&kc_idp_hint=$hint" // Redirect - override fun redirectReceived(redirectUrl: String) { - if (!redirectUrl.contains("code=".toRegex())) { + override fun redirectReceived(redirectIntent: Intent?) { + if (redirectIntent == null) return - } - val code = UrlUtils.extractCode(redirectUrl) - getToken(code) + val response = AuthorizationResponse.fromIntent(redirectIntent) ?: return + + if (response.authorizationCode == null || response.authorizationCode!!.isEmpty()) + return + + getToken(response) } /** * Gets token remotely using Authorization Code and saves locally */ - fun getToken(code: String) { - tokenRepo.getToken(code) + fun getToken(authResponse: AuthorizationResponse) { + tokenRepo.getToken(authResponse) .firstOrError() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) diff --git a/mobileauthentication/src/main/res/layout/activity_login.xml b/mobileauthentication/src/main/res/layout/activity_login.xml index 4d41546..0466c09 100644 --- a/mobileauthentication/src/main/res/layout/activity_login.xml +++ b/mobileauthentication/src/main/res/layout/activity_login.xml @@ -1,5 +1,5 @@ - - \ No newline at end of file + \ No newline at end of file