Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion auth-lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ android {
}
}

def manifestPlaceholdersForTests = [redirectSchemeName: "spotify-sdk", redirectHostName: "auth"]
def manifestPlaceholdersForTests = [redirectSchemeName: "spotify-sdk", redirectHostName: "auth", redirectPathPattern: ".*"]
namespace 'com.spotify.sdk.android.auth'
unitTestVariants.configureEach {
it.mergedFlavor.manifestPlaceholders += manifestPlaceholdersForTests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public interface AccountsQueryParameters {
String CODE = "code";
String ACCESS_TOKEN = "access_token";
String EXPIRES_IN = "expires_in";
String ASSOCIATED_CONTENT = "associated_content";
String CODE_CHALLENGE = "code_challenge";
String CODE_CHALLENGE_METHOD = "code_challenge_method";
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,21 @@

package com.spotify.sdk.android.auth;

import static com.spotify.sdk.android.auth.AccountsQueryParameters.ASSOCIATED_CONTENT;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Base64;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.json.JSONObject;

import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;

Expand All @@ -50,6 +55,8 @@ public class AuthorizationRequest implements Parcelable {
public static final String SPOTIFY_SDK = "spotify-sdk";
@VisibleForTesting
public static final String ANDROID_SDK = "android-sdk";
private static final String KEY_URI = "uri";
private static final String KEY_URL = "url";

private final String mClientId;
private final String mResponseType;
Expand All @@ -59,6 +66,8 @@ public class AuthorizationRequest implements Parcelable {
private final boolean mShowDialog;
private final Map<String, String> mCustomParams;
private final String mCampaign;
private final String mContentUri;
private final String mContentUrl;
private final PKCEInformation mPkceInformation;

/**
Expand All @@ -78,6 +87,8 @@ public static class Builder {
private String mCampaign;
private PKCEInformation mPkceInformation;
private final Map<String, String> mCustomParams = new HashMap<>();
private String mContentUri;
private String mContentUrl;

public Builder(String clientId, AuthorizationResponse.Type responseType, String redirectUri) {
if (clientId == null) {
Expand Down Expand Up @@ -126,14 +137,24 @@ public Builder setCampaign(String campaign) {
return this;
}

public Builder setContentUri(String contentUri) {
mContentUri = contentUri;
return this;
}

public Builder setContentUrl(String contentUrl) {
mContentUrl = contentUrl;
return this;
}

public Builder setPkceInformation(PKCEInformation pkceInformation) {
mPkceInformation = pkceInformation;
return this;
}

public AuthorizationRequest build() {
return new AuthorizationRequest(mClientId, mResponseType, mRedirectUri,
mState, mScopes, mShowDialog, mCustomParams, mCampaign, mPkceInformation);
mState, mScopes, mShowDialog, mCustomParams, mCampaign, mContentUri, mContentUrl, mPkceInformation);
}
}

Expand All @@ -146,6 +167,8 @@ public AuthorizationRequest(Parcel source) {
mShowDialog = source.readByte() == 1;
mCustomParams = new HashMap<>();
mCampaign = source.readString();
mContentUri = source.readString();
mContentUrl = source.readString();
mPkceInformation = source.readParcelable(PKCEInformation.class.getClassLoader());
Bundle bundle = source.readBundle(getClass().getClassLoader());
for (String key : bundle.keySet()) {
Expand Down Expand Up @@ -177,6 +200,38 @@ public String getCustomParam(String key) {
return mCustomParams.get(key);
}

public String getEncodedContent() {
JSONObject contentJson = getContentsJson();

if(contentJson.length() == 0) {
return null; // No content to encode
}

return Base64.encodeToString(contentJson.toString().getBytes(Charset.forName("UTF-8")), Base64.URL_SAFE | Base64.NO_WRAP);
}

@NonNull
private JSONObject getContentsJson() {
JSONObject contentJson = new JSONObject();

if(mContentUri != null && !mContentUri.isEmpty()) {
try {
contentJson.put(KEY_URI, mContentUri);
} catch (Exception e) {
throw new IllegalArgumentException("Error creating JSON for content URI: " + e.getMessage());
}
}

if(mContentUrl != null && !mContentUrl.isEmpty()) {
try {
contentJson.put(KEY_URL, mContentUrl);
} catch (Exception e) {
throw new IllegalArgumentException("Error creating JSON for content URL: " + e.getMessage());
}
}
return contentJson;
}

@NonNull
public String getCampaign() { return TextUtils.isEmpty(mCampaign) ? ANDROID_SDK : mCampaign; }

Expand All @@ -198,8 +253,10 @@ private AuthorizationRequest(String clientId,
boolean showDialog,
Map<String, String> customParams,
String campaign,
PKCEInformation pkceInformation) {

String contentUri,
String contentUrl,
PKCEInformation pkceInformation
) {
mClientId = clientId;
mResponseType = responseType.toString();
mRedirectUri = redirectUri;
Expand All @@ -208,6 +265,8 @@ private AuthorizationRequest(String clientId,
mShowDialog = showDialog;
mCustomParams = customParams;
mCampaign = campaign;
mContentUri = contentUri;
mContentUrl = contentUrl;
mPkceInformation = pkceInformation;
}

Expand Down Expand Up @@ -238,6 +297,11 @@ public Uri toUri() {
}
}

String associatedContent = getEncodedContent();

if(associatedContent != null) {
uriBuilder.appendQueryParameter(ASSOCIATED_CONTENT, associatedContent);
}
if (mPkceInformation != null) {
uriBuilder.appendQueryParameter(AccountsQueryParameters.CODE_CHALLENGE, mPkceInformation.getChallenge());
uriBuilder.appendQueryParameter(AccountsQueryParameters.CODE_CHALLENGE_METHOD, mPkceInformation.getCodeChallengeMethod());
Expand Down Expand Up @@ -269,6 +333,8 @@ public void writeToParcel(Parcel dest, int flags) {
dest.writeStringArray(mScopes);
dest.writeByte((byte) (mShowDialog ? 1 : 0));
dest.writeString(mCampaign);
dest.writeString(mContentUri);
dest.writeString(mContentUrl);
dest.writeParcelable(mPkceInformation, flags);

Bundle bundle = new Bundle();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ public interface IntentExtras {
* DO NOT CHANGE THIS.
*/
String KEY_VERSION = "VERSION";
String KEY_ASSOCIATED_CONTENT = "associated_content";
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

package com.spotify.sdk.android.auth.app;

import static com.spotify.sdk.android.auth.IntentExtras.KEY_ASSOCIATED_CONTENT;
import static com.spotify.sdk.android.auth.IntentExtras.KEY_CLIENT_ID;
import static com.spotify.sdk.android.auth.IntentExtras.KEY_CODE_CHALLENGE;
import static com.spotify.sdk.android.auth.IntentExtras.KEY_CODE_CHALLENGE_METHOD;
Expand Down Expand Up @@ -107,6 +108,11 @@ public boolean startAuthActivity() {
intent.putExtra(KEY_UTM_CAMPAIGN, mRequest.getCampaign());
intent.putExtra(KEY_UTM_MEDIUM, mRequest.getMedium());

String associatedContent = mRequest.getEncodedContent();

if(associatedContent != null) {
intent.putExtra(KEY_ASSOCIATED_CONTENT, associatedContent);
}
final PKCEInformation pkceInfo = mRequest.getPkceInformation();
if (pkceInfo != null) {
intent.putExtra(KEY_CODE_CHALLENGE, pkceInfo.getChallenge());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,26 @@ public void shouldSetCustomParams() {
assertEquals(uri, authorizationRequest.toUri());
}

@Test
public void shouldSetContent() {
String contentUri = "spotify:track:1234567890";
String contentUrl = "https://open.spotify.com/track/1234567890";
String encodedContent = "eyJ1cmkiOiJzcG90aWZ5OnRyYWNrOjEyMzQ1Njc4OTAiLCJ1cmwiOiJodHRwczpcL1wvb3Blbi5zcG90aWZ5LmNvbVwvdHJhY2tcLzEyMzQ1Njc4OTAifQ==";

AuthorizationRequest authorizationRequest = new AuthorizationRequest.Builder(mClientId, mResponseType, mRedirectUri)
.setContentUri(contentUri)
.setContentUrl(contentUrl)
.build();

assertEquals(authorizationRequest.getEncodedContent(), encodedContent);

Uri.Builder uriBuilder = getBaseAuthUri(mClientId, mResponseType.toString(), mRedirectUri, mDefaultCampaign);
uriBuilder.appendQueryParameter(AccountsQueryParameters.ASSOCIATED_CONTENT, encodedContent);
Uri uri = uriBuilder.build();

assertEquals(uri, authorizationRequest.toUri());
}

@Test(expected = IllegalArgumentException.class)
public void shouldThrowWithNullCustomParamKey() {
new AuthorizationRequest.Builder(mClientId, mResponseType, mRedirectUri)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,21 +172,52 @@ public void hasUtmExtrasSetToLoginIntent() {
assertEquals(campaign, intent.getStringExtra(IntentExtras.KEY_UTM_CAMPAIGN));
}

@Test
public void hasContentExtrasSetToLoginIntent() {
String campaign = "campaign";
String contentUri = "spotify:track:1234567890";
String contentUrl = "https://open.spotify.com/track/1234567890";
String encodedContent = "eyJ1cmkiOiJzcG90aWZ5OnRyYWNrOjEyMzQ1Njc4OTAiLCJ1cmwiOiJodHRwczpcL1wvb3Blbi5zcG90aWZ5LmNvbVwvdHJhY2tcLzEyMzQ1Njc4OTAifQ==";
Activity activity = mock(Activity.class);
Mockito.doNothing().when(activity).startActivityForResult(any(Intent.class), anyInt());
AuthorizationRequest authorizationRequest =
new AuthorizationRequest
.Builder("test", AuthorizationResponse.Type.TOKEN, "to://me")
.setScopes(new String[]{"testa", "toppen"})
.setCampaign(campaign)
.setContentUri(contentUri)
.setContentUrl(contentUrl)
.build();
configureMocksWithSigningInfo(activity);
final SpotifyNativeAuthUtil authUtil = new SpotifyNativeAuthUtil(
activity,
authorizationRequest,
new FakeSha1HashUtil(Collections.singletonMap(DEFAULT_TEST_SIGNATURE, SPOTIFY_HASH))
);
authUtil.startAuthActivity();

final ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
verify(activity, times(1)).startActivityForResult(captor.capture(), anyInt());
final Intent intent = captor.getValue();

assertEquals(encodedContent, intent.getStringExtra(IntentExtras.KEY_ASSOCIATED_CONTENT));
}

@Test
public void shouldIncludePkceParametersInIntent() {
final String verifier = "test_verifier_1234567890";
final String challenge = "test_challenge_abcdef";
final PKCEInformation pkceInfo = PKCEInformation.sha256(verifier, challenge);
final Activity activity = mock(Activity.class);
Mockito.doNothing().when(activity).startActivityForResult(any(Intent.class), anyInt());

final AuthorizationRequest authorizationRequest =
new AuthorizationRequest
.Builder("test", AuthorizationResponse.Type.TOKEN, "to://me")
.setScopes(new String[]{"testa", "toppen"})
.setPkceInformation(pkceInfo)
.build();

configureMocksWithSigningInfo(activity);
final SpotifyNativeAuthUtil authUtil = new SpotifyNativeAuthUtil(
activity,
Expand All @@ -207,14 +238,14 @@ public void shouldIncludePkceParametersInIntent() {
public void shouldNotIncludePkceParametersWhenNull() {
final Activity activity = mock(Activity.class);
Mockito.doNothing().when(activity).startActivityForResult(any(Intent.class), anyInt());

final AuthorizationRequest authorizationRequest =
new AuthorizationRequest
.Builder("test", AuthorizationResponse.Type.TOKEN, "to://me")
.setScopes(new String[]{"testa", "toppen"})
.setPkceInformation(null)
.build();

configureMocksWithSigningInfo(activity);
final SpotifyNativeAuthUtil authUtil = new SpotifyNativeAuthUtil(
activity,
Expand Down