kopia lustrzana https://github.com/cloudflare/wildebeest
Merge pull request #14 from cloudflare/sven/MOW-85-access-config
Configure Access automaticallypull/18/head
commit
4480832aee
|
@ -11,6 +11,10 @@ jobs:
|
|||
- uses: actions/checkout@v2
|
||||
- uses: hashicorp/setup-terraform@v2
|
||||
|
||||
- name: Install package
|
||||
run: |
|
||||
sudo apt-get -y install jq
|
||||
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
|
@ -45,6 +49,12 @@ jobs:
|
|||
env:
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
|
||||
- name: retrieve Zero Trust organization
|
||||
run: |
|
||||
auth_domain=$(curl https://api.cloudflare.com/client/v4/accounts/${{ secrets.CF_ACCOUNT_ID }}/access/organizations \
|
||||
-H 'Authorization: Bearer ${{ secrets.CF_API_TOKEN }}' | jq -r '.result.auth_domain')
|
||||
printf "auth_domain=$auth_domain" >> $GITHUB_ENV
|
||||
|
||||
- name: Init
|
||||
run: terraform init
|
||||
working-directory: ./tf
|
||||
|
@ -59,6 +69,7 @@ jobs:
|
|||
TF_VAR_cloudflare_zone_name: ${{ secrets.CF_ZONE_NAME }}
|
||||
TF_VAR_gh_username: ${{ github.actor }}
|
||||
TF_VAR_d1_id: ${{ env.d1_id }}
|
||||
TF_VAR_access_auth_domain: ${{ env.auth_domain }}
|
||||
|
||||
- name: Publish
|
||||
uses: cloudflare/wrangler-action@2.0.0
|
||||
|
|
|
@ -20,18 +20,6 @@ export async function configure(db: D1Database, data: InstanceConfig) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function configureAccess(db: D1Database, domain: string, aud: string) {
|
||||
const sql = `
|
||||
INSERT INTO instance_config
|
||||
VALUES ('accessAud', ?), ('accessDomain', ?);
|
||||
`
|
||||
|
||||
const { success, error } = await db.prepare(sql).bind(aud, domain).run()
|
||||
if (!success) {
|
||||
throw new Error('SQL error: ' + error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateVAPIDKeys(db: D1Database) {
|
||||
const keyPair = (await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, [
|
||||
'sign',
|
||||
|
|
|
@ -7,10 +7,7 @@ import { loadLocalMastodonAccount } from 'wildebeest/backend/src/mastodon/accoun
|
|||
|
||||
async function loadContextData(db: D1Database, clientId: string, email: string, ctx: any): Promise<boolean> {
|
||||
const query = `
|
||||
SELECT
|
||||
actors.*,
|
||||
(SELECT value FROM instance_config WHERE key='accessAud') as accessAud,
|
||||
(SELECT value FROM instance_config WHERE key='accessDomain') as accessDomain
|
||||
SELECT *
|
||||
FROM actors
|
||||
WHERE email=? AND type='Person'
|
||||
`
|
||||
|
@ -30,18 +27,12 @@ async function loadContextData(db: D1Database, clientId: string, email: string,
|
|||
console.warn('person not found')
|
||||
return false
|
||||
}
|
||||
if (!row.accessDomain || !row.accessAud) {
|
||||
console.warn('access configuration not found')
|
||||
return false
|
||||
}
|
||||
|
||||
const person = actors.personFromRow(row)
|
||||
|
||||
ctx.data.connectedActor = person
|
||||
ctx.data.identity = { email }
|
||||
ctx.data.clientId = clientId
|
||||
ctx.data.accessDomain = row.accessDomain
|
||||
ctx.data.accessAud = row.accessAud
|
||||
|
||||
return true
|
||||
}
|
||||
|
@ -101,12 +92,12 @@ export async function main(context: EventContext<Env, any, any>) {
|
|||
|
||||
const validatate = access.generateValidator({
|
||||
jwt,
|
||||
domain: context.data.accessDomain,
|
||||
aud: context.data.accessAud,
|
||||
domain: context.env.ACCESS_AUTH_DOMAIN,
|
||||
aud: context.env.ACCESS_AUD,
|
||||
})
|
||||
await validatate(context.request)
|
||||
|
||||
const identity = await access.getIdentity({ jwt, domain: context.data.accessDomain })
|
||||
const identity = await access.getIdentity({ jwt, domain: context.env.ACCESS_AUTH_DOMAIN })
|
||||
if (!identity) {
|
||||
return errors.notAuthorized('failed to load identity')
|
||||
}
|
||||
|
|
|
@ -9,10 +9,6 @@ export type ContextData = {
|
|||
// ActivityPub Person object of the logged in user
|
||||
connectedActor: Person
|
||||
|
||||
// Configure for Cloudflare Access
|
||||
accessDomain: string
|
||||
accessAud: string
|
||||
|
||||
// Object returned by Cloudflare Access' provider
|
||||
identity: Identity
|
||||
|
||||
|
|
|
@ -2,6 +2,12 @@ export interface Env {
|
|||
DATABASE: D1Database
|
||||
KV_CACHE: KVNamespace
|
||||
userKEK: string
|
||||
|
||||
CF_ACCOUNT_ID: string
|
||||
CF_API_TOKEN: string
|
||||
|
||||
// Configuration for Cloudflare Access
|
||||
DOMAIN: string
|
||||
ACCESS_AUD: string
|
||||
ACCESS_AUTH_DOMAIN: string
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
} from '../utils'
|
||||
import { TEST_JWT, ACCESS_CERTS } from '../test-data'
|
||||
import { strict as assert } from 'node:assert/strict'
|
||||
import { configureAccess } from 'wildebeest/backend/src/config/index'
|
||||
|
||||
const userKEK = 'test_kek3'
|
||||
const accessDomain = 'access.com'
|
||||
|
@ -41,20 +40,18 @@ describe('Mastodon APIs', () => {
|
|||
|
||||
test('authorize missing params', async () => {
|
||||
const db = await makeDB()
|
||||
await configureAccess(db, accessDomain, accessAud)
|
||||
|
||||
let req = new Request('https://example.com/oauth/authorize')
|
||||
let res = await oauth_authorize.handleRequest(req, db, userKEK)
|
||||
let res = await oauth_authorize.handleRequest(req, db, userKEK, accessDomain, accessAud)
|
||||
assert.equal(res.status, 400)
|
||||
|
||||
req = new Request('https://example.com/oauth/authorize?scope=foobar')
|
||||
res = await oauth_authorize.handleRequest(req, db, userKEK)
|
||||
res = await oauth_authorize.handleRequest(req, db, userKEK, accessDomain, accessAud)
|
||||
assert.equal(res.status, 400)
|
||||
})
|
||||
|
||||
test('authorize unsupported response_type', async () => {
|
||||
const db = await makeDB()
|
||||
await configureAccess(db, accessDomain, accessAud)
|
||||
|
||||
const params = new URLSearchParams({
|
||||
redirect_uri: 'https://example.com',
|
||||
|
@ -63,14 +60,13 @@ describe('Mastodon APIs', () => {
|
|||
})
|
||||
|
||||
const req = new Request('https://example.com/oauth/authorize?' + params)
|
||||
const res = await oauth_authorize.handleRequest(req, db, userKEK)
|
||||
const res = await oauth_authorize.handleRequest(req, db, userKEK, accessDomain, accessAud)
|
||||
assert.equal(res.status, 400)
|
||||
})
|
||||
|
||||
test("authorize redirect_uri doesn't match client redirect_uris", async () => {
|
||||
const db = await makeDB()
|
||||
const client = await createTestClient(db, 'https://redirect.com')
|
||||
await configureAccess(db, accessDomain, accessAud)
|
||||
|
||||
const params = new URLSearchParams({
|
||||
redirect_uri: 'https://example.com/a',
|
||||
|
@ -83,14 +79,13 @@ describe('Mastodon APIs', () => {
|
|||
const req = new Request('https://example.com/oauth/authorize?' + params, {
|
||||
headers,
|
||||
})
|
||||
const res = await oauth_authorize.handleRequest(req, db, userKEK)
|
||||
const res = await oauth_authorize.handleRequest(req, db, userKEK, accessDomain, accessAud)
|
||||
assert.equal(res.status, 403)
|
||||
})
|
||||
|
||||
test('authorize redirects with code on success and show first login', async () => {
|
||||
const db = await makeDB()
|
||||
const client = await createTestClient(db)
|
||||
await configureAccess(db, accessDomain, accessAud)
|
||||
|
||||
const params = new URLSearchParams({
|
||||
redirect_uri: client.redirect_uris,
|
||||
|
@ -103,7 +98,7 @@ describe('Mastodon APIs', () => {
|
|||
const req = new Request('https://example.com/oauth/authorize?' + params, {
|
||||
headers,
|
||||
})
|
||||
const res = await oauth_authorize.handleRequest(req, db, userKEK)
|
||||
const res = await oauth_authorize.handleRequest(req, db, userKEK, accessDomain, accessAud)
|
||||
assert.equal(res.status, 302)
|
||||
|
||||
const location = new URL(res.headers.get('location') || '')
|
||||
|
@ -212,7 +207,7 @@ describe('Mastodon APIs', () => {
|
|||
const req = new Request('https://example.com/oauth/authorize', {
|
||||
method: 'OPTIONS',
|
||||
})
|
||||
const res = await oauth_authorize.handleRequest(req, db, userKEK)
|
||||
const res = await oauth_authorize.handleRequest(req, db, userKEK, accessDomain, accessAud)
|
||||
assert.equal(res.status, 200)
|
||||
assertCORS(res)
|
||||
})
|
||||
|
|
|
@ -2,7 +2,6 @@ import { isUrlValid, makeDB, assertCORS } from './utils'
|
|||
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
|
||||
import { TEST_JWT, ACCESS_CERTS } from './test-data'
|
||||
import { strict as assert } from 'node:assert/strict'
|
||||
import { configureAccess } from 'wildebeest/backend/src/config/index'
|
||||
import * as middleware_main from 'wildebeest/backend/src/middleware/main'
|
||||
|
||||
const userKEK = 'test_kek12'
|
||||
|
@ -99,14 +98,13 @@ describe('middleware', () => {
|
|||
|
||||
const db = await makeDB()
|
||||
await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
|
||||
await configureAccess(db, accessDomain, accessAud)
|
||||
|
||||
const headers = { authorization: 'Bearer APPID.' + TEST_JWT }
|
||||
const request = new Request('https://example.com', { headers })
|
||||
const ctx: any = {
|
||||
next: () => new Response(),
|
||||
data: {},
|
||||
env: { DATABASE: db },
|
||||
env: { DATABASE: db, ACCESS_AUD: accessAud, ACCESS_AUTH_DOMAIN: accessDomain },
|
||||
request,
|
||||
}
|
||||
|
||||
|
@ -114,7 +112,7 @@ describe('middleware', () => {
|
|||
assert.equal(res.status, 200)
|
||||
assert(!ctx.data.connectedUser)
|
||||
assert(isUrlValid(ctx.data.connectedActor.id))
|
||||
assert.equal(ctx.data.accessDomain, accessDomain)
|
||||
assert.equal(ctx.data.accessAud, accessAud)
|
||||
assert.equal(ctx.env.ACCESS_AUTH_DOMAIN, accessDomain)
|
||||
assert.equal(ctx.env.ACCESS_AUD, accessAud)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -37,7 +37,7 @@ describe('Wildebeest', () => {
|
|||
}
|
||||
|
||||
const req = new Request('https://example.com', { method: 'POST', body, headers })
|
||||
const res = await startInstance.handlePostRequest(req, db)
|
||||
const res = await startInstance.handlePostRequest(req, db, accessDomain, accessAud)
|
||||
assert.equal(res.status, 201)
|
||||
|
||||
const { value } = await db.prepare("SELECT value FROM instance_config WHERE key = 'vapid_jwk'").first()
|
||||
|
|
|
@ -2,14 +2,12 @@ import { $, component$, useStore, useClientEffect$, useSignal } from '@builder.i
|
|||
import { MastodonLogo } from '~/components/MastodonLogo'
|
||||
import { useDomain } from '~/utils/useDomain'
|
||||
import Step1 from './step-1'
|
||||
import Step2 from './step-2'
|
||||
import { type InstanceConfig, testAccess, testInstance } from './utils'
|
||||
|
||||
export default component$(() => {
|
||||
const domain = useDomain()
|
||||
|
||||
const loading = useSignal(true)
|
||||
const accessConfigured = useSignal(false)
|
||||
const instanceConfigured = useSignal(false)
|
||||
|
||||
const instanceConfig = useStore<InstanceConfig>({
|
||||
|
@ -21,20 +19,15 @@ export default component$(() => {
|
|||
})
|
||||
|
||||
useClientEffect$(async () => {
|
||||
if (await testAccess()) {
|
||||
accessConfigured.value = true
|
||||
|
||||
if (await testInstance()) {
|
||||
instanceConfigured.value = true
|
||||
}
|
||||
}
|
||||
if (await testInstance()) {
|
||||
instanceConfigured.value = true
|
||||
}
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
const getStepToShow = () => {
|
||||
if (loading.value) return 'loading'
|
||||
if (!accessConfigured.value) return 'step-1'
|
||||
if (!instanceConfigured.value) return 'step-2'
|
||||
if (!instanceConfigured.value) return 'step-1'
|
||||
return 'all-good'
|
||||
}
|
||||
|
||||
|
@ -55,9 +48,8 @@ export default component$(() => {
|
|||
</h1>
|
||||
{stepToShow.startsWith('step-') && <p>Welcome to Wildebeest... Your instance hasn't been configured yet.</p>}
|
||||
{stepToShow === 'loading' && <p>Loading...</p>}
|
||||
{stepToShow === 'step-1' && <Step1 instanceConfig={instanceConfig} setLoading={setLoading} />}
|
||||
{stepToShow === 'step-2' && (
|
||||
<Step2 instanceConfig={instanceConfig} setLoading={setLoading} setInstanceConfigured={setInstanceConfigured} />
|
||||
{stepToShow === 'step-1' && (
|
||||
<Step1 instanceConfig={instanceConfig} setLoading={setLoading} setInstanceConfigured={setInstanceConfigured} />
|
||||
)}
|
||||
{stepToShow === 'all-good' && <p>All good, your instance is ready.</p>}
|
||||
</div>
|
||||
|
|
|
@ -1,125 +1,80 @@
|
|||
import { component$, QRL } from '@builder.io/qwik'
|
||||
import { generateLoginURL } from 'wildebeest/backend/src/access'
|
||||
import { configure, type InstanceConfig } from './utils'
|
||||
import { configure, type InstanceConfig, testInstance } from './utils'
|
||||
|
||||
interface Props {
|
||||
instanceConfig: InstanceConfig
|
||||
setLoading: QRL<(loading: boolean) => void>
|
||||
setInstanceConfigured: QRL<(configured: boolean) => void>
|
||||
}
|
||||
|
||||
export default component$<Props>(({ instanceConfig, setLoading }) => {
|
||||
export default component$<Props>(({ instanceConfig, setLoading, setInstanceConfigured }) => {
|
||||
return (
|
||||
<>
|
||||
<h2>Step 1. Configure Cloudflare Pages for user management</h2>
|
||||
|
||||
<p>
|
||||
Wildebeest uses{' '}
|
||||
<a href="https://www.cloudflare.com/products/zero-trust/access/" target="_new">
|
||||
{' '}
|
||||
Cloudflare Access
|
||||
</a>{' '}
|
||||
for user management. You can configure Cloudflare Access to allow users to access Wildebeest.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Go to{' '}
|
||||
<a href="https://one.dash.cloudflare.com/" target="_new">
|
||||
Cloudflare Zero Trust dashboard
|
||||
</a>
|
||||
, select the account and go in Access {'>'} Applications.
|
||||
</p>
|
||||
|
||||
<ViewableImage
|
||||
src="https://imagedelivery.net/NkfPDviynOyTAOI79ar_GQ/f8ee9ab3-31d5-4204-94bd-25d00a971600/public"
|
||||
ariaLabel="Cloudflare Zero Trust Dashboard Applications Screenshot"
|
||||
/>
|
||||
|
||||
<p>
|
||||
An application called <code>wildebeest-username</code> should already be present.
|
||||
</p>
|
||||
|
||||
<p>Click on edit and in Overview copy the field called Application Audience (AUD) Tag.</p>
|
||||
|
||||
<ViewableImage
|
||||
src="https://imagedelivery.net/NkfPDviynOyTAOI79ar_GQ/54b11336-9c64-419b-5a5f-e80d5b833700/public"
|
||||
ariaLabel="Cloudflare Zero Trust Dashboard Application Overview Screenshot"
|
||||
/>
|
||||
|
||||
<p>Paste it bellow.</p>
|
||||
<h2>Configure your instance</h2>
|
||||
|
||||
<div class="flex flex-column mb-6 w-full max-w-md">
|
||||
<label class="mb-2 text-semi text-sm" for="start-instance-access-aud">
|
||||
Access AUD
|
||||
<label class="mb-2 max-w-max text-semi text-sm" for="start-instance-title">
|
||||
Title
|
||||
</label>
|
||||
<div class="flex justify-center items-center flex-wrap gap-1">
|
||||
<input
|
||||
id="start-instance-access-aud"
|
||||
name="access-aud"
|
||||
type="access-aud"
|
||||
id="start-instance-title"
|
||||
name="title"
|
||||
class="bg-black text-white p-3 rounded outline-none border border-black hover:border-indigo-400 focus:border-indigo-400 invalid:border-red-400 flex-1 w-full"
|
||||
value={instanceConfig.accessAud}
|
||||
onInput$={(ev) => (instanceConfig.accessAud = (ev.target as HTMLInputElement).value)}
|
||||
value={instanceConfig.title}
|
||||
onInput$={(ev) => (instanceConfig.title = (ev.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Then go to Settings {'>'} General.</p>
|
||||
|
||||
<ViewableImage
|
||||
src="https://imagedelivery.net/NkfPDviynOyTAOI79ar_GQ/51988fa3-44cc-4ec2-fb9a-2096d2f1c700/public"
|
||||
ariaLabel="Cloudflare Zero Trust Dashboard General Settings Screenshot"
|
||||
/>
|
||||
|
||||
<p>Copy the Team domain and paste it bellow.</p>
|
||||
|
||||
<div class="flex flex-column w-full max-w-md">
|
||||
<div class="flex flex-column mb-6">
|
||||
<label class="mb-2 text-semi text-sm" for="start-instance-access-domain">
|
||||
Access domain
|
||||
</label>
|
||||
<div class="flex justify-center items-center flex-wrap gap-1">
|
||||
<input
|
||||
id="start-instance-access-domain"
|
||||
name="access-domain"
|
||||
type="access-domain"
|
||||
class="bg-black text-white p-3 rounded outline-none border border-black hover:border-indigo-400 focus:border-indigo-400 invalid:border-red-400 flex-1 w-full"
|
||||
value={instanceConfig.accessDomain}
|
||||
onInput$={(ev) => (instanceConfig.accessDomain = (ev.target as HTMLInputElement).value)}
|
||||
/>
|
||||
<span>.cloudflareaccess.com</span>
|
||||
</div>
|
||||
<div class="flex flex-column mb-6 w-full max-w-md">
|
||||
<label class="mb-2 max-w-max text-semi text-sm" for="start-instance-email">
|
||||
Administrator email
|
||||
</label>
|
||||
<div class="flex justify-center items-center flex-wrap gap-1">
|
||||
<input
|
||||
id="start-instance-email"
|
||||
name="email"
|
||||
type="email"
|
||||
class="bg-black text-white p-3 rounded outline-none border border-black hover:border-indigo-400 focus:border-indigo-400 invalid:border-red-400 flex-1 w-full"
|
||||
value={instanceConfig.email}
|
||||
onInput$={(ev) => (instanceConfig.email = (ev.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="mb-9 bg-indigo-600 hover:bg-indigo-500 p-3 text-white text-uppercase border-indigo-600 text-lg text-semi outline-none border rounded hover:border-indigo-500 focus:border-indigo-500 max-w-md"
|
||||
preventdefault:click
|
||||
onClick$={async () => {
|
||||
setLoading(true)
|
||||
await configure({
|
||||
accessDomain: instanceConfig.accessDomain,
|
||||
accessAud: instanceConfig.accessAud,
|
||||
})
|
||||
setLoading(false)
|
||||
|
||||
const url = generateLoginURL({
|
||||
redirectURL: location.href,
|
||||
domain: instanceConfig.accessDomain + '.cloudflareaccess.com',
|
||||
aud: instanceConfig.accessAud,
|
||||
})
|
||||
window.location.href = url
|
||||
}}
|
||||
>
|
||||
Configure and test
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column mb-6 w-full max-w-md">
|
||||
<label class="mb-2 max-w-max text-semi text-sm" for="start-instance-description">
|
||||
Description
|
||||
</label>
|
||||
<div class="flex justify-center items-center flex-wrap gap-1">
|
||||
<input
|
||||
id="start-instance-description"
|
||||
name="description"
|
||||
class="bg-black text-white p-3 rounded outline-none border border-black hover:border-indigo-400 focus:border-indigo-400 invalid:border-red-400 flex-1 w-full"
|
||||
value={instanceConfig.description}
|
||||
onInput$={(ev) => (instanceConfig.description = (ev.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="mb-9 bg-indigo-600 hover:bg-indigo-500 p-3 text-white text-uppercase border-indigo-600 text-lg text-semi outline-none border rounded hover:border-indigo-500 focus:border-indigo-500"
|
||||
preventdefault:click
|
||||
onClick$={async () => {
|
||||
setLoading(true)
|
||||
await configure(instanceConfig)
|
||||
|
||||
if (await testInstance()) {
|
||||
setInstanceConfigured(true)
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}}
|
||||
>
|
||||
Configure and start your instance
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export const ViewableImage = component$(({ src, ariaLabel }: { src: string; ariaLabel: string }) => {
|
||||
return (
|
||||
<a href={src} target="_blank">
|
||||
<img src={src} class="w-full" aria-label={ariaLabel} />
|
||||
</a>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
import { component$, QRL } from '@builder.io/qwik'
|
||||
import { configure, type InstanceConfig, testInstance } from './utils'
|
||||
|
||||
interface Props {
|
||||
instanceConfig: InstanceConfig
|
||||
setLoading: QRL<(loading: boolean) => void>
|
||||
setInstanceConfigured: QRL<(configured: boolean) => void>
|
||||
}
|
||||
|
||||
export default component$<Props>(({ instanceConfig, setLoading, setInstanceConfigured }) => {
|
||||
return (
|
||||
<>
|
||||
<h2>Step 2. Configure instance</h2>
|
||||
|
||||
<div class="flex flex-column mb-6 w-full max-w-md">
|
||||
<label class="mb-2 max-w-max text-semi text-sm" for="start-instance-title">
|
||||
Title
|
||||
</label>
|
||||
<div class="flex justify-center items-center flex-wrap gap-1">
|
||||
<input
|
||||
id="start-instance-title"
|
||||
name="title"
|
||||
class="bg-black text-white p-3 rounded outline-none border border-black hover:border-indigo-400 focus:border-indigo-400 invalid:border-red-400 flex-1 w-full"
|
||||
value={instanceConfig.title}
|
||||
onInput$={(ev) => (instanceConfig.title = (ev.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column mb-6 w-full max-w-md">
|
||||
<label class="mb-2 max-w-max text-semi text-sm" for="start-instance-email">
|
||||
Administrator email
|
||||
</label>
|
||||
<div class="flex justify-center items-center flex-wrap gap-1">
|
||||
<input
|
||||
id="start-instance-email"
|
||||
name="email"
|
||||
type="email"
|
||||
class="bg-black text-white p-3 rounded outline-none border border-black hover:border-indigo-400 focus:border-indigo-400 invalid:border-red-400 flex-1 w-full"
|
||||
value={instanceConfig.email}
|
||||
onInput$={(ev) => (instanceConfig.email = (ev.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column mb-6 w-full max-w-md">
|
||||
<label class="mb-2 max-w-max text-semi text-sm" for="start-instance-description">
|
||||
Description
|
||||
</label>
|
||||
<div class="flex justify-center items-center flex-wrap gap-1">
|
||||
<input
|
||||
id="start-instance-description"
|
||||
name="description"
|
||||
class="bg-black text-white p-3 rounded outline-none border border-black hover:border-indigo-400 focus:border-indigo-400 invalid:border-red-400 flex-1 w-full"
|
||||
value={instanceConfig.description}
|
||||
onInput$={(ev) => (instanceConfig.description = (ev.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="mb-9 bg-indigo-600 hover:bg-indigo-500 p-3 text-white text-uppercase border-indigo-600 text-lg text-semi outline-none border rounded hover:border-indigo-500 focus:border-indigo-500"
|
||||
preventdefault:click
|
||||
onClick$={async () => {
|
||||
setLoading(true)
|
||||
await configure(instanceConfig)
|
||||
|
||||
if (await testInstance()) {
|
||||
setInstanceConfigured(true)
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}}
|
||||
>
|
||||
Configure and start your instance
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
})
|
|
@ -11,10 +11,16 @@ import { getPersonByEmail } from 'wildebeest/backend/src/activitypub/actors'
|
|||
const extractJWTFromRequest = (request: Request) => request.headers.get('Cf-Access-Jwt-Assertion') || ''
|
||||
|
||||
export const onRequest: PagesFunction<Env, any, ContextData> = async ({ data, request, env }) => {
|
||||
return handleRequest(request, env.DATABASE, env.userKEK)
|
||||
return handleRequest(request, env.DATABASE, env.userKEK, env.ACCESS_AUTH_DOMAIN, env.ACCESS_AUD)
|
||||
}
|
||||
|
||||
export async function handleRequest(request: Request, db: D1Database, userKEK: string): Promise<Response> {
|
||||
export async function handleRequest(
|
||||
request: Request,
|
||||
db: D1Database,
|
||||
userKEK: string,
|
||||
accessDomain: string,
|
||||
accessAud: string
|
||||
): Promise<Response> {
|
||||
if (request.method === 'OPTIONS') {
|
||||
const headers = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
|
@ -54,18 +60,11 @@ export async function handleRequest(request: Request, db: D1Database, userKEK: s
|
|||
|
||||
const scope = url.searchParams.get('scope') || ''
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
(SELECT value FROM instance_config WHERE key='accessAud') as accessAud,
|
||||
(SELECT value FROM instance_config WHERE key='accessDomain') as accessDomain
|
||||
`
|
||||
const config: any = await db.prepare(query).first()
|
||||
|
||||
const jwt = extractJWTFromRequest(request)
|
||||
const validate = access.generateValidator({ jwt, domain: config.accessDomain, aud: config.accessAud })
|
||||
const validate = access.generateValidator({ jwt, domain: accessDomain, aud: accessAud })
|
||||
await validate(request)
|
||||
|
||||
const identity = await access.getIdentity({ jwt, domain: config.accessDomain })
|
||||
const identity = await access.getIdentity({ jwt, domain: accessDomain })
|
||||
if (!identity) {
|
||||
return new Response('', { status: 401 })
|
||||
}
|
||||
|
|
|
@ -34,9 +34,9 @@ export async function handleGetRequest(db: D1Database, request: Request): Promis
|
|||
return errors.notAuthorized('missing authorization')
|
||||
}
|
||||
|
||||
const domain = data.accessDomain
|
||||
const domain = env.ACCESS_AUTH_DOMAIN
|
||||
|
||||
const validator = access.generateValidator({ jwt, domain, aud: data.accessAud })
|
||||
const validator = access.generateValidator({ jwt, domain, aud: env.ACCESS_AUD })
|
||||
const { payload } = await validator(request)
|
||||
|
||||
const identity = await access.getIdentity({ jwt, domain })
|
||||
|
|
|
@ -7,28 +7,44 @@ import type { ContextData } from 'wildebeest/backend/src/types/context'
|
|||
import type { InstanceConfig } from 'wildebeest/backend/src/config'
|
||||
import * as config from 'wildebeest/backend/src/config'
|
||||
|
||||
export const onRequestPost: PagesFunction<Env, any, ContextData> = async ({ request, env, data }) => {
|
||||
return handlePostRequest(request, env.DATABASE)
|
||||
export const onRequestPost: PagesFunction<Env, any> = async ({ request, env }) => {
|
||||
return handlePostRequest(request, env.DATABASE, env.ACCESS_AUTH_DOMAIN, env.ACCESS_AUD)
|
||||
}
|
||||
|
||||
export async function handlePostRequest(request: Request, db: D1Database): Promise<Response> {
|
||||
const data = await request.json<InstanceConfig>()
|
||||
if (!data.accessAud || !data.accessDomain) {
|
||||
return new Response('', { status: 400 })
|
||||
export const onRequestGet: PagesFunction<Env, any> = async ({ request, env, next }) => {
|
||||
const cookie = parse(request.headers.get('Cookie') || '')
|
||||
const jwt = cookie['CF_Authorization']
|
||||
if (!jwt) {
|
||||
const { hostname } = new URL(request.url)
|
||||
const url = access.generateLoginURL({
|
||||
redirectURL: new URL('/start-instance', 'https://' + env.DOMAIN),
|
||||
domain: env.ACCESS_AUTH_DOMAIN,
|
||||
aud: env.ACCESS_AUD,
|
||||
})
|
||||
return Response.redirect(url)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
export async function handlePostRequest(
|
||||
request: Request,
|
||||
db: D1Database,
|
||||
accessDomain: string,
|
||||
accessAud: string
|
||||
): Promise<Response> {
|
||||
const data = await request.json<InstanceConfig>()
|
||||
|
||||
const cookie = parse(request.headers.get('Cookie') || '')
|
||||
const jwt = cookie['CF_Authorization']
|
||||
if (!jwt) {
|
||||
// Allow to configure Access without any authentification
|
||||
await config.configureAccess(db, data.accessDomain + '.cloudflareaccess.com', data.accessAud)
|
||||
return new Response()
|
||||
return new Response('', { status: 401 })
|
||||
}
|
||||
|
||||
const validator = access.generateValidator({ jwt, domain: data.accessDomain, aud: data.accessAud })
|
||||
const validator = access.generateValidator({ jwt, domain: accessDomain, aud: accessAud })
|
||||
const { payload } = await validator(request)
|
||||
|
||||
const identity = await access.getIdentity({ jwt, domain: data.accessDomain })
|
||||
const identity = await access.getIdentity({ jwt, domain: accessDomain })
|
||||
if (!identity) {
|
||||
return errors.notAuthorized('failed to load identity')
|
||||
}
|
||||
|
|
12
tf/main.tf
12
tf/main.tf
|
@ -18,6 +18,10 @@ variable "d1_id" {
|
|||
type = string
|
||||
}
|
||||
|
||||
variable "access_auth_domain" {
|
||||
type = string
|
||||
}
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
|
@ -65,6 +69,10 @@ resource "cloudflare_pages_project" "wildebeest_pages_project" {
|
|||
CF_API_TOKEN = ""
|
||||
|
||||
USER_KEY = random_password.user_key.result
|
||||
|
||||
DOMAIN = var.cloudflare_zone_name
|
||||
ACCESS_AUD = cloudflare_access_application.wildebeest_access.aud
|
||||
ACCESS_AUTH_DOMAIN = var.access_auth_domain
|
||||
}
|
||||
kv_namespaces = {
|
||||
KV_CACHE = cloudflare_workers_kv_namespace.wildebeest_cache.id
|
||||
|
@ -111,7 +119,3 @@ resource "cloudflare_access_policy" "policy" {
|
|||
email = ["test@example.com"]
|
||||
}
|
||||
}
|
||||
|
||||
/* output "access_aud" { */
|
||||
/* value = cloudflare_access_application.wildebeest_access.aud */
|
||||
/* } */
|
||||
|
|
Ładowanie…
Reference in New Issue