instant setup flow

This commit is contained in:
Ulysse Carion 2024-05-14 12:04:47 -07:00
parent c31ad152b9
commit 2b64f1de76
5 changed files with 67 additions and 13 deletions

View File

@ -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 />} />

View File

@ -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 {

View File

@ -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}`);
}; };

View 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 <></>;
}

View File

@ -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>