Skip to content

Commit f90d86e

Browse files
committed
finalize implicit/pkce android auth
Signed-off-by: Adam Ratzman <[email protected]>
1 parent d0e848d commit f90d86e

13 files changed

+512
-113
lines changed

build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,11 @@ kotlin {
242242
}
243243

244244
dependencies {
245-
implementation("com.spotify.android:auth:$androidSpotifyAuthVersion")
245+
api("com.spotify.android:auth:$androidSpotifyAuthVersion")
246+
implementation("com.pnikosis:materialish-progress:1.7")
246247
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
247248
implementation("androidx.security:security-crypto:$androidCryptoVersion")
249+
implementation("androidx.appcompat:appcompat:1.2.0")
248250
}
249251
}
250252

src/androidMain/kotlin/com/adamratzman/spotify/auth/AuthUtils.kt renamed to src/androidMain/kotlin/com/adamratzman/spotify/auth/SpotifyDefaultCredentialStore.kt

Lines changed: 79 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,73 +2,24 @@ package com.adamratzman.spotify.auth
22

33
import android.annotation.SuppressLint
44
import android.app.Activity
5+
import android.app.Application
56
import android.content.Context
6-
import android.content.Intent
77
import android.content.SharedPreferences
88
import android.os.Build
99
import androidx.annotation.RequiresApi
1010
import androidx.security.crypto.EncryptedSharedPreferences
1111
import androidx.security.crypto.MasterKeys
12+
import com.adamratzman.spotify.GenericSpotifyApi
13+
import com.adamratzman.spotify.SpotifyApi
1214
import com.adamratzman.spotify.SpotifyApiOptions
13-
import com.adamratzman.spotify.SpotifyException
15+
import com.adamratzman.spotify.SpotifyClientApi
1416
import com.adamratzman.spotify.SpotifyImplicitGrantApi
15-
import com.adamratzman.spotify.auth.SpotifyDefaultAuthHelper.activityBack
17+
import com.adamratzman.spotify.SpotifyUserAuthorization
1618
import com.adamratzman.spotify.models.Token
19+
import com.adamratzman.spotify.spotifyClientPkceApi
1720
import com.adamratzman.spotify.spotifyImplicitGrantApi
21+
import com.adamratzman.spotify.utils.logToConsole
1822

19-
// Starting login activity
20-
21-
/**
22-
* Start Spotify login activity within an existing activity.
23-
*/
24-
public inline fun <reified T : AbstractSpotifyLoginActivity> Activity.startSpotifyLoginActivity() {
25-
startSpotifyLoginActivity(T::class.java)
26-
}
27-
28-
/**
29-
* Start Spotify login activity within an existing activity.
30-
*
31-
* @param spotifyLoginImplementationClass Your implementation of [AbstractSpotifyLoginActivity], defining what to do on Spotify login
32-
*/
33-
public fun <T : AbstractSpotifyLoginActivity> Activity.startSpotifyLoginActivity(spotifyLoginImplementationClass: Class<T>) {
34-
startActivity(Intent(this, spotifyLoginImplementationClass))
35-
}
36-
37-
38-
/**
39-
* Basic authentication guard - verifies that the user is logged in to Spotify and uses [SpotifyDefaultAuthHelper] to
40-
* handle re-authentication and redirection back to the activity.
41-
*
42-
* Note: this should only be used for small applications.
43-
*
44-
* @param spotifyLoginImplementationClass Your implementation of [AbstractSpotifyLoginActivity], defining what to do on Spotify login
45-
* @param classBackTo The activity to return to if re-authentication is necessary
46-
* @block The code block to execute
47-
*/
48-
public fun <T> Activity.guardValidSpotifyApi(
49-
spotifyLoginImplementationClass: Class<out AbstractSpotifyLoginActivity>,
50-
classBackTo: Class<out Activity>? = null,
51-
block: () -> T
52-
): T? {
53-
return try {
54-
block()
55-
} catch (e: SpotifyException.ReAuthenticationNeededException) {
56-
activityBack = classBackTo
57-
startSpotifyLoginActivity(spotifyLoginImplementationClass)
58-
null
59-
}
60-
}
61-
62-
/**
63-
* Default authentiction helper for Android. Contains static variables useful in authentication.
64-
*
65-
*/
66-
public object SpotifyDefaultAuthHelper {
67-
/**
68-
* The activity to return to if re-authentication is necessary. Null except during authentication when using [guardValidSpotifyApi]
69-
*/
70-
public var activityBack: Class<out Activity>? = null
71-
}
7223

7324
/**
7425
* Provided credential store for holding current Spotify token credentials, allowing you to easily store and retrieve
@@ -79,7 +30,11 @@ public object SpotifyDefaultAuthHelper {
7930
*
8031
*/
8132
@RequiresApi(Build.VERSION_CODES.M)
82-
public class SpotifyDefaultCredentialStore constructor(private val clientId: String, applicationContext: Context) {
33+
public class SpotifyDefaultCredentialStore constructor(
34+
private val clientId: String,
35+
private val redirectUri: String,
36+
applicationContext: Context
37+
) {
8338
public companion object {
8439
/**
8540
* The key used with spotify token expiry in [EncryptedSharedPreferences]
@@ -91,8 +46,20 @@ public class SpotifyDefaultCredentialStore constructor(private val clientId: Str
9146
*/
9247
public const val SpotifyAccessTokenKey: String = "spotifyAccessToken"
9348

49+
/**
50+
* The key used with spotify refresh token in [EncryptedSharedPreferences]
51+
*/
52+
public const val SpotifyRefreshTokenKey: String = "spotifyRefreshToken"
53+
54+
/**
55+
* The activity to return to if re-authentication is necessary on implicit authentication. Null except during authentication when using [guardValidImplicitSpotifyApi]
56+
*/
57+
public var activityBackOnImplicitAuth: Class<out Activity>? = null
9458
}
9559

60+
61+
public var credentialTypeStored: CredentialType? = null
62+
9663
/**
9764
* The [EncryptedSharedPreferences] that this API saves to/retrieves from.
9865
*/
@@ -126,6 +93,14 @@ public class SpotifyDefaultCredentialStore constructor(private val clientId: Str
12693
get() = encryptedPreferences.getString(SpotifyAccessTokenKey, null)
12794
set(value) = encryptedPreferences.edit().putString(SpotifyAccessTokenKey, value).apply()
12895

96+
/**
97+
* Get/set the Spotify refresh token.
98+
*/
99+
public var spotifyRefreshToken: String?
100+
get() = encryptedPreferences.getString(SpotifyRefreshTokenKey, null)
101+
set(value) = encryptedPreferences.edit().putString(SpotifyRefreshTokenKey, value).apply()
102+
103+
129104
/**
130105
* Get/set the Spotify [Token] obtained from [spotifyToken].
131106
* If the token has expired according to [spotifyTokenExpiresAt], this will return null.
@@ -136,15 +111,28 @@ public class SpotifyDefaultCredentialStore constructor(private val clientId: Str
136111
val accessToken = spotifyAccessToken ?: return null
137112
if (tokenExpiresAt < System.currentTimeMillis()) return null
138113

139-
return Token(accessToken, "Bearer", (tokenExpiresAt - System.currentTimeMillis()).toInt() / 1000)
114+
val refreshToken = spotifyRefreshToken
115+
return Token(
116+
accessToken,
117+
"Bearer",
118+
(tokenExpiresAt - System.currentTimeMillis()).toInt() / 1000,
119+
refreshToken
120+
)
140121
}
141122
set(token) {
142123
if (token == null) {
143124
spotifyAccessToken = null
144125
spotifyTokenExpiresAt = null
126+
spotifyRefreshToken = null
127+
128+
credentialTypeStored = null
145129
} else {
146130
spotifyAccessToken = token.accessToken
147131
spotifyTokenExpiresAt = token.expiresAt
132+
spotifyRefreshToken = token.refreshToken
133+
134+
credentialTypeStored =
135+
if (token.refreshToken != null) CredentialType.Pkce else CredentialType.ImplicitGrant
148136
}
149137
}
150138

@@ -159,11 +147,33 @@ public class SpotifyDefaultCredentialStore constructor(private val clientId: Str
159147
}
160148

161149
/**
162-
* Sets [spotifyToken] using [SpotifyImplicitGrantApi.token]. This wraps around [spotifyToken]'s setter.
150+
* Create a new [SpotifyClientApi] instance using the [spotifyToken] stored using this credential store.
163151
*
164-
* @param api A valid [SpotifyImplicitGrantApi]
152+
* @param block Applied configuration to the [SpotifyImplicitGrantApi]
165153
*/
166-
public fun setSpotifyImplicitGrantApi(api: SpotifyImplicitGrantApi) {
154+
public suspend fun getSpotifyClientPkceApi(block: ((SpotifyApiOptions).() -> Unit)? = null): SpotifyClientApi? {
155+
val token = spotifyToken ?: return null
156+
return spotifyClientPkceApi(
157+
clientId,
158+
redirectUri,
159+
SpotifyUserAuthorization(token = token),
160+
block ?: {}
161+
).build().apply {
162+
val previousAfterTokenRefresh = spotifyApiOptions.afterTokenRefresh
163+
spotifyApiOptions.afterTokenRefresh = {
164+
spotifyToken = this.token
165+
logToConsole("Refreshed Spotify PKCE token in credential store... $token")
166+
previousAfterTokenRefresh?.invoke(this)
167+
}
168+
}
169+
}
170+
171+
/**
172+
* Sets [spotifyToken] using [SpotifyApi.token]. This wraps around [spotifyToken]'s setter.
173+
*
174+
* @param api A valid [GenericSpotifyApi]
175+
*/
176+
public fun setSpotifyApi(api: GenericSpotifyApi) {
167177
spotifyToken = api.token
168178
}
169179

@@ -179,6 +189,12 @@ public class SpotifyDefaultCredentialStore constructor(private val clientId: Str
179189
}
180190
}
181191

192+
public enum class CredentialType {
193+
ImplicitGrant,
194+
Pkce
195+
}
182196

183-
184-
197+
@RequiresApi(Build.VERSION_CODES.M)
198+
public fun Application.getDefaultCredentialStore(clientId: String, redirectUri: String): SpotifyDefaultCredentialStore {
199+
return SpotifyDefaultCredentialStore(clientId, redirectUri, applicationContext)
200+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.adamratzman.spotify.auth.implicit
2+
3+
import android.app.Activity
4+
import android.content.Intent
5+
import android.os.Bundle
6+
import androidx.appcompat.app.AppCompatActivity
7+
8+
/**
9+
* Wrapper around spotify-auth's [LoginActivity] that allows configuration of the authentication process, along with
10+
* callbacks on successful and failed authentication. Pair this with [SpotifyDefaultCredentialStore] to easily store credentials.
11+
* Inherits from [AppCompatActivity]. If instead you want to inherit from [Activity], please use [AbstractSpotifyAppImplicitLoginActivity].
12+
*
13+
*/
14+
public abstract class AbstractSpotifyAppCompatImplicitLoginActivity : SpotifyImplicitLoginActivity,
15+
AppCompatActivity() {
16+
@Suppress("LeakingThis")
17+
public override val activity: Activity = this
18+
public override val useDefaultRedirectHandler: Boolean = true
19+
20+
21+
override fun onCreate(savedInstanceState: Bundle?) {
22+
super.onCreate(savedInstanceState)
23+
24+
triggerLoginActivity()
25+
}
26+
27+
28+
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
29+
super.onActivityResult(requestCode, resultCode, intent)
30+
processActivityResult(requestCode, resultCode, intent)
31+
}
32+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.adamratzman.spotify.auth.implicit
2+
3+
import android.app.Activity
4+
import android.content.Intent
5+
import android.os.Bundle
6+
import androidx.appcompat.app.AppCompatActivity
7+
import com.spotify.sdk.android.auth.LoginActivity
8+
9+
/**
10+
* Wrapper around spotify-auth's [LoginActivity] that allows configuration of the authentication process, along with
11+
* callbacks on successful and failed authentication. Pair this with [SpotifyDefaultCredentialStore] to easily store credentials.
12+
* Inherits from [Activity]. If instead you want to inherit from [AppCompatActivity], please use [AbstractSpotifyAppCompatImplicitLoginActivity].
13+
*
14+
*/
15+
public abstract class AbstractSpotifyAppImplicitLoginActivity : SpotifyImplicitLoginActivity, Activity() {
16+
@Suppress("LeakingThis")
17+
public override val activity: Activity = this
18+
public override val useDefaultRedirectHandler: Boolean = true
19+
20+
21+
override fun onCreate(savedInstanceState: Bundle?) {
22+
super.onCreate(savedInstanceState)
23+
24+
triggerLoginActivity()
25+
}
26+
27+
28+
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
29+
super.onActivityResult(requestCode, resultCode, intent)
30+
processActivityResult(requestCode, resultCode, intent)
31+
}
32+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.adamratzman.spotify.auth.implicit
2+
3+
import android.app.Activity
4+
import android.content.Intent
5+
import com.adamratzman.spotify.SpotifyException
6+
import com.adamratzman.spotify.auth.SpotifyDefaultCredentialStore.Companion.activityBackOnImplicitAuth
7+
8+
// Starting implicit login activity
9+
10+
/**
11+
* Start Spotify implicit login activity within an existing activity.
12+
*/
13+
public inline fun <reified T : SpotifyImplicitLoginActivity> Activity.startSpotifyImplicitLoginActivity() {
14+
startSpotifyImplicitLoginActivity(T::class.java)
15+
}
16+
17+
/**
18+
* Start Spotify implicit login activity within an existing activity.
19+
*
20+
* @param spotifyLoginImplementationClass Your implementation of [SpotifyImplicitLoginActivity], defining what to do on Spotify login
21+
*/
22+
public fun <T : SpotifyImplicitLoginActivity> Activity.startSpotifyImplicitLoginActivity(spotifyLoginImplementationClass: Class<T>) {
23+
startActivity(Intent(this, spotifyLoginImplementationClass))
24+
}
25+
26+
/**
27+
* Basic implicit authentication guard - verifies that the user is logged in to Spotify and uses [SpotifyDefaultImplicitAuthHelper] to
28+
* handle re-authentication and redirection back to the activity.
29+
*
30+
* Note: this should only be used for small applications.
31+
*
32+
* @param spotifyImplicitLoginImplementationClass Your implementation of [SpotifyImplicitLoginActivity], defining what to do on Spotify login
33+
* @param classBackTo The activity to return to if re-authentication is necessary
34+
* @block The code block to execute
35+
*/
36+
public fun <T> Activity.guardValidImplicitSpotifyApi(
37+
spotifyImplicitLoginImplementationClass: Class<out SpotifyImplicitLoginActivity>,
38+
classBackTo: Class<out Activity>? = null,
39+
block: () -> T
40+
): T? {
41+
return try {
42+
block()
43+
} catch (e: SpotifyException.ReAuthenticationNeededException) {
44+
activityBackOnImplicitAuth = classBackTo
45+
startSpotifyImplicitLoginActivity(spotifyImplicitLoginImplementationClass)
46+
null
47+
}
48+
}
49+

0 commit comments

Comments
 (0)