fix skip passwordless prompt, register with authrequest, passkey

This commit is contained in:
peintnermax
2024-03-25 15:18:51 +01:00
parent a9d0752b5f
commit 7dd709e3e7
13 changed files with 131 additions and 49 deletions

View File

@@ -9,22 +9,25 @@ export default async function Page({
}: { }: {
searchParams: Record<string | number | symbol, string | undefined>; searchParams: Record<string | number | symbol, string | undefined>;
}) { }) {
const { loginName, prompt, organization } = searchParams; const { loginName, promptPasswordless, organization } = searchParams;
const sessionFactors = await loadSession(loginName); const sessionFactors = await loadSession(loginName);
async function loadSession(loginName?: string) { async function loadSession(loginName?: string) {
const recent = await getMostRecentCookieWithLoginname(loginName); const recent = await getMostRecentCookieWithLoginname(
loginName,
organization
);
return getSession(server, recent.id, recent.token).then((response) => { return getSession(server, recent.id, recent.token).then((response) => {
if (response?.session) { if (response?.session) {
return response.session; return response.session;
} }
}); });
} }
const title = !!prompt const title = !!promptPasswordless
? "Authenticate with a passkey" ? "Authenticate with a passkey"
: "Use your passkey to confirm it's really you"; : "Use your passkey to confirm it's really you";
const description = !!prompt const description = !!promptPasswordless
? "When set up, you will be able to authenticate without a password." ? "When set up, you will be able to authenticate without a password."
: "Your device will ask for your fingerprint, face, or screen lock"; : "Your device will ask for your fingerprint, face, or screen lock";
@@ -65,7 +68,10 @@ export default async function Page({
)} )}
{sessionFactors?.id && ( {sessionFactors?.id && (
<RegisterPasskey sessionId={sessionFactors.id} isPrompt={!!prompt} /> <RegisterPasskey
sessionId={sessionFactors.id}
isPrompt={!!promptPasswordless}
/>
)} )}
</div> </div>
); );

View File

@@ -13,12 +13,15 @@ export default async function Page({
}: { }: {
searchParams: Record<string | number | symbol, string | undefined>; searchParams: Record<string | number | symbol, string | undefined>;
}) { }) {
const { loginName, altPassword, authRequestId } = searchParams; const { loginName, altPassword, authRequestId, organization } = searchParams;
const sessionFactors = await loadSession(loginName); const sessionFactors = await loadSession(loginName, organization);
async function loadSession(loginName?: string) { async function loadSession(loginName?: string, organization?: string) {
const recent = await getMostRecentCookieWithLoginname(loginName); const recent = await getMostRecentCookieWithLoginname(
loginName,
organization
);
return getSession(server, recent.id, recent.token).then((response) => { return getSession(server, recent.id, recent.token).then((response) => {
if (response?.session) { if (response?.session) {
return response.session; return response.session;
@@ -50,6 +53,7 @@ export default async function Page({
loginName={loginName} loginName={loginName}
authRequestId={authRequestId} authRequestId={authRequestId}
altPassword={altPassword === "true"} altPassword={altPassword === "true"}
organization={organization}
/> />
)} )}
</div> </div>

View File

@@ -11,13 +11,15 @@ export default async function Page({
}: { }: {
searchParams: Record<string | number | symbol, string | undefined>; searchParams: Record<string | number | symbol, string | undefined>;
}) { }) {
const { firstname, lastname, email } = searchParams; const { firstname, lastname, email, organization, authRequestId } =
searchParams;
const setPassword = !!(firstname && lastname && email); const setPassword = !!(firstname && lastname && email);
const legal = await getLegalAndSupportSettings(server); const legal = await getLegalAndSupportSettings(server);
const passwordComplexitySettings = await getPasswordComplexitySettings( const passwordComplexitySettings = await getPasswordComplexitySettings(
server server,
organization
); );
return setPassword ? ( return setPassword ? (
@@ -31,6 +33,8 @@ export default async function Page({
email={email} email={email}
firstname={firstname} firstname={firstname}
lastname={lastname} lastname={lastname}
organization={organization}
authRequestId={authRequestId}
></SetPasswordForm> ></SetPasswordForm>
)} )}
</div> </div>
@@ -42,6 +46,8 @@ export default async function Page({
{legal && passwordComplexitySettings && ( {legal && passwordComplexitySettings && (
<RegisterFormWithoutPassword <RegisterFormWithoutPassword
legal={legal} legal={legal}
organization={organization}
authRequestId={authRequestId}
></RegisterFormWithoutPassword> ></RegisterFormWithoutPassword>
)} )}
</div> </div>

View File

@@ -25,6 +25,7 @@ export async function POST(request: NextRequest) {
const userId = session?.session?.factors?.user?.id; const userId = session?.session?.factors?.user?.id;
if (userId) { if (userId) {
// TODO: add org context
return createPasskeyRegistrationLink(userId) return createPasskeyRegistrationLink(userId)
.then((resp) => { .then((resp) => {
const code = resp.code; const code = resp.code;

View File

@@ -17,12 +17,20 @@ import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
const body = await request.json(); const body = await request.json();
if (body) { if (body) {
const { userId, idpIntent, loginName, password, authRequestId } = body; const {
userId,
idpIntent,
loginName,
password,
organization,
authRequestId,
} = body;
if (userId && idpIntent) { if (userId && idpIntent) {
return createSessionForIdpAndUpdateCookie( return createSessionForIdpAndUpdateCookie(
userId, userId,
idpIntent, idpIntent,
organization,
authRequestId authRequestId
).then((session) => { ).then((session) => {
return NextResponse.json(session); return NextResponse.json(session);
@@ -32,7 +40,8 @@ export async function POST(request: NextRequest) {
loginName, loginName,
password, password,
undefined, undefined,
undefined organization,
authRequestId
).then((session) => { ).then((session) => {
return NextResponse.json(session); return NextResponse.json(session);
}); });
@@ -54,11 +63,11 @@ export async function PUT(request: NextRequest) {
const body = await request.json(); const body = await request.json();
if (body) { if (body) {
const { loginName, password, webAuthN, authRequestId } = body; const { loginName, organization, password, webAuthN, authRequestId } = body;
const challenges: RequestChallenges = body.challenges; const challenges: RequestChallenges = body.challenges;
const recentPromise: Promise<SessionCookie> = loginName const recentPromise: Promise<SessionCookie> = loginName
? getSessionCookieByLoginName(loginName).catch((error) => { ? getSessionCookieByLoginName(loginName, organization).catch((error) => {
return Promise.reject(error); return Promise.reject(error);
}) })
: getMostRecentSessionCookie().catch((error) => { : getMostRecentSessionCookie().catch((error) => {

View File

@@ -94,12 +94,18 @@ export async function getLegalAndSupportSettings(
} }
export async function getPasswordComplexitySettings( export async function getPasswordComplexitySettings(
server: ZitadelServer server: ZitadelServer,
organization?: string
): Promise<PasswordComplexitySettings | undefined> { ): Promise<PasswordComplexitySettings | undefined> {
const settingsService = settings.getSettings(server); const settingsService = settings.getSettings(server);
return settingsService return settingsService
.getPasswordComplexitySettings({}, {}) .getPasswordComplexitySettings(
organization
? { ctx: { orgId: organization } }
: { ctx: { instance: true } },
{}
)
.then((resp: GetPasswordComplexitySettingsResponse) => resp.settings); .then((resp: GetPasswordComplexitySettingsResponse) => resp.settings);
} }

View File

@@ -7,6 +7,7 @@ import { useRouter } from "next/navigation";
type Props = { type Props = {
userId: string; userId: string;
organization: string;
idpIntent: { idpIntent: {
idpIntentId: string; idpIntentId: string;
idpIntentToken: string; idpIntentToken: string;
@@ -31,6 +32,7 @@ export default function IdpSignin(props: Props) {
userId: props.userId, userId: props.userId,
idpIntent: props.idpIntent, idpIntent: props.idpIntent,
authRequestId: props.authRequestId, authRequestId: props.authRequestId,
organization: props.organization,
}), }),
}); });

View File

@@ -11,12 +11,14 @@ type Props = {
loginName: string; loginName: string;
authRequestId?: string; authRequestId?: string;
altPassword: boolean; altPassword: boolean;
organization?: string;
}; };
export default function LoginPasskey({ export default function LoginPasskey({
loginName, loginName,
authRequestId, authRequestId,
altPassword, altPassword,
organization,
}: Props) { }: Props) {
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
@@ -64,6 +66,7 @@ export default function LoginPasskey({
}, },
body: JSON.stringify({ body: JSON.stringify({
loginName, loginName,
organization,
challenges: { challenges: {
webAuthN: { webAuthN: {
domain: "", domain: "",
@@ -91,6 +94,7 @@ export default function LoginPasskey({
}, },
body: JSON.stringify({ body: JSON.stringify({
loginName, loginName,
organization,
webAuthN: { credentialAssertionData: data }, webAuthN: { credentialAssertionData: data },
authRequestId, authRequestId,
}), }),

View File

@@ -14,6 +14,7 @@ type Inputs = {
type Props = { type Props = {
loginName?: string; loginName?: string;
organization?: string;
authRequestId?: string; authRequestId?: string;
isAlternative?: boolean; // whether password was requested as alternative auth method isAlternative?: boolean; // whether password was requested as alternative auth method
promptPasswordless?: boolean; promptPasswordless?: boolean;
@@ -21,6 +22,7 @@ type Props = {
export default function PasswordForm({ export default function PasswordForm({
loginName, loginName,
organization,
authRequestId, authRequestId,
promptPasswordless, promptPasswordless,
isAlternative, isAlternative,
@@ -46,6 +48,7 @@ export default function PasswordForm({
}, },
body: JSON.stringify({ body: JSON.stringify({
loginName, loginName,
organization,
password: values.password, password: values.password,
authRequestId, authRequestId,
}), }),
@@ -69,36 +72,45 @@ export default function PasswordForm({
promptPasswordless && // if explicitly prompted due policy promptPasswordless && // if explicitly prompted due policy
!isAlternative // escaped if password was used as an alternative method !isAlternative // escaped if password was used as an alternative method
) { ) {
return router.push( const params = new URLSearchParams({
`/passkey/add?` + loginName: resp.factors.user.loginName,
new URLSearchParams({ promptPasswordless: "true",
loginName: resp.factors.user.loginName, });
promptPasswordless: "true",
}) if (organization) {
); params.append("organization", organization);
}
return router.push(`/passkey/add?` + params);
} else { } else {
if (authRequestId && resp && resp.sessionId) { if (authRequestId && resp && resp.sessionId) {
return router.push( const params = new URLSearchParams({
`/login?` + sessionId: resp.sessionId,
new URLSearchParams({ authRequest: authRequestId,
sessionId: resp.sessionId, });
authRequest: authRequestId,
}) if (organization) {
); params.append("organization", organization);
}
return router.push(`/login?` + params);
} else { } else {
return router.push( const params = new URLSearchParams(
`/signedin?` + authRequestId
new URLSearchParams( ? {
authRequestId loginName: resp.factors.user.loginName,
? { authRequestId,
loginName: resp.factors.user.loginName, }
authRequestId, : {
} loginName: resp.factors.user.loginName,
: { }
loginName: resp.factors.user.loginName,
}
)
); );
if (organization) {
params.append("organization", organization);
}
return router.push(`/signedin?` + params);
} }
} }
}); });

View File

@@ -23,9 +23,15 @@ type Inputs =
type Props = { type Props = {
legal: LegalAndSupportSettings; legal: LegalAndSupportSettings;
organization?: string;
authRequestId?: string;
}; };
export default function RegisterFormWithoutPassword({ legal }: Props) { export default function RegisterFormWithoutPassword({
legal,
organization,
authRequestId,
}: Props) {
const { register, handleSubmit, formState } = useForm<Inputs>({ const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur", mode: "onBlur",
}); });
@@ -66,7 +72,8 @@ export default function RegisterFormWithoutPassword({ legal }: Props) {
}, },
body: JSON.stringify({ body: JSON.stringify({
loginName: loginName, loginName: loginName,
// authRequestId, register does not need an oidc callback at the end organization: organization,
authRequestId: authRequestId,
}), }),
}); });

View File

@@ -15,6 +15,7 @@ import {
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Spinner } from "./Spinner"; import { Spinner } from "./Spinner";
import Alert from "./Alert"; import Alert from "./Alert";
import { AuthRequest } from "@zitadel/server";
type Inputs = type Inputs =
| { | {
@@ -28,6 +29,8 @@ type Props = {
email: string; email: string;
firstname: string; firstname: string;
lastname: string; lastname: string;
organization?: string;
authRequestId?: string;
}; };
export default function SetPasswordForm({ export default function SetPasswordForm({
@@ -35,6 +38,8 @@ export default function SetPasswordForm({
email, email,
firstname, firstname,
lastname, lastname,
organization,
authRequestId,
}: Props) { }: Props) {
const { register, handleSubmit, watch, formState } = useForm<Inputs>({ const { register, handleSubmit, watch, formState } = useForm<Inputs>({
mode: "onBlur", mode: "onBlur",
@@ -56,6 +61,8 @@ export default function SetPasswordForm({
email: email, email: email,
firstName: firstname, firstName: firstname,
lastName: lastname, lastName: lastname,
organization: organization,
authRequestId: authRequestId,
password: values.password, password: values.password,
}), }),
}); });
@@ -79,7 +86,8 @@ export default function SetPasswordForm({
body: JSON.stringify({ body: JSON.stringify({
loginName: loginName, loginName: loginName,
password: password, password: password,
// authRequestId, register does not need an oidc callback organization: organization,
authRequestId, //, register does not need an oidc callback
}), }),
}); });

View File

@@ -148,7 +148,8 @@ export async function getSessionCookieById(id: string): Promise<SessionCookie> {
} }
export async function getSessionCookieByLoginName( export async function getSessionCookieByLoginName(
loginName: string loginName: string,
organization?: string
): Promise<SessionCookie> { ): Promise<SessionCookie> {
const cookiesList = cookies(); const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions"); const stringifiedCookie = cookiesList.get("sessions");
@@ -156,7 +157,11 @@ export async function getSessionCookieByLoginName(
if (stringifiedCookie?.value) { if (stringifiedCookie?.value) {
const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value);
const found = sessions.find((s) => s.loginName === loginName); const found = sessions.find((s) =>
s.loginName === loginName && organization
? s.organization === organization
: true
);
if (found) { if (found) {
return found; return found;
} else { } else {

View File

@@ -19,7 +19,8 @@ export async function createSessionAndUpdateCookie(
loginName: string, loginName: string,
password: string | undefined, password: string | undefined,
challenges: RequestChallenges | undefined, challenges: RequestChallenges | undefined,
authRequestId: string | undefined organization?: string,
authRequestId?: string
): Promise<Session> { ): Promise<Session> {
const createdSession = await createSessionForLoginname( const createdSession = await createSessionForLoginname(
server, server,
@@ -49,6 +50,10 @@ export async function createSessionAndUpdateCookie(
sessionCookie.authRequestId = authRequestId; sessionCookie.authRequestId = authRequestId;
} }
if (organization) {
sessionCookie.organization = organization;
}
return addSessionToCookie(sessionCookie).then(() => { return addSessionToCookie(sessionCookie).then(() => {
return response.session as Session; return response.session as Session;
}); });
@@ -95,6 +100,8 @@ export async function createSessionForUserIdAndUpdateCookie(
sessionCookie.authRequestId = authRequestId; sessionCookie.authRequestId = authRequestId;
} }
if ()
return addSessionToCookie(sessionCookie).then(() => { return addSessionToCookie(sessionCookie).then(() => {
return response.session as Session; return response.session as Session;
}); });
@@ -113,6 +120,7 @@ export async function createSessionForIdpAndUpdateCookie(
idpIntentId?: string | undefined; idpIntentId?: string | undefined;
idpIntentToken?: string | undefined; idpIntentToken?: string | undefined;
}, },
organization: string | undefined,
authRequestId: string | undefined authRequestId: string | undefined
): Promise<Session> { ): Promise<Session> {
const createdSession = await createSessionForUserIdAndIdpIntent( const createdSession = await createSessionForUserIdAndIdpIntent(
@@ -142,6 +150,10 @@ export async function createSessionForIdpAndUpdateCookie(
sessionCookie.authRequestId = authRequestId; sessionCookie.authRequestId = authRequestId;
} }
if (organization) {
sessionCookie.organization = organization;
}
return addSessionToCookie(sessionCookie).then(() => { return addSessionToCookie(sessionCookie).then(() => {
return response.session as Session; return response.session as Session;
}); });