@@ -31,12 +31,12 @@ import kotlinx.serialization.json.Json
31
31
* @param state This provides protection against attacks such as cross-site request forgery.
32
32
*/
33
33
fun getSpotifyAuthorizationUrl (
34
- vararg scopes : SpotifyScope ,
35
- clientId : String ,
36
- redirectUri : String ,
37
- isImplicitGrantFlow : Boolean = false,
38
- shouldShowDialog : Boolean = false,
39
- state : String? = null
34
+ vararg scopes : SpotifyScope ,
35
+ clientId : String ,
36
+ redirectUri : String ,
37
+ isImplicitGrantFlow : Boolean = false,
38
+ shouldShowDialog : Boolean = false,
39
+ state : String? = null
40
40
): String {
41
41
return SpotifyApi .getAuthUrlFull(
42
42
* scopes,
@@ -48,6 +48,38 @@ fun getSpotifyAuthorizationUrl(
48
48
)
49
49
}
50
50
51
+ /* *
52
+ * Get the PKCE authorization url for the provided [clientId] and [redirectUri] application settings, when attempting to authorize with
53
+ * specified [scopes]
54
+ *
55
+ * @param scopes Spotify scopes the api instance should be able to access for the user
56
+ * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
57
+ * @param redirectUri Spotify [redirect uri](https://developer.spotify.com/documentation/general/guides/app-settings/)
58
+ * @param state This provides protection against attacks such as cross-site request forgery.
59
+ * @param codeChallenge In order to generate the code challenge, your app should hash the code verifier using the SHA256 algorithm.
60
+ * Then, base64url encode the hash that you generated.
61
+ */
62
+ fun getPkceAuthorizationUrl (
63
+ vararg scopes : SpotifyScope ,
64
+ clientId : String ,
65
+ redirectUri : String ,
66
+ codeChallenge : String ,
67
+ state : String? = null
68
+ ): String {
69
+ return SpotifyApi .getPkceAuthUrlFull(
70
+ * scopes,
71
+ clientId = clientId,
72
+ redirectUri = redirectUri,
73
+ codeChallenge = codeChallenge,
74
+ state = state
75
+ )
76
+ }
77
+
78
+ /* *
79
+ * A utility to get the pkce code challenge for a corresponding code verifier. Only available on JVM/Android
80
+ */
81
+ expect fun getSpotifyPkceCodeChallenge (codeVerifier : String ): String
82
+
51
83
// ==============================================
52
84
53
85
// Implicit grant builder
@@ -192,10 +224,10 @@ fun spotifyAppApi(block: SpotifyAppApiBuilder.() -> Unit) = SpotifyAppApiBuilder
192
224
* @return Configurable [SpotifyClientApiBuilder] that, when built, creates a new [SpotifyClientApi]
193
225
*/
194
226
fun spotifyClientApi (
195
- clientId : String ,
196
- clientSecret : String ,
197
- redirectUri : String ,
198
- block : SpotifyClientApiBuilder .() -> Unit
227
+ clientId : String ,
228
+ clientSecret : String ,
229
+ redirectUri : String ,
230
+ block : SpotifyClientApiBuilder .() -> Unit
199
231
) = SpotifyClientApiBuilder ().apply (block).apply {
200
232
credentials {
201
233
this .clientId = clientId
@@ -216,10 +248,10 @@ fun spotifyClientApi(
216
248
* @return Configurable [SpotifyClientApiBuilder] that, when built, creates a new [SpotifyClientApi]
217
249
*/
218
250
fun spotifyClientApi (
219
- clientId : String? ,
220
- clientSecret : String? ,
221
- redirectUri : String? ,
222
- apiToken : Token
251
+ clientId : String? ,
252
+ clientSecret : String? ,
253
+ redirectUri : String? ,
254
+ apiToken : Token
223
255
) = SpotifyClientApiBuilder ().apply {
224
256
credentials {
225
257
this .clientId = clientId
@@ -249,12 +281,12 @@ fun spotifyClientApi(
249
281
* @return Configurable [SpotifyClientApiBuilder] that, when built, creates a new [SpotifyClientApi]
250
282
*/
251
283
fun spotifyClientApi (
252
- clientId : String ,
253
- clientSecret : String ,
254
- redirectUri : String ,
255
- authorization : SpotifyUserAuthorization ,
256
- options : SpotifyApiOptions ? = null,
257
- block : SpotifyClientApiBuilder .() -> Unit = {}
284
+ clientId : String ,
285
+ clientSecret : String ,
286
+ redirectUri : String ,
287
+ authorization : SpotifyUserAuthorization ,
288
+ options : SpotifyApiOptions ? = null,
289
+ block : SpotifyClientApiBuilder .() -> Unit = {}
258
290
) = SpotifyClientApiBuilder ().apply (block).apply {
259
291
credentials {
260
292
this .clientId = clientId
@@ -281,9 +313,9 @@ fun spotifyClientApi(block: SpotifyClientApiBuilder.() -> Unit) = SpotifyClientA
281
313
* Spotify API builder
282
314
*/
283
315
class SpotifyApiBuilder (
284
- private var clientId : String? ,
285
- private var clientSecret : String? ,
286
- private var redirectUri : String?
316
+ private var clientId : String? ,
317
+ private var clientSecret : String? ,
318
+ private var redirectUri : String?
287
319
) {
288
320
/* *
289
321
* Allows you to authenticate a [SpotifyClientApi] with an authorization code
@@ -561,9 +593,9 @@ interface ISpotifyClientApiBuilder : ISpotifyApiBuilder<SpotifyClientApi, Spotif
561
593
* [SpotifyClientApi] builder for api creation using client authorization
562
594
*/
563
595
class SpotifyClientApiBuilder (
564
- override var credentials : SpotifyCredentials = SpotifyCredentialsBuilder ().build(),
565
- override var authorization : SpotifyUserAuthorization = SpotifyUserAuthorizationBuilder ().build(),
566
- override var options : SpotifyApiOptions = SpotifyApiOptionsBuilder ().build()
596
+ override var credentials : SpotifyCredentials = SpotifyCredentialsBuilder ().build(),
597
+ override var authorization : SpotifyUserAuthorization = SpotifyUserAuthorizationBuilder ().build(),
598
+ override var options : SpotifyApiOptions = SpotifyApiOptionsBuilder ().build()
567
599
) : ISpotifyClientApiBuilder {
568
600
override fun getAuthorizationUrl (vararg scopes : SpotifyScope , state : String? ): String {
569
601
require(credentials.redirectUri != null && credentials.clientId != null ) { " You didn't specify a redirect uri or client id in the credentials block!" }
@@ -583,7 +615,7 @@ class SpotifyClientApiBuilder(
583
615
// either application credentials, or a token is required
584
616
require((clientId != null && clientSecret != null && redirectUri != null ) || authorization.token != null || authorization.tokenString != null ) { " You need to specify a valid clientId, clientSecret, and redirectUri in the credentials block!" }
585
617
return when {
586
- authorization.authorizationCode != null -> try {
618
+ authorization.authorizationCode != null && authorization.pkceCodeVerifier == null -> try {
587
619
require(clientId != null && clientSecret != null && redirectUri != null ) { " You need to specify a valid clientId, clientSecret, and redirectUri in the credentials block!" }
588
620
589
621
val response = executeTokenRequest(
@@ -617,7 +649,50 @@ class SpotifyClientApiBuilder(
617
649
options.allowBulkRequests,
618
650
options.requestTimeoutMillis,
619
651
options.json,
620
- options.refreshTokenProducer
652
+ options.refreshTokenProducer,
653
+ options.usesPkceAuth
654
+ )
655
+ } catch (e: CancellationException ) {
656
+ throw e
657
+ } catch (e: Exception ) {
658
+ throw SpotifyException .AuthenticationException (" Invalid credentials provided in the login process (clientId=$clientId , clientSecret=$clientSecret , authCode=${authorization.authorizationCode} )" , e)
659
+ }
660
+ authorization.authorizationCode != null && authorization.pkceCodeVerifier != null -> try {
661
+ require(clientId != null && redirectUri != null ) { " You need to specify a valid clientId, clientSecret, and redirectUri in the credentials block!" }
662
+
663
+ val response = HttpConnection (
664
+ " https://accounts.spotify.com/api/token" ,
665
+ HttpRequestMethod .POST ,
666
+ mapOf (
667
+ " grant_type" to " authorization_code" ,
668
+ " code" to authorization.authorizationCode,
669
+ " redirect_uri" to redirectUri,
670
+ " client_id" to clientId,
671
+ " code_verifier" to authorization.pkceCodeVerifier
672
+ ),
673
+ null ,
674
+ " application/x-www-form-urlencoded" ,
675
+ listOf (),
676
+ null
677
+ ).execute()
678
+
679
+ SpotifyClientApi (
680
+ clientId,
681
+ clientSecret,
682
+ redirectUri,
683
+ response.body.toObject(Token .serializer(), null , options.json),
684
+ options.useCache,
685
+ options.cacheLimit,
686
+ options.automaticRefresh,
687
+ options.retryWhenRateLimited,
688
+ options.enableLogger,
689
+ options.testTokenValidity,
690
+ options.defaultLimit,
691
+ options.allowBulkRequests,
692
+ options.requestTimeoutMillis,
693
+ options.json,
694
+ options.refreshTokenProducer,
695
+ options.usesPkceAuth
621
696
)
622
697
} catch (e: CancellationException ) {
623
698
throw e
@@ -639,7 +714,8 @@ class SpotifyClientApiBuilder(
639
714
options.allowBulkRequests,
640
715
options.requestTimeoutMillis,
641
716
options.json,
642
- options.refreshTokenProducer
717
+ options.refreshTokenProducer,
718
+ options.usesPkceAuth
643
719
)
644
720
authorization.tokenString != null -> SpotifyClientApi (
645
721
clientId,
@@ -662,7 +738,8 @@ class SpotifyClientApiBuilder(
662
738
options.allowBulkRequests,
663
739
options.requestTimeoutMillis,
664
740
options.json,
665
- options.refreshTokenProducer
741
+ options.refreshTokenProducer,
742
+ options.usesPkceAuth
666
743
)
667
744
else -> throw IllegalArgumentException (
668
745
" At least one of: authorizationCode, tokenString, or token must be provided " +
@@ -681,9 +758,9 @@ interface ISpotifyAppApiBuilder : ISpotifyApiBuilder<SpotifyAppApi, SpotifyAppAp
681
758
* [SpotifyAppApi] builder for api creation using client authorization
682
759
*/
683
760
class SpotifyAppApiBuilder (
684
- override var credentials : SpotifyCredentials = SpotifyCredentialsBuilder ().build(),
685
- override var authorization : SpotifyUserAuthorization = SpotifyUserAuthorizationBuilder ().build(),
686
- override var options : SpotifyApiOptions = SpotifyApiOptionsBuilder ().build()
761
+ override var credentials : SpotifyCredentials = SpotifyCredentialsBuilder ().build(),
762
+ override var authorization : SpotifyUserAuthorization = SpotifyUserAuthorizationBuilder ().build(),
763
+ override var options : SpotifyApiOptions = SpotifyApiOptionsBuilder ().build()
687
764
) : ISpotifyAppApiBuilder {
688
765
/* *
689
766
* Build a public [SpotifyAppApi] using the provided credentials
@@ -806,10 +883,10 @@ data class SpotifyCredentials(val clientId: String?, val clientSecret: String?,
806
883
* limited time constraint on these before the API automatically refreshes them
807
884
*/
808
885
class SpotifyUserAuthorizationBuilder (
809
- var authorizationCode : String? = null ,
810
- var tokenString : String? = null ,
811
- var token : Token ? = null ,
812
- var refreshTokenString : String? = null
886
+ var authorizationCode : String? = null ,
887
+ var tokenString : String? = null ,
888
+ var token : Token ? = null ,
889
+ var refreshTokenString : String? = null
813
890
) {
814
891
fun build () = SpotifyUserAuthorization (authorizationCode, tokenString, token, refreshTokenString)
815
892
}
@@ -824,12 +901,14 @@ class SpotifyUserAuthorizationBuilder(
824
901
* will be your **access** token. If you're building [SpotifyApi], it will be your **refresh** token. There is a *very*
825
902
* limited time constraint on these before the API automatically refreshes them
826
903
* @property refreshTokenString Refresh token, given as a string, to be exchanged to Spotify for a new token
904
+ * @property pkceCodeVerifier The code verifier generated that the client authenticated with (using its code challenge)
827
905
*/
828
906
data class SpotifyUserAuthorization (
829
- var authorizationCode : String? = null ,
830
- var tokenString : String? = null ,
831
- var token : Token ? = null ,
832
- var refreshTokenString : String? = null
907
+ var authorizationCode : String? = null ,
908
+ var tokenString : String? = null ,
909
+ var token : Token ? = null ,
910
+ var refreshTokenString : String? = null ,
911
+ var pkceCodeVerifier : String? = null
833
912
)
834
913
835
914
/* *
@@ -850,18 +929,18 @@ data class SpotifyUserAuthorization(
850
929
*
851
930
*/
852
931
class SpotifyApiOptionsBuilder (
853
- var useCache : Boolean = true ,
854
- var cacheLimit : Int? = 200 ,
855
- var automaticRefresh : Boolean = true ,
856
- var retryWhenRateLimited : Boolean = true ,
857
- var enableLogger : Boolean = true ,
858
- var testTokenValidity : Boolean = false ,
859
- var enableAllOptions : Boolean = false ,
860
- var defaultLimit : Int = 50 ,
861
- var allowBulkRequests : Boolean = true ,
862
- var requestTimeoutMillis : Long? = null ,
863
- var json : Json = nonstrictJson,
864
- var refreshTokenProducer : (suspend (SpotifyApi <* , * >) -> Token )? = null
932
+ var useCache : Boolean = true ,
933
+ var cacheLimit : Int? = 200 ,
934
+ var automaticRefresh : Boolean = true ,
935
+ var retryWhenRateLimited : Boolean = true ,
936
+ var enableLogger : Boolean = true ,
937
+ var testTokenValidity : Boolean = false ,
938
+ var enableAllOptions : Boolean = false ,
939
+ var defaultLimit : Int = 50 ,
940
+ var allowBulkRequests : Boolean = true ,
941
+ var requestTimeoutMillis : Long? = null ,
942
+ var json : Json = nonstrictJson,
943
+ var refreshTokenProducer : (suspend (SpotifyApi <* , * >) -> Token )? = null
865
944
) {
866
945
fun build () =
867
946
if (enableAllOptions)
@@ -912,17 +991,18 @@ class SpotifyApiOptionsBuilder(
912
991
*/
913
992
914
993
data class SpotifyApiOptions (
915
- var useCache : Boolean ,
916
- var cacheLimit : Int? ,
917
- var automaticRefresh : Boolean ,
918
- var retryWhenRateLimited : Boolean ,
919
- var enableLogger : Boolean ,
920
- var testTokenValidity : Boolean ,
921
- var defaultLimit : Int ,
922
- var allowBulkRequests : Boolean ,
923
- var requestTimeoutMillis : Long? ,
924
- var json : Json ,
925
- var refreshTokenProducer : (suspend (SpotifyApi <* , * >) -> Token )?
994
+ var useCache : Boolean ,
995
+ var cacheLimit : Int? ,
996
+ var automaticRefresh : Boolean ,
997
+ var retryWhenRateLimited : Boolean ,
998
+ var enableLogger : Boolean ,
999
+ var testTokenValidity : Boolean ,
1000
+ var defaultLimit : Int ,
1001
+ var allowBulkRequests : Boolean ,
1002
+ var requestTimeoutMillis : Long? ,
1003
+ var json : Json ,
1004
+ var refreshTokenProducer : (suspend (SpotifyApi <* , * >) -> Token )? ,
1005
+ val usesPkceAuth : Boolean = false
926
1006
)
927
1007
928
1008
@Deprecated(" Name has been replaced by `options`" , ReplaceWith (" SpotifyApiOptions" ))
0 commit comments