Skip to content

Commit

Permalink
client: revise auth token functions. (#353)
Browse files Browse the repository at this point in the history
Previously we had an `authToken` function exported from
`electric-sql/auth` that:

1. was a mock/test function with mock data in it
2. used in the examples
3. importing the Jose library

Plus in the examples, we had separate code copied around used to
generate insecure tokens.

The PR:

1. provides user-usable `secureAuthToken(claims, iss, key)` and
`insecureAuthToken(claims)` functions
2. renames the authToken function to `mockSecureAuthToken`
3. updates the docs and examples accordingly -- examples *should* use
the insecure function. The backend stack for the examples is configured
in insecure mode and this is what it is for.
4. moves the secure functions to an optional import path, so that we
remove the Jose JWT signing library from users' JS builds if they are
not using it (generally they won't be -- tokens will come from the
server side)
  • Loading branch information
thruflo authored Aug 24, 2023
1 parent bd9b34b commit 345cfc6
Show file tree
Hide file tree
Showing 11 changed files with 109 additions and 48 deletions.
6 changes: 6 additions & 0 deletions .changeset/rotten-years-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"electric-sql": patch
"create-electric-app": patch
---

Added auth.insecureAuthToken function and updated examples to use it.
6 changes: 5 additions & 1 deletion clients/typescript/src/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { authToken } from './util'
export { insecureAuthToken } from './insecure'

export interface AuthState {
clientId: string
Expand All @@ -9,3 +9,7 @@ export interface AuthConfig {
clientId?: string
token: string
}

export interface TokenClaims {
[key: string]: any
}
25 changes: 25 additions & 0 deletions clients/typescript/src/auth/insecure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { TokenClaims } from './index'

export function insecureAuthToken(claims: TokenClaims): string {
const header = {
alg: 'none',
}

return `${encode(header)}.${encode(claims)}.`
}

function encode(data: object): string {
const str = JSON.stringify(data)
const encoded = base64(str)

return encoded.replace(/\+/g, '-').replace(/\//, '_').replace(/=+$/, '')
}

function base64(s: string): string {
const bytes = new TextEncoder().encode(s)

const binArray = Array.from(bytes, (x) => String.fromCodePoint(x))
const binString = binArray.join('')

return btoa(binString)
}
37 changes: 37 additions & 0 deletions clients/typescript/src/auth/secure/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { SignJWT } from 'jose'

import { TokenClaims } from '../index'

export function secureAuthToken(
claims: TokenClaims,
iss: string,
key: string,
alg?: string,
exp?: string
): Promise<string> {
const algorithm = alg ?? 'HS256'
const expiration = exp ?? '2h'

const nowInSecs = Math.floor(Date.now() / 1000)
// Subtract 1 second to account for clock precision when validating the token
const iat = nowInSecs - 1

const encodedKey = new TextEncoder().encode(key)

return new SignJWT({ ...claims, type: 'access' })
.setIssuedAt(iat)
.setProtectedHeader({ alg: algorithm })
.setExpirationTime(expiration)
.setIssuer(iss)
.sign(encodedKey)
}

export function mockSecureAuthToken(
iss?: string,
key?: string
): Promise<string> {
const mockIss = iss ?? 'dev.electric-sql.com'
const mockKey = key ?? 'integration-tests-signing-key-example'

return secureAuthToken({ user_id: 'test-user' }, mockIss, mockKey)
}
18 changes: 0 additions & 18 deletions clients/typescript/src/auth/util.ts

This file was deleted.

18 changes: 18 additions & 0 deletions clients/typescript/test/auth/insecure.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import test from 'ava'
import { decodeJwt } from 'jose'

import { insecureAuthToken } from '../../src/auth'

test('insecureAuthToken generates expected token', async (t) => {
const token = insecureAuthToken({ user_id: 'dummy-user' })

const claims = decodeJwt(token)
t.deepEqual(claims, { user_id: 'dummy-user' })
})

test('insecureAuthToken supports non-latin characters', async (t) => {
const token = insecureAuthToken({ user_id: '⚡' })

const claims = decodeJwt(token)
t.deepEqual(claims, { user_id: '⚡' })
})
10 changes: 6 additions & 4 deletions docs/api/clients/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@ passing along the generated database schema.

```ts
import { schema } from './generated/client'
import { insecureAuthToken } from 'electric-sql/auth'
import { electrify, ElectricDatabase } from 'electric-sql/wa-sqlite'

const db = await ElectricDatabase.init('electric.db', '')
const electric = await electrify(db, schema, {
const config = {
auth: {
token: await authToken('local-development', 'local-development-key-minimum-32-symbols')
token: await insecureAuthToken({user_id: 'dummy'})
}
})
}
const conn = await ElectricDatabase.init('electric.db', '')
const electric = await electrify(conn, schema, config)
```

The electrify call returns a promise that will resolve to an `ElectricClient` for our database.
Expand Down
10 changes: 6 additions & 4 deletions docs/integrations/frontend/react.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ We provide this dynamic API rather than static `ElectricProvider` and `useElectr
```tsx
// wrapper.tsx
import React, { ReactNode, useEffect, useState } from 'react'
import { insecureAuthToken } from 'electric-sql/auth'
import { makeElectricContext } from 'electric-sql/react'
import { ElectricDatabase, electrify } from 'electric-sql/wa-sqlite'
import { Electric, schema } from './generated/client'
Expand All @@ -48,12 +49,13 @@ export const ElectricWrapper = ({ children }) => {
const isMounted = true

const init = async () => {
const conn = await ElectricDatabase.init('electric.db', '')
const electric = await electrify(conn, schema, {
const config = {
auth: {
token: '...'
token: insecureAuthToken({user_id: 'dummy'})
}
})
}
const conn = await ElectricDatabase.init('electric.db', '')
const electric = await electrify(conn, schema, config)

if (!isMounted) {
return
Expand Down
4 changes: 2 additions & 2 deletions e2e/satellite_client/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Database from 'better-sqlite3'
import { ElectricConfig } from 'electric-sql'
import { authToken } from 'electric-sql/auth'
import { mockSecureAuthToken } from 'electric-sql/auth/secure'

import { setLogLevel } from 'electric-sql/debug'
import { electrify } from 'electric-sql/node'
Expand All @@ -24,7 +24,7 @@ export const electrify_db = async (
url: `electric://${host}:${port}`,
debug: true,
auth: {
token: await authToken()
token: await mockSecureAuthToken()
}
}
console.log(`(in electrify_db) config: ${JSON.stringify(config)}`)
Expand Down
12 changes: 2 additions & 10 deletions examples/starter/template/src/Example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react'
import { makeElectricContext, useLiveQuery } from 'electric-sql/react'
import { ElectricDatabase, electrify } from 'electric-sql/wa-sqlite'

import { authToken } from 'electric-sql/auth'
import { insecureAuthToken } from 'electric-sql/auth'
import { genUUID } from 'electric-sql/util'

import { DEBUG_MODE, ELECTRIC_URL } from './config'
Expand All @@ -13,13 +13,6 @@ import './Example.css'

const { ElectricProvider, useElectric } = makeElectricContext<Electric>()

const localAuthToken = (): Promise<string> => {
const issuer = 'local-development'
const signingKey = 'local-development-key-minimum-32-symbols'

return authToken(issuer, signingKey)
}

export const Example = () => {
const [ electric, setElectric ] = useState<Electric>()

Expand All @@ -29,12 +22,11 @@ export const Example = () => {
const init = async () => {
const config = {
auth: {
token: await localAuthToken()
token: insecureAuthToken({user_id: 'dummy'})
},
debug: DEBUG_MODE,
url: ELECTRIC_URL
}

const conn = await ElectricDatabase.init('electric.db', '')
const electric = await electrify(conn, schema, config)

Expand Down
11 changes: 2 additions & 9 deletions examples/web-wa-sqlite/src/Example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react'
import { makeElectricContext, useLiveQuery } from 'electric-sql/react'
import { ElectricDatabase, electrify } from 'electric-sql/wa-sqlite'

import { authToken } from 'electric-sql/auth'
import { insecureAuthToken } from 'electric-sql/auth'
import { genUUID } from 'electric-sql/util'

import { DEBUG_MODE, ELECTRIC_URL } from './config'
Expand All @@ -13,13 +13,6 @@ import './Example.css'

const { ElectricProvider, useElectric } = makeElectricContext<Electric>()

const localAuthToken = (): Promise<string> => {
const issuer = 'local-development'
const signingKey = 'local-development-key-minimum-32-symbols'

return authToken(issuer, signingKey)
}

export const Example = () => {
const [ electric, setElectric ] = useState<Electric>()

Expand All @@ -29,7 +22,7 @@ export const Example = () => {
const init = async () => {
const config = {
auth: {
token: await localAuthToken()
token: insecureAuthToken({'user_id': 'dummy'})
},
debug: DEBUG_MODE,
url: ELECTRIC_URL
Expand Down

0 comments on commit 345cfc6

Please sign in to comment.