Skip to content

Commit

Permalink
feat(auth): implement siwe (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
satish-ravi authored Apr 27, 2022
1 parent 56da338 commit d0fef35
Show file tree
Hide file tree
Showing 19 changed files with 1,228 additions and 1,778 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ You can run unit tests with jest as follows:
yarn test
```
#### Integration tests
Integration tests are not included with this starter project. However, an example of how to create a JWT is included
Integration tests are not included with this starter project. However, an
example of how to create a [Sign-In with Ethereum](https://login.xyz/) message for authentication is included
in `scripts/example-request.ts`.

## Structure & Schema Validation
Expand Down
9 changes: 3 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,15 @@
"eslint-plugin-react-native": "^4.0.0",
"ethers": "^5.5.4",
"express": "^4.17.3",
"express-jwt": "^6.1.1",
"jsonwebtoken": "^8.5.1",
"jwt-decode": "^3.1.2",
"key-encoder": "^2.0.3",
"express-session": "^1.17.2",
"prettier": "^2.5.1",
"siwe": "^1.1.6",
"typescript": "^4.3.5",
"yargs": "^17.3.1"
},
"devDependencies": {
"@types/express-jwt": "^6.0.4",
"@types/express-session": "^1.17.4",
"@types/jest": "^27.4.1",
"@types/jsonwebtoken": "^8.5.8",
"@types/node-fetch": "2",
"@types/supertest": "^2.0.11",
"jest": "^27.5.1",
Expand Down
49 changes: 35 additions & 14 deletions scripts/example-request.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,54 @@
import jwt from 'jsonwebtoken'
import { ethers } from 'ethers'
import KeyEncoder from 'key-encoder'
import { trimLeading0x, ensureLeading0x } from '@celo/utils/lib/address'
import { ensureLeading0x } from '@celo/utils/lib/address'
import fetch from 'node-fetch'

const keyEncoder = new KeyEncoder('secp256k1')
import { SiweMessage } from 'siwe'

/**
* This function shows how to generate and sign a JWT for authentication with a local server via the FiatConnect API.
* This function shows how to use SIWE for authentication with a local server via the FiatConnect API.
*
* (Note that the transferId given is 'test', which probably won't exist. Note also that this function doesn't start
* the local server. It's just an example that you can optionally borrow from for integration testing.)
*/
async function main() {
const privateKey = '0x9999999999999999999999999999999999999999999999999999999999999999'
const privateKeyPem = keyEncoder.encodePrivate(trimLeading0x(privateKey), 'raw', 'pem')

const privateKey =
'0x9999999999999999999999999999999999999999999999999999999999999999'
const publicKey = new ethers.utils.SigningKey(privateKey).compressedPublicKey

const accountAddress = ethers.utils.computeAddress(ensureLeading0x(publicKey))
const wallet = new ethers.Wallet(privateKey)

const expirationDate = new Date(Date.now() + 14400000) // 4 hours from now

const token = jwt.sign({ iss: publicKey, sub: accountAddress }, privateKeyPem, {
algorithm: 'ES256',
expiresIn: '5m'
const siweMessage = new SiweMessage({
domain: 'example-provider.com',
address: accountAddress,
statement: 'Sign in with Ethereum',
uri: 'https://example-provider.com',
version: '1',
chainId: 42220,
nonce: '12345678',
expirationTime: expirationDate.toISOString(),
})
const message = siweMessage.prepareMessage()
const signature = await wallet.signMessage(message)

const authResponse = await fetch('http://localhost:8080/auth/login', {
method: 'POST',
body: JSON.stringify({ message, signature }),
headers: { 'Content-Type': 'application/json' },
})

if (!authResponse.ok) {
console.log('Auth request failed:', await authResponse.text())
return
}

// set-cookie will be of the form:
// api-starter=<cookie-val>; Path=/; Expires=Fri, 22 Apr 2022 10:36:40 GMT; HttpOnly; SameSite=Strict
const authCookie = authResponse.headers.raw()['set-cookie'][0]

const response = await fetch('http://localhost:8080/transfer/test/status', {
headers: {
'Authorization': `Bearer ${token}`
'cookie': authCookie.split(';')[0] // strip out additional fields like Path, Expires
}
})
const data = await response.json()
Expand Down
38 changes: 27 additions & 11 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import express from 'express'
import {JwtAuthorizationMiddleware, NotImplementedError} from './types'
import Session from 'express-session'
import { quoteRouter } from './routes/quote'
import { kycRouter } from './routes/kyc'
import { accountsRouter } from './routes/accounts'
import { transferRouter } from './routes/transfer'
import { errorToStatusCode } from './middleware/error'
import { authRouter } from './routes/auth'
import { NotImplementedError } from './types'

function getSessionName(): string {
// must return a name for the session cookie, typically the provider name
throw new NotImplementedError('getSessionName not implemented')
}

export function initApp({
jwtAuthMiddleware,
clientAuthMiddleware,
sessionSecret,
chainId,
}: {
jwtAuthMiddleware: JwtAuthorizationMiddleware
clientAuthMiddleware: express.RequestHandler[]
sessionSecret: string
chainId: number
}): express.Application {
const app = express()

Expand All @@ -23,17 +32,24 @@ export function initApp({
throw new NotImplementedError()
})

app.use('/quote', quoteRouter({ jwtAuthMiddleware, clientAuthMiddleware }))
app.use('/kyc', kycRouter({ jwtAuthMiddleware, clientAuthMiddleware }))
app.use(
'/accounts',
accountsRouter({ jwtAuthMiddleware, clientAuthMiddleware }),
)
app.use(
'/transfer',
transferRouter({ jwtAuthMiddleware, clientAuthMiddleware }),
// https://www.npmjs.com/package/express-session-expire-timeout#sessionoptions
Session({
name: getSessionName(),
secret: sessionSecret,
resave: true,
saveUninitialized: true,
cookie: { secure: true, sameSite: true },
}),
)

app.use('/auth', authRouter({ chainId }))

app.use('/quote', quoteRouter({ clientAuthMiddleware }))
app.use('/kyc', kycRouter({ clientAuthMiddleware }))
app.use('/accounts', accountsRouter({ clientAuthMiddleware }))
app.use('/transfer', transferRouter({ clientAuthMiddleware }))

app.use(errorToStatusCode)

return app
Expand Down
17 changes: 8 additions & 9 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
Config,
JwtAuthStrategy,
ClientAuthStrategy,
AuthenticationConfig,
Network,
Expand All @@ -13,22 +12,16 @@ export const ALFAJORES_FORNO_URL = 'https://alfajores-forno.celo-testnet.org'
export const MAINNET_FORNO_URL = 'https://forno.celo.org'

export const authConfigOptions: Record<string, AuthenticationConfig> = {
test: {
web3ProviderUrl: ALFAJORES_FORNO_URL,
network: Network.Alfajores,
jwtAuthStrategy: JwtAuthStrategy.DecodeOnly,
clientAuthStrategy: ClientAuthStrategy.Optional,
},
alfajores: {
web3ProviderUrl: ALFAJORES_FORNO_URL,
network: Network.Alfajores,
jwtAuthStrategy: JwtAuthStrategy.SignatureAndAddress,
chainId: 44787,
clientAuthStrategy: ClientAuthStrategy.Optional,
},
mainnet: {
web3ProviderUrl: MAINNET_FORNO_URL,
network: Network.Mainnet,
jwtAuthStrategy: JwtAuthStrategy.SignatureAndAddress,
chainId: 42220,
clientAuthStrategy: ClientAuthStrategy.Required,
},
}
Expand All @@ -53,10 +46,16 @@ export function loadConfig(): Config {
type: 'number',
default: DEFAULT_PORT,
})
.option('session-secret', {
description: 'The secret for signing the session',
type: 'string',
demandOption: true,
})
.parseSync()

return {
authConfig: authConfigOptions[argv['auth-config-option']],
port: argv.port,
sessionSecret: argv.sessionSecret,
}
}
11 changes: 4 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import { loadConfig } from './config'
import {
getJwtAuthMiddleware,
getClientAuthMiddleware,
} from './middleware/authenticate'
import { getClientAuthMiddleware } from './middleware/authenticate'
import { initApp } from './app'

async function main() {
const { port, authConfig } = loadConfig()
const { port, authConfig, sessionSecret } = loadConfig()

const jwtAuthMiddleware = getJwtAuthMiddleware(authConfig)
const clientAuthMiddleware = getClientAuthMiddleware(authConfig)

const app = initApp({
jwtAuthMiddleware,
clientAuthMiddleware,
sessionSecret,
chainId: authConfig.chainId,
})

app.listen(port, () => {
Expand Down
Loading

0 comments on commit d0fef35

Please sign in to comment.