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

FEAT: Revoke Refresh Tokens on Session-Destroy #129

Merged
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,8 @@ TELEGRAM_BOT_TOKEN=
MAILJET_API_KEY=
MAILJET_SECRET_KEY=

# Block
# https://dev.to/vvo/how-to-add-firebase-service-account-json-files-to-vercel-ph5
# Make sure to add single quote here
FIREBASE_SERVICE_ACCOUNT_KEY='Your JSON value'
# Realtime Database: to handle Firebase token revocation
REALTIME_DATABASE_URL=
16 changes: 16 additions & 0 deletions .firebase/rule.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"rules": {
"metadata": {
"$user_id": {
".read": "$user_id === auth.uid",
".write": "false"
}
},
"users": {
"$user_id": {
".read": "auth != null && $user_id === auth.uid && (!root.child('metadata').child(auth.uid).child('revokeTime').exists() || auth.token.auth_time > root.child('metadata').child(auth.uid).child('revokeTime').val())",
".write": "auth != null && $user_id === auth.uid && (!root.child('metadata').child(auth.uid).child('revokeTime').exists() || auth.token.auth_time > root.child('metadata').child(auth.uid).child('revokeTime').val())"
}
}
}
}
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
.service-account-key
.service-account-key

# secret
*-firebase-adminsdk.json
*-firebase-adminsdk.production.json
52 changes: 44 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,57 @@ An anonymous question bank platform

## Setup in Local

### Preprequisites
### Prerequisites

- `node`, minimum version `18.16.0`
- `pnpm`, [see installation instruction](https://pnpm.io/installation)
- Firebase project, go to [console.firebase.google.com](https://console.firebase.google.com)
- Activate the authentication for the web
- Notion Account
- [Create a notion integration](https://developers.notion.com/docs/create-a-notion-integration)
- Duplicate the template from: [TanyaAja DB Template](https://general-lady-e21.notion.site/TanyaAja-Template-d6454b3d41934057badb0e389ada5e73)
- Add the integration to the page

### Development
#### Firebase and Firebase Admin

- Create Firebase project, go to [console.firebase.google.com](https://console.firebase.google.com)
- Activate the authentication for the web
- [Firebase Admin #1] Go to **Project settings** -> **Service accounts** -> **Generate new private key**
- Copy the downloaded Private key to the Project root directory, rename to `TanyaAja-firebase-adminsdk.json`
- [Firebase Admin #2] Create Realtime Database
- Copy the Database url, e.g. `https://tanyaaja-rtdb.asia-southeast1.firebasedatabase.app`
- Still on Realtime Database, go to Rules -> create Rule.
- Copy the Rule from [here](.firebase/rule.json) then Publish the Rule

> [!IMPORTANT]
> In some conditions, you need to add role "Service Usage Consumer" manually to your Service Account created via Firebase.

#### Notion Account

- [Create a notion integration](https://developers.notion.com/docs/create-a-notion-integration)
- Duplicate the template from: [TanyaAja DB Template](https://general-lady-e21.notion.site/TanyaAja-Template-d6454b3d41934057badb0e389ada5e73)
- Add the integration to the page

### Development Setup

- Install all dependencies, by running `pnpm install`
- Create new `.env.local` file, copy from the `.env.example` and fill it with your value from Firebase and Notion

### Additional Steps For Firebase Admin Setup

- Copy value from `TanyaAja-firebase-adminsdk.json` to https://www.textfixer.com/tools/remove-line-breaks.php and click button "Remove Line Breaks"
- Fill the `FIREBASE_SERVICE_ACCOUNT_KEY` in the `.env.local` with the value from the website previously.

```bash
# Make sure to add single quote here
FIREBASE_SERVICE_ACCOUNT_KEY='Your JSON value'
```

Read more in: https://dev.to/vvo/how-to-add-firebase-service-account-json-files-to-vercel-ph5


### Additional Steps For Firebase RTDB Setup

- Fill the `REALTIME_DATABASE_URL` in the `.env.local` with the Realtime Database URL, e.g.

```bash
REALTIME_DATABASE_URL=https://tanyaaja-rtdb.asia-southeast1.firebasedatabase.app
```

- Run in local, using command `pnpm run dev`

## Contributing
Expand Down
12 changes: 5 additions & 7 deletions src/app/api/private/user/session-destroy/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { headers } from 'next/headers'
import { NextResponse } from 'next/server'

import { destroySession, getSession } from '@/lib/notion'
import { revokeRefreshTokens, verifyIdToken } from '@/lib/firebase-admin'

export const dynamic = 'force-dynamic'

Expand All @@ -11,12 +11,10 @@ export async function DELETE(request: Request) {

try {
if (token) {
const session = await getSession(token)
if (session.results.length > 0) {
const foundPage = session.results[0]
if (foundPage) {
destroySession(foundPage?.id)
}
const decodedToken = await verifyIdToken(token)

if (decodedToken?.uid) {
await revokeRefreshTokens(decodedToken.uid)
}
}
return NextResponse.json({ message: 'Session destroyed' })
Expand Down
36 changes: 33 additions & 3 deletions src/lib/firebase-admin.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,44 @@
import * as admin from 'firebase-admin'
import { type App, getApp, getApps, initializeApp } from 'firebase-admin/app'
import { getAuth } from 'firebase-admin/auth'
import { type UserRecord, getAuth } from 'firebase-admin/auth'
import { getDatabase } from 'firebase-admin/database'

import { firebaseConfig } from '@/lib/firebase'

const PROJECT_ID = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID ?? ''

// Read: https://dev.to/vvo/how-to-add-firebase-service-account-json-files-to-vercel-ph5
const serviceAccount = JSON.parse(
process.env.FIREBASE_SERVICE_ACCOUNT_KEY as string,
)

const app: App = getApps().length
? getApp(PROJECT_ID)
: initializeApp(firebaseConfig, PROJECT_ID)
: initializeApp(
{
...firebaseConfig,
credential: admin.credential.cert(serviceAccount),
databaseURL: process.env.REALTIME_DATABASE_URL,
},
PROJECT_ID,
)

const Auth = getAuth(app)
const Database = getDatabase(app)

export const verifyIdToken = (idToken: string) =>
Auth.verifyIdToken(idToken, true)

export const revokeRefreshTokens = async (uid: string) => {
// Revoke all refresh tokens for a specified user for whatever reason
await Auth.revokeRefreshTokens(uid)

// Ref: https://firebase.google.com/docs/auth/admin/manage-sessions?hl=en#revoke_refresh_tokens
const user: UserRecord = await Auth.getUser(uid)
const revokeTime: number =
new Date(user.tokensValidAfterTime as string).getTime() / 1000

export const verifyIdToken = (idToken: string) => Auth.verifyIdToken(idToken)
// Ref: https://firebase.google.com/docs/auth/admin/manage-sessions?hl=en#detect_id_token_revocation_in
const metadataRef = Database.ref(`metadata/${uid}`)
await metadataRef.set({ revokeTime })
}