Skip to content

Commit

Permalink
feat: support Next.js v12.2 middleware
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Next.js v12.2 has a disruptive change to the middleware.
See Vercel's documentation for details.
  • Loading branch information
aiji42 committed Jul 11, 2022
1 parent 78d05c7 commit 24d206f
Show file tree
Hide file tree
Showing 7 changed files with 744 additions and 947 deletions.
42 changes: 35 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type Middleware = (request: NextRequest, event?: NextFetchEvent) => Response | u
[example](https://next-fortress.vercel.app/ip)

```ts
// /pages/admin/_middleware.ts
// middleware.ts
import { makeIPInspector } from 'next-fortress/ip'

/*
Expand All @@ -58,6 +58,10 @@ export const middleware = makeIPInspector('123.123.123.123/32', {
type: 'redirect',
destination: '/'
})

export const config = {
matcher: ['/admin/:path*'],
}
```

### Control by Firebase
Expand All @@ -66,7 +70,7 @@ export const middleware = makeIPInspector('123.123.123.123/32', {


```ts
// /pages/mypage/_middleware.ts
// middleware.ts
import { makeFirebaseInspector } from 'next-fortress/firebase'

/*
Expand All @@ -78,6 +82,10 @@ import { makeFirebaseInspector } from 'next-fortress/firebase'
export const middleware = makeFirebaseInspector(
{ type: 'redirect', destination: '/signin' }
)

export const config = {
matcher: ['/mypage/:path*'],
}
```

Put the Firebase user token into the cookie using the following example.
Expand All @@ -103,14 +111,18 @@ firebase.auth().onAuthStateChanged(function (user) {
For the second argument of `makeFirebaseInspector`, you can pass a payload inspection function. This is useful, for example, if you want to ignore some authentication providers, or if you need to ensure that the email has been verified.
If this function returns false, it will enter the fallback case.
```ts
// /pages/mypage/_middleware.ts
// middleware.ts
import { makeFirebaseInspector } from 'next-fortress/firebase'

// Redirect for anonymous users.
export const middleware = makeFirebaseInspector(
{ type: 'redirect', destination: '/signin' },
(payload) => payload.firebase.sign_in_provider !== 'anonymous'
)

export const config = {
matcher: ['/mypage/:path*'],
}
```

**NOTE**
Expand All @@ -122,7 +134,7 @@ export const middleware = makeFirebaseInspector(
[example](https://next-fortress.vercel.app/cognito)

```ts
// /pages/mypage/_middleware.ts
// middleware.ts
import { makeCognitoInspector } from 'next-fortress/cognito'

/*
Expand All @@ -146,6 +158,10 @@ export const middleware = makeCognitoInspector(
userPoolWebClientId: process.env.COGNITO_USER_POOL_WEB_CLIENT_ID,
}
)

export const config = {
matcher: ['/mypage/:path*'],
}
```

Add `ssr: true` option to `Amplify.configure` to handle the Cognito cookies on the edge.
Expand All @@ -163,7 +179,7 @@ Amplify.configure({
For the 3rd argument of `makeCognitoInspector`, you can pass a payload inspection function. This is useful, for example, if you want to ignore some authentication providers, or if you need to ensure that the email has been verified.
If this function returns false, it will enter the fallback case.
```ts
// /pages/mypage/_middleware.ts
// middleware.ts
import { makeCognitoInspector } from 'next-fortress/cognito'

// Fallback if the email address is not verified.
Expand All @@ -176,14 +192,18 @@ export const middleware = makeCognitoInspector(
},
(payload) => payload.email_verified
)

export const config = {
matcher: ['/mypage/:path*'],
}
```

### Control by Auth0

[example](https://next-fortress.vercel.app/auth0)

```ts
// /pages/mypage/_middleware.ts
// middleware.ts
import { makeAuth0Inspector } from 'next-fortress/auth0'

/*
Expand All @@ -197,6 +217,10 @@ export const middleware = makeAuth0Inspector(
{ type: 'redirect', destination: '/singin' },
'/api/auth/me' // api endpoint for auth0 profile
)

export const config = {
matcher: ['/mypage/:path*'],
}
```

To use Auth0, the api root must have an endpoint. [@auth0/nextjs-auth0](https://github.com/auth0/nextjs-auth0#basic-setup)
Expand All @@ -210,7 +234,7 @@ export default handleAuth()
For the third argument of `makeAuth0Inspector`, you can pass a payload inspection function. This is useful, for example, if you need to ensure that the email has been verified.
If this function returns false, it will enter the fallback case.
```ts
// /pages/mypage/_middleware.ts
// middleware.ts
import { makeAuth0Inspector } from 'next-fortress/auth0'

// Fallback if the email address is not verified.
Expand All @@ -219,6 +243,10 @@ export const middleware = makeAuth0Inspector(
'/api/auth/me',
(payload) => payload.email_verified
)

export const config = {
matcher: ['/mypage/:path*'],
}
```

## Contributing
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,15 @@
"devDependencies": {
"@commitlint/cli": "^17.0.3",
"@commitlint/config-conventional": "^17.0.3",
"@edge-runtime/vm": "^1.1.0-beta.11",
"@types/netmask": "^1.0.30",
"@types/node": "^18.0.3",
"c8": "^7.11.3",
"esbuild": "^0.14.49",
"fetch-mock": "^9.11.0",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"next": "^12.1.5",
"next": "^12.2.2",
"prettier": "^2.7.1",
"semantic-release": "^19.0.3",
"typescript": "^4.7.4",
Expand Down
99 changes: 29 additions & 70 deletions src/__tests__/firebase.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
/**
* @vitest-environment edge-runtime
*/
import { vi, describe, beforeEach, test, expect, Mock } from 'vitest'
import { makeFirebaseInspector } from '../firebase'
import { NextFetchEvent, NextRequest } from 'next/server'
import type { NextFetchEvent } from 'next/server'
const { NextRequest } = require('next/server')
import { handleFallback } from '../handle-fallback'
import { Fallback } from '../types'
import fetchMock from 'fetch-mock'
import * as fetchMock from 'fetch-mock'
import { decodeProtectedHeader, jwtVerify, importX509 } from 'jose'

vi.mock('jose', () => ({
Expand Down Expand Up @@ -51,9 +55,10 @@ describe('makeFirebaseInspector', () => {
})

test('has no cookies', async () => {
await makeFirebaseInspector(fallback)({ cookies: {} } as NextRequest, event)
const req = new NextRequest('https://example.com')
await makeFirebaseInspector(fallback)(req, event)

expect(handleFallback).toBeCalledWith(fallback, { cookies: {} }, event)
expect(handleFallback).toBeCalledWith(fallback, req, event)
})

test('has the firebase cookie', async () => {
Expand All @@ -63,14 +68,9 @@ describe('makeFirebaseInspector', () => {
;(jwtVerify as Mock).mockReturnValue(
new Promise((resolve) => resolve(true))
)
await makeFirebaseInspector(fallback)(
{
cookies: {
__fortressFirebaseSession: 'x.x.x'
}
} as unknown as NextRequest,
event
)
const req = new NextRequest('https://example.com')
req.cookies.set('__fortressFirebaseSession', 'x.x.x')
await makeFirebaseInspector(fallback)(req, event)

expect(handleFallback).not.toBeCalled()
})
Expand All @@ -86,14 +86,9 @@ describe('makeFirebaseInspector', () => {
;(jwtVerify as Mock).mockReturnValue(
new Promise((resolve) => resolve(true))
)
await makeFirebaseInspector(fallback)(
{
cookies: {
session: 'x.x.x'
}
} as unknown as NextRequest,
event
)
const req = new NextRequest('https://example.com')
req.cookies.set('session', 'x.x.x')
await makeFirebaseInspector(fallback)(req, event)

expect(handleFallback).not.toBeCalled()
})
Expand All @@ -113,17 +108,12 @@ describe('makeFirebaseInspector', () => {
})
)
)
const req = new NextRequest('https://example.com')
req.cookies.set('__fortressFirebaseSession', 'x.x.x')
await makeFirebaseInspector(
fallback,
(res) => res.firebase.sign_in_provider === 'google.com'
)(
{
cookies: {
__fortressFirebaseSession: 'x.x.x'
}
} as unknown as NextRequest,
event
)
)(req, event)

expect(handleFallback).not.toBeCalled()
})
Expand All @@ -136,49 +126,23 @@ describe('makeFirebaseInspector', () => {
new Promise((resolve, reject) => reject(false))
)
const token = 'x.y.z'
await makeFirebaseInspector(fallback)(
{
cookies: {
__fortressFirebaseSession: token
}
} as unknown as NextRequest,
event
)
const req = new NextRequest('https://example.com')
req.cookies.set('__fortressFirebaseSession', token)
await makeFirebaseInspector(fallback)(req, event)

expect(handleFallback).toBeCalledWith(
fallback,
{
cookies: {
__fortressFirebaseSession: token
}
},
event
)
expect(handleFallback).toBeCalledWith(fallback, req, event)
})

test('jwks expired.', async () => {
;(decodeProtectedHeader as Mock).mockReturnValue({
kid: 'kid3'
})
const token = 'x.y.z'
await makeFirebaseInspector(fallback)(
{
cookies: {
__fortressFirebaseSession: token
}
} as unknown as NextRequest,
event
)
const req = new NextRequest('https://example.com')
req.cookies.set('__fortressFirebaseSession', token)
await makeFirebaseInspector(fallback)(req, event)

expect(handleFallback).toBeCalledWith(
fallback,
{
cookies: {
__fortressFirebaseSession: token
}
},
event
)
expect(handleFallback).toBeCalledWith(fallback, req, event)
expect(importX509).toBeCalledWith(undefined, 'RS256')
})

Expand All @@ -194,14 +158,9 @@ describe('makeFirebaseInspector', () => {
new Promise((resolve) => resolve(true))
)
const token = 'x.y.z'
await makeFirebaseInspector(fallback)(
{
cookies: {
__fortressFirebaseSession: token
}
} as unknown as NextRequest,
event
)
const req = new NextRequest('https://example.com')
req.cookies.set('__fortressFirebaseSession', token)
await makeFirebaseInspector(fallback)(req, event)

expect(handleFallback).not.toBeCalled()
expect(importX509).toBeCalledWith('zzzzzzzzzz', 'RS256')
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/handle-fallback.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('handleFallback', () => {
NextResponse.redirect = vi.fn()
handleFallback(
{ type: 'redirect', destination: '/foo/bar' },
{ ...dummyRequest, preflight: 1 } as unknown as NextRequest,
{ ...dummyRequest, method: 'OPTIONS' } as unknown as NextRequest,
dummyEvent
)
expect(NextResponse.redirect).not.toBeCalled()
Expand Down
2 changes: 1 addition & 1 deletion src/firebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const verifyFirebaseIdToken = async (
): Promise<boolean> => {
const cookieKey =
process.env.FORTRESS_FIREBASE_COOKIE_KEY ?? FIREBASE_COOKIE_KEY
const token = req.cookies[cookieKey]
const token = req.cookies.get(cookieKey)
if (!token) return false

const endpoint =
Expand Down
2 changes: 1 addition & 1 deletion src/handle-fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const handleFallback = (
event: NextFetchEvent
): ReturnType<NextMiddleware> => {
if (typeof fallback === 'function') return fallback(request, event)
if (request.preflight) return new NextResponse(null)
if (request.method === 'OPTIONS') return new NextResponse(null)
if (fallback.type === 'rewrite') {
const url = request.nextUrl.clone()
url.pathname = fallback.destination
Expand Down
Loading

0 comments on commit 24d206f

Please sign in to comment.