Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
stebenz authored Dec 5, 2024
2 parents b38c9ab + cf07c70 commit 16902a0
Show file tree
Hide file tree
Showing 17 changed files with 359 additions and 242 deletions.
1 change: 1 addition & 0 deletions apps/login/cypress/integration/login.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe("login", () => {
data: {
settings: {
passkeysType: 1,
allowUsernamePassword: true,
},
},
});
Expand Down
9 changes: 5 additions & 4 deletions apps/login/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,8 @@ In future, self service options to jump to are shown below, like:
## Currently NOT Supported

- loginSettings.disableLoginWithEmail
- loginSettings.disableLoginWithPhone
- loginSettings.allowExternalIdp - this will be deprecated with the new login as it can be determined by the available IDPs
- loginSettings.forceMfaLocalOnly
Timebased features like the multifactor init prompt or password expiry, are not supported due to a current limitation in the API. Lockout settings which keeps track of the password retries, will also be implemented in a later stage.

- Lockout Settings
- Password Expiry Settings
- Login Settings: multifactor init prompt
58 changes: 43 additions & 15 deletions apps/login/src/app/(login)/authenticator/set/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export default async function Page(props: {
});
}

if (!sessionWithData) {
return <Alert>{tError("unknownContext")}</Alert>;
}

const branding = await getBrandingSettings(
sessionWithData.factors?.user?.organizationId,
);
Expand All @@ -82,48 +86,72 @@ export default async function Page(props: {
sessionWithData.factors?.user?.organizationId,
);

/* - TODO: Implement after https://github.com/zitadel/zitadel/issues/8981 */

// const identityProviders = await getActiveIdentityProviders(
// sessionWithData.factors?.user?.organizationId,
// ).then((resp) => {
// return resp.identityProviders;
// });

const params = new URLSearchParams({
initial: "true", // defines that a code is not required and is therefore not shown in the UI
});

if (loginName) {
params.set("loginName", loginName);
if (sessionWithData.factors?.user?.loginName) {
params.set("loginName", sessionWithData.factors?.user?.loginName);
}

if (organization) {
params.set("organization", organization);
if (sessionWithData.factors?.user?.organizationId) {
params.set("organization", sessionWithData.factors?.user?.organizationId);
}

if (authRequestId) {
params.set("authRequestId", authRequestId);
}

const host = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: "http://localhost:3000";

return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("title")}</h1>

<p className="ztdl-p">{t("description")}</p>

{sessionWithData && (
<UserAvatar
loginName={loginName ?? sessionWithData.factors?.user?.loginName}
displayName={sessionWithData.factors?.user?.displayName}
showDropdown
searchParams={searchParams}
></UserAvatar>
)}

{!(loginName || sessionId) && <Alert>{tError("unknownContext")}</Alert>}
<UserAvatar
loginName={sessionWithData.factors?.user?.loginName}
displayName={sessionWithData.factors?.user?.displayName}
showDropdown
searchParams={searchParams}
></UserAvatar>

{loginSettings && sessionWithData && (
{loginSettings && (
<ChooseAuthenticatorToSetup
authMethods={sessionWithData.authMethods}
loginSettings={loginSettings}
params={params}
></ChooseAuthenticatorToSetup>
)}

{/* - TODO: Implement after https://github.com/zitadel/zitadel/issues/8981 */}

{/* <p className="ztdl-p text-center">
or sign in with an Identity Provider
</p>
{loginSettings?.allowExternalIdp && identityProviders && (
<SignInWithIdp
host={host}
identityProviders={identityProviders}
authRequestId={authRequestId}
organization={sessionWithData.factors?.user?.organizationId}
linkOnly={true} // tell the callback function to just link the IDP and not login, to get an error when user is already available
></SignInWithIdp>
)} */}

<div className="mt-8 flex w-full flex-row items-center">
<BackButton />
<span className="flex-grow"></span>
Expand Down
5 changes: 3 additions & 2 deletions apps/login/src/app/(login)/idp/[provider]/success/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export default async function Page(props: {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
const { id, token, authRequestId, organization } = searchParams;
const { id, token, authRequestId, organization, link } = searchParams;
const { provider } = params;

const branding = await getBrandingSettings(organization);
Expand All @@ -50,7 +50,8 @@ export default async function Page(props: {

const { idpInformation, userId } = intent;

if (userId) {
// sign in user. If user should be linked continue
if (userId && !link) {
// TODO: update user if idp.options.isAutoUpdate is true

return (
Expand Down
5 changes: 0 additions & 5 deletions apps/login/src/app/(login)/idp/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ export default async function Page(props: {

const identityProviders = await getIdentityProviders(organization);

const host = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: "http://localhost:3000";

const branding = await getBrandingSettings(organization);

return (
Expand All @@ -38,7 +34,6 @@ export default async function Page(props: {

{identityProviders && (
<SignInWithIdp
host={host}
identityProviders={identityProviders}
authRequestId={authRequestId}
organization={organization}
Expand Down
24 changes: 6 additions & 18 deletions apps/login/src/app/(login)/loginname/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,14 @@ import { DynamicTheme } from "@/components/dynamic-theme";
import { SignInWithIdp } from "@/components/sign-in-with-idp";
import { UsernameForm } from "@/components/username-form";
import {
getActiveIdentityProviders,
getBrandingSettings,
getDefaultOrg,
getLoginSettings,
settingsService,
} from "@/lib/zitadel";
import { makeReqCtx } from "@zitadel/client/v2";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { getLocale, getTranslations } from "next-intl/server";

function getIdentityProviders(orgId?: string) {
return settingsService
.getActiveIdentityProviders({ ctx: makeReqCtx(orgId) }, {})
.then((resp) => {
return resp.identityProviders;
});
}

export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
Expand All @@ -39,17 +30,15 @@ export default async function Page(props: {
}
}

const host = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: "http://localhost:3000";

const loginSettings = await getLoginSettings(
organization ?? defaultOrganization,
);

const identityProviders = await getIdentityProviders(
const identityProviders = await getActiveIdentityProviders(
organization ?? defaultOrganization,
);
).then((resp) => {
return resp.identityProviders;
});

const branding = await getBrandingSettings(
organization ?? defaultOrganization,
Expand All @@ -68,9 +57,8 @@ export default async function Page(props: {
submit={submit}
allowRegister={!!loginSettings?.allowRegister}
>
{identityProviders && process.env.ZITADEL_API_URL && (
{identityProviders && (
<SignInWithIdp
host={host}
identityProviders={identityProviders}
authRequestId={authRequestId}
organization={organization ?? defaultOrganization} // use the organization from the searchParams here otherwise fallback to the default organization
Expand Down
5 changes: 5 additions & 0 deletions apps/login/src/app/(login)/otp/[method]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { UserAvatar } from "@/components/user-avatar";
import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getLoginSettings } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers";

export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
Expand All @@ -30,6 +31,8 @@ export default async function Page(props: {

const loginSettings = await getLoginSettings(organization);

const host = (await headers()).get("host");

return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
Expand Down Expand Up @@ -67,6 +70,8 @@ export default async function Page(props: {
organization={organization}
method={method}
loginSettings={loginSettings}
host={host}
code={code}
></LoginOTP>
)}
</div>
Expand Down
6 changes: 3 additions & 3 deletions apps/login/src/app/login/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export async function GET(request: NextRequest) {
const idp = identityProviders.find((idp) => idp.id === idpId);

if (idp) {
const host = request.nextUrl.origin;
const origin = request.nextUrl.origin;

const identityProviderType = identityProviders[0].type;
let provider = idpTypeToSlug(identityProviderType);
Expand All @@ -193,10 +193,10 @@ export async function GET(request: NextRequest) {
idpId,
urls: {
successUrl:
`${host}/idp/${provider}/success?` +
`${origin}/idp/${provider}/success?` +
new URLSearchParams(params),
failureUrl:
`${host}/idp/${provider}/failure?` +
`${origin}/idp/${provider}/failure?` +
new URLSearchParams(params),
},
}).then((resp) => {
Expand Down
15 changes: 14 additions & 1 deletion apps/login/src/components/login-otp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Props = {
method: string;
code?: string;
loginSettings?: LoginSettings;
host: string | null;
};

type Inputs = {
Expand All @@ -39,6 +40,7 @@ export function LoginOTP({
method,
code,
loginSettings,
host,
}: Props) {
const t = useTranslations("otp");

Expand Down Expand Up @@ -76,7 +78,18 @@ export function LoginOTP({

if (method === "email") {
challenges = create(RequestChallengesSchema, {
otpEmail: { deliveryType: { case: "sendCode", value: {} } },
otpEmail: {
deliveryType: {
case: "sendCode",
value: host
? {
urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}/otp/method=${method}?code={{.Code}}&userId={{.UserID}}&sessionId={{.SessionID}}&organization={{.OrgID}}` +
(authRequestId ? `&authRequestId=${authRequestId}` : ""),
}
: {},
},
},
});
}

Expand Down
2 changes: 1 addition & 1 deletion apps/login/src/components/password-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ export function PasswordForm({
password: { password: values.password },
}),
authRequestId,
forceMfa: loginSettings?.forceMfa,
})
.catch(() => {
setError("Could not verify password");
Expand Down Expand Up @@ -87,6 +86,7 @@ export function PasswordForm({
const response = await resetPassword({
loginName,
organization,
authRequestId,
})
.catch(() => {
setError("Could not reset password");
Expand Down
16 changes: 12 additions & 4 deletions apps/login/src/components/session-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ export function isSessionValid(session: Partial<Session>): {
} {
const validPassword = session?.factors?.password?.verifiedAt;
const validPasskey = session?.factors?.webAuthN?.verifiedAt;
const validIDP = session?.factors?.intent?.verifiedAt;

const stillValid = session.expirationDate
? timestampDate(session.expirationDate) > new Date()
: true;

const verifiedAt = validPassword || validPasskey;
const valid = !!((validPassword || validPasskey) && stillValid);
const verifiedAt = validPassword || validPasskey || validIDP;
const valid = !!((validPassword || validPasskey || validIDP) && stillValid);

return { valid, verifiedAt };
}
Expand Down Expand Up @@ -102,15 +104,21 @@ export function SessionItem({
/>
</div>

<div className="flex flex-col overflow-hidden">
<div className="flex flex-col items-start overflow-hidden">
<span className="">{session.factors?.user?.displayName}</span>
<span className="text-xs opacity-80 text-ellipsis">
{session.factors?.user?.loginName}
</span>
{valid && (
{valid ? (
<span className="text-xs opacity-80 text-ellipsis">
{verifiedAt && moment(timestampDate(verifiedAt)).fromNow()}
</span>
) : (
<span className="text-xs opacity-80 text-ellipsis">
expired{" "}
{session.expirationDate &&
moment(timestampDate(session.expirationDate)).fromNow()}
</span>
)}
</div>

Expand Down
Loading

0 comments on commit 16902a0

Please sign in to comment.