├── .github
├── dependabot.yml
└── workflows
│ ├── pull-request-jobs.yml
│ └── release-package.yml
├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── halcyonmobile
│ │ └── oauth_retrofit
│ │ ├── ExampleApplication.kt
│ │ ├── MainActivity.kt
│ │ ├── SessionExpiredEventHandlerImpl.kt
│ │ └── SharedPreferencesManager.kt
│ └── res
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── drawable
│ └── ic_launcher_background.xml
│ ├── layout
│ └── activity_main.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ └── values
│ ├── colors.xml
│ ├── strings.xml
│ └── styles.xml
├── build.gradle
├── core
├── .gitignore
├── build.gradle
└── src
│ ├── main
│ └── java
│ │ └── com
│ │ └── halcyonmobile
│ │ └── core
│ │ ├── ExampleRemoteSource.kt
│ │ ├── SessionExampleService.kt
│ │ ├── SessionlessExampleService.kt
│ │ ├── basicsetup
│ │ ├── RefreshTokenResponse.kt
│ │ ├── RefreshTokenService.kt
│ │ └── networkmodule.kt
│ │ ├── oauthgson
│ │ └── networkmodule.kt
│ │ ├── oauthmoshi
│ │ └── networkmodule.kt
│ │ └── oauthmoshikoin
│ │ └── networkModule.kt
│ └── test
│ ├── java
│ └── com
│ │ └── halcyonmobile
│ │ └── core
│ │ ├── AuthenticationTest.kt
│ │ └── compatibility
│ │ └── error
│ │ └── wrapping
│ │ └── ExceptionWrappingCompatibilityTest.kt
│ └── resources
│ └── authentication_service
│ ├── refresh_token_bad_token.json
│ ├── refresh_token_expired.json
│ └── refresh_token_positive.json
├── deploy.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── oauth
├── .gitignore
├── build.gradle
└── src
│ └── main
│ └── java
│ └── com
│ └── halcyonmobile
│ └── oauth
│ ├── AuthFinishedInvalidationException.kt
│ ├── AuthenticationLocalStorageExtensions.kt
│ ├── AuthenticationService.kt
│ ├── AuthenticationServiceAdapter.kt
│ ├── DeprecatedIsSessionExpiredExceptionAdapter.kt
│ ├── IsSessionExpiredException.kt
│ ├── OauthRetrofitContainer.kt
│ ├── OauthRetrofitContainerBuilder.kt
│ ├── SessionDataResponse.kt
│ ├── ThrowableException.kt
│ └── internal
│ ├── AuthenticationHeaderInterceptor.kt
│ ├── Authenticator.kt
│ ├── ClientIdParameterInterceptor.kt
│ ├── DefaultIsSessionExpiredException.kt
│ └── SetAuthorizationHeaderUseCase.kt
├── oauthadaptergenerator
├── .gitignore
├── build.gradle
└── src
│ └── main
│ ├── java
│ └── com
│ │ └── halcyonmobile
│ │ └── oauthadaptergenerator
│ │ └── Processor.kt
│ └── resources
│ └── META-INF
│ └── services
│ └── javax.annotation.processing.Processor
├── oauthdependencies
├── .gitignore
├── build.gradle
└── src
│ └── main
│ └── java
│ └── com
│ └── halcyonmobile
│ └── oauth
│ └── dependencies
│ ├── AuthenticationLocalStorage.kt
│ ├── IsSessionExpiredException.kt
│ ├── RefreshService.kt
│ └── SessionExpiredEventHandler.kt
├── oauthgson
├── .gitignore
├── build.gradle
└── src
│ ├── main
│ └── java
│ │ └── com
│ │ └── halcyonmobile
│ │ └── oauthgson
│ │ ├── OauthRetrofitContainerWithGson.kt
│ │ ├── OauthRetrofitWithGsonContainerBuilder.kt
│ │ ├── RefreshTokenResponse.kt
│ │ └── SessionDataResponseDeserializer.kt
│ └── test
│ └── java
│ └── com
│ └── halcyonmobile
│ └── oauthmoshi
│ └── OauthRetrofitWithMoshiContainerBuilderTest.kt
├── oauthmoshi
├── .gitignore
├── build.gradle
└── src
│ ├── main
│ └── java
│ │ └── com
│ │ └── halcyonmobile
│ │ └── oauthmoshi
│ │ ├── OauthRetrofitContainerWithMoshi.kt
│ │ ├── OauthRetrofitWithMoshiContainerBuilder.kt
│ │ ├── RefreshTokenResponse.kt
│ │ └── RefreshTokenResponseWrapper.kt
│ └── test
│ └── java
│ └── com
│ └── halcyonmobile
│ └── oauthmoshi
│ └── OauthRetrofitWithMoshiContainerBuilderTest.kt
├── oauthmoshikoin
├── .gitignore
├── build.gradle
└── src
│ └── main
│ └── java
│ └── com
│ └── halcyonmobile
│ └── oauthmoshikoin
│ └── createOauthModule.kt
├── oauthparsing
├── .gitignore
├── build.gradle
└── src
│ └── main
│ └── java
│ └── com
│ └── halcyonmobile
│ └── oauthparsing
│ ├── AuthenticationServiceAdapterImpl.kt
│ ├── OauthRetrofitContainerWithParser.kt
│ ├── OauthRetrofitWithParserContainerBuilder.kt
│ ├── RefreshServiceFieldParameterProvider.kt
│ └── RefreshTokenService.kt
├── oauthsecurestorage
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── halcyonmobile
│ │ └── oauthsecurestorage
│ │ └── AuthenticationSecureSharedPreferencesStorage.kt
│ └── res
│ └── values
│ └── strings.xml
├── oauthsecurestoragecompat
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── org
│ │ └── fnives
│ │ └── android
│ │ └── oauthsecurestoragecompat
│ │ └── MigrationTest.kt
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── halcyonmobile
│ │ └── oauthsecurestoragecompat
│ │ └── AuthenticationSecureSharedPreferencesStorageCompat.kt
│ └── res
│ └── values
│ └── strings.xml
├── oauthstorage
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── halcyonmobile
│ │ └── oauthstorage
│ │ └── ExampleInstrumentedTest.java
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── halcyonmobile
│ │ └── oauthstorage
│ │ └── AuthenticationSharedPreferencesStorage.kt
│ └── res
│ └── values
│ └── strings.xml
└── settings.gradle.kts
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: gradle
4 | directory: "/"
5 | target-branch: "develop"
6 | schedule:
7 | interval: "weekly"
8 | day: "monday"
9 | time: "12:00"
10 | open-pull-requests-limit: 15
--------------------------------------------------------------------------------
/.github/workflows/pull-request-jobs.yml:
--------------------------------------------------------------------------------
1 | name: Verify Pull request is publishable
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - develop
7 |
8 | env:
9 | GITHUB_USERNAME: "halcyonmobile"
10 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
11 |
12 | jobs:
13 | run-tests:
14 | runs-on: ubuntu-latest
15 | permissions:
16 | contents: read
17 | steps:
18 | - name: Checkout repository
19 | uses: actions/checkout@v2
20 | - name: Setup Java
21 | uses: actions/setup-java@v2
22 | with:
23 | distribution: 'adopt'
24 | java-version: '11'
25 | - name: Run Android Unit Tests
26 | run: ./gradlew testReleaseUnit
27 | - name: Run Core Unit Tests
28 | run: ./gradlew core:test
29 |
--------------------------------------------------------------------------------
/.github/workflows/release-package.yml:
--------------------------------------------------------------------------------
1 | name: Library Package Publishing
2 |
3 | on:
4 | release:
5 | types:
6 | - created
7 |
8 | jobs:
9 | publish-library:
10 | runs-on: ubuntu-latest
11 | permissions:
12 | packages: write
13 | contents: read
14 | steps:
15 | - name: Checkout repository
16 | uses: actions/checkout@v2
17 | - name: Setup Java
18 | uses: actions/setup-java@v2
19 | with:
20 | distribution: 'adopt'
21 | java-version: '11'
22 | - name: Assemble project
23 | env:
24 | GITHUB_USERNAME: "halcyonmobile"
25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26 | run: ./gradlew publishToGitHub
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/*
5 | /.idea/caches
6 | /.idea/libraries
7 | /.idea/modules.xml
8 | /.idea/workspace.xml
9 | /.idea/navEditor.xml
10 | /.idea/assetWizardSettings.xml
11 | .DS_Store
12 | /build
13 | /captures
14 | .externalNativeBuild
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 |
4 | android {
5 | compileSdkVersion project.compileSdkVersion
6 | defaultConfig {
7 | applicationId "com.halcyonmobile.oauth_retrofit"
8 | minSdkVersion project.minSdkVersion
9 | targetSdkVersion project.compileSdkVersion
10 | versionCode 1
11 | versionName "1.0"
12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
13 | }
14 | buildTypes {
15 | release {
16 | minifyEnabled false
17 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
18 | }
19 | }
20 | kotlinOptions {
21 | freeCompilerArgs += ['-Xsanitize-parentheses']
22 | }
23 | }
24 |
25 | dependencies {
26 | implementation fileTree(dir: 'libs', include: ['*.jar'])
27 | implementation "androidx.appcompat:appcompat:$appCompatVersion"
28 |
29 | implementation project(':core')
30 | implementation project(':oauthstorage')
31 | implementation project(':oauthsecurestoragecompat')
32 | implementation "io.insert-koin:koin-android:$koinVersion"
33 |
34 | testImplementation "junit:junit:$junitVersion"
35 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
36 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
37 | }
38 |
--------------------------------------------------------------------------------
/app/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/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/halcyonmobile/oauth_retrofit/ExampleApplication.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth_retrofit
18 |
19 | import android.app.Application
20 | import com.halcyonmobile.core.oauthgson.createNetworkModules
21 | import org.koin.android.ext.koin.androidContext
22 | import org.koin.core.context.startKoin
23 | import org.koin.core.module.Module
24 | import org.koin.dsl.module
25 |
26 | class ExampleApplication : Application() {
27 |
28 | override fun onCreate() {
29 | super.onCreate()
30 | startKoin{
31 | androidContext(this@ExampleApplication)
32 | modules(createModules())
33 | }
34 | }
35 |
36 | fun createModules(): List =
37 | listOf(
38 | module {
39 | single { SharedPreferencesManager(get(), encrypted = false, compat = true) }
40 | }
41 | )
42 | .plus(
43 | createNetworkModules(
44 | clientId = "CLIENT-ID",
45 | baseUrl = "https://google.com/",
46 | provideAuthenticationLocalStorage = { get() },
47 | provideSessionExpiredEventHandler = { SessionExpiredEventHandlerImpl(get()) }
48 | ))
49 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/halcyonmobile/oauth_retrofit/MainActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth_retrofit
18 |
19 | import android.os.Bundle
20 | import androidx.appcompat.app.AppCompatActivity
21 | import com.halcyonmobile.core.ExampleRemoteSource
22 | import com.halcyonmobile.oauth.dependencies.AuthenticationLocalStorage
23 | import org.koin.android.ext.android.inject
24 |
25 | class MainActivity : AppCompatActivity() {
26 |
27 | private val authenticationLocalStorage by inject()
28 | private val exampleRemoteSource by inject()
29 |
30 | override fun onCreate(savedInstanceState: Bundle?) {
31 | super.onCreate(savedInstanceState)
32 | setContentView(R.layout.activity_main)
33 |
34 | authenticationLocalStorage.tokenType = "type"
35 | authenticationLocalStorage.accessToken = "accessToken"
36 | authenticationLocalStorage.refreshToken = "accessToken"
37 | authenticationLocalStorage.userId = "1234"
38 | exampleRemoteSource.nonsession(object : ExampleRemoteSource.Callback {
39 | override fun success() = runSessionService()
40 | override fun error() = runSessionService()
41 | })
42 | }
43 |
44 | fun runSessionService() {
45 | exampleRemoteSource.session(object : ExampleRemoteSource.Callback {
46 | override fun success() {
47 | }
48 |
49 | override fun error() {
50 | }
51 | })
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/java/com/halcyonmobile/oauth_retrofit/SessionExpiredEventHandlerImpl.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth_retrofit
18 |
19 | import android.content.Context
20 | import android.os.Handler
21 | import android.os.Looper
22 | import com.halcyonmobile.oauth.dependencies.SessionExpiredEventHandler
23 |
24 | class SessionExpiredEventHandlerImpl(private val context: Context) : SessionExpiredEventHandler {
25 | override fun onSessionExpired() {
26 | Handler(Looper.getMainLooper()).post {
27 | // todo navigate to sign in
28 | }
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/halcyonmobile/oauth_retrofit/SharedPreferencesManager.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth_retrofit
18 |
19 | import android.content.Context
20 | import com.halcyonmobile.oauth.dependencies.AuthenticationLocalStorage
21 | import com.halcyonmobile.oauthsecurestorage.AuthenticationSecureSharedPreferencesStorage
22 | import com.halcyonmobile.oauthsecurestoragecompat.AuthenticationSecureSharedPreferencesStorageCompat
23 | import com.halcyonmobile.oauthstorage.AuthenticationSharedPreferencesStorage
24 |
25 | class SharedPreferencesManager private constructor(authenticationLocalStorage: AuthenticationLocalStorage) :
26 | AuthenticationLocalStorage by authenticationLocalStorage {
27 |
28 | constructor(context: Context, encrypted: Boolean = false, compat: Boolean = false) : this(createAuthenticationLocalStorage(context, encrypted, compat))
29 |
30 |
31 | companion object {
32 | private fun createAuthenticationLocalStorage(context: Context, encrypted: Boolean, compat: Boolean): AuthenticationLocalStorage =
33 | when {
34 | compat -> AuthenticationSecureSharedPreferencesStorageCompat(context)
35 | encrypted -> AuthenticationSecureSharedPreferencesStorage.create(context)
36 | else -> AuthenticationSharedPreferencesStorage(context)
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halcyonmobile/retrofit-oauth2-helper/cc59afbeb284e41188fa65b6bdecd605d2526596/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halcyonmobile/retrofit-oauth2-helper/cc59afbeb284e41188fa65b6bdecd605d2526596/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halcyonmobile/retrofit-oauth2-helper/cc59afbeb284e41188fa65b6bdecd605d2526596/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halcyonmobile/retrofit-oauth2-helper/cc59afbeb284e41188fa65b6bdecd605d2526596/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halcyonmobile/retrofit-oauth2-helper/cc59afbeb284e41188fa65b6bdecd605d2526596/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halcyonmobile/retrofit-oauth2-helper/cc59afbeb284e41188fa65b6bdecd605d2526596/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halcyonmobile/retrofit-oauth2-helper/cc59afbeb284e41188fa65b6bdecd605d2526596/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halcyonmobile/retrofit-oauth2-helper/cc59afbeb284e41188fa65b6bdecd605d2526596/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halcyonmobile/retrofit-oauth2-helper/cc59afbeb284e41188fa65b6bdecd605d2526596/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halcyonmobile/retrofit-oauth2-helper/cc59afbeb284e41188fa65b6bdecd605d2526596/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | oauth-retrofit
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | minSdkVersion = 21
4 | compileSdkVersion = 30
5 | jvmTarget = "1.8"
6 |
7 | annotationVersion = "1.3.0"
8 | appCompatVersion = "1.3.1"
9 | coroutinesVersion = "1.6.0"
10 | gsonVersion = "2.8.9"
11 | junitVersion = "4.13.2"
12 | koinVersion = "3.1.5"
13 | kotlinVersion = "1.6.10"
14 | kotlinPoetVersion = "1.10.2"
15 | moshiVersion = "1.13.0"
16 | okHttpVersion = "4.9.3"
17 | okioVersion = "2.10.0"
18 | retrofitVersion = "2.9.0"
19 | securityVersion = "1.0.0"
20 | }
21 | repositories {
22 | google()
23 | maven { url "https://plugins.gradle.org/m2/" }
24 | maven {
25 | url "https://maven.pkg.github.com/halcyonmobile/halcyon-custom-gradle-publish-plugin"
26 | credentials {
27 | username = project.findProperty("GITHUB_USERNAME") ?: System.getenv("GITHUB_USERNAME")
28 | password = project.findProperty("GITHUB_TOKEN") ?: System.getenv("GITHUB_TOKEN")
29 | }
30 | }
31 | }
32 | dependencies {
33 | classpath "com.android.tools.build:gradle:7.1.2"
34 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
35 | classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4.24.14"
36 | classpath "com.github.dcendents:android-maven-gradle-plugin:2.1"
37 | classpath "com.halcyonmobile.publish.custom:java-and-aar:2.2.0"
38 | }
39 | }
40 |
41 | allprojects {
42 | repositories {
43 | google()
44 | mavenCentral()
45 | maven {
46 | url "https://maven.pkg.github.com/halcyonmobile/halcyon-custom-gradle-publish-plugin"
47 | credentials {
48 | username = project.findProperty("GITHUB_USERNAME") ?: System.getenv("GITHUB_USERNAME")
49 | password = project.findProperty("GITHUB_TOKEN") ?: System.getenv("GITHUB_TOKEN")
50 | }
51 | }
52 | }
53 | }
54 |
55 | task clean(type: Delete) {
56 | delete rootProject.buildDir
57 | }
58 |
59 | apply from: "./deploy.gradle"
--------------------------------------------------------------------------------
/core/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/core/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java-library'
2 | apply plugin: 'kotlin'
3 | apply plugin: 'kotlin-kapt'
4 |
5 | dependencies {
6 | implementation fileTree(dir: 'libs', include: ['*.jar'])
7 | implementation "com.squareup.moshi:moshi:$moshiVersion"
8 | kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion"
9 |
10 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
11 | // ouath-moshi-koin dependency
12 | implementation(project(':oauthmoshikoin'))
13 |
14 | // oauth-moshi dependency
15 | implementation(project(':oauthmoshi'))
16 |
17 | // oauth-gson dependency
18 | implementation(project(':oauthgson'))
19 |
20 | // basic dependencies
21 | implementation(project(':oauth'))
22 | kapt project(':oauthadaptergenerator')
23 |
24 | implementation ("com.squareup.okhttp3:logging-interceptor:$okHttpVersion") {
25 | exclude module: 'okhttp'
26 | }
27 |
28 | testImplementation ("com.squareup.okhttp3:mockwebserver:$okHttpVersion") {
29 | exclude module: 'okhttp'
30 | }
31 | testImplementation "junit:junit:$junitVersion"
32 | testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0"
33 |
34 | // compatibility
35 | testImplementation "com.halcyonmobile.retrofit-error-parsing:retrofit-error-parsing:2.0.1"
36 | // incompatible with error-parsing 2.0.0,
37 | // testImplementation "com.halcyonmobile.error-handler:rest:1.0.0"
38 | }
39 |
40 | sourceSets {
41 | main.java.srcDirs += 'build/generated/source/kaptKotlin'
42 | }
43 | compileKotlin {
44 | kotlinOptions {
45 | jvmTarget = project.jvmTarget
46 | }
47 | }
48 | compileTestKotlin {
49 | kotlinOptions {
50 | jvmTarget = project.jvmTarget
51 | }
52 | }
53 | sourceCompatibility = JavaVersion.VERSION_1_8
54 | targetCompatibility = JavaVersion.VERSION_1_8
--------------------------------------------------------------------------------
/core/src/main/java/com/halcyonmobile/core/ExampleRemoteSource.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.core
18 |
19 | import retrofit2.Call
20 | import retrofit2.Response
21 |
22 | class ExampleRemoteSource internal constructor(
23 | private val sessionExampleService: SessionExampleService,
24 | private val sessionlessExampleService: SessionlessExampleService){
25 |
26 | fun session(callback: Callback) {
27 | sessionExampleService.get().enqueue(object: retrofit2.Callback{
28 | override fun onFailure(call: Call, t: Throwable) {
29 | callback.error()
30 | }
31 |
32 | override fun onResponse(call: Call, response: Response) {
33 | callback.success()
34 | }
35 | })
36 | }
37 |
38 | fun nonsession(callback: Callback) {
39 | sessionlessExampleService.get().enqueue(object: retrofit2.Callback{
40 | override fun onFailure(call: Call, t: Throwable) {
41 | callback.error()
42 | }
43 |
44 | override fun onResponse(call: Call, response: Response) {
45 | callback.success()
46 | }
47 | })
48 | }
49 |
50 | interface Callback{
51 | fun success()
52 | fun error()
53 | }
54 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/halcyonmobile/core/SessionExampleService.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.core
18 |
19 | import retrofit2.Call
20 | import retrofit2.http.GET
21 |
22 | internal interface SessionExampleService{
23 |
24 | @GET("program")
25 | fun get() : Call
26 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/halcyonmobile/core/SessionlessExampleService.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.core
18 |
19 | import retrofit2.Call
20 | import retrofit2.http.GET
21 |
22 | internal interface SessionlessExampleService {
23 |
24 | @GET("programs")
25 | fun get() : Call
26 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/halcyonmobile/core/basicsetup/RefreshTokenResponse.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.core.basicsetup
18 |
19 | import com.halcyonmobile.oauth.SessionDataResponse
20 | import com.squareup.moshi.Json
21 | import com.squareup.moshi.JsonClass
22 |
23 |
24 | @JsonClass(generateAdapter = true)
25 | data class RefreshTokenResponse(
26 | @field:Json(name = "user_id") override val userId: String,
27 | @field:Json(name = "access_token") override val token: String,
28 | @field:Json(name = "refresh_token") override val refreshToken: String,
29 | @field:Json(name = "token_type") override val tokenType: String
30 | ) : SessionDataResponse
--------------------------------------------------------------------------------
/core/src/main/java/com/halcyonmobile/core/basicsetup/RefreshTokenService.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.core.basicsetup
18 |
19 | import com.halcyonmobile.oauth.dependencies.RefreshService
20 | import retrofit2.Call
21 | import retrofit2.http.Field
22 | import retrofit2.http.FormUrlEncoded
23 | import retrofit2.http.POST
24 |
25 | /**
26 | * You have to define a service which will do the token refreshing.
27 | *
28 | * In order to use [RefreshService] annotation to generate the [com.halcyonmobile.oauth.RefreshTokenServiceAuthenticationServiceAdapter]
29 | * you must have only one function, which returns a [Call] with a typeParameter subtype of [com.halcyonmobile.oauth.SessionDataResponse]
30 | * Your first parameter must be the refresh token, every other parameter has to have default value.
31 | * If that's not possible consider creating your own specific implementation for the [com.halcyonmobile.oauth.AuthenticationServiceAdapter].
32 | */
33 | @RefreshService
34 | interface RefreshTokenService {
35 |
36 | @POST("oauth/token")
37 | @FormUrlEncoded
38 | fun refresh(@Field("refresh_token") refreshToken: String, @Field("grant_type") grantType: String = "refresh_token"): Call
39 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/halcyonmobile/core/basicsetup/networkmodule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.core.basicsetup
18 |
19 | import com.halcyonmobile.core.ExampleRemoteSource
20 | import com.halcyonmobile.core.SessionExampleService
21 | import com.halcyonmobile.core.SessionlessExampleService
22 | import com.halcyonmobile.oauth.OauthRetrofitContainer
23 | import com.halcyonmobile.oauth.OauthRetrofitContainerBuilder
24 | import com.halcyonmobile.oauth.RefreshTokenServiceAuthenticationServiceAdapter
25 | import com.halcyonmobile.oauth.dependencies.AuthenticationLocalStorage
26 | import com.halcyonmobile.oauth.dependencies.SessionExpiredEventHandler
27 | import com.halcyonmobile.oauthmoshikoin.NON_SESSION_RETROFIT
28 | import com.halcyonmobile.oauthmoshikoin.SESSION_RETROFIT
29 | import com.squareup.moshi.Moshi
30 | import org.koin.core.module.Module
31 | import org.koin.core.scope.Scope
32 | import org.koin.dsl.module
33 | import retrofit2.Retrofit
34 | import retrofit2.converter.moshi.MoshiConverterFactory
35 |
36 | /**
37 | * Example network module which uses the [com.halcyonmobile.oauth.OauthRetrofitContainerBuilder] to create the needed two
38 | * retrofit instance, then provides two example service [SessionExampleService] [SessionlessExampleService].
39 | */
40 | fun createNetworkModules(
41 | clientId: String,
42 | baseUrl: String,
43 | provideAuthenticationLocalStorage: Scope.() -> AuthenticationLocalStorage,
44 | provideSessionExpiredEventHandler: Scope.() -> SessionExpiredEventHandler
45 | ): List {
46 | return listOf(
47 | module {
48 | factory { get(SESSION_RETROFIT).create(SessionExampleService::class.java) }
49 | factory { get(NON_SESSION_RETROFIT).create(SessionlessExampleService::class.java) }
50 | factory { ExampleRemoteSource(get(), get()) }
51 | },
52 | module {
53 | single { provideAuthenticationLocalStorage() }
54 | single { provideSessionExpiredEventHandler() }
55 | single { Moshi.Builder().build() }
56 | single {
57 | OauthRetrofitContainerBuilder(
58 | clientId = clientId,
59 | refreshServiceClass = RefreshTokenService::class,
60 | authenticationLocalStorage = provideAuthenticationLocalStorage(),
61 | sessionExpiredEventHandler = provideSessionExpiredEventHandler(),
62 | adapter = RefreshTokenServiceAuthenticationServiceAdapter()
63 | )
64 | .configureRetrofit {
65 | baseUrl(baseUrl).addConverterFactory(MoshiConverterFactory.create(get()))
66 | }
67 | .build()
68 | }
69 | single(SESSION_RETROFIT) { get().sessionRetrofit }
70 | single(NON_SESSION_RETROFIT) { get().sessionlessRetrofit }
71 | }
72 | )
73 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/halcyonmobile/core/oauthgson/networkmodule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.core.oauthgson
18 |
19 | import com.halcyonmobile.core.ExampleRemoteSource
20 | import com.halcyonmobile.core.SessionExampleService
21 | import com.halcyonmobile.core.SessionlessExampleService
22 | import com.halcyonmobile.oauth.dependencies.AuthenticationLocalStorage
23 | import com.halcyonmobile.oauth.dependencies.SessionExpiredEventHandler
24 | import com.halcyonmobile.oauthgson.OauthRetrofitContainerWithGson
25 | import com.halcyonmobile.oauthgson.OauthRetrofitWithGsonContainerBuilder
26 | import com.halcyonmobile.oauthmoshikoin.NON_SESSION_RETROFIT
27 | import com.halcyonmobile.oauthmoshikoin.SESSION_RETROFIT
28 | import org.koin.core.module.Module
29 | import org.koin.core.scope.Scope
30 | import org.koin.dsl.module
31 | import retrofit2.Retrofit
32 |
33 | /**
34 | * Example network module which uses the [com.halcyonmobile.oauthgson.OauthRetrofitContainerWithGson] to create the needed two
35 | * retrofit instance, then provides two example service [SessionExampleService] [SessionlessExampleService].
36 | */
37 | fun createNetworkModules(
38 | clientId: String,
39 | baseUrl: String,
40 | provideAuthenticationLocalStorage: Scope.() -> AuthenticationLocalStorage,
41 | provideSessionExpiredEventHandler: Scope.() -> SessionExpiredEventHandler
42 | ): List {
43 | return listOf(
44 | module {
45 | factory { get(SESSION_RETROFIT).create(SessionExampleService::class.java) }
46 | factory { get(NON_SESSION_RETROFIT).create(SessionlessExampleService::class.java) }
47 | factory { ExampleRemoteSource(get(), get()) }
48 | },
49 | module {
50 | single { provideAuthenticationLocalStorage() }
51 | single { provideSessionExpiredEventHandler() }
52 | single {
53 | OauthRetrofitWithGsonContainerBuilder(
54 | clientId = clientId,
55 | authenticationLocalStorage = provideAuthenticationLocalStorage(),
56 | sessionExpiredEventHandler = provideSessionExpiredEventHandler()
57 | )
58 | .configureRetrofit {
59 | baseUrl(baseUrl)
60 | }
61 | .build()
62 | }
63 | single(SESSION_RETROFIT) { get().oauthRetrofitContainer.sessionRetrofit }
64 | single(NON_SESSION_RETROFIT) { get().oauthRetrofitContainer.sessionlessRetrofit }
65 | single { get().gson }
66 | }
67 | )
68 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/halcyonmobile/core/oauthmoshi/networkmodule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.core.oauthmoshi
18 |
19 | import com.halcyonmobile.core.ExampleRemoteSource
20 | import com.halcyonmobile.core.SessionExampleService
21 | import com.halcyonmobile.core.SessionlessExampleService
22 | import com.halcyonmobile.oauth.dependencies.AuthenticationLocalStorage
23 | import com.halcyonmobile.oauth.dependencies.SessionExpiredEventHandler
24 | import com.halcyonmobile.oauthmoshi.OauthRetrofitContainerWithMoshi
25 | import com.halcyonmobile.oauthmoshi.OauthRetrofitWithMoshiContainerBuilder
26 | import com.halcyonmobile.oauthmoshikoin.NON_SESSION_RETROFIT
27 | import com.halcyonmobile.oauthmoshikoin.SESSION_RETROFIT
28 | import org.koin.core.module.Module
29 | import org.koin.core.scope.Scope
30 | import org.koin.dsl.module
31 | import retrofit2.Retrofit
32 |
33 | /**
34 | * Example network module which uses the [com.halcyonmobile.oauthmoshi.OauthRetrofitContainerWithMoshi] to create the needed two
35 | * retrofit instance, then provides two example service [SessionExampleService] [SessionlessExampleService].
36 | */
37 | fun createNetworkModules(
38 | clientId: String,
39 | baseUrl: String,
40 | provideAuthenticationLocalStorage: Scope.() -> AuthenticationLocalStorage,
41 | provideSessionExpiredEventHandler: Scope.() -> SessionExpiredEventHandler
42 | ): List {
43 | return listOf(
44 | module {
45 | factory { get(SESSION_RETROFIT).create(SessionExampleService::class.java) }
46 | factory { get(NON_SESSION_RETROFIT).create(SessionlessExampleService::class.java) }
47 | factory { ExampleRemoteSource(get(), get()) }
48 | },
49 | module {
50 | single { provideAuthenticationLocalStorage() }
51 | single { provideSessionExpiredEventHandler() }
52 | single {
53 | OauthRetrofitWithMoshiContainerBuilder(
54 | clientId = clientId,
55 | authenticationLocalStorage = provideAuthenticationLocalStorage(),
56 | sessionExpiredEventHandler = provideSessionExpiredEventHandler()
57 | )
58 | .configureRetrofit {
59 | baseUrl(baseUrl)
60 | }
61 | .build()
62 | }
63 | single(SESSION_RETROFIT) { get().oauthRetrofitContainer.sessionRetrofit }
64 | single(NON_SESSION_RETROFIT) { get().oauthRetrofitContainer.sessionlessRetrofit }
65 | single { get().moshi }
66 | }
67 | )
68 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/halcyonmobile/core/oauthmoshikoin/networkModule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.core.oauthmoshikoin
18 |
19 | import com.halcyonmobile.core.ExampleRemoteSource
20 | import com.halcyonmobile.core.SessionExampleService
21 | import com.halcyonmobile.core.SessionlessExampleService
22 | import com.halcyonmobile.oauth.dependencies.AuthenticationLocalStorage
23 | import com.halcyonmobile.oauth.dependencies.SessionExpiredEventHandler
24 | import com.halcyonmobile.oauthmoshikoin.NON_SESSION_RETROFIT
25 | import com.halcyonmobile.oauthmoshikoin.SESSION_RETROFIT
26 | import com.halcyonmobile.oauthmoshikoin.createOauthModule
27 | import org.koin.core.module.Module
28 | import org.koin.core.scope.Scope
29 | import org.koin.dsl.module
30 | import retrofit2.Retrofit
31 |
32 | /**
33 | * Example network module which uses the [com.halcyonmobile.oauthmoshikoin.createOauthModule] to create and provide the
34 | * needed two retrofit instance and moshi.
35 | */
36 | fun createNetworkModules(
37 | clientId: String,
38 | baseUrl: String,
39 | provideAuthenticationLocalStorage: Scope.() -> AuthenticationLocalStorage,
40 | provideSessionExpiredEventHandler: Scope.() -> SessionExpiredEventHandler
41 | ): List {
42 | return listOf(
43 | module {
44 | factory { get(SESSION_RETROFIT).create(SessionExampleService::class.java) }
45 | factory { get(NON_SESSION_RETROFIT).create(SessionlessExampleService::class.java) }
46 | factory { ExampleRemoteSource(get(), get()) }
47 | },
48 | createOauthModule(
49 | clientId = clientId,
50 | provideSessionExpiredEventHandler = provideSessionExpiredEventHandler,
51 | provideAuthenticationLocalStorage = provideAuthenticationLocalStorage,
52 | configureRetrofit = {
53 | it.baseUrl(baseUrl)
54 | }
55 | )
56 | )
57 | }
--------------------------------------------------------------------------------
/core/src/test/java/com/halcyonmobile/core/compatibility/error/wrapping/ExceptionWrappingCompatibilityTest.kt:
--------------------------------------------------------------------------------
1 | package com.halcyonmobile.core.compatibility.error.wrapping
2 |
3 | import com.halcyonmobile.errorparsing2.ErrorWrappingAndParserCallAdapterFactory
4 | import com.halcyonmobile.oauth.dependencies.AuthenticationLocalStorage
5 | import com.halcyonmobile.oauth.dependencies.SessionExpiredEventHandler
6 | import com.halcyonmobile.oauthgson.OauthRetrofitWithGsonContainerBuilder
7 | import com.halcyonmobile.oauthmoshi.OauthRetrofitWithMoshiContainerBuilder
8 | import com.nhaarman.mockitokotlin2.doReturn
9 | import com.nhaarman.mockitokotlin2.mock
10 | import com.nhaarman.mockitokotlin2.times
11 | import com.nhaarman.mockitokotlin2.verify
12 | import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
13 | import com.nhaarman.mockitokotlin2.whenever
14 | import okhttp3.mockwebserver.MockResponse
15 | import okhttp3.mockwebserver.MockWebServer
16 | import org.junit.After
17 | import org.junit.Before
18 | import org.junit.Test
19 | import org.junit.runner.RunWith
20 | import org.junit.runners.Parameterized
21 | import retrofit2.Call
22 | import retrofit2.Retrofit
23 | import retrofit2.create
24 | import retrofit2.http.GET
25 |
26 | @Suppress("TestFunctionName")
27 | @RunWith(Parameterized::class)
28 | class ExceptionWrappingCompatibilityTest(
29 | private val additionalRetrofitConfiguration: (Retrofit.Builder) -> Retrofit.Builder
30 | ) {
31 |
32 | private lateinit var basePath: String
33 | private lateinit var mockAuthenticationLocalStorage: AuthenticationLocalStorage
34 | private lateinit var mockSessionExpiredEventHandler: SessionExpiredEventHandler
35 | private lateinit var mockWebServer: MockWebServer
36 |
37 | @Before
38 | fun setup() {
39 | basePath = "/google.com/test/"
40 | mockWebServer = MockWebServer()
41 | mockAuthenticationLocalStorage = mock()
42 | mockSessionExpiredEventHandler = mock()
43 | }
44 |
45 |
46 | @After
47 | fun tearDown() {
48 | mockWebServer.shutdown()
49 | }
50 |
51 | @Test//(timeout = 5000L)
52 | fun GIVEN_moshi_and_standard_sessionExpiration_exception_WHEN_a_refresh_request_is_run_THEN_the_session_expiration_is_Called() {
53 | whenever(mockAuthenticationLocalStorage.refreshToken).doReturn("alma")
54 | mockWebServer.enqueue(MockResponse().apply {
55 | setResponseCode(401)
56 | setBody("")
57 | })
58 | mockWebServer.enqueue(MockResponse().apply {
59 | setResponseCode(400)
60 | setBody("{\"Invalid refresh token: unrecognized\"}")
61 | })
62 |
63 | val service = OauthRetrofitWithMoshiContainerBuilder(
64 | clientId = "clientId",
65 | authenticationLocalStorage = mockAuthenticationLocalStorage,
66 | sessionExpiredEventHandler = mockSessionExpiredEventHandler
67 | )
68 | .configureRetrofit {
69 | additionalRetrofitConfiguration(baseUrl(mockWebServer.url(basePath)))
70 | }
71 | .build()
72 | .oauthRetrofitContainer
73 | .sessionRetrofit
74 | .create()
75 |
76 | try {
77 | service.request().execute()
78 | } catch (throwable: Throwable) {
79 | throwable.printStackTrace()
80 | } finally {
81 | verify(mockSessionExpiredEventHandler, times(1)).onSessionExpired()
82 | verifyNoMoreInteractions(mockSessionExpiredEventHandler)
83 | }
84 | }
85 |
86 | @Test(timeout = 5000L)
87 | fun GIVEN_gson_and_standard_sessionExpiration_exception_WHEN_a_refresh_request_is_run_THEN_the_session_expiration_is_Called() {
88 | whenever(mockAuthenticationLocalStorage.refreshToken).doReturn("alma")
89 | mockWebServer.enqueue(MockResponse().apply {
90 | setResponseCode(401)
91 | setBody("")
92 | })
93 | mockWebServer.enqueue(MockResponse().apply {
94 | setResponseCode(400)
95 | setBody("{\"Invalid refresh token: unrecognized\"}")
96 | })
97 |
98 | val service = OauthRetrofitWithGsonContainerBuilder(
99 | clientId = "clientId",
100 | authenticationLocalStorage = mockAuthenticationLocalStorage,
101 | sessionExpiredEventHandler = mockSessionExpiredEventHandler
102 | )
103 | .configureRetrofit {
104 | additionalRetrofitConfiguration(baseUrl(mockWebServer.url(basePath)))
105 | }
106 | .build()
107 | .oauthRetrofitContainer
108 | .sessionRetrofit
109 | .create()
110 |
111 | try {
112 | service.request().execute()
113 | } catch (throwable: Throwable) {
114 | throwable.printStackTrace()
115 | } finally {
116 | verify(mockSessionExpiredEventHandler, times(1)).onSessionExpired()
117 | verifyNoMoreInteractions(mockSessionExpiredEventHandler)
118 | }
119 | }
120 |
121 | interface Service {
122 | @GET("service")
123 | fun request(): Call
124 | }
125 |
126 | companion object {
127 |
128 | private fun standard(builder: Retrofit.Builder): Retrofit.Builder = builder
129 |
130 | private fun errorWrapping(builder: Retrofit.Builder): Retrofit.Builder =
131 | builder.addCallAdapterFactory(ErrorWrappingAndParserCallAdapterFactory(workWithoutAnnotation = true))
132 |
133 | // library is incompatible with 2.0.0 of error wrapper
134 | // private fun errorHandler(builder: Retrofit.Builder): Retrofit.Builder =
135 | // builder.addCallAdapterFactory(RestHandlerCallAdapter.Builder().build())
136 |
137 | @Parameterized.Parameters
138 | @JvmStatic
139 | fun setupParameters(): Collection> {
140 | return listOf(
141 | arrayOf(::standard),
142 | arrayOf(::errorWrapping),
143 | // arrayOf(::errorHandler)
144 | )
145 | }
146 | }
147 | }
--------------------------------------------------------------------------------
/core/src/test/resources/authentication_service/refresh_token_bad_token.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": 400,
3 | "message": "Bad Request",
4 | "timestamp": "2019-04-04T09:37:35.608516Z",
5 | "errors": [
6 | {
7 | "code": "invalid_request",
8 | "errorMessage": "Invalid refresh token: the_value_of_refresh_token"
9 | }
10 | ]
11 | }
--------------------------------------------------------------------------------
/core/src/test/resources/authentication_service/refresh_token_expired.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": 401,
3 | "message": "Unauthorized",
4 | "timestamp": "2019-04-10T08:53:07.353967Z",
5 | "errors": [
6 | {
7 | "code": "invalid_request",
8 | "errorMessage": "Invalid refresh token (expired): "
9 | }
10 | ]
11 | }
--------------------------------------------------------------------------------
/core/src/test/resources/authentication_service/refresh_token_positive.json:
--------------------------------------------------------------------------------
1 | {
2 | "access_token": "best-token",
3 | "token_type": "bearer",
4 | "refresh_token": "new-refresh-token",
5 | "expires_in": 299,
6 | "scope": "trust",
7 | "user_id": "user-id",
8 | "jti": "82015e11-da47-4fb9-aa0a-6b518f402ed7"
9 | }
--------------------------------------------------------------------------------
/deploy.gradle:
--------------------------------------------------------------------------------
1 | // RELEASING
2 | // ./gradlew howToPublish
3 |
4 | ext.libraryGroupId = 'oauth-setup'
5 | ext.libraryVersion = '1.0.3'
6 | ext.githubPackagePath = 'halcyonmobile/retrofit-oauth2-helper'
--------------------------------------------------------------------------------
/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=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # Kotlin code style for this project: "official" or "obsolete":
15 | kotlin.code.style=official
16 | android.useAndroidX=true
17 | android.enableJetifier=true
18 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halcyonmobile/retrofit-oauth2-helper/cc59afbeb284e41188fa65b6bdecd605d2526596/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Mar 09 11:45:33 EET 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/oauth/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/oauth/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java-library'
2 | apply plugin: 'kotlin'
3 | apply plugin: 'com.jfrog.artifactory'
4 | apply plugin: 'maven-publish'
5 |
6 | dependencies {
7 | implementation fileTree(dir: 'libs', include: ['*.jar'])
8 | }
9 | compileKotlin {
10 | kotlinOptions {
11 | jvmTarget = project.jvmTarget
12 | }
13 | }
14 | compileTestKotlin {
15 | kotlinOptions {
16 | jvmTarget = project.jvmTarget
17 | }
18 | }
19 | dependencies {
20 | implementation "androidx.annotation:annotation:$annotationVersion"
21 | implementation "com.squareup.okio:okio:$okioVersion"
22 | api ("com.squareup.okhttp3:okhttp:$okHttpVersion") {
23 | exclude module: 'okio'
24 | }
25 | api ("com.squareup.retrofit2:retrofit:$retrofitVersion") {
26 | exclude module: 'okhttp'
27 | }
28 | api project(':oauthdependencies')
29 | }
30 |
31 | project.ext.set("libraryArtifactId", "oauth-setup")
32 | apply plugin: 'com.halcyonmobile.plugin.publish.custom.jar-library'
33 |
34 | sourceCompatibility = JavaVersion.VERSION_1_8
35 | targetCompatibility = JavaVersion.VERSION_1_8
--------------------------------------------------------------------------------
/oauth/src/main/java/com/halcyonmobile/oauth/AuthFinishedInvalidationException.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth
18 |
19 | import java.io.IOException
20 |
21 | /**
22 | * A specific exception which is only thrown when a request has a header with [key][INVALIDATION_AFTER_REFRESH_HEADER_NAME] and [value][INVALIDATION_AFTER_REFRESH_HEADER_VALUE] pair
23 | * AND authentication happened, meaning the refresh and access tokens were changed.
24 | *
25 | * This is useful when you have to send the refresh or access token in the request.
26 | * Then you can add the header and after the tokens are refreshed you will receive this exception.
27 | *
28 | * example:
29 | * ```
30 | * suspend fun logout(@Header(INVALIDATION_AFTER_REFRESH_HEADER_NAME) authExceptionHeader: String = INVALIDATION_AFTER_REFRESH_HEADER_VALUE)
31 | * ```
32 | */
33 | val authFinishedInvalidationException = IOException("Authentication was finished, invalidating the request with this exception", null)
34 |
35 | inline fun runCatchingCausedByAuthFinishedInvalidation(crossinline request: () -> T, crossinline doOnCatch: () -> T): T =
36 | try {
37 | request()
38 | } catch (ioException: Throwable) {
39 | if (ioException.findCauseMatching(authFinishedInvalidationException)) {
40 | doOnCatch()
41 | } else {
42 | throw ioException
43 | }
44 | }
45 |
46 | suspend inline fun runCatchingCausedByAuthFinishedInvalidationSuspend(crossinline request: suspend () -> T, crossinline doOnCatch: suspend () -> T): T =
47 | try {
48 | request()
49 | } catch (ioException: Throwable) {
50 | if (ioException.findCauseMatching(authFinishedInvalidationException)) {
51 | doOnCatch()
52 | } else {
53 | throw ioException
54 | }
55 | }
56 |
57 | fun Throwable.findCauseMatching(toFind: Throwable): Boolean {
58 | val cause = cause
59 | return when {
60 | this === toFind -> true
61 | this == cause -> false
62 | cause == null -> false
63 | else -> cause.findCauseMatching(toFind)
64 | }
65 | }
66 |
67 |
68 | const val INVALIDATION_AFTER_REFRESH_HEADER_NAME = "INVALIDATION_AFTER_REFRESH_HEADER_NAME"
69 | const val INVALIDATION_AFTER_REFRESH_HEADER_VALUE = "INVALIDATION_AFTER_REFRESH_HEADER_VALUE"
--------------------------------------------------------------------------------
/oauth/src/main/java/com/halcyonmobile/oauth/AuthenticationLocalStorageExtensions.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth
18 |
19 | import androidx.annotation.WorkerThread
20 | import com.halcyonmobile.oauth.dependencies.AuthenticationLocalStorage
21 |
22 | /**
23 | * Saves all the necessary data from the [sessionDataResponse] to the given [receiver][AuthenticationLocalStorage]
24 | */
25 | @WorkerThread
26 | fun AuthenticationLocalStorage.save(sessionDataResponse: SessionDataResponse) {
27 | accessToken = sessionDataResponse.token
28 | refreshToken = sessionDataResponse.refreshToken
29 | tokenType = sessionDataResponse.tokenType
30 | userId = sessionDataResponse.userId
31 | }
--------------------------------------------------------------------------------
/oauth/src/main/java/com/halcyonmobile/oauth/AuthenticationService.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth
18 |
19 | import retrofit2.Call
20 |
21 | /**
22 | * Service which is used by the [com.halcyonmobile.oauth.internal.Authenticator].
23 | *
24 | * It is bridge between the [com.halcyonmobile.oauth.internal.Authenticator] and a user defined retrofit service which
25 | * ultimately does the request.
26 | *
27 | * The instance will be created by the [AuthenticationServiceAdapter].
28 | */
29 | interface AuthenticationService {
30 |
31 | fun refreshToken(refreshToken: String): Call
32 | }
--------------------------------------------------------------------------------
/oauth/src/main/java/com/halcyonmobile/oauth/AuthenticationServiceAdapter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth
18 |
19 | /**
20 | * Adapter which adapts the given [T] type which should be a retrofit service for refresh token,
21 | * to a [AuthenticationService] which then can be used by the [com.halcyonmobile.oauth.internal.Authenticator]
22 | *
23 | * You may generate simple adapters using [com.halcyonmobile.dependencies.RefreshService] on your [T] retrofit service.
24 | */
25 | interface AuthenticationServiceAdapter {
26 |
27 | /**
28 | * When the [com.halcyonmobile.oauth.internal.Authenticator] calls the [AuthenticationService] returned by this
29 | * function should call the given [service]'s method with the refreshToken from the [AuthenticationService.refreshToken].
30 | */
31 | fun adapt(service: T): AuthenticationService
32 | }
--------------------------------------------------------------------------------
/oauth/src/main/java/com/halcyonmobile/oauth/DeprecatedIsSessionExpiredExceptionAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.halcyonmobile.oauth
2 |
3 | import com.halcyonmobile.oauth.IsSessionExpiredException as DeprecatedIsSessionExpiredException
4 | import com.halcyonmobile.oauth.dependencies.IsSessionExpiredException
5 |
6 | @Deprecated("Only needed as long as com.halcyonmobile.oauth.IsSessionExpiredException] is kept.", level = DeprecationLevel.WARNING)
7 | internal class DeprecatedIsSessionExpiredExceptionAdapter(
8 | private val delegate: DeprecatedIsSessionExpiredException
9 | ) : IsSessionExpiredException {
10 | override fun invoke(throwable: Throwable): Boolean {
11 | val httpException = throwable.causeHttpException ?: return false
12 | return delegate.invoke(httpException)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/oauth/src/main/java/com/halcyonmobile/oauth/IsSessionExpiredException.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth
18 |
19 | import retrofit2.HttpException
20 |
21 | /**
22 | * Defines what [HttpException] should be considered as SessionExpiration.
23 | */
24 | @Deprecated(
25 | message = "Deprecated API. Suggestion: replace import.",
26 | level = DeprecationLevel.WARNING,
27 | replaceWith = ReplaceWith("IsSessionExpiredException", "com.halcyonmobile.oauth.dependencies.IsSessionExpiredException")
28 | )
29 | interface IsSessionExpiredException {
30 |
31 | /**
32 | * @return true if the given [httpException] comming from the refresh-token-request should be considered as SessionExpiration.
33 | */
34 | operator fun invoke(httpException: HttpException): Boolean
35 | }
36 |
--------------------------------------------------------------------------------
/oauth/src/main/java/com/halcyonmobile/oauth/OauthRetrofitContainer.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth
18 |
19 | import retrofit2.Retrofit
20 |
21 | /**
22 | * The end result of the [OauthRetrofitContainerBuilder].
23 | *
24 | * Contains two instance of [Retrofit]:
25 | * - [sessionlessRetrofit] contains a [com.halcyonmobile.oauth.internal.ClientIdParameterInterceptor] and should be used
26 | * for requests which doesn't require session such as sign-in
27 | * - [sessionRetrofit] contains an [com.halcyonmobile.oauth.internal.Authenticator] which refreshes the expired token
28 | * when a running request gets 401 - Unauthorized and an [com.halcyonmobile.oauth.internal.AuthenticationHeaderInterceptor]
29 | * which adds an Authorization header.
30 | *
31 | * The Session is saved in the [com.halcyonmobile.oauth.dependencies.AuthenticationLocalStorage] and SessionExpiration
32 | * is notified via [com.halcyonmobile.oauth.dependencies.SessionExpiredEventHandler].
33 | */
34 | class OauthRetrofitContainer(val sessionRetrofit: Retrofit, val sessionlessRetrofit: Retrofit)
--------------------------------------------------------------------------------
/oauth/src/main/java/com/halcyonmobile/oauth/OauthRetrofitContainerBuilder.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth
18 |
19 | import com.halcyonmobile.oauth.IsSessionExpiredException as DeprecatedIsSessionExpiredException
20 | import com.halcyonmobile.oauth.dependencies.AuthenticationLocalStorage
21 | import com.halcyonmobile.oauth.dependencies.IsSessionExpiredException
22 | import com.halcyonmobile.oauth.dependencies.SessionExpiredEventHandler
23 | import com.halcyonmobile.oauth.internal.AuthenticationHeaderInterceptor
24 | import com.halcyonmobile.oauth.internal.Authenticator
25 | import com.halcyonmobile.oauth.internal.ClientIdParameterInterceptor
26 | import com.halcyonmobile.oauth.internal.DefaultIsSessionExpiredException
27 | import com.halcyonmobile.oauth.internal.SetAuthorizationHeaderUseCase
28 | import okhttp3.OkHttpClient
29 | import retrofit2.Retrofit
30 | import kotlin.reflect.KClass
31 |
32 | /**
33 | * Builder for the [OauthRetrofitContainer].
34 | *
35 | * @param clientId refers to the clientId header to attach for request which do not require session.
36 | * @param authenticationLocalStorage A persistent storage which stores the session related data.
37 | * @param refreshServiceClass This will be used to create a retrofit service which will be called for refreshing the tokens.
38 | * @param adapter An adapter which creates an [AuthenticationService] from the given [refreshServiceClass] retrofit-service instance.
39 | * @param sessionExpiredEventHandler A callback for session expiration which means the session is no longer valid and can't be refreshed via tokens.
40 | */
41 | class OauthRetrofitContainerBuilder(
42 | private val clientId: String,
43 | private val authenticationLocalStorage: AuthenticationLocalStorage,
44 | private val refreshServiceClass: KClass,
45 | private val adapter: AuthenticationServiceAdapter,
46 | private val sessionExpiredEventHandler: SessionExpiredEventHandler
47 | ) {
48 |
49 | private val retrofitBuilder = Retrofit.Builder()
50 | private val okHttpClient = OkHttpClient.Builder()
51 | private var isSessionExpiredException: IsSessionExpiredException = DefaultIsSessionExpiredException()
52 | private val sessionlessOkHttpClientConfigurations = mutableListOf OkHttpClient.Builder>()
53 | private val sessionOkHttpClientConfigurations = mutableListOf OkHttpClient.Builder>()
54 |
55 | /**
56 | * Additional configuration for the [okHttpClient] used by the sessionless-retrofit instance
57 | */
58 | fun configureSessionlessOkHttpClient(configure: OkHttpClient.Builder.() -> OkHttpClient.Builder) = apply {
59 | sessionlessOkHttpClientConfigurations.add(configure)
60 | }
61 |
62 | /**
63 | * Additionally configuration for the [okHttpClient] used by the session-retrofit instance
64 | */
65 | fun configureSessionOkHttpClient(configure: OkHttpClient.Builder.() -> OkHttpClient.Builder) = apply {
66 | sessionOkHttpClientConfigurations.add(configure)
67 | }
68 |
69 | /**
70 | * Additional configuration for the [okHttpClient] used by all retrofit instances provided.
71 | * Example usage: Logging.
72 | */
73 | fun configureBothOkHttpClient(configure: OkHttpClient.Builder.() -> OkHttpClient.Builder) = apply {
74 | configureSessionlessOkHttpClient(configure)
75 | configureSessionOkHttpClient(configure)
76 | }
77 |
78 | /**
79 | * Sets a class which decided what should be considered sessionExpiration.
80 | * By default a response containing "Invalid refresh token" or "Invalid refresh token (expired):" is considered, see [DefaultIsSessionExpiredException]
81 | */
82 | fun setIsSessionExpiredExceptionDecider(isSessionExpiredException: IsSessionExpiredException) = apply {
83 | this.isSessionExpiredException = isSessionExpiredException
84 | }
85 |
86 | /**
87 | * Sets a class which decided what should be considered sessionExpiration.
88 | * By default a response containing "Invalid refresh token" or "Invalid refresh token (expired):" is considered, see [DefaultIsSessionExpiredException]
89 | */
90 | fun setIsSessionExpiredExceptionDecider(isSessionExpiredException: DeprecatedIsSessionExpiredException) = apply {
91 | this.isSessionExpiredException = DeprecatedIsSessionExpiredExceptionAdapter(isSessionExpiredException)
92 | }
93 |
94 | /**
95 | * Configuration for the retrofit instance, here you should define your baseUrl, parsing etc.
96 | */
97 | fun configureRetrofit(configure: Retrofit.Builder.() -> Retrofit.Builder): OauthRetrofitContainerBuilder = apply {
98 | configure(retrofitBuilder)
99 | }
100 |
101 | /**
102 | * Creates the [OauthRetrofitContainer]
103 | */
104 | fun build(): OauthRetrofitContainer {
105 | val authorizationHeaderUseCase = SetAuthorizationHeaderUseCase(authenticationLocalStorage)
106 | val okHttpClient = okHttpClient.build()
107 | val retrofit = retrofitBuilder.build()
108 | val sessionlessOkHttpClient =
109 | sessionlessOkHttpClientConfigurations.fold(
110 | okHttpClient.newBuilder()
111 | .addInterceptor(ClientIdParameterInterceptor(clientId))
112 | ) { client, configurator ->
113 | configurator(client)
114 | }
115 | .build()
116 | val sessionlessRetrofit = retrofit.newBuilder()
117 | .client(sessionlessOkHttpClient)
118 | .build()
119 |
120 | val sessionOkHttpClient = sessionOkHttpClientConfigurations.fold(
121 | okHttpClient.newBuilder()
122 | .addInterceptor(AuthenticationHeaderInterceptor(authorizationHeaderUseCase))
123 | .authenticator(
124 | Authenticator(
125 | refreshTokenService = adapter.adapt(sessionlessRetrofit.create(refreshServiceClass.java)),
126 | authenticationLocalStorage = authenticationLocalStorage,
127 | sessionExpiredEventHandler = sessionExpiredEventHandler,
128 | isSessionExpiredException = isSessionExpiredException,
129 | setAuthorizationHeader = authorizationHeaderUseCase
130 | )
131 | )
132 | ) { client, configurator ->
133 | configurator(client)
134 | }
135 | .build()
136 |
137 | val sessionRetrofit = sessionlessRetrofit.newBuilder()
138 | .client(sessionOkHttpClient)
139 | .build()
140 |
141 | return OauthRetrofitContainer(sessionRetrofit, sessionlessRetrofit)
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/oauth/src/main/java/com/halcyonmobile/oauth/SessionDataResponse.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth
18 |
19 | /**
20 | * Interface defining the session response.
21 | *
22 | * When creating your refresh-token retrofit service, you should return a [retrofit2.Call] containing a subtype of this
23 | * class, which can be parsed properly.
24 | * This approach will make it easier to implement the [AuthenticationServiceAdapter]
25 | */
26 | interface SessionDataResponse {
27 |
28 | val userId: String
29 | val token: String
30 | val refreshToken: String
31 | val tokenType: String
32 | }
--------------------------------------------------------------------------------
/oauth/src/main/java/com/halcyonmobile/oauth/ThrowableException.kt:
--------------------------------------------------------------------------------
1 | package com.halcyonmobile.oauth
2 |
3 | import retrofit2.HttpException
4 |
5 | val Throwable.causeHttpException: HttpException?
6 | get() {
7 | var current: Throwable? = this
8 | while (current?.cause !== current && current != null && current !is HttpException) {
9 | current = current.cause
10 | }
11 |
12 | return current as? HttpException
13 | }
14 |
--------------------------------------------------------------------------------
/oauth/src/main/java/com/halcyonmobile/oauth/internal/AuthenticationHeaderInterceptor.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth.internal
18 |
19 | import okhttp3.Interceptor
20 | import okhttp3.Response
21 | import java.io.IOException
22 |
23 | /**
24 | * Interceptor which adds the Authorization token to the header of the request.
25 | */
26 | internal class AuthenticationHeaderInterceptor(private val setAuthorizationHeader: SetAuthorizationHeaderUseCase) : Interceptor {
27 |
28 | @Throws(IOException::class)
29 | override fun intercept(chain: Interceptor.Chain): Response =
30 | chain.proceed(setAuthorizationHeader(chain.request()))
31 | }
--------------------------------------------------------------------------------
/oauth/src/main/java/com/halcyonmobile/oauth/internal/Authenticator.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth.internal
18 |
19 | import com.halcyonmobile.oauth.AuthenticationService
20 | import com.halcyonmobile.oauth.INVALIDATION_AFTER_REFRESH_HEADER_NAME
21 | import com.halcyonmobile.oauth.INVALIDATION_AFTER_REFRESH_HEADER_VALUE
22 | import com.halcyonmobile.oauth.SessionDataResponse
23 | import com.halcyonmobile.oauth.authFinishedInvalidationException
24 | import com.halcyonmobile.oauth.dependencies.AuthenticationLocalStorage
25 | import com.halcyonmobile.oauth.dependencies.IsSessionExpiredException
26 | import com.halcyonmobile.oauth.dependencies.SessionExpiredEventHandler
27 | import com.halcyonmobile.oauth.save
28 | import java.io.IOException
29 | import okhttp3.Authenticator
30 | import okhttp3.Request
31 | import okhttp3.Response
32 | import okhttp3.Route
33 | import retrofit2.HttpException
34 |
35 | /**
36 | * Synchronized [okhttp3.Authenticator] which refreshes the token when
37 | * the request response is 401 - Unauthorized.
38 | *
39 | * @param refreshTokenService is used to run the token-refreshing
40 | * request returning a [SessionDataResponse]
41 | * @param authenticationLocalStorage the persistent storage for the
42 | * session [SessionDataResponse]
43 | * @param setAuthorizationHeader an internal use-case which adds the
44 | * authorization header to the request based on the stored session.
45 | * @param isSessionExpiredException the component defining if an
46 | * exception can be considered SessionExpired exception.
47 | * @param sessionExpiredEventHandler a listener for session expiration.
48 | */
49 | internal class Authenticator(
50 | private val refreshTokenService: AuthenticationService,
51 | private val authenticationLocalStorage: AuthenticationLocalStorage,
52 | private val setAuthorizationHeader: SetAuthorizationHeaderUseCase,
53 | private val isSessionExpiredException: IsSessionExpiredException,
54 | private val sessionExpiredEventHandler: SessionExpiredEventHandler
55 | ) : Authenticator {
56 |
57 | @Throws(IOException::class)
58 | override fun authenticate(route: Route?, response: Response): Request? {
59 | synchronized(this) {
60 | if (!setAuthorizationHeader.isSame(response.request)) {
61 | return setAuthorizationHeader(response.request)
62 | } else if (authenticationLocalStorage.refreshToken.isEmpty()) {
63 | return null
64 | }
65 |
66 | repeat(REFRESH_TOKEN_RETRY_COUNT) {
67 | try {
68 | val refreshTokenResponse = refreshTokenService.refreshToken(authenticationLocalStorage.refreshToken).execute()
69 | val sessionDataResponse: SessionDataResponse? = refreshTokenResponse.body()
70 | if (refreshTokenResponse.isSuccessful && sessionDataResponse != null) {
71 | authenticationLocalStorage.save(sessionDataResponse)
72 |
73 | // throw exception since the header is present
74 | if (response.request.header(INVALIDATION_AFTER_REFRESH_HEADER_NAME) == INVALIDATION_AFTER_REFRESH_HEADER_VALUE) {
75 | throw authFinishedInvalidationException
76 | }
77 |
78 | // retry request with the new tokens
79 | return setAuthorizationHeader(response.request)
80 | } else {
81 | throw HttpException(refreshTokenResponse)
82 | }
83 | } catch (throwable: Throwable) {
84 | when (throwable) {
85 | authFinishedInvalidationException -> throw throwable
86 | else -> {
87 | if (isSessionExpiredException(throwable)) {
88 | onSessionExpiration()
89 | return null
90 | }
91 | }
92 | }
93 | throwable.printStackTrace()
94 | }
95 | }
96 |
97 | // return the request with 401 error since the refresh token failed 3 times.
98 | return null
99 | }
100 | }
101 |
102 | /** On SessionExpiration we clear the data and report the event. */
103 | private fun onSessionExpiration() {
104 | authenticationLocalStorage.clear()
105 | sessionExpiredEventHandler.onSessionExpired()
106 | }
107 |
108 | companion object {
109 | private const val REFRESH_TOKEN_RETRY_COUNT = 3
110 | }
111 | }
--------------------------------------------------------------------------------
/oauth/src/main/java/com/halcyonmobile/oauth/internal/ClientIdParameterInterceptor.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth.internal
18 |
19 | import okhttp3.Interceptor
20 | import okhttp3.Response
21 |
22 | /**
23 | * Simple interceptor which adds the given [clientId] as query parameter to the request.
24 | */
25 | internal class ClientIdParameterInterceptor(private val clientId: String) : Interceptor {
26 |
27 | override fun intercept(chain: Interceptor.Chain): Response {
28 | val original = chain.request()
29 |
30 | val url = original.url.newBuilder()
31 | .addQueryParameter("client_id", clientId)
32 | .addQueryParameter("clientId", clientId)
33 | .build()
34 |
35 | return chain.proceed(original.newBuilder().url(url).build())
36 | }
37 | }
--------------------------------------------------------------------------------
/oauth/src/main/java/com/halcyonmobile/oauth/internal/DefaultIsSessionExpiredException.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth.internal
18 |
19 | import com.halcyonmobile.oauth.causeHttpException
20 | import com.halcyonmobile.oauth.dependencies.IsSessionExpiredException
21 | import retrofit2.HttpException
22 | import java.net.HttpURLConnection
23 |
24 | /**
25 | * Default implementation of [IsSessionExpiredException].
26 | *
27 | * Checks the [Throwable] against to most common session expiration
28 | * responses.
29 | */
30 | class DefaultIsSessionExpiredException : IsSessionExpiredException {
31 | override fun invoke(throwable: Throwable): Boolean {
32 | val httpException = throwable.causeHttpException ?: return false
33 | return httpException.isInvalidTokenException() || httpException.isExpiredTokenException()
34 | }
35 |
36 | companion object {
37 | private fun HttpException.isInvalidTokenException() =
38 | code() == HttpURLConnection.HTTP_BAD_REQUEST &&
39 | errorBodyAsString().contains("\"Invalid refresh token:")
40 |
41 | private fun HttpException.isExpiredTokenException() =
42 | code() == HttpURLConnection.HTTP_UNAUTHORIZED &&
43 | errorBodyAsString().contains("\"Invalid refresh token (expired):")
44 |
45 | private fun HttpException.errorBodyAsString() = response()?.errorBody()?.string().orEmpty()
46 | }
47 | }
--------------------------------------------------------------------------------
/oauth/src/main/java/com/halcyonmobile/oauth/internal/SetAuthorizationHeaderUseCase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth.internal
18 |
19 | import androidx.annotation.CheckResult
20 | import com.halcyonmobile.oauth.dependencies.AuthenticationLocalStorage
21 | import okhttp3.Request
22 |
23 | /**
24 | * Sets the authorization header to the given request with the tokens from [authenticationLocalStorage] and returns a new request instance.
25 | */
26 | internal class SetAuthorizationHeaderUseCase(private val authenticationLocalStorage: AuthenticationLocalStorage) {
27 |
28 | /**
29 | * Adds the authentication header to the given [request].
30 | */
31 | @CheckResult
32 | operator fun invoke(request: Request): Request =
33 | request.newBuilder()
34 | .header(AUTHORIZATION_KEY, "${authenticationLocalStorage.tokenType} ${authenticationLocalStorage.accessToken}")
35 | .build()
36 |
37 | /**
38 | * Checks if the given request already contains the same authentication header.
39 | */
40 | fun isSame(request: Request): Boolean = invoke(request).header(AUTHORIZATION_KEY) == request.header(AUTHORIZATION_KEY)
41 |
42 | companion object {
43 | private const val AUTHORIZATION_KEY = "Authorization"
44 | }
45 | }
--------------------------------------------------------------------------------
/oauthadaptergenerator/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/oauthadaptergenerator/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java-library'
2 | apply plugin: 'kotlin'
3 | apply plugin: 'com.jfrog.artifactory'
4 | apply plugin: 'maven-publish'
5 |
6 | dependencies {
7 | implementation fileTree(dir: 'libs', include: ['*.jar'])
8 | implementation "com.squareup:kotlinpoet:$kotlinPoetVersion"
9 | implementation project(':oauthdependencies')
10 | }
11 |
12 | compileKotlin {
13 | kotlinOptions {
14 | jvmTarget = project.jvmTarget
15 | }
16 | }
17 | compileTestKotlin {
18 | kotlinOptions {
19 | jvmTarget = project.jvmTarget
20 | }
21 | }
22 |
23 | project.ext.set("libraryArtifactId", "oauth-adapter-generator")
24 | apply plugin: 'com.halcyonmobile.plugin.publish.custom.jar-library'
25 |
26 | sourceCompatibility = JavaVersion.VERSION_1_8
27 | targetCompatibility = JavaVersion.VERSION_1_8
--------------------------------------------------------------------------------
/oauthadaptergenerator/src/main/java/com/halcyonmobile/oauthadaptergenerator/Processor.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthadaptergenerator
18 |
19 | import com.halcyonmobile.oauth.dependencies.RefreshService
20 | import com.squareup.kotlinpoet.ClassName
21 | import com.squareup.kotlinpoet.FileSpec
22 | import com.squareup.kotlinpoet.FunSpec
23 | import com.squareup.kotlinpoet.KModifier
24 | import com.squareup.kotlinpoet.ParameterSpec
25 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
26 | import com.squareup.kotlinpoet.PropertySpec
27 | import com.squareup.kotlinpoet.TypeSpec
28 | import com.squareup.kotlinpoet.TypeVariableName
29 | import com.squareup.kotlinpoet.asClassName
30 | import java.io.File
31 | import java.io.IOException
32 | import javax.annotation.processing.AbstractProcessor
33 | import javax.annotation.processing.Messager
34 | import javax.annotation.processing.ProcessingEnvironment
35 | import javax.annotation.processing.RoundEnvironment
36 | import javax.lang.model.SourceVersion
37 | import javax.lang.model.element.Element
38 | import javax.lang.model.element.ElementKind
39 | import javax.lang.model.element.ExecutableElement
40 | import javax.lang.model.element.TypeElement
41 | import javax.lang.model.util.Elements
42 | import javax.tools.Diagnostic
43 |
44 | /**
45 | * Annotation Processor which generated [com.halcyonmobile.oauth.AuthenticationServiceAdapter]
46 | * The generated adapter should be used in the [com.halcyonmobile.oauth.OauthRetrofitContainerBuilder]
47 | *
48 | * Example:
49 | *
50 | * Given the following file:
51 | * ```
52 | * @RefreshService
53 | * interface RefreshTokenService {
54 | *
55 | * @POST("oauth/token")
56 | * @FormUrlEncoded
57 | * fun refresh(@Field("refresh_token") refreshToken: String, @Field("grant_type") grantType: String = "refresh_token"): Call
58 | * }
59 | * ```
60 | *
61 | * Where RefreshTokenResponse looks lke this:
62 | * ```
63 | * data class RefreshTokenResponse(
64 | * override val userId: String,
65 | * override val token: String,
66 | * override val refreshToken: String,
67 | * override val tokenType: String
68 | * ) : SessionDataResponse
69 | * ```
70 | *
71 | * Will generate the following adapter:
72 | * ```
73 | * package com.halcyonmobile.oauth
74 | *
75 | * import com.halcyonmobile.core.RefreshTokenService
76 | * import kotlin.String
77 | * import retrofit2.Call
78 | *
79 | * /**
80 | * * [AuthenticationServiceAdapter] implementation generated for
81 | * * [com.halcyonmobile.core.RefreshTokenService] class annotated with [com.halcyonmobile.oauth.dependencies.RefreshService]
82 | * */
83 | * class RefreshTokenServiceAuthenticationServiceAdapter :
84 | * AuthenticationServiceAdapter {
85 | *
86 | * override fun adapt(service: RefreshTokenService): AuthenticationService = AuthenticationServiceImpl(service)
87 | *
88 | * class AuthenticationServiceImpl(private val service: RefreshTokenService) : AuthenticationService {
89 | *
90 | * override fun refreshToken(refreshToken: String): Call = service.refresh(refreshToken)
91 | * }
92 | * }
93 | * ```
94 | */
95 | class Processor : AbstractProcessor() {
96 |
97 | private lateinit var processingEnvironment: ProcessingEnvironment
98 | private lateinit var messager: Messager
99 | private lateinit var elementUtils: Elements
100 |
101 | @Synchronized
102 | override fun init(processingEnvironment: ProcessingEnvironment) {
103 | super.init(processingEnvironment)
104 | this.processingEnvironment = processingEnvironment
105 | messager = processingEnvironment.messager
106 | elementUtils = processingEnvironment.elementUtils
107 | }
108 |
109 | override fun getSupportedAnnotationTypes(): Set {
110 | val set = HashSet()
111 | set.add(RefreshService::class.java.canonicalName)
112 | return set
113 | }
114 |
115 | override fun getSupportedSourceVersion(): SourceVersion {
116 | return SourceVersion.latestSupported()
117 | }
118 |
119 | override fun process(set: Set, roundEnvironment: RoundEnvironment): Boolean {
120 | try {
121 | val annotatedElements = roundEnvironment.getElementsAnnotatedWith(RefreshService::class.java)
122 | showLogsForInvalidAnnotatedElements(annotatedElements)
123 | val types = annotatedElements.filterIsInstance()
124 | showLogsForInvalidClasses(types)
125 |
126 | processFilteredElements(types.filter { it.enclosedElements.filterIsInstance().count() == 1 })
127 |
128 | } catch (e: IOException) {
129 | e.printStackTrace()
130 | }
131 | return true
132 | }
133 |
134 | private fun showLogsForInvalidAnnotatedElements(elements: Set) {
135 | elements.filter { element -> element.kind != ElementKind.INTERFACE }
136 | .forEach {
137 | messager.printMessage(
138 | Diagnostic.Kind.ERROR,
139 | "Can only be applied to interfaces and ${it.simpleName} is not an interface"
140 | )
141 | }
142 | }
143 |
144 | private fun processFilteredElements(types: List) {
145 | types.map(::InterfaceToProcess)
146 | .map { it.toFileSpec() }
147 | .forEach { file ->
148 | processingEnvironment.options[KAPT_KOTLIN_GENERATED_OPTION_NAME]?.let(::File)?.let(file::writeTo)
149 | }
150 | }
151 |
152 | private fun showLogsForInvalidClasses(types: List) {
153 | types.filterNot { it.enclosedElements.filterIsInstance().count() == 1 }
154 | .forEach {
155 | messager.printMessage(
156 | Diagnostic.Kind.ERROR,
157 | "Will not generate class for $it, because it contains too many functions"
158 | )
159 | }
160 | }
161 |
162 | private fun InterfaceToProcess.toFileSpec() =
163 | FileSpec.builder("com.halcyonmobile.oauth", "${simpleName}AuthenticationServiceAdapter")
164 | .addType(createServiceAdapter())
165 | .build()
166 |
167 | private fun InterfaceToProcess.createServiceAdapter(): TypeSpec {
168 | val classOfTypeVariableName = ClassName.bestGuess("com.halcyonmobile.oauth.AuthenticationServiceAdapter")
169 | .parameterizedBy(TypeVariableName(className.toString()))
170 |
171 | return TypeSpec.classBuilder("${simpleName}AuthenticationServiceAdapter")
172 | .addModifiers(KModifier.INTERNAL)
173 | .addSuperinterface(classOfTypeVariableName)
174 | .addKdoc("[AuthenticationServiceAdapter] implementation generated for [$className] class annotated with [${RefreshService::class.asClassName()}]")
175 | .addType(createAuthenticationServiceImpl())
176 | .addFunction(
177 | FunSpec.builder("adapt")
178 | .addModifiers(KModifier.OVERRIDE)
179 | .addParameter(ParameterSpec.builder("service", className).build())
180 | .returns(ClassName.bestGuess("com.halcyonmobile.oauth.AuthenticationService"))
181 | .addStatement("return AuthenticationServiceImpl(service)")
182 | .build()
183 | )
184 | .build()
185 | }
186 |
187 | private fun InterfaceToProcess.createAuthenticationServiceImpl(): TypeSpec {
188 | val callType = ClassName.bestGuess("retrofit2.Call")
189 | .parameterizedBy(TypeVariableName("out SessionDataResponse"))
190 |
191 | return TypeSpec.classBuilder("AuthenticationServiceImpl")
192 | .addSuperinterface(ClassName.bestGuess("com.halcyonmobile.oauth.AuthenticationService"))
193 | .addFunction(
194 | FunSpec.builder("refreshToken")
195 | .returns(callType)
196 | .addModifiers(KModifier.OVERRIDE)
197 | .addParameter(
198 | ParameterSpec.builder("refreshToken", String::class.asClassName())
199 | .build()
200 | )
201 | .addStatement("return service.$functionName(refreshToken)")
202 | .build()
203 | )
204 | .primaryConstructor(
205 | FunSpec.constructorBuilder()
206 | .addParameter(ParameterSpec.builder("service", className).build())
207 | .build()
208 | )
209 | .addProperty(
210 | PropertySpec.builder("service", className)
211 | .initializer("service")
212 | .addModifiers(KModifier.PRIVATE)
213 | .build()
214 | )
215 | .build()
216 | }
217 |
218 | data class InterfaceToProcess(val simpleName: String, val functionName: String, val className: ClassName) {
219 |
220 | constructor(typeElement: TypeElement) : this(
221 | simpleName = typeElement.simpleName.toString(),
222 | functionName = typeElement.enclosedElements.filterIsInstance().first().simpleName.toString(),
223 | className = typeElement.asClassName()
224 | )
225 | }
226 |
227 | companion object {
228 | private const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated"
229 | }
230 | }
--------------------------------------------------------------------------------
/oauthadaptergenerator/src/main/resources/META-INF/services/javax.annotation.processing.Processor:
--------------------------------------------------------------------------------
1 | com.halcyonmobile.oauthadaptergenerator.Processor
--------------------------------------------------------------------------------
/oauthdependencies/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/oauthdependencies/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java-library'
2 | apply plugin: 'kotlin'
3 | apply plugin: 'com.jfrog.artifactory'
4 | apply plugin: 'maven-publish'
5 |
6 | dependencies {
7 | implementation fileTree(dir: 'libs', include: ['*.jar'])
8 | }
9 | compileKotlin {
10 | kotlinOptions {
11 | jvmTarget = project.jvmTarget
12 | }
13 | }
14 | compileTestKotlin {
15 | kotlinOptions {
16 | jvmTarget = project.jvmTarget
17 | }
18 | }
19 | dependencies {
20 | implementation fileTree(dir: 'libs', include: ['*.jar'])
21 | implementation "androidx.annotation:annotation:$annotationVersion"
22 | }
23 |
24 | project.ext.set("libraryArtifactId", "oauth-setup-dependencies")
25 | apply plugin: 'com.halcyonmobile.plugin.publish.custom.jar-library'
26 |
27 | sourceCompatibility = JavaVersion.VERSION_1_8
28 | targetCompatibility = JavaVersion.VERSION_1_8
--------------------------------------------------------------------------------
/oauthdependencies/src/main/java/com/halcyonmobile/oauth/dependencies/AuthenticationLocalStorage.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth.dependencies
18 |
19 | import androidx.annotation.WorkerThread
20 |
21 | /**
22 | * Simple storage for the session data.
23 | *
24 | * Note: workerThread annotation is just a suggestion
25 | */
26 | interface AuthenticationLocalStorage {
27 |
28 | /**
29 | * The id of signed-in user.
30 | */
31 | @set:WorkerThread
32 | @get:WorkerThread
33 | var userId: String
34 |
35 | /**
36 | * The access Token of signed-in user.
37 | */
38 | @set:WorkerThread
39 | @get:WorkerThread
40 | var accessToken: String
41 |
42 | /**
43 | * The type of [accessToken] of signed-in used.
44 | */
45 | @set:WorkerThread
46 | @get:WorkerThread
47 | var tokenType: String
48 |
49 | /**
50 | * The refresh Token of signed-in used.
51 | */
52 | @set:WorkerThread
53 | @get:WorkerThread
54 | var refreshToken: String
55 |
56 | /**
57 | * Clears all data from the storage, should be called when the session is no longer valid.
58 | */
59 | @WorkerThread
60 | fun clear()
61 | }
--------------------------------------------------------------------------------
/oauthdependencies/src/main/java/com/halcyonmobile/oauth/dependencies/IsSessionExpiredException.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth.dependencies
18 |
19 | /**
20 | * Defines what [Throwable] should be considered as SessionExpiration.
21 | */
22 | interface IsSessionExpiredException {
23 |
24 | /**
25 | * @return true if the given [throwable] coming from the
26 | * refresh-token-request should be considered as SessionExpiration.
27 | */
28 | operator fun invoke(throwable: Throwable): Boolean
29 |
30 | }
--------------------------------------------------------------------------------
/oauthdependencies/src/main/java/com/halcyonmobile/oauth/dependencies/RefreshService.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth.dependencies
18 |
19 | /**
20 | * Interfaces annotated with this class will generate a [com.halcyonmobile.oauth.AuthenticationServiceAdapter].
21 | * The generated adapter should be used in the [com.halcyonmobile.oauth.OauthRetrofitContainerBuilder]
22 | *
23 | * Restrictions:
24 | * - the annotated class must be an interface.
25 | * - it must contain only one function, which has only one parameter without default value and it's the first one
26 | * - the function has to return a [retrofit2.Call] with a typeParameter which extends [SessionDataResponse]
27 | *
28 | * Example:
29 | *
30 | * Given the following file:
31 | * ```
32 | * @RefreshService
33 | * interface RefreshTokenService {
34 | *
35 | * @POST("oauth/token")
36 | * @FormUrlEncoded
37 | * fun refresh(@Field("refresh_token") refreshToken: String, @Field("grant_type") grantType: String = "refresh_token"): Call
38 | * }
39 | * ```
40 | *
41 | * Where RefreshTokenResponse looks lke this:
42 | * ```
43 | * data class RefreshTokenResponse(
44 | * override val userId: String,
45 | * override val token: String,
46 | * override val refreshToken: String,
47 | * override val tokenType: String
48 | * ) : SessionDataResponse
49 | * ```
50 | *
51 | * Will generate the following adapter:
52 | * ```
53 | * package com.halcyonmobile.oauth
54 | *
55 | * import com.halcyonmobile.core.RefreshTokenService
56 | * import kotlin.String
57 | * import retrofit2.Call
58 | *
59 | * /**
60 | * * [AuthenticationServiceAdapter] implementation generated for
61 | * * [com.halcyonmobile.core.RefreshTokenService] class annotated with [com.halcyonmobile.oauth.dependencies.RefreshService]
62 | * */
63 | * class RefreshTokenServiceAuthenticationServiceAdapter :
64 | * AuthenticationServiceAdapter {
65 | *
66 | * override fun adapt(service: RefreshTokenService): AuthenticationService = AuthenticationServiceImpl(service)
67 | *
68 | * class AuthenticationServiceImpl(private val service: RefreshTokenService) : AuthenticationService {
69 | *
70 | * override fun refreshToken(refreshToken: String): Call = service.refresh(refreshToken)
71 | * }
72 | * }
73 | * ```
74 | */
75 | @Target(AnnotationTarget.CLASS)
76 | annotation class RefreshService
--------------------------------------------------------------------------------
/oauthdependencies/src/main/java/com/halcyonmobile/oauth/dependencies/SessionExpiredEventHandler.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauth.dependencies
18 |
19 | import androidx.annotation.WorkerThread
20 |
21 | /**
22 | * Callback to handle SessionExpiration which means the authentication token expired and the refreshToken can't be used to get a new one.
23 | * The user has to log in again.
24 | *
25 | * When this method is called the [AuthenticationLocalStorage] is already cleared.
26 | */
27 | interface SessionExpiredEventHandler {
28 |
29 | @WorkerThread
30 | fun onSessionExpired()
31 | }
--------------------------------------------------------------------------------
/oauthgson/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/oauthgson/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java-library'
2 | apply plugin: 'kotlin'
3 | apply plugin: 'kotlin-kapt'
4 | apply plugin: 'com.jfrog.artifactory'
5 | apply plugin: 'maven-publish'
6 |
7 | dependencies {
8 | implementation fileTree(dir: 'libs', include: ['*.jar'])
9 |
10 | api "com.google.code.gson:gson:$gsonVersion"
11 | implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
12 | api project(':oauthparsing')
13 |
14 | testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0"
15 | testImplementation "com.squareup.okhttp3:mockwebserver:$okHttpVersion"
16 | }
17 | compileKotlin {
18 | kotlinOptions {
19 | jvmTarget = project.jvmTarget
20 | }
21 | }
22 | compileTestKotlin {
23 | kotlinOptions {
24 | jvmTarget = project.jvmTarget
25 | }
26 | }
27 |
28 | project.ext.set("libraryArtifactId", "oauth-setup-gson")
29 | apply plugin: 'com.halcyonmobile.plugin.publish.custom.jar-library'
30 |
31 | sourceCompatibility = JavaVersion.VERSION_1_8
32 | targetCompatibility = JavaVersion.VERSION_1_8
--------------------------------------------------------------------------------
/oauthgson/src/main/java/com/halcyonmobile/oauthgson/OauthRetrofitContainerWithGson.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthgson
18 |
19 | import com.google.gson.Gson
20 | import com.halcyonmobile.oauth.OauthRetrofitContainer
21 | import com.halcyonmobile.oauthparsing.OauthRetrofitContainerWithParser
22 |
23 | class OauthRetrofitContainerWithGson(
24 | override val oauthRetrofitContainer: OauthRetrofitContainer,
25 | override val parser: Gson
26 | ) : OauthRetrofitContainerWithParser {
27 | val gson: Gson = parser
28 | }
--------------------------------------------------------------------------------
/oauthgson/src/main/java/com/halcyonmobile/oauthgson/OauthRetrofitWithGsonContainerBuilder.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthgson
18 |
19 | import com.halcyonmobile.oauth.IsSessionExpiredException as DeprecatedIsSessionExpiredException
20 | import com.google.gson.Gson
21 | import com.google.gson.GsonBuilder
22 | import com.halcyonmobile.oauth.OauthRetrofitContainer
23 | import com.halcyonmobile.oauth.OauthRetrofitContainerBuilder
24 | import com.halcyonmobile.oauth.SessionDataResponse
25 | import com.halcyonmobile.oauth.dependencies.AuthenticationLocalStorage
26 | import com.halcyonmobile.oauth.dependencies.IsSessionExpiredException
27 | import com.halcyonmobile.oauth.dependencies.SessionExpiredEventHandler
28 | import com.halcyonmobile.oauthparsing.AuthenticationServiceAdapterImpl
29 | import com.halcyonmobile.oauthparsing.OauthRetrofitWithParserContainerBuilder
30 | import com.halcyonmobile.oauthparsing.RefreshServiceFieldParameterProvider
31 | import com.halcyonmobile.oauthparsing.RefreshTokenService
32 | import okhttp3.OkHttpClient
33 | import retrofit2.Retrofit
34 | import retrofit2.converter.gson.GsonConverterFactory
35 |
36 | /**
37 | * Builder class for [OauthRetrofitContainer] which uses moshi as it's [retrofit2.Converter.Factory].
38 | *
39 | * It has a default parser, which can be disabled, see [disableDefaultParsing].
40 | */
41 | class OauthRetrofitWithGsonContainerBuilder(
42 | clientId: String,
43 | authenticationLocalStorage: AuthenticationLocalStorage,
44 | sessionExpiredEventHandler: SessionExpiredEventHandler
45 | ) : OauthRetrofitWithParserContainerBuilder {
46 |
47 | private var disableDefaultParsing = false
48 | private val gsonBuilder = GsonBuilder()
49 | private val authenticationServiceAdapterImpl = AuthenticationServiceAdapterImpl()
50 | private var oauthRetrofitContainerBuilder = OauthRetrofitContainerBuilder(
51 | clientId = clientId,
52 | adapter = authenticationServiceAdapterImpl,
53 | sessionExpiredEventHandler = sessionExpiredEventHandler,
54 | authenticationLocalStorage = authenticationLocalStorage,
55 | refreshServiceClass = RefreshTokenService::class
56 | )
57 |
58 | override fun setRefreshServicePath(refreshServicePath: String): OauthRetrofitWithGsonContainerBuilder = apply {
59 | authenticationServiceAdapterImpl.refreshServicePath = refreshServicePath
60 | }
61 |
62 | override fun setRefreshTokenFieldName(refreshTokenFieldName: String): OauthRetrofitWithGsonContainerBuilder =
63 | apply {
64 | authenticationServiceAdapterImpl.refreshTokenFieldName = refreshTokenFieldName
65 | }
66 |
67 | override fun setGrantType(grantType: String?) = apply {
68 | authenticationServiceAdapterImpl.grantType = grantType
69 | }
70 |
71 | override fun setRefreshServiceFieldParameterProvider(refreshServiceFieldParameterProvider: RefreshServiceFieldParameterProvider?): OauthRetrofitWithGsonContainerBuilder =
72 | apply {
73 | authenticationServiceAdapterImpl.refreshServiceFieldParameterProvider = refreshServiceFieldParameterProvider
74 | }
75 |
76 | override fun configureSessionlessOkHttpClient(configure: OkHttpClient.Builder.() -> OkHttpClient.Builder): OauthRetrofitWithGsonContainerBuilder =
77 | apply {
78 | oauthRetrofitContainerBuilder = oauthRetrofitContainerBuilder.configureSessionlessOkHttpClient(configure)
79 | }
80 |
81 | override fun configureSessionOkHttpClient(configure: OkHttpClient.Builder.() -> OkHttpClient.Builder): OauthRetrofitWithGsonContainerBuilder =
82 | apply {
83 | oauthRetrofitContainerBuilder = oauthRetrofitContainerBuilder.configureSessionOkHttpClient(configure)
84 | }
85 |
86 | override fun configureBothOkHttpClient(configure: OkHttpClient.Builder.() -> OkHttpClient.Builder): OauthRetrofitWithGsonContainerBuilder =
87 | apply {
88 | oauthRetrofitContainerBuilder = oauthRetrofitContainerBuilder.configureBothOkHttpClient(configure)
89 | }
90 |
91 | override fun configureRetrofit(configure: Retrofit.Builder.() -> Retrofit.Builder): OauthRetrofitWithGsonContainerBuilder =
92 | apply {
93 | oauthRetrofitContainerBuilder = oauthRetrofitContainerBuilder.configureRetrofit(configure)
94 | }
95 |
96 | override fun disableDefaultParsing() = apply {
97 | disableDefaultParsing = true
98 | }
99 |
100 | override fun setIsSessionExpiredExceptionDecider(isSessionExpiredException: IsSessionExpiredException) = apply {
101 | oauthRetrofitContainerBuilder = oauthRetrofitContainerBuilder.setIsSessionExpiredExceptionDecider(isSessionExpiredException)
102 | }
103 |
104 | override fun setIsSessionExpiredExceptionDecider(isSessionExpiredException: DeprecatedIsSessionExpiredException) = apply {
105 | oauthRetrofitContainerBuilder = oauthRetrofitContainerBuilder.setIsSessionExpiredExceptionDecider(isSessionExpiredException)
106 | }
107 |
108 | /**
109 | * Configuration for the [Gson] instance.
110 | */
111 | fun configureGson(configure: GsonBuilder.() -> GsonBuilder): OauthRetrofitWithGsonContainerBuilder =
112 | apply {
113 | configure(gsonBuilder)
114 | }
115 |
116 | override fun build(): OauthRetrofitContainerWithGson {
117 | val gson = gsonBuilder
118 | .let {
119 | if (disableDefaultParsing) {
120 | it
121 | } else {
122 | it.registerTypeAdapter(SessionDataResponse::class.java, SessionDataResponseDeserializer())
123 | }
124 | }
125 | .create()
126 | oauthRetrofitContainerBuilder = oauthRetrofitContainerBuilder.configureRetrofit {
127 | addConverterFactory(GsonConverterFactory.create(gson))
128 | }
129 | return OauthRetrofitContainerWithGson(oauthRetrofitContainerBuilder.build(), gson)
130 | }
131 |
132 | }
--------------------------------------------------------------------------------
/oauthgson/src/main/java/com/halcyonmobile/oauthgson/RefreshTokenResponse.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthgson
18 |
19 | import com.google.gson.annotations.SerializedName
20 | import com.halcyonmobile.oauth.SessionDataResponse
21 |
22 | /**
23 | * Default model implementing [SessionDataResponse].
24 | */
25 | data class RefreshTokenResponse(
26 | @SerializedName("user_id") override val userId: String,
27 | @SerializedName("access_token") override val token: String,
28 | @SerializedName("refresh_token") override val refreshToken: String,
29 | @SerializedName("token_type") override val tokenType: String
30 | ) : SessionDataResponse
--------------------------------------------------------------------------------
/oauthgson/src/main/java/com/halcyonmobile/oauthgson/SessionDataResponseDeserializer.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthgson
18 |
19 | import com.google.gson.JsonDeserializationContext
20 | import com.google.gson.JsonDeserializer
21 | import com.google.gson.JsonElement
22 | import com.google.gson.JsonParseException
23 | import com.halcyonmobile.oauth.SessionDataResponse
24 | import java.lang.reflect.Type
25 |
26 | class SessionDataResponseDeserializer : JsonDeserializer {
27 |
28 | @Throws(JsonParseException::class)
29 | override fun deserialize(
30 | jsonElement: JsonElement,
31 | typeOfT: Type,
32 | context: JsonDeserializationContext
33 | ): SessionDataResponse =
34 | context.deserialize(jsonElement.asJsonObject, RefreshTokenResponse::class.java)
35 |
36 | }
--------------------------------------------------------------------------------
/oauthmoshi/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/oauthmoshi/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java-library'
2 | apply plugin: 'kotlin'
3 | apply plugin: 'kotlin-kapt'
4 | apply plugin: 'com.jfrog.artifactory'
5 | apply plugin: 'maven-publish'
6 |
7 | dependencies {
8 | implementation fileTree(dir: 'libs', include: ['*.jar'])
9 |
10 | api project(':oauthparsing')
11 | api "com.squareup.moshi:moshi:$moshiVersion"
12 | api("com.squareup.retrofit2:converter-moshi:$retrofitVersion") {
13 | exclude module: 'okhttp'
14 | exclude module: 'moshi'
15 | }
16 | kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion"
17 |
18 | testImplementation "junit:junit:$junitVersion"
19 | testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0"
20 | testImplementation "com.squareup.okhttp3:mockwebserver:$okHttpVersion"
21 | }
22 | compileKotlin {
23 | kotlinOptions {
24 | jvmTarget = project.jvmTarget
25 | }
26 | }
27 | compileTestKotlin {
28 | kotlinOptions {
29 | jvmTarget = project.jvmTarget
30 | }
31 | }
32 |
33 | project.ext.set("libraryArtifactId", "oauth-setup-moshi")
34 | apply plugin: 'com.halcyonmobile.plugin.publish.custom.jar-library'
35 |
36 | sourceCompatibility = JavaVersion.VERSION_1_8
37 | targetCompatibility = JavaVersion.VERSION_1_8
38 |
--------------------------------------------------------------------------------
/oauthmoshi/src/main/java/com/halcyonmobile/oauthmoshi/OauthRetrofitContainerWithMoshi.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthmoshi
18 |
19 | import com.halcyonmobile.oauth.OauthRetrofitContainer
20 | import com.halcyonmobile.oauthparsing.OauthRetrofitContainerWithParser
21 | import com.squareup.moshi.Moshi
22 |
23 | class OauthRetrofitContainerWithMoshi(
24 | override val oauthRetrofitContainer: OauthRetrofitContainer,
25 | override val parser: Moshi
26 | ) : OauthRetrofitContainerWithParser {
27 | val moshi = parser
28 | }
--------------------------------------------------------------------------------
/oauthmoshi/src/main/java/com/halcyonmobile/oauthmoshi/OauthRetrofitWithMoshiContainerBuilder.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthmoshi
18 |
19 | import com.halcyonmobile.oauth.OauthRetrofitContainer
20 | import com.halcyonmobile.oauth.OauthRetrofitContainerBuilder
21 | import com.halcyonmobile.oauth.dependencies.AuthenticationLocalStorage
22 | import com.halcyonmobile.oauth.dependencies.IsSessionExpiredException
23 | import com.halcyonmobile.oauth.dependencies.SessionExpiredEventHandler
24 | import com.halcyonmobile.oauthparsing.AuthenticationServiceAdapterImpl
25 | import com.halcyonmobile.oauthparsing.OauthRetrofitWithParserContainerBuilder
26 | import com.halcyonmobile.oauthparsing.RefreshServiceFieldParameterProvider
27 | import com.halcyonmobile.oauthparsing.RefreshTokenService
28 | import com.squareup.moshi.Moshi
29 | import okhttp3.OkHttpClient
30 | import retrofit2.Retrofit
31 | import retrofit2.converter.moshi.MoshiConverterFactory
32 | import com.halcyonmobile.oauth.IsSessionExpiredException as DeprecatedIsSessionExpiredException
33 |
34 | /**
35 | * Builder class for [OauthRetrofitContainer] which uses moshi as it's [retrofit2.Converter.Factory].
36 | *
37 | * It has a default parser, which can be disabled, see [disableDefaultParsing].
38 | */
39 | class OauthRetrofitWithMoshiContainerBuilder(
40 | clientId: String,
41 | authenticationLocalStorage: AuthenticationLocalStorage,
42 | sessionExpiredEventHandler: SessionExpiredEventHandler
43 | ) : OauthRetrofitWithParserContainerBuilder {
44 |
45 | private var disableDefaultParsing = false
46 | private val moshiBuilder = Moshi.Builder()
47 | private val authenticationServiceAdapterImpl = AuthenticationServiceAdapterImpl()
48 | private var oauthRetrofitContainerBuilder = OauthRetrofitContainerBuilder(
49 | clientId = clientId,
50 | adapter = authenticationServiceAdapterImpl,
51 | sessionExpiredEventHandler = sessionExpiredEventHandler,
52 | authenticationLocalStorage = authenticationLocalStorage,
53 | refreshServiceClass = RefreshTokenService::class
54 | )
55 |
56 | override fun setRefreshServicePath(refreshServicePath: String): OauthRetrofitWithMoshiContainerBuilder = apply {
57 | authenticationServiceAdapterImpl.refreshServicePath = refreshServicePath
58 | }
59 |
60 | override fun setRefreshTokenFieldName(refreshTokenFieldName: String): OauthRetrofitWithMoshiContainerBuilder =
61 | apply {
62 | authenticationServiceAdapterImpl.refreshTokenFieldName = refreshTokenFieldName
63 | }
64 |
65 | override fun setGrantType(grantType: String?): OauthRetrofitWithMoshiContainerBuilder = apply {
66 | authenticationServiceAdapterImpl.grantType = grantType
67 | }
68 |
69 |
70 | override fun setRefreshServiceFieldParameterProvider(refreshServiceFieldParameterProvider: RefreshServiceFieldParameterProvider?): OauthRetrofitWithMoshiContainerBuilder =
71 | apply {
72 | authenticationServiceAdapterImpl.refreshServiceFieldParameterProvider = refreshServiceFieldParameterProvider
73 | }
74 |
75 | override fun configureSessionlessOkHttpClient(configure: OkHttpClient.Builder.() -> OkHttpClient.Builder): OauthRetrofitWithMoshiContainerBuilder =
76 | apply {
77 | oauthRetrofitContainerBuilder = oauthRetrofitContainerBuilder.configureSessionlessOkHttpClient(configure)
78 | }
79 |
80 | override fun configureSessionOkHttpClient(configure: OkHttpClient.Builder.() -> OkHttpClient.Builder): OauthRetrofitWithMoshiContainerBuilder =
81 | apply {
82 | oauthRetrofitContainerBuilder = oauthRetrofitContainerBuilder.configureSessionOkHttpClient(configure)
83 | }
84 |
85 | override fun configureBothOkHttpClient(configure: OkHttpClient.Builder.() -> OkHttpClient.Builder): OauthRetrofitWithMoshiContainerBuilder =
86 | apply {
87 | oauthRetrofitContainerBuilder = oauthRetrofitContainerBuilder.configureBothOkHttpClient(configure)
88 | }
89 |
90 | override fun configureRetrofit(configure: Retrofit.Builder.() -> Retrofit.Builder): OauthRetrofitWithMoshiContainerBuilder =
91 | apply {
92 | oauthRetrofitContainerBuilder = oauthRetrofitContainerBuilder.configureRetrofit(configure)
93 | }
94 |
95 | override fun disableDefaultParsing() = apply {
96 | disableDefaultParsing = true
97 | }
98 |
99 | override fun setIsSessionExpiredExceptionDecider(isSessionExpiredException: IsSessionExpiredException) = apply{
100 | oauthRetrofitContainerBuilder = oauthRetrofitContainerBuilder.setIsSessionExpiredExceptionDecider(isSessionExpiredException)
101 | }
102 |
103 | override fun setIsSessionExpiredExceptionDecider(isSessionExpiredException: DeprecatedIsSessionExpiredException) = apply{
104 | oauthRetrofitContainerBuilder = oauthRetrofitContainerBuilder.setIsSessionExpiredExceptionDecider(isSessionExpiredException)
105 | }
106 |
107 | /**
108 | * Configuration for the [Moshi] instance.
109 | */
110 | fun configureMoshi(configure: Moshi.Builder.() -> Moshi.Builder): OauthRetrofitWithMoshiContainerBuilder =
111 | apply {
112 | configure(moshiBuilder)
113 | }
114 |
115 | override fun build(): OauthRetrofitContainerWithMoshi {
116 | val moshi = moshiBuilder
117 | .let {
118 | if (disableDefaultParsing) {
119 | it
120 | } else {
121 | it.add(RefreshTokenResponseWrapper())
122 | }
123 | }
124 | .build()
125 | oauthRetrofitContainerBuilder = oauthRetrofitContainerBuilder.configureRetrofit {
126 | addConverterFactory(MoshiConverterFactory.create(moshi))
127 | }
128 | return OauthRetrofitContainerWithMoshi(oauthRetrofitContainerBuilder.build(), moshi)
129 | }
130 |
131 | }
--------------------------------------------------------------------------------
/oauthmoshi/src/main/java/com/halcyonmobile/oauthmoshi/RefreshTokenResponse.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthmoshi
18 |
19 | import com.halcyonmobile.oauth.SessionDataResponse
20 | import com.squareup.moshi.Json
21 | import com.squareup.moshi.JsonClass
22 |
23 | /**
24 | * Default model implementing [SessionDataResponse].
25 | */
26 | @JsonClass(generateAdapter = true)
27 | data class RefreshTokenResponse(
28 | @Json(name = "user_id") override val userId: String,
29 | @Json(name = "access_token") override val token: String,
30 | @Json(name = "refresh_token") override val refreshToken: String,
31 | @Json(name = "token_type") override val tokenType: String
32 | ) : SessionDataResponse
--------------------------------------------------------------------------------
/oauthmoshi/src/main/java/com/halcyonmobile/oauthmoshi/RefreshTokenResponseWrapper.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthmoshi
18 |
19 | import com.halcyonmobile.oauth.SessionDataResponse
20 | import com.squareup.moshi.FromJson
21 | import com.squareup.moshi.JsonAdapter
22 | import com.squareup.moshi.JsonWriter
23 | import com.squareup.moshi.ToJson
24 |
25 | /**
26 | * JsonAdapter to enable moshi to parse [SessionDataResponse] via [RefreshTokenResponse] JsonAdapter.
27 | */
28 | class RefreshTokenResponseWrapper {
29 |
30 | @FromJson
31 | fun fromJson(refreshTokenResponse: RefreshTokenResponse?): SessionDataResponse? = refreshTokenResponse
32 |
33 | @ToJson
34 | fun toJson(
35 | jsonWriter: JsonWriter,
36 | sessionDataResponse: SessionDataResponse,
37 | delegate: JsonAdapter
38 | ) {
39 | val refreshTokenResponse = if (sessionDataResponse is RefreshTokenResponse) {
40 | sessionDataResponse
41 | } else {
42 | RefreshTokenResponse(
43 | userId = sessionDataResponse.userId,
44 | refreshToken = sessionDataResponse.refreshToken,
45 | token = sessionDataResponse.token,
46 | tokenType = sessionDataResponse.tokenType
47 | )
48 | }
49 | delegate.toJson(jsonWriter, refreshTokenResponse)
50 | }
51 | }
--------------------------------------------------------------------------------
/oauthmoshi/src/test/java/com/halcyonmobile/oauthmoshi/OauthRetrofitWithMoshiContainerBuilderTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthmoshi
18 |
19 | import com.halcyonmobile.oauth.IsSessionExpiredException as DeprecatedIsSessionExpiredException
20 | import com.halcyonmobile.oauth.SessionDataResponse
21 | import com.halcyonmobile.oauth.dependencies.AuthenticationLocalStorage
22 | import com.halcyonmobile.oauth.dependencies.IsSessionExpiredException
23 | import com.halcyonmobile.oauth.dependencies.SessionExpiredEventHandler
24 | import com.halcyonmobile.oauthparsing.RefreshServiceFieldParameterProvider
25 | import com.nhaarman.mockitokotlin2.doReturn
26 | import com.nhaarman.mockitokotlin2.mock
27 | import com.nhaarman.mockitokotlin2.times
28 | import com.nhaarman.mockitokotlin2.verify
29 | import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
30 | import com.nhaarman.mockitokotlin2.verifyZeroInteractions
31 | import com.nhaarman.mockitokotlin2.whenever
32 | import com.squareup.moshi.FromJson
33 | import com.squareup.moshi.JsonAdapter
34 | import com.squareup.moshi.JsonReader
35 | import com.squareup.moshi.JsonWriter
36 | import com.squareup.moshi.ToJson
37 | import okhttp3.mockwebserver.MockResponse
38 | import okhttp3.mockwebserver.MockWebServer
39 | import org.junit.After
40 | import org.junit.Assert
41 | import org.junit.Before
42 | import org.junit.Test
43 | import retrofit2.Call
44 | import retrofit2.HttpException
45 | import retrofit2.create
46 | import retrofit2.http.GET
47 |
48 | @Suppress("TestFunctionName")
49 | class OauthRetrofitWithMoshiContainerBuilderTest {
50 |
51 | private lateinit var basePath: String
52 | private lateinit var mockAuthenticationLocalStorage: AuthenticationLocalStorage
53 | private lateinit var mockSessionExpiredEventHandler: SessionExpiredEventHandler
54 | private lateinit var builder: OauthRetrofitWithMoshiContainerBuilder
55 | private lateinit var mockWebServer: MockWebServer
56 |
57 | @Before
58 | fun setup() {
59 | basePath = "/google.com/test/"
60 | mockWebServer = MockWebServer()
61 | mockAuthenticationLocalStorage = mock()
62 | mockSessionExpiredEventHandler = mock()
63 | builder = OauthRetrofitWithMoshiContainerBuilder("clientId", mockAuthenticationLocalStorage, mockSessionExpiredEventHandler)
64 | .configureRetrofit {
65 | baseUrl(mockWebServer.url(basePath))
66 | }
67 | }
68 |
69 | @After
70 | fun tearDown() {
71 | mockWebServer.shutdown()
72 | }
73 |
74 | @Test
75 | fun GIVEN_default_setup_WHEN_a_refresh_request_is_run_THEN_it_has_proper_default_setup() {
76 | whenever(mockAuthenticationLocalStorage.refreshToken).doReturn("alma")
77 | mockWebServer.setUnauthorizedThenProperRefreshThenSuccessRequests()
78 | val service = builder.build().oauthRetrofitContainer.sessionRetrofit.create()
79 |
80 | service.request().execute()
81 |
82 | val originalRequests = mockWebServer.takeRequest()
83 | val refreshRequest = mockWebServer.takeRequest()
84 |
85 | Assert.assertEquals("POST", refreshRequest.method)
86 | Assert.assertEquals("${basePath}oauth/token", refreshRequest.requestUrl?.encodedPath)
87 | Assert.assertEquals(setOf("grant_type=refresh_token", "refresh_token=alma"), refreshRequest.body.readString(Charsets.UTF_8).split("&").toSet())
88 | verifyZeroInteractions(mockSessionExpiredEventHandler)
89 | }
90 |
91 | @Test
92 | fun GIVEN_configured_grant_type_WHEN_a_refresh_request_is_run_THEN_it_has_proper_default_setup() {
93 | whenever(mockAuthenticationLocalStorage.refreshToken).doReturn("alma")
94 | mockWebServer.setUnauthorizedThenProperRefreshThenSuccessRequests()
95 | val service = builder.setGrantType("my_grant").build().oauthRetrofitContainer.sessionRetrofit.create()
96 |
97 | service.request().execute()
98 |
99 | val originalRequests = mockWebServer.takeRequest()
100 | val refreshRequest = mockWebServer.takeRequest()
101 |
102 | Assert.assertEquals(setOf("grant_type=my_grant", "refresh_token=alma"), refreshRequest.body.readString(Charsets.UTF_8).split("&").toSet())
103 | }
104 |
105 | @Test
106 | fun GIVEN_configured_refresh_token_field_name_WHEN_a_refresh_request_is_run_THEN_it_has_proper_default_setup() {
107 | whenever(mockAuthenticationLocalStorage.refreshToken).doReturn("alma")
108 | mockWebServer.setUnauthorizedThenProperRefreshThenSuccessRequests()
109 | val service = builder.setRefreshTokenFieldName("new_refresh_token_field_name").build().oauthRetrofitContainer.sessionRetrofit.create()
110 |
111 | service.request().execute()
112 |
113 | val originalRequests = mockWebServer.takeRequest()
114 | val refreshRequest = mockWebServer.takeRequest()
115 |
116 | Assert.assertEquals(setOf("grant_type=refresh_token", "new_refresh_token_field_name=alma"), refreshRequest.body.readString(Charsets.UTF_8).split("&").toSet())
117 | }
118 |
119 | @Test
120 | fun GIVEN_configured_service_path_WHEN_a_refresh_request_is_run_THEN_it_has_proper_default_setup() {
121 | whenever(mockAuthenticationLocalStorage.refreshToken).doReturn("alma")
122 | mockWebServer.setUnauthorizedThenProperRefreshThenSuccessRequests()
123 | val service = builder.setRefreshServicePath("banan").build().oauthRetrofitContainer.sessionRetrofit.create()
124 |
125 | service.request().execute()
126 |
127 | val originalRequests = mockWebServer.takeRequest()
128 | val refreshRequest = mockWebServer.takeRequest()
129 |
130 | Assert.assertEquals("${basePath}banan", refreshRequest.requestUrl?.encodedPath)
131 | }
132 |
133 | @Test
134 | fun GIVEN_configured_additional_fields_WHEN_a_refresh_request_is_run_THEN_it_has_proper_default_setup() {
135 | whenever(mockAuthenticationLocalStorage.refreshToken).doReturn("alma")
136 | mockWebServer.setUnauthorizedThenProperRefreshThenSuccessRequests()
137 | val service = builder.setRefreshServiceFieldParameterProvider(object : RefreshServiceFieldParameterProvider {
138 | override fun get(token: String): Map =
139 | mapOf("a" to token, "grant" to "token")
140 |
141 | }).build().oauthRetrofitContainer.sessionRetrofit.create()
142 |
143 | service.request().execute()
144 |
145 | val originalRequests = mockWebServer.takeRequest()
146 | val refreshRequest = mockWebServer.takeRequest()
147 |
148 | Assert.assertEquals(setOf("grant=token", "a=alma", "grant_type=refresh_token", "refresh_token=alma"), refreshRequest.body.readString(Charsets.UTF_8).split("&").toSet())
149 | }
150 |
151 | @Test
152 | fun GIVEN_custom_session_expiration_exception_decider_WHEN_a_refresh_request_is_run_THEN_the_exception_is_delegated_properly_and_the_decistion_is_respected() {
153 | whenever(mockAuthenticationLocalStorage.refreshToken).doReturn("alma")
154 | mockWebServer.enqueue(MockResponse().apply {
155 | setResponseCode(401)
156 | setBody("")
157 | })
158 | mockWebServer.enqueue(MockResponse().apply {
159 | setResponseCode(400)
160 | setBody(REFRESH_RESPONSE)
161 | })
162 | var caughtHttpException: HttpException? = null
163 | val service = builder.setIsSessionExpiredExceptionDecider(object : IsSessionExpiredException {
164 | override fun invoke(throwable: Throwable): Boolean {
165 | caughtHttpException = throwable as HttpException
166 | return true
167 | }
168 |
169 | }).build().oauthRetrofitContainer.sessionRetrofit.create()
170 |
171 | try {
172 | service.request().execute()
173 | } catch (httpException: HttpException) {
174 | Assert.assertEquals(401, httpException.code())
175 | }
176 |
177 | Assert.assertEquals(400, caughtHttpException?.code())
178 | verify(mockSessionExpiredEventHandler, times(1)).onSessionExpired()
179 | verifyNoMoreInteractions(mockSessionExpiredEventHandler)
180 | }
181 |
182 | @Test
183 | fun GIVEN_custom_deprecated_session_expiration_exception_decider_WHEN_a_refresh_request_is_run_THEN_the_exception_is_delegated_properly_and_the_decistion_is_respected() {
184 | whenever(mockAuthenticationLocalStorage.refreshToken).doReturn("alma")
185 | mockWebServer.enqueue(MockResponse().apply {
186 | setResponseCode(401)
187 | setBody("")
188 | })
189 | mockWebServer.enqueue(MockResponse().apply {
190 | setResponseCode(400)
191 | setBody(REFRESH_RESPONSE)
192 | })
193 | var caughtHttpException: HttpException? = null
194 | val service = builder.setIsSessionExpiredExceptionDecider(object : DeprecatedIsSessionExpiredException {
195 | override fun invoke(httpException: HttpException): Boolean {
196 | caughtHttpException = httpException
197 | return true
198 | }
199 |
200 | }).build().oauthRetrofitContainer.sessionRetrofit.create()
201 |
202 | try {
203 | service.request().execute()
204 | } catch (httpException: HttpException) {
205 | Assert.assertEquals(401, httpException.code())
206 | }
207 |
208 | Assert.assertEquals(400, caughtHttpException?.code())
209 | verify(mockSessionExpiredEventHandler, times(1)).onSessionExpired()
210 | verifyNoMoreInteractions(mockSessionExpiredEventHandler)
211 | }
212 |
213 | @Test
214 | fun GIVEN_disabled_default_parsing_and_custom_implementation_WHEN_a_refresh_request_is_run_THEN_that_is_used() {
215 | whenever(mockAuthenticationLocalStorage.refreshToken).doReturn("alma")
216 | mockWebServer.enqueue(MockResponse().apply {
217 | setResponseCode(401)
218 | setBody("")
219 | })
220 | mockWebServer.enqueue(MockResponse().apply {
221 | setResponseCode(200)
222 | setBody("{}")
223 | })
224 | mockWebServer.enqueue(MockResponse().apply {
225 | setResponseCode(200)
226 | setBody("{}")
227 | })
228 | val service = builder.disableDefaultParsing()
229 | .configureMoshi {
230 | add(object : JsonAdapter() {
231 | @FromJson
232 | override fun fromJson(reader: JsonReader): SessionDataResponse? =
233 | object : SessionDataResponse {
234 | override val userId: String get() = "a"
235 | override val token: String get() = "b"
236 | override val refreshToken: String get() = "c"
237 | override val tokenType: String get() = "d"
238 | }.also {
239 | reader.beginObject()
240 | reader.endObject()
241 | }
242 |
243 | @ToJson
244 | override fun toJson(writer: JsonWriter, value: SessionDataResponse?): Unit = TODO()
245 |
246 | })
247 | }
248 | .build()
249 | .oauthRetrofitContainer.sessionRetrofit.create()
250 |
251 | service.request().execute()
252 |
253 | verify(mockAuthenticationLocalStorage, times(1)).refreshToken = "c"
254 | verify(mockAuthenticationLocalStorage, times(1)).accessToken = "b"
255 | verify(mockAuthenticationLocalStorage, times(1)).tokenType = "d"
256 | verify(mockAuthenticationLocalStorage, times(1)).userId = "a"
257 | }
258 |
259 |
260 | interface Service {
261 | @GET("service")
262 | fun request(): Call
263 | }
264 |
265 | companion object {
266 |
267 | private fun MockWebServer.setUnauthorizedThenProperRefreshThenSuccessRequests() {
268 | enqueue(MockResponse().apply {
269 | setResponseCode(401)
270 | setBody("")
271 | })
272 | enqueue(MockResponse().apply {
273 | setResponseCode(200)
274 | setBody(REFRESH_RESPONSE)
275 | })
276 | enqueue(MockResponse().apply {
277 | setResponseCode(200)
278 | setBody("{}")
279 | })
280 | }
281 |
282 | val REFRESH_RESPONSE = """
283 | {
284 | "access_token": "best-token",
285 | "token_type": "bearer",
286 | "refresh_token": "new-refresh-token",
287 | "expires_in": 299,
288 | "scope": "trust",
289 | "user_id": "user-id",
290 | "jti": "82015e11-da47-4fb9-aa0a-6b518f402ed7"
291 | }
292 | """.trimIndent()
293 | }
294 | }
--------------------------------------------------------------------------------
/oauthmoshikoin/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/oauthmoshikoin/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java-library'
2 | apply plugin: 'kotlin'
3 | apply plugin: 'com.jfrog.artifactory'
4 | apply plugin: 'maven-publish'
5 |
6 | dependencies {
7 | implementation fileTree(dir: 'libs', include: ['*.jar'])
8 |
9 | api project(':oauthmoshi')
10 | api "io.insert-koin:koin-core:$koinVersion"
11 | }
12 |
13 | compileKotlin {
14 | kotlinOptions {
15 | jvmTarget = project.jvmTarget
16 | }
17 | }
18 | compileTestKotlin {
19 | kotlinOptions {
20 | jvmTarget = project.jvmTarget
21 | }
22 | }
23 |
24 | project.ext.set("libraryArtifactId", "oauth-setup-moshi-koin")
25 | apply plugin: 'com.halcyonmobile.plugin.publish.custom.jar-library'
26 |
27 | sourceCompatibility = JavaVersion.VERSION_1_8
28 | targetCompatibility = JavaVersion.VERSION_1_8
--------------------------------------------------------------------------------
/oauthmoshikoin/src/main/java/com/halcyonmobile/oauthmoshikoin/createOauthModule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthmoshikoin
18 |
19 | import com.halcyonmobile.oauth.dependencies.AuthenticationLocalStorage
20 | import com.halcyonmobile.oauth.dependencies.IsSessionExpiredException
21 | import com.halcyonmobile.oauth.dependencies.SessionExpiredEventHandler
22 | import com.halcyonmobile.oauthmoshi.OauthRetrofitContainerWithMoshi
23 | import com.halcyonmobile.oauthmoshi.OauthRetrofitWithMoshiContainerBuilder
24 | import com.halcyonmobile.oauthparsing.RefreshServiceFieldParameterProvider
25 | import com.squareup.moshi.Moshi
26 | import okhttp3.OkHttpClient
27 | import org.koin.core.module.Module
28 | import org.koin.core.qualifier.Qualifier
29 | import org.koin.core.qualifier.StringQualifier
30 | import org.koin.core.scope.Scope
31 | import org.koin.dsl.module
32 | import retrofit2.Retrofit
33 |
34 | /**
35 | * All in one method to create a module for the retrofit and it's necessities created with oauth.
36 | *
37 | * @param clientId The clientId used by non-session requests, see [OauthRetrofitWithMoshiContainerBuilder]
38 | * @param provideAuthenticationLocalStorage lambda which is able to create an [AuthenticationLocalStorage] from a koin scope. Note: The created instance will be provided from the returned module. see [OauthRetrofitWithMoshiContainerBuilder]
39 | * @param provideSessionExpiredEventHandler lambda which is able to create a [SessionExpiredEventHandler] from a koin scope. Note: The created instance will be provided from the returned module. see [OauthRetrofitWithMoshiContainerBuilder]
40 | * @param configureRetrofit a lambda to configure both retrofit instances. here you minimum should set your base url. see [OauthRetrofitWithMoshiContainerBuilder.configureRetrofit]
41 | * @param disableDefaultParsing disables the default parsing, so the user will be responsible for it, see [OauthRetrofitWithMoshiContainerBuilder.disableDefaultParsing]
42 | * @param refreshServicePath sets the path for the token-refresh request, see [OauthRetrofitWithMoshiContainerBuilder.setRefreshServicePath]
43 | * @param refreshTokenFieldName sets the name of the refresh token parameter, see [OauthRetrofitWithMoshiContainerBuilder.setRefreshTokenFieldName]
44 | * @param grantType sets the grantType, empty string means the default is used, see [OauthRetrofitWithMoshiContainerBuilder.setGrantType]
45 | * @param refreshServiceFieldParameterProvider lambda which is able to create a [RefreshServiceFieldParameterProvider] from koin scope. Note: The created instance will NOT be provided from the returned module. see [OauthRetrofitWithMoshiContainerBuilder.setRefreshServiceFieldParameterProvider]
46 | * @param configureBothOkHttpClient lambda to configure both [OkHttpClient], see [OauthRetrofitWithMoshiContainerBuilder.configureBothOkHttpClient]
47 | * @param configureSessionOkHttpClient lambda to configure [OkHttpClient] with session, see [OauthRetrofitWithMoshiContainerBuilder.configureSessionOkHttpClient]
48 | * @param configureSessionlessOkHttpClient lambda to configure [OkHttpClient] without session, see [OauthRetrofitWithMoshiContainerBuilder.configureSessionlessOkHttpClient]
49 | * @param configureMoshi lambda to configure [Moshi]. see [OauthRetrofitWithMoshiContainerBuilder.configureMoshi]
50 | * @param provideIsSessionExpiredException lambda which is able to create a [IsSessionExpiredException] from koin scope. Note: The created instance will NOT be provided from the returned module. see [OauthRetrofitWithMoshiContainerBuilder.setIsSessionExpiredExceptionDecider]
51 | *
52 | * @return a koin module which provides a [AuthenticationLocalStorage], [SessionExpiredEventHandler], [Moshi], and two [Retrofit] instances as singletons.
53 | */
54 | inline fun createOauthModule(
55 | clientId: String,
56 | crossinline provideAuthenticationLocalStorage: Scope.() -> AuthenticationLocalStorage,
57 | crossinline provideSessionExpiredEventHandler: Scope.() -> SessionExpiredEventHandler,
58 | crossinline configureRetrofit: Scope.(Retrofit.Builder) -> Retrofit.Builder,
59 | disableDefaultParsing: Boolean = false,
60 | refreshServicePath: String? = null,
61 | refreshTokenFieldName: String? = null,
62 | grantType: String? = "",
63 | noinline refreshServiceFieldParameterProvider: (Scope.() -> RefreshServiceFieldParameterProvider)? = null,
64 | crossinline configureBothOkHttpClient: Scope.(OkHttpClient.Builder) -> OkHttpClient.Builder = { it },
65 | crossinline configureSessionOkHttpClient: Scope.(OkHttpClient.Builder) -> OkHttpClient.Builder = { it },
66 | crossinline configureSessionlessOkHttpClient: Scope.(OkHttpClient.Builder) -> OkHttpClient.Builder = { it },
67 | crossinline configureMoshi: Scope.(Moshi.Builder) -> Moshi.Builder = { it },
68 | noinline provideIsSessionExpiredException: (Scope.() -> IsSessionExpiredException)? = null
69 | ): Module =
70 | createOauthModule(
71 | provideClientId = { clientId},
72 | provideAuthenticationLocalStorage = provideAuthenticationLocalStorage,
73 | provideIsSessionExpiredException = provideIsSessionExpiredException,
74 | configureRetrofit = configureRetrofit,
75 | disableDefaultParsing = disableDefaultParsing,
76 | refreshServicePath = refreshServicePath,
77 | refreshTokenFieldName = refreshTokenFieldName,
78 | grantType = grantType,
79 | refreshServiceFieldParameterProvider = refreshServiceFieldParameterProvider,
80 | configureBothOkHttpClient = configureBothOkHttpClient,
81 | configureSessionOkHttpClient = configureSessionOkHttpClient,
82 | configureSessionlessOkHttpClient = configureSessionlessOkHttpClient,
83 | configureMoshi = configureMoshi,
84 | provideSessionExpiredEventHandler = provideSessionExpiredEventHandler
85 | )
86 |
87 | /**
88 | * All in one method to create a module for the retrofit and it's necessities created with oauth.
89 | *
90 | * @param provideClientId Providing the clientId used by non-session requests, see [OauthRetrofitWithMoshiContainerBuilder]. Note: the clientId will be requested only once.
91 | * @param provideAuthenticationLocalStorage lambda which is able to create an [AuthenticationLocalStorage] from a koin scope. Note: The created instance will be provided from the returned module. see [OauthRetrofitWithMoshiContainerBuilder]
92 | * @param provideSessionExpiredEventHandler lambda which is able to create a [SessionExpiredEventHandler] from a koin scope. Note: The created instance will be provided from the returned module. see [OauthRetrofitWithMoshiContainerBuilder]
93 | * @param configureRetrofit a lambda to configure both retrofit instances. here you minimum should set your base url. see [OauthRetrofitWithMoshiContainerBuilder.configureRetrofit]
94 | * @param disableDefaultParsing disables the default parsing, so the user will be responsible for it, see [OauthRetrofitWithMoshiContainerBuilder.disableDefaultParsing]
95 | * @param refreshServicePath sets the path for the token-refresh request, see [OauthRetrofitWithMoshiContainerBuilder.setRefreshServicePath]
96 | * @param refreshTokenFieldName sets the name of the refresh token parameter, see [OauthRetrofitWithMoshiContainerBuilder.setRefreshTokenFieldName]
97 | * @param grantType sets the grantType, empty string means the default is used, see [OauthRetrofitWithMoshiContainerBuilder.setGrantType]
98 | * @param refreshServiceFieldParameterProvider lambda which is able to create a [RefreshServiceFieldParameterProvider] from koin scope. Note: The created instance will NOT be provided from the returned module. see [OauthRetrofitWithMoshiContainerBuilder.setRefreshServiceFieldParameterProvider]
99 | * @param configureBothOkHttpClient lambda to configure both [OkHttpClient], see [OauthRetrofitWithMoshiContainerBuilder.configureBothOkHttpClient]
100 | * @param configureSessionOkHttpClient lambda to configure [OkHttpClient] with session, see [OauthRetrofitWithMoshiContainerBuilder.configureSessionOkHttpClient]
101 | * @param configureSessionlessOkHttpClient lambda to configure [OkHttpClient] without session, see [OauthRetrofitWithMoshiContainerBuilder.configureSessionlessOkHttpClient]
102 | * @param configureMoshi lambda to configure [Moshi]. see [OauthRetrofitWithMoshiContainerBuilder.configureMoshi]
103 | * @param provideIsSessionExpiredException lambda which is able to create a [IsSessionExpiredException] from koin scope. Note: The created instance will NOT be provided from the returned module. see [OauthRetrofitWithMoshiContainerBuilder.setIsSessionExpiredExceptionDecider]
104 | *
105 | * @return a koin module which provides a [AuthenticationLocalStorage], [SessionExpiredEventHandler], [Moshi], and two [Retrofit] instances as singletons.
106 | */
107 | inline fun createOauthModule(
108 | crossinline provideClientId: Scope.() -> String,
109 | crossinline provideAuthenticationLocalStorage: Scope.() -> AuthenticationLocalStorage,
110 | crossinline provideSessionExpiredEventHandler: Scope.() -> SessionExpiredEventHandler,
111 | crossinline configureRetrofit: Scope.(Retrofit.Builder) -> Retrofit.Builder,
112 | disableDefaultParsing: Boolean = false,
113 | refreshServicePath: String? = null,
114 | refreshTokenFieldName: String? = null,
115 | grantType: String? = "",
116 | noinline refreshServiceFieldParameterProvider: (Scope.() -> RefreshServiceFieldParameterProvider)? = null,
117 | crossinline configureBothOkHttpClient: Scope.(OkHttpClient.Builder) -> OkHttpClient.Builder = { it },
118 | crossinline configureSessionOkHttpClient: Scope.(OkHttpClient.Builder) -> OkHttpClient.Builder = { it },
119 | crossinline configureSessionlessOkHttpClient: Scope.(OkHttpClient.Builder) -> OkHttpClient.Builder = { it },
120 | crossinline configureMoshi: Scope.(Moshi.Builder) -> Moshi.Builder = { it },
121 | noinline provideIsSessionExpiredException: (Scope.() -> IsSessionExpiredException)? = null
122 | ): Module = module {
123 | single { provideAuthenticationLocalStorage() }
124 | single { provideSessionExpiredEventHandler() }
125 |
126 | single {
127 | OauthRetrofitWithMoshiContainerBuilder(
128 | clientId = provideClientId(),
129 | authenticationLocalStorage = get(),
130 | sessionExpiredEventHandler = get()
131 | )
132 | .let { if (refreshServicePath == null) it else it.setRefreshServicePath(refreshServicePath) }
133 | .let { if (refreshTokenFieldName == null) it else it.setRefreshTokenFieldName(refreshTokenFieldName) }
134 | .let { if (disableDefaultParsing) it.disableDefaultParsing() else it }
135 | .let { if (grantType != "") it.setGrantType(grantType) else it }
136 | .let {
137 | provideIsSessionExpiredException?.invoke(this)?.let { isSessionExpiredException ->
138 | it.setIsSessionExpiredExceptionDecider(isSessionExpiredException)
139 | } ?: it
140 | }
141 | .setRefreshServiceFieldParameterProvider(refreshServiceFieldParameterProvider?.invoke(this))
142 | .configureRetrofit {
143 | configureRetrofit(this)
144 | }
145 | .configureBothOkHttpClient {
146 | configureBothOkHttpClient(this)
147 | }
148 | .configureSessionOkHttpClient {
149 | configureSessionOkHttpClient(this)
150 | }
151 | .configureSessionlessOkHttpClient {
152 | configureSessionlessOkHttpClient(this)
153 | }
154 | .configureMoshi {
155 | configureMoshi(this)
156 | }
157 | .build()
158 | }
159 |
160 | single { get().moshi }
161 | single(SESSION_RETROFIT) { get().oauthRetrofitContainer.sessionRetrofit }
162 | single(NON_SESSION_RETROFIT) { get().oauthRetrofitContainer.sessionlessRetrofit }
163 | }
164 |
165 | val NON_SESSION_RETROFIT: Qualifier = StringQualifier("retrofit_for_request_without_session")
166 | val SESSION_RETROFIT: Qualifier = StringQualifier("retrofit_for_request_with_session")
--------------------------------------------------------------------------------
/oauthparsing/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/oauthparsing/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java-library'
2 | apply plugin: 'kotlin'
3 | apply plugin: 'kotlin-kapt'
4 | apply plugin: 'com.jfrog.artifactory'
5 | apply plugin: 'maven-publish'
6 |
7 | dependencies {
8 | implementation fileTree(dir: 'libs', include: ['*.jar'])
9 |
10 | api project(':oauth')
11 | }
12 | compileKotlin {
13 | kotlinOptions {
14 | jvmTarget = project.jvmTarget
15 | }
16 | }
17 | compileTestKotlin {
18 | kotlinOptions {
19 | jvmTarget = project.jvmTarget
20 | }
21 | }
22 |
23 | project.ext.set("libraryArtifactId", "oauth-setup-parsing")
24 | apply plugin: 'com.halcyonmobile.plugin.publish.custom.jar-library'
25 |
26 | sourceCompatibility = JavaVersion.VERSION_1_8
27 | targetCompatibility = JavaVersion.VERSION_1_8
--------------------------------------------------------------------------------
/oauthparsing/src/main/java/com/halcyonmobile/oauthparsing/AuthenticationServiceAdapterImpl.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthparsing
18 |
19 | import com.halcyonmobile.oauth.AuthenticationService
20 | import com.halcyonmobile.oauth.AuthenticationServiceAdapter
21 | import com.halcyonmobile.oauth.SessionDataResponse
22 | import retrofit2.Call
23 |
24 | /**
25 | * [AuthenticationServiceAdapter] implementation which uses [RefreshTokenService]
26 | */
27 | class AuthenticationServiceAdapterImpl : AuthenticationServiceAdapter {
28 |
29 | var refreshServiceFieldParameterProvider: RefreshServiceFieldParameterProvider? = null
30 | var refreshServicePath: String = DEFAULT_REFRESH_SERVICE_PATH
31 | var refreshTokenFieldName: String = REFRESH_TOKEN_DEFAULT_FIELD_NAME
32 | var grantType: String? = DEFAULT_GRANT_TYPE
33 |
34 | override fun adapt(service: RefreshTokenService): AuthenticationService =
35 | AuthenticationServiceImpl(
36 | refreshServiceFieldParameterProvider = refreshServiceFieldParameterProvider,
37 | refreshTokenQueryName = refreshTokenFieldName,
38 | refreshServicePath = refreshServicePath,
39 | grantType = grantType,
40 | service = service
41 | )
42 |
43 | class AuthenticationServiceImpl(
44 | private val refreshServiceFieldParameterProvider: RefreshServiceFieldParameterProvider?,
45 | private val refreshTokenQueryName: String,
46 | private val refreshServicePath: String,
47 | private val grantType: String?,
48 | private val service: RefreshTokenService
49 | ) : AuthenticationService {
50 |
51 | override fun refreshToken(refreshToken: String): Call {
52 | val fieldMap = refreshServiceFieldParameterProvider?.get(refreshToken)?.toMutableMap() ?: mutableMapOf()
53 | fieldMap[refreshTokenQueryName] = refreshToken
54 | grantType?.let { fieldMap[GRANT_TYPE] = it }
55 | return service.refresh(refreshServicePath, fieldMap)
56 | }
57 | }
58 |
59 | companion object {
60 | private const val GRANT_TYPE = "grant_type"
61 | private const val REFRESH_TOKEN_DEFAULT_FIELD_NAME = "refresh_token"
62 | private const val DEFAULT_GRANT_TYPE = "refresh_token"
63 | private const val DEFAULT_REFRESH_SERVICE_PATH = "oauth/token"
64 | }
65 | }
--------------------------------------------------------------------------------
/oauthparsing/src/main/java/com/halcyonmobile/oauthparsing/OauthRetrofitContainerWithParser.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthparsing
18 |
19 | import com.halcyonmobile.oauth.OauthRetrofitContainer
20 |
21 | /**
22 | * Interface defining OauthRetrofitContainers which can also do parsing for [RefreshTokenService]'s response.
23 | */
24 | interface OauthRetrofitContainerWithParser {
25 | val oauthRetrofitContainer: OauthRetrofitContainer
26 | val parser: Parser
27 | }
--------------------------------------------------------------------------------
/oauthparsing/src/main/java/com/halcyonmobile/oauthparsing/OauthRetrofitWithParserContainerBuilder.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthparsing
18 |
19 | import com.halcyonmobile.oauth.OauthRetrofitContainer
20 | import com.halcyonmobile.oauth.SessionDataResponse
21 | import com.halcyonmobile.oauth.dependencies.IsSessionExpiredException
22 | import com.halcyonmobile.oauth.internal.DefaultIsSessionExpiredException
23 | import okhttp3.OkHttpClient
24 | import retrofit2.Retrofit
25 | import com.halcyonmobile.oauth.IsSessionExpiredException as DeprecatedIsSessionExpiredException
26 |
27 | /**
28 | * Builder interface for [OauthRetrofitContainer] which uses a parser as it's [retrofit2.Converter.Factory].
29 | */
30 | interface OauthRetrofitWithParserContainerBuilder {
31 |
32 | /**
33 | * Sets the path of the refresh service, the default is "oauth/token", see [AuthenticationServiceAdapterImpl]
34 | */
35 | fun setRefreshServicePath(refreshServicePath: String): T
36 |
37 | /**
38 | * Sets the field name of the refresh token, the default is "refresh_token", see [AuthenticationServiceAdapterImpl]
39 | */
40 | fun setRefreshTokenFieldName(refreshTokenFieldName: String): T
41 |
42 | /**
43 | * Sets the grant type of the service, the default is "refresh_token", see [AuthenticationServiceAdapterImpl].
44 | * Setting it null will disable adding the grant_type field, so it won't be added similar as grant_type=refresh_token
45 | */
46 | fun setGrantType(grantType: String?): T
47 |
48 | /**
49 | * Sets a [RefreshServiceFieldParameterProvider] which can provide additional field params to the [RefreshTokenService].
50 | */
51 | fun setRefreshServiceFieldParameterProvider(refreshServiceFieldParameterProvider: RefreshServiceFieldParameterProvider?): T
52 |
53 | /**
54 | * Additional configuration for the [OkHttpClient] used by the sessionless-retrofit instance
55 | */
56 | fun configureSessionlessOkHttpClient(configure: OkHttpClient.Builder.() -> OkHttpClient.Builder): T
57 |
58 | /**
59 | * Additionally configuration for the [OkHttpClient] used by the session-retrofit instance
60 | */
61 | fun configureSessionOkHttpClient(configure: OkHttpClient.Builder.() -> OkHttpClient.Builder): T
62 |
63 | /**
64 | * Additional configuration for the [OkHttpClient] used by all retrofit instances provided.
65 | * Example usage: Logging.
66 | */
67 | fun configureBothOkHttpClient(configure: OkHttpClient.Builder.() -> OkHttpClient.Builder): T
68 |
69 | /**
70 | * Configuration for the [Retrofit] instance, here you should define your baseUrl, parsing etc.
71 | */
72 | fun configureRetrofit(configure: Retrofit.Builder.() -> Retrofit.Builder): T
73 |
74 | /**
75 | * Sets a class which decided what should be considered sessionExpiration.
76 | * By default a response containing "Invalid refresh token" or "Invalid refresh token (expired):" is considered, see [DefaultIsSessionExpiredException]
77 | */
78 | fun setIsSessionExpiredExceptionDecider(isSessionExpiredException: IsSessionExpiredException) : T
79 |
80 | /**
81 | * Sets a class which decided what should be considered sessionExpiration.
82 | * By default a response containing "Invalid refresh token" or "Invalid refresh token (expired):" is considered, see [DefaultIsSessionExpiredException]
83 | */
84 | fun setIsSessionExpiredExceptionDecider(isSessionExpiredException: DeprecatedIsSessionExpiredException) : T
85 |
86 | /**
87 | * Disables the default parsing.
88 | * In this case the user is responsible to setup a JsonAdapter which parses [SessionDataResponse] and add it to the parser
89 | */
90 | fun disableDefaultParsing(): T
91 |
92 | /**
93 | * Builds the container which holds the [Retrofit] instances and parser.
94 | */
95 | fun build(): OauthRetrofitContainerWithParser
96 |
97 | }
--------------------------------------------------------------------------------
/oauthparsing/src/main/java/com/halcyonmobile/oauthparsing/RefreshServiceFieldParameterProvider.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthparsing
18 |
19 | /**
20 | * This interface enables to add any additional field parameter to the [RefreshTokenService] call.
21 | */
22 | interface RefreshServiceFieldParameterProvider {
23 |
24 | fun get(token: String): Map
25 | }
--------------------------------------------------------------------------------
/oauthparsing/src/main/java/com/halcyonmobile/oauthparsing/RefreshTokenService.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthparsing
18 |
19 | import com.halcyonmobile.oauth.SessionDataResponse
20 | import com.halcyonmobile.oauth.dependencies.RefreshService
21 | import retrofit2.Call
22 | import retrofit2.http.FieldMap
23 | import retrofit2.http.FormUrlEncoded
24 | import retrofit2.http.POST
25 | import retrofit2.http.Url
26 |
27 | /**
28 | * You have to define a service which will do the token refreshing.
29 | *
30 | * In order to use [RefreshService] annotation to generate the [com.halcyonmobile.oauth.RefreshTokenServiceAuthenticationServiceAdapter]
31 | * you must have only one function, which returns a [Call] with a typeParameter subtype of [com.halcyonmobile.oauth.SessionDataResponse]
32 | * Your first parameter must be the refresh token, every other parameter has to have default value.
33 | * If that's not possible consider creating your own specific implementation for the [com.halcyonmobile.oauth.AuthenticationServiceAdapter].
34 | */
35 | interface RefreshTokenService {
36 |
37 | @POST
38 | @FormUrlEncoded
39 | fun refresh(@Url path: String, @FieldMap fields: Map): Call
40 | }
--------------------------------------------------------------------------------
/oauthsecurestorage/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/oauthsecurestorage/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'com.jfrog.artifactory'
4 | apply plugin: 'maven-publish'
5 |
6 | android {
7 | compileSdkVersion project.compileSdkVersion
8 |
9 |
10 | defaultConfig {
11 | minSdkVersion 23
12 | targetSdkVersion project.compileSdkVersion
13 | versionCode 1
14 | versionName "1.0"
15 |
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 |
18 | }
19 |
20 | buildTypes {
21 | release {
22 | minifyEnabled false
23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
24 | }
25 | }
26 |
27 | }
28 | dependencies {
29 | implementation fileTree(dir: 'libs', include: ['*.jar'])
30 |
31 | implementation "androidx.annotation:annotation:$annotationVersion"
32 | api project(':oauthdependencies')
33 | api project(':oauthstorage')
34 | testImplementation "junit:junit:$junitVersion"
35 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
36 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
37 | implementation "androidx.security:security-crypto:$securityVersion"
38 | }
39 | project.ext.set("libraryArtifactId", "oauth-setup-secure-storage")
40 | apply plugin: 'com.halcyonmobile.plugin.publish.custom.aar-library'
41 |
42 | sourceCompatibility = JavaVersion.VERSION_1_8
43 | targetCompatibility = JavaVersion.VERSION_1_8
--------------------------------------------------------------------------------
/oauthsecurestorage/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halcyonmobile/retrofit-oauth2-helper/cc59afbeb284e41188fa65b6bdecd605d2526596/oauthsecurestorage/consumer-rules.pro
--------------------------------------------------------------------------------
/oauthsecurestorage/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 |
--------------------------------------------------------------------------------
/oauthsecurestorage/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/oauthsecurestorage/src/main/java/com/halcyonmobile/oauthsecurestorage/AuthenticationSecureSharedPreferencesStorage.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthsecurestorage
18 |
19 | import android.content.Context
20 | import androidx.security.crypto.EncryptedSharedPreferences
21 | import androidx.security.crypto.MasterKeys
22 | import com.halcyonmobile.oauth.dependencies.AuthenticationLocalStorage
23 | import com.halcyonmobile.oauthstorage.AuthenticationSharedPreferencesStorage
24 |
25 |
26 | /**
27 | * [EncryptedSharedPreferences] based implementation of [AuthenticationLocalStorage].
28 | *
29 | * Can be used as a separate SharedPreferencesManager just for session, or added to your custom one.
30 | * Because all functions are defined in [AuthenticationLocalStorage], you may use this as delegate for your custom
31 | * SharedPreferencesManager.
32 | */
33 | open class AuthenticationSecureSharedPreferencesStorage(sharedPreferences: EncryptedSharedPreferences) : AuthenticationSharedPreferencesStorage(sharedPreferences) {
34 |
35 | constructor(context: Context, encryptedFileName: String = ENCRYPTED_FILE_NAME) : this(
36 | createEncryptedSharedPreferences(context, encryptedFileName)
37 | )
38 |
39 | companion object {
40 | const val ENCRYPTED_FILE_NAME = "encrypted_preferences"
41 |
42 | fun create(context: Context, encryptedFileName: String = ENCRYPTED_FILE_NAME) : AuthenticationSecureSharedPreferencesStorage =
43 | AuthenticationSecureSharedPreferencesStorage(context, encryptedFileName)
44 |
45 | fun createEncryptedSharedPreferences(context: Context, encryptedFileName: String = ENCRYPTED_FILE_NAME): EncryptedSharedPreferences {
46 | val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
47 |
48 | return EncryptedSharedPreferences.create(
49 | encryptedFileName,
50 | masterKeyAlias,
51 | context,
52 | EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
53 | EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
54 | ) as EncryptedSharedPreferences
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/oauthsecurestorage/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | oauthsecurestorage
3 |
4 |
--------------------------------------------------------------------------------
/oauthsecurestoragecompat/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/oauthsecurestoragecompat/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'com.jfrog.artifactory'
4 | apply plugin: 'maven-publish'
5 |
6 | android {
7 | compileSdkVersion project.compileSdkVersion
8 |
9 |
10 | defaultConfig {
11 | minSdkVersion project.minSdkVersion
12 | targetSdkVersion project.compileSdkVersion
13 | versionCode 1
14 | versionName "1.0"
15 |
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 |
18 | }
19 | testOptions {
20 | unitTests {
21 | includeAndroidResources = true
22 | }
23 | }
24 |
25 | buildTypes {
26 | release {
27 | minifyEnabled false
28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
29 | }
30 | }
31 |
32 | }
33 | dependencies {
34 | implementation fileTree(dir: 'libs', include: ['*.jar'])
35 |
36 | implementation "androidx.annotation:annotation:$annotationVersion"
37 | api project(':oauthdependencies')
38 | api project(':oauthstorage')
39 | api project(':oauthsecurestorage')
40 |
41 | implementation "androidx.security:security-crypto:$securityVersion"
42 |
43 | testImplementation "junit:junit:$junitVersion"
44 |
45 |
46 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
47 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
48 | }
49 | project.ext.set("libraryArtifactId", "oauth-setup-secure-storage-compat")
50 | apply plugin: 'com.halcyonmobile.plugin.publish.custom.aar-library'
51 |
52 | sourceCompatibility = JavaVersion.VERSION_1_8
53 | targetCompatibility = JavaVersion.VERSION_1_8
--------------------------------------------------------------------------------
/oauthsecurestoragecompat/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halcyonmobile/retrofit-oauth2-helper/cc59afbeb284e41188fa65b6bdecd605d2526596/oauthsecurestoragecompat/consumer-rules.pro
--------------------------------------------------------------------------------
/oauthsecurestoragecompat/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 |
--------------------------------------------------------------------------------
/oauthsecurestoragecompat/src/androidTest/java/org/fnives/android/oauthsecurestoragecompat/MigrationTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package org.fnives.android.oauthsecurestoragecompat
18 |
19 | import androidx.security.crypto.EncryptedSharedPreferences
20 | import androidx.test.ext.junit.runners.AndroidJUnit4
21 | import androidx.test.platform.app.InstrumentationRegistry
22 | import com.halcyonmobile.oauthsecurestorage.AuthenticationSecureSharedPreferencesStorage
23 | import com.halcyonmobile.oauthsecurestoragecompat.AuthenticationSecureSharedPreferencesStorageCompat
24 | import com.halcyonmobile.oauthstorage.AuthenticationSharedPreferencesStorage
25 | import org.junit.Assert
26 | import org.junit.Test
27 | import org.junit.runner.RunWith
28 |
29 | @RunWith(AndroidJUnit4::class)
30 | class MigrationTest {
31 |
32 | @Test
33 | fun useAppContext() {
34 | // Context of the app under test.
35 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
36 | val expectedValue = mutableMapOf(
37 | "INT" to 1,
38 | "BOOLEAN" to true,
39 | "FLOAT" to 123.4f,
40 | "LONG" to 123L,
41 | "STRING" to "example",
42 | "STRING_SET" to setOf("A", "B")
43 | )
44 |
45 | AuthenticationSecureSharedPreferencesStorage(appContext).sharedPreferences.edit().clear().apply()
46 | val sharedPreferences = AuthenticationSharedPreferencesStorage.create(appContext)
47 | sharedPreferences.sharedPreferences.edit()
48 | .putInt("INT", 1)
49 | .putBoolean("BOOLEAN", true)
50 | .putFloat("FLOAT", 123.4f)
51 | .putLong("LONG", 123L)
52 | .putString("STRING", "example")
53 | .putStringSet("STRING_SET", setOf("A", "B"))
54 | .apply()
55 |
56 | val createAuthStorage = AuthenticationSecureSharedPreferencesStorageCompat(appContext)
57 |
58 | Assert.assertEquals(true, createAuthStorage.sharedPreferences is EncryptedSharedPreferences)
59 | Assert.assertEquals(expectedValue, createAuthStorage.sharedPreferences.all)
60 | Assert.assertEquals(emptyMap(), sharedPreferences.sharedPreferences.all)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/oauthsecurestoragecompat/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/oauthsecurestoragecompat/src/main/java/com/halcyonmobile/oauthsecurestoragecompat/AuthenticationSecureSharedPreferencesStorageCompat.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthsecurestoragecompat
18 |
19 | import android.annotation.SuppressLint
20 | import android.content.Context
21 | import android.content.SharedPreferences
22 | import android.os.Build
23 | import com.halcyonmobile.oauth.dependencies.AuthenticationLocalStorage
24 | import com.halcyonmobile.oauthsecurestorage.AuthenticationSecureSharedPreferencesStorage
25 | import com.halcyonmobile.oauthstorage.AuthenticationSharedPreferencesStorage
26 |
27 | /**
28 | * Implementation of [AuthenticationLocalStorage] if possible uses [AuthenticationSecureSharedPreferencesStorage] (api version above 23) or fallbacks to
29 | * [AuthenticationSharedPreferencesStorage] otherwise.
30 | *
31 | * Note: to make sure if the user upgrades their api lvl a migration has been introduced.
32 | * This can also be used if you were using [AuthenticationSharedPreferencesStorage] previously and want to update to [AuthenticationSecureSharedPreferencesStorage].
33 | */
34 | open class AuthenticationSecureSharedPreferencesStorageCompat(private val authenticationLocalStorage: AuthenticationSharedPreferencesStorage) :
35 | AuthenticationLocalStorage by authenticationLocalStorage {
36 |
37 | val sharedPreferences get() = authenticationLocalStorage.sharedPreferences
38 |
39 | constructor(
40 | context: Context,
41 | encryptedFileName: String = AuthenticationSecureSharedPreferencesStorage.ENCRYPTED_FILE_NAME,
42 | preferenceKey: String = AuthenticationSharedPreferencesStorage.PREFERENCES_KEY
43 | ) : this(createAndMigrate(context, encryptedFileName, preferenceKey))
44 |
45 | companion object {
46 |
47 | fun createAndMigrate(
48 | context: Context,
49 | encryptedFileName: String,
50 | preferenceKey: String
51 | ): AuthenticationSharedPreferencesStorage {
52 | val nonEncryptedSharedPreferences = AuthenticationSharedPreferencesStorage.create(context, preferenceKey)
53 |
54 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
55 | val authenticationSecureSharedPreferencesStorage = AuthenticationSecureSharedPreferencesStorage.create(context, encryptedFileName)
56 | migrateIfNeeded(nonEncryptedSharedPreferences, authenticationSecureSharedPreferencesStorage)
57 |
58 | authenticationSecureSharedPreferencesStorage
59 | } else {
60 | nonEncryptedSharedPreferences
61 | }
62 | }
63 |
64 | private fun migrateIfNeeded(
65 | nonEncryptedAuthenticationLocalStorage: AuthenticationSharedPreferencesStorage,
66 | encryptedAuthenticationLocalStorage: AuthenticationSecureSharedPreferencesStorage
67 | ) {
68 | if (nonEncryptedAuthenticationLocalStorage.sharedPreferences.all.isNotEmpty()) {
69 | nonEncryptedAuthenticationLocalStorage.sharedPreferences.copyTo(encryptedAuthenticationLocalStorage.sharedPreferences)
70 |
71 | nonEncryptedAuthenticationLocalStorage.sharedPreferences.edit().clear().apply()
72 | }
73 | }
74 |
75 | @SuppressLint("CommitPrefEdits")
76 | fun SharedPreferences.copyTo(sharedPreferences: SharedPreferences) {
77 | all.asSequence().fold(sharedPreferences.edit()) { editor, (key, value) ->
78 | when(value){
79 | is Long -> editor.putLong(key, value)
80 | is Boolean -> editor.putBoolean(key, value)
81 | is Float -> editor.putFloat(key, value)
82 | is Int -> editor.putInt(key, value)
83 | is String -> editor.putString(key, value)
84 | is Set<*> -> editor.putStringSet(key, value.filterIsInstance().toSet())
85 | else -> editor
86 | }
87 | }
88 | .apply()
89 | }
90 | }
91 | }
--------------------------------------------------------------------------------
/oauthsecurestoragecompat/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | oauthsecurestoragecompat
3 |
4 |
--------------------------------------------------------------------------------
/oauthstorage/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/oauthstorage/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'com.jfrog.artifactory'
4 | apply plugin: 'maven-publish'
5 |
6 | android {
7 | compileSdkVersion project.compileSdkVersion
8 |
9 |
10 | defaultConfig {
11 | minSdkVersion project.minSdkVersion
12 | targetSdkVersion project.compileSdkVersion
13 | versionCode 1
14 | versionName "1.0"
15 |
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 |
18 | }
19 |
20 | buildTypes {
21 | release {
22 | minifyEnabled false
23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
24 | }
25 | }
26 |
27 | }
28 | dependencies {
29 | implementation fileTree(dir: 'libs', include: ['*.jar'])
30 |
31 | implementation "androidx.annotation:annotation:$annotationVersion"
32 | api project(':oauthdependencies')
33 | testImplementation "junit:junit:$junitVersion"
34 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
35 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
36 | }
37 | project.ext.set("libraryArtifactId", "oauth-setup-storage")
38 | apply plugin: 'com.halcyonmobile.plugin.publish.custom.aar-library'
39 |
40 | sourceCompatibility = JavaVersion.VERSION_1_8
41 | targetCompatibility = JavaVersion.VERSION_1_8
--------------------------------------------------------------------------------
/oauthstorage/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 |
--------------------------------------------------------------------------------
/oauthstorage/src/androidTest/java/com/halcyonmobile/oauthstorage/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthstorage;
18 |
19 | import androidx.test.ext.junit.runners.AndroidJUnit4;
20 |
21 | import org.junit.Test;
22 | import org.junit.runner.RunWith;
23 |
24 | /**
25 | * Instrumented test, which will execute on an Android device.
26 | *
27 | * @see Testing documentation
28 | */
29 | @RunWith(AndroidJUnit4.class)
30 | public class ExampleInstrumentedTest {
31 | @Test
32 | public void useAppContext() {
33 | // Context of the app under test.
34 | //Context appContext = InstrumentationRegistry.getTargetContext();
35 | //assertEquals("com.halcyonmobile.oauthstorage.test", appContext.getPackageName());
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/oauthstorage/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/oauthstorage/src/main/java/com/halcyonmobile/oauthstorage/AuthenticationSharedPreferencesStorage.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Halcyon Mobile.
3 | * https://www.halcyonmobile.com
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package com.halcyonmobile.oauthstorage
18 |
19 | import android.content.Context
20 | import android.content.SharedPreferences
21 | import androidx.annotation.CallSuper
22 | import com.halcyonmobile.oauth.dependencies.AuthenticationLocalStorage
23 |
24 | /**
25 | * [SharedPreferences] based implementation of [AuthenticationLocalStorage].
26 | *
27 | * Can be used as a separate SharedPreferencesManager just for session, or added to your custom one the custom one.
28 | * Because all functions are defined in [AuthenticationLocalStorage], you may use this as delegate for your custom
29 | * SharedPreferencesManager.
30 | */
31 | open class AuthenticationSharedPreferencesStorage(val sharedPreferences: SharedPreferences) : AuthenticationLocalStorage {
32 |
33 | constructor(context: Context, key: String = PREFERENCES_KEY): this(createSharedPreferences(context, key))
34 |
35 | final override var userId: String
36 | get() = sharedPreferences.getString(USER_ID, "").orEmpty()
37 | set(value) = sharedPreferences.putString(USER_ID, value)
38 |
39 | final override var accessToken: String
40 | get() = sharedPreferences.getString(AUTH_TOKEN, "").orEmpty()
41 | set(value) = sharedPreferences.putString(AUTH_TOKEN, value)
42 |
43 | final override var tokenType: String
44 | get() = sharedPreferences.getString(AUTH_TOKEN_TYPE, "").orEmpty()
45 | set(value) = sharedPreferences.putString(AUTH_TOKEN_TYPE, value)
46 |
47 | final override var refreshToken: String
48 | get() = sharedPreferences.getString(REFRESH_TOKEN, "").orEmpty()
49 | set(value) = sharedPreferences.putString(REFRESH_TOKEN, value)
50 |
51 |
52 | @CallSuper
53 | override fun clear() {
54 | sharedPreferences.edit()
55 | .remove(AUTH_TOKEN)
56 | .remove(REFRESH_TOKEN)
57 | .remove(AUTH_TOKEN_TYPE)
58 | .remove(USER_ID)
59 | .apply()
60 | }
61 |
62 | companion object {
63 | const val PREFERENCES_KEY = "preferences"
64 | private const val AUTH_TOKEN = "authentication_token"
65 | private const val REFRESH_TOKEN = "refresh_token"
66 | private const val AUTH_TOKEN_TYPE = "auth_token_type"
67 | private const val USER_ID = "user_id"
68 |
69 | private fun SharedPreferences.putString(key: String, value: String) =
70 | edit().putString(key, value).apply()
71 |
72 | fun create(context: Context, key: String = PREFERENCES_KEY): AuthenticationSharedPreferencesStorage =
73 | AuthenticationSharedPreferencesStorage(context, key)
74 |
75 | fun createSharedPreferences(context: Context, key: String): SharedPreferences =
76 | context.getSharedPreferences(key, Context.MODE_PRIVATE)
77 | }
78 | }
--------------------------------------------------------------------------------
/oauthstorage/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | oauthstorage
3 |
4 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | include(
2 | ":app",
3 | ":core",
4 | ":oauth",
5 | ":oauthdependencies",
6 | ":oauthstorage",
7 | ":oauthadaptergenerator",
8 | ":oauthmoshi",
9 | ":oauthmoshikoin",
10 | ":oauthgson",
11 | ":oauthparsing",
12 | ":oauthsecurestorage",
13 | ":oauthsecurestoragecompat"
14 | )
--------------------------------------------------------------------------------