Skip to content
This repository was archived by the owner on Feb 8, 2024. It is now read-only.

Commit

Permalink
Add support for client domain and additional signatures when fetching…
Browse files Browse the repository at this point in the history
… auth token. (#338)
  • Loading branch information
fnando authored May 2, 2023
1 parent 840c3d0 commit 57db46c
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 17 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## In master

- Add support for client domain when fetching an auth token.
- Add support for adding additional signatures when fetching an auth token.

## [v0.9.0](https://github.com/stellar/js-stellar-wallets/compare/v0.8.0...v0.9.0)

- Make sure "kind" param works on fetchTransactions
Expand All @@ -10,13 +13,16 @@

## [v0.7.0-rc.0](https://github.com/stellar/js-stellar-wallets/compare/v0.6.0-rc.1...v0.7.0-rc.0)

This release updates the SDK to accommodate latest changes from [SEP-24 spec](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#changelog):
This release updates the SDK to accommodate latest changes from
[SEP-24 spec](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#changelog):

- [v2.4.0](https://github.com/stellar/stellar-protocol/pull/1195)
- [v2.3.0](https://github.com/stellar/stellar-protocol/pull/1191)
- [v2.2.1](https://github.com/stellar/stellar-protocol/pull/1185)
- [v2.2.0](https://github.com/stellar/stellar-protocol/pull/1128)

All changes:

- Add support for optional `lang` parameter when fetching transactions
- Add `pending_user_transfer_complete` and `refunded` transaction statuses
- Add `refunds` object to transaction interface
Expand Down
210 changes: 209 additions & 1 deletion src/KeyManager.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { mockRandomForEach } from "jest-mock-random";
import randomBytes from "randombytes";
import sinon from "sinon";
import StellarBase, { Operation } from "stellar-base";
import StellarBase, { Operation, Transaction } from "stellar-base";

import { KeyType } from "./constants/keys";
import { KeyManager } from "./KeyManager";
Expand Down Expand Up @@ -387,6 +387,214 @@ describe("KeyManager", function() {
authServerHomeDomains: ["stellar.org"],
});

// @ts-ignore accessing mock property
const xdr: string = JSON.parse(fetch.mock.calls[1][1].body).transaction;
const submittedTx = new Transaction(xdr, keyNetwork);

expect(submittedTx.signatures.length).toEqual(2);
expect(res).toBe(token);
});

test("Sets client domain when fetching challenges", async () => {
const authServer = "https://www.stellar.org/auth";
const password = "very secure password";

const keyNetwork = StellarBase.Networks.TESTNET;

const token = "👍";
const accountKey = StellarBase.Keypair.random();
const account = new StellarBase.Account(accountKey.publicKey(), "-1");

// set up the manager
const testStore = new MemoryKeyStore();
const testKeyManager = new KeyManager({
keyStore: testStore,
});

testKeyManager.registerEncrypter(IdentityEncrypter);

const keypair = StellarBase.Keypair.master(keyNetwork);

// A Base64 digit represents 6 bits, to generate a random 64 bytes
// base64 string, we need 48 random bytes = (64 * 6)/8
//
// Each Base64 digit is in ASCII and each ASCII characters when
// turned into binary represents 8 bits = 1 bytes.
const value = randomBytes(48).toString("base64");

const tx = new StellarBase.TransactionBuilder(account, {
fee: StellarBase.BASE_FEE,
networkPassphrase: keyNetwork,
})
.addOperation(
Operation.manageData({
name: `stellar.org auth`,
value,
source: keypair.publicKey(),
}),
)
.addOperation(
Operation.manageData({
name: "web_auth_domain",
value: new URL(authServer).hostname,
source: account.accountId(),
}),
)
.setTimeout(300)
.build();

tx.sign(accountKey);

fetch
// @ts-ignore
.mockResponseOnce(
JSON.stringify({
transaction: tx.toXDR(),
network_passphrase: keyNetwork,
}),
)
// @ts-ignore
.mockResponseOnce(
JSON.stringify({
token,
status: 1,
message: "Good job friend",
}),
);

// save this key
const keyMetadata = await testKeyManager.storeKey({
key: {
type: KeyType.plaintextKey,
publicKey: keypair.publicKey(),
privateKey: keypair.secret(),
network: keyNetwork,
},
password,
encrypterName: "IdentityEncrypter",
});

await testKeyManager.fetchAuthToken({
id: keyMetadata.id,
password,
authServer,
authServerKey: account.accountId(),
authServerHomeDomains: ["stellar.org"],
clientDomain: "example.com",
});

expect(fetch).toHaveBeenNthCalledWith(
1,
"https://www.stellar.org/auth?account=" +
"GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H" +
"&client_domain=example.com",
);
});

test("Sets additional signature", async () => {
const authServer = "https://www.stellar.org/auth";
const password = "very secure password";

const keyNetwork = StellarBase.Networks.TESTNET;

const token = "👍";
const accountKey = StellarBase.Keypair.random();
const account = new StellarBase.Account(accountKey.publicKey(), "-1");

// set up the manager
const testStore = new MemoryKeyStore();
const testKeyManager = new KeyManager({
keyStore: testStore,
});

testKeyManager.registerEncrypter(IdentityEncrypter);

const keypair = StellarBase.Keypair.master(keyNetwork);
const otherKeypair = StellarBase.Keypair.master(keyNetwork);

// A Base64 digit represents 6 bits, to generate a random 64 bytes
// base64 string, we need 48 random bytes = (64 * 6)/8
//
// Each Base64 digit is in ASCII and each ASCII characters when
// turned into binary represents 8 bits = 1 bytes.
const value = randomBytes(48).toString("base64");

const tx = new StellarBase.TransactionBuilder(account, {
fee: StellarBase.BASE_FEE,
networkPassphrase: keyNetwork,
})
.addOperation(
Operation.manageData({
name: `stellar.org auth`,
value,
source: keypair.publicKey(),
}),
)
.addOperation(
Operation.manageData({
name: "web_auth_domain",
value: new URL(authServer).hostname,
source: account.accountId(),
}),
)
.setTimeout(300)
.build();

tx.sign(accountKey);

fetch
// @ts-ignore
.mockResponseOnce(
JSON.stringify({
transaction: tx.toXDR(),
network_passphrase: keyNetwork,
}),
)
// @ts-ignore
.mockResponseOnce(
JSON.stringify({
token,
status: 1,
message: "Good job friend",
}),
);

// save this key
const keyMetadata = await testKeyManager.storeKey({
key: {
type: KeyType.plaintextKey,
publicKey: keypair.publicKey(),
privateKey: keypair.secret(),
network: keyNetwork,
},
password,
encrypterName: "IdentityEncrypter",
});

const res = await testKeyManager.fetchAuthToken({
id: keyMetadata.id,
password,
authServer,
authServerKey: account.accountId(),
authServerHomeDomains: ["stellar.org"],
onChallengeTransactionSignature: (txx) => {
txx.sign(otherKeypair);

return Promise.resolve(txx);
},
});

// @ts-ignore accessing mock property
const xdr: string = JSON.parse(fetch.mock.calls[1][1].body).transaction;
const submittedTx = new Transaction(xdr, keyNetwork);

expect(submittedTx.signatures.length).toEqual(3);

const verified = submittedTx.signatures.some((sig) =>
otherKeypair.verify(tx.hash(), sig.signature()),
);

expect(verified).toBeTruthy();
expect(res).toBe(token);
});

Expand Down
45 changes: 30 additions & 15 deletions src/KeyManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,12 +281,18 @@ export class KeyManager {
* computed as `sha1(private key + public key)`.
* @param {string} params.password The password that will decrypt that secret
* @param {string} params.authServer The URL of the authentication server
* @param {array} params.authServerHomeDomains The home domain(s) of the authentication server
* @param {array} params.authServerHomeDomains The home domain(s) of the
* authentication server
* @param {string} params.authServerKey Check the challenge transaction
* for this key as source and signature.
* for this key as source and signature.
* @param {string} params.clientDomain a domain hosting a SEP-1 stellar.toml
* containing a SIGNING_KEY used for verifying the client domain.
* @param {function} params.onChallengeTransactionSignature When
* `params.clientDomain` is set, you need to provide a function that will add
* the signature identified by the SIGNING_KEY present on your client domain's
* toml file.
* @param {string} [params.account] The authenticating public key. If not
* provided, then the signers's public key will
* be used instead.
* provided, then the signers's public key will be used instead.
* @returns {Promise<string>} authToken JWT
*/
// tslint:enable max-line-length
Expand All @@ -297,7 +303,11 @@ export class KeyManager {
authServer,
authServerKey,
authServerHomeDomains,
clientDomain,
onChallengeTransactionSignature = (tx: Transaction) =>
Promise.resolve(tx),
} = params;

let { account } = params;

// throw errors for missing params
Expand Down Expand Up @@ -337,9 +347,13 @@ export class KeyManager {
// account.
account = account || key.publicKey;

const challengeRes = await fetch(
`${authServer}?account=${encodeURIComponent(account)}`,
);
let challengeUrl = `${authServer}?account=${encodeURIComponent(account)}`;

if (clientDomain) {
challengeUrl += `&client_domain=${encodeURIComponent(clientDomain)}`;
}

const challengeRes = await fetch(challengeUrl);

if (challengeRes.status !== 200) {
const challengeText = await challengeRes.text();
Expand Down Expand Up @@ -371,27 +385,28 @@ export class KeyManager {
keyNetwork !== json.network_passphrase
) {
throw new Error(
`
Network mismatch: the transfer server expects "${
json.network_passphrase
}",
but you're using "${keyNetwork}"
`,
`Network mismatch: the transfer server expects "${
json.network_passphrase
}", but you're using "${keyNetwork}"`,
);
}

const firstTransaction = Utils.readChallengeTx(
let transaction = Utils.readChallengeTx(
json.transaction,
authServerKey,
keyNetwork,
authServerHomeDomains,
new URL(authServer).hostname,
).tx;

// Add extra signatures.
// By default, the input transaction is returned as it is.
transaction = await onChallengeTransactionSignature(transaction);

const keyHandler = this.keyHandlerMap[key.type];

const signedTransaction = await keyHandler.signTransaction({
transaction: firstTransaction,
transaction,
key,
});

Expand Down
2 changes: 2 additions & 0 deletions src/types/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,6 @@ export interface GetAuthTokenParams {
authServerHomeDomains: [string];
authServerKey: string;
account?: string;
clientDomain?: string;
onChallengeTransactionSignature?: (tx: Transaction) => Promise<Transaction>;
}

0 comments on commit 57db46c

Please sign in to comment.