-
-
Notifications
You must be signed in to change notification settings - Fork 231
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- [#44](#43): feat: Add `withMiddlewareAuthRequired` Nextjs Middleware util to protect directories. - [#43](#43): feat: store `provider_token` in cookie.
- Loading branch information
1 parent
e188cb9
commit 39c8a06
Showing
11 changed files
with
307 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
// pages/protected-page.js | ||
import { | ||
User, | ||
withAuthRequired, | ||
getUser | ||
} from '@supabase/supabase-auth-helpers/nextjs'; | ||
import Link from 'next/link'; | ||
|
||
export default function ProtectedPage({ | ||
user, | ||
allRepos | ||
}: { | ||
user: User; | ||
allRepos: any; | ||
}) { | ||
return ( | ||
<> | ||
<p> | ||
[<Link href="/">Home</Link>] | [ | ||
<Link href="/profile">withAuthRequired</Link>] | ||
</p> | ||
<div>Protected content for {user.email}</div> | ||
<p>Data fetched with provider token:</p> | ||
<pre>{JSON.stringify(allRepos, null, 2)}</pre> | ||
<p>user:</p> | ||
<pre>{JSON.stringify(user, null, 2)}</pre> | ||
</> | ||
); | ||
} | ||
|
||
export const getServerSideProps = withAuthRequired({ | ||
redirectTo: '/', | ||
async getServerSideProps(ctx) { | ||
// Retrieve provider_token from cookies | ||
const provider_token = ctx.req.cookies['sb-provider-token']; | ||
// Get logged in user's third-party id from metadata | ||
const { user } = await getUser(ctx); | ||
const userId = user?.user_metadata.user_name; | ||
const allRepos = await ( | ||
await fetch( | ||
`https://api.github.com/search/repositories?q=user:${userId}`, | ||
{ | ||
method: 'GET', | ||
headers: { | ||
Authorization: `token ${provider_token}` | ||
} | ||
} | ||
) | ||
).json(); | ||
return { props: { allRepos, user } }; | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { withMiddlewareAuthRequired } from '@supabase/supabase-auth-helpers/nextjs'; | ||
|
||
export const middleware = withMiddlewareAuthRequired(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import type { NextPage } from 'next'; | ||
|
||
const Page: NextPage = () => <p>Authenticated</p>; | ||
|
||
export default Page; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
// import { NextResponse } from 'next/server'; TODO fix import | ||
import { NextResponse } from 'next/dist/server/web/spec-extension/response'; | ||
import { NextMiddleware } from 'next/server'; | ||
import { User, ApiError, createClient } from '@supabase/supabase-js'; | ||
import { CookieOptions } from '../types'; | ||
import { COOKIE_OPTIONS } from '../../shared/utils/constants'; | ||
import { jwtDecoder } from '../../shared/utils/jwt'; | ||
import { setCookies } from '../../shared/utils/cookies'; | ||
import { | ||
NextRequestAdapter, | ||
NextResponseAdapter | ||
} from '../../shared/adapters/NextMiddlewareAdapter'; | ||
|
||
export type WithMiddlewareAuthRequired = (options?: { | ||
/** | ||
* Path relative to the site root to redirect an | ||
* unauthenticated visitor. | ||
* | ||
* The original request route will be appended via | ||
* a `redirectedFrom` query parameter, ex: `?redirectedFrom=%2Fdashboard` | ||
*/ | ||
redirectTo?: string; | ||
cookieOptions?: CookieOptions; | ||
}) => NextMiddleware; | ||
|
||
export const withMiddlewareAuthRequired: WithMiddlewareAuthRequired = | ||
(options: { redirectTo?: string; cookieOptions?: CookieOptions } = {}) => | ||
async (req) => { | ||
try { | ||
if ( | ||
!process.env.NEXT_PUBLIC_SUPABASE_URL || | ||
!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY | ||
) { | ||
throw new Error( | ||
'NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY env variables are required!' | ||
); | ||
} | ||
if (!req.cookies) { | ||
throw new Error('Not able to parse cookies!'); | ||
} | ||
const supabase = createClient( | ||
process.env.NEXT_PUBLIC_SUPABASE_URL, | ||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, | ||
{ fetch } | ||
); | ||
const cookieOptions = { ...COOKIE_OPTIONS, ...options.cookieOptions }; | ||
const access_token = req.cookies[`${cookieOptions.name!}-access-token`]; | ||
const refresh_token = req.cookies[`${cookieOptions.name!}-refresh-token`]; | ||
|
||
const res = NextResponse.next(); | ||
|
||
const getUser = async (): Promise<{ | ||
user: User | null; | ||
error: ApiError | null; | ||
}> => { | ||
if (!access_token) { | ||
throw new Error('No cookie found!'); | ||
} | ||
// Get payload from access token. | ||
const jwtUser = jwtDecoder(access_token); | ||
if (!jwtUser?.exp) { | ||
throw new Error('Not able to parse JWT payload!'); | ||
} | ||
const timeNow = Math.round(Date.now() / 1000); | ||
if (jwtUser.exp < timeNow) { | ||
if (!refresh_token) { | ||
throw new Error('No refresh_token cookie found!'); | ||
} | ||
const { data, error } = await supabase.auth.api.refreshAccessToken( | ||
refresh_token | ||
); | ||
setCookies( | ||
new NextRequestAdapter(req), | ||
new NextResponseAdapter(res), | ||
[ | ||
{ key: 'access-token', value: data!.access_token }, | ||
{ key: 'refresh-token', value: data!.refresh_token! } | ||
].map((token) => ({ | ||
name: `${cookieOptions.name}-${token.key}`, | ||
value: token.value, | ||
domain: cookieOptions.domain, | ||
maxAge: cookieOptions.lifetime ?? 0, | ||
path: cookieOptions.path, | ||
sameSite: cookieOptions.sameSite | ||
})) | ||
); | ||
return { user: data?.user ?? null, error }; | ||
} | ||
return { user: jwtUser, error: null }; | ||
}; | ||
|
||
const authResult = await getUser(); | ||
|
||
if (authResult.error) { | ||
throw new Error( | ||
`Authorization error, redirecting to login page: ${authResult.error.message}` | ||
); | ||
} else if (!authResult.user) { | ||
throw new Error('No auth user, redirecting'); | ||
} | ||
|
||
// Authentication successful, forward request to protected route | ||
return res; | ||
} catch (err: unknown) { | ||
const { redirectTo = '/' } = options; | ||
if (err instanceof Error) { | ||
console.log( | ||
`Could not authenticate request, redirecting to ${redirectTo}:`, | ||
err | ||
); | ||
} | ||
const redirectUrl = req.nextUrl.clone(); | ||
redirectUrl.pathname = redirectTo; | ||
redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname); | ||
// Authentication failed, redirect request | ||
return NextResponse.redirect(redirectUrl); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { NextRequest, NextResponse } from 'next/server'; | ||
|
||
import { RequestAdapter, ResponseAdapter } from './types'; | ||
|
||
export class NextRequestAdapter implements RequestAdapter { | ||
private req: NextRequest; | ||
constructor(request: NextRequest) { | ||
this.req = request; | ||
} | ||
|
||
setRequestCookie(name: string, value: string) { | ||
this.req.cookies[name] = value; | ||
} | ||
|
||
getHeader(name: string) { | ||
return this.req.headers.get(name); | ||
} | ||
} | ||
|
||
export class NextResponseAdapter implements ResponseAdapter { | ||
private res: NextResponse; | ||
constructor(response: NextResponse) { | ||
this.res = response; | ||
} | ||
|
||
getHeader(name: string) { | ||
return this.res.headers.get(name); | ||
} | ||
|
||
setHeader(name: string, value: string) { | ||
this.res.headers.set(name, value); | ||
return this; | ||
} | ||
} |