├── app-demo ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── values │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ └── strings.xml │ │ │ ├── values-ru │ │ │ │ └── strings.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ └── ic_launcher.xml │ │ │ ├── xml │ │ │ │ ├── data_extraction_rules.xml │ │ │ │ └── network_security_config.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ │ └── ic_launcher_background.xml │ │ │ └── layout │ │ │ │ └── activity_partner.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── ru │ │ │ └── tinkoff │ │ │ └── core │ │ │ └── app_demo_partner │ │ │ ├── PartnerPresenter.kt │ │ │ └── PartnerActivity.kt │ └── androidTest │ │ └── java │ │ └── ru │ │ └── tinkoff │ │ └── core │ │ └── app_demo_partner │ │ └── TinkoffIdSignInButtonTest.kt ├── proguard-rules.pro └── build.gradle ├── tinkoff-id ├── .gitignore ├── gradle.properties ├── src │ ├── main │ │ ├── res │ │ │ ├── font │ │ │ │ └── neue_haas_unica_w1g.ttf │ │ │ ├── drawable-hdpi │ │ │ │ ├── tinkoff_id_tinkoff_logo.png │ │ │ │ ├── tinkoff_id_tinkoff_small_logo.png │ │ │ │ └── tinkoff_id_tinkoff_small_logo_border.png │ │ │ ├── drawable-mdpi │ │ │ │ ├── tinkoff_id_tinkoff_logo.png │ │ │ │ ├── tinkoff_id_tinkoff_small_logo.png │ │ │ │ └── tinkoff_id_tinkoff_small_logo_border.png │ │ │ ├── drawable-xhdpi │ │ │ │ ├── tinkoff_id_tinkoff_logo.png │ │ │ │ ├── tinkoff_id_tinkoff_small_logo.png │ │ │ │ └── tinkoff_id_tinkoff_small_logo_border.png │ │ │ ├── drawable-xxhdpi │ │ │ │ ├── tinkoff_id_tinkoff_logo.png │ │ │ │ ├── tinkoff_id_tinkoff_small_logo.png │ │ │ │ └── tinkoff_id_tinkoff_small_logo_border.png │ │ │ ├── drawable-xxxhdpi │ │ │ │ ├── tinkoff_id_tinkoff_logo.png │ │ │ │ ├── tinkoff_id_tinkoff_small_logo.png │ │ │ │ └── tinkoff_id_tinkoff_small_logo_border.png │ │ │ ├── values-night │ │ │ │ └── colors.xml │ │ │ ├── layout │ │ │ │ └── tinkoff_id_web_view_activity.xml │ │ │ ├── values-en │ │ │ │ └── strings.xml │ │ │ ├── drawable │ │ │ │ ├── tinkoff_id_badge_background_gray_style.xml │ │ │ │ ├── tinkoff_id_badge_background_black_style.xml │ │ │ │ ├── tinkoff_id_badge_background_yellow_style.xml │ │ │ │ ├── tinkoff_id_compact_background_black_style.xml │ │ │ │ ├── tinkoff_id_compact_background_gray_style.xml │ │ │ │ ├── tinkoff_id_compact_background_yellow_style.xml │ │ │ │ ├── tinkoff_id_close.xml │ │ │ │ └── tinkoff_id_reload_icon.xml │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── styles.xml │ │ │ │ ├── attr.xml │ │ │ │ ├── colors.xml │ │ │ │ └── dimens.xml │ │ │ └── menu │ │ │ │ └── tinkoff_id_web_view_auth_menu.xml │ │ ├── java │ │ │ └── ru │ │ │ │ └── tinkoff │ │ │ │ └── core │ │ │ │ └── tinkoffId │ │ │ │ ├── ui │ │ │ │ └── webView │ │ │ │ │ ├── TinkoffWebViewUiData.kt │ │ │ │ │ ├── TinkoffWebViewListener.kt │ │ │ │ │ ├── TinkoffWebViewAuthPresenter.kt │ │ │ │ │ ├── TinkoffWebViewClient.kt │ │ │ │ │ └── TinkoffWebViewAuthActivity.kt │ │ │ │ ├── TinkoffIdStatusCode.kt │ │ │ │ ├── error │ │ │ │ ├── TinkoffRequestException.kt │ │ │ │ ├── TinkoffErrorMessage.kt │ │ │ │ ├── TokenSignOutErrorConstants.kt │ │ │ │ └── TinkoffTokenErrorConstants.kt │ │ │ │ ├── TinkoffTokenPayload.kt │ │ │ │ ├── codeVerifier │ │ │ │ ├── CodeVerifierStore.kt │ │ │ │ └── CodeVerifierUtil.kt │ │ │ │ ├── TinkoffCall.kt │ │ │ │ ├── TinkoffPartnerApiService.kt │ │ │ │ ├── AppLinkUtil.kt │ │ │ │ ├── api │ │ │ │ └── TinkoffIdApi.kt │ │ │ │ └── TinkoffIdAuth.kt │ │ └── AndroidManifest.xml │ └── test │ │ └── java │ │ └── ru │ │ └── tinkoff │ │ └── core │ │ └── tinkoffId │ │ ├── TinkoffIdSignInButtonTest.kt │ │ ├── TinkoffIdApiTest.kt │ │ ├── AppLinkUtilTest.kt │ │ ├── TinkoffIdAuthTest.kt │ │ └── TinkoffPartnerApiServiceTest.kt └── build.gradle ├── .gitattributes ├── settings.gradle ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── plugin │ ├── kotlin-library.gradle │ ├── detekt.gradle │ ├── android.gradle │ └── publish-lib.gradle └── versions.gradle ├── imgs └── tinkoff_id_sign_in_button │ ├── compact.png │ └── standard.png ├── .idea └── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── .gitignore ├── .github └── workflows │ ├── merge_request.yml │ └── publish.yml ├── CHANGELOG.md ├── MIGRATION.md ├── gradle.properties ├── gradlew.bat ├── gradlew ├── README.md └── LICENSE /app-demo/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /tinkoff-id/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | changelog.md merge=union -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app-demo', ':tinkoff-id' 2 | rootProject.name = "TinkoffID" -------------------------------------------------------------------------------- /tinkoff-id/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=tinkoff-id 2 | POM_ARTIFACT_ID=tinkoff-id 3 | POM_PACKAGING=aar 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /imgs/tinkoff_id_sign_in_button/compact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/imgs/tinkoff_id_sign_in_button/compact.png -------------------------------------------------------------------------------- /imgs/tinkoff_id_sign_in_button/standard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/imgs/tinkoff_id_sign_in_button/standard.png -------------------------------------------------------------------------------- /app-demo/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app-demo/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app-demo/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app-demo/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app-demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/font/neue_haas_unica_w1g.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/font/neue_haas_unica_w1g.ttf -------------------------------------------------------------------------------- /app-demo/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #E8ADAB 4 | -------------------------------------------------------------------------------- /app-demo/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app-demo/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /app-demo/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app-demo/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app-demo/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable-hdpi/tinkoff_id_tinkoff_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-hdpi/tinkoff_id_tinkoff_logo.png -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable-mdpi/tinkoff_id_tinkoff_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-mdpi/tinkoff_id_tinkoff_logo.png -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable-xhdpi/tinkoff_id_tinkoff_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-xhdpi/tinkoff_id_tinkoff_logo.png -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable-xxhdpi/tinkoff_id_tinkoff_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-xxhdpi/tinkoff_id_tinkoff_logo.png -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable-xxxhdpi/tinkoff_id_tinkoff_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-xxxhdpi/tinkoff_id_tinkoff_logo.png -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable-hdpi/tinkoff_id_tinkoff_small_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-hdpi/tinkoff_id_tinkoff_small_logo.png -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable-mdpi/tinkoff_id_tinkoff_small_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-mdpi/tinkoff_id_tinkoff_small_logo.png -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable-xhdpi/tinkoff_id_tinkoff_small_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-xhdpi/tinkoff_id_tinkoff_small_logo.png -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable-xxhdpi/tinkoff_id_tinkoff_small_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-xxhdpi/tinkoff_id_tinkoff_small_logo.png -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable-xxxhdpi/tinkoff_id_tinkoff_small_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-xxxhdpi/tinkoff_id_tinkoff_small_logo.png -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable-hdpi/tinkoff_id_tinkoff_small_logo_border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-hdpi/tinkoff_id_tinkoff_small_logo_border.png -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable-mdpi/tinkoff_id_tinkoff_small_logo_border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-mdpi/tinkoff_id_tinkoff_small_logo_border.png -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable-xhdpi/tinkoff_id_tinkoff_small_logo_border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-xhdpi/tinkoff_id_tinkoff_small_logo_border.png -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable-xxhdpi/tinkoff_id_tinkoff_small_logo_border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-xxhdpi/tinkoff_id_tinkoff_small_logo_border.png -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable-xxxhdpi/tinkoff_id_tinkoff_small_logo_border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-xxxhdpi/tinkoff_id_tinkoff_small_logo_border.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | .idea 3 | !.idea/codeStyles 4 | build/ 5 | .DS_Store 6 | 7 | /local.properties 8 | /gradle.properties 9 | installlocal.bat 10 | 11 | *.iml 12 | *.eml 13 | *.iws 14 | 15 | gradle/plugin/custom.gradle 16 | -------------------------------------------------------------------------------- /.github/workflows/merge_request.yml: -------------------------------------------------------------------------------- 1 | name: merge request 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'main' 7 | 8 | jobs: 9 | check: 10 | uses: tinkoff-mobile-tech/workflows/.github/workflows/android_lib.merge_request.yml@v1 11 | -------------------------------------------------------------------------------- /gradle/plugin/kotlin-library.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin-android' 2 | apply from: rootProject.file('gradle/plugin/detekt.gradle') 3 | apply plugin: 'kotlin-kapt' 4 | 5 | dependencies { 6 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" 7 | } 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed May 04 12:08:39 MSK 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /app-demo/src/main/res/values-ru/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Partner Auth 4 | Обновить токен 5 | Отозвать токен 6 | 7 | -------------------------------------------------------------------------------- /app-demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | 7 | jobs: 8 | publish: 9 | uses: tinkoff-mobile-tech/workflows/.github/workflows/android_lib.publish.yml@v1 10 | secrets: 11 | gpg_key: ${{ secrets.GPG_KEY }} 12 | sign_ossrh_gradle_properties: ${{ secrets.SIGN_OSSRH_GRADLE_PROPERTIES }} 13 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/ui/webView/TinkoffWebViewUiData.kt: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.core.tinkoffId.ui.webView 2 | 3 | /** 4 | * @author k.voskrebentsev 5 | */ 6 | internal class TinkoffWebViewUiData( 7 | val clientId: String, 8 | val codeChallenge: String, 9 | val codeChallengeMethod: String, 10 | val redirectUri: String, 11 | val callbackUrl: String, 12 | ) 13 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/ui/webView/TinkoffWebViewListener.kt: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.core.tinkoffId.ui.webView 2 | 3 | /** 4 | * @author k.voskrebentsev 5 | */ 6 | internal interface TinkoffWebViewListener { 7 | 8 | fun isUrlForAuthCompletion(url: String): Boolean 9 | 10 | fun completeAuthWithSuccess(url: String) 11 | 12 | fun completeAuthWithCancellation() 13 | } 14 | -------------------------------------------------------------------------------- /app-demo/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/ui/webView/TinkoffWebViewAuthPresenter.kt: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.core.tinkoffId.ui.webView 2 | 3 | import ru.tinkoff.core.tinkoffId.api.TinkoffIdApi 4 | 5 | /** 6 | * @author k.voskrebentsev 7 | */ 8 | internal class TinkoffWebViewAuthPresenter { 9 | 10 | fun buildWebViewAuthStartUrl(uiData: TinkoffWebViewUiData): String { 11 | return TinkoffIdApi.buildWebViewAuthStartUrl( 12 | clientId = uiData.clientId, 13 | codeChallenge = uiData.codeChallenge, 14 | codeChallengeMethod = uiData.codeChallengeMethod, 15 | redirectUri = uiData.redirectUri, 16 | ) 17 | } 18 | 19 | fun parseCode(url: String): String { 20 | return TinkoffIdApi.parseCode(url) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app-demo/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app-demo/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | #121212 21 | 22 | 23 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/layout/tinkoff_id_web_view_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /gradle/plugin/detekt.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'io.gitlab.arturbosch.detekt' 2 | apply from: rootProject.file('gradle/versions.gradle') 3 | 4 | dependencies { 5 | detekt "io.gitlab.arturbosch.detekt:detekt-formatting:${detektVersion}" 6 | detekt("io.gitlab.arturbosch.detekt:detekt-cli:${detektVersion}") 7 | } 8 | 9 | detekt { 10 | toolVersion = detektVersion 11 | source = files("src") 12 | config = rootProject.files('gradle/plugin/config/detekt.yml') 13 | } 14 | 15 | tasks.named("detekt").configure { 16 | reports { 17 | xml.required.set(false) 18 | 19 | sarif.required.set(false) 20 | 21 | txt.required.set(true) 22 | txt.outputLocation.set(file("$project.buildDir/reports/detekt-results-${project.name}.txt")) 23 | 24 | html.required.set(true) 25 | html.outputLocation.set(file("$project.buildDir/reports/detekt-results-${project.name}.html")) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tinkoff-id/build.gradle: -------------------------------------------------------------------------------- 1 | apply from: '../gradle/plugin/android.gradle' 2 | apply from: '../gradle/plugin/publish-lib.gradle' 3 | 4 | android { 5 | resourcePrefix 'tinkoff_id_' 6 | 7 | testOptions { 8 | unitTests.includeAndroidResources = true 9 | } 10 | } 11 | 12 | dependencies { 13 | implementation "ru.tinkoff.core.components.security:ssl-trusted-certs:$sslTrustedCertsVersion" 14 | 15 | implementation "androidx.appcompat:appcompat:$appCompatVersion" 16 | implementation "com.squareup.okhttp3:okhttp:$okHttpVersion" 17 | 18 | testImplementation "junit:junit:$junitVersion" 19 | testImplementation "com.google.truth:truth:$truthVersion" 20 | testImplementation "org.robolectric:robolectric:$robolectricVersion" 21 | testImplementation "androidx.test:core:$testCoreVersion" 22 | testImplementation "com.squareup.okhttp3:mockwebserver:$okHttpVersion" 23 | } 24 | 25 | repositories { 26 | mavenCentral() 27 | } -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/values-en/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | Tinkoff 21 | Reload 22 | Close 23 | 24 | 25 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable/tinkoff_id_badge_background_gray_style.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | Tinkoff 21 | Перезагрузить 22 | Закрыть 23 | 24 | 25 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable/tinkoff_id_badge_background_black_style.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable/tinkoff_id_badge_background_yellow_style.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/menu/tinkoff_id_web_view_auth_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 26 | 27 | -------------------------------------------------------------------------------- /gradle/versions.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | compileSdkVersion = 33 3 | minSdkVersion = 21 4 | targetSdkVersion = 33 5 | 6 | isRelease = project.hasProperty('release') 7 | authCode = VERSION_CODE.toInteger() 8 | authVersion = getVersionName(VERSION_NAME) 9 | 10 | // tinkoff libs 11 | sslTrustedCertsVersion = '1.12.2' 12 | 13 | // android libs 14 | kotlinVersion = '1.8.10' 15 | appCompatVersion = '1.6.1' 16 | materialVersion = '1.9.0' 17 | constraintLayoutVersion = '2.1.4' 18 | androidPluginVersion = '7.4.2' 19 | 20 | // 3-rd party libs 21 | rxJavaVersion = '2.2.21' 22 | rxAndroidVersion = '2.1.1' 23 | okHttpVersion = '4.9.3' 24 | 25 | // documentation & quality libs 26 | detektVersion = '1.20.0' 27 | dokkaVersion = '1.6.21' 28 | 29 | // testing automation libs 30 | junitVersion = '4.13.2' 31 | truthVersion = '1.1.3' 32 | robolectricVersion = '4.8' 33 | testCoreVersion = '1.5.0' 34 | espressoVersion = '3.5.1' 35 | testRuleVersion = '1.5.0' 36 | uiautomatorVersion = '2.2.0' 37 | } 38 | 39 | def getVersionName(version) { 40 | return isRelease ? version : "${version}-SNAPSHOT" 41 | } 42 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/TinkoffIdStatusCode.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 Tinkoff Bank 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ru.tinkoff.core.tinkoffId 18 | 19 | /** 20 | * Status when returning to the partner application after requesting partner authorization 21 | * 22 | * @author Stanislav Mukhametshin 23 | */ 24 | public enum class TinkoffIdStatusCode { 25 | 26 | /** Success: authorization succeeded, you can retrieve the code for requesting a token */ 27 | SUCCESS, 28 | 29 | /** Authorization has been canceled by the user */ 30 | CANCELLED_BY_USER 31 | } 32 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/error/TinkoffRequestException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 Tinkoff Bank 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ru.tinkoff.core.tinkoffId.error 18 | 19 | /** 20 | * Any exception related to requests is wrapped to [TinkoffRequestException] 21 | * 22 | * @author Stanislav Mukhametshin 23 | */ 24 | public class TinkoffRequestException( 25 | public val reason: Throwable, 26 | override val message: String? = null, 27 | /** Information about Tinkoff Api errors */ 28 | public val errorMessage: TinkoffErrorMessage? = null 29 | ) : Exception(message, reason) 30 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable/tinkoff_id_compact_background_black_style.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable/tinkoff_id_compact_background_gray_style.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable/tinkoff_id_compact_background_yellow_style.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/TinkoffTokenPayload.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 Tinkoff Bank 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ru.tinkoff.core.tinkoffId 18 | 19 | /** 20 | * Model that contains tinkoff session information 21 | * 22 | * @author Stanislav Mukhametshin 23 | */ 24 | public data class TinkoffTokenPayload( 25 | /** Token to access Tinkoff Api */ 26 | val accessToken: String, 27 | /** Time after which access token will expire */ 28 | val expiresIn: Int, 29 | /** User id in jwt format */ 30 | val idToken: String?, 31 | /** Token needed to get new [accessToken] */ 32 | val refreshToken: String 33 | ) 34 | -------------------------------------------------------------------------------- /app-demo/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Partner Auth 3 | Refresh token 4 | Revoke token 5 | ClientId 6 | RedirectUrl 7 | Reset 8 | compact 9 | standard | small size | black style 10 | standard | medium size | gray style 11 | standard | large size | yellow style 12 | Сannot be empty 13 | Sign in with 14 | Cashback up to 5% 15 | 16 | 17 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/error/TinkoffErrorMessage.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 Tinkoff Bank 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ru.tinkoff.core.tinkoffId.error 18 | 19 | /** 20 | * Information about errors received in request responses 21 | * 22 | * @author Stanislav Mukhametshin 23 | */ 24 | public class TinkoffErrorMessage( 25 | 26 | /** Human readable message of api error */ 27 | public val message: String?, 28 | 29 | /** 30 | * Error type returned after sending request to endpoints, you can find all error types 31 | * in [TinkoffTokenErrorConstants], [TokenSignOutErrorConstants] 32 | */ 33 | public val errorType: Int 34 | ) 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.1 2 | 3 | #### Fixed 4 | #### Changes 5 | #### Additions 6 | 7 | ## 1.1.0 8 | 9 | #### Fixed 10 | #### Changes 11 | #### Additions 12 | 13 | - [app-demo], [tinkoff-id] Добавлен флоу авторизации через WebView MC-7723 14 | 15 | ## 1.0.5 16 | 17 | #### Fixed 18 | 19 | - [tinkoff-id] Внесены правки в документацию MC-6740 20 | 21 | #### Changes 22 | 23 | - [tinkoff-id] update versions of target and compile sdk, AGP and Gradle, dependencies MC-9132 24 | 25 | #### Additions 26 | 27 | - [tinkoff-id] В Intent для запуска авторизации добавлен параметр версии SDK MC-6740 28 | 29 | ## 1.0.4 30 | 31 | #### Fixed 32 | #### Changes 33 | #### Additions 34 | - [tinkoff-id] add self-signed SSL certificates MC-7689 35 | 36 | ## 1.0.3 37 | 38 | #### Fixed 39 | #### Changes 40 | - [app-demo], [tinkoff-id] redesign TinkoffIdSignInButton MC-6146 41 | #### Additions 42 | 43 | ## 1.0.2 44 | 45 | #### Fixed 46 | #### Changes 47 | - [app-demo], [tinkoff-id] bump up API versions (targetSdk and compileSdk to 31) and dependencies versions MC-5659 48 | #### Additions 49 | 50 | ## 1.0.1 51 | 52 | #### Fixed 53 | #### Changes 54 | #### Additions 55 | - Added redirect_uri configuration logic 56 | 57 | ## 1.0.0 58 | 59 | #### Fixed 60 | #### Changes 61 | #### Additions 62 | 63 | [app-demo]: app-demo 64 | [tinkoff-id]: tinkoff-id 65 | -------------------------------------------------------------------------------- /MIGRATION.md: -------------------------------------------------------------------------------- 1 | # Документ по миграции 2 | 3 | ## 1.0.6 -> 1.1.0 4 | 5 | В связи с добавлением альтернативного способа авторизации через веб Тинькофф с помощью WebView, 6 | переработана логика метода `createTinkoffAuthIntent(callbackUrl: Uri): Intent`. Теперь внутри него, на основе значения 7 | `isTinkoffAppAuthAvailable(): Boolean`, происходит создание Intent для открытия или приложения Тинькофф, 8 | или `TinkoffWebViewAuthActivity` (для прохождения авторизации в вебе). 9 | 10 | Рекомендуется использовать `createTinkoffAuthIntent(callbackUrl: Uri): Intent`, чтобы пользователю в любом случае была 11 | доступна авторизации через Тинькофф: 12 | 13 | **Before**: 14 | 15 | ```kotlin 16 | if (tinkoffPartnerAuth.isTinkoffAuthAvailable()) { 17 | val intent = tinkoffPartnerAuth.createTinkoffAuthIntent(callbackUrl) 18 | startActivity(intent) 19 | } else { 20 | // The logic of disabling authorization via Tinkoff 21 | } 22 | ``` 23 | 24 | **After**: 25 | 26 | ```kotlin 27 | val intent = tinkoffPartnerAuth.createTinkoffAuthIntent(callbackUrl) 28 | startActivity(intent) 29 | ``` 30 | 31 | Изменения в методах класса `TinkoffIdAuth`: 32 | - `createTinkoffAuthIntent(callbackUrl: Uri): Intent` -> `createTinkoffAppAuthIntent(callbackUrl: Uri): Intent` 33 | - `isTinkoffAuthAvailable(): Boolean` -> `isTinkoffAppAuthAvailable(): Boolean` 34 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/error/TokenSignOutErrorConstants.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 Tinkoff Bank 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ru.tinkoff.core.tinkoffId.error 18 | 19 | /** 20 | * Endpoint /auth/revoke. 21 | * Error types when revoking token 22 | * 23 | * @author Stanislav Mukhametshin 24 | */ 25 | public object TokenSignOutErrorConstants { 26 | 27 | /** 28 | * User is not contains in audience 29 | */ 30 | public const val INVALID_GRANT: Int = 1 31 | 32 | /** 33 | * There is no token in request 34 | */ 35 | public const val INVALID_REQUEST: Int = 2 36 | 37 | /** 38 | * Unknown type of error 39 | */ 40 | public const val UNKNOWN_ERROR: Int = 3 41 | } 42 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | android.useAndroidX=true 11 | android.enableJetifier=true 12 | kotlin.code.style=official 13 | 14 | POM_DESCRIPTION=Android library for Tinkoff authorization 15 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 16 | POM_LICENCE_URL=https://github.com/tinkoff-mobile-tech/TinkoffID-Android/blob/master/LICENSE 17 | POM_LICENCE_DIST=repo 18 | POM_DEVELOPER_ID=tcs 19 | POM_DEVELOPER_NAME=Tinkoff Credit Systems 20 | POM_DEVELOPER_EMAIL=Tinkoff_id@tinkoff.ru 21 | POM_URL=https://github.com/tinkoff-mobile-tech/TinkoffID-Android 22 | POM_SCM_URL=https://github.com/tinkoff-mobile-tech/TinkoffID-Android/tree/main 23 | POM_SCM_CONNECTION=scm:git:git://github.com/tinkoff-mobile-tech/TinkoffID-Android.git 24 | POM_SCM_DEV_CONNECTION=scm:git:ssh://github.com:tinkoff-mobile-tech/TinkoffID-Android.git 25 | 26 | VERSION_NAME=1.1.1 27 | VERSION_CODE=8 28 | GROUP=ru.tinkoff.core.tinkoffauth 29 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable/tinkoff_id_close.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/codeVerifier/CodeVerifierStore.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 Tinkoff Bank 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ru.tinkoff.core.tinkoffId.codeVerifier 18 | 19 | import android.content.Context 20 | import android.content.SharedPreferences 21 | 22 | /** 23 | * @author Stanislav Mukhametshin 24 | */ 25 | internal class CodeVerifierStore(context: Context) { 26 | 27 | private val preference: SharedPreferences = context.getSharedPreferences(PREFS_FILENAME, Context.MODE_PRIVATE) 28 | 29 | internal var codeVerifier: String 30 | get() = requireNotNull(preference.getString(CODE_VERIFIER, "")) 31 | set(value) = preference.edit().putString(CODE_VERIFIER, value).apply() 32 | 33 | companion object { 34 | 35 | private const val PREFS_FILENAME = "prefs_tinkoff_partner" 36 | private const val CODE_VERIFIER = "code_verifier" 37 | } 38 | } -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/drawable/tinkoff_id_reload_icon.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 26 | 27 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/values/attr.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/TinkoffCall.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 Tinkoff Bank 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ru.tinkoff.core.tinkoffId 18 | 19 | import androidx.annotation.WorkerThread 20 | import ru.tinkoff.core.tinkoffId.error.TinkoffRequestException 21 | 22 | /** 23 | * @author Stanislav Mukhametshin 24 | * 25 | * Main class to perform requests from Tinkoff Api 26 | */ 27 | public interface TinkoffCall { 28 | 29 | /** 30 | * Function for synchronous operations 31 | * 32 | * NOTE should not be called from the main thread 33 | * 34 | * @return T 35 | * 36 | * @throws TinkoffRequestException if something goes wrong. 37 | * It can contain message [TinkoffErrorMessage][ru.tinkoff.core.tinkoffId.error.TinkoffErrorMessage] 38 | * with problem description 39 | */ 40 | @WorkerThread 41 | @Throws(TinkoffRequestException::class) 42 | public fun getResponse(): T 43 | 44 | /** 45 | * Function to cancel [TinkoffCall] 46 | */ 47 | public fun cancel() 48 | } 49 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | #333333 21 | #FFDD2D 22 | #FAB619 23 | #FFEE95 24 | 25 | #333333 26 | #F5F5F6 27 | #CECECF 28 | #FFFFFF 29 | 30 | #FFFFFF 31 | #000000 32 | #333333 33 | #3D3D3D 34 | 35 | #428BF9 36 | #FFFFFF 37 | 38 | 39 | -------------------------------------------------------------------------------- /gradle/plugin/android.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply from: '../gradle/plugin/kotlin-library.gradle' 3 | apply plugin: 'org.jetbrains.dokka' 4 | 5 | android { 6 | compileSdkVersion rootProject.compileSdkVersion 7 | 8 | defaultConfig { 9 | minSdkVersion rootProject.minSdkVersion 10 | targetSdkVersion rootProject.compileSdkVersion 11 | multiDexEnabled = true 12 | versionCode rootProject.authCode 13 | versionName rootProject.authVersion 14 | buildConfigField 'String', 'VERSION_NAME', "\"${rootProject.authVersion}\"" 15 | vectorDrawables.useSupportLibrary = true 16 | } 17 | 18 | compileOptions { 19 | sourceCompatibility JavaVersion.VERSION_1_8 20 | targetCompatibility JavaVersion.VERSION_1_8 21 | } 22 | 23 | lintOptions { 24 | htmlReport true 25 | abortOnError true 26 | warningsAsErrors true 27 | disable 'GradleDependency' 28 | } 29 | 30 | packagingOptions { 31 | exclude 'LICENSE.txt' 32 | exclude 'META-INF/LICENSE.txt' 33 | exclude 'META-INF/NOTICE.txt' 34 | exclude 'META-INF/androidx.exifinterface_exifinterface.version' 35 | } 36 | 37 | testOptions { 38 | unitTests { 39 | returnDefaultValues = true 40 | includeAndroidResources = true 41 | } 42 | } 43 | 44 | kotlinOptions { 45 | freeCompilerArgs += '-Xexplicit-api=strict' 46 | } 47 | 48 | dokkaHtml.configure { 49 | dokkaSourceSets { 50 | configureEach { 51 | includeNonPublic.set(false) 52 | noAndroidSdkLink.set(false) 53 | suppressInheritedMembers.set(true) 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app-demo/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply from: rootProject.file('gradle/versions.gradle') 4 | apply from: rootProject.file('gradle/plugin/detekt.gradle') 5 | 6 | android { 7 | compileSdkVersion rootProject.ext.compileSdkVersion 8 | 9 | defaultConfig { 10 | multiDexEnabled = true 11 | minSdkVersion rootProject.minSdkVersion 12 | targetSdkVersion rootProject.targetSdkVersion 13 | versionCode rootProject.authCode 14 | versionName rootProject.authVersion 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | 29 | } 30 | 31 | dependencies { 32 | api project(':tinkoff-id') 33 | api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" 34 | api "androidx.appcompat:appcompat:${appCompatVersion}" 35 | api "androidx.constraintlayout:constraintlayout:${constraintLayoutVersion}" 36 | api "com.google.android.material:material:${materialVersion}" 37 | //rx 38 | api "io.reactivex.rxjava2:rxjava:$rxJavaVersion" 39 | api "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion" 40 | //tests 41 | androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" 42 | androidTestImplementation "androidx.test:rules:$testRuleVersion" 43 | androidTestImplementation "androidx.test.uiautomator:uiautomator:$uiautomatorVersion" 44 | } 45 | -------------------------------------------------------------------------------- /app-demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 18 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app-demo/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/error/TinkoffTokenErrorConstants.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 Tinkoff Bank 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ru.tinkoff.core.tinkoffId.error 18 | 19 | /** 20 | * Endpoint /auth/token. 21 | * Error types when refreshing or obtaining token 22 | * 23 | * @author Stanislav Mukhametshin 24 | */ 25 | public object TinkoffTokenErrorConstants { 26 | 27 | /** 28 | * There is not required parameters, cookies, headers and so on 29 | */ 30 | public const val INVALID_REQUEST: Int = 1 31 | 32 | /** 33 | * redirect_uri does not match the client 34 | */ 35 | public const val INVALID_CLIENT: Int = 2 36 | 37 | /** 38 | * Invalid/expired refresh_token or code passed 39 | */ 40 | public const val INVALID_GRANT: Int = 3 41 | 42 | /** 43 | * No required headers 44 | */ 45 | public const val UNAUTHORIZED_CLIENT: Int = 4 46 | 47 | /** 48 | * Unknown grant_type passed 49 | */ 50 | public const val UNSUPPORTED_GRANT_TYPE: Int = 5 51 | 52 | /** 53 | * Something went wrong. You can try to repeat request again 54 | */ 55 | public const val SERVER_ERROR: Int = 6 56 | 57 | /** 58 | * The app is asking for tokens too often (current limit is 50 per hour), it might be worth looking for a bug in the app 59 | */ 60 | public const val LIMIT_EXCEEDED: Int = 7 61 | 62 | /** 63 | * Unknown error type returned 64 | */ 65 | public const val UNKNOWN_ERROR: Int = 8 66 | } 67 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/ui/webView/TinkoffWebViewClient.kt: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.core.tinkoffId.ui.webView 2 | 3 | import android.webkit.CookieManager 4 | import android.webkit.RenderProcessGoneDetail 5 | import android.webkit.WebResourceRequest 6 | import android.webkit.WebView 7 | import android.webkit.WebViewClient 8 | 9 | /** 10 | * @author k.voskrebentsev 11 | */ 12 | internal class TinkoffWebViewClient( 13 | private val listener: TinkoffWebViewListener, 14 | ) : WebViewClient() { 15 | 16 | private var lastUrl: String? = null 17 | 18 | override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { 19 | return processUrl(request?.url.toString()) 20 | } 21 | 22 | @Deprecated("Deprecated in Java") 23 | override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { 24 | return processUrl(url) 25 | } 26 | 27 | private fun processUrl(url: String?): Boolean { 28 | if (url == null || !listener.isUrlForAuthCompletion(url)) { 29 | lastUrl = url 30 | return false 31 | } 32 | 33 | lastUrl?.let { clearCookies(it) } 34 | listener.completeAuthWithSuccess(url) 35 | return true 36 | } 37 | 38 | private fun clearCookies(url: String) { 39 | val cookieManager = CookieManager.getInstance() 40 | val cookiesNames = CookieManager.getInstance().getCookie(url) 41 | .split(COOKIES_SEPARATOR) 42 | .map { cookie -> 43 | val endIndexOfCookieName = cookie.indexOf(COOKIE_NAME_AND_VALUE_SEPARATOR) 44 | cookie.substring(0, endIndexOfCookieName) 45 | } 46 | cookiesNames.forEach { cookieName -> 47 | cookieManager.setCookie(url, "$cookieName$COOKIE_NAME_AND_VALUE_SEPARATOR") 48 | } 49 | cookieManager.flush() 50 | } 51 | 52 | override fun onRenderProcessGone(view: WebView?, detail: RenderProcessGoneDetail?): Boolean { 53 | listener.completeAuthWithCancellation() 54 | return true 55 | } 56 | 57 | private companion object { 58 | const val COOKIES_SEPARATOR = "; " 59 | const val COOKIE_NAME_AND_VALUE_SEPARATOR = "=" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tinkoff-id/src/test/java/ru/tinkoff/core/tinkoffId/TinkoffIdSignInButtonTest.kt: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.core.tinkoffId 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.text.SpannedString 6 | import androidx.core.content.res.ResourcesCompat 7 | import androidx.test.core.app.ApplicationProvider 8 | import org.junit.Assert.assertEquals 9 | import org.junit.Before 10 | import org.junit.Test 11 | import org.junit.runner.RunWith 12 | import org.robolectric.RobolectricTestRunner 13 | import org.robolectric.annotation.Config 14 | import ru.tinkoff.core.tinkoffId.ui.TinkoffDimen 15 | import ru.tinkoff.core.tinkoffId.ui.TinkoffIdSignInButton 16 | 17 | /** 18 | * @author k.voskrebentsev 19 | */ 20 | @Config(sdk = [Build.VERSION_CODES.P]) 21 | @RunWith(RobolectricTestRunner::class) 22 | internal class TinkoffIdSignInButtonTest { 23 | 24 | private lateinit var button: TinkoffIdSignInButton 25 | 26 | @Before 27 | fun setUp() { 28 | button = TinkoffIdSignInButton(context = context) 29 | } 30 | 31 | @Test 32 | fun testSetEmptyTitle() { 33 | button.title = "" 34 | 35 | val result = button.title as SpannedString 36 | 37 | assertEquals(PERMANENT_TITLE_PART, result.toString()) 38 | } 39 | 40 | @Test 41 | fun testSetTitle() { 42 | button.title = CUSTOM_TITLE_PART 43 | 44 | val result = button.title as SpannedString 45 | 46 | assertEquals("$CUSTOM_TITLE_PART $PERMANENT_TITLE_PART", result.toString()) 47 | } 48 | 49 | @Test 50 | fun testDefaultBadgeText() { 51 | assertEquals("", button.badgeText) 52 | } 53 | 54 | @Test 55 | fun testDefaultCompact() { 56 | assertEquals(false, button.isCompact) 57 | } 58 | 59 | @Test 60 | fun testDefaultStyle() { 61 | assertEquals(TinkoffIdSignInButton.ButtonStyle.YELLOW, button.style) 62 | } 63 | 64 | @Test 65 | fun testDefaultCornerSize() { 66 | assertEquals(context.resources.getDimension(TinkoffDimen.tinkoff_id_default_corner_radius).toInt(), button.cornerRadius) 67 | } 68 | 69 | @Test 70 | fun testDefaultFont() { 71 | assertEquals(ResourcesCompat.getFont(context, R.font.neue_haas_unica_w1g), button.textFont) 72 | } 73 | 74 | companion object { 75 | val context: Context = ApplicationProvider.getApplicationContext() 76 | 77 | const val CUSTOM_TITLE_PART = "title" 78 | val PERMANENT_TITLE_PART = context.getString(R.string.tinkoff_id_tinkoff_text) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /tinkoff-id/src/test/java/ru/tinkoff/core/tinkoffId/TinkoffIdApiTest.kt: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.core.tinkoffId 2 | 3 | import android.net.Uri 4 | import android.os.Build 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | import org.robolectric.RobolectricTestRunner 9 | import org.robolectric.annotation.Config 10 | import ru.tinkoff.core.tinkoffId.api.TinkoffIdApi 11 | 12 | /** 13 | * @author k.voskrebentsev 14 | */ 15 | @Config(sdk = [Build.VERSION_CODES.P]) 16 | @RunWith(RobolectricTestRunner::class) 17 | internal class TinkoffIdApiTest { 18 | 19 | @Test 20 | fun testWebViewUrlCreation() { 21 | val result = TinkoffIdApi.buildWebViewAuthStartUrl( 22 | clientId = VALUE_CLIENT_ID, 23 | codeChallenge = VALUE_CODE_CHALLENGE, 24 | codeChallengeMethod = VALUE_CODE_CHALLENGE_METHOD, 25 | redirectUri = VALUE_REDIRECT_URI, 26 | ) 27 | 28 | val uri = Uri.parse(result) 29 | assertEquals(HOST, "${uri.scheme}://${uri.host}") 30 | assertEquals("/$PATH_AUTHORIZE", uri.path) 31 | uri.assertQueryParam(FIELD_CLIENT_ID, VALUE_CLIENT_ID) 32 | uri.assertQueryParam(FIELD_CODE_CHALLENGE, VALUE_CODE_CHALLENGE) 33 | uri.assertQueryParam(FIELD_CODE_CHALLENGE_METHOD, VALUE_CODE_CHALLENGE_METHOD) 34 | uri.assertQueryParam(FIELD_REDIRECT_URI, VALUE_REDIRECT_URI) 35 | uri.assertQueryParam(FIELD_RESPONSE_TYPE, VALUE_RESPONSE_TYPE) 36 | uri.assertQueryParam(FIELD_RESPONSE_MODE, VALUE_RESPONSE_MODE) 37 | } 38 | 39 | @Test 40 | fun testCodeParsing() { 41 | val url = Uri.parse(HOST).buildUpon() 42 | .appendQueryParameter(FIELD_CODE, VALUE_CODE) 43 | .build() 44 | .toString() 45 | 46 | val result = TinkoffIdApi.parseCode(url) 47 | 48 | assertEquals(VALUE_CODE, result) 49 | } 50 | 51 | @Test(expected = IllegalArgumentException::class) 52 | fun testCodeAbsence() { 53 | TinkoffIdApi.parseCode("") 54 | } 55 | 56 | private fun Uri.assertQueryParam(paramName: String, expectedValue: String) { 57 | val actualValue = getQueryParameter(paramName) 58 | assertEquals(expectedValue, actualValue) 59 | } 60 | 61 | companion object { 62 | private const val HOST = "https://id.tinkoff.ru" 63 | private const val PATH_AUTHORIZE = "auth/authorize" 64 | 65 | private const val FIELD_CLIENT_ID = "client_id" 66 | private const val FIELD_REDIRECT_URI = "redirect_uri" 67 | private const val FIELD_CODE_CHALLENGE = "code_challenge" 68 | private const val FIELD_CODE_CHALLENGE_METHOD = "code_challenge_method" 69 | private const val FIELD_RESPONSE_TYPE = "response_type" 70 | private const val FIELD_RESPONSE_MODE = "response_mode" 71 | private const val FIELD_CODE = "code" 72 | 73 | private const val VALUE_CLIENT_ID = "client_id_value" 74 | private const val VALUE_CODE_CHALLENGE = "code_challenge_value" 75 | private const val VALUE_CODE_CHALLENGE_METHOD = "code_challenge_method_value" 76 | private const val VALUE_REDIRECT_URI = "redirect_uri_value" 77 | private const val VALUE_CODE = "code_value" 78 | private const val VALUE_RESPONSE_TYPE = "code" 79 | private const val VALUE_RESPONSE_MODE = "query" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 8dp 21 | 22 | 30dp 23 | 40dp 24 | 60dp 25 | 26 | 5dp 27 | 10dp 28 | 16dp 29 | 10dp 30 | 13dp 31 | 20dp 32 | 33 | 12sp 34 | 15sp 35 | 19sp 36 | 37 | 4dp 38 | 8dp 39 | 8dp 40 | 41 | 15dp 42 | 30dp 43 | 19dp 44 | 37dp 45 | 22dp 46 | 45dp 47 | 2dp 48 | 49 | 20dp 50 | 26dp 51 | 40dp 52 | 53 | 11sp 54 | 11sp 55 | 13sp 56 | 2.5dp 57 | 4dp 58 | 4.5dp 59 | 6dp 60 | 5.5dp 61 | 8dp 62 | 14dp 63 | 64 | 12dp 65 | 10dp 66 | 67 | 68 | -------------------------------------------------------------------------------- /gradle/plugin/publish-lib.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Tinkoff Bank 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: 'maven-publish' 18 | apply plugin: 'signing' 19 | 20 | def isReleaseBuild() { 21 | return !VERSION_NAME.contains("SNAPSHOT") 22 | } 23 | 24 | def getReleaseRepositoryUrl() { 25 | return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL 26 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 27 | } 28 | 29 | def getRepositoryUsername() { 30 | return hasProperty('ossrhUsername') ? ossrhUsername : '' 31 | } 32 | 33 | def getRepositoryPassword() { 34 | return hasProperty('ossrhPassword') ? ossrhPassword : '' 35 | } 36 | 37 | afterEvaluate { 38 | publishing { 39 | publications { 40 | release(MavenPublication) { 41 | groupId GROUP 42 | artifactId POM_ARTIFACT_ID 43 | version VERSION_NAME 44 | 45 | from components.release 46 | 47 | afterEvaluate { 48 | artifact androidSourcesJar 49 | artifact androidJavadocsJar 50 | } 51 | 52 | pom { 53 | name = POM_NAME 54 | packaging = POM_PACKAGING 55 | description = POM_DESCRIPTION 56 | url = POM_URL 57 | scm { 58 | url = POM_SCM_URL 59 | connection = POM_SCM_CONNECTION 60 | developerConnection = POM_SCM_DEV_CONNECTION 61 | } 62 | licenses { 63 | license { 64 | name = POM_LICENCE_NAME 65 | url = POM_LICENCE_URL 66 | distribution = POM_LICENCE_DIST 67 | } 68 | } 69 | developers { 70 | developer { 71 | id = POM_DEVELOPER_ID 72 | name = POM_DEVELOPER_NAME 73 | email = POM_DEVELOPER_EMAIL 74 | } 75 | } 76 | } 77 | } 78 | } 79 | repositories { 80 | maven { 81 | url = getReleaseRepositoryUrl() 82 | credentials { 83 | username = getRepositoryUsername() 84 | password = getRepositoryPassword() 85 | } 86 | } 87 | } 88 | } 89 | 90 | task androidSourcesJar(type: Jar) { 91 | archiveClassifier.set("sources") 92 | from android.sourceSets.main.java.srcDirs 93 | } 94 | 95 | task androidJavadocsJar(type: Jar, dependsOn: dokkaJavadoc) { 96 | archiveClassifier.set("javadoc") 97 | from dokkaJavadoc.outputDirectory 98 | } 99 | 100 | signing { 101 | required { isReleaseBuild() && gradle.taskGraph.hasTask("publish") } 102 | sign publishing.publications.release 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tinkoff-id/src/test/java/ru/tinkoff/core/tinkoffId/AppLinkUtilTest.kt: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.core.tinkoffId 2 | 3 | import android.content.Intent 4 | import android.os.Build 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | import org.robolectric.RobolectricTestRunner 9 | import org.robolectric.annotation.Config 10 | 11 | /** 12 | * @author k.voskrebentsev 13 | */ 14 | @Config(sdk = [Build.VERSION_CODES.P]) 15 | @RunWith(RobolectricTestRunner::class) 16 | internal class AppLinkUtilTest { 17 | 18 | @Test 19 | fun testWebViewIntentParsing() { 20 | val intent = Intent() 21 | .putExtra(QUERY_PARAMETER_CLIENT_ID, VALUE_PARAMETER_CLIENT_ID) 22 | .putExtra(QUERY_PARAMETER_CODE_CHALLENGE, VALUE_PARAMETER_CODE_CHALLENGE) 23 | .putExtra(QUERY_PARAMETER_CODE_CHALLENGE_METHOD, VALUE_PARAMETER_CODE_CHALLENGE_METHOD) 24 | .putExtra(QUERY_PARAMETER_REDIRECT_URI, VALUE_PARAMETER_REDIRECT_URI) 25 | .putExtra(QUERY_PARAMETER_CALLBACK_URL, VALUE_PARAMETER_CALLBACK_URL) 26 | 27 | val result = AppLinkUtil.parseTinkoffWebViewUiData(intent) 28 | 29 | assertEquals(VALUE_PARAMETER_CLIENT_ID, result.clientId) 30 | assertEquals(VALUE_PARAMETER_CODE_CHALLENGE, result.codeChallenge) 31 | assertEquals(VALUE_PARAMETER_CODE_CHALLENGE_METHOD, result.codeChallengeMethod) 32 | assertEquals(VALUE_PARAMETER_REDIRECT_URI, result.redirectUri) 33 | assertEquals(VALUE_PARAMETER_CALLBACK_URL, result.callbackUrl) 34 | } 35 | 36 | @Test 37 | fun testCodeIntentCreation() { 38 | val code = "code_value" 39 | 40 | val result = AppLinkUtil.createBackAppCodeIntent( 41 | callbackUrl = VALUE_PARAMETER_CALLBACK_URL, 42 | code = code, 43 | ) 44 | 45 | assertEquals(Intent.ACTION_VIEW, result.action) 46 | assertEquals(VALUE_PARAMETER_CALLBACK_URL, result.data?.path) 47 | assertEquals(code, result.data?.getQueryParameter(QUERY_PARAMETER_CODE)) 48 | assertEquals(AUTH_STATUS_CODE_SUCCESS, result.data?.getQueryParameter(QUERY_PARAMETER_AUTH_STATUS_CODE)) 49 | } 50 | 51 | @Test 52 | fun testCancelIntentCreation() { 53 | val result = AppLinkUtil.createBackAppCancelIntent( 54 | callbackUrl = VALUE_PARAMETER_CALLBACK_URL, 55 | ) 56 | 57 | assertEquals(Intent.ACTION_VIEW, result.action) 58 | assertEquals(VALUE_PARAMETER_CALLBACK_URL, result.data?.path) 59 | assertEquals(AUTH_STATUS_CODE_CANCELLED_BY_USER, result.data?.getQueryParameter(QUERY_PARAMETER_AUTH_STATUS_CODE)) 60 | } 61 | 62 | companion object { 63 | private const val QUERY_PARAMETER_CLIENT_ID = "clientId" 64 | private const val QUERY_PARAMETER_CODE_CHALLENGE = "code_challenge" 65 | private const val QUERY_PARAMETER_CODE_CHALLENGE_METHOD = "code_challenge_method" 66 | private const val QUERY_PARAMETER_CALLBACK_URL = "callback_url" 67 | private const val QUERY_PARAMETER_REDIRECT_URI = "redirect_uri" 68 | 69 | private const val VALUE_PARAMETER_CLIENT_ID = "clientId_value" 70 | private const val VALUE_PARAMETER_CODE_CHALLENGE = "code_challenge_value" 71 | private const val VALUE_PARAMETER_CODE_CHALLENGE_METHOD = "code_challenge_method_value" 72 | private const val VALUE_PARAMETER_CALLBACK_URL = "callback_url_value" 73 | private const val VALUE_PARAMETER_REDIRECT_URI = "redirect_uri_value" 74 | 75 | private const val QUERY_PARAMETER_CODE = "code" 76 | private const val QUERY_PARAMETER_AUTH_STATUS_CODE = "auth_status_code" 77 | 78 | private const val AUTH_STATUS_CODE_SUCCESS = "success" 79 | private const val AUTH_STATUS_CODE_CANCELLED_BY_USER = "cancelled_by_user" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app-demo/src/androidTest/java/ru/tinkoff/core/app_demo_partner/TinkoffIdSignInButtonTest.kt: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.core.app_demo_partner 2 | 3 | import androidx.test.espresso.Espresso.onView 4 | import androidx.test.espresso.assertion.ViewAssertions.matches 5 | import androidx.test.espresso.matcher.ViewMatchers 6 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed 7 | import androidx.test.espresso.matcher.ViewMatchers.withChild 8 | import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility 9 | import androidx.test.espresso.matcher.ViewMatchers.withId 10 | import androidx.test.espresso.matcher.ViewMatchers.withText 11 | import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner 12 | import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation 13 | import androidx.test.rule.ActivityTestRule 14 | import androidx.test.runner.AndroidJUnitRunner 15 | import org.hamcrest.CoreMatchers.allOf 16 | import org.junit.Before 17 | import org.junit.Rule 18 | import org.junit.Test 19 | import org.junit.runner.RunWith 20 | import ru.tinkoff.core.tinkoffId.ui.TinkoffIdSignInButton 21 | 22 | /** 23 | * @author k.voskrebentsev 24 | */ 25 | @RunWith(AndroidJUnit4ClassRunner::class) 26 | class TinkoffIdSignInButtonTest : AndroidJUnitRunner() { 27 | 28 | @get:Rule 29 | val activityRule = ActivityTestRule(PartnerActivity::class.java, false, true) 30 | 31 | private lateinit var standardButton: TinkoffIdSignInButton 32 | 33 | @Before 34 | fun setUp() { 35 | runOnMainThread { 36 | standardButton = activityRule.activity.findViewById(standardButtonId) 37 | } 38 | } 39 | 40 | @Test 41 | fun testTitleChanging() { 42 | runOnMainThread { 43 | standardButton.title = SOME_TEXT 44 | } 45 | 46 | checkTextOnView(createFinalTitle(SOME_TEXT)) 47 | } 48 | 49 | @Test 50 | fun testBadgeTextChanging() { 51 | runOnMainThread { 52 | standardButton.badgeText = SOME_TEXT 53 | } 54 | 55 | checkTextOnView(SOME_TEXT) 56 | } 57 | 58 | @Test 59 | fun testTextElementsHidingInCompactMode() { 60 | runOnMainThread { 61 | standardButton.title = SOME_TEXT 62 | standardButton.badgeText = SOME_TEXT 63 | standardButton.isCompact = true 64 | } 65 | 66 | onView( 67 | allOf( 68 | withId(standardButtonId), 69 | // title element 70 | withChild( 71 | allOf( 72 | withText(createFinalTitle(SOME_TEXT)), 73 | withEffectiveVisibility(ViewMatchers.Visibility.GONE) 74 | ) 75 | ), 76 | // badge element 77 | withChild( 78 | allOf( 79 | withText(SOME_TEXT), 80 | withEffectiveVisibility(ViewMatchers.Visibility.GONE) 81 | ) 82 | ), 83 | ) 84 | ).check(matches(isDisplayed())) 85 | } 86 | 87 | private fun checkTextOnView(text: String) { 88 | onView( 89 | allOf( 90 | withId(standardButtonId), 91 | withChild(withText(text)) 92 | ) 93 | ).check(matches(isDisplayed())) 94 | } 95 | 96 | private fun createFinalTitle(titlePart: String) = "$titlePart $permanentTitlePart" 97 | private fun runOnMainThread(block: () -> Unit) { 98 | activityRule.runOnUiThread(block) 99 | } 100 | 101 | private companion object { 102 | private const val SOME_TEXT = "test" 103 | 104 | private const val standardButtonId = R.id.standardSmallBlackButtonTinkoffAuth 105 | 106 | private val permanentTitlePart = getInstrumentation().targetContext.resources.getString(ru.tinkoff.core.tinkoffId.R.string.tinkoff_id_tinkoff_text) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tinkoff-id/src/test/java/ru/tinkoff/core/tinkoffId/TinkoffIdAuthTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 Tinkoff Bank 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ru.tinkoff.core.tinkoffId 18 | 19 | import android.content.Context 20 | import android.net.Uri 21 | import android.os.Build 22 | import androidx.test.core.app.ApplicationProvider 23 | import com.google.common.truth.Truth.assertThat 24 | import org.junit.Test 25 | import org.junit.runner.RunWith 26 | import org.robolectric.RobolectricTestRunner 27 | import org.robolectric.annotation.Config 28 | 29 | /** 30 | * @author Stanislav Mukhametshin 31 | */ 32 | @Config(sdk = [Build.VERSION_CODES.P]) 33 | @RunWith(RobolectricTestRunner::class) 34 | public class TinkoffIdAuthTest { 35 | 36 | private var context: Context = ApplicationProvider.getApplicationContext() 37 | private val tinkoffAuth: TinkoffIdAuth = TinkoffIdAuth(context, CLIENT_ID, REDIRECT_URL) 38 | private val testUri: Uri = Uri.Builder() 39 | .scheme("https") 40 | .authority("www.partner.com") 41 | .appendPath("partner") 42 | .build() 43 | 44 | @Test 45 | public fun testIntentCreation() { 46 | val intent = tinkoffAuth.createTinkoffAppAuthIntent(testUri) 47 | assertThat(intent.data).isNotNull() 48 | val queryParam = { paramName: String -> requireNotNull(intent.data).getQueryParameter(paramName) } 49 | assertThat(queryParam("clientId")).isEqualTo(CLIENT_ID) 50 | assertThat(queryParam("code_challenge")).isNotEmpty() 51 | assertThat(queryParam("code_challenge_method")).isNotEmpty() 52 | assertThat(queryParam("redirect_uri")).isEqualTo(REDIRECT_URL) 53 | assertThat(queryParam("callback_url")).isEqualTo(testUri.toString()) 54 | assertThat(queryParam("package_name")).isNotEmpty() 55 | assertThat(queryParam("partner_sdk_version")).isEqualTo(BuildConfig.VERSION_NAME) 56 | } 57 | 58 | @Test 59 | public fun testWebViewIntentCreation() { 60 | val intent = tinkoffAuth.createTinkoffWebViewAuthIntent(testUri) 61 | assertThat(intent.extras).isNotNull() 62 | val queryParam = { paramName: String -> requireNotNull(intent.getStringExtra(paramName)) } 63 | assertThat(queryParam("clientId")).isEqualTo(CLIENT_ID) 64 | assertThat(queryParam("code_challenge")).isNotEmpty() 65 | assertThat(queryParam("code_challenge_method")).isNotEmpty() 66 | assertThat(queryParam("redirect_uri")).isEqualTo(REDIRECT_URL) 67 | assertThat(queryParam("callback_url")).isEqualTo(testUri.toString()) 68 | } 69 | 70 | @Test 71 | public fun testStatusWhenReturningBack() { 72 | listOf(AUTH_STATUS_CODE_SUCCESS to TinkoffIdStatusCode.SUCCESS, AUTH_STATUS_CODE_CANCELLED_BY_USER to TinkoffIdStatusCode.CANCELLED_BY_USER) 73 | .forEach { (param, status) -> 74 | val uri = testUri.buildUpon().appendQueryParameter(QUERY_PARAMETER_AUTH_STATUS_CODE, param).build() 75 | assertThat(tinkoffAuth.getStatusCode(uri)).isEqualTo(status) 76 | } 77 | } 78 | 79 | private companion object { 80 | 81 | private const val CLIENT_ID = "testClientId" 82 | private const val REDIRECT_URL = "mobile://redirectUrl" 83 | private const val QUERY_PARAMETER_AUTH_STATUS_CODE = "auth_status_code" 84 | 85 | private const val AUTH_STATUS_CODE_SUCCESS = "success" 86 | private const val AUTH_STATUS_CODE_CANCELLED_BY_USER = "cancelled_by_user" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/ui/webView/TinkoffWebViewAuthActivity.kt: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.core.tinkoffId.ui.webView 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.webkit.WebSettings.LOAD_NO_CACHE 7 | import android.webkit.WebView 8 | import androidx.activity.OnBackPressedCallback 9 | import androidx.appcompat.app.AppCompatActivity 10 | import androidx.appcompat.widget.Toolbar 11 | import ru.tinkoff.core.tinkoffId.AppLinkUtil 12 | import ru.tinkoff.core.tinkoffId.R 13 | 14 | /** 15 | * @author k.voskrebentsev 16 | */ 17 | internal class TinkoffWebViewAuthActivity : AppCompatActivity() { 18 | 19 | private val presenter: TinkoffWebViewAuthPresenter by lazy { TinkoffWebViewAuthPresenter() } 20 | 21 | private lateinit var webView: WebView 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | setContentView(R.layout.tinkoff_id_web_view_activity) 26 | 27 | val uiData = AppLinkUtil.parseTinkoffWebViewUiData(intent) 28 | 29 | initWebView(uiData) 30 | initToolbar(uiData) 31 | initBackPress(uiData) 32 | } 33 | 34 | private fun initToolbar(uiData: TinkoffWebViewUiData) { 35 | val toolbar = findViewById(R.id.toolbar) 36 | 37 | toolbar.inflateMenu(R.menu.tinkoff_id_web_view_auth_menu) 38 | toolbar.setOnMenuItemClickListener { menuItem -> 39 | if (menuItem.itemId == R.id.reloadMenuItem) { 40 | webView.reload() 41 | true 42 | } else { 43 | false 44 | } 45 | } 46 | 47 | toolbar.setNavigationOnClickListener { 48 | finishWithCancellation(uiData.callbackUrl) 49 | } 50 | } 51 | 52 | private fun initBackPress(uiData: TinkoffWebViewUiData) { 53 | val callback = object : OnBackPressedCallback(true) { 54 | override fun handleOnBackPressed() { 55 | finishWithCancellation(uiData.callbackUrl) 56 | } 57 | } 58 | onBackPressedDispatcher.addCallback(callback) 59 | } 60 | 61 | @SuppressLint("SetJavaScriptEnabled") 62 | private fun initWebView(uiData: TinkoffWebViewUiData) { 63 | webView = findViewById(R.id.webView) 64 | val url = presenter.buildWebViewAuthStartUrl(uiData) 65 | webView.run { 66 | webViewClient = TinkoffWebViewClient(createTinkoffWebViewCallback(uiData)) 67 | with(settings) { 68 | javaScriptEnabled = true 69 | setGeolocationEnabled(false) 70 | cacheMode = LOAD_NO_CACHE 71 | allowFileAccess = false 72 | allowContentAccess = false 73 | } 74 | loadUrl(url) 75 | } 76 | } 77 | 78 | private fun createTinkoffWebViewCallback(uiData: TinkoffWebViewUiData): TinkoffWebViewListener { 79 | return object : TinkoffWebViewListener { 80 | 81 | override fun isUrlForAuthCompletion(url: String): Boolean { 82 | return url.startsWith(uiData.redirectUri) 83 | } 84 | 85 | override fun completeAuthWithSuccess(url: String) { 86 | finish( 87 | intent = AppLinkUtil.createBackAppCodeIntent( 88 | callbackUrl = uiData.callbackUrl, 89 | code = presenter.parseCode(url), 90 | ) 91 | ) 92 | } 93 | 94 | override fun completeAuthWithCancellation() { 95 | finishWithCancellation(uiData.callbackUrl) 96 | } 97 | } 98 | } 99 | 100 | private fun finishWithCancellation(callbackUrl: String) { 101 | finish( 102 | intent = AppLinkUtil.createBackAppCancelIntent(callbackUrl) 103 | ) 104 | } 105 | 106 | private fun finish(intent: Intent) { 107 | intent.setPackage(packageName) 108 | startActivity(intent) 109 | finish() 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app-demo/src/main/java/ru/tinkoff/core/app_demo_partner/PartnerPresenter.kt: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.core.app_demo_partner 2 | 3 | import android.net.Uri 4 | import android.util.Log 5 | import androidx.lifecycle.DefaultLifecycleObserver 6 | import androidx.lifecycle.LifecycleOwner 7 | import io.reactivex.Single 8 | import io.reactivex.android.schedulers.AndroidSchedulers 9 | import io.reactivex.disposables.CompositeDisposable 10 | import io.reactivex.schedulers.Schedulers 11 | import ru.tinkoff.core.tinkoffId.TinkoffCall 12 | import ru.tinkoff.core.tinkoffId.TinkoffIdAuth 13 | import ru.tinkoff.core.tinkoffId.TinkoffIdStatusCode 14 | import ru.tinkoff.core.tinkoffId.TinkoffTokenPayload 15 | 16 | /** 17 | * @author Stanislav Mukhametshin 18 | */ 19 | class PartnerPresenter( 20 | private val tinkoffPartnerAuth: TinkoffIdAuth, 21 | private val partnerActivity: PartnerActivity 22 | ) : DefaultLifecycleObserver { 23 | 24 | private val compositeDisposable = CompositeDisposable() 25 | private var tokenPayload: TinkoffTokenPayload? = null 26 | 27 | init { 28 | partnerActivity.lifecycle.addObserver(this) 29 | } 30 | 31 | fun getToken(uri: Uri) { 32 | when (tinkoffPartnerAuth.getStatusCode(uri)) { 33 | TinkoffIdStatusCode.SUCCESS -> { 34 | tinkoffPartnerAuth.getTinkoffTokenPayload(uri) 35 | .toSingle() 36 | .subscribeOn(Schedulers.io()) 37 | .observeOn(AndroidSchedulers.mainThread()) 38 | .subscribe( 39 | { 40 | Log.d(LOG_TAG, it.accessToken) 41 | tokenPayload = it 42 | partnerActivity.onTokenAvailable() 43 | }, 44 | { 45 | Log.e(LOG_TAG, "GetToken Error", it) 46 | partnerActivity.onAuthError() 47 | } 48 | ).apply { 49 | compositeDisposable.add(this) 50 | } 51 | } 52 | TinkoffIdStatusCode.CANCELLED_BY_USER -> { 53 | partnerActivity.onCancelledByUser() 54 | } 55 | else -> { 56 | partnerActivity.onAuthError() 57 | } 58 | } 59 | } 60 | 61 | fun refreshToken() { 62 | val refreshToken = tokenPayload?.refreshToken ?: return 63 | tinkoffPartnerAuth.obtainTokenPayload(refreshToken) 64 | .toSingle() 65 | .subscribeOn(Schedulers.io()) 66 | .observeOn(AndroidSchedulers.mainThread()) 67 | .subscribe( 68 | { 69 | Log.d(LOG_TAG, it.accessToken) 70 | partnerActivity.onTokenRefresh() 71 | tokenPayload = it 72 | }, 73 | { 74 | Log.e(LOG_TAG, "RefreshToken Error", it) 75 | } 76 | ).apply { 77 | compositeDisposable.add(this) 78 | } 79 | } 80 | 81 | fun revokeToken() { 82 | val refreshToken = tokenPayload?.refreshToken ?: return 83 | tinkoffPartnerAuth.signOutByRefreshToken(refreshToken) 84 | .toSingle() 85 | .subscribeOn(Schedulers.io()) 86 | .observeOn(AndroidSchedulers.mainThread()) 87 | .subscribe( 88 | { 89 | Log.d(LOG_TAG, "Token Revoked") 90 | partnerActivity.onTokenRevoke() 91 | }, 92 | { 93 | Log.e(LOG_TAG, "RevokeToken Error", it) 94 | } 95 | ).apply { 96 | compositeDisposable.add(this) 97 | } 98 | } 99 | 100 | override fun onDestroy(owner: LifecycleOwner) { 101 | super.onDestroy(owner) 102 | compositeDisposable.clear() 103 | } 104 | 105 | private fun TinkoffCall.toSingle(): Single { 106 | return Single.fromCallable { getResponse() } 107 | .doOnDispose { cancel() } 108 | } 109 | 110 | private companion object { 111 | 112 | private const val LOG_TAG = "TokenResponse" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/codeVerifier/CodeVerifierUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 Tinkoff Bank 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ru.tinkoff.core.tinkoffId.codeVerifier 18 | 19 | import android.annotation.SuppressLint 20 | import android.os.Build 21 | import android.util.Base64 22 | import androidx.annotation.RequiresApi 23 | import java.io.UnsupportedEncodingException 24 | import java.security.MessageDigest 25 | import java.security.NoSuchAlgorithmException 26 | import java.security.SecureRandom 27 | 28 | /** 29 | * Generates code verifiers and challenges for PKCE exchange. 30 | * 31 | * @see "Proof Key for Code Exchange by OAuth Public Clients" 32 | * @author Stanislav Mukhametshin 33 | */ 34 | internal object CodeVerifierUtil { 35 | 36 | /** 37 | * SHA-256 based code verifier challenge method. 38 | * 39 | * @see "Proof Key for Code Exchange by OAuth Public Clients" 40 | */ 41 | const val CODE_CHALLENGE_METHOD_S256 = "S256" 42 | 43 | /** 44 | * Plain-text code verifier challenge method. This is only used by AppAuth for Android if 45 | * SHA-256 is not supported on this platform. 46 | * 47 | * @see "Proof Key for Code Exchange by OAuth Public Clients" 48 | */ 49 | const val CODE_CHALLENGE_METHOD_PLAIN = "plain" 50 | 51 | /** 52 | * The default entropy (in bytes) used for the code verifier. 53 | */ 54 | private const val DEFAULT_CODE_VERIFIER_ENTROPY = 64 55 | 56 | /** 57 | * Base64 encoding settings used for generated code verifiers. 58 | */ 59 | private const val PKCE_BASE64_ENCODE_SETTINGS = 60 | Base64.NO_WRAP or Base64.NO_PADDING or Base64.URL_SAFE 61 | 62 | /** 63 | * Generates a random code verifier string using the provided entropy source and the specified 64 | * number of bytes of entropy. 65 | */ 66 | @SuppressLint("TrulyRandom") 67 | @RequiresApi(Build.VERSION_CODES.M) 68 | fun generateRandomCodeVerifier(): String { 69 | val entropySource = SecureRandom() 70 | val randomBytes = ByteArray(DEFAULT_CODE_VERIFIER_ENTROPY) 71 | entropySource.nextBytes(randomBytes) 72 | return Base64.encodeToString(randomBytes, PKCE_BASE64_ENCODE_SETTINGS) 73 | } 74 | 75 | /** 76 | * Produces a challenge from a code verifier, using SHA-256 as the challenge method if the 77 | * system supports it (all Android devices _should_ support SHA-256), and falls back 78 | * to the ["plain" challenge type][CODE_CHALLENGE_METHOD_PLAIN] if 79 | * unavailable. 80 | */ 81 | fun deriveCodeVerifierChallenge(codeVerifier: String): String { 82 | return try { 83 | val sha256Digester = MessageDigest.getInstance("SHA-256") 84 | sha256Digester.update(codeVerifier.toByteArray(charset("ISO_8859_1"))) 85 | val digestBytes = sha256Digester.digest() 86 | Base64.encodeToString(digestBytes, PKCE_BASE64_ENCODE_SETTINGS) 87 | } catch (e: NoSuchAlgorithmException) { 88 | codeVerifier 89 | } catch (e: UnsupportedEncodingException) { 90 | throw IllegalStateException("ISO-8859-1 encoding not supported", e) 91 | } 92 | } 93 | 94 | /** 95 | * Returns the challenge method utilized on this system: typically 96 | * [SHA-256][CODE_CHALLENGE_METHOD_S256] if supported by 97 | * the system, [plain][CODE_CHALLENGE_METHOD_PLAIN] otherwise. 98 | */ 99 | // no exception, so SHA-256 is supported 100 | fun getCodeVerifierChallengeMethod(): String { 101 | return try { 102 | MessageDigest.getInstance("SHA-256") 103 | CODE_CHALLENGE_METHOD_S256 104 | } catch (e: NoSuchAlgorithmException) { 105 | CODE_CHALLENGE_METHOD_PLAIN 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 142 | -------------------------------------------------------------------------------- /tinkoff-id/src/test/java/ru/tinkoff/core/tinkoffId/TinkoffPartnerApiServiceTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 Tinkoff Bank 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ru.tinkoff.core.tinkoffId 18 | 19 | import android.os.Build 20 | import com.google.common.truth.Truth.assertThat 21 | import okhttp3.OkHttpClient 22 | import okhttp3.mockwebserver.MockResponse 23 | import okhttp3.mockwebserver.MockWebServer 24 | import org.junit.After 25 | import org.junit.Assert.assertThrows 26 | import org.junit.Before 27 | import org.junit.Test 28 | import org.junit.runner.RunWith 29 | import org.robolectric.RobolectricTestRunner 30 | import org.robolectric.annotation.Config 31 | import ru.tinkoff.core.tinkoffId.api.TinkoffIdApi 32 | import ru.tinkoff.core.tinkoffId.error.TinkoffRequestException 33 | import ru.tinkoff.core.tinkoffId.error.TinkoffTokenErrorConstants 34 | import java.net.HttpURLConnection 35 | 36 | /** 37 | * @author Stanislav Mukhametshin 38 | */ 39 | @Config(sdk = [Build.VERSION_CODES.P]) 40 | @RunWith(RobolectricTestRunner::class) 41 | public class TinkoffPartnerApiServiceTest { 42 | 43 | private lateinit var mockWebServer: MockWebServer 44 | private lateinit var tinkoffPartnerApiService: TinkoffPartnerApiService 45 | 46 | @Before 47 | public fun setUp() { 48 | mockWebServer = MockWebServer() 49 | mockWebServer.start() 50 | val tinkoffIdApi = TinkoffIdApi(OkHttpClient(), mockWebServer.url("/")) 51 | tinkoffPartnerApiService = TinkoffPartnerApiService(tinkoffIdApi) 52 | } 53 | 54 | @Test 55 | public fun testValidTokenGet() { 56 | val call = tinkoffPartnerApiService.getToken("test", "test", "test", "test") 57 | testTokenRequestResponsesValidation(call) 58 | } 59 | 60 | @Test 61 | public fun testInvalidTokenGet() { 62 | val response = MockResponse() 63 | .setResponseCode(HttpURLConnection.HTTP_BAD_REQUEST) 64 | .setBody( 65 | """{ 66 | "error": "invalid_request", 67 | "error_message": "Some message" 68 | }""".trimIndent() 69 | ) 70 | mockWebServer.enqueue(response) 71 | val call = tinkoffPartnerApiService.getToken("test", "test", "test", "test") 72 | val exception = assertThrows(TinkoffRequestException::class.java) { call.getResponse() } 73 | assertThat(exception.errorMessage).isNotNull() 74 | val message = requireNotNull(exception.errorMessage) 75 | assertThat(message.errorType).isEqualTo(TinkoffTokenErrorConstants.INVALID_REQUEST) 76 | assertThat(message.message).isEqualTo("Some message") 77 | } 78 | 79 | @Test 80 | public fun testValidTokenUpdate() { 81 | val call = tinkoffPartnerApiService.refreshToken("refresh_token", "test") 82 | testTokenRequestResponsesValidation(call) 83 | } 84 | 85 | @Test 86 | public fun testRevokeAccessToken() { 87 | testTokenRevokeValidation(tinkoffPartnerApiService.revokeAccessToken("test", "test")) 88 | } 89 | 90 | @Test 91 | public fun revokeRefreshToken() { 92 | testTokenRevokeValidation(tinkoffPartnerApiService.revokeRefreshToken("test", "test")) 93 | } 94 | 95 | private fun testTokenRequestResponsesValidation(call: TinkoffCall) { 96 | val response = MockResponse() 97 | .setResponseCode(HttpURLConnection.HTTP_OK) 98 | .setBody(TOKEN_JSON) 99 | mockWebServer.enqueue(response) 100 | val payload = call.getResponse() 101 | assertThat(payload.accessToken).isEqualTo(ACCESS_TOKEN) 102 | assertThat(payload.expiresIn).isEqualTo(EXPIRES_IN) 103 | assertThat(payload.idToken).isEqualTo(ID_TOKEN) 104 | assertThat(payload.refreshToken).isEqualTo(REFRESH_TOKEN) 105 | } 106 | 107 | private fun testTokenRevokeValidation(call: TinkoffCall) { 108 | val response = MockResponse() 109 | .setResponseCode(HttpURLConnection.HTTP_OK) 110 | .setBody("OK") 111 | mockWebServer.enqueue(response) 112 | assertThat(call.getResponse()).isEqualTo(Unit) 113 | } 114 | 115 | @After 116 | public fun tearDown() { 117 | mockWebServer.shutdown() 118 | } 119 | 120 | private companion object { 121 | 122 | private const val ACCESS_TOKEN = "DR_Y7iifsfsdfRKGuXtMovTocYD4MnA7RxhwAMX3ydRKeDOYFls4a1S4IC1Daq7poz2k2AoJIOICsgA" 123 | private const val EXPIRES_IN = 1834 124 | private const val ID_TOKEN = "yJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ" 125 | private const val REFRESH_TOKEN = "OuyFBEMG2yhjh0KHNIbeS8sb0VmBmdqd08rY52ZniOyDmCnn" 126 | 127 | private val TOKEN_JSON = """{ 128 | "access_token": "$ACCESS_TOKEN", 129 | "token_type": "Bearer", 130 | "expires_in": $EXPIRES_IN, 131 | "id_token": "$ID_TOKEN", 132 | "refresh_token": "$REFRESH_TOKEN" 133 | }""".trimIndent() 134 | } 135 | } -------------------------------------------------------------------------------- /app-demo/src/main/java/ru/tinkoff/core/app_demo_partner/PartnerActivity.kt: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.core.app_demo_partner 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import android.view.View 7 | import android.widget.Button 8 | import android.widget.EditText 9 | import android.widget.Toast 10 | import androidx.appcompat.app.AppCompatActivity 11 | import ru.tinkoff.core.tinkoffId.TinkoffIdAuth 12 | import ru.tinkoff.core.tinkoffId.ui.TinkoffIdSignInButton 13 | import kotlin.LazyThreadSafetyMode.NONE 14 | 15 | class PartnerActivity : AppCompatActivity() { 16 | 17 | private val callbackUrl: Uri = Uri.Builder() 18 | .scheme("https") 19 | .authority("www.partner.com") 20 | .appendPath("partner") 21 | .build() 22 | 23 | private val redirectUri = "mobile://" 24 | private val clientId = "test-partner-mobile" 25 | 26 | private lateinit var tinkoffPartnerAuth: TinkoffIdAuth 27 | 28 | private val partnerPresenter by lazy(NONE) { PartnerPresenter(tinkoffPartnerAuth, this) } 29 | private val clientIdEditText by lazy(NONE) { findViewById(R.id.etClientId) } 30 | private val redirectUriEditText by lazy(NONE) { findViewById(R.id.etRedirectUri) } 31 | 32 | private val reset by lazy(NONE) { findViewById