Skip to content

Commit

Permalink
OIDC protocol for automatic discovery/configuration of oauth2 clients.
Browse files Browse the repository at this point in the history
  • Loading branch information
ivarflakstad committed Jun 25, 2024
1 parent ac8d43e commit fa1b731
Show file tree
Hide file tree
Showing 22 changed files with 957 additions and 518 deletions.
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
}
1 change: 0 additions & 1 deletion frontend/src/actions/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ function curieoApiUrl(reqInfo?: RequestInfo): URL {

export async function curieoFetch(reqInfo: RequestInfo, init?: RequestInit): Promise<Response> {
const cookie = next_headers().get('cookie')
console.debug(cookie)
let headers = init ? new Headers(init.headers) : new Headers()
if (cookie) {
headers.set('cookie', cookie)
Expand Down
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
}
},
}
}
46 changes: 3 additions & 43 deletions frontend/src/auth.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
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({
Expand All @@ -20,28 +18,8 @@ const providers: Provider[] = [
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)
const response = await curieoCredentialsSignIn(credentials as AuthParams)
if (response !== null) {
return {
id: response.user_id,
Expand Down Expand Up @@ -87,24 +65,6 @@ 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]
}
Expand Down
Loading

0 comments on commit fa1b731

Please sign in to comment.