spike: create assertions
This commit is contained in:
commit
b049eb8fa5
30
.eslintrc.json
Normal file
30
.eslintrc.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react-hooks/recommended"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint",
|
||||||
|
"react"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"react/no-unescaped-entities": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/node_modules
|
||||||
|
/public
|
||||||
|
!/public/index.html
|
||||||
2
.npmrc
Normal file
2
.npmrc
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
@fortawesome:registry=https://npm.fontawesome.com/
|
||||||
|
//npm.fontawesome.com/:_authToken=${FONTAWESOME_NPM_AUTH_TOKEN}
|
||||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
/public
|
||||||
1
.prettierrc.json
Normal file
1
.prettierrc.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
29
build.mjs
Normal file
29
build.mjs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import * as esbuild from "esbuild";
|
||||||
|
|
||||||
|
const APP_BUILD_IS_DEV = process.env.APP_BUILD_IS_DEV === "1";
|
||||||
|
|
||||||
|
const define = {
|
||||||
|
global: 'window',
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(process.env)
|
||||||
|
.filter(([k, _v]) => k.startsWith("APP_"))
|
||||||
|
.map(([k, v]) => [`process.env.${k}`, JSON.stringify(v)]),
|
||||||
|
)}
|
||||||
|
|
||||||
|
const context = await esbuild.context({
|
||||||
|
entryPoints: ["./src"],
|
||||||
|
outfile: "./public/index.js",
|
||||||
|
minify: !APP_BUILD_IS_DEV,
|
||||||
|
bundle: true,
|
||||||
|
sourcemap: true,
|
||||||
|
target: ["chrome58", "firefox57", "safari11", "edge18"],
|
||||||
|
define,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (APP_BUILD_IS_DEV) {
|
||||||
|
console.log("watching");
|
||||||
|
await context.watch();
|
||||||
|
} else {
|
||||||
|
await context.rebuild();
|
||||||
|
await context.dispose();
|
||||||
|
}
|
||||||
17
components.json
Normal file
17
components.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "slate",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils"
|
||||||
|
}
|
||||||
|
}
|
||||||
19
convertkey.js
Normal file
19
convertkey.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const jose = require("node-jose");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
// Function to convert RSA private key to JWK
|
||||||
|
async function convertToJWK() {
|
||||||
|
// Read the PEM formatted private key
|
||||||
|
const keyPEM = fs.readFileSync("dummyidp.key");
|
||||||
|
|
||||||
|
// Create a keystore
|
||||||
|
const keystore = jose.JWK.createKeyStore();
|
||||||
|
|
||||||
|
// Add the RSA private key to the keystore
|
||||||
|
const jwk = await keystore.add(keyPEM, "pem");
|
||||||
|
|
||||||
|
// Output the JWK
|
||||||
|
console.log(JSON.stringify(jwk.toJSON(true), null, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
convertToJWK().catch(console.error);
|
||||||
19
dummyidp.crt
Normal file
19
dummyidp.crt
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
-----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-----
|
||||||
28
dummyidp.key
Normal file
28
dummyidp.key
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCoZoEJlm/Dbxoc
|
||||||
|
/edl2OEpSZaViCu9kYpYTmUvX6w2IallSR7Bh/We7Es6zP9PmLwshFrmVfYKTHiG
|
||||||
|
h2cRDw6lt0SeyhudX5/7P6hb8HLX/gmQ48nxg82LPlyIH+6eelOPBJmHlqQf2lFu
|
||||||
|
uIEm5z6q7VplRQYrDnyMeWmNzbm7U7J/oNF23V9C+e/jYErz0B14fxj1wO8QiFpu
|
||||||
|
eBuZKo6M8WyhBQNA0t8m+qB0UFh6iADP+5iTHVFWTSFdau3NiSIAJdt7g1qvtikx
|
||||||
|
NkeZYj/yo748CqJ1SrU6hql+notua7Exzmg6QR/y3UCtrcYLJu0e/iE4Vjb1CNE7
|
||||||
|
ba5sAasjAgMBAAECggEAEggj0gx/PCyF3cvcPr4Z4gtkqe9SS7KtXyZJ1GhIruUs
|
||||||
|
19EcD3oI9XL03T99KR9AKv4jI53ZwiGNGE6gXSXBGkKFAQHAMjo+ja8zzmBxU6p6
|
||||||
|
iL6zbX6BAGN1kgflS6fqkZpa/DdHrLd6V8I+5hUF01SmBMj+z5Z2BK6tfEcml6XC
|
||||||
|
VFBzd0QqZuACrdO9ScZvL1cd4DCHJJNBarCVgC9akxOi9THtgD72EYJQyVcsLbzK
|
||||||
|
qoxnT9JW6onCBBKtUaP8fEzp9WMK6APJCkYK5I+1lX8tSf48+lzgBrn3cb9Zo5DY
|
||||||
|
cGo0bJgVwzI4w+HS/etiOOrURmbbX1x9W/rGBhS8tQKBgQDscf1AyNmZoPRS8O12
|
||||||
|
tfVoGCjxkJZhKEx/HO3WAjLqNpKCZzIaYwl4H2z6cIainLwjJ3iOWCCaCdY/gKph
|
||||||
|
uXp+a9n7hQse5zQMZpHU/rOGwXh8jrMLFabqKPIhczvlDrtxyiQtB5HMg0qg6aQR
|
||||||
|
DaGAUqGJoo3hNV9qe58QJ++rNQKBgQC2U98Sipsapgm3EqU0PSKFcxONJUZNvBQF
|
||||||
|
RdR3NhTHuco4ZBJ4IthdumbgogERhrgY2m65vy+dkPxz4W7Mhv7IofrqcJr9Darz
|
||||||
|
OF8zvrikBqj0/U5KNteQDuYvqc7ovACTs4U1rbbPQfkZNGTNfwMmIAb3TP/ruqtA
|
||||||
|
+Z+YV9nv9wKBgQC4F6BI2pihhrH0CeW5cb6ax4TJX/vVtZyps4px/9BIjyjPIy3d
|
||||||
|
YZKz1jPxYb9RyJqq/EZe/bqUdGg9lR4TbGg1Gh/kNxgLfZQGu617mruIhgYbZLd+
|
||||||
|
P+NvmWW8KY5Or4O9+tbjwGsCQo7OblrxdB10XeGr2caBvB6IN6wG1jFCqQKBgDg/
|
||||||
|
Y6AatoLgGjsqO2EEQzQcLjnq9+dfUGXYBxXHz11WSbZf2PrK9SjlKnu+PsojX4P7
|
||||||
|
TxFqk8vuQJOXRlE+jDdlET1mA8pxfv2NtIEII3omu9TomFB43sOIdSbbIgPWi+8F
|
||||||
|
AOFwd+c0mR5XdYmX12bZloyQaptUeSSQXdXntEo9AoGBAMqa/z2FOVK1fy8Ntr6W
|
||||||
|
vMCfgv1MfZ4Ys+FrVOI4BF/0aH4DK2uIg1yFVC+gWo1mGe74+/wBTdrRC0zEN14X
|
||||||
|
3OBUPggUJL7C6Fzun87bXcQXf8jtNWLYo4k/2cKMzt8MM1bjeuRBjQkPYWhd7xFh
|
||||||
|
H0xDf/se0RusWZxbXpel/K6i
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
4
netlify.toml
Normal file
4
netlify.toml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[[redirects]]
|
||||||
|
from = "/*"
|
||||||
|
to = "/index.html"
|
||||||
|
status = 200
|
||||||
10560
package-lock.json
generated
Normal file
10560
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
68
package.json
Normal file
68
package.json
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"build": "npm-run-all tailwind esbuild",
|
||||||
|
"dev": "dotenv -- npm-run-all -l -p serve tailwind-watch esbuild-watch tsc-watch",
|
||||||
|
"serve": "http-server -c-1 -p 8084 -P http://localhost:8084?",
|
||||||
|
"tailwind": "tailwindcss -i ./src/index.css -o ./public/index.css",
|
||||||
|
"tailwind-watch": "tailwindcss -i ./src/index.css -o ./public/index.css --watch",
|
||||||
|
"esbuild": "node ./build.mjs",
|
||||||
|
"esbuild-watch": "APP_BUILD_IS_DEV=1 node ./build.mjs",
|
||||||
|
"tsc": "tsc --noEmit",
|
||||||
|
"tsc-watch": "tsc --noEmit --watch --preserveWatchOutput",
|
||||||
|
"fmt": "prettier --write .",
|
||||||
|
"fmt-check": "prettier --check ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@bufbuild/buf": "^1.31.0",
|
||||||
|
"@bufbuild/protobuf": "^1.9.0",
|
||||||
|
"@connectrpc/connect": "^1.4.0",
|
||||||
|
"@connectrpc/connect-query": "^1.3.1",
|
||||||
|
"@connectrpc/connect-web": "^1.4.0",
|
||||||
|
"@hookform/resolvers": "^3.3.4",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||||
|
"@radix-ui/react-collapsible": "^1.0.3",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
|
"@react-oauth/google": "^0.12.1",
|
||||||
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
|
"@tanstack/react-query": "^5.32.0",
|
||||||
|
"@types/node": "^20.12.7",
|
||||||
|
"@types/react": "^18.3.1",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^7.8.0",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"dotenv-cli": "^7.4.1",
|
||||||
|
"eckles": "^1.4.1",
|
||||||
|
"esbuild": "^0.20.2",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-plugin-react": "^7.34.1",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
|
"highlight.js": "^11.9.0",
|
||||||
|
"http-server": "^14.1.1",
|
||||||
|
"lucide-react": "^0.376.0",
|
||||||
|
"moment": "^2.30.1",
|
||||||
|
"next-themes": "^0.3.0",
|
||||||
|
"node-jose": "^2.2.0",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-hook-form": "^7.51.3",
|
||||||
|
"react-router": "^6.23.0",
|
||||||
|
"react-router-dom": "^6.23.0",
|
||||||
|
"sonner": "^1.4.41",
|
||||||
|
"tailwind-merge": "^2.3.0",
|
||||||
|
"tailwindcss": "^3.4.3",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"xml-formatter": "^3.6.2",
|
||||||
|
"zod": "^3.23.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
public/index.html
Normal file
18
public/index.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="h-full">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="/index.css">
|
||||||
|
<link rel="icon" href="/apple-touch-icon.png">
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="react-root" class="h-full min-h-screen bg-muted/40"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script src="/index.js"></script>
|
||||||
|
</html>
|
||||||
13
src/App.tsx
Normal file
13
src/App.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||||
|
import { SSOPage } from "@/pages/SSOPage";
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/apps/:appId/sso" element={<SSOPage />} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
src/index.css
Normal file
60
src/index.css
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 224 71.4% 4.1%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 224 71.4% 4.1%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 224 71.4% 4.1%;
|
||||||
|
--primary: 262.1 83.3% 57.8%;
|
||||||
|
--primary-foreground: 210 20% 98%;
|
||||||
|
--secondary: 220 14.3% 95.9%;
|
||||||
|
--secondary-foreground: 220.9 39.3% 11%;
|
||||||
|
--muted: 220 14.3% 95.9%;
|
||||||
|
--muted-foreground: 220 8.9% 46.1%;
|
||||||
|
--accent: 220 14.3% 95.9%;
|
||||||
|
--accent-foreground: 220.9 39.3% 11%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 20% 98%;
|
||||||
|
--border: 220 13% 91%;
|
||||||
|
--input: 220 13% 91%;
|
||||||
|
--ring: 262.1 83.3% 57.8%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 224 71.4% 4.1%;
|
||||||
|
--foreground: 210 20% 98%;
|
||||||
|
--card: 224 71.4% 4.1%;
|
||||||
|
--card-foreground: 210 20% 98%;
|
||||||
|
--popover: 224 71.4% 4.1%;
|
||||||
|
--popover-foreground: 210 20% 98%;
|
||||||
|
--primary: 263.4 70% 50.4%;
|
||||||
|
--primary-foreground: 210 20% 98%;
|
||||||
|
--secondary: 215 27.9% 16.9%;
|
||||||
|
--secondary-foreground: 210 20% 98%;
|
||||||
|
--muted: 215 27.9% 16.9%;
|
||||||
|
--muted-foreground: 217.9 10.6% 64.9%;
|
||||||
|
--accent: 215 27.9% 16.9%;
|
||||||
|
--accent-foreground: 210 20% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 20% 98%;
|
||||||
|
--border: 215 27.9% 16.9%;
|
||||||
|
--input: 215 27.9% 16.9%;
|
||||||
|
--ring: 263.4 70% 50.4%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/index.tsx
Normal file
6
src/index.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { App } from "./App";
|
||||||
|
|
||||||
|
const root = createRoot(document.getElementById("react-root")!);
|
||||||
|
root.render(<App />);
|
||||||
77
src/lib/saml.ts
Normal file
77
src/lib/saml.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
export interface AssertionData {
|
||||||
|
idpEntityId: string;
|
||||||
|
subjectId: string;
|
||||||
|
sessionId: string;
|
||||||
|
now: string;
|
||||||
|
expire: string;
|
||||||
|
spEntityId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function encodeAssertion(
|
||||||
|
key: CryptoKey,
|
||||||
|
assertionData: AssertionData,
|
||||||
|
): Promise<string> {
|
||||||
|
return btoa(await signAssertion(key, assertionData));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signAssertion(
|
||||||
|
key: CryptoKey,
|
||||||
|
assertionData: AssertionData,
|
||||||
|
): Promise<string> {
|
||||||
|
const digest = await digestValue(assertionData);
|
||||||
|
const signature = await signatureValue(key, assertionData);
|
||||||
|
return signedAssertion(assertionData, digest, signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
function signedAssertion(
|
||||||
|
assertionData: AssertionData,
|
||||||
|
digest: string,
|
||||||
|
signature: string,
|
||||||
|
): string {
|
||||||
|
return `<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol"><saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>${digest}</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>${signature}</ds:SignatureValue></ds:Signature><saml2:Issuer>${assertionData.idpEntityId}</saml2:Issuer><saml2:Subject><saml2:NameID>${assertionData.subjectId}</saml2:NameID><saml2:SubjectConfirmation><saml2:SubjectConfirmationData InResponseTo="${assertionData.sessionId}"></saml2:SubjectConfirmationData></saml2:SubjectConfirmation></saml2:Subject><saml2:Conditions NotBefore="${assertionData.now}" NotOnOrAfter="${assertionData.expire}"><saml2:AudienceRestriction><saml2:Audience>${assertionData.spEntityId}</saml2:Audience></saml2:AudienceRestriction></saml2:Conditions></saml2:Assertion></saml2p:Response>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signatureValue(
|
||||||
|
key: CryptoKey,
|
||||||
|
assertionData: AssertionData,
|
||||||
|
): Promise<string> {
|
||||||
|
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,
|
||||||
|
enc.encode(signaturePart(digest)),
|
||||||
|
);
|
||||||
|
return arrayBufferToBase64(signatureData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function signaturePart(digest: string): string {
|
||||||
|
return `<ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:CanonicalizationMethod><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"></ds:SignatureMethod><ds:Reference><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></ds:Transform><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></ds:DigestMethod><ds:DigestValue>${digest}</ds:DigestValue></ds:Reference></ds:SignedInfo>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function digestValue(assertionData: AssertionData): Promise<string> {
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
console.log("digest part");
|
||||||
|
console.log(digestPart(assertionData));
|
||||||
|
const digestData = await crypto.subtle.digest(
|
||||||
|
"SHA-256",
|
||||||
|
enc.encode(digestPart(assertionData)),
|
||||||
|
);
|
||||||
|
return arrayBufferToBase64(digestData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function digestPart(assertionData: AssertionData): string {
|
||||||
|
return `<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"><saml2:Issuer>${assertionData.idpEntityId}</saml2:Issuer><saml2:Subject><saml2:NameID>${assertionData.subjectId}</saml2:NameID><saml2:SubjectConfirmation><saml2:SubjectConfirmationData InResponseTo="${assertionData.sessionId}"></saml2:SubjectConfirmationData></saml2:SubjectConfirmation></saml2:Subject><saml2:Conditions NotBefore="${assertionData.now}" NotOnOrAfter="${assertionData.expire}"><saml2:AudienceRestriction><saml2:Audience>${assertionData.spEntityId}</saml2:Audience></saml2:AudienceRestriction></saml2:Conditions></saml2:Assertion>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
const len = bytes.byteLength;
|
||||||
|
let binary = "";
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
62
src/pages/SSOPage.tsx
Normal file
62
src/pages/SSOPage.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { encodeAssertion } from "@/lib/saml";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
|
export function SSOPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
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",
|
||||||
|
};
|
||||||
|
|
||||||
|
const key = await window.crypto.subtle.importKey(
|
||||||
|
"jwk",
|
||||||
|
jwk,
|
||||||
|
{
|
||||||
|
name: "RSASSA-PKCS1-v1_5",
|
||||||
|
hash: "SHA-256",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
["sign"],
|
||||||
|
);
|
||||||
|
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
|
||||||
|
inputRef.current!.value = assertion;
|
||||||
|
inputRef.current!.form!.submit();
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
// const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form
|
||||||
|
method="post"
|
||||||
|
action="http://localhost:8080/saml/saml_conn_e4wryo0hq30mcrzc32b67otha/acs"
|
||||||
|
// ref={formRef}
|
||||||
|
>
|
||||||
|
<input type="hidden" name="SAMLResponse" ref={inputRef} />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
76
tailwind.config.js
Normal file
76
tailwind.config.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: ["./src/**/*.{ts,tsx}", "./public/index.html"],
|
||||||
|
prefix: "",
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: `"Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`,
|
||||||
|
mono: `"Roboto Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace`,
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: "0" },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: "0" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
};
|
||||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
"rootDir": "src",
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es2015",
|
||||||
|
"sourceMap": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"lib": ["es2017", "dom"],
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user