An example of using multi-user JWT authentication with Redwood"
Re: this thread
An example repo showing 2 factor JWT auth which works with a netflix-style account system (aka one Account
which can have many User
s.) Handles switching accounts, logging in via email or username.
It TypeScript-ifies and builds on the work from 3nvy in in "Local JWT Auth Implementation".
This code can handle auth via cookies, headers (bearer) and embedded JSON requests which is enough to handle the default Redwood setup and external clients like apps.
There are 5 new functions in api/src/functions
:
jwtLogin.ts
- Handles logging in and either returns a full set of tokens or just one temporary token to select users withjwtLogout.ts
- Handles logout and removing cookiesjwtRefresh.ts
- Handles recycling the short term token every 30mjwtSignup.ts
- Handles the creation of a new accountjwtUserSwitch.ts
- Lets you switch access tokens between users on an accountLooks like this:
// Represents a person
model User {
id String @id @unique
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
username String @unique
roles String @default("user")
account Account @relation(fields: [accountID], references: [id])
accountID String
}
// The paying entity
model Account {
id String @id @unique
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
email String @unique
hashedPassword String
salt String
resetToken String?
resetTokenExpiresAt DateTime?
users User[]
jwts JWT[]
}
// A long-term JWT token
model JWT {
// The ID is the actual JWT token
id String @id
account Account @relation(fields: [accountID], references: [id])
accountID String
}
App.tsx
needs to use the new jwtAuthClient at /web/src/networking/jwtAuthClient.ts
The API client needs to silently handle token refreshes - here's how it works for me in my relay code - I'm sure someone knows how to port this to an Apollo link pretty trivially
if (user) {
let token = user.accessToken
const refresh = user.refreshToken
const { exp } = jwt_decode<JwtPayload>(token)
// Checks if access token has expired and refresh tokens before proceeding
if (exp * 1000 < Date.now()) {
const apiURL = (path: string) => `${global.RWJS_API_URL}/${path}`
// Send off the long-term JWT in order to ask for a new access token
const res = await fetch(apiURL("jwtRefresh"), { headers: { Authorization: `Bearer ${refresh}`, "auth-provider": "custom" } })
const data = await res.json()
if (res.ok) {
localStorage.setItem("myAppAuth", JSON.stringify(data))
token = data.accessToken
} else {
console.error("JWT refresh failed")
console.error(data)
localStorage.removeItem("myAppAuth")
}
}
if (token) {
// We either pass the main token of the new revised refresh token
headers["authorization"] = `Bearer ${token}`
headers["auth-provider"] = "custom"
}
}
A User switcher UI. All my code is Relay, and I'm not re-creating it here. You'd need to take this into account in your login screen, and inside your user dashboard.
A new user button. Same problem as above.
I threw the server-side code in for into the repo anyway though.
isAuthenticated
from useAuth
is not to be trusted, prefer a check for currentUser
from useAuth
. In my app, I don't rely on useAuth
because most of it comes from RNW and so I don't make the same getCurrentUser
calls. Open to fixes.
getCurrentUser
in auth.ts uses the 2nd param - not the session, not sure what to make of this.