instant setup flow
This commit is contained in:
parent
c31ad152b9
commit
2b64f1de76
@ -4,11 +4,13 @@ import { SSOPage } from "@/pages/SSOPage";
|
|||||||
import { HomePage } from "@/pages/HomePage";
|
import { HomePage } from "@/pages/HomePage";
|
||||||
import { ViewAppPage } from "@/pages/ViewAppPage";
|
import { ViewAppPage } from "@/pages/ViewAppPage";
|
||||||
import { Page } from "@/components/Page";
|
import { Page } from "@/components/Page";
|
||||||
|
import { InstantSetupPage } from "@/pages/InstantSetupPage";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<Route path="/instant-setup" element={<InstantSetupPage />} />
|
||||||
<Route path="/" element={<Page />}>
|
<Route path="/" element={<Page />}>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/apps/:appId" element={<ViewAppPage />} />
|
<Route path="/apps/:appId" element={<ViewAppPage />} />
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
export interface AssertionData {
|
export interface AssertionData {
|
||||||
idpEntityId: string;
|
idpEntityId: string;
|
||||||
subjectId: string;
|
subjectId: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
now: string;
|
now: string;
|
||||||
expire: string;
|
expire: string;
|
||||||
@ -28,7 +30,7 @@ function signedAssertion(
|
|||||||
digest: string,
|
digest: string,
|
||||||
signature: string,
|
signature: string,
|
||||||
): 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>`;
|
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:AttributeStatement><saml2:Attribute Name="firstName"><saml2:AttributeValue>${assertionData.firstName}</saml2:AttributeValue></saml2:Attribute><saml2:Attribute Name="lastName"><saml2:AttributeValue>${assertionData.lastName}</saml2:AttributeValue></saml2:Attribute></saml2:AttributeStatement></saml2:Assertion></saml2p:Response>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function signatureValue(
|
async function signatureValue(
|
||||||
@ -59,7 +61,7 @@ async function digestValue(assertionData: AssertionData): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function digestPart(assertionData: AssertionData): string {
|
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>`;
|
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:AttributeStatement><saml2:Attribute Name="firstName"><saml2:AttributeValue>${assertionData.firstName}</saml2:AttributeValue></saml2:Attribute><saml2:Attribute Name="lastName"><saml2:AttributeValue>${assertionData.lastName}</saml2:AttributeValue></saml2:Attribute></saml2:AttributeStatement></saml2:Assertion>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||||
|
|||||||
@ -9,18 +9,17 @@ export function HomePage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const handleCreateApp = () => {
|
const handleCreateApp = () => {
|
||||||
const id = uuidv4();
|
const id = uuidv4();
|
||||||
const newState = {
|
setStoreData({
|
||||||
...storeData,
|
...storeData,
|
||||||
apps: {
|
apps: {
|
||||||
...storeData.apps,
|
...storeData.apps,
|
||||||
[id]: {
|
[id]: {
|
||||||
id,
|
id,
|
||||||
spAcsUrl: "",
|
spAcsUrl: "",
|
||||||
|
spEntityId: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
console.log("want new state", newState);
|
|
||||||
setStoreData(newState);
|
|
||||||
|
|
||||||
navigate(`/apps/${id}`);
|
navigate(`/apps/${id}`);
|
||||||
};
|
};
|
||||||
|
|||||||
38
src/pages/InstantSetupPage.tsx
Normal file
38
src/pages/InstantSetupPage.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { createSearchParams, useSearchParams } from "react-router-dom";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import { useStore } from "@/lib/store";
|
||||||
|
|
||||||
|
export function InstantSetupPage() {
|
||||||
|
const [storeData, setStoreData] = useStore();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const appId = searchParams.get("appId")!;
|
||||||
|
const spAcsUrl = searchParams.get("spAcsUrl")!;
|
||||||
|
const spEntityId = searchParams.get("spEntityId")!;
|
||||||
|
const email = searchParams.get("email")!;
|
||||||
|
const firstName = searchParams.get("firstName")!;
|
||||||
|
const lastName = searchParams.get("lastName")!;
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
useEffect(() => {
|
||||||
|
setStoreData({
|
||||||
|
...storeData,
|
||||||
|
apps: {
|
||||||
|
...storeData.apps,
|
||||||
|
[appId]: {
|
||||||
|
id: appId,
|
||||||
|
spAcsUrl,
|
||||||
|
spEntityId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
pathname: `/apps/${appId}/sso`,
|
||||||
|
search: createSearchParams({ email, firstName, lastName }).toString(),
|
||||||
|
});
|
||||||
|
}, [appId, spAcsUrl, spEntityId]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
@ -26,6 +26,7 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import { clsx } from "clsx";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
email: z.string().email({ message: "Email must be a well-formed email." }),
|
email: z.string().email({ message: "Email must be a well-formed email." }),
|
||||||
@ -39,6 +40,9 @@ export function SSOPage() {
|
|||||||
const app = storeData.apps[appId!];
|
const app = storeData.apps[appId!];
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
const email = searchParams.get("email");
|
||||||
|
const firstName = searchParams.get("firstName");
|
||||||
|
const lastName = searchParams.get("lastName");
|
||||||
const samlRequest = searchParams.get("SAMLRequest");
|
const samlRequest = searchParams.get("SAMLRequest");
|
||||||
|
|
||||||
const [sessionId, setSessionId] = useState("");
|
const [sessionId, setSessionId] = useState("");
|
||||||
@ -71,9 +75,9 @@ export function SSOPage() {
|
|||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: "",
|
email: email ?? "",
|
||||||
firstName: "",
|
firstName: firstName ?? "",
|
||||||
lastName: "",
|
lastName: lastName ?? "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -95,6 +99,8 @@ export function SSOPage() {
|
|||||||
inputRef.current!.value = await encodeAssertion(key, {
|
inputRef.current!.value = await encodeAssertion(key, {
|
||||||
idpEntityId: `https://dummyidp.com/apps/${app.id}`,
|
idpEntityId: `https://dummyidp.com/apps/${app.id}`,
|
||||||
subjectId: values.email,
|
subjectId: values.email,
|
||||||
|
firstName: values.firstName,
|
||||||
|
lastName: values.lastName,
|
||||||
spEntityId: app.spEntityId,
|
spEntityId: app.spEntityId,
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
now: now.format(),
|
now: now.format(),
|
||||||
@ -134,7 +140,7 @@ export function SSOPage() {
|
|||||||
<FormItem className="col-span-2">
|
<FormItem className="col-span-2">
|
||||||
<FormLabel>Email</FormLabel>
|
<FormLabel>Email</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="email" {...field} />
|
<Input disabled={!!email} type="email" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -148,7 +154,7 @@ export function SSOPage() {
|
|||||||
<FormItem className="col-span-1">
|
<FormItem className="col-span-1">
|
||||||
<FormLabel>First Name</FormLabel>
|
<FormLabel>First Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input disabled={!!firstName} {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -162,7 +168,7 @@ export function SSOPage() {
|
|||||||
<FormItem className="col-span-1">
|
<FormItem className="col-span-1">
|
||||||
<FormLabel>Last Name</FormLabel>
|
<FormLabel>Last Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input disabled={!!lastName} {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -170,7 +176,14 @@ export function SSOPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="col-span-2 flex justify-end">
|
<div className="col-span-2 flex justify-end">
|
||||||
<Button>Log on</Button>
|
<Button
|
||||||
|
className={clsx(
|
||||||
|
email &&
|
||||||
|
"bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 shadow-xl",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Log on
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user