Skip to content

Commit

Permalink
feat: support next v13 app dir routes
Browse files Browse the repository at this point in the history
  • Loading branch information
PHILLIPS71 committed Feb 25, 2023
1 parent be80fec commit 7e82016
Show file tree
Hide file tree
Showing 10 changed files with 182 additions and 128 deletions.
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@
"react-dom": ">=18"
},
"dependencies": {
"cookie": "^0.5.0",
"isomorphic-fetch": "^3.0.0",
"query-string": "^8.1.0"
}
}
61 changes: 35 additions & 26 deletions src/api/authentication.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,47 @@
import type { AuthProvider } from '@/typings/providers.types'
import type { NextAuthRequest } from '@/typings/route.types'
import type { NextApiResponse } from 'next'
import type { NextAuthRequest, NextAuthRequestParams } from '@/typings/route.types'

import { NextResponse } from 'next/server'

import logout from '@/api/routes/logout'
import session from '@/api/routes/session'
import signin from '@/api/routes/signin'
import CookieStore from '@/storage/cookie-store'

type AuthenticationOptions = {
providers: AuthProvider[]
}

const NextAuth = (options: AuthenticationOptions) => async (req: NextAuthRequest, res: NextApiResponse) => {
const { providers } = options

if (!req.query.auth) {
return res.status(400).end()
}

const params = Array.isArray(req.query.auth) ? req.query.auth : [req.query.auth]
const provider = providers.find((p) => p.id === params[1])
if (provider == null) {
return res.status(400).end()
const NextAuth =
(options: AuthenticationOptions) =>
async (request: NextAuthRequest, { params }: NextAuthRequestParams) => {
const { providers } = options

if (!params.auth) {
return NextResponse.next({
status: 400,
statusText: `The provider route params 'auth' were not found in the request.`,
})
}

const provider = providers.find((p) => p.id === params?.auth?.[1])
if (provider == null) {
return NextResponse.next({
status: 400,
statusText: `The provider id '${params?.auth?.[1]}' has not been defined.`,
})
}

const redirectUrl = request.nextUrl.searchParams.get('redirect_url') || request.nextUrl.origin
switch (params.auth[0]) {
case 'signin':
return signin(request, { provider, redirect_url: redirectUrl })
case 'logout':
return logout(request, { provider, redirect_url: redirectUrl })
case 'session':
return session(request, { provider })
default:
return NextResponse.next({ status: 400 })
}
}

const store = new CookieStore(req, res)

switch (req.query.auth[0]) {
case 'signin':
return signin(req, res, store, { provider, redirect_url: req.query.redirect_url as string })
case 'session':
return session(req, res, { provider })
default:
return res.status(400).end()
}
}

export default NextAuth
37 changes: 21 additions & 16 deletions src/api/grants/password.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
import type { SignInRouteOptions } from '@/api/routes/signin'
import type CookieStore from '@/storage/cookie-store'
import type { OAuthTokenResponse } from '@/typings/oauth.types'
import type { NextAuthRequest } from '@/typings/route.types'
import type { NextApiResponse } from 'next'

import * as qs from 'query-string'
import { NextResponse } from 'next/server'
import qs from 'query-string'

import 'isomorphic-fetch'
import CookieStore from '@/storage/cookie-store'

const doPasswordSignIn = async (
req: NextAuthRequest,
res: NextApiResponse,
store: CookieStore,
options: SignInRouteOptions
): Promise<NextApiResponse<any> | any> => {
const doPasswordSignIn = async (request: NextAuthRequest, options: SignInRouteOptions): Promise<NextResponse> => {
const { provider } = options

const endpoint = provider.endpoints.token
if (!endpoint) {
return res.status(400).end(`The provider '${provider.name}' has no token endpoint defined.`)
return NextResponse.next({
status: 400,
statusText: `The provider '${provider.name}' has no token endpoint defined.`,
})
}

const body = await request.formData()
const params = {
grant_type: provider.grant_type,
client_id: provider.client_id,
client_secret: provider.client_secret,
scope: provider.scope,
username: req.body.username,
password: req.body.password,
username: body.get('username'),
password: body.get('password'),
}

const response = await fetch(endpoint, {
method: 'POST',
cache: 'no-cache',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
Expand All @@ -40,13 +39,19 @@ const doPasswordSignIn = async (

if (!response.ok) {
const json = await response.json()
return res.status(400).json(json)
return NextResponse.json(json, {
status: 400,
statusText: `The login password route received a non 200 response and got '${response.status}'.`,
})
}

const payload = (await response.json()) as OAuthTokenResponse
store.store(provider, payload)

return res.redirect(options.redirect_url ?? '/')
const redirect = NextResponse.redirect(options.redirect_url)
const cookies = CookieStore.create(provider, payload)
cookies.forEach((cookie) => redirect.cookies.set(cookie))

return redirect
}

export default doPasswordSignIn
57 changes: 57 additions & 0 deletions src/api/routes/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { AuthProvider } from '@/typings/providers.types'
import type { NextAuthRequest } from '@/typings/route.types'

import { NextResponse } from 'next/server'

export type LogoutRouteOptions = {
provider: AuthProvider
redirect_url: string
}

const logout = async (req: NextAuthRequest, options: LogoutRouteOptions): Promise<NextResponse> => {
const { provider } = options

if (req.method !== 'POST') {
return NextResponse.next({
status: 400,
statusText: `The logout route expects a POST request but received '${req.method}'.`,
})
}

if (!provider) {
return NextResponse.next({
status: 400,
statusText: `The logout route expects a provider to be included but received '${provider}'.`,
})
}

const endpoint = provider.endpoints.logout
if (!endpoint) {
return NextResponse.next({
status: 400,
statusText: `The logout route expects a logout endpoint to be configured but received '${endpoint}'.`,
})
}

const response = await fetch(endpoint, {
method: 'POST',
cache: 'no-cache',
headers: {
Authorization: req.headers.get('authorization') as string,
'Content-Type': 'application/x-www-form-urlencoded',
'X-Forwarded-Host': req.headers.get('host') as string,
},
})

if (!response.ok) {
const json = await response.json()
return NextResponse.json(json, {
status: 400,
statusText: `The logout route received a non 200 response and got '${response.status}'.`,
})
}

return NextResponse.redirect(options.redirect_url)
}

export default logout
38 changes: 24 additions & 14 deletions src/api/routes/session.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,56 @@
import type { OAuthUserInfoResponse } from '@/typings/oauth.types'
import type { AuthProvider } from '@/typings/providers.types'
import type { NextAuthRequest } from '@/typings/route.types'
import type { NextApiResponse } from 'next'

import { NextResponse } from 'next/server'

export type SessionRouteOptions = {
provider: AuthProvider
}

const session = async (
req: NextAuthRequest,
res: NextApiResponse,
options: SessionRouteOptions
): Promise<NextApiResponse<any> | any> => {
const session = async (req: NextAuthRequest, options: SessionRouteOptions): Promise<NextResponse> => {
const { provider } = options

if (req.method === 'POST') {
return res.status(400).end()
return NextResponse.next({
status: 400,
statusText: `The session route expects a POST request but received '${req.method}'.`,
})
}

if (!provider) {
return res.status(400).end()
return NextResponse.next({
status: 400,
statusText: `The session route expects a provider to be included but received '${provider}'.`,
})
}

const endpoint = provider.endpoints.userinfo
if (endpoint == null) {
return res.status(400).end()
if (!endpoint) {
return NextResponse.next({
status: 400,
statusText: `The session route expects a userinfo endpoint to be configured but received '${endpoint}'.`,
})
}

const response = await fetch(endpoint, {
method: 'GET',
cache: 'no-cache',
headers: {
Authorization: req.headers.authorization as string,
'X-Forwarded-Host': req.headers.host as string,
Authorization: req.headers.get('authorization') as string,
'X-Forwarded-Host': req.headers.get('host') as string,
},
})

if (!response.ok) {
return res.status(400).end()
return NextResponse.next({
status: 400,
statusText: `The session route received a non 200 response and got '${response.status}'.`,
})
}

const payload = (await response.json()) as OAuthUserInfoResponse
return res.json(payload)
return NextResponse.json(payload)
}

export default session
30 changes: 17 additions & 13 deletions src/api/routes/signin.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,40 @@
import type CookieStore from '@/storage/cookie-store'
import type { AuthProvider } from '@/typings/providers.types'
import type { NextAuthRequest } from '@/typings/route.types'
import type { NextApiResponse } from 'next'

import { NextResponse } from 'next/server'

import doPasswordSignIn from '@/api/grants/password'

export type SignInRouteOptions = {
provider: AuthProvider
redirect_url?: string
redirect_url: string
}

const signin = async (
req: NextAuthRequest,
res: NextApiResponse,
store: CookieStore,
options: SignInRouteOptions
): Promise<NextApiResponse<any> | any> => {
const signin = async (req: NextAuthRequest, options: SignInRouteOptions): Promise<NextResponse> => {
const { provider } = options

if (req.method !== 'POST') {
return res.status(400).end()
return NextResponse.next({
status: 400,
statusText: `The signin route expects a POST request but received '${req.method}'.`,
})
}

if (!provider) {
return res.status(400).end()
return NextResponse.next({
status: 400,
statusText: `The signin route expects a provider to be included but received '${provider}'.`,
})
}

switch (provider.grant_type) {
case 'password':
return doPasswordSignIn(req, res, store, options)
return doPasswordSignIn(req, options)
default:
return res.status(400).end()
return NextResponse.next({
status: 400,
statusText: `The signin route cannot handle grant_type of '${provider.grant_type}'.`,
})
}
}

Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export { default as OAuth } from '@/api/authentication'
export { getServerSession } from '@/api/session'

export { default as SessionContext, SessionProvider } from '@/react/context'
export { default as getSessionServerSideProps } from '@/react/ssr'

export { default as PasswordProvider } from '@/providers/password'
2 changes: 2 additions & 0 deletions src/react/context.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client'

import type { NextSession } from '@/typings/session.types'

import React, { createContext, useMemo, useState } from 'react'
Expand Down
Loading

0 comments on commit 7e82016

Please sign in to comment.