style: cleanup oidc spaghetti

This commit is contained in:
Aarnav Tale 2024-05-21 23:57:03 -04:00
parent 84855d9d51
commit 694b22f205
No known key found for this signature in database
3 changed files with 70 additions and 56 deletions

View File

@ -27,11 +27,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
// Only set if OIDC is properly enabled anyways // Only set if OIDC is properly enabled anyways
if (context.oidc?.disableKeyLogin) { if (context.oidc?.disableKeyLogin) {
return startOidc( return startOidc(context.oidc, request)
context.oidc.issuer,
context.oidc.client,
request,
)
} }
return { return {
@ -46,16 +42,13 @@ export async function action({ request }: ActionFunctionArgs) {
if (oidcStart) { if (oidcStart) {
const context = await loadContext() const context = await loadContext()
const issuer = context.oidc?.issuer
const id = context.oidc?.client
if (!issuer || !id) { if (!context.oidc) {
throw new Error('An invalid OIDC configuration was provided') throw new Error('An invalid OIDC configuration was provided')
} }
// We know it exists here because this action only happens on OIDC // We know it exists here because this action only happens on OIDC
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion return startOidc(context.oidc, request)
return startOidc(issuer, id, request)
} }
const apiKey = String(formData.get('api-key')) const apiKey = String(formData.get('api-key'))

View File

@ -9,10 +9,5 @@ export async function loader({ request }: LoaderFunctionArgs) {
throw new Error('An invalid OIDC configuration was provided') throw new Error('An invalid OIDC configuration was provided')
} }
return finishOidc( return finishOidc(context.oidc, request)
context.oidc.issuer,
context.oidc.client,
context.oidc.secret,
request,
)
} }

View File

@ -1,36 +1,42 @@
import { redirect } from '@remix-run/node' import { redirect } from '@remix-run/node'
import { import {
authorizationCodeGrantRequest, authorizationCodeGrantRequest,
calculatePKCECodeChallenge, type Client, calculatePKCECodeChallenge,
type Client,
discoveryRequest, discoveryRequest,
generateRandomCodeVerifier, generateRandomCodeVerifier,
generateRandomNonce, generateRandomNonce,
generateRandomState, generateRandomState,
getValidatedIdTokenClaims, isOAuth2Error, getValidatedIdTokenClaims,
isOAuth2Error,
parseWwwAuthenticateChallenges, parseWwwAuthenticateChallenges,
processAuthorizationCodeOpenIDResponse, processAuthorizationCodeOpenIDResponse,
processDiscoveryResponse, processDiscoveryResponse,
validateAuthResponse } from 'oauth4webapi' validateAuthResponse,
} from 'oauth4webapi'
import { post } from '~/utils/headscale' import { post } from '~/utils/headscale'
import { commitSession, getSession } from '~/utils/sessions' import { commitSession, getSession } from '~/utils/sessions'
export async function startOidc(issuer: string, client: string, request: Request) { import { HeadplaneContext } from './config/headplane'
const session = await getSession(request.headers.get('Cookie'))
type OidcConfig = NonNullable<HeadplaneContext['oidc']>
export async function startOidc(oidc: OidcConfig, req: Request) {
const session = await getSession(req.headers.get('Cookie'))
if (session.has('hsApiKey')) { if (session.has('hsApiKey')) {
return redirect('/', { return redirect('/', {
status: 302, status: 302,
headers: { headers: {
// eslint-disable-next-line @typescript-eslint/naming-convention 'Set-Cookie': await commitSession(session),
'Set-Cookie': await commitSession(session) },
}
}) })
} }
const issuerUrl = new URL(issuer) const issuerUrl = new URL(oidc.issuer)
const oidcClient = { const oidcClient = {
client_id: client, client_id: oidc.client,
token_endpoint_auth_method: 'client_secret_basic' token_endpoint_auth_method: 'client_secret_basic',
} satisfies Client } satisfies Client
const response = await discoveryRequest(issuerUrl) const response = await discoveryRequest(issuerUrl)
@ -44,9 +50,9 @@ export async function startOidc(issuer: string, client: string, request: Request
const verifier = generateRandomCodeVerifier() const verifier = generateRandomCodeVerifier()
const challenge = await calculatePKCECodeChallenge(verifier) const challenge = await calculatePKCECodeChallenge(verifier)
const callback = new URL('/admin/oidc/callback', request.url) const callback = new URL('/admin/oidc/callback', req.url)
callback.protocol = request.url.includes('localhost') ? 'http:' : 'https:' callback.protocol = req.url.includes('localhost') ? 'http:' : 'https:'
callback.hostname = request.headers.get('Host') ?? '' callback.hostname = req.headers.get('Host') ?? ''
const authUrl = new URL(processed.authorization_endpoint) const authUrl = new URL(processed.authorization_endpoint)
authUrl.searchParams.set('client_id', oidcClient.client_id) authUrl.searchParams.set('client_id', oidcClient.client_id)
@ -65,29 +71,27 @@ export async function startOidc(issuer: string, client: string, request: Request
return redirect(authUrl.href, { return redirect(authUrl.href, {
status: 302, status: 302,
headers: { headers: {
// eslint-disable-next-line @typescript-eslint/naming-convention 'Set-Cookie': await commitSession(session),
'Set-Cookie': await commitSession(session) },
}
}) })
} }
export async function finishOidc(issuer: string, client: string, secret: string, request: Request) { export async function finishOidc(oidc: OidcConfig, req: Request) {
const session = await getSession(request.headers.get('Cookie')) const session = await getSession(req.headers.get('Cookie'))
if (session.has('hsApiKey')) { if (session.has('hsApiKey')) {
return redirect('/', { return redirect('/', {
status: 302, status: 302,
headers: { headers: {
// eslint-disable-next-line @typescript-eslint/naming-convention 'Set-Cookie': await commitSession(session),
'Set-Cookie': await commitSession(session) },
}
}) })
} }
const issuerUrl = new URL(issuer) const issuerUrl = new URL(oidc.issuer)
const oidcClient = { const oidcClient = {
client_id: client, client_id: oidc.client,
client_secret: secret, client_secret: oidc.secret,
token_endpoint_auth_method: 'client_secret_basic' token_endpoint_auth_method: 'client_secret_basic',
} satisfies Client } satisfies Client
const response = await discoveryRequest(issuerUrl) const response = await discoveryRequest(issuerUrl)
@ -103,22 +107,41 @@ export async function finishOidc(issuer: string, client: string, secret: string,
throw new Error('No OIDC state found in the session') throw new Error('No OIDC state found in the session')
} }
const parameters = validateAuthResponse(processed, oidcClient, new URL(request.url), state) const parameters = validateAuthResponse(
processed,
oidcClient,
new URL(req.url),
state,
)
if (isOAuth2Error(parameters)) { if (isOAuth2Error(parameters)) {
throw new Error('Invalid response from the OIDC provider') throw new Error('Invalid response from the OIDC provider')
} }
const callback = new URL('/admin/oidc/callback', request.url) const callback = new URL('/admin/oidc/callback', req.url)
callback.protocol = request.url.includes('localhost') ? 'http:' : 'https:' callback.protocol = req.url.includes('localhost') ? 'http:' : 'https:'
callback.hostname = request.headers.get('Host') ?? '' callback.hostname = req.headers.get('Host') ?? ''
const tokenResponse = await authorizationCodeGrantRequest(
processed,
oidcClient,
parameters,
callback.href,
verifier,
)
const tokenResponse = await authorizationCodeGrantRequest(processed, oidcClient, parameters, callback.href, verifier)
const challenges = parseWwwAuthenticateChallenges(tokenResponse) const challenges = parseWwwAuthenticateChallenges(tokenResponse)
if (challenges) { if (challenges) {
throw new Error('Recieved a challenge from the OIDC provider') throw new Error('Recieved a challenge from the OIDC provider')
} }
const result = await processAuthorizationCodeOpenIDResponse(processed, oidcClient, tokenResponse, nonce) const result = await processAuthorizationCodeOpenIDResponse(
processed,
oidcClient,
tokenResponse,
nonce,
)
if (isOAuth2Error(result)) { if (isOAuth2Error(result)) {
throw new Error('Invalid response from the OIDC provider') throw new Error('Invalid response from the OIDC provider')
} }
@ -126,21 +149,24 @@ export async function finishOidc(issuer: string, client: string, secret: string,
const claims = getValidatedIdTokenClaims(result) const claims = getValidatedIdTokenClaims(result)
const expDate = new Date(claims.exp * 1000).toISOString() const expDate = new Date(claims.exp * 1000).toISOString()
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion const keyResponse = await post<{ apiKey: string }>(
const keyResponse = await post<{ apiKey: string }>('v1/apikey', process.env.API_KEY!, { 'v1/apikey',
expiration: expDate // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
}) process.env.API_KEY!,
{
expiration: expDate,
},
)
session.set('hsApiKey', keyResponse.apiKey) session.set('hsApiKey', keyResponse.apiKey)
session.set('user', { session.set('user', {
name: claims.name ? String(claims.name) : 'Anonymous', name: claims.name ? String(claims.name) : 'Anonymous',
email: claims.email ? String(claims.email) : undefined email: claims.email ? String(claims.email) : undefined,
}) })
return redirect('/machines', { return redirect('/machines', {
headers: { headers: {
// eslint-disable-next-line @typescript-eslint/naming-convention 'Set-Cookie': await commitSession(session),
'Set-Cookie': await commitSession(session) },
}
}) })
} }