Skip to content

Commit

Permalink
add AuthHeaderSigner
Browse files Browse the repository at this point in the history
  • Loading branch information
acharb committed Apr 9, 2024
1 parent 55d39aa commit 3b8c5f1
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 1 deletion.
3 changes: 3 additions & 0 deletions @stellar/typescript-wallet-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,14 @@
"dependencies": {
"@stellar/stellar-sdk": "^11.1.0",
"axios": "^1.4.0",
"base64url": "^3.0.1",
"https-browserify": "^1.0.0",
"jws": "^4.0.0",
"lodash": "^4.17.21",
"query-string": "^7.1.3",
"stream-http": "^3.2.0",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"url": "^0.11.0",
"util": "^0.12.5",
"utility-types": "^3.10.0",
Expand Down
5 changes: 5 additions & 0 deletions @stellar/typescript-wallet-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export { Anchor } from "./walletSdk/Anchor";
export { Sep24 } from "./walletSdk/Anchor/Sep24";
export { IssuedAssetId, NativeAssetId, FiatAssetId } from "./walletSdk/Asset";
export { Sep10, WalletSigner, DefaultSigner } from "./walletSdk/Auth";
export {
AuthHeaderSigner,
DefaultAuthHeaderSigner,
DomainAuthHeaderSigner,
} from "./walletSdk/Auth/AuthHeaderSigner";
export {
AccountKeypair,
PublicKeypair,
Expand Down
158 changes: 158 additions & 0 deletions @stellar/typescript-wallet-sdk/src/walletSdk/Auth/AuthHeaderSigner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { AxiosInstance } from "axios";
import { StrKey } from "@stellar/stellar-sdk";
import nacl from "tweetnacl";
import naclUtil from "tweetnacl-util";
import base64url from "base64url";

import { SigningKeypair } from "../Horizon/Account";
import { DefaultClient } from "../";
import { AuthHeaderClaims, AuthHeaderCreateTokenParams } from "../Types";
import {
AuthHeaderSigningKeypairRequiredError,
AuthHeaderClientDomainRequiredError,
} from "../Exceptions";

export interface AuthHeaderSigner {
createToken({
claims,
clientDomain,
issuer,
}: AuthHeaderCreateTokenParams): Promise<string>;
}

/**
* Signer for signing JWT for GET /Auth with a custodial private key
*
* @class
*/
export class DefaultAuthHeaderSigner implements AuthHeaderSigner {
expiration: number;

constructor(expiration: number = 900) {
this.expiration = expiration;
}

/**
* Create a signed JWT for the auth header
* @constructor
* @param {AuthHeaderCreateTokenParams} params - The create token parameters
* @param {AuthHeaderClaims} params.claims - the data to be signed in the JWT
* @param {string} [params.clientDomain] - the client domain hosting SEP-1 toml
* @param {AccountKeypair} [params.issuer] - the account signing the JWT
* @returns {Promise<string>} The signed JWT
*/
// eslint-disable-next-line @typescript-eslint/require-await
async createToken({
claims,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
clientDomain,
issuer,
}: AuthHeaderCreateTokenParams): Promise<string> {
if (!(issuer instanceof SigningKeypair)) {
throw new AuthHeaderSigningKeypairRequiredError();
}

const issuedAt = Math.floor(Date.now() / 1000);
const timeExp = Math.floor(Date.now() / 1000) + this.expiration;

const rawSeed = StrKey.decodeEd25519SecretSeed(issuer.secretKey);
const naclKP = nacl.sign.keyPair.fromSeed(rawSeed);

const header = { alg: "EdDSA", typ: "JWT" };
const encodedHeader = base64url(JSON.stringify(header));
const encodedPayload = base64url(
JSON.stringify({ ...claims, exp: timeExp, iat: issuedAt }),
);

const signature = nacl.sign.detached(
naclUtil.decodeUTF8(`${encodedHeader}.${encodedPayload}`),
naclKP.secretKey,
);
const encodedSignature = base64url(Buffer.from(signature));

const jwt = `${encodedHeader}.${encodedPayload}.${encodedSignature}`;
return jwt;
}
}

/**
* Signer for signing JWT for GET /Auth using a remote server to sign.
*
* @class
*/
export class DomainAuthHeaderSigner implements AuthHeaderSigner {
signerUrl: string;
expiration: number;
httpClient: AxiosInstance;

constructor(
signerUrl: string,
expiration: number = 900,
httpClient?: AxiosInstance,
) {
this.signerUrl = signerUrl;
this.expiration = expiration;
this.httpClient = httpClient || DefaultClient;
}

/**
* Create a signed JWT for the auth header by using a remote server to sign the JWT
* @constructor
* @param {AuthHeaderCreateTokenParams} params - The create token parameters
* @param {AuthHeaderClaims} params.claims - the data to be signed in the JWT
* @param {string} [params.clientDomain] - the client domain hosting SEP-1 toml
* @param {AccountKeypair} [params.issuer] - unused, will not be used to sign
* @returns {Promise<string>} The signed JWT
*/
async createToken({
claims,
clientDomain,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
issuer,
}: AuthHeaderCreateTokenParams): Promise<string> {
if (!clientDomain) {
throw new AuthHeaderClientDomainRequiredError();
}

const issuedAt = Math.floor(Date.now() / 1000);
const expiration = Math.floor(Date.now() / 1000) + this.expiration;

return await this.signTokenRemote({
claims,
clientDomain,
expiration,
issuedAt,
});
}

/**
* Sign JWT by calling a remote server
* @constructor
* @param {SignTokenRemoteParams} params - the sign token params
* @param {AuthHeaderClaims} params.claims - the data to be signed in the JWT
* @param {string} params.clientDomain - the client domain hosting SEP-1 toml
* @param {number} params.expiration - when the token should expire
* @param {number} params.issuedAt - when the token was created
* @returns {Promise<string>} The signed JWT
*/
async signTokenRemote({
claims,
clientDomain,
expiration,
issuedAt,
}: {
claims: AuthHeaderClaims;
clientDomain: string;
expiration: number;
issuedAt: number;
}): Promise<string> {
const resp = await this.httpClient.post(this.signerUrl, {
clientDomain,
expiration,
issuedAt,
...claims,
});

return resp.data.token;
}
}
48 changes: 47 additions & 1 deletion @stellar/typescript-wallet-sdk/src/walletSdk/Auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import {
ChallengeParams,
ChallengeResponse,
SignParams,
AuthHeaderClaims,
} from "../Types";
import { AccountKeypair } from "../Horizon/Account";
import { AuthHeaderSigner } from "./AuthHeaderSigner";

export { WalletSigner, DefaultSigner } from "./WalletSigner";

Expand Down Expand Up @@ -71,11 +74,13 @@ export class Sep10 {
walletSigner,
memoId,
clientDomain,
authHeaderSigner,
}: AuthenticateParams): Promise<AuthToken> {
const challengeResponse = await this.challenge({
accountKp,
memoId,
clientDomain: clientDomain || this.cfg.app.defaultClientDomain,
authHeaderSigner,
});
const signedTransaction = await this.sign({
accountKp,
Expand All @@ -90,6 +95,7 @@ export class Sep10 {
accountKp,
memoId,
clientDomain,
authHeaderSigner,
}: ChallengeParams): Promise<ChallengeResponse> {
if (memoId && parseInt(memoId) < 0) {
throw new InvalidMemoError();
Expand All @@ -104,8 +110,29 @@ export class Sep10 {
}${clientDomain ? `&client_domain=${clientDomain}` : ""}${
this.homeDomain ? `&home_domain=${this.homeDomain}` : ""
}`;

const claims = {
account: accountKp.publicKey,
home_domain: this.homeDomain,
memo: memoId,
client_domain: clientDomain,
webAuthEndpoint: this.webAuthEndpoint,
};

const token = await createAuthSignToken(
accountKp,
claims,
clientDomain,
authHeaderSigner,
);

let headers = {};
if (token) {
headers = { Authorization: `Bearer ${token}` };
}

try {
const resp = await this.httpClient.get(url);
const resp = await this.httpClient.get(url, { headers });
const challengeResponse: ChallengeResponse = resp.data;
return challengeResponse;
} catch (e) {
Expand Down Expand Up @@ -165,3 +192,22 @@ const validateToken = (token: string) => {
throw new ExpiredTokenError(parsedToken.expiresAt);
}
};

const createAuthSignToken = async (
account: AccountKeypair,
claims: AuthHeaderClaims,
clientDomain?: string,
authHeaderSigner?: AuthHeaderSigner,
) => {
if (!authHeaderSigner) {
return null;
}

const issuer = clientDomain ? null : account;

return authHeaderSigner.createToken({
claims,
clientDomain,
issuer,
});
};
19 changes: 19 additions & 0 deletions @stellar/typescript-wallet-sdk/src/walletSdk/Exceptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,3 +305,22 @@ export class InvalidJsonError extends Error {
Object.setPrototypeOf(this, InvalidJsonError.prototype);
}
}

export class AuthHeaderSigningKeypairRequiredError extends Error {
constructor() {
super("Must be SigningKeypair to sign auth header");
Object.setPrototypeOf(
this,
AuthHeaderSigningKeypairRequiredError.prototype,
);
}
}

export class AuthHeaderClientDomainRequiredError extends Error {
constructor() {
super(
"This class should only be used for remote signing. For local signing use DefaultAuthHeaderSigner.",
);
Object.setPrototypeOf(this, AuthHeaderClientDomainRequiredError.prototype);
}
}
17 changes: 17 additions & 0 deletions @stellar/typescript-wallet-sdk/src/walletSdk/Types/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { decode } from "jws";

import { WalletSigner } from "../Auth/WalletSigner";
import { AccountKeypair, SigningKeypair } from "../Horizon/Account";
import { AuthHeaderSigner } from "../Auth/AuthHeaderSigner";

export type AuthenticateParams = {
accountKp: AccountKeypair;
walletSigner?: WalletSigner;
memoId?: string;
clientDomain?: string;
authHeaderSigner?: AuthHeaderSigner;
};

export class AuthToken {
Expand Down Expand Up @@ -49,6 +51,7 @@ export type ChallengeParams = {
accountKp: AccountKeypair;
memoId?: string;
clientDomain?: string;
authHeaderSigner?: AuthHeaderSigner;
};

export type XdrEncodedTransaction = string;
Expand Down Expand Up @@ -91,3 +94,17 @@ export type SignChallengeTxnResponse = {
transaction: XdrEncodedTransaction;
networkPassphrase: NetworkPassphrase;
};

export type AuthHeaderClaims = {
account: string;
home_domain: string;
webAuthEndpoint: string;
memo?: string;
client_domain?: string;
};

export type AuthHeaderCreateTokenParams = {
claims: AuthHeaderClaims;
clientDomain?: string;
issuer?: AccountKeypair;
};
Loading

0 comments on commit 3b8c5f1

Please sign in to comment.