Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ARC-0031 : Authentication with Algorand accounts #160

Draft
wants to merge 39 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
8de6348
arc-0014 first version
Apr 5, 2022
d33f1ad
arc-0014 update
Apr 5, 2022
34deaba
linters fix
Apr 5, 2022
ecd70d7
review until overview
Apr 7, 2022
5755c11
review until overview (typos)
Apr 7, 2022
26e9e06
ultimated version
Apr 14, 2022
65bac9a
style fix on overview
Apr 14, 2022
2a2e7f2
style fix specs
Apr 14, 2022
f5cdda1
style fix random
Apr 14, 2022
e0cb732
typo fix
Apr 20, 2022
7c705ed
new prefix and fix on txn-auth-msg sender param
May 25, 2022
d91fadc
rename of session design section
Aug 19, 2022
3747928
removing reference to PR#41
Aug 29, 2022
886b0da
Merge branch 'main' into pr/84
SudoWeezy Sep 9, 2022
1f088af
updating to match ARC-0 convention
SudoWeezy Sep 9, 2022
1401c2d
fix broken link
SudoWeezy Sep 9, 2022
ca7075e
removed session-related content
Sep 17, 2022
4fff311
fix typos + grammar
Oct 14, 2022
af20ddd
[WIP] Arc31
Jan 4, 2023
781a40e
arc31 rekeyed accounts
Jan 5, 2023
ea66bb4
minor typo
Jan 5, 2023
cb936e8
finalized arc-31
Jan 11, 2023
0da7597
ARC-0031: Reference Implementation
mrcointreau Jan 12, 2023
1ce6371
ref implementation details
deanstef Jan 12, 2023
5b9bbf6
readme disclamer ref impl
deanstef Jan 12, 2023
151f0fc
ref-impl readme requirements
deanstef Jan 12, 2023
ce6a80a
Remove ref-impl unnecessary package-lock.json files
mrcointreau Jan 12, 2023
33b301e
env vars in dockerfile + nit in api-v1
Jan 13, 2023
e207398
env examples
Jan 13, 2023
f3583e9
Merge pull request #1 from deanstef/reference-implementation
Jan 13, 2023
4fb50de
Merge branch 'algorandfoundation:main' into arc-0031
deanstef Jan 13, 2023
143b735
nit in multisig threshold
Jan 14, 2023
d2bf119
Replace myalgo-conect with @perawallet/connect
mrcointreau Feb 6, 2024
6978019
Merge branch 'algorandfoundation:main' into arc-0031
deanstef Feb 7, 2024
13d861e
Remove unused code
mrcointreau Feb 8, 2024
128b89f
Add api request body validation
mrcointreau Feb 8, 2024
c1a6c8d
Fix notifications icons and colors
mrcointreau Feb 8, 2024
51024c2
Refactor notification system
mrcointreau Feb 8, 2024
c6465fe
Merge pull request #2 from deanstef/feature/perawallet-connect
deanstef Feb 8, 2024
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
269 changes: 269 additions & 0 deletions ARCs/arc-0031.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
---
arc: 31
title: Authentication with Algorand accounts
description: Use Algorand accounts to authenticate with third-party services
author: Stefano De Angelis (@deanstef)
discussions-to: https://github.com/algorandfoundation/ARCs/issues/42
status: Draft
type: Meta
created: 2022-04-05
---

# Authentication with Algorand accounts

A standard for authentication with Algorand accounts.

## Abstract

This ARC introduces a standard for authenticating users with their Algorand accounts. It leverages asymmetric encryption <*PK, SK*> to verify the identity of a user, owner of an Algorand account. This approach fosters the adoption of novel identity management systems for both Web3 and Web2 applications.

### Definitions

- **System**: any frontend/backend application, service provider, or in general an entity not based on blockchain;
- **Credentials**: any type of authentication used by users to access their online accounts, e.g. username/password, PIN, public/secret key pair;
- **Blockchain identity**: a public/secret key pair <*PK, SK*> representing a blockchain account;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe specify here that multisig are supported too.

- **Algorand account**: a blockchain identity on Algorand identified with the key pair <*PKa, SKa*>;
- **Algorand address**: the public key *PKa* of an Algorand account;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically the address is not exactly the public key as there is additional base32 encoding + checksum. I see the distinction is made clear later.

- **User**: an Algorand account holder;
- **Verifier**: a system that needs to verify the identity of a User;
- **dApp**: a decentralized Algorand application that natively runs on the Algorand blockchain, aka "*smart contract*";
- **Wallet**: an off-chain application that stores the secret keys *SKa*s of Algorand accounts and can display and sign transactions for these accounts;
- **message**: a generic string of bytes;
- **digital signature**: a message signed with the private key of a blockchain identity.

## Motivation

In Web3 users interacting with dApps must be authenticated with a blockchain identity (account for Algorand). Having dApps and traditional Web2 systems increasingly more interconnected, it is not difficult to imagine users consuming services both from a dApp and a Web2 application simultaneously. In this case, a single source of authentication should be used to avoid separation between credentials used for dApps and traditional Web2 services.

This ARC provides a standard for users' authentication in Web2 services leveraging Algorand accounts.

## Specification

The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", "**SHALL NOT**", "**SHOULD**", "**SHOULD NOT**", "**RECOMMENDED**", "**MAY**", and "**OPTIONAL**" in this document are to be interpreted as described in <a href="https://www.ietf.org/rfc/rfc2119.txt">RFC-2119</a>
> Comments like this are non-normative.

Interfaces are defined in TypeScript. All the objects that are defined are valid JSON objects, and all JSON string types are UTF-8 encoded.

This ARC uses interchangeably the terms "*blockchain address*", "*public key*", and "*PK*" to indicate the on-chain address of a blockchain identity, and in particular of an Algorand account.

### Overview

This document describes a standard approach to authenticate a User with a blockchain identity. Algorand addresses are used as a *unique identifiers*, and the secret keys to digitally sign *messages* as a proof of identity.

To sum up, given an Algorand account <*PKa, SKa*>, this ARC defines a standards for:

- creating an [ARC-31](./arc-0031.md) compliant digital signature with *SKa*;
- verifying an [ARC-31](./arc-0031.md) compliant digital signature with *PKa*.

### Assumptions

The standard proposed in this document works under the following assumptions:

- User and Verifier communicate over secure SSL/TLS encrypted channels;
- The Verifier knows the Users’ *PKa*;
- For each *PKa* the Verifier generates a unique message to be signed;
- The message MUST change arbitrarily for each authentication request to avoid [replay attacks](https://en.wikipedia.org/wiki/Replay_attack);
- User's secret key is safely kept into a Wallet;
- Users do not change their public address *PKa* for authentication;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this explicitly forbid rekeying? How is rekeying hgandled?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it would be better to reconsider this. IMO, the protocol (and the library) should also handle rekeyed accounts. dApp developers should not need to deal with handling this situation.

I know that this means dropping the offline support, but when thinking about the main use cases of this feature, I think this is a must.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover from the previous ARC-13. This ARC now allows rekey

- Users **MUST** use Algorand compliant keys to sign the messages;
- LogicSigs and Application addresses are not supported;

### Authentication Mechanism

The authentication mechanism defined in this ARC works as follows: a Users sends an authentication request to the Verifier specifying the Algorand account <*PKa, SKa*>.

> Note that Algorand transforms traditional 32-bytes cryptographic keys into more readable and user-friendly objects. A detailed description of such a transformation can be found on the <a href="https://developer.algorand.org/docs/get-details/accounts/#keys-and-addresses">developer portal</a>.

The Verifier responds with a message to be signed with the account's secret key *SKa*. The User queries the Wallet to sign the message. At that stage, the Wallet **MUST** check the message origin with the expected Verifier (to protect Users from [man-in-the-middle attacks](https://en.wikipedia.org/wiki/Man-in-the-middle_attack)). Once the message is signed, the User sends the result back to the Verifier. Finally, the Verifier checks the signature and, if it is all good, authenticates the User.

```mermaid
sequenceDiagram
actor A as Alice
participant W as Wallet
actor B as Bob
A-->>W: Connect to Bob with PKa
activate W
W->>B: GET message for PKa
activate B
B->>B: Create a new msg for PKa
B->>W: msg
deactivate B
W->>W: Check <msg, Bob>
W-->>A: Show msg origin
Note right of A: Confirm signature
A-->>W:
W->>B: <PKa, Sig(msg)>
deactivate W
activate B
B->>B: Verify Sig(msg) with PKa and msg
Note right of B: if Sig(msg) is valid<br/>then authenticate user
B-->>A: Authentication OK/KO
deactivate B
```

The diagram above summarizes the proposed mechanism. We consider the User, **Alice**, owner of an Algorand account <*PKa, SKa*> of which the secret key *SKa* is stored into a **Wallet**.

> A wallet is any type of Algorand wallet, such as hot wallets like <a href="https://www.purestake.com/technology/algosigner/">AlgoSigner</a>, <a href="https://wallet.myalgo.com/">MyAlgo Wallet</a> for browser and mobile wallets used through <a href="https://developer.algorand.org/docs/get-details/walletconnect/">WalletConnect</a>, and cold wallets like the <a href="https://www.ledger.com">Ledger Nano</a>.

Alice authenticates herself to the Verifier **Bob** sending back the digital signature `Sig(msg)` of message `msg` provided by Bob. The mechanism proceeds as follows:

1. Alice initiates an authentication with *PKa* to Bob;
2. Bob generates a message `msg` to be signed by Alice's *SKa*;
3. Alice signs `msg` using her wallet; the Wallet inspects and displays the `msg` origin to be sure it came from Bob;
4. Alice sends back the tuple `<PKa, Sig(msg)>` to Bob;
5. Bob verifies `Sig(msg)` with Alice's *PKa* and `msg`;
6. If the signature is valid, Bob authenticates Alice.

The ARC-31 defines a standardized message for authentication, called *Authentication Message*.

### Authentication Message

An *Authentication Message* `auth-msg` is a sequence of bytes representing a message to be signed. The Verifier requests Users to sign an `auth-msg` with their secret keys. Such a message **MUST** include the following information:

- `domain name`: name of the Verifier domain;
- `Algorand address`: User's *PKa* to be authenticated;
- `nonce`: a unique/random value generated by the Verifier;
- `description`: Verifier details or general description;
- `metadata`: arbitrary message data.

The *Authentication Message* is represented with the following interface:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any length constraints?


```typescript
interface AuthMessage {
/** The domain name of the Verifier */
domain: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the rules for the domain?
For example, if I'm connected to www.example.com, can the domain be example.com? Or do I need to be the exact domain?
I would recommend using exact domain

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should do exact protocol + domain + port

Copy link
Author

@deanstef deanstef Feb 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@0x9090 as specified in the assumptions, the protocol MUST be https; port cannot should not be different from 443.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Careful - https is just the scheme. You can still connect via https on other ports. Requiring TLS is one thing via HTTPS:[...] but the port is independent and shouldn't be prevented from being part of the URI. 8443 is quite common as well as there being arbitrary ports that might be used in development, testing, etc.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for clarifying, Patrick.

What I meant is that in the most cases port will be 443 (it is the most common port for web browsing data transmission). Certainly, other ports can be used over HTTPS, and I agree that we should not prevent them from being part of the URI. As long as we require secure communication channels, we should be good.

/** Algorand account to authenticate with*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** Algorand account to authenticate with*/
/** Algorand account to authenticate with, encoded as an Algorand address */

authAcc: string;
/** Unique random nonce generated by the Verifier */
nonce: string;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just throwing a consideration here. Perhaps we should use challenge instead of nonce.

There are similar auth standards being developed and proposed (DIDAuth, FIDO U2F / HID)

Might be a small thing, but operating close to auth standards might be useful with integrations, complaince, etc.

/** Optional, description of the Verifier */
desc?: string;
/** Optional, metadata */
meta?: string;
}
```

The `auth-msg` **SHOULD** be compliant with [ARC-2](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md), having the parameter `\<dapp-name\>`=`arc31`, for example:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is needed. ARC-2 is for notes in transactions.
Since there is no transaction, I think we should use directly the JSON


```json
arc31:j{
"domain": "www.verifierdomain.com",
"authAcc": "KTGP47G64KCXWJS64W7SGJNKTHE37TYDCI64USXI3XOYE6ZSH4LCI7NIDA",
"nonce": "1234abcde!%",
"desc": "The Verifier",
"meta": "arbitrary attached data",
}
```

The `nonce` field **MUST** be unique for each authentication and **MUST NOT** be used more than once to avoid replay attacks.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minimum recommended length?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

15 characters minimum, which must include upper case and lower case letters and numbers. Nonce checking must be case sensitive. Nonce must be derived from a cryptographically strong PRNG. This would give a search space of 7.82 x 10^26 making it impossible to guess


#### Signing the Authentication Message

The `auth-msg` **MUST** be exchanged as a base64 encoded [msgpacked](https://msgpack.org/index.html) message, prefixed with the `"AX"` domain separator, such that:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Above the authentication message is presented as a JSON object.
Why converting now to msgpack?
Why using base64 since raw-bytes can be directly signed?


`msg =("AX" + msgpack_encode(auth-msg))`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend using prefix arc0031 instead of AX to be consistent with https://github.com/algorand/go-algorand/blob/master/protocol/hash.go#L28


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An issue with the above is that currently Ledger would not support signing such messages. This would require an update of Ledger.

### Authenticate Rekeyed Accounts

Algorand accounts can be rekeyed. Rekeying means that the signing key of a static public address *PKa* is dynamically rotated to another secret key. In this case, the original address controlled by *SKa'* is called *authorization address* of *PKa*, and it **MUST** be used to check the signature of *PKa*. In this ARC we indicate the *authorization address* with the account *<PKa', SKa'>*.

> To learn more about Algorand Rekeying feature visit the [Rekey section](https://developer.algorand.org/docs/get-details/accounts/rekey/?from_query=rekey#create-publication-overlay) of the developer portal.

The *authorization address* of an account can be checked directly from the Algorand blockchain. Indeed, a Verifier can inspect the [account API](https://developer.algorand.org/docs/rest-apis/algod/v2/#get-v2accountsaddress) to check the account's `auth-addr` parameter. This parameter, if not empty, indicates the *authorization address* *PKa'*.

The ARC-31 allows rekeyed accounts to be used for authentication. In this case, the Verifier must check the signature of a *PKa* with the *authorization address* *PKa'*. This address must be provided by the User along with the original address *PKa* and the digital signature. The Verifier, on his side, can check the validity of *PKa'* by looking at the Algorand blockchain. The diagram below details the protocol handling rekeyed accounts.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that the verifier MUST always check if an account is rekeyed?
How to handle multiple network? Should the network genesis hash be inside the authentication message to be signed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I would say gh should be passed as default when the protocol is initiated.

IIUC the ARC is defining a way to authenticate with Algorand Account. Now, an Algorand Account is an entity that has a meaning iff I specify both an address and the gh of the network on which the Account exists. The same address could be rekeyed on MainNet but not on TestNet.

In particular the following steps of the protocol:

  • Wallet pass <PKa, Sig(msg)>
  • Verify Sig(msg) with PKa and msg

Can not work unless the verifier knows gh and is sure that PKa is still the auth_address. Client could cheat not declaring it.


```mermaid
sequenceDiagram
actor A as Alice
participant W as Wallet
actor B as Bob
participant Algorand
A-->>W: Connect to Bob with PKa
activate W
W->>B: GET message for PKa
activate B
B->>B: Create a new msg for PKa
B->>W: msg
deactivate B
W->>W: Check <msg, Bob>
W-->>A: Show msg origin
Note right of A: Confirm signature with <br/>auth-addr <PKa', SKa'>
A-->>W:
W->>B: <PKa, PKa', Sig(msg)>
deactivate W
activate B
B->>Algorand: Retrieve PKa auth-addr
activate Algorand
Algorand-->>B: PKa'
deactivate Algorand
B->>B: Verify PKa auth-addr
B->>B: Verify Sig(msg) with PKa' and msg
Note right of B: if Sig(msg) is valid<br/>then authenticate user
B-->>A: Authentication OK/KO
deactivate B
```

### Authenticate Multisignature Accounts

Algorand accounts can be Multisignature (or MultiSig). A MultiSig account is a logical representation of an ordered set of addresses with a threshold and version.

> To learn more about Algorand MultiSig feature visit the [Multisignature section](https://developer.algorand.org/docs/get-details/accounts/create/#multisignature) of the developer portal.

The ARC-31 allows MultiSig accounts to be used for authentication. Assuming a MultiSig address *PKa_msig* composed by three Algorand accounts identified with the addresses *PKa', PKa'', PKa'''*, `threshold=2`, and `version=1`, the authentication should work as follows:

1. An authentication request with *PKa_msig* is sent from the User to the Verifier;
2. The Verifier responds with a new authentication message `msg`;
3. The User collects the threshold signatures of `msg` and responds with *<PKa_msig, PKa', PKa'', PKa''', Sig'(msg), Sig''(msg), 2, 1>*, where *PKa*s are the ordered set of addresses of *PKa_msig*, *Sig*s are the collected signatures, `2` is the threshold and `1` is the MultiSig version;
4. The Verifier firstly checks the *PKa_msig* against the list of addresses, the threshold and the version received, then verifies the signatures; if a threshold of valid signatures is received, the User can be authenticated.

The diagram below details the protocol handling MultiSig accounts.

```mermaid
sequenceDiagram
actor A as Alice
participant W as Wallet
actor B as Bob
A-->>W: Connect to Bob with PKa_msig
activate W
W->>B: GET message for PKa_msig
activate B
B->>B: Create a new msg for PKa_msig
B->>W: msg
deactivate B
W->>W: Check <msg, Bob>
W-->>A: Show msg origin
Note right of A: Collect signatures with <br/>PKa' and PKa''
A-->>W:
W->>B: <PKa_msig, PKa', PKa'', PKa''', Sig'(msg), Sig''(msg), 2, 1>
deactivate W
activate B
B->>B: Verify PKa_msig with (PKa', PKa'', PKa''', 2, 1)
B->>B: Verify Sig'(msg) with PKa' and msg
B->>B: Verify Sig''(msg) with PKa'' and msg
Note right of B: if threshold of valid signatures<br/>then authenticate user
B-->>A: Authentication OK/KO
deactivate B
```

### How to verify a digital signature?

A digital signature generated with the secret key *SKa* of an Algorand account can be verified with its respective 32-byte public key *PKa*. The Verifier needs to decode the public key *PK* from the Algorand address, and it must know the original `auth-msg`. For example, assuming the digital signature `Sig(msg)` the Verifier can validate it using the Algorand SDK as follows:

1. decode the Algorand address into a traditional 32-bytes public key *PK*;
2. Compute `msg =("AX" + msgpack_encode(auth-msg))`;
3. use an open-source cryptographic library (e.g. Python lib PyNaCl) to verify the signature `Sig(msg)` with *PK*.

## Security Considerations

An attacker **MAY** attempt to cheat with the system by impersonating another User or Verifier. This is possible if the attacker can intercept the digital signature and use the same signature in a replay-attack or man-in-the-middle attack. To mitigate this scenario, the Verifier **MUST** generate a new message for each authentication request, and Wallets must always check the `auth-msg` domain field.

## Reference Implementation

The ARC-31 reference implementation is available in the `assets` directory of this repo `assets/arc-0031`. It provides an example of client-server authentication with ARC-31. The reference implementation uses [MyAlgoWallet](https://wallet.myalgo.com/) as the unique wallet (at the time of writing) providing the possibility of signing random bytes.

Reference implementation credits: [mrcointreau](https://github.com/mrcointreau)

## Copyright

Copyright and related rights waived via <a href="https://creativecommons.org/publicdomain/zero/1.0/">CCO</a>.
30 changes: 30 additions & 0 deletions assets/arc-0031/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
.env
.env.*

/cypress/videos/
/cypress/screenshots/

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
36 changes: 36 additions & 0 deletions assets/arc-0031/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
.env
.env.*.
!.env.example

# Nuxt dev/build outputs
.output
.nuxt

/cypress/videos/
/cypress/screenshots/

# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json.example
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
11 changes: 11 additions & 0 deletions assets/arc-0031/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid",
"htmlWhitespaceSensitivity": "strict"
}
6 changes: 6 additions & 0 deletions assets/arc-0031/.vscode/settings.json.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"eslint.workingDirectories": ["client", "server"],
"prettier.requireConfig": true
}
Loading