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

docs: SIWE tutorial #459

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 21 additions & 20 deletions contracts/contracts/auth/A13e.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,33 @@ import {SignatureRSV} from "../EthereumUtils.sol";
* @title Interface for authenticatable contracts
* @notice This is the interface for universal authentication mechanism (e.g.
* SIWE):
* 1. The user-facing app calls login() to generate the bearer token on-chain.
* 2. Any smart contract method that requires authentication accept this token
* as an argument. Then, it passes the token to authMsgSender() to verify it
* and obtain the **authenticated** user address. This address can then serve
* as a user ID for authorization.
* 1. The user-facing app calls `login()` which generates the authentication
* token on-chain.
* 2. Any smart contract method that requires authentication can take this token
* as an argument. Passing this token to `authMsgSender()` to verifies it
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
* as an argument. Passing this token to `authMsgSender()` to verifies it
* as an argument. Passing this token to `authMsgSender()` verifies it

* and returns the **authenticated** user address. This verified address can
* then serve as a user ID for authorization.
*/
abstract contract A13e {
/// A mapping of revoked bearers. Access it directly or use the checkRevokedBearer modifier.
mapping(bytes32 => bool) internal _revokedBearers;
/// A mapping of revoked authentication tokens. Access it directly or use the checkRevokedAuthToken modifier.
mapping(bytes32 => bool) internal _revokedAuthTokens;

/// The bearer token was revoked
error RevokedBearer();
/// The authentication token was revoked
error RevokedAuthToken();

/**
* @notice Reverts if the given bearer was revoked
* @notice Reverts if the given token was revoked
*/
modifier checkRevokedBearer(bytes calldata bearer) {
if (_revokedBearers[keccak256(bearer)]) {
revert RevokedBearer();
modifier checkRevokedAuthToken(bytes memory token) {
if (_revokedAuthTokens[keccak256(token)]) {
revert RevokedAuthToken();
}
_;
}

/**
* @notice Verify the login message and its signature and generate the
* bearer token.
* token.
*/
function login(string calldata message, SignatureRSV calldata sig)
external
Expand All @@ -41,20 +42,20 @@ abstract contract A13e {
returns (bytes memory);

/**
* @notice Validate the bearer token and return authenticated msg.sender.
* @notice Validate the token and return authenticated msg.sender.
*/
function authMsgSender(bytes calldata bearer)
function authMsgSender(bytes memory token)
internal
view
virtual
returns (address);

/**
* @notice Revoke the bearer token with the corresponding hash.
* e.g. In case when the bearer token is leaked or for extra-secure apps on
* @notice Revoke the authentication token with the corresponding hash.
* e.g. In case when the token is leaked or for extra-secure apps on
* every logout.
*/
function revokeBearer(bytes32 bearer) internal {
_revokedBearers[bearer] = true;
function revokeAuthToken(bytes32 token) internal {
_revokedAuthTokens[token] = true;
}
}
48 changes: 28 additions & 20 deletions contracts/contracts/auth/SiweAuth.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,27 @@ import {SignatureRSV, A13e} from "./A13e.sol";
import {ParsedSiweMessage, SiweParser} from "../SiweParser.sol";
import {Sapphire} from "../Sapphire.sol";

struct Bearer {
struct AuthToken {
string domain; // [ scheme "://" ] domain.
address userAddr;
uint256 validUntil; // in Unix timestamp.
}

/**
* @title Base contract for SIWE-based authentication
* @notice Inherit this contract, if you wish to enable SIWE-based
* authentication for your contract methods that require authenticated calls.
* @notice Inherit this contract if you wish to enable SIWE-based
* authentication in your contract functions that require authentication.
* The smart contract needs to be bound to a domain (passed in constructor).
*
* #### Example
*
* ```solidity
* contract MyContract is SiweAuth {
* address private _owner;
* string private _message;
*
* modifier onlyOwner(bytes calldata bearer) {
* if (authMsgSender(bearer) != _owner) {
* modifier onlyOwner(bytes calldata token) {
* if (msg.sender != _owner && authMsgSender(token) != _owner) {
* revert("not allowed");
* }
* _;
Expand All @@ -36,18 +37,22 @@ struct Bearer {
* _owner = msg.sender;
* }
*
* function getSecretMessage(bytes calldata bearer) external view onlyOwner(bearer) returns (string memory) {
* return "Very secret message";
* function getSecretMessage(bytes calldata token) external view onlyOwner(token) returns (string memory) {
* return _message;
* }
*
* function setSecretMessage(string calldata message) external onlyOwner("") {
* _message = message;
* }
* }
* ```
*/
contract SiweAuth is A13e {
/// Domain which the dApp is associated with
string private _domain;
/// Encryption key which the bearer tokens are encrypted with
bytes32 private _bearerEncKey;
/// Default bearer token validity, if no expiration-time provided
/// Encryption key which the authentication tokens are encrypted with
bytes32 private _authTokenEncKey;
/// Default authentication token validity, if no expiration-time provided
uint256 private constant DEFAULT_VALIDITY = 24 hours;

/// Chain ID in the SIWE message does not match the actual chain ID
Expand All @@ -58,15 +63,15 @@ contract SiweAuth is A13e {
error AddressMismatch();
/// The Not before value in the SIWE message is still in the future
error NotBeforeInFuture();
/// The bearer token validity or the Expires value in the SIWE message is in the past
/// The authentication token validity or the Expires value in the SIWE message is in the past
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
/// The authentication token validity or the Expires value in the SIWE message is in the past
/// Validity of the authentication token or the Expires value in the SIWE message is in the past

error Expired();

/**
* @notice Instantiate the contract which uses SIWE for authentication and
* runs on the specified domain.
*/
constructor(string memory inDomain) {
_bearerEncKey = bytes32(Sapphire.randomBytes(32, ""));
_authTokenEncKey = bytes32(Sapphire.randomBytes(32, ""));
_domain = inDomain;
}

Expand All @@ -76,7 +81,7 @@ contract SiweAuth is A13e {
override
returns (bytes memory)
{
Bearer memory b;
AuthToken memory b;

// Derive the user's address from the signature.
bytes memory eip191msg = abi.encodePacked(
Expand Down Expand Up @@ -129,7 +134,7 @@ contract SiweAuth is A13e {
}

bytes memory encB = Sapphire.encrypt(
_bearerEncKey,
_authTokenEncKey,
0,
abi.encode(b),
""
Expand All @@ -144,20 +149,23 @@ contract SiweAuth is A13e {
return _domain;
}

function authMsgSender(bytes calldata bearer)
function authMsgSender(bytes memory token)
internal
view
override
checkRevokedBearer(bearer)
checkRevokedAuthToken(token)
returns (address)
{
bytes memory bearerEncoded = Sapphire.decrypt(
_bearerEncKey,
if (token.length == 0) {
return address(0);
}
bytes memory authTokenEncoded = Sapphire.decrypt(
_authTokenEncKey,
0,
bearer,
token,
""
);
Bearer memory b = abi.decode(bearerEncoded, (Bearer));
AuthToken memory b = abi.decode(authTokenEncoded, (AuthToken));

if (keccak256(bytes(b.domain)) != keccak256(bytes(_domain))) {
revert DomainMismatch();
Expand Down
12 changes: 6 additions & 6 deletions contracts/contracts/tests/auth/SiweAuthTests.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ contract SiweAuthTests is SiweAuth {
_owner = msg.sender;
}

function testVerySecretMessage(bytes calldata bearer)
function testVerySecretMessage(bytes calldata token)
external
view
returns (string memory)
{
if (authMsgSender(bearer) != _owner) {
if (authMsgSender(token) != _owner) {
revert("not allowed");
}
return "Very secret message";
Expand All @@ -30,16 +30,16 @@ contract SiweAuthTests is SiweAuth {
return this.login(message, sig);
}

function testAuthMsgSender(bytes calldata bearer)
function testAuthMsgSender(bytes calldata token)
external
view
returns (address)
{
return authMsgSender(bearer);
return authMsgSender(token);
}

function testRevokeBearer(bytes32 bearer) external {
return revokeBearer(bearer);
function testRevokeAuthToken(bytes32 token) external {
return revokeAuthToken(token);
}

function doNothing() external { // solhint-disable-line
Expand Down
28 changes: 14 additions & 14 deletions contracts/test/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,11 @@ describe('Auth', function () {
accounts.path + '/0',
);
const siweStr = await siweMsg('localhost', 0);
const bearer = await siweAuthTests.testLogin(
const token = await siweAuthTests.testLogin(
siweStr,
await erc191sign(siweStr, account),
);
expect(await siweAuthTests.testVerySecretMessage(bearer)).to.be.equal(
expect(await siweAuthTests.testVerySecretMessage(token)).to.be.equal(
'Very secret message',
);

Expand All @@ -137,26 +137,26 @@ describe('Auth', function () {
accounts.path + '/1',
);
const siweStr2 = await siweMsg('localhost', 1);
const bearer2 = await siweAuthTests.testLogin(
const token2 = await siweAuthTests.testLogin(
siweStr2,
await erc191sign(siweStr2, acc2),
);
await expect(siweAuthTests.testVerySecretMessage(bearer2)).to.be.reverted;
await expect(siweAuthTests.testVerySecretMessage(token2)).to.be.reverted;

// Same user, hijacked bearer from another contract/domain.
// Same user, hijacked token from another contract/domain.
const siweAuthTests2 = await deploy('localhost2');
const siweStr3 = await siweMsg('localhost2', 0);
const bearer3 = await siweAuthTests2.testLogin(
const token3 = await siweAuthTests2.testLogin(
siweStr3,
await erc191sign(siweStr3, account),
);
await expect(siweAuthTests.testVerySecretMessage(bearer3)).to.be.reverted;
await expect(siweAuthTests.testVerySecretMessage(token3)).to.be.reverted;

// Expired bearer
// Expired token
// on-chain block timestamps are integers representing seconds
const expiration = new Date(Date.now() + 1000);
const siweStr4 = await siweMsg('localhost', 0, expiration);
const bearer4 = await siweAuthTests.testLogin(
const token4 = await siweAuthTests.testLogin(
siweStr4,
await erc191sign(siweStr4, account),
);
Expand All @@ -169,14 +169,14 @@ describe('Auth', function () {
}
});
});
await expect(siweAuthTests.testVerySecretMessage(bearer4)).to.be.reverted;
await expect(siweAuthTests.testVerySecretMessage(token4)).to.be.reverted;

// Revoke bearer.
const bearer5 = await siweAuthTests.testLogin(
// Revoke token.
const token5 = await siweAuthTests.testLogin(
siweStr,
await erc191sign(siweStr, account),
);
await siweAuthTests.testRevokeBearer(ethers.keccak256(bearer5));
await expect(siweAuthTests.testVerySecretMessage(bearer5)).to.be.reverted;
await siweAuthTests.testRevokeAuthToken(ethers.keccak256(token5));
await expect(siweAuthTests.testVerySecretMessage(token5)).to.be.reverted;
});
});
Loading
Loading