From 854b6084c2501f78c7e15df13a76f7c4abcf3845 Mon Sep 17 00:00:00 2001 From: JoblersTune Date: Fri, 1 Mar 2024 12:53:36 +0200 Subject: [PATCH] TMP: Kratos configured and basic registration and verifications flows are set --- .../frontend/app/lib/kratos_checks.server.ts | 32 +++++ packages/frontend/app/root.tsx | 6 + .../app/routes/{auth.tsx => auth._index.tsx} | 46 +++++-- .../app/routes/{login.tsx => auth.login.tsx} | 2 +- .../frontend/app/routes/auth.registration.tsx | 102 +++++++++++++++ .../frontend/app/routes/auth.verification.tsx | 121 ++++++++++++++++++ packages/frontend/app/routes/callback.tsx | 8 +- packages/frontend/app/routes/consent.tsx | 10 +- packages/frontend/app/routes/registration.tsx | 101 --------------- packages/frontend/hydra/docker-compose.yml | 6 +- packages/frontend/kratos/Dockerfile | 2 +- packages/frontend/kratos/config/kratos.yml | 31 ++--- packages/frontend/kratos/docker-compose.yml | 10 +- 13 files changed, 334 insertions(+), 143 deletions(-) create mode 100644 packages/frontend/app/lib/kratos_checks.server.ts rename packages/frontend/app/routes/{auth.tsx => auth._index.tsx} (57%) rename packages/frontend/app/routes/{login.tsx => auth.login.tsx} (98%) create mode 100644 packages/frontend/app/routes/auth.registration.tsx create mode 100644 packages/frontend/app/routes/auth.verification.tsx delete mode 100644 packages/frontend/app/routes/registration.tsx diff --git a/packages/frontend/app/lib/kratos_checks.server.ts b/packages/frontend/app/lib/kratos_checks.server.ts new file mode 100644 index 0000000000..f37dfc6e2f --- /dev/null +++ b/packages/frontend/app/lib/kratos_checks.server.ts @@ -0,0 +1,32 @@ +// todo remove session id logic since its actually a token and being handled differently +import { redirect } from '@remix-run/node' +import axios from 'axios' + +export async function requireSession(cookieHeader?: string | null) { + console.log('COOKIES: ', cookieHeader) + + try { + const session = await axios.get(`http://kratos:4433/sessions/whoami`, { + headers: { + cookie: cookieHeader + }, + withCredentials: true + }) + + console.log('SESSION DATA: ', session.data) + console.log( + 'VERIFIABLE ADDRESSES: ', + session.data.identity.verifiable_addresses + ) + + if (session.status !== 200 || !session.data?.active) { + // does active here mean it is a legit logged in session? + // Redirect to auth if there's no valid session + throw redirect('/auth') + } + + return session + } catch { + throw redirect('/auth') + } +} diff --git a/packages/frontend/app/root.tsx b/packages/frontend/app/root.tsx index 865c037b9b..62b8453cfb 100644 --- a/packages/frontend/app/root.tsx +++ b/packages/frontend/app/root.tsx @@ -22,6 +22,7 @@ import { messageStorage, type Message } from './lib/message.server' import tailwind from './styles/tailwind.css' import { getOpenPaymentsUrl } from './shared/utils' import { PublicEnv, type PublicEnvironment } from './PublicEnv' +import { requireSession } from './lib/kratos_checks.server' export const meta: MetaFunction = () => [ { title: 'Rafiki Admin' }, @@ -30,6 +31,11 @@ export const meta: MetaFunction = () => [ ] export const loader = async ({ request }: LoaderFunctionArgs) => { + console.log('URL: ', request.url) + const url = new URL(request.url) + if (!url.pathname.startsWith('/auth')) { + await requireSession(request.headers.get('cookie')) + } const session = await messageStorage.getSession(request.headers.get('cookie')) const message = session.get('message') as Message diff --git a/packages/frontend/app/routes/auth.tsx b/packages/frontend/app/routes/auth._index.tsx similarity index 57% rename from packages/frontend/app/routes/auth.tsx rename to packages/frontend/app/routes/auth._index.tsx index c5b6391f57..01c3833a73 100644 --- a/packages/frontend/app/routes/auth.tsx +++ b/packages/frontend/app/routes/auth._index.tsx @@ -1,6 +1,7 @@ import { version } from '../../../../package.json' import { Form } from '@remix-run/react' import { redirect, type ActionFunctionArgs } from '@remix-run/node' +import { Button } from '../components/ui' export default function Auth() { return ( @@ -19,10 +20,31 @@ export default function Auth() { In this web application, you'll be able to manage peering relationships, assets, and wallet addresses, among other settings.

+

+ + https://rafiki.dev + +

-
- - + + +
@@ -36,15 +58,17 @@ export async function action({ request }: ActionFunctionArgs) { const action = formData.get('action') if (action === 'login') { - // TODO: Make an API call to Ory Kratos for login - return + return redirect('http://127.0.0.1:4433/self-service/login/browser', { + headers: { + Accept: 'text/html' + } + }) } else if (action === 'register') { - return redirect('http://localhost:4433/self-service/registration/browser', - { - headers: { - 'Accept': 'text/html' - } - }) + return redirect('http://127.0.0.1:4433/self-service/registration/browser', { + headers: { + Accept: 'text/html' + } + }) } throw new Error('Invalid auth action') } diff --git a/packages/frontend/app/routes/login.tsx b/packages/frontend/app/routes/auth.login.tsx similarity index 98% rename from packages/frontend/app/routes/login.tsx rename to packages/frontend/app/routes/auth.login.tsx index 1ff786234b..f35c794b5f 100644 --- a/packages/frontend/app/routes/login.tsx +++ b/packages/frontend/app/routes/auth.login.tsx @@ -20,7 +20,7 @@ export default function Login() {

Login to Rafiki Admin

-
+ { + const url = new URL(request.url) + const flowId = url.searchParams.get('flow') + const cookies = request.headers.get('cookie') + + if (!flowId) { + throw redirect('http://127.0.0.1:4433/self-service/registration/browser') + } else { + const response = await fetch( + `http://kratos:4433/self-service/registration/flows?id=${flowId}`, + { + headers: { + Cookie: cookies || '' + }, + credentials: 'include' + } + ) + + const responseData = await response.json() + const formFields: Field[] = responseData.ui.nodes // returns an array of form fields -> there's also a oauth2_login_challenge here you should investigate + formFields.push({ + type: 'input', + group: 'default', + attributes: { + name: 'method', + type: 'hidden', + disabled: false, + node_type: 'input', + value: 'password', + required: true + }, + messages: [], + meta: {} + }) + return { formFields, flowId } + } +} + +export default function Registration() { + const { formFields } = useLoaderData() + const { flowId } = useLoaderData() + const actionUrl = `http://127.0.0.1:4433/self-service/registration?flow=${flowId}` + return ( +
+
+
+

Register for Rafiki Admin

+
+ +
+ {formFields.map((field, index) => { + const { attributes, type } = field + if (type === 'input' && attributes.type !== 'submit') { + return ( + + ) + } + return null + })} + +
+ +
+
+
+
+ ) +} diff --git a/packages/frontend/app/routes/auth.verification.tsx b/packages/frontend/app/routes/auth.verification.tsx new file mode 100644 index 0000000000..164812fa83 --- /dev/null +++ b/packages/frontend/app/routes/auth.verification.tsx @@ -0,0 +1,121 @@ +import { Button } from '../components/ui' +import { useLoaderData, Form } from '@remix-run/react' +import { + redirect, + type LoaderFunctionArgs, + type ActionFunctionArgs +} from '@remix-run/node' + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const url = new URL(request.url) + const flowId = url.searchParams.get('flow') + const cookies = request.headers.get('cookie') + + if (!flowId) { + throw new Error('No verification flow ID found') + } else { + const response = await fetch( + `http://kratos:4433/self-service/verification/flows?id=${flowId}`, + { + headers: { + Cookie: cookies || '' + }, + credentials: 'include' + } + ) + + const flowData = await response.json() + + return { flowData } + } +} + +export default function Verification() { + const { flowData } = useLoaderData() + const actionUrl = flowData.ui.action + + if (flowData.state === 'passed_challenge') { + return ( +
+
+
+

Success

+
+ {flowData.ui.messages.map((message) => { + return

{message.text}

+ })} +
+
+
+ +
+
+
+
+
+ ) + } else { + return ( +
+
+
+

Verification in progress

+
+ {flowData.ui.messages.map((message) => { + return

{message.text}

+ })} +
+
+
+
+ {flowData.ui.nodes.map((field, index) => { + const { attributes, type } = field + if (type === 'input' && attributes.type !== 'submit') { + return ( + + ) + } + return null + })} + +
+
+
+
+
+
+ ) + } +} + +export async function action({ request }: ActionFunctionArgs) { + const formData = await request.formData() + const action = formData.get('action') + + if (action === 'login') { + return redirect('http://127.0.0.1:4433/self-service/login/browser', { + headers: { + Accept: 'text/html' + } + }) + } + throw new Error('Invalid auth action') +} diff --git a/packages/frontend/app/routes/callback.tsx b/packages/frontend/app/routes/callback.tsx index 9ec762ceae..caff2fcc3f 100644 --- a/packages/frontend/app/routes/callback.tsx +++ b/packages/frontend/app/routes/callback.tsx @@ -1,8 +1,8 @@ import { redirect, json, - type LoaderArgs, - type ActionArgs + type LoaderFunctionArgs, + type ActionFunctionArgs } from '@remix-run/node' import { useLoaderData } from '@remix-run/react' import { useEffect, useRef } from 'react' @@ -39,7 +39,7 @@ export default function Callback() { ) } -export const loader = async ({ request }: LoaderArgs) => { +export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url) const authorizationCode = url.searchParams.get('code') if (!authorizationCode) { @@ -48,7 +48,7 @@ export const loader = async ({ request }: LoaderArgs) => { return json({ authorizationCode }) } -export const action = async ({ request }: ActionArgs) => { +export const action = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData() const authorizationCode = formData.get('code') diff --git a/packages/frontend/app/routes/consent.tsx b/packages/frontend/app/routes/consent.tsx index 0e960b1a88..114b2a52c4 100644 --- a/packages/frontend/app/routes/consent.tsx +++ b/packages/frontend/app/routes/consent.tsx @@ -1,4 +1,8 @@ -import { redirect, type LoaderArgs, type ActionArgs } from '@remix-run/node' +import { + redirect, + type LoaderFunctionArgs, + type ActionFunctionArgs +} from '@remix-run/node' import { getConsentChallenge, setChallengeAndRedirect, @@ -25,7 +29,7 @@ export default function Consent() { ) } -export const loader = async ({ request }: LoaderArgs) => { +export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url) const urlConsentChallenge = url.searchParams.get('consent_challenge') // TODO: Add safe parse @@ -66,7 +70,7 @@ export const loader = async ({ request }: LoaderArgs) => { } // TODO: Submit accept/reject response to consent screen -export const action = async ({ request }: ActionArgs) => { +export const action = async ({ request }: ActionFunctionArgs) => { const session = await authStorage.getSession(request.headers.get('cookie')) const sessionConsentChallenge = getConsentChallenge(session) diff --git a/packages/frontend/app/routes/registration.tsx b/packages/frontend/app/routes/registration.tsx deleted file mode 100644 index 7c6256f9cd..0000000000 --- a/packages/frontend/app/routes/registration.tsx +++ /dev/null @@ -1,101 +0,0 @@ -// This is a dummy page -// TODO: Integrate with Ory Kratos -import { - redirect, - type LoaderFunctionArgs, - type ActionFunctionArgs -} from '@remix-run/node' -// import { z } from 'zod' -import axios from 'axios' -import { - getLoginChallenge, - authStorage -} from '../lib/auth.server' - -export default function Registration() { - return ( -
-
-
-

Register for Rafiki Admin

-
-
- - - -
-
-
-
-
- ) -} - -export const loader = async ({ request }: LoaderFunctionArgs) => { - const url = new URL(request.url) - const flowId = url.searchParams.get('flow') - let csrfToken = '' - - if (!flowId) { - console.log('no flow id') - const response = await fetch('http://kratos:4433/self-service/registration/browser', - { - headers: { - 'Accept': 'text/html' - } - }) - const setCookieHeader = response.headers.get('set-cookie') - if (setCookieHeader) { - const matches = setCookieHeader.match(/csrf_token=([^;]+);/) - if (matches) { - csrfToken = matches[1]; - } - } - - console.log('CSRF TOKEN: ', csrfToken) - return {} - } else { - console.log('flow id found', flowId) - // const response = await fetch(`http://kratos:4433/self-service/registration/flows?id=${flowId}`, { - // headers: { - // 'X-CSRF-Token': csrfToken - // } - // }) - const response = await fetch(`http://kratos:4433/self-service/registration/flows?id=${flowId}`) - const responseData = await response.json() - console.log('RESPONSE DATA: ', responseData) - return responseData - } -} - -export async function action({ request }: ActionFunctionArgs) { - const formData = await request.formData() - const username = formData.get('username') - const session = await authStorage.getSession(request.headers.get('cookie')) - - const loginChallenge = getLoginChallenge(session) - - if (!loginChallenge) { - throw new Error('Login challenge empty') - } - - const response = await axios.put( - `http://hydra:4445/admin/oauth2/auth/requests/login/accept?login_challenge=${loginChallenge}`, - { - subject: username - // other data Hydra needs - } - ) - - return redirect(response.data.redirect_to) -} diff --git a/packages/frontend/hydra/docker-compose.yml b/packages/frontend/hydra/docker-compose.yml index 72b88cb982..e906c1c302 100644 --- a/packages/frontend/hydra/docker-compose.yml +++ b/packages/frontend/hydra/docker-compose.yml @@ -21,9 +21,9 @@ services: - shared-database environment: - DSN=postgres://hydra:hydra_password@shared-database:5432/hydra?sslmode=disable - - URLS_SELF_ISSUER=http://localhost:4444/ - - URLS_CONSENT=http://localhost:3010/consent - - URLS_LOGIN=http://localhost:3010/login + - URLS_SELF_ISSUER=http://127.0.0.1:4444/ + - URLS_CONSENT=http://127.0.0.1:3010/consent + - URLS_LOGIN=http://127.0.0.1:3010/login - SECRETS_SYSTEM=some-random-secret - OAUTH2_EXPOSE_INTERNAL_ERRORS=true - LOG_LEVEL=debug diff --git a/packages/frontend/kratos/Dockerfile b/packages/frontend/kratos/Dockerfile index c51585bf66..d5736fa83b 100644 --- a/packages/frontend/kratos/Dockerfile +++ b/packages/frontend/kratos/Dockerfile @@ -2,6 +2,6 @@ FROM oryd/kratos:v0.13.0 COPY scripts/entrypoint.sh /entrypoint.sh COPY config/kratos.yml /etc/config/kratos/kratos.yml -COPY config//identity.schema.json /etc/config/kratos/identity.schema.json +COPY config/identity.schema.json /etc/config/kratos/identity.schema.json ENTRYPOINT ["/entrypoint.sh"] diff --git a/packages/frontend/kratos/config/kratos.yml b/packages/frontend/kratos/config/kratos.yml index 441f989574..d8890050e6 100644 --- a/packages/frontend/kratos/config/kratos.yml +++ b/packages/frontend/kratos/config/kratos.yml @@ -11,13 +11,9 @@ serve: base_url: http://kratos:4434/ selfservice: - default_browser_return_url: http://localhost:3010/ + default_browser_return_url: http://127.0.0.1:3010/ allowed_return_urls: - - http://localhost:3010 - - http://localhost:3010/login - - http://localhost:3010/registration - - http://localhost:3010/consent - - http://localhost:3010/callback + - http://127.0.0.1:3010 methods: password: @@ -39,22 +35,27 @@ selfservice: verification: enabled: true - ui_url: http://127.0.0.1:3010/verification + ui_url: http://127.0.0.1:3010/auth/verification use: code - after: - default_browser_return_url: http://127.0.0.1:3010/ logout: after: - default_browser_return_url: http://127.0.0.1:3010/login + default_browser_return_url: http://127.0.0.1:3010/auth/login login: - ui_url: http://127.0.0.1:3010/login + ui_url: http://127.0.0.1:3010/auth/login lifespan: 10m + after: + hooks: + - hook: require_verified_address registration: lifespan: 10m - ui_url: http://127.0.0.1:3010/registration + ui_url: http://127.0.0.1:3010/auth/registration + after: + password: + hooks: + - hook: show_verification_ui log: level: debug @@ -77,12 +78,12 @@ hashers: identity: schemas: - - id: default - url: file:///etc/config/kratos/identity.schema.json + - id: default + url: file:///etc/config/kratos/identity.schema.json courier: smtp: - connection_uri: smtp://mailhog:1025/?skip_ssl_verify=true + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true session: lifespan: 1h diff --git a/packages/frontend/kratos/docker-compose.yml b/packages/frontend/kratos/docker-compose.yml index d532601ef5..b155a50afd 100644 --- a/packages/frontend/kratos/docker-compose.yml +++ b/packages/frontend/kratos/docker-compose.yml @@ -26,11 +26,13 @@ services: - "4434:4434" # Admin port networks: - rafiki - mailhog: - image: mailhog/mailhog + mailslurper: + image: oryd/mailslurper:latest-smtps ports: - - "1025:1025" # SMTP - - "8025:8025" # Web interface + - "4436:4436" # Public port web interface + - "4437:4437" + networks: + - rafiki volumes: database-data: # named volumes can be managed easier using docker-compose