Skip to content

Commit

Permalink
Merged PR 4324: Integration of the Aleo wallet in Boloney!
Browse files Browse the repository at this point in the history
- Removed old authentication flow
- Authenticating with Aleo wallet
- Creating Nakama session with Aleo wallet
- Refreshing connection to Aleo wallet and Nakama session

Figma designs (don't mind the italic font): https://www.figma.com/file/UB1htt9Flaql9UyxjsXnvb/Boloney!?type=design&node-id=7420-110178

QA:

First things first, pull `verify-account` branch of the toolkit and run it locally with `yarn start`

0. Make sure to install and login with Aleo wallet browser extension
1. Run `skaffold delete` to remove the old db
2. Wait a few seconds
3. Run `skaffold run`
4. Go to the auth page and try to create an account
5. After you are logged in try refreshing the page to make sure the session is preserved
6. Log out
7. Go to the auth page and try to authenticate. This time you should not be asked for a username

Related work items: #19486, #19487, #19500
  • Loading branch information
Mautjee authored and silimarius committed Jun 12, 2023
1 parent 1fb5d95 commit 4f92290
Show file tree
Hide file tree
Showing 56 changed files with 636 additions and 362 deletions.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
7 changes: 7 additions & 0 deletions backend/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const MATCH_STAGES: readonly MatchStage[] = [
export const TOOLKIT_ENDPOINTS = {
account: {
create: "/account/create",
verify: "/account/verify",
},
match: {
create: "/boloney/create-match",
Expand Down Expand Up @@ -129,6 +130,12 @@ export const STORAGE_ACCOUNT_COLLECTION = "accounts";
export const STORAGE_ADDRESS_KEY = "aleo-address";
export const STORAGE_KEYS_KEY = "aleo-keys";

export const ADDRESSES_COLLECTION = "addresses";

export const PUBLIC_USER_ID = "00000000-0000-0000-0000-000000000000";

export const AUTH_SIGN_MESSAGE = "zeroknowledgeisbeautiful";

export const MAX_TOOLKIT_REQUESTS_ATTEMPTS = 3;
export const MAX_ROLL_BACK_ATTEMPTS = 3;

Expand Down
76 changes: 56 additions & 20 deletions backend/src/hooks/auth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import { errors, handleError, profanityFilter, savePlayerAddress, savePlayerKeys, getPlayerAddress } from "../services";
import { AUTH_SIGN_MESSAGE, TOOLKIT_ENDPOINTS } from "../constants";
import {
errors,
handleError,
savePlayerAddress,
savePlayerKeys,
getPlayerAddress,
getUsername,
saveUsername,
profanityFilter,
} from "../services";
import { handleToolkitRequest } from "../toolkit-api";
import { createAleoAccount } from "../toolkit-api/account";
import { AfterAuthHookHandler, BeforeAuthHookHandler, isAddress } from "../types";
import { env } from "../utils";
import { AfterAuthHookHandler, BeforeAuthHookHandler, isAddress, isVerifySignatureResToolkit, VerifySignatureBodyToolkit } from "../types";
import { env, tkUrl } from "../utils";

export const beforeHookHandler: BeforeAuthHookHandler = (cb) => (ctx, logger, nk, data) => {
try {
Expand All @@ -21,34 +32,59 @@ export const afterHookHandler: AfterAuthHookHandler = (cb) => (ctx, logger, nk,
}
};

// TODO: fix the following scenario:
// 1. user creates account
// 2. before hook succeeds and user creation as well
// 3. after hook fails, so no keys are stored
// The after hook will be retriggered after login, so the server will try to recall the toolkit if keys were not generated.
// The issue is in the fact that if the key creation fails, the user will receive an error, even though the nakama account is actually created.
// In order to generate the keys, the user will have to counterintuitively try to login.
export const verifySignature = (
nk: nkruntime.Nakama,
ctx: nkruntime.Context,
logger: nkruntime.Logger,
address: string,
signature: string
): boolean => {
const url = tkUrl(ctx, TOOLKIT_ENDPOINTS.account.verify);
const body: VerifySignatureBodyToolkit = { message: AUTH_SIGN_MESSAGE, playerSign: signature, pubAddress: address };
const res = handleToolkitRequest(url, "post", body, nk, logger);
const parsedBody = JSON.parse(res.body);
return isVerifySignatureResToolkit(parsedBody) && parsedBody.verified;
};

export const beforeAuthenticateCustom = beforeHookHandler((_ctx, _logger, nk, data) => {
export const beforeAuthenticateCustom = beforeHookHandler((ctx, logger, nk, data) => {
if (!data.username || !data.account?.id) throw errors.noUsernamePasswordProvided;

data.username = data.username.toLowerCase();
const isRegistering = !!data.create;
const username: string = data.username;
const password: string = data.account.id;
const splitId = data.account.id.split(";");
const address: string = data.username;

const storedUsername = getUsername(nk, address);

if (!splitId.length) throw errors.noUsernamePasswordProvided;
if (!storedUsername && splitId.length === 1) throw errors.notFound;

const userExists = isRegistering && nk.usersGetUsername([username]).length;
if (storedUsername) {
// log in
data.create = false;
const signature: string = splitId[0];
if (!verifySignature(nk, ctx, logger, address, signature)) throw errors.invalidSignature;
data.username = storedUsername;
} else if (splitId.length === 2) {
// registration
data.create = true;

if (userExists) throw errors.usernameAlreadyExists;
const signature: string = splitId[0];
const username: string = splitId[1].toLowerCase();

if (profanityFilter.isProfane(username)) throw errors.usernameContainsProfanity;
if (!verifySignature(nk, ctx, logger, address, signature)) throw errors.invalidSignature;
if (profanityFilter.isProfane(username)) throw errors.containsProfanity;

saveUsername(nk, address, username);
data.username = username;
} else {
throw errors.invalidPayload;
}

const encryptedKey = String(nk.sha256Hash(password + username));
data.account.id = encryptedKey;
data.account.id = String(nk.sha256Hash(address));

return data;
});

// TODO: delete the following hook after moving program calls to client
export const afterAuthenticateCustom = afterHookHandler((ctx, logger, nk, _data, _request) => {
if (!env(ctx).ZK_ENABLED) return;

Expand Down
3 changes: 1 addition & 2 deletions backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
matchSignal,
matchTerminate,
} from "./game-modes/standard";
import { rollDice, createMatch, findMatch, rtBeforeChannelMessageSend } from "./rpc";
import { createMatch, findMatch, rtBeforeChannelMessageSend } from "./rpc";
import { MatchState } from "./types";

function InitModule(_ctx: nkruntime.Context, logger: nkruntime.Logger, _nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
Expand All @@ -31,7 +31,6 @@ function InitModule(_ctx: nkruntime.Context, logger: nkruntime.Logger, _nk: nkru
initializer.registerMatchmakerMatched(matchmakerMatched);

// rpc registration
initializer.registerRpc("roll_dice", rollDice);
initializer.registerRpc("create_match", createMatch);
initializer.registerRpc("find_match", findMatch);

Expand Down
38 changes: 0 additions & 38 deletions backend/src/rpc/dice.ts

This file was deleted.

1 change: 0 additions & 1 deletion backend/src/rpc/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from "./dice";
export * from "./create-match";
export * from "./find-match";
export * from "./chat";
20 changes: 13 additions & 7 deletions backend/src/services/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,17 @@ const loggerText = {
rollingBack: "Rolling back match state",
};

// TODO: These should not be sentences, but codes. It is the client's concern to parse those and write a sentence.
export const errorText: Record<ErrorKind, string> = {
usernameAlreadyExists: "Username already exists",
usernameContainsProfanity: "Username contains profanity",
alreadyExists: "alreadyExists",
containsProfanity: "containsProfanity",
noUsernamePasswordProvided: "No username/password provided",
invalidSignature: "Invalid signature",
noIdInContext: "No user ID in context",
noPayload: "No payload provided",
invalidPayload: "Invalid payload",
invalidMetadata: "Invalid metadata",
notFound: "Not found",
notFound: "notFound",
internal: "internalError",
};

Expand Down Expand Up @@ -197,16 +199,20 @@ export const sendError = ({ dispatcher, logger }: MatchLoopParams, sender: nkrun
};

export const errors: Record<ErrorKind, nkruntime.Error> = {
usernameAlreadyExists: {
message: errorText.usernameAlreadyExists,
alreadyExists: {
message: errorText.alreadyExists,
code: nkruntime.Codes.ALREADY_EXISTS,
},
noUsernamePasswordProvided: {
message: errorText.noUsernamePasswordProvided,
code: nkruntime.Codes.INVALID_ARGUMENT,
},
usernameContainsProfanity: {
message: errorText.usernameContainsProfanity,
invalidSignature: {
message: errorText.invalidSignature,
code: nkruntime.Codes.PERMISSION_DENIED,
},
containsProfanity: {
message: errorText.containsProfanity,
code: nkruntime.Codes.INVALID_ARGUMENT,
},
noIdInContext: {
Expand Down
24 changes: 23 additions & 1 deletion backend/src/services/storage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
// Storage services
import { STORAGE_ACCOUNT_COLLECTION, STORAGE_ADDRESS_KEY, STORAGE_KEYS_KEY } from "../constants";
import { ADDRESSES_COLLECTION, PUBLIC_USER_ID, STORAGE_ACCOUNT_COLLECTION, STORAGE_ADDRESS_KEY, STORAGE_KEYS_KEY } from "../constants";
import { AleoKeys, isViewKey, isPrivateKey, AleoAccount } from "../types";

export const saveUsername = (nk: nkruntime.Nakama, address: string, username: string): void => {
const writeRequest: nkruntime.StorageWriteRequest[] = [
{
collection: ADDRESSES_COLLECTION,
key: address,
userId: PUBLIC_USER_ID,
value: { username },
permissionRead: 1,
permissionWrite: 1,
},
];

nk.storageWrite(writeRequest);
};

export const getUsername = (nk: nkruntime.Nakama, address: string): string | undefined => {
const readRequest: nkruntime.StorageReadRequest[] = [{ collection: ADDRESSES_COLLECTION, key: address, userId: PUBLIC_USER_ID }];
const response = nk.storageRead(readRequest);

return response.at(0)?.value.username;
};

export const savePlayerAddress = (nk: nkruntime.Nakama, playerId: string, address: string): void => {
const writeRequest: nkruntime.StorageWriteRequest[] = [
{
Expand Down
5 changes: 3 additions & 2 deletions backend/src/types/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { isNumber, isString } from "./primitive";
import { StatusCodes } from "./status-codes";

export type ErrorKind =
| "usernameAlreadyExists"
| "alreadyExists"
| "noUsernamePasswordProvided"
| "usernameContainsProfanity"
| "invalidSignature"
| "containsProfanity"
| "noIdInContext"
| "noPayload"
| "invalidPayload"
Expand Down
15 changes: 15 additions & 0 deletions backend/src/types/toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ export interface DiceDataToolkit {
dice_10: number;
}

export interface VerifySignatureBodyToolkit {
message: string;
playerSign: string;
pubAddress: string;
}

export interface VerifySignatureResToolkit {
verified: boolean;
}

export const isVerifySignatureResToolkit = (value: unknown): value is VerifySignatureResToolkit => {
const assertedVal = value as VerifySignatureResToolkit;
return assertedVal.verified !== undefined && typeof assertedVal.verified === "boolean";
};

export interface UseBirdsEyeBodyToolkit {
powerUp: PowerUpToolkit;
diceData: DiceDataToolkit;
Expand Down
4 changes: 4 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"test": "jest"
},
"dependencies": {
"@demox-labs/aleo-wallet-adapter-base": "^0.0.13",
"@demox-labs/aleo-wallet-adapter-leo": "^0.0.12",
"@demox-labs/aleo-wallet-adapter-react": "^0.0.13",
"@demox-labs/aleo-wallet-adapter-reactui": "^0.0.25",
"@emotion/react": "^11.9.3",
"@emotion/styled": "^11.9.3",
"@heroiclabs/nakama-js": "^2.6.1",
Expand Down
29 changes: 18 additions & 11 deletions frontend/src/assets/text/auth-form.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
import { MINIMUM_PASSWORD_LENGTH, MINIMUM_USERNAME_LENGTH } from "../../constants";
import { MINIMUM_USERNAME_LENGTH } from "../../constants";

export const authForm = {
somethingWentWrong: "something went wrong.",
createAccount: "create account",
welcomeBack: "hello again",
login: "login",
readyToBluff: "ready to Bluff",
whoAreYou: "who goes there? Looks like you don’t have an account yet, so create one to play.",
goodSeeingYouAgain: "good to see you’re fulla Boloney! Sign back in to play.",
iAlreadyHaveAnAccount: "do you have an account? Sign in",
iDontHaveAnAccountYet: "don’t have an account yet? Create one",
register: "register",
signIn: "sign in",
here: "here",
followWalletSteps: "follow the steps on the Aleo Popup to connect your wallet...",
completeWalletSignature: "complete the wallet signature to continue.",
walletCorrectlyConnected: "your Aleo Wallet is correctly connected.",
connectWithWallet: "connect with wallet",
connecting: "connecting",
connected: "connected",
validatingSignature: "validating signature",
signatureValidated: "signature validated",
join: "join",
username: "username",
password: "password",
confirmUsername: "confirm username",
letsRoll: "let's roll!",
errorMessages: {
usernameRequired: "username is required.",
passwordRequired: "password is required.",
usernameMinimum: `username must have at least ${MINIMUM_USERNAME_LENGTH} characters.`,
passwordMinimum: `password must have at least ${MINIMUM_PASSWORD_LENGTH} characters.`,
usernameAlreadyTaken: "username is already taken.",
usernameProfanity: "username contains profanity.",
usernameCharacters: "username contains invalid characters.",
usernamesDoNotMatch: "usernames do not match.",
invalidCredentials: "invalid credentials.",
internal: "internal error. Please try again later.",
},

welcomeBack: (username: string) => `Welcome Back ${username}, seems you're ready to go! `,
welcome: (username: string) => `Welcome ${username}, seems you're ready to go! `,
};
4 changes: 2 additions & 2 deletions frontend/src/components/top-navigation/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ export const MenuDropdown: FC<MenuDropdownProps> = ({ setHover, isActive, setAct
const setModalComponentChildren = useStore((state) => state.setModalComponentChildren);
const { broadcastPlayerLeft } = useMatch();

const handleLogout = () => {
const handleLogout = async () => {
broadcastPlayerLeft();
logout();
await logout();
setActiveDropdown(undefined);
navigate(routes.root);
};
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ export const TEN_SECONDS = 10;
export const GO_BACK = -1;

export const MINIMUM_USERNAME_LENGTH = 2;
export const MINIMUM_PASSWORD_LENGTH = 8;
export const APPEAR_ONLINE = true;
export const CREATE_ACCOUNT = true;

Expand All @@ -71,6 +70,8 @@ export const FLOATING_ANIMATION_SPEED = 4;

export const AUTH_TOKEN_STORAGE_KEY = "auth_token";
export const REFRESH_TOKEN_STORAGE_KEY = "refresh_token";
export const AUTH_SIGN_MESSAGE = "zeroknowledgeisbeautiful";
export const WALLET_APP_NAME = "Boloney!";

// TODO: define these in a separate file
export const RPC_CREATE_MATCH = "create_match";
Expand Down
1 change: 1 addition & 0 deletions frontend/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from "./use-analytics";
export * from "./use-mobile";
export * from "./use-mount";
export * from "./use-update-value";
export * from "./use-query";
11 changes: 11 additions & 0 deletions frontend/src/hooks/use-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useMemo } from "react";
import { useLocation } from "react-router-dom";

/**
* A custom hook that builds on useLocation to parse the query string for you.
*/
export const useQueryParams = () => {
const { search } = useLocation();

return useMemo(() => new URLSearchParams(search), [search]);
};
Loading

0 comments on commit 4f92290

Please sign in to comment.