├── .eslintrc.json ├── app ├── globals.css ├── favicon.ico ├── applications │ ├── layout.tsx │ ├── tailwind.css │ └── page.tsx ├── layout.tsx ├── sign-in │ └── page.tsx ├── tailwind.css ├── api │ └── sso-apps │ │ └── route.ts └── page.tsx ├── postcss.config.js ├── public ├── logos │ ├── duo-logo.webp │ ├── ping-logo.png │ ├── azure-logo.png │ ├── descope-logo.png │ ├── google-logo.png │ └── okta-logo.webp ├── vercel.svg ├── next.svg └── placeholder-logo.svg ├── utils ├── data.ts ├── tailwind.ts └── assets.ts ├── .env.local.example ├── lib └── utils.ts ├── next.config.mjs ├── middleware.ts ├── components.json ├── components ├── layout │ ├── theme-switcher │ │ ├── theme-provider.tsx │ │ └── theme-switcher.tsx │ ├── providers.tsx │ ├── header.tsx │ └── user-nav.tsx ├── ui │ ├── label.tsx │ ├── progress.tsx │ ├── input.tsx │ ├── popover.tsx │ ├── avatar.tsx │ ├── scroll-area.tsx │ ├── button.tsx │ ├── card.tsx │ ├── dialog.tsx │ ├── command.tsx │ └── dropdown-menu.tsx ├── framework-rotation.tsx ├── project-not-found.tsx ├── tailwind.css └── icons.tsx ├── .gitignore ├── tsconfig.json ├── package.json ├── tailwind.config.ts └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #13131e; 3 | color: #ffffff; 4 | } 5 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/descope-sample-apps/descope-sso-applications/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/logos/duo-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/descope-sample-apps/descope-sso-applications/HEAD/public/logos/duo-logo.webp -------------------------------------------------------------------------------- /public/logos/ping-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/descope-sample-apps/descope-sso-applications/HEAD/public/logos/ping-logo.png -------------------------------------------------------------------------------- /public/logos/azure-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/descope-sample-apps/descope-sso-applications/HEAD/public/logos/azure-logo.png -------------------------------------------------------------------------------- /public/logos/descope-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/descope-sample-apps/descope-sso-applications/HEAD/public/logos/descope-logo.png -------------------------------------------------------------------------------- /public/logos/google-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/descope-sample-apps/descope-sso-applications/HEAD/public/logos/google-logo.png -------------------------------------------------------------------------------- /public/logos/okta-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/descope-sample-apps/descope-sso-applications/HEAD/public/logos/okta-logo.webp -------------------------------------------------------------------------------- /utils/data.ts: -------------------------------------------------------------------------------- 1 | export const frameworks = ["okta", "azure", "ping", "duo", "google"] as const; 2 | 3 | export type Framework = (typeof frameworks)[number]; 4 | -------------------------------------------------------------------------------- /.env.local.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_DESCOPE_PROJECT_ID="" 2 | NEXT_PUBLIC_DESCOPE_FLOW_ID="sign-up-or-in" 3 | 4 | DESCOPE_MANAGEMENT_KEY="" 5 | SIGN_IN_ROUTE="/sign-in" -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /utils/tailwind.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...classes: ClassValue[]) { 5 | return twMerge(clsx(...classes)); 6 | } 7 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: ["localhost", "cdn.builder.io"], 5 | dangerouslyAllowSVG: true, 6 | }, 7 | }; 8 | 9 | export default nextConfig; 10 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { authMiddleware } from "@descope/nextjs-sdk/server"; 2 | 3 | export default authMiddleware({ 4 | projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID, 5 | redirectUrl: "/", 6 | publicRoutes: ["/sign-in", "/"], 7 | }); 8 | 9 | export const config = { 10 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], 11 | }; 12 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": false 11 | }, 12 | "aliases": { 13 | "utils": "@/lib/utils", 14 | "components": "@/components" 15 | } 16 | } -------------------------------------------------------------------------------- /components/layout/theme-switcher/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 4 | import { type ThemeProviderProps } from "next-themes/dist/types"; 5 | 6 | export default function ThemeProvider({ 7 | children, 8 | ...props 9 | }: ThemeProviderProps) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /components/layout/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import ThemeProvider from "./theme-switcher/theme-provider"; 4 | export default function Providers({ children }: { children: React.ReactNode }) { 5 | return ( 6 | <> 7 | 8 | {children} 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /utils/assets.ts: -------------------------------------------------------------------------------- 1 | export const assets = { 2 | gradient: 3 | "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F266e05dba3864799b4715cf4bfd8aa2a", 4 | square: 5 | "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F8997f779f33b430bb22ca667d1b73ade", 6 | okta: "/logos/okta-logo.webp", 7 | ping: "/logos/ping-logo.png", 8 | azure: "/logos/azure-logo.png", 9 | duo: "/logos/duo-logo.webp", 10 | google: "/logos/google-logo.png", 11 | }; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /app/applications/layout.tsx: -------------------------------------------------------------------------------- 1 | import Header from "@/components/layout/header"; 2 | import type { Metadata } from "next"; 3 | 4 | export const metadata: Metadata = { 5 | title: "Applications", 6 | description: "Applications List", 7 | }; 8 | 9 | export default function DashboardLayout({ 10 | children, 11 | }: { 12 | children: React.ReactNode; 13 | }) { 14 | return ( 15 | <> 16 |
17 |
18 |
{children}
19 |
20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import { AuthProvider } from "@descope/nextjs-sdk"; 4 | import Providers from "@/components/layout/providers"; 5 | import "./globals.css"; 6 | 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | export const metadata: Metadata = { 10 | title: "Descope SSO Applications", 11 | description: "Tenant-specific easy-to-use SSO application tiles.", 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: Readonly<{ 17 | children: React.ReactNode; 18 | }>) { 19 | return ( 20 | 21 | 22 | 23 | {children} 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ProgressPrimitive from "@radix-ui/react-progress" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Progress = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, value, ...props }, ref) => ( 12 | 20 | 24 | 25 | )) 26 | Progress.displayName = ProgressPrimitive.Root.displayName 27 | 28 | export { Progress } 29 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /components/layout/header.tsx: -------------------------------------------------------------------------------- 1 | import ThemeSwitcher from "@/components/layout/theme-switcher/theme-switcher"; 2 | import { UserNav } from "./user-nav"; 3 | import Image from "next/image"; 4 | 5 | export default function Header() { 6 | return ( 7 |
8 | 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /components/framework-rotation.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { assets } from "@/utils/assets"; 3 | import { Framework, frameworks } from "@/utils/data"; 4 | import { cn } from "@/utils/tailwind"; 5 | 6 | type AssetKey = keyof typeof assets; 7 | 8 | export const FrameworkRotation = ({ 9 | currentFramework, 10 | }: { 11 | currentFramework: Framework; 12 | }) => { 13 | return ( 14 |
15 | {frameworks.map((name, index) => ( 16 | frameworks.indexOf(currentFramework as Framework) 24 | ? "opacity-0 -translate-y-2" 25 | : "opacity-0 translate-y-2" 26 | )} 27 | alt="Framework logo" 28 | width="80" 29 | height="80" 30 | /> 31 | ))} 32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /app/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Descope } from "@descope/nextjs-sdk"; 4 | import Image from "next/image"; 5 | import { assets } from "@/utils/assets"; 6 | 7 | export default function SignInPage() { 8 | return ( 9 |
10 |
15 | gradient-bg 23 |
30 | 31 |
32 |
33 | 38 |
39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "descope-sso-applications", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@descope/nextjs-sdk": "^0.0.5", 13 | "@radix-ui/react-avatar": "^1.0.4", 14 | "@radix-ui/react-dialog": "^1.0.5", 15 | "@radix-ui/react-dropdown-menu": "^2.0.6", 16 | "@radix-ui/react-icons": "^1.3.0", 17 | "@radix-ui/react-label": "^2.0.2", 18 | "@radix-ui/react-popover": "^1.0.7", 19 | "@radix-ui/react-progress": "^1.0.3", 20 | "@radix-ui/react-scroll-area": "^1.0.5", 21 | "@radix-ui/react-slot": "^1.0.2", 22 | "animejs": "^3.2.2", 23 | "class-variance-authority": "^0.7.0", 24 | "clsx": "^2.1.0", 25 | "cmdk": "^1.0.0", 26 | "lucide-react": "^0.344.0", 27 | "next": "^14.1.4", 28 | "next-themes": "^0.3.0", 29 | "react": "^18", 30 | "react-dom": "^18", 31 | "tailwind-merge": "^2.2.1", 32 | "tailwindcss-animate": "^1.0.7" 33 | }, 34 | "devDependencies": { 35 | "@types/node": "^20", 36 | "@types/react": "^18", 37 | "@types/react-dom": "^18", 38 | "autoprefixer": "^10.0.1", 39 | "eslint": "^8", 40 | "eslint-config-next": "14.1.2", 41 | "postcss": "^8", 42 | "tailwindcss": "^3.3.0", 43 | "typescript": "^5" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverTrigger, PopoverContent } 32 | -------------------------------------------------------------------------------- /components/layout/theme-switcher/theme-switcher.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { MoonIcon, SunIcon } from "@radix-ui/react-icons"; 3 | import { useTheme } from "next-themes"; 4 | 5 | import { Button } from "@/components/ui/button"; 6 | import { 7 | DropdownMenu, 8 | DropdownMenuContent, 9 | DropdownMenuItem, 10 | DropdownMenuTrigger, 11 | } from "@/components/ui/dropdown-menu"; 12 | type CompProps = {}; 13 | 14 | export default function ThemeSwitcher({}: CompProps) { 15 | const { setTheme } = useTheme(); 16 | return ( 17 | 18 | 19 | 24 | 25 | 26 | setTheme("light")}> 27 | Light 28 | 29 | setTheme("dark")}> 30 | Dark 31 | 32 | setTheme("system")}> 33 | System 34 | 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /components/project-not-found.tsx: -------------------------------------------------------------------------------- 1 | import { assets } from "@/utils/assets"; 2 | import { cn } from "@/utils/tailwind"; 3 | 4 | import "../app/tailwind.css"; 5 | 6 | export default function ProjectNotFound() { 7 | return ( 8 |
9 |
16 |
21 |
22 |
23 |

24 | Please provide a Project ID as a parameter in the URL. 25 |

26 |

27 | Example URL: 28 | http://localhost:3000?project=PROJECT_ID&flow=sign-up-or-in 29 |

30 |

31 | You can also deploy this app in{" "} 32 | 38 | Vercel 39 | 40 | , and use environment variables instead. 41 |

42 |
43 |
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )) 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )) 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 47 | 48 | export { ScrollArea, ScrollBar } 49 | -------------------------------------------------------------------------------- /app/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | background: #13131e; 7 | color: #ffffff; 8 | } 9 | 10 | @layer base { 11 | :root { 12 | --background: #13131e; 13 | --foreground: 222.2 84% 4.9%; 14 | 15 | --card: 0 0% 100%; 16 | --card-foreground: 222.2 84% 4.9%; 17 | 18 | --popover: 0 0% 100%; 19 | --popover-foreground: 222.2 84% 4.9%; 20 | 21 | --primary: 222.2 47.4% 11.2%; 22 | --primary-foreground: 210 40% 98%; 23 | 24 | --secondary: 210 40% 96.1%; 25 | --secondary-foreground: 222.2 47.4% 11.2%; 26 | 27 | --muted: 210 40% 96.1%; 28 | --muted-foreground: 215.4 16.3% 46.9%; 29 | 30 | --accent: 210 40% 96.1%; 31 | --accent-foreground: 222.2 47.4% 11.2%; 32 | 33 | --destructive: 0 84.2% 60.2%; 34 | --destructive-foreground: 210 40% 98%; 35 | 36 | --border: 214.3 31.8% 91.4%; 37 | --input: 214.3 31.8% 91.4%; 38 | --ring: 222.2 84% 4.9%; 39 | 40 | --radius: 0.5rem; 41 | } 42 | 43 | .dark { 44 | --background: #13131e; 45 | --foreground: #fff; 46 | 47 | --card: 222.2 84% 4.9%; 48 | --card-foreground: 210 40% 98%; 49 | 50 | --popover: 222.2 84% 4.9%; 51 | --popover-foreground: 210 40% 98%; 52 | 53 | --primary: 210 40% 98%; 54 | --primary-foreground: 222.2 47.4% 11.2%; 55 | 56 | --secondary: 217.2 32.6% 17.5%; 57 | --secondary-foreground: 210 40% 98%; 58 | 59 | --muted: 217.2 32.6% 17.5%; 60 | --muted-foreground: 215 20.2% 65.1%; 61 | 62 | --accent: 217.2 32.6% 17.5%; 63 | --accent-foreground: 210 40% 98%; 64 | 65 | --destructive: 0 62.8% 30.6%; 66 | --destructive-foreground: 210 40% 98%; 67 | 68 | --border: 217.2 32.6% 17.5%; 69 | --input: 217.2 32.6% 17.5%; 70 | --ring: 212.7 26.8% 83.9%; 71 | } 72 | } 73 | 74 | @layer base { 75 | * { 76 | @apply border-border; 77 | } 78 | body { 79 | @apply bg-background text-foreground; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /components/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | background: #13131e; 7 | color: #ffffff; 8 | } 9 | 10 | @layer base { 11 | :root { 12 | --background: #13131e; 13 | --foreground: 222.2 84% 4.9%; 14 | 15 | --card: 0 0% 100%; 16 | --card-foreground: 222.2 84% 4.9%; 17 | 18 | --popover: 0 0% 100%; 19 | --popover-foreground: 222.2 84% 4.9%; 20 | 21 | --primary: 222.2 47.4% 11.2%; 22 | --primary-foreground: 210 40% 98%; 23 | 24 | --secondary: 210 40% 96.1%; 25 | --secondary-foreground: 222.2 47.4% 11.2%; 26 | 27 | --muted: 210 40% 96.1%; 28 | --muted-foreground: 215.4 16.3% 46.9%; 29 | 30 | --accent: 210 40% 96.1%; 31 | --accent-foreground: 222.2 47.4% 11.2%; 32 | 33 | --destructive: 0 84.2% 60.2%; 34 | --destructive-foreground: 210 40% 98%; 35 | 36 | --border: 214.3 31.8% 91.4%; 37 | --input: 214.3 31.8% 91.4%; 38 | --ring: 222.2 84% 4.9%; 39 | 40 | --radius: 0.5rem; 41 | } 42 | 43 | .dark { 44 | --background: #13131e; 45 | --foreground: #fff; 46 | 47 | --card: 222.2 84% 4.9%; 48 | --card-foreground: 210 40% 98%; 49 | 50 | --popover: 222.2 84% 4.9%; 51 | --popover-foreground: 210 40% 98%; 52 | 53 | --primary: 210 40% 98%; 54 | --primary-foreground: 222.2 47.4% 11.2%; 55 | 56 | --secondary: 217.2 32.6% 17.5%; 57 | --secondary-foreground: 210 40% 98%; 58 | 59 | --muted: 217.2 32.6% 17.5%; 60 | --muted-foreground: 215 20.2% 65.1%; 61 | 62 | --accent: 217.2 32.6% 17.5%; 63 | --accent-foreground: 210 40% 98%; 64 | 65 | --destructive: 0 62.8% 30.6%; 66 | --destructive-foreground: 210 40% 98%; 67 | 68 | --border: 217.2 32.6% 17.5%; 69 | --input: 217.2 32.6% 17.5%; 70 | --ring: 212.7 26.8% 83.9%; 71 | } 72 | } 73 | 74 | @layer base { 75 | * { 76 | @apply border-border; 77 | } 78 | body { 79 | @apply bg-background text-foreground; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/applications/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 240 10% 3.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 240 10% 3.9%; 15 | 16 | --primary: 240 5.9% 10%; 17 | --primary-foreground: 0 0% 98%; 18 | 19 | --secondary: 240 4.8% 95.9%; 20 | --secondary-foreground: 240 5.9% 10%; 21 | 22 | --muted: 240 4.8% 95.9%; 23 | --muted-foreground: 240 3.8% 46.1%; 24 | 25 | --accent: 240 4.8% 95.9%; 26 | --accent-foreground: 240 5.9% 10%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 0 0% 98%; 30 | 31 | --border: 240 5.9% 90%; 32 | --input: 240 5.9% 90%; 33 | --ring: 240 10% 3.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 240 10% 3.9%; 40 | --foreground: 0 0% 98%; 41 | 42 | --card: 240 10% 3.9%; 43 | --card-foreground: 0 0% 98%; 44 | 45 | --popover: 240 10% 3.9%; 46 | --popover-foreground: 0 0% 98%; 47 | 48 | --primary: 0 0% 98%; 49 | --primary-foreground: 240 5.9% 10%; 50 | 51 | --secondary: 240 3.7% 15.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | 54 | --muted: 240 3.7% 15.9%; 55 | --muted-foreground: 240 5% 64.9%; 56 | 57 | --accent: 240 3.7% 15.9%; 58 | --accent-foreground: 0 0% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | 63 | --border: 240 3.7% 15.9%; 64 | --input: 240 3.7% 15.9%; 65 | --ring: 240 4.9% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground box-border; 75 | } 76 | } 77 | 78 | @layer utilities { 79 | .min-h-screen { 80 | min-height: 100vh; /* Fallback */ 81 | min-height: 100dvh; 82 | } 83 | .h-screen { 84 | height: 100vh; /* Fallback */ 85 | height: 100dvh; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-gray-950 dark:focus-visible:ring-gray-300", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | "./pages/**/*.{ts,tsx}", 6 | "./components/**/*.{ts,tsx}", 7 | "./constants/**/*.{ts,tsx}", 8 | "./app/**/*.{ts,tsx}", 9 | "./src/**/*.{ts,tsx}", 10 | ], 11 | theme: { 12 | container: { 13 | center: true, 14 | padding: "2rem", 15 | screens: { 16 | "2xl": "1400px", 17 | }, 18 | }, 19 | extend: { 20 | colors: { 21 | border: "hsl(var(--border))", 22 | input: "hsl(var(--input))", 23 | ring: "hsl(var(--ring))", 24 | background: "hsl(var(--background))", 25 | foreground: "hsl(var(--foreground))", 26 | primary: { 27 | DEFAULT: "hsl(var(--primary))", 28 | foreground: "hsl(var(--primary-foreground))", 29 | }, 30 | secondary: { 31 | DEFAULT: "hsl(var(--secondary))", 32 | foreground: "hsl(var(--secondary-foreground))", 33 | }, 34 | destructive: { 35 | DEFAULT: "hsl(var(--destructive))", 36 | foreground: "hsl(var(--destructive-foreground))", 37 | }, 38 | muted: { 39 | DEFAULT: "hsl(var(--muted))", 40 | foreground: "hsl(var(--muted-foreground))", 41 | }, 42 | accent: { 43 | DEFAULT: "hsl(var(--accent))", 44 | foreground: "hsl(var(--accent-foreground))", 45 | }, 46 | popover: { 47 | DEFAULT: "hsl(var(--popover))", 48 | foreground: "hsl(var(--popover-foreground))", 49 | }, 50 | card: { 51 | DEFAULT: "hsl(var(--card))", 52 | foreground: "hsl(var(--card-foreground))", 53 | }, 54 | }, 55 | borderRadius: { 56 | lg: "var(--radius)", 57 | md: "calc(var(--radius) - 2px)", 58 | sm: "calc(var(--radius) - 4px)", 59 | }, 60 | keyframes: { 61 | "accordion-down": { 62 | from: { height: 0 }, 63 | to: { height: "var(--radix-accordion-content-height)" }, 64 | }, 65 | "accordion-up": { 66 | from: { height: "var(--radix-accordion-content-height)" }, 67 | to: { height: 0 }, 68 | }, 69 | }, 70 | animation: { 71 | "accordion-down": "accordion-down 0.2s ease-out", 72 | "accordion-up": "accordion-up 0.2s ease-out", 73 | }, 74 | }, 75 | }, 76 | plugins: [require("tailwindcss-animate")], 77 | }; 78 | -------------------------------------------------------------------------------- /components/icons.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AlertTriangle, 3 | ArrowRight, 4 | Check, 5 | ChevronLeft, 6 | ChevronRight, 7 | CircuitBoardIcon, 8 | Command, 9 | CreditCard, 10 | File, 11 | FileText, 12 | HelpCircle, 13 | Image, 14 | Laptop, 15 | LayoutDashboardIcon, 16 | Loader2, 17 | LogIn, 18 | LucideIcon, 19 | LucideProps, 20 | Moon, 21 | MoreVertical, 22 | Pizza, 23 | Plus, 24 | Settings, 25 | SunMedium, 26 | Trash, 27 | User, 28 | User2Icon, 29 | UserX2Icon, 30 | X, 31 | } from "lucide-react"; 32 | 33 | export type Icon = LucideIcon; 34 | 35 | export const Icons = { 36 | dashboard: LayoutDashboardIcon, 37 | logo: Command, 38 | login: LogIn, 39 | close: X, 40 | profile: User2Icon, 41 | spinner: Loader2, 42 | kanban: CircuitBoardIcon, 43 | chevronLeft: ChevronLeft, 44 | chevronRight: ChevronRight, 45 | trash: Trash, 46 | employee: UserX2Icon, 47 | post: FileText, 48 | page: File, 49 | media: Image, 50 | settings: Settings, 51 | billing: CreditCard, 52 | ellipsis: MoreVertical, 53 | add: Plus, 54 | warning: AlertTriangle, 55 | user: User, 56 | arrowRight: ArrowRight, 57 | help: HelpCircle, 58 | pizza: Pizza, 59 | sun: SunMedium, 60 | moon: Moon, 61 | laptop: Laptop, 62 | gitHub: ({ ...props }: LucideProps) => ( 63 | 78 | ), 79 | check: Check, 80 | }; 81 | -------------------------------------------------------------------------------- /components/layout/user-nav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | DropdownMenu, 6 | DropdownMenuContent, 7 | DropdownMenuGroup, 8 | DropdownMenuItem, 9 | DropdownMenuLabel, 10 | DropdownMenuSeparator, 11 | DropdownMenuShortcut, 12 | DropdownMenuTrigger, 13 | } from "@/components/ui/dropdown-menu"; 14 | import { useUser, useDescope } from "@descope/nextjs-sdk/client"; 15 | import { useRouter } from "next/navigation"; 16 | import { useCallback } from "react"; 17 | 18 | export function UserNav() { 19 | const { user } = useUser(); 20 | const sdk = useDescope(); 21 | 22 | const router = useRouter(); 23 | 24 | const handleLogout = useCallback(() => { 25 | sdk.logout(); 26 | router.push("/"); 27 | }, [sdk, router]); 28 | 29 | if (user) { 30 | // TODO: Add a settings page for admins to restrict and allow access to applications 31 | // const isAdmin = user?.customAttributes?.descoper; 32 | 33 | return ( 34 | 35 | 36 | 42 | 43 | 44 | 45 |
46 |

{user?.name}

47 |

48 | {user?.email} 49 |

50 |
51 |
52 | {/* {isAdmin && ( 53 | { 55 | router.push("/settings"); 56 | }} 57 | > 58 | Settings 59 | ⇧⌘S 60 | 61 | )} */} 62 | 63 | 64 | 69 | Deploy to Vercel 70 | 71 | ⌘D 72 | 73 | 74 | Sign Out 75 | ⇧⌘Q 76 | 77 |
78 |
79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /public/placeholder-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Screenshot 2024-03-30 at 7 35 13 PM 2 | 3 | # Descope SSO Applications 🌐 4 | 5 | Descope SSO Applications is designed to simplify the authentication process for employees and other users utilizing [Dynamic Federation](https://www.descope.com/use-cases/identity-federation) and [IdP-initiated SSO](https://www.descope.com/blog/post/idp-vs-sp-sso?_gl=1*m72a5o*_gcl_aw*R0NMLjE3MTAxODM1MjAuQ2p3S0NBancxN3F2QmhCckVpd0ExclU5dzV0dUFuNm95MG9hRWtVOXpMQkdiVU4ySWI0b0dwS2tHT1o5REl2SGRHbGN4ZTZzaXdMNk9Sb0NBV01RQXZEX0J3RQ..*_gcl_au*MTExNTU5OTU2NS4xNzA2MTI5NTQ5). By acting as a centralized SSO provider, it offers seamless, frictionless sign-in capabilities across multiple SAML SSO-based applications. 6 | 7 | ## Features / Use Cases ✨ 8 | 9 | 1. **Streamlined Access**: Provide a unified interface for both internal organization and external users to access SAML applications without any friction, leveraging existing login sessions with other SSO providers. 🔑 10 | 2. **Dynamic Federation**: Authentication requests can be re-routed to any of the SAML SSO providers you've previously configured, allowing you to add multiple IdPs to applications that may only support one IdP, with Descope in the middle guiding your users through to the right provider. 🔄 11 | 3. **Works with Other Auth Providers**: With IdP-initiated SSO, you can add Descope as a layer on top of any current implementation you have (with Auth0, Cognito, Firebase, etc.) to provide you and your users with seamless SAML-based SSO to all of your apps, without changing any app-level SAML configurations. 🤝 12 | 13 | ## Getting Started 🚀 14 | 15 | ### Deployment to Vercel 🚀 16 | 17 | This application will need to be deployed to Vercel or some other hosting service, in order to work with your application. Since this application requires a backend with your specific Descope Management Key, it's important that you properly deploy this app with secured environment secrets. 18 | 19 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdescope-sample-apps%2Fdescope-sso-applications&env=NEXT_PUBLIC_DESCOPE_PROJECT_ID,DESCOPE_MANAGEMENT_KEY) 20 | 21 | ### Local Setup Instructions 🛠️ 22 | 23 | 1. **Clone the Repository** 24 | 25 | ```bash 26 | git clone https://github.com/descope-sample-apps/descope-sso-applications 27 | cd descope-sso-applications 28 | ``` 29 | 30 | 2. **Install Dependencies** 31 | 32 | ```bash 33 | yarn install 34 | ``` 35 | 36 | 3. **Configure Environment Variables** 37 | 38 | Create a `.env.local` file at the root of your project and define the following variables: 39 | 40 | ```env 41 | NEXT_PUBLIC_DESCOPE_PROJECT_ID=YOUR_DESCOPE_PROJECT_ID 42 | DESCOPE_MANAGEMENT_KEY=YOUR_DESCOPE_MANAGEMENT_KEY 43 | NEXT_PUBLIC_DESCOPE_FLOW_ID="sign-up-or-in" // Optional, if you want to use a different flow 44 | ``` 45 | 46 | 4. **Run the Application Locally** 47 | 48 | ```bash 49 | yarn dev 50 | ``` 51 | 52 | Visit `http://localhost:3000` in your browser to view the app. 🌐 53 | 54 | ## Contributing 🤝 55 | 56 | Contributions are welcome! Please feel free to submit a pull request or open an issue if you have feedback, suggestions, or contributions. ❤️ 57 | 58 | ## License 📄 59 | 60 | This project is licensed under the MIT License - see the LICENSE file for details. 🔏 61 | -------------------------------------------------------------------------------- /app/api/sso-apps/route.ts: -------------------------------------------------------------------------------- 1 | import { createSdk, session } from "@descope/nextjs-sdk/server"; 2 | 3 | interface UserResponse { 4 | ssoAppIds?: string[]; 5 | } 6 | 7 | interface SSOApplication { 8 | appType: string; 9 | description: string; 10 | enabled: boolean; 11 | id: string; 12 | logo: string; 13 | name: string; 14 | samlSettings: { 15 | idpSsoUrl: string; 16 | }; 17 | } 18 | 19 | const sdk = createSdk({ 20 | projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID, 21 | managementKey: process.env.DESCOPE_MANAGEMENT_KEY, 22 | }); 23 | 24 | export async function GET(req: Request) { 25 | const currentSession = session(); 26 | if (!currentSession || !currentSession.token.sub) { 27 | return new Response("Unauthorized", { status: 401 }); 28 | } 29 | 30 | if (!sdk.management) { 31 | console.error( 32 | "Management SDK is not available, Make sure you have the DESCOPE_MANAGEMENT_KEY environment variable set" 33 | ); 34 | return new Response("Internal error", { status: 500 }); 35 | } 36 | 37 | const res = await sdk.management.user.loadByUserId(currentSession.token.sub); 38 | 39 | if (!res.ok) { 40 | console.error("Failed to load user", res.error); 41 | return new Response("Not found", { status: 404 }); 42 | } 43 | 44 | const userData = res.data as UserResponse; 45 | let ssoAppsPromise; 46 | let baseURL = "api.descope.com" 47 | if (process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID && process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID.length >= 32) { 48 | const localURL = process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID.substring(1, 5) 49 | baseURL = [baseURL.slice(0, 4), localURL, ".", baseURL.slice(4)].join('') 50 | } 51 | if (!userData.ssoAppIds || userData.ssoAppIds.length === 0) { 52 | // Fetch all applications if no specific apps are assigned to the user 53 | const allAppsUrl = `https://${baseURL}/v1/mgmt/sso/idp/apps/load`; 54 | ssoAppsPromise = fetch(allAppsUrl, { 55 | method: "GET", 56 | headers: { 57 | Authorization: `Bearer ${process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID}:${process.env.DESCOPE_MANAGEMENT_KEY}`, 58 | }, 59 | }).then((res) => (res.ok ? res.json().then((data) => data.apps) : [])); 60 | } else { 61 | // Fetch specific applications assigned to the user 62 | ssoAppsPromise = Promise.all( 63 | userData.ssoAppIds.map(async (appId) => { 64 | const appUrl = `https://${baseURL}/v1/mgmt/sso/idp/app/load?id=${appId}`; 65 | const appRes = await fetch(appUrl, { 66 | method: "GET", 67 | headers: { 68 | Authorization: `Bearer ${process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID}:${process.env.DESCOPE_MANAGEMENT_KEY}`, 69 | }, 70 | }); 71 | return appRes.ok ? appRes.json() : null; 72 | }) 73 | ); 74 | } 75 | 76 | const ssoApps = await ssoAppsPromise; 77 | 78 | // Filter for SAML apps and map to required fields 79 | const validSamlApps = (Array.isArray(ssoApps) ? ssoApps : [ssoApps]) 80 | .filter((app) => app !== null && app.appType === "saml") 81 | .map((app: SSOApplication) => ({ 82 | description: app.description, 83 | enabled: app.enabled, 84 | name: app.name, 85 | id: app.id, 86 | logo: app.logo, 87 | samlSettings: { 88 | // Create the IdP initiated SSO URL since it isn't returned by default by the Descope API 89 | idpSsoUrl: app.samlSettings.idpSsoUrl.replace("/sso", "/initiate"), 90 | }, 91 | })); 92 | 93 | return new Response(JSON.stringify(validSamlApps), { status: 200 }); 94 | } 95 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /components/ui/command.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { type DialogProps } from "@radix-ui/react-dialog" 5 | import { Command as CommandPrimitive } from "cmdk" 6 | import { Search } from "lucide-react" 7 | 8 | import { cn } from "@/lib/utils" 9 | import { Dialog, DialogContent } from "@/components/ui/dialog" 10 | 11 | const Command = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 23 | )) 24 | Command.displayName = CommandPrimitive.displayName 25 | 26 | interface CommandDialogProps extends DialogProps {} 27 | 28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => { 29 | return ( 30 | 31 | 32 | 33 | {children} 34 | 35 | 36 | 37 | ) 38 | } 39 | 40 | const CommandInput = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 |
45 | 46 | 54 |
55 | )) 56 | 57 | CommandInput.displayName = CommandPrimitive.Input.displayName 58 | 59 | const CommandList = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, ...props }, ref) => ( 63 | 68 | )) 69 | 70 | CommandList.displayName = CommandPrimitive.List.displayName 71 | 72 | const CommandEmpty = React.forwardRef< 73 | React.ElementRef, 74 | React.ComponentPropsWithoutRef 75 | >((props, ref) => ( 76 | 81 | )) 82 | 83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName 84 | 85 | const CommandGroup = React.forwardRef< 86 | React.ElementRef, 87 | React.ComponentPropsWithoutRef 88 | >(({ className, ...props }, ref) => ( 89 | 97 | )) 98 | 99 | CommandGroup.displayName = CommandPrimitive.Group.displayName 100 | 101 | const CommandSeparator = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )) 111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName 112 | 113 | const CommandItem = React.forwardRef< 114 | React.ElementRef, 115 | React.ComponentPropsWithoutRef 116 | >(({ className, ...props }, ref) => ( 117 | 125 | )) 126 | 127 | CommandItem.displayName = CommandPrimitive.Item.displayName 128 | 129 | const CommandShortcut = ({ 130 | className, 131 | ...props 132 | }: React.HTMLAttributes) => { 133 | return ( 134 | 141 | ) 142 | } 143 | CommandShortcut.displayName = "CommandShortcut" 144 | 145 | export { 146 | Command, 147 | CommandDialog, 148 | CommandInput, 149 | CommandList, 150 | CommandEmpty, 151 | CommandGroup, 152 | CommandItem, 153 | CommandShortcut, 154 | CommandSeparator, 155 | } 156 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { useState, useEffect } from "react"; 5 | import { useSession } from "@descope/nextjs-sdk/client"; 6 | import Image from "next/image"; 7 | import { assets } from "@/utils/assets"; 8 | import { type Framework, frameworks } from "@/utils/data"; 9 | import { cn } from "@/utils/tailwind"; 10 | import { FrameworkRotation } from "@/components/framework-rotation"; 11 | import { Button } from "@/components/ui/button"; 12 | 13 | import "./tailwind.css"; 14 | 15 | export default function Home() { 16 | const [currentFramework, setCurrentFramework] = useState( 17 | frameworks[0] 18 | ); 19 | 20 | const [projectId, setProjectId] = useState(null); 21 | const [showBackground, setShowBackground] = useState(false); 22 | const { isAuthenticated } = useSession(); 23 | 24 | useEffect(() => { 25 | let currentIndex = 0; 26 | const rotateFrameworks = () => { 27 | setCurrentFramework(frameworks[currentIndex]); 28 | currentIndex = (currentIndex + 1) % frameworks.length; 29 | }; 30 | const intervalId = setInterval(rotateFrameworks, 2000); 31 | return () => clearInterval(intervalId); 32 | }, []); 33 | 34 | useEffect(() => { 35 | setShowBackground(true); 36 | }, []); 37 | 38 | return ( 39 |
40 |
52 | gradient-bg 60 |
67 |
73 | 74 |
75 |
76 |

77 | 86 | Easy-to-use 87 | {" "} 88 | SSO Apps with{" "} 89 | 90 |

91 | 92 |

93 | Powered by 94 | 103 | 104 | Descope 105 | 106 | 107 |

108 | {!isAuthenticated && ( 109 |
110 | 111 | 130 | 131 |
132 | )} 133 | {isAuthenticated && ( 134 |
135 | 136 | 155 | 156 |
157 | )} 158 |
159 |
160 |
161 | ); 162 | } 163 | -------------------------------------------------------------------------------- /app/applications/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Card, CardContent } from "@/components/ui/card"; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogDescription, 8 | DialogFooter, 9 | DialogHeader, 10 | DialogClose, 11 | DialogTrigger, 12 | DialogTitle, 13 | } from "@/components/ui/dialog"; 14 | import { Button } from "@/components/ui/button"; 15 | import { Progress } from "@/components/ui/progress"; 16 | import Image from "next/image"; 17 | import Link from "next/link"; 18 | import React, { useEffect, useState } from "react"; 19 | import { useSession, useUser } from "@descope/nextjs-sdk/client"; 20 | 21 | import "./tailwind.css"; 22 | 23 | interface SSOApplication { 24 | appType: string; 25 | description: string; 26 | enabled: boolean; 27 | id: string; 28 | logo: string; 29 | name: string; 30 | samlSettings: { 31 | idpSsoUrl: string; 32 | }; 33 | } 34 | 35 | export default function AppPage() { 36 | const [apps, setApps] = useState([]); 37 | const [isLoading, setIsLoading] = useState(true); 38 | const [progress, setProgress] = React.useState(13); 39 | 40 | const { isAuthenticated } = useSession(); 41 | const { user, isUserLoading } = useUser(); 42 | 43 | useEffect(() => { 44 | async function fetchUserInfo() { 45 | setIsLoading(true); 46 | try { 47 | const response = await fetch("/api/sso-apps"); 48 | const data = await response.json(); 49 | setApps(data); 50 | } catch (error) { 51 | console.error("Failed to fetch user info:", error); 52 | } finally { 53 | setIsLoading(false); 54 | } 55 | } 56 | 57 | fetchUserInfo(); 58 | }, []); 59 | 60 | useEffect(() => { 61 | const fakeFetch = setTimeout(() => { 62 | setProgress(66); 63 | setTimeout(() => { 64 | setProgress(100); 65 | setIsLoading(false); 66 | }, 1000); 67 | }, 500); 68 | 69 | return () => clearTimeout(fakeFetch); 70 | }, []); 71 | 72 | const isAdmin = user?.customAttributes?.descoper; 73 | 74 | return ( 75 | <> 76 | {isLoading && ( 77 |
78 |
79 | 80 |
81 |
82 | )} 83 | {!isLoading && !isUserLoading && isAuthenticated && ( 84 |
85 |
86 | {apps.length > 0 ? ( 87 |
88 | {apps.map((app) => ( 89 | 93 | 94 | 102 |
103 | {app.logo ? ( 104 |
105 | {app.name} 112 |
113 | ) : ( 114 |
115 | {app.name.charAt(0)} 116 |
117 | )} 118 |

119 | {app.name} 120 |

121 |
122 | 123 |
124 |
125 | ))} 126 | {isAdmin && ( 127 | 128 | 129 | 130 | 131 | 132 |
133 |
134 | + 135 |
136 |

137 | Create New App 138 |

139 |
140 |
141 |
142 | 143 | 144 | Create New App 145 | 146 | 147 | You can create a new application in the Descope 148 | Console, under{" "} 149 | 153 | Applications 154 | 155 | . 156 | 157 | 158 | 159 | 162 | 163 | 164 | 165 | 166 |
167 |
168 | )} 169 |
170 | ) : ( 171 |
172 |
173 |

174 | You have no apps configured. 175 |

176 |

177 | Please contact your system administrator if you think this 178 | is an error. 179 |

180 |
181 |
182 | )} 183 |
184 |
185 | )} 186 | 187 | ); 188 | } 189 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 5 | import { Check, ChevronRight, Circle } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )) 40 | DropdownMenuSubTrigger.displayName = 41 | DropdownMenuPrimitive.SubTrigger.displayName 42 | 43 | const DropdownMenuSubContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )) 56 | DropdownMenuSubContent.displayName = 57 | DropdownMenuPrimitive.SubContent.displayName 58 | 59 | const DropdownMenuContent = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, sideOffset = 4, ...props }, ref) => ( 63 | 64 | 73 | 74 | )) 75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 76 | 77 | const DropdownMenuItem = React.forwardRef< 78 | React.ElementRef, 79 | React.ComponentPropsWithoutRef & { 80 | inset?: boolean 81 | } 82 | >(({ className, inset, ...props }, ref) => ( 83 | 92 | )) 93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 94 | 95 | const DropdownMenuCheckboxItem = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, children, checked, ...props }, ref) => ( 99 | 108 | 109 | 110 | 111 | 112 | 113 | {children} 114 | 115 | )) 116 | DropdownMenuCheckboxItem.displayName = 117 | DropdownMenuPrimitive.CheckboxItem.displayName 118 | 119 | const DropdownMenuRadioItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )) 139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 140 | 141 | const DropdownMenuLabel = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef & { 144 | inset?: boolean 145 | } 146 | >(({ className, inset, ...props }, ref) => ( 147 | 156 | )) 157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 158 | 159 | const DropdownMenuSeparator = React.forwardRef< 160 | React.ElementRef, 161 | React.ComponentPropsWithoutRef 162 | >(({ className, ...props }, ref) => ( 163 | 168 | )) 169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 170 | 171 | const DropdownMenuShortcut = ({ 172 | className, 173 | ...props 174 | }: React.HTMLAttributes) => { 175 | return ( 176 | 180 | ) 181 | } 182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 183 | 184 | export { 185 | DropdownMenu, 186 | DropdownMenuTrigger, 187 | DropdownMenuContent, 188 | DropdownMenuItem, 189 | DropdownMenuCheckboxItem, 190 | DropdownMenuRadioItem, 191 | DropdownMenuLabel, 192 | DropdownMenuSeparator, 193 | DropdownMenuShortcut, 194 | DropdownMenuGroup, 195 | DropdownMenuPortal, 196 | DropdownMenuSub, 197 | DropdownMenuSubContent, 198 | DropdownMenuSubTrigger, 199 | DropdownMenuRadioGroup, 200 | } 201 | --------------------------------------------------------------------------------