mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-11 19:42:16 +00:00
cleanup session, idp api
This commit is contained in:
@@ -1,29 +0,0 @@
|
||||
import { startIdentityProviderFlow } from "@/lib/zitadel";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { toJson } from "@zitadel/client";
|
||||
import { StartIdentityProviderIntentResponseSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
if (body) {
|
||||
let { idpId, successUrl, failureUrl } = body;
|
||||
|
||||
return startIdentityProviderFlow({
|
||||
idpId,
|
||||
urls: {
|
||||
successUrl,
|
||||
failureUrl,
|
||||
},
|
||||
})
|
||||
.then((resp) => {
|
||||
return NextResponse.json(
|
||||
toJson(StartIdentityProviderIntentResponseSchema, resp),
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
return NextResponse.json(error, { status: 500 });
|
||||
});
|
||||
} else {
|
||||
return NextResponse.json({}, { status: 400 });
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
import { idpTypeToSlug } from "@/lib/idp";
|
||||
import {
|
||||
getActiveIdentityProviders,
|
||||
getLoginSettings,
|
||||
getOrgsByDomain,
|
||||
listAuthenticationMethodTypes,
|
||||
listUsers,
|
||||
startIdentityProviderFlow,
|
||||
} from "@/lib/zitadel";
|
||||
import { createSessionForUserIdAndUpdateCookie } from "@/utils/session";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
if (body) {
|
||||
const { loginName, authRequestId, organization } = body;
|
||||
return listUsers({
|
||||
userName: loginName,
|
||||
organizationId: organization,
|
||||
}).then(async (users) => {
|
||||
if (users.details?.totalResult == BigInt(1) && users.result[0].userId) {
|
||||
const userId = users.result[0].userId;
|
||||
return createSessionForUserIdAndUpdateCookie(
|
||||
userId,
|
||||
undefined,
|
||||
undefined,
|
||||
authRequestId,
|
||||
)
|
||||
.then((session) => {
|
||||
if (session.factors?.user?.id) {
|
||||
return listAuthenticationMethodTypes(session.factors?.user?.id)
|
||||
.then((methods) => {
|
||||
return NextResponse.json({
|
||||
authMethodTypes: methods.authMethodTypes,
|
||||
sessionId: session.id,
|
||||
factors: session.factors,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
return NextResponse.json(error, { status: 500 });
|
||||
});
|
||||
} else {
|
||||
throw { details: "No user id found in session" };
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
return NextResponse.json(error, { status: 500 });
|
||||
});
|
||||
} else {
|
||||
const loginSettings = await getLoginSettings(organization);
|
||||
// TODO: check if allowDomainDiscovery has to be allowed too, to redirect to the register page
|
||||
// user not found, check if register is enabled on organization
|
||||
|
||||
if (
|
||||
loginSettings?.allowRegister &&
|
||||
!loginSettings?.allowUsernamePassword
|
||||
) {
|
||||
// TODO redirect to loginname page with idp hint
|
||||
const identityProviders = await getActiveIdentityProviders(
|
||||
organization,
|
||||
).then((resp) => {
|
||||
return resp.identityProviders;
|
||||
});
|
||||
|
||||
if (identityProviders.length === 1) {
|
||||
const host = request.nextUrl.origin;
|
||||
|
||||
const identityProviderType = identityProviders[0].type;
|
||||
|
||||
const provider = idpTypeToSlug(identityProviderType);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (authRequestId) {
|
||||
params.set("authRequestId", authRequestId);
|
||||
}
|
||||
|
||||
if (organization) {
|
||||
params.set("organization", organization);
|
||||
}
|
||||
|
||||
return startIdentityProviderFlow({
|
||||
idpId: identityProviders[0].id,
|
||||
urls: {
|
||||
successUrl:
|
||||
`${host}/idp/${provider}/success?` +
|
||||
new URLSearchParams(params),
|
||||
failureUrl:
|
||||
`${host}/idp/${provider}/failure?` +
|
||||
new URLSearchParams(params),
|
||||
},
|
||||
}).then((resp: any) => {
|
||||
if (resp.authUrl) {
|
||||
return NextResponse.json({ nextStep: resp.authUrl });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ message: "Could not find user" },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
loginSettings?.allowRegister &&
|
||||
loginSettings?.allowUsernamePassword
|
||||
) {
|
||||
let orgToRegisterOn: string | undefined = organization;
|
||||
|
||||
if (
|
||||
!orgToRegisterOn &&
|
||||
loginName &&
|
||||
ORG_SUFFIX_REGEX.test(loginName)
|
||||
) {
|
||||
const matched = ORG_SUFFIX_REGEX.exec(loginName);
|
||||
const suffix = matched?.[1] ?? "";
|
||||
|
||||
// this just returns orgs where the suffix is set as primary domain
|
||||
const orgs = await getOrgsByDomain(suffix);
|
||||
const orgToCheckForDiscovery =
|
||||
orgs.result && orgs.result.length === 1
|
||||
? orgs.result[0].id
|
||||
: undefined;
|
||||
|
||||
const orgLoginSettings = await getLoginSettings(
|
||||
orgToCheckForDiscovery,
|
||||
);
|
||||
if (orgLoginSettings?.allowDomainDiscovery) {
|
||||
orgToRegisterOn = orgToCheckForDiscovery;
|
||||
}
|
||||
}
|
||||
|
||||
const params: any = {};
|
||||
|
||||
if (authRequestId) {
|
||||
params.authRequestId = authRequestId;
|
||||
}
|
||||
|
||||
if (loginName) {
|
||||
params.email = loginName;
|
||||
}
|
||||
|
||||
if (orgToRegisterOn) {
|
||||
params.organization = orgToRegisterOn;
|
||||
}
|
||||
|
||||
const registerUrl = new URL(
|
||||
"/register?" + new URLSearchParams(params),
|
||||
request.url,
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
nextStep: registerUrl,
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: "Could not find user" },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return NextResponse.error();
|
||||
}
|
||||
}
|
||||
20
apps/login/src/lib/server/idp.ts
Normal file
20
apps/login/src/lib/server/idp.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
"use server";
|
||||
|
||||
import { startIdentityProviderFlow } from "@/lib/zitadel";
|
||||
|
||||
export type StartIDPFlowOptions = {
|
||||
idpId: string;
|
||||
successUrl: string;
|
||||
failureUrl: string;
|
||||
};
|
||||
export async function startIDPFlow(options: StartIDPFlowOptions) {
|
||||
const { idpId, successUrl, failureUrl } = options;
|
||||
|
||||
return startIdentityProviderFlow({
|
||||
idpId,
|
||||
urls: {
|
||||
successUrl,
|
||||
failureUrl,
|
||||
},
|
||||
});
|
||||
}
|
||||
172
apps/login/src/lib/server/session.ts
Normal file
172
apps/login/src/lib/server/session.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
"use server";
|
||||
|
||||
import {
|
||||
deleteSession,
|
||||
getSession,
|
||||
getUserByID,
|
||||
listAuthenticationMethodTypes,
|
||||
} from "@/lib/zitadel";
|
||||
import {
|
||||
getMostRecentSessionCookie,
|
||||
getSessionCookieById,
|
||||
getSessionCookieByLoginName,
|
||||
removeSessionFromCookie,
|
||||
} from "@zitadel/next";
|
||||
import {
|
||||
createSessionAndUpdateCookie,
|
||||
createSessionForIdpAndUpdateCookie,
|
||||
setSessionAndUpdateCookie,
|
||||
} from "@/utils/session";
|
||||
import { headers } from "next/headers";
|
||||
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||
import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
|
||||
|
||||
type CreateNewSessionOptinos = {
|
||||
userId: string;
|
||||
idpIntent: {
|
||||
idpIntentId: string;
|
||||
idpIntentType: string;
|
||||
};
|
||||
loginName: string;
|
||||
password: string;
|
||||
organization: string;
|
||||
authRequestId: string;
|
||||
};
|
||||
|
||||
export async function createNewSession(options: CreateNewSessionOptinos) {
|
||||
const {
|
||||
userId,
|
||||
idpIntent,
|
||||
loginName,
|
||||
password,
|
||||
organization,
|
||||
authRequestId,
|
||||
} = options;
|
||||
|
||||
if (userId && idpIntent) {
|
||||
return createSessionForIdpAndUpdateCookie(
|
||||
userId,
|
||||
idpIntent,
|
||||
organization,
|
||||
authRequestId,
|
||||
);
|
||||
} else {
|
||||
return createSessionAndUpdateCookie(
|
||||
loginName,
|
||||
password,
|
||||
undefined,
|
||||
organization,
|
||||
authRequestId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type UpdateSessionOptions = {
|
||||
loginName?: string;
|
||||
sessionId?: string;
|
||||
organization?: string;
|
||||
checks: Checks;
|
||||
authRequestId?: string;
|
||||
challenges?: RequestChallenges;
|
||||
};
|
||||
|
||||
export async function updateSession(options: UpdateSessionOptions) {
|
||||
const {
|
||||
loginName,
|
||||
sessionId,
|
||||
organization,
|
||||
checks,
|
||||
authRequestId,
|
||||
challenges,
|
||||
} = options;
|
||||
|
||||
const sessionPromise = sessionId
|
||||
? getSessionCookieById({ sessionId }).catch((error) => {
|
||||
return Promise.reject(error);
|
||||
})
|
||||
: loginName
|
||||
? getSessionCookieByLoginName({ loginName, organization }).catch(
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
},
|
||||
)
|
||||
: getMostRecentSessionCookie().catch((error) => {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
const host = headers().get("host");
|
||||
|
||||
if (
|
||||
host &&
|
||||
challenges &&
|
||||
challenges.webAuthN &&
|
||||
!challenges.webAuthN.domain
|
||||
) {
|
||||
challenges.webAuthN.domain = host;
|
||||
}
|
||||
|
||||
const recent = await sessionPromise;
|
||||
|
||||
if (recent && challenges && (!challenges.otpEmail || !challenges.otpSms)) {
|
||||
const sessionResponse = await getSession(recent.id, recent.token);
|
||||
|
||||
if (sessionResponse && sessionResponse.session.factors.user.id) {
|
||||
const userResponse = await getUserByID(
|
||||
sessionResponse.session.factors.user.id,
|
||||
);
|
||||
const humanUser =
|
||||
userResponse.user.type.case === "human"
|
||||
? userResponse.user.type.value
|
||||
: undefined;
|
||||
|
||||
if (!challenges.otpEmail && humanUser.email.email) {
|
||||
challenges.otpEmail = humanUser.email.email;
|
||||
}
|
||||
|
||||
if (!challenges.otpSms && humanUser.phone.phone) {
|
||||
challenges.otpSms = humanUser.phone.phone;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const session = await setSessionAndUpdateCookie(
|
||||
recent,
|
||||
checks,
|
||||
challenges,
|
||||
authRequestId,
|
||||
);
|
||||
|
||||
// if password, check if user has MFA methods
|
||||
let authMethods;
|
||||
if (checks && checks.password && session.factors.user.id) {
|
||||
const response = await listAuthenticationMethodTypes(
|
||||
session.factors.user.id,
|
||||
);
|
||||
if (response.authMethodTypes && response.authMethodTypes.length) {
|
||||
authMethods = response.authMethodTypes;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId: session.id,
|
||||
factors: session.factors,
|
||||
challenges: session.challenges,
|
||||
authMethods,
|
||||
};
|
||||
}
|
||||
|
||||
type ClearSessionOptions = {
|
||||
sessionId: string;
|
||||
};
|
||||
|
||||
export async function clearSession(options: ClearSessionOptions) {
|
||||
const { sessionId } = options;
|
||||
|
||||
const session = await getSessionCookieById({ sessionId });
|
||||
|
||||
const deletedSession = await deleteSession(session.id, session.token);
|
||||
|
||||
if (deletedSession) {
|
||||
return removeSessionFromCookie(session);
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,12 @@ import { Spinner } from "./Spinner";
|
||||
import Alert from "./Alert";
|
||||
import BackButton from "./BackButton";
|
||||
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||
import {
|
||||
CheckPassword,
|
||||
Checks,
|
||||
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||
import { updateSession } from "@/lib/server/session";
|
||||
|
||||
type Inputs = {
|
||||
password: string;
|
||||
@@ -47,28 +51,19 @@ export default function PasswordForm({
|
||||
setError("");
|
||||
setLoading(true);
|
||||
|
||||
const res = await fetch("/api/session", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
loginName,
|
||||
organization,
|
||||
checks: {
|
||||
password: { password: values.password },
|
||||
} as Checks,
|
||||
authRequestId,
|
||||
}),
|
||||
const response = await updateSession({
|
||||
loginName,
|
||||
organization,
|
||||
checks: {
|
||||
password: { password: values.password },
|
||||
} as Checks,
|
||||
authRequestId,
|
||||
}).catch((error) => {
|
||||
setError(error ?? "Could not verify password");
|
||||
});
|
||||
|
||||
const response = await res.json();
|
||||
|
||||
setLoading(false);
|
||||
if (!res.ok) {
|
||||
setError(response.details?.details ?? "Could not verify password");
|
||||
return Promise.reject(response.details);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -99,123 +94,129 @@ export default function PasswordForm({
|
||||
return response;
|
||||
}
|
||||
|
||||
function submitPasswordAndContinue(value: Inputs): Promise<boolean | void> {
|
||||
return submitPassword(value).then((resp) => {
|
||||
// if user has mfa -> /otp/[method] or /u2f
|
||||
// if mfa is forced and user has no mfa -> /mfa/set
|
||||
// if no passwordless -> /passkey/add
|
||||
async function submitPasswordAndContinue(
|
||||
value: Inputs,
|
||||
): Promise<boolean | void> {
|
||||
const submitted = await submitPassword(value);
|
||||
// if user has mfa -> /otp/[method] or /u2f
|
||||
// if mfa is forced and user has no mfa -> /mfa/set
|
||||
// if no passwordless -> /passkey/add
|
||||
|
||||
// exclude password and passwordless
|
||||
const availableSecondFactors = resp.authMethods?.filter(
|
||||
(m: AuthenticationMethodType) =>
|
||||
m !== AuthenticationMethodType.PASSWORD &&
|
||||
m !== AuthenticationMethodType.PASSKEY,
|
||||
// exclude password and passwordless
|
||||
if (!submitted || !submitted.authMethods) {
|
||||
setError("Could not verify password");
|
||||
return;
|
||||
}
|
||||
|
||||
const availableSecondFactors = submitted?.authMethods?.filter(
|
||||
(m: AuthenticationMethodType) =>
|
||||
m !== AuthenticationMethodType.PASSWORD &&
|
||||
m !== AuthenticationMethodType.PASSKEY,
|
||||
);
|
||||
|
||||
if (availableSecondFactors.length == 1) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: submitted.factors.user.loginName,
|
||||
});
|
||||
|
||||
if (authRequestId) {
|
||||
params.append("authRequestId", authRequestId);
|
||||
}
|
||||
|
||||
if (organization) {
|
||||
params.append("organization", organization);
|
||||
}
|
||||
|
||||
const factor = availableSecondFactors[0];
|
||||
// if passwordless is other method, but user selected password as alternative, perform a login
|
||||
if (factor === AuthenticationMethodType.TOTP) {
|
||||
return router.push(`/otp/time-based?` + params);
|
||||
} else if (factor === AuthenticationMethodType.OTP_SMS) {
|
||||
return router.push(`/otp/sms?` + params);
|
||||
} else if (factor === AuthenticationMethodType.OTP_EMAIL) {
|
||||
return router.push(`/otp/email?` + params);
|
||||
} else if (factor === AuthenticationMethodType.U2F) {
|
||||
return router.push(`/u2f?` + params);
|
||||
}
|
||||
} else if (availableSecondFactors.length >= 1) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: submitted.factors.user.loginName,
|
||||
});
|
||||
|
||||
if (authRequestId) {
|
||||
params.append("authRequestId", authRequestId);
|
||||
}
|
||||
|
||||
if (organization) {
|
||||
params.append("organization", organization);
|
||||
}
|
||||
|
||||
return router.push(`/mfa?` + params);
|
||||
} else if (
|
||||
submitted.factors &&
|
||||
!submitted.factors.passwordless && // if session was not verified with a passkey
|
||||
promptPasswordless && // if explicitly prompted due policy
|
||||
!isAlternative // escaped if password was used as an alternative method
|
||||
) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: submitted.factors.user.loginName,
|
||||
promptPasswordless: "true",
|
||||
});
|
||||
|
||||
if (authRequestId) {
|
||||
params.append("authRequestId", authRequestId);
|
||||
}
|
||||
|
||||
if (organization) {
|
||||
params.append("organization", organization);
|
||||
}
|
||||
|
||||
return router.push(`/passkey/add?` + params);
|
||||
} else if (loginSettings?.forceMfa && !availableSecondFactors.length) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: submitted.factors.user.loginName,
|
||||
checkAfter: "true", // this defines if the check is directly made after the setup
|
||||
});
|
||||
|
||||
if (authRequestId) {
|
||||
params.append("authRequestId", authRequestId);
|
||||
}
|
||||
|
||||
if (organization) {
|
||||
params.append("organization", organization);
|
||||
}
|
||||
|
||||
return router.push(`/mfa/set?` + params);
|
||||
} else if (authRequestId && submitted.sessionId) {
|
||||
const params = new URLSearchParams({
|
||||
sessionId: submitted.sessionId,
|
||||
authRequest: authRequestId,
|
||||
});
|
||||
|
||||
if (organization) {
|
||||
params.append("organization", organization);
|
||||
}
|
||||
|
||||
return router.push(`/login?` + params);
|
||||
} else {
|
||||
// without OIDC flow
|
||||
const params = new URLSearchParams(
|
||||
authRequestId
|
||||
? {
|
||||
loginName: submitted.factors.user.loginName,
|
||||
authRequestId,
|
||||
}
|
||||
: {
|
||||
loginName: submitted.factors.user.loginName,
|
||||
},
|
||||
);
|
||||
|
||||
if (availableSecondFactors.length == 1) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: resp.factors.user.loginName,
|
||||
});
|
||||
|
||||
if (authRequestId) {
|
||||
params.append("authRequestId", authRequestId);
|
||||
}
|
||||
|
||||
if (organization) {
|
||||
params.append("organization", organization);
|
||||
}
|
||||
|
||||
const factor = availableSecondFactors[0];
|
||||
// if passwordless is other method, but user selected password as alternative, perform a login
|
||||
if (factor === AuthenticationMethodType.TOTP) {
|
||||
return router.push(`/otp/time-based?` + params);
|
||||
} else if (factor === AuthenticationMethodType.OTP_SMS) {
|
||||
return router.push(`/otp/sms?` + params);
|
||||
} else if (factor === AuthenticationMethodType.OTP_EMAIL) {
|
||||
return router.push(`/otp/email?` + params);
|
||||
} else if (factor === AuthenticationMethodType.U2F) {
|
||||
return router.push(`/u2f?` + params);
|
||||
}
|
||||
} else if (availableSecondFactors.length >= 1) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: resp.factors.user.loginName,
|
||||
});
|
||||
|
||||
if (authRequestId) {
|
||||
params.append("authRequestId", authRequestId);
|
||||
}
|
||||
|
||||
if (organization) {
|
||||
params.append("organization", organization);
|
||||
}
|
||||
|
||||
return router.push(`/mfa?` + params);
|
||||
} else if (
|
||||
resp.factors &&
|
||||
!resp.factors.passwordless && // if session was not verified with a passkey
|
||||
promptPasswordless && // if explicitly prompted due policy
|
||||
!isAlternative // escaped if password was used as an alternative method
|
||||
) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: resp.factors.user.loginName,
|
||||
promptPasswordless: "true",
|
||||
});
|
||||
|
||||
if (authRequestId) {
|
||||
params.append("authRequestId", authRequestId);
|
||||
}
|
||||
|
||||
if (organization) {
|
||||
params.append("organization", organization);
|
||||
}
|
||||
|
||||
return router.push(`/passkey/add?` + params);
|
||||
} else if (loginSettings?.forceMfa && !availableSecondFactors.length) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: resp.factors.user.loginName,
|
||||
checkAfter: "true", // this defines if the check is directly made after the setup
|
||||
});
|
||||
|
||||
if (authRequestId) {
|
||||
params.append("authRequestId", authRequestId);
|
||||
}
|
||||
|
||||
if (organization) {
|
||||
params.append("organization", organization);
|
||||
}
|
||||
|
||||
return router.push(`/mfa/set?` + params);
|
||||
} else if (authRequestId && resp.sessionId) {
|
||||
const params = new URLSearchParams({
|
||||
sessionId: resp.sessionId,
|
||||
authRequest: authRequestId,
|
||||
});
|
||||
|
||||
if (organization) {
|
||||
params.append("organization", organization);
|
||||
}
|
||||
|
||||
return router.push(`/login?` + params);
|
||||
} else {
|
||||
// without OIDC flow
|
||||
const params = new URLSearchParams(
|
||||
authRequestId
|
||||
? {
|
||||
loginName: resp.factors.user.loginName,
|
||||
authRequestId,
|
||||
}
|
||||
: {
|
||||
loginName: resp.factors.user.loginName,
|
||||
},
|
||||
);
|
||||
|
||||
if (organization) {
|
||||
params.append("organization", organization);
|
||||
}
|
||||
|
||||
return router.push(`/signedin?` + params);
|
||||
if (organization) {
|
||||
params.append("organization", organization);
|
||||
}
|
||||
});
|
||||
|
||||
return router.push(`/signedin?` + params);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,6 +12,7 @@ import Alert from "./Alert";
|
||||
import { IdentityProvider } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||
import { idpTypeToSlug } from "@/lib/idp";
|
||||
import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||
import { startIDPFlow } from "@/lib/server/idp";
|
||||
|
||||
export interface SignInWithIDPProps {
|
||||
children?: ReactNode;
|
||||
@@ -44,27 +45,18 @@ export function SignInWithIDP({
|
||||
params.set("organization", organization);
|
||||
}
|
||||
|
||||
const res = await fetch("/api/idp/start", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
idpId,
|
||||
successUrl:
|
||||
`${host}/idp/${provider}/success?` + new URLSearchParams(params),
|
||||
failureUrl:
|
||||
`${host}/idp/${provider}/failure?` + new URLSearchParams(params),
|
||||
}),
|
||||
const response = await startIDPFlow({
|
||||
idpId,
|
||||
successUrl:
|
||||
`${host}/idp/${provider}/success?` + new URLSearchParams(params),
|
||||
failureUrl:
|
||||
`${host}/idp/${provider}/failure?` + new URLSearchParams(params),
|
||||
}).catch((err) => {
|
||||
setError(response.details);
|
||||
});
|
||||
|
||||
const response = await res.json();
|
||||
|
||||
setLoading(false);
|
||||
if (!res.ok) {
|
||||
setError(response.details);
|
||||
return Promise.reject(response.details);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
PasskeysType,
|
||||
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||
import BackButton from "./BackButton";
|
||||
import { sendLoginname, SendLoginnameOptions } from "@/lib/server/loginname";
|
||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||
|
||||
type Inputs = {
|
||||
loginName: string;
|
||||
@@ -51,161 +53,142 @@ export default function UsernameForm({
|
||||
async function submitLoginName(values: Inputs, organization?: string) {
|
||||
setLoading(true);
|
||||
|
||||
let body: any = {
|
||||
const options: SendLoginnameOptions = {
|
||||
loginName: values.loginName,
|
||||
organization,
|
||||
authRequestId,
|
||||
};
|
||||
|
||||
if (organization) {
|
||||
body.organization = organization;
|
||||
}
|
||||
|
||||
if (authRequestId) {
|
||||
body.authRequestId = authRequestId;
|
||||
}
|
||||
|
||||
const res = await fetch("/api/loginname", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
const res = await sendLoginname(options).catch((error) => {
|
||||
setError(error ?? "An internal error occurred");
|
||||
return Promise.reject(error ?? "An internal error occurred");
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
|
||||
setError(response.message ?? "An internal error occurred");
|
||||
return Promise.reject(response.message ?? "An internal error occurred");
|
||||
}
|
||||
return res.json();
|
||||
return res;
|
||||
}
|
||||
|
||||
function setLoginNameAndGetAuthMethods(
|
||||
async function setLoginNameAndGetAuthMethods(
|
||||
values: Inputs,
|
||||
organization?: string,
|
||||
) {
|
||||
return submitLoginName(values, organization).then((response) => {
|
||||
if (response.nextStep) {
|
||||
return router.push(response.nextStep);
|
||||
} else if (response.authMethodTypes.length == 1) {
|
||||
const method = response.authMethodTypes[0];
|
||||
switch (method) {
|
||||
case 1: // user has only password as auth method
|
||||
const paramsPassword: any = {
|
||||
loginName: response.factors.user.loginName,
|
||||
};
|
||||
const response = await submitLoginName(values, organization);
|
||||
|
||||
// TODO: does this have to be checked in loginSettings.allowDomainDiscovery
|
||||
if (response?.authMethodTypes && response.authMethodTypes.length === 0) {
|
||||
setError(
|
||||
"User has no available authentication methods. Contact your administrator to setup authentication for the requested user.",
|
||||
);
|
||||
}
|
||||
|
||||
if (organization || response.factors.user.organizationId) {
|
||||
paramsPassword.organization =
|
||||
organization ?? response.factors.user.organizationId;
|
||||
}
|
||||
|
||||
if (
|
||||
loginSettings?.passkeysType &&
|
||||
(loginSettings?.passkeysType === PasskeysType.ALLOWED ||
|
||||
(loginSettings.passkeysType as string) ===
|
||||
"PASSKEYS_TYPE_ALLOWED")
|
||||
) {
|
||||
paramsPassword.promptPasswordless = `true`;
|
||||
}
|
||||
|
||||
if (authRequestId) {
|
||||
paramsPassword.authRequestId = authRequestId;
|
||||
}
|
||||
|
||||
return router.push(
|
||||
"/password?" + new URLSearchParams(paramsPassword),
|
||||
);
|
||||
case 2: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY
|
||||
const paramsPasskey: any = { loginName: values.loginName };
|
||||
if (authRequestId) {
|
||||
paramsPasskey.authRequestId = authRequestId;
|
||||
}
|
||||
|
||||
if (organization || response.factors.user.organizationId) {
|
||||
paramsPasskey.organization =
|
||||
organization ?? response.factors.user.organizationId;
|
||||
}
|
||||
|
||||
return router.push(
|
||||
"/passkey/login?" + new URLSearchParams(paramsPasskey),
|
||||
);
|
||||
default:
|
||||
const paramsPasskeyDefault: any = { loginName: values.loginName };
|
||||
|
||||
if (loginSettings?.passkeysType === 1) {
|
||||
paramsPasskeyDefault.promptPasswordless = `true`; // PasskeysType.PASSKEYS_TYPE_ALLOWED,
|
||||
}
|
||||
|
||||
if (authRequestId) {
|
||||
paramsPasskeyDefault.authRequestId = authRequestId;
|
||||
}
|
||||
|
||||
if (organization || response.factors.user.organizationId) {
|
||||
paramsPasskeyDefault.organization =
|
||||
organization ?? response.factors.user.organizationId;
|
||||
}
|
||||
|
||||
return router.push(
|
||||
"/password?" + new URLSearchParams(paramsPasskeyDefault),
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
response.authMethodTypes &&
|
||||
response.authMethodTypes.length === 0
|
||||
) {
|
||||
setError(
|
||||
"User has no available authentication methods. Contact your administrator to setup authentication for the requested user.",
|
||||
);
|
||||
} else {
|
||||
// prefer passkey in favor of other methods
|
||||
if (response.authMethodTypes.includes(2)) {
|
||||
const passkeyParams: any = {
|
||||
loginName: values.loginName,
|
||||
altPassword: `${response.authMethodTypes.includes(1)}`, // show alternative password option
|
||||
if (response?.authMethodTypes.length == 1) {
|
||||
const method = response.authMethodTypes[0];
|
||||
switch (method) {
|
||||
case AuthenticationMethodType.PASSWORD: // user has only password as auth method
|
||||
const paramsPassword: any = {
|
||||
loginName: response?.factors?.user?.loginName,
|
||||
};
|
||||
|
||||
if (authRequestId) {
|
||||
passkeyParams.authRequestId = authRequestId;
|
||||
// TODO: does this have to be checked in loginSettings.allowDomainDiscovery
|
||||
|
||||
if (organization || response?.factors?.user?.organizationId) {
|
||||
paramsPassword.organization =
|
||||
organization ?? response?.factors?.user?.organizationId;
|
||||
}
|
||||
|
||||
if (organization || response.factors.user.organizationId) {
|
||||
passkeyParams.organization =
|
||||
organization ?? response.factors.user.organizationId;
|
||||
if (
|
||||
loginSettings?.passkeysType &&
|
||||
(loginSettings?.passkeysType === PasskeysType.ALLOWED ||
|
||||
(loginSettings.passkeysType as string) ===
|
||||
"PASSKEYS_TYPE_ALLOWED")
|
||||
) {
|
||||
paramsPassword.promptPasswordless = `true`;
|
||||
}
|
||||
|
||||
if (authRequestId) {
|
||||
paramsPassword.authRequestId = authRequestId;
|
||||
}
|
||||
|
||||
return router.push(
|
||||
"/passkey/login?" + new URLSearchParams(passkeyParams),
|
||||
"/password?" + new URLSearchParams(paramsPassword),
|
||||
);
|
||||
} else {
|
||||
// user has no passkey setup and login settings allow passkeys
|
||||
const paramsPasswordDefault: any = { loginName: values.loginName };
|
||||
case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY
|
||||
const paramsPasskey: any = { loginName: values.loginName };
|
||||
if (authRequestId) {
|
||||
paramsPasskey.authRequestId = authRequestId;
|
||||
}
|
||||
|
||||
if (organization || response?.factors?.user?.organizationId) {
|
||||
paramsPasskey.organization =
|
||||
organization ?? response?.factors?.user?.organizationId;
|
||||
}
|
||||
|
||||
return router.push(
|
||||
"/passkey/login?" + new URLSearchParams(paramsPasskey),
|
||||
);
|
||||
default:
|
||||
const paramsPasskeyDefault: any = { loginName: values.loginName };
|
||||
|
||||
if (loginSettings?.passkeysType === 1) {
|
||||
paramsPasswordDefault.promptPasswordless = `true`; // PasskeysType.PASSKEYS_TYPE_ALLOWED,
|
||||
paramsPasskeyDefault.promptPasswordless = `true`; // PasskeysType.PASSKEYS_TYPE_ALLOWED,
|
||||
}
|
||||
|
||||
if (authRequestId) {
|
||||
paramsPasswordDefault.authRequestId = authRequestId;
|
||||
paramsPasskeyDefault.authRequestId = authRequestId;
|
||||
}
|
||||
|
||||
if (organization || response.factors.user.organizationId) {
|
||||
paramsPasswordDefault.organization =
|
||||
organization ?? response.factors.user.organizationId;
|
||||
if (organization || response?.factors?.user?.organizationId) {
|
||||
paramsPasskeyDefault.organization =
|
||||
organization ?? response?.factors?.user?.organizationId;
|
||||
}
|
||||
|
||||
return router.push(
|
||||
"/password?" + new URLSearchParams(paramsPasswordDefault),
|
||||
"/password?" + new URLSearchParams(paramsPasskeyDefault),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// prefer passkey in favor of other methods
|
||||
if (response?.authMethodTypes.includes(2)) {
|
||||
const passkeyParams: any = {
|
||||
loginName: values.loginName,
|
||||
altPassword: `${response.authMethodTypes.includes(1)}`, // show alternative password option
|
||||
};
|
||||
|
||||
const { errors } = formState;
|
||||
if (authRequestId) {
|
||||
passkeyParams.authRequestId = authRequestId;
|
||||
}
|
||||
|
||||
if (organization || response?.factors?.user?.organizationId) {
|
||||
passkeyParams.organization =
|
||||
organization ?? response?.factors?.user?.organizationId;
|
||||
}
|
||||
|
||||
return router.push(
|
||||
"/passkey/login?" + new URLSearchParams(passkeyParams),
|
||||
);
|
||||
} else {
|
||||
// user has no passkey setup and login settings allow passkeys
|
||||
const paramsPasswordDefault: any = { loginName: values.loginName };
|
||||
|
||||
if (loginSettings?.passkeysType === 1) {
|
||||
paramsPasswordDefault.promptPasswordless = `true`; // PasskeysType.PASSKEYS_TYPE_ALLOWED,
|
||||
}
|
||||
|
||||
if (authRequestId) {
|
||||
paramsPasswordDefault.authRequestId = authRequestId;
|
||||
}
|
||||
|
||||
if (organization || response?.factors?.user?.organizationId) {
|
||||
paramsPasswordDefault.organization =
|
||||
organization ?? response?.factors?.user?.organizationId;
|
||||
}
|
||||
|
||||
return router.push(
|
||||
"/password?" + new URLSearchParams(paramsPasswordDefault),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (submit && loginName) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"tasks": {
|
||||
"generate": {
|
||||
"outputs": ["zitadel/**"],
|
||||
"cache": true
|
||||
"cache": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10046
pnpm-lock.yaml
generated
10046
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user