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

ENG-101-2: OAuth providers #58

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 69 additions & 2 deletions frontend/src/actions/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
'use server'

import { signIn, signUp } from '@/auth'
import { signIn, SignUpError } from '@/auth'
import { AuthError } from 'next-auth'
import { redirect } from 'next/navigation'
import { AuthParams, AuthResponse } from '@/types/auth'
import { curieoFetch } from '@/actions/fetch'
import { encodeAsUrlSearchParams, formToUrlParams } from '@/utils'
import { ResponseCookies } from 'next/dist/server/web/spec-extension/cookies'
import { cookies } from 'next/headers'

export async function signin(formData: FormData) {
try {
Expand All @@ -17,7 +22,7 @@ export async function signin(formData: FormData) {

export async function signup(formData: FormData) {
try {
await signUp(formData)
await curieoCredentialsSignUp(formData)
return redirect('/auth/signin')
} catch (error) {
if (error instanceof AuthError) {
Expand All @@ -26,3 +31,65 @@ export async function signup(formData: FormData) {
throw error
}
}

export async function curieoCredentialsSignIn({ username, password }: AuthParams): Promise<AuthResponse | null> {
/**
* Signs in to the search server using credentials and assigns the resulting session cookies correctly.
*/
const response = await curieoFetch('/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: encodeAsUrlSearchParams({
username: username.trim(),
password: password,
}),
})
if (response.ok) {
const setCookies = new ResponseCookies(response.headers)
setCookies.getAll().forEach(cookie => cookies().set(cookie))
return (await response.json()) as AuthResponse
}
return null
}

export async function curieoCredentialsSignUp(f: FormData): Promise<AuthResponse> {
// If email is not set we use username
if (!f.has('email')) {
f.set('email', f.get('username') || '')
}
let response = await curieoFetch('/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formToUrlParams(f),
})
if (response.ok) {
return (await response.json()) as AuthResponse
}
throw new SignUpError('Could not sign up')
}

export async function curieoOAuthSignInCallback({ email, accessToken }: any): Promise<AuthResponse | null> {
/**
* Signs in to the search server using credentials and assigns the resulting session cookies correctly.
*/
const response = await curieoFetch('/auth/oauth_callback', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: encodeAsUrlSearchParams({
username: username.trim(),
password: password,
}),
})
if (response.ok) {
const setCookies = new ResponseCookies(response.headers)
setCookies.getAll().forEach(cookie => cookies().set(cookie))
return (await response.json()) as AuthResponse
}
return null
}
130 changes: 128 additions & 2 deletions frontend/src/auth.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { NextAuthConfig } from 'next-auth'
import type { Account, NextAuthConfig, Profile, User } from 'next-auth'
import { AdapterUser } from '@auth/core/adapters'
import { CredentialInput } from 'next-auth/providers'
import type { Adapter } from 'next-auth/adapters'
import { passwordErrorMessage } from '@/constants/messages'

export const authConfig: NextAuthConfig = {
theme: {
Expand All @@ -16,7 +20,45 @@ export const authConfig: NextAuthConfig = {
},
providers: [], // Set in auth.ts
callbacks: {
async signIn() {
async signIn({
user,
account,
profile,
email,
credentials,
}: {
user: User | AdapterUser
account: Account | null
profile?: Profile
email?: {
verificationRequest?: boolean
}
credentials?: Record<string, CredentialInput>
}) {
switch (account?.type) {
case 'oidc':
console.debug('oidc')
break
case 'oauth':
console.debug('oauth')
break
case 'email':
console.debug('email')
break
case 'credentials':
console.debug('credentials')
break
case 'webauthn':
console.debug('webauthn')
break
}
console.debug('SignIn:')
console.debug(user)
console.debug(account)
console.debug(profile)
console.debug(email)
console.debug(credentials)

return true
},
authorized({ auth, request: { nextUrl } }) {
Expand All @@ -28,4 +70,88 @@ export const authConfig: NextAuthConfig = {
},
},
debug: process.env.NODE_ENV !== 'production',
adapter: httpAdapter(),
}

export function httpAdapter(): Adapter {
return {
async createUser(user) {
return user
},
async getUser(id) {
try {
return null
} catch (error) {
return null
}
},
async getUserByEmail(email) {
try {
return null
} catch (error) {
return null
}
},
async getUserByAccount(payload) {
try {
return null
} catch (error) {
return null
}
},
async updateUser(user) {
return { id: 1 }
},
async deleteUser(userId) {
try {
return null
} catch (error) {
return null
}
},
async linkAccount(account) {
try {
return null
} catch (error) {
return null
}
},
async unlinkAccount(args) {
return undefined
},
async createSession(session) {
return session
},
async getSessionAndUser(sessionToken) {
try {
return null
} catch (error) {
return null
}
},
async updateSession(session) {
try {
return null
} catch (error) {
return null
}
},
async deleteSession(sessionToken) {
return null
},
async createVerificationToken(verificationToken) {
try {
return null
} catch (error) {
return null
}
},
async useVerificationToken(params) {
try {
return null
} catch (error) {
return null
}
},
}
}
122 changes: 48 additions & 74 deletions frontend/src/auth.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,42 @@
import { authConfig } from '@/auth.config'
import { AuthParams, AuthResponse } from '@/types/auth'
import { encodeAsUrlSearchParams, formToUrlParams } from '@/utils'
import { AuthParams } from '@/types/auth'
import NextAuth, { AuthError, Session, User } from 'next-auth'
import { AccessDenied } from '@auth/core/errors'
import Credentials from 'next-auth/providers/credentials'
import { cookies } from 'next/headers'
import { curieoFetch } from '@/actions/fetch'
import { ResponseCookies } from 'next/dist/server/web/spec-extension/cookies'
import Google from '@auth/core/providers/google'
import { Provider } from '@auth/core/providers'
import Apple from '@auth/core/providers/apple'
import { curieoCredentialsSignIn } from '@/actions/auth'

const providers: Provider[] = [
Credentials({
// You can specify which fields should be submitted, by adding keys to the `credentials` object.
// e.g. domain, username, password, 2FA token, etc.
credentials: {
username: { label: 'username', type: 'text' },
password: { label: 'password', type: 'password' },
},
authorize: async (credentials, req) => {
try {
const response = await curieoCredentialsSignIn(credentials as AuthParams)
if (response !== null) {
return {
id: response.user_id,
name: response.email,
} as User
}
} catch (e) {
if (e instanceof AuthError) {
throw e
}
}
throw new AccessDenied('Could not log in')
},
}),
Google,
Apple,
]

export const {
handlers: { GET, POST },
Expand All @@ -15,62 +45,9 @@ export const {
signOut,
} = NextAuth({
...authConfig,
providers: [
Credentials({
// You can specify which fields should be submitted, by adding keys to the `credentials` object.
// e.g. domain, username, password, 2FA token, etc.
credentials: {
username: { label: 'username', type: 'text' },
password: { label: 'password', type: 'password' },
},
authorize: async (credentials, req) => {
async function login(p: AuthParams): Promise<AuthResponse | null> {
'use server'
const response = await curieoFetch('/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: encodeAsUrlSearchParams({
username: p.username.trim(),
password: p.password,
}),
})
if (response.ok) {
const setCookies = new ResponseCookies(response.headers)
setCookies.getAll().forEach(cookie => cookies().set(cookie))
return (await response.json()) as AuthResponse
}
return null
}

try {
const response = await login(credentials as AuthParams)
if (response !== null) {
return {
id: response.user_id,
name: response.email,
} as User
}
} catch (e) {
if (e instanceof AuthError) {
throw e
}
}
throw new AccessDenied('Could not log in')
},
}),
//Google({
// clientId: process.env.GOOGLE_CLIENT_ID,
// clientSecret: process.env.GOOGLE_CLIENT_SECRET,
//}),
],
providers: providers,
})

export function getCsrfToken() {
return cookies().get('next-auth.csrf-token')?.value.split('|')[0]
}

export async function auth(): Promise<Session | null> {
const session = await next_auth()
// Strips information from the returned user as a secondary defense against oversharing user info with the client
Expand All @@ -88,20 +65,17 @@ export class SignUpError extends AuthError {
static type = 'SignUpError'
}

export async function signUp(f: FormData): Promise<AuthResponse> {
// If email is not set we use username
if (!f.has('email')) {
f.set('email', f.get('username') || '')
}
let response = await curieoFetch('/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formToUrlParams(f),
})
if (response.ok) {
return (await response.json()) as AuthResponse
}
throw new SignUpError('Could not sign up')
export function getCsrfToken() {
return cookies().get('next-auth.csrf-token')?.value.split('|')[0]
}

export const oauthProviders = providers
.filter(p => p.name != 'Credentials')
.map(provider => {
if (typeof provider === 'function') {
const providerData = provider()
return { id: providerData.id, name: providerData.name }
} else {
return { id: provider.id, name: provider.name }
}
})
Loading