diff --git a/package-lock.json b/package-lock.json
index 5b84f4f..837cc99 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -28,6 +28,7 @@
"@types/node": "^20.12.7",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
+ "@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
@@ -47,7 +48,7 @@
"prettier": "^3.2.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
- "react-hook-form": "^7.51.3",
+ "react-hook-form": "^7.51.4",
"react-router": "^6.23.0",
"react-router-dom": "^6.23.0",
"sonner": "^1.4.41",
@@ -55,8 +56,9 @@
"tailwindcss": "^3.4.3",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.4.5",
+ "uuid": "^9.0.1",
"xml-formatter": "^3.6.2",
- "zod": "^3.23.5"
+ "zod": "^3.23.8"
}
},
"node_modules/@alloc/quick-lru": {
@@ -1273,6 +1275,11 @@
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ=="
},
+ "node_modules/@types/uuid": {
+ "version": "9.0.8",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
+ "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz",
@@ -5044,9 +5051,9 @@
}
},
"node_modules/react-hook-form": {
- "version": "7.51.3",
- "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.3.tgz",
- "integrity": "sha512-cvJ/wbHdhYx8aviSWh28w9ImjmVsb5Y05n1+FW786vEZQJV5STNM0pW6ujS+oiBecb0ARBxJFyAnXj9+GHXACQ==",
+ "version": "7.51.4",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.4.tgz",
+ "integrity": "sha512-V14i8SEkh+V1gs6YtD0hdHYnoL4tp/HX/A45wWQN15CYr9bFRmmRdYStSO5L65lCCZRF+kYiSKhm9alqbcdiVA==",
"engines": {
"node": ">=12.22.0"
},
@@ -6373,9 +6380,9 @@
}
},
"node_modules/zod": {
- "version": "3.23.5",
- "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.5.tgz",
- "integrity": "sha512-fkwiq0VIQTksNNA131rDOsVJcns0pfVUjHzLrNBiF/O/Xxb5lQyEXkhZWcJ7npWsYlvs+h0jFWXXy4X46Em1JA==",
+ "version": "3.23.8",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
+ "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -7089,6 +7096,11 @@
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ=="
},
+ "@types/uuid": {
+ "version": "9.0.8",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
+ "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="
+ },
"@typescript-eslint/eslint-plugin": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz",
@@ -9644,9 +9656,9 @@
}
},
"react-hook-form": {
- "version": "7.51.3",
- "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.3.tgz",
- "integrity": "sha512-cvJ/wbHdhYx8aviSWh28w9ImjmVsb5Y05n1+FW786vEZQJV5STNM0pW6ujS+oiBecb0ARBxJFyAnXj9+GHXACQ==",
+ "version": "7.51.4",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.4.tgz",
+ "integrity": "sha512-V14i8SEkh+V1gs6YtD0hdHYnoL4tp/HX/A45wWQN15CYr9bFRmmRdYStSO5L65lCCZRF+kYiSKhm9alqbcdiVA==",
"requires": {}
},
"react-is": {
@@ -10552,9 +10564,9 @@
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
},
"zod": {
- "version": "3.23.5",
- "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.5.tgz",
- "integrity": "sha512-fkwiq0VIQTksNNA131rDOsVJcns0pfVUjHzLrNBiF/O/Xxb5lQyEXkhZWcJ7npWsYlvs+h0jFWXXy4X46Em1JA=="
+ "version": "3.23.8",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
+ "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="
}
}
}
diff --git a/package.json b/package.json
index f389c99..6dad17c 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
"@types/node": "^20.12.7",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
+ "@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
@@ -54,7 +55,7 @@
"prettier": "^3.2.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
- "react-hook-form": "^7.51.3",
+ "react-hook-form": "^7.51.4",
"react-router": "^6.23.0",
"react-router-dom": "^6.23.0",
"sonner": "^1.4.41",
@@ -62,7 +63,8 @@
"tailwindcss": "^3.4.3",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.4.5",
+ "uuid": "^9.0.1",
"xml-formatter": "^3.6.2",
- "zod": "^3.23.5"
+ "zod": "^3.23.8"
}
}
diff --git a/src/App.tsx b/src/App.tsx
index e9ab127..0cfa5ca 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,12 +1,19 @@
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { SSOPage } from "@/pages/SSOPage";
+import { HomePage } from "@/pages/HomePage";
+import { ViewAppPage } from "@/pages/ViewAppPage";
+import { Page } from "@/components/Page";
export function App() {
return (
- } />
+ }>
+ } />
+ } />
+ } />
+
);
diff --git a/src/components/Page.tsx b/src/components/Page.tsx
new file mode 100644
index 0000000..66af1b0
--- /dev/null
+++ b/src/components/Page.tsx
@@ -0,0 +1,23 @@
+import React from "react";
+import { Outlet } from "react-router";
+
+export function Page() {
+ return (
+
+
+
+
+
+ DummyIDP is a dumbed-down Identity Provider you can use to test
+ Enterprise Single-Sign On for free.
+
+
+
+
+
+ );
+}
diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..8722561
--- /dev/null
+++ b/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,139 @@
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
new file mode 100644
index 0000000..0ba4277
--- /dev/null
+++ b/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
new file mode 100644
index 0000000..afa13ec
--- /dev/null
+++ b/src/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/src/components/ui/collapsible.tsx b/src/components/ui/collapsible.tsx
new file mode 100644
index 0000000..a23e7a2
--- /dev/null
+++ b/src/components/ui/collapsible.tsx
@@ -0,0 +1,9 @@
+import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
+
+const Collapsible = CollapsiblePrimitive.Root
+
+const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
+
+const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent }
diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx
new file mode 100644
index 0000000..4603f8b
--- /dev/null
+++ b/src/components/ui/form.tsx
@@ -0,0 +1,176 @@
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { Slot } from "@radix-ui/react-slot"
+import {
+ Controller,
+ ControllerProps,
+ FieldPath,
+ FieldValues,
+ FormProvider,
+ useFormContext,
+} from "react-hook-form"
+
+import { cn } from "@/lib/utils"
+import { Label } from "@/components/ui/label"
+
+const Form = FormProvider
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+> = {
+ name: TName
+}
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue
+)
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ )
+}
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext)
+ const itemContext = React.useContext(FormItemContext)
+ const { getFieldState, formState } = useFormContext()
+
+ const fieldState = getFieldState(fieldContext.name, formState)
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within ")
+ }
+
+ const { id } = itemContext
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ }
+}
+
+type FormItemContextValue = {
+ id: string
+}
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue
+)
+
+const FormItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const id = React.useId()
+
+ return (
+
+
+
+ )
+})
+FormItem.displayName = "FormItem"
+
+const FormLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { error, formItemId } = useFormField()
+
+ return (
+
+ )
+})
+FormLabel.displayName = "FormLabel"
+
+const FormControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+ return (
+
+ )
+})
+FormControl.displayName = "FormControl"
+
+const FormDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField()
+
+ return (
+
+ )
+})
+FormDescription.displayName = "FormDescription"
+
+const FormMessage = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => {
+ const { error, formMessageId } = useFormField()
+ const body = error ? String(error?.message) : children
+
+ if (!body) {
+ return null
+ }
+
+ return (
+
+ {body}
+
+ )
+})
+FormMessage.displayName = "FormMessage"
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+}
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
new file mode 100644
index 0000000..677d05f
--- /dev/null
+++ b/src/components/ui/input.tsx
@@ -0,0 +1,25 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+export interface InputProps
+ extends React.InputHTMLAttributes {}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Input.displayName = "Input"
+
+export { Input }
diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx
new file mode 100644
index 0000000..683faa7
--- /dev/null
+++ b/src/components/ui/label.tsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+)
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/src/key.ts b/src/key.ts
new file mode 100644
index 0000000..41b4a0c
--- /dev/null
+++ b/src/key.ts
@@ -0,0 +1,33 @@
+export const GLOBAL_NONSECURE_KEY = {
+ kty: "RSA",
+ kid: "hSDIRRN-JumgcJ6YrpvCHpc0X46FLAzWXNFxPdmfmz0",
+ n: "qGaBCZZvw28aHP3nZdjhKUmWlYgrvZGKWE5lL1-sNiGpZUkewYf1nuxLOsz_T5i8LIRa5lX2Ckx4hodnEQ8OpbdEnsobnV-f-z-oW_By1_4JkOPJ8YPNiz5ciB_unnpTjwSZh5akH9pRbriBJuc-qu1aZUUGKw58jHlpjc25u1Oyf6DRdt1fQvnv42BK89AdeH8Y9cDvEIhabngbmSqOjPFsoQUDQNLfJvqgdFBYeogAz_uYkx1RVk0hXWrtzYkiACXbe4Nar7YpMTZHmWI_8qO-PAqidUq1Ooapfp6LbmuxMc5oOkEf8t1Ara3GCybtHv4hOFY29QjRO22ubAGrIw",
+ e: "AQAB",
+ d: "Eggj0gx_PCyF3cvcPr4Z4gtkqe9SS7KtXyZJ1GhIruUs19EcD3oI9XL03T99KR9AKv4jI53ZwiGNGE6gXSXBGkKFAQHAMjo-ja8zzmBxU6p6iL6zbX6BAGN1kgflS6fqkZpa_DdHrLd6V8I-5hUF01SmBMj-z5Z2BK6tfEcml6XCVFBzd0QqZuACrdO9ScZvL1cd4DCHJJNBarCVgC9akxOi9THtgD72EYJQyVcsLbzKqoxnT9JW6onCBBKtUaP8fEzp9WMK6APJCkYK5I-1lX8tSf48-lzgBrn3cb9Zo5DYcGo0bJgVwzI4w-HS_etiOOrURmbbX1x9W_rGBhS8tQ",
+ p: "7HH9QMjZmaD0UvDtdrX1aBgo8ZCWYShMfxzt1gIy6jaSgmcyGmMJeB9s-nCGopy8Iyd4jlggmgnWP4CqYbl6fmvZ-4ULHuc0DGaR1P6zhsF4fI6zCxWm6ijyIXM75Q67ccokLQeRzINKoOmkEQ2hgFKhiaKN4TVfanufECfvqzU",
+ q: "tlPfEoqbGqYJtxKlND0ihXMTjSVGTbwUBUXUdzYUx7nKOGQSeCLYXbpm4KIBEYa4GNpuub8vnZD8c-FuzIb-yKH66nCa_Q2q8zhfM764pAao9P1OSjbXkA7mL6nO6LwAk7OFNa22z0H5GTRkzX8DJiAG90z_67qrQPmfmFfZ7_c",
+ dp: "uBegSNqYoYax9AnluXG-mseEyV_71bWcqbOKcf_QSI8ozyMt3WGSs9Yz8WG_UciaqvxGXv26lHRoPZUeE2xoNRof5DcYC32UBrute5q7iIYGG2S3fj_jb5llvCmOTq-DvfrW48BrAkKOzm5a8XQddF3hq9nGgbweiDesBtYxQqk",
+ dq: "OD9joBq2guAaOyo7YQRDNBwuOer3519QZdgHFcfPXVZJtl_Y-sr1KOUqe74-yiNfg_tPEWqTy-5Ak5dGUT6MN2URPWYDynF-_Y20gQgjeia71OiYUHjew4h1JtsiA9aL7wUA4XB35zSZHld1iZfXZtmWjJBqm1R5JJBd1ee0Sj0",
+ qi: "ypr_PYU5UrV_Lw22vpa8wJ-C_Ux9nhiz4WtU4jgEX_RofgMra4iDXIVUL6BajWYZ7vj7_AFN2tELTMQ3Xhfc4FQ-CBQkvsLoXO6fzttdxBd_yO01YtijiT_ZwozO3wwzVuN65EGNCQ9haF3vEWEfTEN_-x7RG6xZnFtel6X8rqI",
+};
+
+export const GLOBAL_NONSECURE_CERT = `-----BEGIN CERTIFICATE-----
+MIIDBzCCAe+gAwIBAgIUCLBK4f75EXEe4gyroYnVaqLoSp4wDQYJKoZIhvcNAQEL
+BQAwEzERMA8GA1UEAwwIZHVtbXlpZHAwHhcNMjQwNTEzMjE1NDE2WhcNMzQwNTEx
+MjE1NDE2WjATMREwDwYDVQQDDAhkdW1teWlkcDCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAKhmgQmWb8NvGhz952XY4SlJlpWIK72RilhOZS9frDYhqWVJ
+HsGH9Z7sSzrM/0+YvCyEWuZV9gpMeIaHZxEPDqW3RJ7KG51fn/s/qFvwctf+CZDj
+yfGDzYs+XIgf7p56U48EmYeWpB/aUW64gSbnPqrtWmVFBisOfIx5aY3NubtTsn+g
+0XbdX0L57+NgSvPQHXh/GPXA7xCIWm54G5kqjozxbKEFA0DS3yb6oHRQWHqIAM/7
+mJMdUVZNIV1q7c2JIgAl23uDWq+2KTE2R5liP/KjvjwKonVKtTqGqX6ei25rsTHO
+aDpBH/LdQK2txgsm7R7+IThWNvUI0TttrmwBqyMCAwEAAaNTMFEwHQYDVR0OBBYE
+FD142gxIAJMhpgMkgpzmRNoW9XbEMB8GA1UdIwQYMBaAFD142gxIAJMhpgMkgpzm
+RNoW9XbEMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADQd6k6z
+FIc20GfGHY5C2MFwyGOmP5/UG/JiTq7Zky28G6D0NA0je+GztzXx7VYDfCfHxLcm
+2k5t9nYhb9kVawiLUUDVF6s+yZUXA4gUA3KoTWh1/oRxR3ggW7dKYm9fsNOdQAbx
+UUkzp7HLZ45ZlpKUS0hO7es+fPyF5KVw0g0SrtQWwWucnQMAQE9m+B0aOf+92y7J
+QkdgdR8Gd/XZ4NZfoOnKV7A1utT4rWxYCgICeRTHx9tly5OhPW4hQr5qOpngcsJ9
+vhr86IjznQXhfj3hql5lA3VbHW04ro37ROIkh2bShDq5dwJJHpYCGrF3MQv8S3m+
+jzGhYL6m9gFTm/8=
+-----END CERTIFICATE-----
+`;
diff --git a/src/lib/saml.ts b/src/lib/saml.ts
index 215a1cb..e16a0d1 100644
--- a/src/lib/saml.ts
+++ b/src/lib/saml.ts
@@ -37,8 +37,6 @@ async function signatureValue(
): Promise {
const digest = await digestValue(assertionData);
const enc = new TextEncoder();
- console.log("signature part");
- console.log(signaturePart(digest));
const signatureData = await crypto.subtle.sign(
"RSASSA-PKCS1-v1_5",
key,
@@ -53,8 +51,6 @@ function signaturePart(digest: string): string {
async function digestValue(assertionData: AssertionData): Promise {
const enc = new TextEncoder();
- console.log("digest part");
- console.log(digestPart(assertionData));
const digestData = await crypto.subtle.digest(
"SHA-256",
enc.encode(digestPart(assertionData)),
diff --git a/src/lib/store.ts b/src/lib/store.ts
new file mode 100644
index 0000000..5bb1bb6
--- /dev/null
+++ b/src/lib/store.ts
@@ -0,0 +1,26 @@
+import { useEffect, useState } from "react";
+
+interface StoreData {
+ apps: Record;
+}
+
+interface App {
+ id: string;
+ spAcsUrl: string;
+ spEntityId: string;
+}
+
+export function useStore(): [StoreData, (_: StoreData) => void] {
+ const [state, setState] = useState(() => {
+ const item = localStorage.getItem("store-data");
+ return item === null ? { apps: {} } : JSON.parse(item);
+ });
+
+ return [
+ state,
+ (state) => {
+ localStorage.setItem("store-data", JSON.stringify(state));
+ setState(state);
+ },
+ ];
+}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000..d084cca
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx
new file mode 100644
index 0000000..788df9b
--- /dev/null
+++ b/src/pages/HomePage.tsx
@@ -0,0 +1,29 @@
+import React from "react";
+import { Button } from "@/components/ui/button";
+import { useStore } from "@/lib/store";
+import { v4 as uuidv4 } from "uuid";
+import { useNavigate } from "react-router";
+
+export function HomePage() {
+ const [storeData, setStoreData] = useStore();
+ const navigate = useNavigate();
+ const handleCreateApp = () => {
+ const id = uuidv4();
+ const newState = {
+ ...storeData,
+ apps: {
+ ...storeData.apps,
+ [id]: {
+ id,
+ spAcsUrl: "",
+ },
+ },
+ };
+ console.log("want new state", newState);
+ setStoreData(newState);
+
+ navigate(`/apps/${id}`);
+ };
+
+ return Create new app ;
+}
diff --git a/src/pages/SSOPage.tsx b/src/pages/SSOPage.tsx
index 976ae08..e371f5d 100644
--- a/src/pages/SSOPage.tsx
+++ b/src/pages/SSOPage.tsx
@@ -1,62 +1,181 @@
import React, { useEffect, useRef, useState } from "react";
import { encodeAssertion } from "@/lib/saml";
-import { useNavigate } from "react-router";
+import { GLOBAL_NONSECURE_KEY } from "@/key";
+import { useStore } from "@/lib/store";
+import { useParams } from "react-router";
+import { z } from "zod";
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { useSearchParams } from "react-router-dom";
+import moment from "moment";
+
+const formSchema = z.object({
+ email: z.string().email({ message: "Email must be a well-formed email." }),
+ firstName: z.string(),
+ lastName: z.string(),
+});
export function SSOPage() {
- const navigate = useNavigate();
+ const [storeData, _] = useStore();
+ const { appId } = useParams();
+ const app = storeData.apps[appId!];
+
+ const [searchParams] = useSearchParams();
+ const samlRequest = searchParams.get("SAMLRequest");
+
+ const [sessionId, setSessionId] = useState("");
+
useEffect(() => {
- (async () => {
- const jwk = {
- kty: "RSA",
- kid: "hSDIRRN-JumgcJ6YrpvCHpc0X46FLAzWXNFxPdmfmz0",
- n: "qGaBCZZvw28aHP3nZdjhKUmWlYgrvZGKWE5lL1-sNiGpZUkewYf1nuxLOsz_T5i8LIRa5lX2Ckx4hodnEQ8OpbdEnsobnV-f-z-oW_By1_4JkOPJ8YPNiz5ciB_unnpTjwSZh5akH9pRbriBJuc-qu1aZUUGKw58jHlpjc25u1Oyf6DRdt1fQvnv42BK89AdeH8Y9cDvEIhabngbmSqOjPFsoQUDQNLfJvqgdFBYeogAz_uYkx1RVk0hXWrtzYkiACXbe4Nar7YpMTZHmWI_8qO-PAqidUq1Ooapfp6LbmuxMc5oOkEf8t1Ara3GCybtHv4hOFY29QjRO22ubAGrIw",
- e: "AQAB",
- d: "Eggj0gx_PCyF3cvcPr4Z4gtkqe9SS7KtXyZJ1GhIruUs19EcD3oI9XL03T99KR9AKv4jI53ZwiGNGE6gXSXBGkKFAQHAMjo-ja8zzmBxU6p6iL6zbX6BAGN1kgflS6fqkZpa_DdHrLd6V8I-5hUF01SmBMj-z5Z2BK6tfEcml6XCVFBzd0QqZuACrdO9ScZvL1cd4DCHJJNBarCVgC9akxOi9THtgD72EYJQyVcsLbzKqoxnT9JW6onCBBKtUaP8fEzp9WMK6APJCkYK5I-1lX8tSf48-lzgBrn3cb9Zo5DYcGo0bJgVwzI4w-HS_etiOOrURmbbX1x9W_rGBhS8tQ",
- p: "7HH9QMjZmaD0UvDtdrX1aBgo8ZCWYShMfxzt1gIy6jaSgmcyGmMJeB9s-nCGopy8Iyd4jlggmgnWP4CqYbl6fmvZ-4ULHuc0DGaR1P6zhsF4fI6zCxWm6ijyIXM75Q67ccokLQeRzINKoOmkEQ2hgFKhiaKN4TVfanufECfvqzU",
- q: "tlPfEoqbGqYJtxKlND0ihXMTjSVGTbwUBUXUdzYUx7nKOGQSeCLYXbpm4KIBEYa4GNpuub8vnZD8c-FuzIb-yKH66nCa_Q2q8zhfM764pAao9P1OSjbXkA7mL6nO6LwAk7OFNa22z0H5GTRkzX8DJiAG90z_67qrQPmfmFfZ7_c",
- dp: "uBegSNqYoYax9AnluXG-mseEyV_71bWcqbOKcf_QSI8ozyMt3WGSs9Yz8WG_UciaqvxGXv26lHRoPZUeE2xoNRof5DcYC32UBrute5q7iIYGG2S3fj_jb5llvCmOTq-DvfrW48BrAkKOzm5a8XQddF3hq9nGgbweiDesBtYxQqk",
- dq: "OD9joBq2guAaOyo7YQRDNBwuOer3519QZdgHFcfPXVZJtl_Y-sr1KOUqe74-yiNfg_tPEWqTy-5Ak5dGUT6MN2URPWYDynF-_Y20gQgjeia71OiYUHjew4h1JtsiA9aL7wUA4XB35zSZHld1iZfXZtmWjJBqm1R5JJBd1ee0Sj0",
- qi: "ypr_PYU5UrV_Lw22vpa8wJ-C_Ux9nhiz4WtU4jgEX_RofgMra4iDXIVUL6BajWYZ7vj7_AFN2tELTMQ3Xhfc4FQ-CBQkvsLoXO6fzttdxBd_yO01YtijiT_ZwozO3wwzVuN65EGNCQ9haF3vEWEfTEN_-x7RG6xZnFtel6X8rqI",
- };
+ if (!samlRequest) {
+ return;
+ }
- const key = await window.crypto.subtle.importKey(
- "jwk",
- jwk,
- {
- name: "RSASSA-PKCS1-v1_5",
- hash: "SHA-256",
- },
- true,
- ["sign"],
- );
+ const samlRequestXML = atob(
+ // url to std base64
+ samlRequest.replace(/-/g, "+").replace(/_/g, "/"),
+ );
- const assertion = await encodeAssertion(key, {
- idpEntityId: "IDP_ENTITY_ID",
- subjectId: "ulysse@dummyidp.com",
- spEntityId:
- "http://localhost:8080/saml/saml_conn_e4wryo0hq30mcrzc32b67otha",
- sessionId: "",
- expire: "2025-01-01T00:00:00Z",
- now: "2022-01-01T00:00:00Z",
- });
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(samlRequestXML, "text/xml");
- inputRef.current!.value = assertion;
- inputRef.current!.form!.submit();
- })();
+ // use xpath to get the AuthnRequest ID, throwing away all namespace information to make that easier
+ const id = doc.evaluate(
+ "string(/_:AuthnRequest/@ID)",
+ doc,
+ (_) => "urn:oasis:names:tc:SAML:2.0:protocol",
+ XPathResult.STRING_TYPE,
+ null,
+ ).stringValue;
+
+ setSessionId(id);
+ }, [samlRequest]);
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ email: "",
+ firstName: "",
+ lastName: "",
+ },
});
- // const formRef = useRef(null);
+ async function onSubmit(values: z.infer, e: any) {
+ const key = await window.crypto.subtle.importKey(
+ "jwk",
+ GLOBAL_NONSECURE_KEY,
+ {
+ name: "RSASSA-PKCS1-v1_5",
+ hash: "SHA-256",
+ },
+ true,
+ ["sign"],
+ );
+
+ const now = moment(new Date());
+ const expire = moment(new Date()).add(1, "hour");
+
+ inputRef.current!.value = await encodeAssertion(key, {
+ idpEntityId: `https://dummyidp.com/apps/${app.id}`,
+ subjectId: values.email,
+ spEntityId: app.spEntityId,
+ sessionId: sessionId,
+ now: now.format(),
+ expire: expire.format(),
+ });
+ inputRef.current!.form!.submit();
+ }
+
const inputRef = useRef(null);
return (
-
+ <>
-
+
+
+
+ Log on
+
+ Enter some details about who you want DummyIDP to log you in as.
+
+
+
+
+
+
+
+ >
);
}
diff --git a/src/pages/ViewAppPage.tsx b/src/pages/ViewAppPage.tsx
new file mode 100644
index 0000000..bd1ba70
--- /dev/null
+++ b/src/pages/ViewAppPage.tsx
@@ -0,0 +1,239 @@
+import React, { useState } from "react";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from "@/components/ui/alert-dialog";
+import { z } from "zod";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { useStore } from "@/lib/store";
+import { useParams } from "react-router";
+import { Button } from "@/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/ui/collapsible";
+import { GLOBAL_NONSECURE_CERT } from "@/key";
+import { Link } from "react-router-dom";
+
+const formSchema = z.object({
+ spAcsUrl: z
+ .string()
+ .url({ message: "Service Provider ACS URL must be a valid URL." }),
+ spEntityId: z.string(),
+});
+
+export function ViewAppPage() {
+ const [storeData, setStoreData] = useStore();
+ const { appId } = useParams();
+ const app = storeData.apps[appId!];
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ spAcsUrl: app.spAcsUrl,
+ spEntityId: app.spEntityId,
+ },
+ });
+
+ const [open, setOpen] = useState(false);
+
+ function onSubmit(values: z.infer, e: any) {
+ e.preventDefault();
+
+ setStoreData({
+ ...storeData,
+ apps: {
+ ...storeData.apps,
+ [app.id]: {
+ ...storeData.apps[app.id],
+ spAcsUrl: values.spAcsUrl,
+ spEntityId: values.spEntityId,
+ },
+ },
+ });
+
+ setOpen(false);
+ }
+
+ return (
+
+
+
+
+
+
+ App
+
+
+ {app.id}
+
+
+
+ This is a dummy app. This is a dumbed-down equivalent to an
+ app/tile in Okta, Google Workspace, Microsoft Entra, etc.
+
+
+
+
+ Edit
+
+
+
+ Edit app
+
+ Edit the settings associated with this dummy SSO app.
+
+
+
+
+
+
+
+
+
+
+
+
+ Service Provider ACS URL
+
+
{app.spAcsUrl}
+
+ Service Provider Entity ID
+
+
{app.spEntityId}
+
+
+
+
+
+
+ Start SAML login
+
+ Click the button below to do the equivalent of clicking on an
+ app/tile in Okta, Google Workspace, Microsoft Entra, etc.
+
+
+
+
+
+ Sign on
+
+
+
+ This will perform a SAML IDP-initiated flow, meaning you'll be
+ redirected to the Service Provider ACS URL, which you've configured
+ as {app.spAcsUrl} .
+
+
+
+
+
+
+ Identity Provider settings
+
+ These are settings that the identity provider (i.e. DummyIDP, in
+ this case) chooses.
+
+
+
+
+
+
+ Identity Provider Sign-on URL
+
+
+ https://dummyidp.com/apps/{app.id}/sso
+
+
+ Identity Provider Entity ID
+
+
+ https://dummyidp.com/apps/{app.id}
+
+
+ Certificate
+
+
+
+
+ {GLOBAL_NONSECURE_CERT}
+
+
+
+
+
+
+
+ );
+}