diff --git a/README.md b/README.md index 3eeceaf..cdfd27b 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,14 @@ # Next-Ory -Get started with ORY authentication quickly and easily. +Get started with the Ory stack quickly and easily. > [!Warning] > This project is work in progress. There is no guarantee that everything will work as it should and breaking changes in > the future are possible. -The goal of this project is to create an easy-to-use setup to self-host [Ory Kratos](https://www.ory.sh/kratos) -and [Ory Hydra](https://www.ory.sh/hydra). It will contain an authentication UI, implementing all self-service flows for -Ory Kratos and Ory Hydra, as well as an admin UI. All UI components are written in NextJS and Typescript, and styled -using shadcn/ui and TailwindCSS. +The goal of this project is to create an easy-to-use setup to self-host the [Ory](https://www.ory.sh) stack with all its +components. It will contain an authentication UI, implementing all self-service flows for Ory Kratos and Ory Hydra, as +well as an admin UI. All UI components are written in NextJS and Typescript, and styled using shadcn/ui and TailwindCSS. ## Getting started @@ -41,6 +40,15 @@ bun install bun run dev ``` +To access the admin dashboard, the `identity` has to be a `member` of the `admin` role. (Relation: roles:admin@< +identity_id>#member)
+The identity ID is displayed on the screen when accessing the dashboard without sufficient permissions.
+Use the identity ID to execute the following script with the identity ID as an argument. + +```bash +sh docker/ory-dev/keto-make-admin.sh +``` + ## Deployment *soon.* @@ -58,11 +66,10 @@ Hydra. It is implemented in a way, that customizing style and page layout is ver ## Admin Dashboard -Right now I am working on the admin dashboard for Ory Kratos. It will provide you with an overview of your instance and -let you manage users, OAuth2 applications and more. It is ***work in progress*** and should not be used in anything -important as it is not yet protected by Keto permissions but only by a valid Kratos session! +Right now I am working on the admin dashboard for all Ory applications. It will provide you with an overview of your +instances and let you manage users, OAuth2 applications and more. It is ***work in progress*** and should be handled +with caution. ![A browser window showing the home page of the dashboard UI in dark mode](./documentation/.img/d-dashboard-dark.png) ![A browser window showing the users page of the dashboard UI in dark mode](./documentation/.img/d-users-dark.png) - diff --git a/authentication/README.md b/authentication/README.md index 0e6613c..40979b3 100644 --- a/authentication/README.md +++ b/authentication/README.md @@ -1,6 +1,6 @@ # Next-Ory - Authentication -This directory contains a NextJS 14 (app router) UI Node, implementing all Ory Kratos and Ory Hydra UI flows. +This directory contains a NextJS 15 (app router) UI Node, implementing all Ory Kratos and Ory Hydra UI flows. ## Stack diff --git a/authentication/src/app/layout.tsx b/authentication/src/app/layout.tsx index 9c43841..d3513e3 100644 --- a/authentication/src/app/layout.tsx +++ b/authentication/src/app/layout.tsx @@ -11,7 +11,7 @@ const inter = Inter({ subsets: ['latin'] }); const APP_NAME = 'Next Ory'; const APP_DEFAULT_TITLE = 'Next Ory'; const APP_TITLE_TEMPLATE = `%s | ${APP_DEFAULT_TITLE}`; -const APP_DESCRIPTION = 'Get started with ORY authentication quickly and easily.'; +const APP_DESCRIPTION = 'Get started with Ory authentication quickly and easily.'; export const metadata = { applicationName: APP_NAME, diff --git a/dashboard/README.md b/dashboard/README.md index 1d8021d..dcca7ae 100644 --- a/dashboard/README.md +++ b/dashboard/README.md @@ -1,6 +1,6 @@ # Next-Ory - Dashboard -This directory contains a NextJS 15 (app router) UI Node, implementing the admin dashboard to the ORY Kratos instance. +This directory contains a NextJS 15 (app router) UI Node, implementing the admin dashboard for the Ory admin APIs. ## Stack diff --git a/dashboard/src/app/application/page.tsx b/dashboard/src/app/(inside)/application/page.tsx similarity index 100% rename from dashboard/src/app/application/page.tsx rename to dashboard/src/app/(inside)/application/page.tsx diff --git a/dashboard/src/app/(inside)/layout.tsx b/dashboard/src/app/(inside)/layout.tsx new file mode 100644 index 0000000..5c8f890 --- /dev/null +++ b/dashboard/src/app/(inside)/layout.tsx @@ -0,0 +1,37 @@ +import '../globals.css'; +import { Toaster } from '@/components/ui/sonner'; +import React from 'react'; +import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'; +import { AppSidebar } from '@/components/app-sidebar'; +import { Separator } from '@/components/ui/separator'; +import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList } from '@/components/ui/breadcrumb'; + +export default function InsideLayout({ children }: Readonly<{ children: React.ReactNode }>) { + return ( + + + +
+ + + { + // TODO: implement dynamic Breadcrumbs + } + + + + + Ory Dashboard + + + + +
+
+ {children} +
+
+ +
+ ); +} diff --git a/dashboard/src/app/page.tsx b/dashboard/src/app/(inside)/page.tsx similarity index 68% rename from dashboard/src/app/page.tsx rename to dashboard/src/app/(inside)/page.tsx index 72fa7dc..cf379ab 100644 --- a/dashboard/src/app/page.tsx +++ b/dashboard/src/app/(inside)/page.tsx @@ -1,4 +1,4 @@ -import { getHydraMetadataApi, getKratosMetadataApi } from '@/ory/sdk/server'; +import { getHydraMetadataApi, getKetoMetadataApi, getKratosMetadataApi } from '@/ory/sdk/server'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; @@ -17,6 +17,7 @@ export default async function RootPage() { const kratosDBStatusData = await fetch(process.env.ORY_KRATOS_ADMIN_URL + '/health/ready'); const kratosDBStatus = await kratosDBStatusData.json() as { status: string }; + const hydraMetadataApi = await getHydraMetadataApi(); const hydraVersion = await hydraMetadataApi @@ -30,13 +31,27 @@ export default async function RootPage() { const hydraDBStatusData = await fetch(process.env.ORY_KRATOS_ADMIN_URL + '/health/ready'); const hydraDBStatus = await hydraDBStatusData.json() as { status: string }; + + const ketoMetadataApi = await getKetoMetadataApi(); + + const ketoVersion = await ketoMetadataApi + .getVersion() + .then(res => res.data.version) + .catch(() => ''); + + const ketoStatusData = await fetch(process.env.ORY_KETO_ADMIN_URL + '/health/alive'); + const ketoStatus = await ketoStatusData.json() as { status: string }; + + const ketoDBStatusData = await fetch(process.env.ORY_KETO_ADMIN_URL + '/health/ready'); + const ketoDBStatus = await ketoDBStatusData.json() as { status: string }; + return (

Software Stack

See the list of all applications in your stack

-
+
@@ -73,7 +88,24 @@ export default async function RootPage() { -
+ + + + Ory Keto + + + Version {ketoVersion} + + + + + Keto {ketoStatus.status.toUpperCase()} + + + Database {ketoDBStatus.status.toUpperCase()} + + +
diff --git a/dashboard/src/app/user/[id]/page.tsx b/dashboard/src/app/(inside)/user/[id]/page.tsx similarity index 100% rename from dashboard/src/app/user/[id]/page.tsx rename to dashboard/src/app/(inside)/user/[id]/page.tsx diff --git a/dashboard/src/app/user/action.ts b/dashboard/src/app/(inside)/user/action.ts similarity index 100% rename from dashboard/src/app/user/action.ts rename to dashboard/src/app/(inside)/user/action.ts diff --git a/dashboard/src/app/user/data-table.tsx b/dashboard/src/app/(inside)/user/data-table.tsx similarity index 99% rename from dashboard/src/app/user/data-table.tsx rename to dashboard/src/app/(inside)/user/data-table.tsx index 5819331..6476d7f 100644 --- a/dashboard/src/app/user/data-table.tsx +++ b/dashboard/src/app/(inside)/user/data-table.tsx @@ -6,7 +6,7 @@ import { DataTable } from '@/components/ui/data-table'; import { CircleCheck, CircleX, Copy, MoreHorizontal, Trash, UserCheck, UserMinus, UserPen, UserX } from 'lucide-react'; import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'; import React, { useEffect, useRef, useState } from 'react'; -import { FetchIdentityPageProps } from '@/app/user/page'; +import { FetchIdentityPageProps } from '@/app/(inside)/user/page'; import { Spinner } from '@/components/ui/spinner'; import { DropdownMenu, @@ -29,7 +29,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; -import { blockIdentity, deleteIdentity, deleteIdentitySessions, unblockIdentity } from '@/app/user/action'; +import { blockIdentity, deleteIdentity, deleteIdentitySessions, unblockIdentity } from '@/app/(inside)/user/action'; interface IdentityDataTableProps { data: Identity[]; diff --git a/dashboard/src/app/user/page.tsx b/dashboard/src/app/(inside)/user/page.tsx similarity index 97% rename from dashboard/src/app/user/page.tsx rename to dashboard/src/app/(inside)/user/page.tsx index 6fe3108..f02c636 100644 --- a/dashboard/src/app/user/page.tsx +++ b/dashboard/src/app/(inside)/user/page.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { IdentityDataTable } from '@/app/user/data-table'; +import { IdentityDataTable } from '@/app/(inside)/user/data-table'; import { getIdentityApi } from '@/ory/sdk/server'; import { SearchInput } from '@/components/search-input'; diff --git a/dashboard/src/app/(outside)/unauthorised/page.tsx b/dashboard/src/app/(outside)/unauthorised/page.tsx new file mode 100644 index 0000000..21905ef --- /dev/null +++ b/dashboard/src/app/(outside)/unauthorised/page.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { ErrorDisplay } from '@/components/error'; +import { Button } from '@/components/ui/button'; +import { kratos, LogoutLink } from '@/ory'; +import { LogOut } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { Session } from '@ory/client'; +import { Skeleton } from '@/components/ui/skeleton'; + +export default function UnauthorizedPage() { + + const [session, setSession] = useState(undefined); + + useEffect(() => { + kratos.toSession() + .then((response) => setSession(response.data)); + }, []); + + return ( +
+ + + { + session ? +

USER ID {session.identity?.id}

+ : + + } + + +
+ ); +} diff --git a/dashboard/src/app/layout.tsx b/dashboard/src/app/layout.tsx index 8fda46d..3cbf18b 100644 --- a/dashboard/src/app/layout.tsx +++ b/dashboard/src/app/layout.tsx @@ -2,20 +2,15 @@ import type { Viewport } from 'next'; import { Inter } from 'next/font/google'; import './globals.css'; import { cn } from '@/lib/utils'; -import { Toaster } from '@/components/ui/sonner'; import React from 'react'; import { ThemeProvider } from 'next-themes'; -import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'; -import { AppSidebar } from '@/components/app-sidebar'; -import { Separator } from '@/components/ui/separator'; -import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList } from '@/components/ui/breadcrumb'; const inter = Inter({ subsets: ['latin'] }); const APP_NAME = 'Next Ory'; const APP_DEFAULT_TITLE = 'Next Ory'; const APP_TITLE_TEMPLATE = `%s | ${APP_DEFAULT_TITLE}`; -const APP_DESCRIPTION = 'Get started with ORY authentication quickly and easily.'; +const APP_DESCRIPTION = 'Get started with Ory authentication quickly and easily.'; export const metadata = { applicationName: APP_NAME, @@ -56,31 +51,7 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac enableSystem disableTransitionOnChange > - - - -
- - - { - // TODO: implement dynamic Breadcrumbs - } - - - - - Ory Dashboard - - - - -
-
- {children} -
-
- -
+ {children} diff --git a/dashboard/src/components/error.tsx b/dashboard/src/components/error.tsx index e264180..5e5ac12 100644 --- a/dashboard/src/components/error.tsx +++ b/dashboard/src/components/error.tsx @@ -3,11 +3,11 @@ interface ErrorDisplayProps { message: string; } -export async function ErrorDisplay({ title, message }: ErrorDisplayProps) { +export function ErrorDisplay({ title, message }: ErrorDisplayProps) { return ( -
+ <>

{title}

{message}

-
+ ); } \ No newline at end of file diff --git a/dashboard/src/middleware.ts b/dashboard/src/middleware.ts index 4ef97c3..52d7714 100644 --- a/dashboard/src/middleware.ts +++ b/dashboard/src/middleware.ts @@ -1,13 +1,13 @@ -import { NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server'; import { cookies } from 'next/headers'; -import { getFrontendApi } from '@/ory/sdk/server'; +import { getFrontendApi, getPermissionApi } from '@/ory/sdk/server'; -export async function middleware() { +export async function middleware(request: NextRequest) { - const api = await getFrontendApi(); + const frontendApi = await getFrontendApi(); const cookie = await cookies(); - const session = await api + const session = await frontendApi .toSession({ cookie: 'ory_kratos_session=' + cookie.get('ory_kratos_session')?.value }) .then((response) => response.data) .catch(() => null); @@ -25,7 +25,40 @@ export async function middleware() { return NextResponse.redirect(url); } - return NextResponse.next(); + const permissionApi = await getPermissionApi(); + const isAdmin = await permissionApi.checkPermission({ + namespace: 'roles', + object: 'admin', + relation: 'member', + subjectId: session!.identity!.id, + }) + .then(({ data: { allowed } }) => { + console.log('is_admin', session!.identity!.id, allowed); + return allowed; + }) + .catch((response) => { + console.log('is_admin', session!.identity!.id, response, 'check failed'); + return false; + }); + + if (isAdmin) { + if (request.nextUrl.pathname === '/unauthorised') { + return redirect('/', 'HAS PERMISSION BUT ACCESSING /unauthorized'); + } + return NextResponse.next(); + } else { + if (request.nextUrl.pathname === '/unauthorised') { + return NextResponse.next(); + } + return redirect('/unauthorised', 'MISSING SESSION'); + } +} + +function redirect(path: string, reason: string) { + console.log(reason); + const url = `${process.env.NEXT_PUBLIC_DASHBOARD_NODE_URL}${path}`; + console.log('REDIRECT TO', url); + return NextResponse.redirect(url!); } export const config = { diff --git a/dashboard/src/ory/sdk/server/index.ts b/dashboard/src/ory/sdk/server/index.ts index fb2a176..7649433 100644 --- a/dashboard/src/ory/sdk/server/index.ts +++ b/dashboard/src/ory/sdk/server/index.ts @@ -1,6 +1,14 @@ 'use server'; -import { Configuration, FrontendApi, IdentityApi, MetadataApi, OAuth2Api } from '@ory/client'; +import { + Configuration, + FrontendApi, + IdentityApi, + MetadataApi, + OAuth2Api, + PermissionApi, + RelationshipApi, +} from '@ory/client'; // #################################################################################### @@ -92,3 +100,57 @@ const kratosMetadataApi = new MetadataApi( export async function getKratosMetadataApi() { return kratosMetadataApi; } + + +// #################################################################################### +// Relationship API +// #################################################################################### + +const relationshipApi = new RelationshipApi(new Configuration( + { + basePath: process.env.ORY_KETO_ADMIN_URL, + baseOptions: { + withCredentials: true, + }, + }, +)); + +export async function getRelationshipApi() { + return relationshipApi; +} + + +// #################################################################################### +// Permission API +// #################################################################################### + +const permissionApi = new PermissionApi(new Configuration( + { + basePath: process.env.ORY_KETO_ADMIN_URL, + baseOptions: { + withCredentials: true, + }, + }, +)); + +export async function getPermissionApi() { + return permissionApi; +} + + +// #################################################################################### +// Keto Metadata API +// #################################################################################### + +const ketoMetadataApi = new MetadataApi(new Configuration( + { + basePath: process.env.ORY_KETO_ADMIN_URL, + baseOptions: { + withCredentials: true, + }, + }, +)); + +export async function getKetoMetadataApi() { + return ketoMetadataApi; +} diff --git a/docker/README.md b/docker/README.md index da0d4a8..0a39252 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,6 +1,6 @@ # Starting as a container -Starting this project in a container makes testing it really easy. \ +Starting this project in a container makes testing it really easy. ```bash # move to the environment you want to start (here development) @@ -17,7 +17,7 @@ sh ./hydra-test-consent.sh ``` These commands will start up multiple containers in the background. -Then continue with starting the authentication UI development server as described in the authentication README. +Then continue with starting the authentication UI development server as described in the root README. ## Services and Ports @@ -28,13 +28,15 @@ If you start up the environment on a remote server, you will need to tunnel the | Service | Port (Public) | Description | |----------------|---------------|---------------------------------------------------------------------------| -| Console | 4411 (✗) | Admin dashboard for Kratos data management (soon) | +| Console | 4000 (✗) | Admin dashboard for Kratos data management (soon) | | Authentication | 3000 (✗) | User interface for authentication and account management (no docker yet) | -| ORY Kratos | 4433 (✗) | User management system handling users and self-service flows (Public API) | -| ORY Kratos | 4434 (✗) | User management system handling users and self-service flows (Admin API) | +| Ory Kratos | 4433 (✗) | User management system handling users and self-service flows (Public API) | +| | 4434 (✗) | User management system handling users and self-service flows (Admin API) | | Mailslurper | 4436 (✗) | Mock mailing server (Dashboard) | -| Mailslurper | 4437 (✗) | Mock mailing server (API) | -| ORY Hydra | 4444 (✗) | OAuth2 and OIDC server connected to Kratos (Public API) | -| ORY Hydra | 4445 (✗) | OAuth2 and OIDC server connected to Kratos (Admin API) | -| ORY Hydra | 5555 (✗) | Hydra test application to test the consent flow | +| | 4437 (✗) | Mock mailing server (API) | +| Ory Hydra | 4444 (✗) | OAuth2 and OIDC server connected to Kratos (Public API) | +| | 4445 (✗) | OAuth2 and OIDC server connected to Kratos (Admin API) | +| | 5555 (✗) | Hydra test application to test the consent flow | +| Ory Keto | 4466 (✗) | Read Endpoint for Ory Keto authorization ("Public" API) | +| | 4467 (✗) | Write Endpoint for Ory Keto authorization ("Admin" API) | | Postgres DB | 4455 (✗) | Postgres database for storing user data | diff --git a/docker/ory-dev/.env b/docker/ory-dev/.env index 2d3b681..5714cfa 100644 --- a/docker/ory-dev/.env +++ b/docker/ory-dev/.env @@ -1,2 +1,2 @@ -# The URL of ORY Hydras admin API +# The URL of Ory Hydras admin API HYDRA_ADMIN_API=http://hydra:4445 diff --git a/docker/ory-dev/docker-compose.yaml b/docker/ory-dev/docker-compose.yaml index 190a121..1b603e5 100644 --- a/docker/ory-dev/docker-compose.yaml +++ b/docker/ory-dev/docker-compose.yaml @@ -70,6 +70,39 @@ services: networks: - internal + ory-keto-migrate: + container_name: ory-keto-migrate + image: oryd/keto:v0.12.0 + restart: on-failure + volumes: + - ./ory/keto:/etc/config/keto + - ory-keto-data:/home/ory + - ory-keto-data:/var/lib/sqlite + command: migrate -c /etc/config/keto/keto.yaml up --yes + depends_on: + ory-postgres: + condition: service_healthy + networks: + - internal + + + ory-keto: + container_name: ory-keto + image: oryd/keto:v0.12.0 + restart: unless-stopped + ports: + - 127.0.0.1:4466:4466 # public + - 127.0.0.1:4467:4467 # admin + volumes: + - ./ory/keto:/etc/config/keto + - ory-keto-data:/home/ory + - ory-keto-data:/var/lib/sqlite + command: serve -c /etc/config/keto/keto.yaml all + depends_on: + ory-keto-migrate: + condition: service_completed_successfully + networks: + - internal ory-mailslurper: container_name: ory-mailslurper @@ -111,4 +144,5 @@ networks: volumes: ory-kratos-data: ory-hydra-data: + ory-keto-data: ory-postgres-data: diff --git a/docker/ory-dev/keto-make-admin.sh b/docker/ory-dev/keto-make-admin.sh new file mode 100644 index 0000000..a08624b --- /dev/null +++ b/docker/ory-dev/keto-make-admin.sh @@ -0,0 +1,24 @@ +# this script gives the referenced identity the admin role +# make sure to provide the id of the identity + +# check if a identity id argument was provided +if [ -z "$1" ]; then + echo "Error: please provide an identity id." + exit 1 +fi + +# set user id variable +IDENTITY_ID=$1 + +# execute curl to Ory Keto write endpoint +curl --request PUT \ + --url http://localhost:4467/admin/relation-tuples \ + --data '{ + "namespace": "roles", + "object": "admin", + "relation": "member", + "subject_id": "'"$IDENTITY_ID"'" + }' + +# write success response to terminal +echo "Applied admin role to the user with ID $IDENTITY_ID" diff --git a/docker/ory-dev/ory/keto/keto.yaml b/docker/ory-dev/ory/keto/keto.yaml new file mode 100644 index 0000000..21dad3d --- /dev/null +++ b/docker/ory-dev/ory/keto/keto.yaml @@ -0,0 +1,43 @@ +# +# Documentation: https://www.ory.sh/docs/keto/reference/configuration +# Configuration UI: https://www.ory.sh/docs/keto/reference/configuration-editor +# + +# +# Configure the Keto logging +# +log: + level: info + format: text + leak_sensitive_values: true + +# +# Configure the datasource. Alternative for development purposes is 'memory' (not persistent!) +# +dsn: postgres://postgres:postgres@ory-postgres:5432/keto?sslmode=disable&max_conns=20&max_idle_conns=4 + +# +# Set the required namespaces +# +namespaces: + - id: 0 + name: roles + +serve: + read: + host: 0.0.0.0 + port: 4466 + cors: + enabled: true + allowed_origins: + - http://localhost:3000 + - http://localhost:4000 + + write: + host: 0.0.0.0 + port: 4467 + cors: + enabled: true + allowed_origins: + - http://localhost:3000 + - http://localhost:4000 diff --git a/docker/ory-dev/ory/kratos/identity.schema.json b/docker/ory-dev/ory/kratos/identity.schema.json index b937fc1..c3572c0 100644 --- a/docker/ory-dev/ory/kratos/identity.schema.json +++ b/docker/ory-dev/ory/kratos/identity.schema.json @@ -15,9 +15,6 @@ "credentials": { "password": { "identifier": true - }, - "webauthn": { - "identifier": true } }, "recovery": { diff --git a/docker/ory-dev/postgres/init.sql b/docker/ory-dev/postgres/init.sql index 1e2b2ed..45836a3 100644 --- a/docker/ory-dev/postgres/init.sql +++ b/docker/ory-dev/postgres/init.sql @@ -3,3 +3,6 @@ GRANT ALL PRIVILEGES ON DATABASE kratos TO postgres; CREATE DATABASE hydra; GRANT ALL PRIVILEGES ON DATABASE hydra TO postgres; + +CREATE DATABASE keto; +GRANT ALL PRIVILEGES ON DATABASE keto TO postgres;