Skip to content

Commit

Permalink
Sign in with Solana (#584)
Browse files Browse the repository at this point in the history
* preparing SIWS parsing with tests

* bit of cleaning

* siws sdk updates (prelim, needs work)

* sign in working!

* centralize sign in payload logic

* add some more tests

* garrrrr fix bad refactor in test code

* ui tests

* simulate sign in not supported (fallback on sign message)
  • Loading branch information
Funkatronics authored Nov 13, 2023
1 parent ac83e6d commit f3f5739
Show file tree
Hide file tree
Showing 29 changed files with 1,702 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class LocalAdapterOperations(
): MobileWalletAdapterClient.AuthorizationResult {
return withContext(ioDispatcher) {
@Suppress("BlockingMethodInNonBlockingContext")
client?.authorize(identityUri, iconUri, identityName, chain, authToken, features, addresses)?.get()
client?.authorize(identityUri, iconUri, identityName, chain, authToken, features, addresses, null)?.get()
?: throw InvalidObjectException("Provide a client before performing adapter operations")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class MobileWalletAdapterTest {
sample20AuthResult = AuthorizationResult.create("20AUTHTOKENRESULT", byteArrayOf(), "Some Label", Uri.EMPTY)

mockClient = mock {
on { authorize(any(), any(), any(), any(), anyOrNull(), anyOrNull(), anyOrNull()) } doAnswer {
on { authorize(any(), any(), any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doAnswer {
mock {
on { get() } doReturn sample20AuthResult
}
Expand Down Expand Up @@ -151,7 +151,7 @@ class MobileWalletAdapterTest {
refString
}

verify(mockClient, times(1)).authorize(any(), any(), any(), any(), anyOrNull(), anyOrNull(),anyOrNull())
verify(mockClient, times(1)).authorize(any(), any(), any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
assertTrue { result is TransactionResult.Success<String> }
assertTrue { result.successPayload == refString }
assertTrue { (result as TransactionResult.Success<String>).authResult == sample20AuthResult }
Expand Down Expand Up @@ -200,7 +200,7 @@ class MobileWalletAdapterTest {
refString
}

verify(mockClient, times(1)).authorize(any(), any(), any(), any(), any(), anyOrNull(),anyOrNull())
verify(mockClient, times(1)).authorize(any(), any(), any(), any(), any(), anyOrNull(), anyOrNull(), anyOrNull())

assertTrue { result is TransactionResult.Success<String> }
assertTrue { result.successPayload == refString }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import com.solana.mobilewalletadapter.clientlib.transaction.TransactionVersion;
import com.solana.mobilewalletadapter.common.ProtocolContract;
import com.solana.mobilewalletadapter.common.signin.SignInWithSolana;
import com.solana.mobilewalletadapter.common.util.Identifier;
import com.solana.mobilewalletadapter.common.util.JsonPack;
import com.solana.mobilewalletadapter.common.util.NotifyOnCompleteFuture;
Expand Down Expand Up @@ -146,13 +147,17 @@ public static class AuthorizationResult {
public final Uri walletUriBase;
@NonNull @Size(min = 1)
public final AuthorizedAccount[] accounts;
@Nullable
public final SignInResult signInResult;

private AuthorizationResult(@NonNull String authToken,
@NonNull @Size(min = 1) AuthorizedAccount[] accounts,
@Nullable Uri walletUriBase) {
@Nullable Uri walletUriBase,
@Nullable SignInResult signInResult) {
this.authToken = authToken;
this.walletUriBase = walletUriBase;
this.accounts = accounts;
this.signInResult = signInResult;
this.publicKey = accounts[0].publicKey;
this.accountLabel = accounts[0].accountLabel;
}
Expand Down Expand Up @@ -199,6 +204,36 @@ public String toString() {
}
}

public static class SignInResult {
@NonNull
public final byte[] publicKey;
@NonNull
public final byte[] signedMessage;
@NonNull
public final byte[] signature;
@NonNull
public final String signatureType;

public SignInResult(@NonNull byte[] publicKey, @NonNull byte[] signedMessage,
@NonNull byte[] signature, @NonNull String signatureType) {
this.publicKey = publicKey;
this.signedMessage = signedMessage;
this.signature = signature;
this.signatureType = signatureType;
}

@NonNull
@Override
public String toString() {
return "SignInResult{" +
"publicKey=" + Arrays.toString(publicKey) +
", signedMessage=" + Arrays.toString(signedMessage) +
", signature=" + Arrays.toString(signature) +
", signatureType='" + signatureType + '\'' +
'}';
}
}

@Deprecated @TestOnly @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static AuthorizationResult create(
String authToken,
Expand All @@ -207,7 +242,7 @@ public static AuthorizationResult create(
Uri walletUriBase
) {
AuthorizedAccount[] accounts = new AuthorizedAccount[] { new AuthorizedAccount(publicKey, accountLabel, null, null) };
return new AuthorizationResult(authToken, accounts, walletUriBase);
return new AuthorizationResult(authToken, accounts, walletUriBase, null);
}

@TestOnly @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
Expand All @@ -216,7 +251,7 @@ public static AuthorizationResult create(
AuthorizedAccount[] accounts,
Uri walletUriBase
) {
return new AuthorizationResult(authToken, accounts, walletUriBase);
return new AuthorizationResult(authToken, accounts, walletUriBase, null);
}
}

Expand Down Expand Up @@ -291,7 +326,36 @@ protected AuthorizationResult processResult(@Nullable Object o)
walletUriBaseStr + "'; expected a 'https' URI");
}

return new AuthorizationResult(authToken, authorizedAccounts, walletUriBase);
final JSONObject signInResultJson = jo.has(ProtocolContract.RESULT_SIGN_IN) ?
jo.optJSONObject(ProtocolContract.RESULT_SIGN_IN) : null;
final AuthorizationResult.SignInResult signInResult;
if (signInResultJson != null) {
final String address = signInResultJson.optString(ProtocolContract.RESULT_SIGN_IN_ADDRESS);
if (address.isEmpty()) {
throw new JsonRpc20InvalidResponseException("expected an address in sign_in_result");
}
final byte[] publicKey = JsonPack.unpackBase64PayloadToByteArray(address);

final String signedMessageStr = signInResultJson.optString(ProtocolContract.RESULT_SIGN_IN_SIGNED_MESSAGE);
if (signedMessageStr.isEmpty()) {
throw new JsonRpc20InvalidResponseException("expected an address in sign_in_result");
}
final byte[] signedMessage = JsonPack.unpackBase64PayloadToByteArray(signedMessageStr);

final String signatureStr = signInResultJson.optString(ProtocolContract.RESULT_SIGN_IN_SIGNATURE);
if (signatureStr.isEmpty()) {
throw new JsonRpc20InvalidResponseException("expected an address in sign_in_result");
}
final byte[] signature = JsonPack.unpackBase64PayloadToByteArray(signatureStr);

final String signatureType = signInResultJson.has(ProtocolContract.RESULT_SIGN_IN_SIGNATURE_TYPE) ?
signInResultJson.optString(ProtocolContract.RESULT_SIGN_IN_SIGNATURE_TYPE) : "ed25519";
signInResult = new AuthorizationResult.SignInResult(publicKey, signedMessage, signature, signatureType);
} else {
signInResult = null;
}

return new AuthorizationResult(authToken, authorizedAccounts, walletUriBase, signInResult);
}

@Override
Expand Down Expand Up @@ -344,8 +408,8 @@ public AuthorizationFuture authorize(@Nullable Uri identityUri,
@Nullable String chain,
@Nullable String authToken,
@Nullable String[] features,
@Nullable byte[][] addresses
/* TODO: sign in payload */)
@Nullable byte[][] addresses,
@Nullable SignInWithSolana.Payload signInPayload)
throws IOException {
if (identityUri != null && (!identityUri.isAbsolute() || !identityUri.isHierarchical())) {
throw new IllegalArgumentException("If non-null, identityUri must be an absolute, hierarchical Uri");
Expand All @@ -372,6 +436,9 @@ public AuthorizationFuture authorize(@Nullable Uri identityUri,
authorize.put(ProtocolContract.PARAMETER_AUTH_TOKEN, authToken); // null is OK
authorize.put(ProtocolContract.PARAMETER_FEATURES, featuresArr); // null is OK
authorize.put(ProtocolContract.PARAMETER_ADDRESSES, addressesArr); // null is OK
if (signInPayload != null ) {
authorize.put(ProtocolContract.PARAMETER_SIGN_IN_PAYLOAD, signInPayload.toJson());
}
} catch (JSONException e) {
throw new UnsupportedOperationException("Failed to create authorize JSON params", e);
}
Expand Down
5 changes: 5 additions & 0 deletions android/common/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,9 @@ tasks.whenTaskAdded { task ->

dependencies {
compileOnly 'androidx.annotation:annotation:1.5.0'

testImplementation 'junit:junit:4.13.2'
testImplementation 'androidx.test:core:1.5.0'
testImplementation 'androidx.arch.core:core-testing:2.2.0'
testImplementation 'org.robolectric:robolectric:4.10.3'
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ public class ProtocolContract {

public static final String PARAMETER_AUTH_TOKEN = "auth_token"; // type: String

public static final String PARAMETER_SIGN_IN_PAYLOAD = "sign_in_payload"; // type: String

public static final String PARAMETER_PAYLOADS = "payloads"; // type: JSON array of String (base64-encoded payloads)

public static final String RESULT_AUTH_TOKEN = "auth_token"; // type: String
Expand All @@ -87,6 +89,12 @@ public class ProtocolContract {

public static final String RESULT_WALLET_URI_BASE = "wallet_uri_base"; // type: String (absolute URI)

public static final String RESULT_SIGN_IN = "sign_in_result"; // type JSON object
public static final String RESULT_SIGN_IN_ADDRESS = "address"; // type: String (address)
public static final String RESULT_SIGN_IN_SIGNED_MESSAGE = "signed_message"; // type: String (base64-encoded signed message)
public static final String RESULT_SIGN_IN_SIGNATURE = "signature"; // type: String (base64-encoded signature)
public static final String RESULT_SIGN_IN_SIGNATURE_TYPE = "signature_type"; // type: String

public static final String RESULT_SIGNED_PAYLOADS = "signed_payloads"; // type: JSON array of String (base64-encoded signed payloads)

// Keep these in sync with `mobile-wallet-adapter-protocol/src/errors.ts`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.solana.mobilewalletadapter.common.datetime;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

public class Iso8601DateTime {

static final String ISO_8601_FORMAT_STRING = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
static final String ISO_8601_FORMAT_STRING_NO_ZONE = "yyyy-MM-dd'T'HH:mm:ss.SSS";

public static String now() {
return formatUtc(new Date());
}

public static String formatUtc(Date date) {
SimpleDateFormat format = new SimpleDateFormat(ISO_8601_FORMAT_STRING_NO_ZONE, Locale.US);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
return format.format(date) + "Z";
}

public static Date parse(String iso8601String) throws ParseException {
SimpleDateFormat format = new SimpleDateFormat(ISO_8601_FORMAT_STRING, Locale.US);
try {
String formattedDate = iso8601String;
if (formattedDate.endsWith("Z")) {
// SimpleDateFormat does not comprehend "Z" (UTC), so replace it
formattedDate = formattedDate.replace("Z", "+0000");
} else {
// SimpleDateFormat requires zone to be in +/-hhmm, so remove ":"
formattedDate = formattedDate.replaceAll("([+-]\\d\\d):(\\d\\d)\\s*$", "$1$2");
}
// add microseconds field if missing
formattedDate = formattedDate.replaceAll("(T\\d\\d)(:\\d\\d)(:\\d\\d)([+-])", "$1$2$3.000$4");

return format.parse(formattedDate);
} catch (ParseException e) {
throw new ParseException("Failed to parse input as ISO 8601", e.getErrorOffset());
}
}

private Iso8601DateTime() {}
}
Loading

0 comments on commit f3f5739

Please sign in to comment.