Merge pull request #142 from yordis/buf-v2-breakingchanges

chore: upgrade to buf connect v2
This commit is contained in:
Max Peintner
2024-09-04 15:24:28 +02:00
committed by GitHub
62 changed files with 2010 additions and 2272 deletions

View File

@@ -1,3 +1,6 @@
.next/
dist/
packages/zitadel-proto/google
packages/zitadel-proto/protoc-gen-openapiv2
packages/zitadel-proto/validate
packages/zitadel-proto/zitadel

View File

@@ -14,6 +14,6 @@ describe("/verify", () => {
// TODO: Avoid uncaught exception in application
cy.once("uncaught:exception", () => false);
cy.visit("/verify?userId=123&code=abc&submit=true");
cy.contains("error validating code");
cy.contains("Could not verify email");
});
});

View File

@@ -31,7 +31,7 @@ export default async function Page({
<h1>{sessionFactors?.factors?.user?.displayName ?? "Password"}</h1>
<p className="ztdl-p mb-6 block">Enter your password.</p>
{!sessionFactors && (
{(!sessionFactors || !loginName) && (
<div className="py-4">
<Alert>
Could not get the context of the user. Make sure to enter the
@@ -49,14 +49,16 @@ export default async function Page({
></UserAvatar>
)}
<PasswordForm
loginName={loginName}
authRequestId={authRequestId}
organization={organization}
loginSettings={loginSettings}
promptPasswordless={promptPasswordless === "true"}
isAlternative={alt === "true"}
/>
{loginName && (
<PasswordForm
loginName={loginName}
authRequestId={authRequestId}
organization={organization}
loginSettings={loginSettings}
promptPasswordless={promptPasswordless === "true"}
isAlternative={alt === "true"}
/>
)}
</div>
</DynamicTheme>
);

View File

@@ -1,20 +1,30 @@
import { createCallback, getBrandingSettings, getSession } from "@/lib/zitadel";
import DynamicTheme from "@/ui/DynamicTheme";
import UserAvatar from "@/ui/UserAvatar";
import { create } from "@zitadel/client";
import { getMostRecentCookieWithLoginname } from "@zitadel/next";
import { redirect } from "next/navigation";
import {
CreateCallbackRequestSchema,
SessionSchema,
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
async function loadSession(loginName: string, authRequestId?: string) {
const recent = await getMostRecentCookieWithLoginname({ loginName });
if (authRequestId) {
return createCallback({
authRequestId,
callbackKind: {
case: "session",
value: { sessionId: recent.id, sessionToken: recent.token },
},
}).then(({ callbackUrl }) => {
return createCallback(
create(CreateCallbackRequestSchema, {
authRequestId,
callbackKind: {
case: "session",
value: create(SessionSchema, {
sessionId: recent.id,
sessionToken: recent.token,
}),
},
}),
).then(({ callbackUrl }) => {
return redirect(callbackUrl);
});
}
@@ -42,7 +52,7 @@ export default async function Page({ searchParams }: { searchParams: any }) {
displayName={sessionFactors?.factors?.user?.displayName}
showDropdown
searchParams={searchParams}
></UserAvatar>
/>
</div>
</DynamicTheme>
);

View File

@@ -1,25 +0,0 @@
import { startIdentityProviderFlow } from "@/lib/zitadel";
import { NextRequest, NextResponse } from "next/server";
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(resp);
})
.catch((error) => {
return NextResponse.json(error, { status: 500 });
});
} else {
return NextResponse.json({}, { status: 400 });
}
}

View File

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

View File

@@ -1,72 +0,0 @@
import {
getMostRecentSessionCookie,
getSessionCookieById,
getSessionCookieByLoginName,
} from "@zitadel/next";
import { setSessionAndUpdateCookie } from "@/utils/session";
import { NextRequest, NextResponse, userAgent } from "next/server";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { PlainMessage } from "@zitadel/client";
export async function POST(request: NextRequest) {
const body = await request.json();
if (body) {
const { loginName, sessionId, organization, authRequestId, code, method } =
body;
const recentPromise = 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);
});
return recentPromise
.then((recent) => {
const checks: PlainMessage<Checks> = {};
if (method === "time-based") {
checks.totp = {
code,
};
} else if (method === "sms") {
checks.otpSms = {
code,
};
} else if (method === "email") {
checks.otpEmail = {
code,
};
}
return setSessionAndUpdateCookie(
recent,
checks,
undefined,
authRequestId,
).then((session) => {
return NextResponse.json({
sessionId: session.id,
factors: session.factors,
challenges: session.challenges,
});
});
})
.catch((error) => {
return NextResponse.json({ details: error }, { status: 500 });
});
} else {
return NextResponse.json(
{ details: "Request body is missing" },
{ status: 400 },
);
}
}

View File

@@ -1,47 +0,0 @@
import {
createPasskeyRegistrationLink,
getSession,
registerPasskey,
} from "@/lib/zitadel";
import { getSessionCookieById } from "@zitadel/next";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
if (body) {
const { sessionId } = body;
const sessionCookie = await getSessionCookieById({ sessionId });
const session = await getSession(sessionCookie.id, sessionCookie.token);
const domain: string = request.nextUrl.hostname;
const userId = session?.session?.factors?.user?.id;
if (userId) {
// TODO: add org context
return createPasskeyRegistrationLink(userId)
.then((resp) => {
const code = resp.code;
if (!code) {
throw new Error("Missing code in response");
}
return registerPasskey(userId, code, domain).then((resp) => {
return NextResponse.json(resp);
});
})
.catch((error) => {
console.error("error on creating passkey registration link");
return NextResponse.json(error, { status: 500 });
});
} else {
return NextResponse.json(
{ details: "could not get session" },
{ status: 500 },
);
}
} else {
return NextResponse.json({}, { status: 400 });
}
}

View File

@@ -1,49 +0,0 @@
import { getSession, verifyPasskeyRegistration } from "@/lib/zitadel";
import { getSessionCookieById } from "@zitadel/next";
import { NextRequest, NextResponse, userAgent } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
if (body) {
let { passkeyId, passkeyName, publicKeyCredential, sessionId } = body;
if (!!!passkeyName) {
const { browser, device, os } = userAgent(request);
passkeyName = `${device.vendor ?? ""} ${device.model ?? ""}${
device.vendor || device.model ? ", " : ""
}${os.name}${os.name ? ", " : ""}${browser.name}`;
}
const sessionCookie = await getSessionCookieById({ sessionId });
const session = await getSession(sessionCookie.id, sessionCookie.token);
const userId = session?.session?.factors?.user?.id;
console.log("payload", {
passkeyId,
passkeyName,
publicKeyCredential,
userId,
});
if (userId) {
return verifyPasskeyRegistration({
passkeyId,
passkeyName,
publicKeyCredential,
userId,
})
.then((resp) => {
return NextResponse.json(resp);
})
.catch((error) => {
return NextResponse.json(error, { status: 500 });
});
} else {
return NextResponse.json(
{ details: "could not get session" },
{ status: 500 },
);
}
} else {
return NextResponse.json({}, { status: 400 });
}
}

View File

@@ -1,47 +0,0 @@
import { addHumanUser } from "@/lib/zitadel";
import {
createSessionAndUpdateCookie,
createSessionForUserIdAndUpdateCookie,
} from "@/utils/session";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
if (body) {
const {
email,
password,
firstName,
lastName,
organization,
authRequestId,
} = body;
return addHumanUser({
email: email,
firstName,
lastName,
password: password ? password : undefined,
organization,
})
.then((user) => {
return createSessionForUserIdAndUpdateCookie(
user.userId,
password,
undefined,
authRequestId,
).then((session) => {
return NextResponse.json({
userId: user.userId,
sessionId: session.id,
factors: session.factors,
});
});
})
.catch((error) => {
return NextResponse.json(error, { status: 500 });
});
} else {
return NextResponse.error();
}
}

View File

@@ -1,20 +0,0 @@
import { resendEmailCode } from "@/lib/zitadel";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
if (body) {
const { userId } = body;
// replace with resend Mail method once its implemented
return resendEmailCode(userId)
.then((resp) => {
return NextResponse.json(resp);
})
.catch((error) => {
return NextResponse.json(error, { status: 500 });
});
} else {
return NextResponse.error();
}
}

View File

@@ -1,31 +0,0 @@
import { listUsers, passwordReset } from "@/lib/zitadel";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
if (body) {
const { loginName, organization } = body;
return listUsers({
userName: loginName,
organizationId: organization,
}).then((users) => {
if (
users.details &&
Number(users.details.totalResult) == 1 &&
users.result[0].userId
) {
const userId = users.result[0].userId;
return passwordReset(userId)
.then((resp) => {
return NextResponse.json(resp);
})
.catch((error) => {
return NextResponse.json(error, { status: 500 });
});
} else {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}
});
}
}

View File

@@ -1,194 +0,0 @@
import {
deleteSession,
getSession,
getUserByID,
listAuthenticationMethodTypes,
} from "@/lib/zitadel";
import {
getMostRecentSessionCookie,
getSessionCookieById,
getSessionCookieByLoginName,
removeSessionFromCookie,
} from "@zitadel/next";
import {
createSessionAndUpdateCookie,
createSessionForIdpAndUpdateCookie,
setSessionAndUpdateCookie,
} from "@/utils/session";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
if (body) {
const {
userId,
idpIntent,
loginName,
password,
organization,
authRequestId,
} = body;
if (userId && idpIntent) {
return createSessionForIdpAndUpdateCookie(
userId,
idpIntent,
organization,
authRequestId,
).then((session) => {
return NextResponse.json(session);
});
} else {
return createSessionAndUpdateCookie(
loginName,
password,
undefined,
organization,
authRequestId,
).then((session) => {
return NextResponse.json(session);
});
}
} else {
return NextResponse.json(
{ details: "Session could not be created" },
{ status: 500 },
);
}
}
/**
*
* @param request password for the most recent session
* @returns the updated most recent Session with the added password
*/
export async function PUT(request: NextRequest) {
const body = await request.json();
if (body) {
const {
loginName,
sessionId,
organization,
checks,
authRequestId,
challenges,
} = body;
const recentPromise = 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 domain: string = request.nextUrl.hostname;
if (challenges && challenges.webAuthN && !challenges.webAuthN.domain) {
challenges.webAuthN.domain = domain;
}
return recentPromise
.then(async (recent) => {
if (
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;
}
}
}
return setSessionAndUpdateCookie(
recent,
checks,
challenges,
authRequestId,
).then(async (session) => {
// 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 NextResponse.json({
sessionId: session.id,
factors: session.factors,
challenges: session.challenges,
authMethods,
});
});
})
.catch((error) => {
console.error(error);
return NextResponse.json({ details: error }, { status: 500 });
});
} else {
return NextResponse.json(
{ details: "Request body is missing" },
{ status: 400 },
);
}
}
/**
*
* @param request id of the session to be deleted
*/
export async function DELETE(request: NextRequest) {
const { searchParams } = new URL(request.url);
const sessionId = searchParams.get("id");
if (sessionId) {
const session = await getSessionCookieById({ sessionId });
return deleteSession(session.id, session.token)
.then(() => {
return removeSessionFromCookie(session)
.then(() => {
return NextResponse.json({});
})
.catch((error) => {
return NextResponse.json(
{ details: "could not set cookie" },
{ status: 500 },
);
});
})
.catch((error) => {
return NextResponse.json(
{ details: "could not delete session" },
{ status: 500 },
);
});
} else {
return NextResponse.error();
}
}

View File

@@ -1,41 +0,0 @@
import {
createPasskeyRegistrationLink,
getSession,
registerPasskey,
registerU2F,
} from "@/lib/zitadel";
import { getSessionCookieById } from "@zitadel/next";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
if (body) {
const { sessionId } = body;
const sessionCookie = await getSessionCookieById({ sessionId });
const session = await getSession(sessionCookie.id, sessionCookie.token);
const domain: string = request.nextUrl.hostname;
const userId = session?.session?.factors?.user?.id;
if (userId) {
return registerU2F(userId, domain)
.then((resp) => {
return NextResponse.json(resp);
})
.catch((error) => {
console.error("error on creating passkey registration link");
return NextResponse.json(error, { status: 500 });
});
} else {
return NextResponse.json(
{ details: "could not get session" },
{ status: 500 },
);
}
} else {
return NextResponse.json({}, { status: 400 });
}
}

View File

@@ -1,50 +0,0 @@
import { getSession, verifyU2FRegistration } from "@/lib/zitadel";
import { getSessionCookieById } from "@zitadel/next";
import { NextRequest, NextResponse, userAgent } from "next/server";
import { VerifyU2FRegistrationRequest } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { PlainMessage } from "@zitadel/client";
export async function POST(request: NextRequest) {
const body = await request.json();
if (body) {
let { u2fId, passkeyName, publicKeyCredential, sessionId } = body;
if (!!!passkeyName) {
const { browser, device, os } = userAgent(request);
passkeyName = `${device.vendor ?? ""} ${device.model ?? ""}${
device.vendor || device.model ? ", " : ""
}${os.name}${os.name ? ", " : ""}${browser.name}`;
}
const sessionCookie = await getSessionCookieById({ sessionId });
const session = await getSession(sessionCookie.id, sessionCookie.token);
const userId = session?.session?.factors?.user?.id;
if (userId) {
let req: PlainMessage<VerifyU2FRegistrationRequest> = {
publicKeyCredential,
u2fId,
userId,
tokenName: passkeyName,
};
req = VerifyU2FRegistrationRequest.fromJson(request as any);
return verifyU2FRegistration(req)
.then((resp) => {
return NextResponse.json(resp);
})
.catch((error) => {
return NextResponse.json(error, { status: 500 });
});
} else {
return NextResponse.json(
{ details: "could not get session" },
{ status: 500 },
);
}
} else {
return NextResponse.json({}, { status: 400 });
}
}

View File

@@ -1,19 +0,0 @@
import { verifyEmail } from "@/lib/zitadel";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
if (body) {
const { userId, code } = body;
return verifyEmail(userId, code)
.then((resp) => {
return NextResponse.json(resp);
})
.catch((error) => {
return NextResponse.json(error, { status: 500 });
});
} else {
return NextResponse.error();
}
}

View File

@@ -1,3 +1,8 @@
import {
CreateCallbackRequestSchema,
SessionSchema,
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
export const dynamic = "force-dynamic";
export const revalidate = false;
export const fetchCache = "default-no-store";
@@ -19,6 +24,7 @@ import {
} from "@zitadel/proto/zitadel/oidc/v2/authorization_pb";
import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { idpTypeToSlug } from "@/lib/idp";
import { create } from "@zitadel/client";
async function loadSessions(ids: string[]): Promise<Session[]> {
const response = await listSessions(
@@ -98,13 +104,15 @@ export async function GET(request: NextRequest) {
// works not with _rsc request
try {
const { callbackUrl } = await createCallback({
authRequestId,
callbackKind: {
case: "session",
value: session,
},
});
const { callbackUrl } = await createCallback(
create(CreateCallbackRequestSchema, {
authRequestId,
callbackKind: {
case: "session",
value: create(SessionSchema, session),
},
}),
);
if (callbackUrl) {
return NextResponse.redirect(callbackUrl);
} else {
@@ -262,13 +270,15 @@ export async function GET(request: NextRequest) {
sessionId: cookie?.id,
sessionToken: cookie?.token,
};
const { callbackUrl } = await createCallback({
authRequestId,
callbackKind: {
case: "session",
value: session,
},
});
const { callbackUrl } = await createCallback(
create(CreateCallbackRequestSchema, {
authRequestId,
callbackKind: {
case: "session",
value: create(SessionSchema, session),
},
}),
);
return NextResponse.redirect(callbackUrl);
} else {
return NextResponse.json(
@@ -297,13 +307,15 @@ export async function GET(request: NextRequest) {
sessionToken: cookie?.token,
};
try {
const { callbackUrl } = await createCallback({
authRequestId,
callbackKind: {
case: "session",
value: session,
},
});
const { callbackUrl } = await createCallback(
create(CreateCallbackRequestSchema, {
authRequestId,
callbackKind: {
case: "session",
value: create(SessionSchema, session),
},
}),
);
if (callbackUrl) {
return NextResponse.redirect(callbackUrl);
} else {

View File

@@ -1,7 +1,10 @@
import { AddHumanUserRequest } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { IDPInformation, IDPLink } from "@zitadel/proto/zitadel/user/v2/idp_pb";
import {
AddHumanUserRequest,
AddHumanUserRequestSchema,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { IDPInformation } from "@zitadel/proto/zitadel/user/v2/idp_pb";
import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { PartialMessage } from "@zitadel/client";
import { create } from "@zitadel/client";
// This maps the IdentityProviderType to a slug which is used in the /success and /failure routes
export function idpTypeToSlug(idpType: IdentityProviderType) {
@@ -33,20 +36,13 @@ export type OIDC_USER = {
};
export const PROVIDER_MAPPING: {
[provider: string]: (
rI: IDPInformation,
) => PartialMessage<AddHumanUserRequest>;
[provider: string]: (rI: IDPInformation) => AddHumanUserRequest;
} = {
[idpTypeToSlug(IdentityProviderType.GOOGLE)]: (idp: IDPInformation) => {
const rawInfo = idp.rawInformation?.toJson() as OIDC_USER;
const rawInfo = idp.rawInformation as OIDC_USER;
console.log(rawInfo);
const idpLink: PartialMessage<IDPLink> = {
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
};
const req: PartialMessage<AddHumanUserRequest> = {
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
email: rawInfo.User?.email,
@@ -57,13 +53,17 @@ export const PROVIDER_MAPPING: {
givenName: rawInfo.User?.given_name ?? "",
familyName: rawInfo.User?.family_name ?? "",
},
idpLinks: [idpLink],
};
return req;
idpLinks: [
{
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
},
],
});
},
[idpTypeToSlug(IdentityProviderType.AZURE_AD)]: (idp: IDPInformation) => {
const rawInfo = idp.rawInformation?.toJson() as {
const rawInfo = idp.rawInformation as {
jobTitle: string;
mail: string;
mobilePhone: string;
@@ -76,15 +76,9 @@ export const PROVIDER_MAPPING: {
userPrincipalName: string;
};
const idpLink: PartialMessage<IDPLink> = {
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
};
console.log(rawInfo, rawInfo.userPrincipalName);
const req: PartialMessage<AddHumanUserRequest> = {
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
email: rawInfo.mail || rawInfo.userPrincipalName || "",
@@ -95,24 +89,22 @@ export const PROVIDER_MAPPING: {
givenName: rawInfo.givenName ?? "",
familyName: rawInfo.surname ?? "",
},
idpLinks: [idpLink],
};
return req;
idpLinks: [
{
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
},
],
});
},
[idpTypeToSlug(IdentityProviderType.GITHUB)]: (idp: IDPInformation) => {
const rawInfo = idp.rawInformation?.toJson() as {
const rawInfo = idp.rawInformation as {
email: string;
name: string;
};
const idpLink: PartialMessage<IDPLink> = {
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
};
const req: PartialMessage<AddHumanUserRequest> = {
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
email: rawInfo.email,
@@ -123,9 +115,13 @@ export const PROVIDER_MAPPING: {
givenName: rawInfo.name ?? "",
familyName: rawInfo.name ?? "",
},
idpLinks: [idpLink],
};
return req;
idpLinks: [
{
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
},
],
});
},
};

View File

@@ -0,0 +1,24 @@
"use server";
import { resendEmailCode, verifyEmail } from "@/lib/zitadel";
type VerifyUserByEmailCommand = {
userId: string;
code: string;
};
export async function verifyUserByEmail(command: VerifyUserByEmailCommand) {
const { userId, code } = command;
return verifyEmail(userId, code);
}
type resendVerifyEmailCommand = {
userId: string;
};
export async function resendVerifyEmail(command: resendVerifyEmailCommand) {
const { userId } = command;
// replace with resend Mail method once its implemented
return resendEmailCode(userId);
}

View File

@@ -0,0 +1,21 @@
"use server";
import { startIdentityProviderFlow } from "@/lib/zitadel";
export type StartIDPFlowCommand = {
idpId: string;
successUrl: string;
failureUrl: string;
};
export async function startIDPFlow(command: StartIDPFlowCommand) {
const { idpId, successUrl, failureUrl } = command;
return startIdentityProviderFlow({
idpId,
urls: {
successUrl,
failureUrl,
},
});
}

View File

@@ -0,0 +1,118 @@
"use server";
import { headers } from "next/headers";
import { idpTypeToSlug } from "../idp";
import {
getActiveIdentityProviders,
getLoginSettings,
listAuthenticationMethodTypes,
listUsers,
startIdentityProviderFlow,
} from "../zitadel";
import { createSessionForUserIdAndUpdateCookie } from "../../utils/session";
import { redirect } from "next/navigation";
export type SendLoginnameCommand = {
loginName: string;
authRequestId?: string;
organization?: string;
};
export async function sendLoginname(options: SendLoginnameCommand) {
const { loginName, authRequestId, organization } = options;
const users = await listUsers({
userName: loginName,
organizationId: organization,
});
if (users.details?.totalResult == BigInt(1) && users.result[0].userId) {
const userId = users.result[0].userId;
const session = await createSessionForUserIdAndUpdateCookie(
userId,
undefined,
undefined,
authRequestId,
);
if (!session.factors?.user?.id) {
throw Error("Could not create session for user");
}
const methods = await listAuthenticationMethodTypes(
session.factors?.user?.id,
);
return {
authMethodTypes: methods.authMethodTypes,
sessionId: session.id,
factors: session.factors,
};
}
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 = headers().get("host");
console.log("host", host);
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 redirect(resp.authUrl);
}
});
} else {
throw Error("Could not find user");
}
} else if (
loginSettings?.allowRegister &&
loginSettings?.allowUsernamePassword
) {
const params: any = { organization };
if (authRequestId) {
params.authRequestId = authRequestId;
}
if (loginName) {
params.email = loginName;
}
const registerUrl = new URL(
"/register?" + new URLSearchParams(params),
// request.url,
);
return redirect(registerUrl.toString());
}
throw Error("Could not find user");
}

View File

@@ -0,0 +1,74 @@
"use server";
import {
getMostRecentSessionCookie,
getSessionCookieById,
getSessionCookieByLoginName,
} from "@zitadel/next";
import { setSessionAndUpdateCookie } from "@/utils/session";
import { NextRequest, NextResponse } from "next/server";
import {
CheckOTPSchema,
ChecksSchema,
CheckTOTPSchema,
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { create } from "@zitadel/client";
export type SetOTPCommand = {
loginName?: string;
sessionId?: string;
organization?: string;
authRequestId?: string;
code: string;
method: string;
};
export async function setOTP(command: SetOTPCommand) {
const { loginName, sessionId, organization, authRequestId, code, method } =
command;
const recentPromise = 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);
});
return recentPromise.then((recent) => {
const checks = create(ChecksSchema, {});
if (method === "time-based") {
checks.totp = create(CheckTOTPSchema, {
code,
});
} else if (method === "sms") {
checks.otpSms = create(CheckOTPSchema, {
code,
});
} else if (method === "email") {
checks.otpEmail = create(CheckOTPSchema, {
code,
});
}
return setSessionAndUpdateCookie(
recent,
checks,
undefined,
authRequestId,
).then((session) => {
return {
sessionId: session.id,
factors: session.factors,
challenges: session.challenges,
};
});
});
}

View File

@@ -0,0 +1,86 @@
"use server";
import {
createPasskeyRegistrationLink,
getSession,
registerPasskey,
verifyPasskeyRegistration,
} from "@/lib/zitadel";
import { getSessionCookieById } from "@zitadel/next";
import { userAgent } from "next/server";
import { create } from "@zitadel/client";
import { VerifyPasskeyRegistrationRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { headers } from "next/headers";
import { RegisterPasskeyResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
type VerifyPasskeyCommand = {
passkeyId: string;
passkeyName?: string;
publicKeyCredential: any;
sessionId: string;
};
type RegisterPasskeyCommand = {
sessionId: string;
};
export async function registerPasskeyLink(
command: RegisterPasskeyCommand,
): Promise<RegisterPasskeyResponse> {
const { sessionId } = command;
const sessionCookie = await getSessionCookieById({ sessionId });
const session = await getSession(sessionCookie.id, sessionCookie.token);
const domain = headers().get("host");
if (!domain) {
throw new Error("Could not get domain");
}
const userId = session?.session?.factors?.user?.id;
if (!userId) {
throw new Error("Could not get session");
}
// TODO: add org context
const registerLink = await createPasskeyRegistrationLink(userId);
if (!registerLink.code) {
throw new Error("Missing code in response");
}
return registerPasskey(userId, registerLink.code, domain);
}
export async function verifyPasskey(command: VerifyPasskeyCommand) {
let { passkeyId, passkeyName, publicKeyCredential, sessionId } = command;
// if no name is provided, try to generate one from the user agent
if (!!!passkeyName) {
const headersList = headers();
const userAgentStructure = { headers: headersList };
const { browser, device, os } = userAgent(userAgentStructure);
passkeyName = `${device.vendor ?? ""} ${device.model ?? ""}${
device.vendor || device.model ? ", " : ""
}${os.name}${os.name ? ", " : ""}${browser.name}`;
}
const sessionCookie = await getSessionCookieById({ sessionId });
const session = await getSession(sessionCookie.id, sessionCookie.token);
const userId = session?.session?.factors?.user?.id;
if (!userId) {
throw new Error("Could not get session");
}
return verifyPasskeyRegistration(
create(VerifyPasskeyRegistrationRequestSchema, {
passkeyId,
passkeyName,
publicKeyCredential,
userId,
}),
);
}

View File

@@ -0,0 +1,27 @@
"use server";
import { listUsers, passwordReset } from "@/lib/zitadel";
type ResetPasswordCommand = {
loginName: string;
organization?: string;
};
export async function resetPassword(command: ResetPasswordCommand) {
const { loginName, organization } = command;
const users = await listUsers({
userName: loginName,
organizationId: organization,
});
if (
!users.details ||
Number(users.details.totalResult) !== 1 ||
users.result[0].userId
) {
throw Error("Could not find user");
}
const userId = users.result[0].userId;
return passwordReset(userId);
}

View File

@@ -0,0 +1,41 @@
"use server";
import { addHumanUser } from "@/lib/zitadel";
import { createSessionForUserIdAndUpdateCookie } from "@/utils/session";
type RegisterUserCommand = {
email: string;
firstName: string;
lastName: string;
password?: string;
organization?: string;
authRequestId?: string;
};
export async function registerUser(command: RegisterUserCommand) {
const { email, password, firstName, lastName, organization, authRequestId } =
command;
const human = await addHumanUser({
email: email,
firstName,
lastName,
password: password ? password : undefined,
organization,
});
if (!human) {
throw Error("Could not create user");
}
return createSessionForUserIdAndUpdateCookie(
human.userId,
password,
undefined,
authRequestId,
).then((session) => {
return {
userId: human.userId,
sessionId: session.id,
factors: session.factors,
};
});
}

View File

@@ -0,0 +1,199 @@
"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,
RequestChallengesSchema,
} from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { create } from "@zitadel/client";
type CreateNewSessionCommand = {
userId: string;
idpIntent: {
idpIntentId: string;
idpIntentToken: string;
};
loginName?: string;
password?: string;
organization?: string;
authRequestId?: string;
};
export async function createNewSession(options: CreateNewSessionCommand) {
const {
userId,
idpIntent,
loginName,
password,
organization,
authRequestId,
} = options;
if (userId && idpIntent) {
return createSessionForIdpAndUpdateCookie(
userId,
idpIntent,
organization,
authRequestId,
);
} else if (loginName) {
return createSessionAndUpdateCookie(
loginName,
password,
undefined,
organization,
authRequestId,
);
} else {
throw new Error("No userId or loginName provided");
}
}
export type UpdateSessionCommand = {
loginName?: string;
sessionId?: string;
organization?: string;
checks?: Checks;
authRequestId?: string;
challenges?: RequestChallenges;
};
export async function updateSession(options: UpdateSessionCommand) {
let {
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 = create(RequestChallengesSchema, {
otpEmail: { deliveryType: { case: "sendCode", value: {} } },
});
}
if (!challenges.otpEmail && humanUser?.email?.email) {
challenges = create(RequestChallengesSchema, {
otpSms: { returnCode: true },
});
}
}
}
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);
}
}
type CleanupSessionCommand = {
sessionId: string;
};
export async function cleanupSession({ sessionId }: CleanupSessionCommand) {
const sessionCookie = await getSessionCookieById({ sessionId });
const deleteResponse = await deleteSession(
sessionCookie.id,
sessionCookie.token,
);
if (!deleteResponse) {
throw new Error("Could not delete session");
}
return removeSessionFromCookie(sessionCookie);
}

View File

@@ -0,0 +1,71 @@
"use server";
import { getSession, registerU2F, verifyU2FRegistration } from "@/lib/zitadel";
import { getSessionCookieById } from "@zitadel/next";
import { userAgent } from "next/server";
import { VerifyU2FRegistrationRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { create } from "@zitadel/client";
import { headers } from "next/headers";
type RegisterU2FCommand = {
sessionId: string;
};
type VerifyU2FCommand = {
u2fId: string;
passkeyName?: string;
publicKeyCredential: any;
sessionId: string;
};
export async function addU2F(command: RegisterU2FCommand) {
const { sessionId } = command;
const sessionCookie = await getSessionCookieById({ sessionId });
const session = await getSession(sessionCookie.id, sessionCookie.token);
const domain = headers().get("host");
if (!domain) {
throw Error("Could not get domain");
}
const userId = session?.session?.factors?.user?.id;
if (!userId) {
throw Error("Could not get session");
}
return registerU2F(userId, domain);
}
export async function verifyU2F(command: VerifyU2FCommand) {
let { passkeyName, sessionId } = command;
if (!!!passkeyName) {
const headersList = headers();
const userAgentStructure = { headers: headersList };
const { browser, device, os } = userAgent(userAgentStructure);
passkeyName = `${device.vendor ?? ""} ${device.model ?? ""}${
device.vendor || device.model ? ", " : ""
}${os.name}${os.name ? ", " : ""}${browser.name}`;
}
const sessionCookie = await getSessionCookieById({ sessionId });
const session = await getSession(sessionCookie.id, sessionCookie.token);
const userId = session?.session?.factors?.user?.id;
if (!userId) {
throw new Error("Could not get session");
}
const req = create(
VerifyU2FRegistrationRequestSchema,
// TODO: why did we passed the request instead of body here?
command,
);
return verifyU2FRegistration(req);
}

View File

@@ -19,12 +19,14 @@ import {
import { IDPInformation } from "@zitadel/proto/zitadel/user/v2/idp_pb";
import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb";
import type { RedirectURLs } from "@zitadel/proto/zitadel/user/v2/idp_pb";
import { PartialMessage, PlainMessage } from "@zitadel/client";
import { SearchQuery as UserSearchQuery } from "@zitadel/proto/zitadel/user/v2/query_pb";
import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb";
import {
SearchQuery,
SearchQuerySchema,
} from "@zitadel/proto/zitadel/user/v2/query_pb";
import { PROVIDER_MAPPING } from "./idp";
import { create } from "@zitadel/client";
import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb";
const SESSION_LIFETIME_S = 3000;
@@ -122,8 +124,8 @@ export async function getPasswordComplexitySettings(organization?: string) {
}
export async function createSessionFromChecks(
checks: PlainMessage<Checks>,
challenges: PlainMessage<RequestChallenges> | undefined,
checks: Checks,
challenges: RequestChallenges | undefined,
) {
return sessionService.createSession(
{
@@ -166,7 +168,7 @@ export async function setSession(
sessionId: string,
sessionToken: string,
challenges: RequestChallenges | undefined,
checks?: PlainMessage<Checks>,
checks?: Checks,
) {
return sessionService.setSession(
{
@@ -249,48 +251,49 @@ export async function listUsers({
email?: string;
organizationId?: string;
}) {
const queries: PartialMessage<UserSearchQuery>[] = [];
const queries: SearchQuery[] = [];
if (userName) {
queries.push({
query: {
case: "userNameQuery",
value: {
userName,
method: TextQueryMethod.EQUALS,
queries.push(
create(SearchQuerySchema, {
query: {
case: "userNameQuery",
value: {
userName,
method: TextQueryMethod.EQUALS,
},
},
},
});
}),
);
}
if (organizationId) {
queries.push({
query: {
case: "organizationIdQuery",
value: {
organizationId,
queries.push(
create(SearchQuerySchema, {
query: {
case: "organizationIdQuery",
value: {
organizationId,
},
},
},
});
}),
);
}
if (email) {
queries.push({
query: {
case: "emailQuery",
value: {
emailAddress: email,
queries.push(
create(SearchQuerySchema, {
query: {
case: "emailQuery",
value: {
emailAddress: email,
},
},
},
});
}),
);
}
return userService.listUsers(
{
queries: queries,
},
{},
);
return userService.listUsers({ queries: queries });
}
export async function getOrgsByDomain(domain: string) {
@@ -314,7 +317,7 @@ export async function startIdentityProviderFlow({
urls,
}: {
idpId: string;
urls: PlainMessage<RedirectURLs>;
urls: RedirectURLsJson;
}) {
return userService.startIdentityProviderIntent({
idpId,
@@ -345,7 +348,7 @@ export async function getAuthRequest({
});
}
export async function createCallback(req: PlainMessage<CreateCallbackRequest>) {
export async function createCallback(req: CreateCallbackRequest) {
return oidcService.createCallback(req);
}
@@ -478,7 +481,7 @@ export async function registerU2F(userId: string, domain: string) {
* @returns the newly set email
*/
export async function verifyU2FRegistration(
request: PlainMessage<VerifyU2FRegistrationRequest>,
request: VerifyU2FRegistrationRequest,
) {
return userService.verifyU2FRegistration(request, {});
}
@@ -496,10 +499,8 @@ export async function getActiveIdentityProviders(orgId?: string) {
* @returns the newly set email
*/
export async function verifyPasskeyRegistration(
request: PartialMessage<VerifyPasskeyRegistrationRequest>,
request: VerifyPasskeyRegistrationRequest,
) {
// TODO: find a better way to handle this
request = VerifyPasskeyRegistrationRequest.fromJson(request as any);
return userService.verifyPasskeyRegistration(request, {});
}

View File

@@ -13,15 +13,8 @@ export default function DynamicTheme({
children: React.ReactNode;
branding?: BrandingSettings;
}) {
let partial: Partial<BrandingSettings> | undefined;
if (branding) {
partial = {
lightTheme: branding?.lightTheme,
darkTheme: branding?.darkTheme,
};
}
return (
<ThemeWrapper branding={partial}>
<ThemeWrapper branding={branding}>
{/* <ThemeProvider> */}
<LayoutProviders>
<div className="rounded-lg bg-vc-border-gradient dark:bg-dark-vc-border-gradient p-px shadow-lg shadow-black/5 dark:shadow-black/20 mb-10">

View File

@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
import { Spinner } from "./Spinner";
import Alert from "./Alert";
import { useRouter } from "next/navigation";
import { createNewSession } from "@/lib/server/session";
type Props = {
userId: string;
@@ -15,66 +16,54 @@ type Props = {
authRequestId?: string;
};
export default function IdpSignin(props: Props) {
export default function IdpSignin({
userId,
idpIntent: { idpIntentId, idpIntentToken },
authRequestId,
}: Props) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
async function createSessionForIdp() {
setLoading(true);
const res = await fetch("/api/session", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: props.userId,
idpIntent: props.idpIntent,
authRequestId: props.authRequestId,
// organization: props.organization,
}),
});
if (!res.ok) {
const error = await res.json();
throw error.details.details;
}
return res.json();
}
useEffect(() => {
createSessionForIdp()
createNewSession({
userId,
idpIntent: {
idpIntentId,
idpIntentToken,
},
authRequestId,
// organization: props.organization,
})
.then((session) => {
setLoading(false);
if (props.authRequestId && session && session.sessionId) {
if (authRequestId && session && session.id) {
return router.push(
`/login?` +
new URLSearchParams({
sessionId: session.sessionId,
authRequest: props.authRequestId,
sessionId: session.id,
authRequest: authRequestId,
}),
);
} else {
return router.push(
`/signedin?` +
new URLSearchParams(
props.authRequestId
? {
loginName: session.factors.user.loginName,
authRequestId: props.authRequestId,
}
: {
loginName: session.factors.user.loginName,
},
),
);
const params = new URLSearchParams({});
if (session.factors?.user?.loginName) {
params.set("loginName", session.factors?.user?.loginName);
}
if (authRequestId) {
params.set("authRequestId", authRequestId);
}
return router.push(`/signedin?` + params);
}
})
.catch((error) => {
setLoading(false);
setError(error.message);
return;
});
setLoading(false);
}, []);
return (

View File

@@ -8,9 +8,8 @@ import { Spinner } from "./Spinner";
import { useForm } from "react-hook-form";
import { TextInput } from "./Input";
import BackButton from "./BackButton";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { PlainMessage } from "@zitadel/client";
import { Challenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { ChecksJson } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { ChallengesJson } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
// either loginName or sessionId must be provided
type Props = {
@@ -64,7 +63,7 @@ export default function LoginOTP({
}, []);
async function updateSessionForOTPChallenge() {
const challenges: PlainMessage<Challenges> = {};
const challenges: ChallengesJson = {};
if (method === "email") {
challenges.otpEmail = "";
@@ -112,7 +111,7 @@ export default function LoginOTP({
body.authRequestId = authRequestId;
}
const checks: PlainMessage<Checks> = {};
const checks: ChecksJson = {};
if (method === "sms") {
checks.otpSms = { code: values.code };
}

View File

@@ -8,7 +8,12 @@ import Alert from "./Alert";
import { Spinner } from "./Spinner";
import BackButton from "./BackButton";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { updateSession } from "@/lib/server/session";
import {
RequestChallengesSchema,
UserVerificationRequirement,
} from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { create } from "@zitadel/client";
// either loginName or sessionId must be provided
type Props = {
@@ -43,8 +48,8 @@ export default function LoginPasskey({
updateSessionForChallenge()
.then((response) => {
const pK =
response.challenges.webAuthN.publicKeyCredentialRequestOptions
.publicKey;
response?.challenges?.webAuthN?.publicKeyCredentialRequestOptions
?.publicKey;
if (pK) {
submitLoginAndContinue(pK)
.then(() => {
@@ -67,65 +72,46 @@ export default function LoginPasskey({
}, []);
async function updateSessionForChallenge(
userVerificationRequirement: number = login ? 1 : 3,
userVerificationRequirement: number = login
? UserVerificationRequirement.REQUIRED
: UserVerificationRequirement.DISCOURAGED,
) {
setLoading(true);
const res = await fetch("/api/session", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
loginName,
sessionId,
organization,
challenges: RequestChallenges.fromJson({
webAuthN: {
domain: "",
// USER_VERIFICATION_REQUIREMENT_UNSPECIFIED = 0;
// USER_VERIFICATION_REQUIREMENT_REQUIRED = 1; - passkey login
// USER_VERIFICATION_REQUIREMENT_PREFERRED = 2;
// USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 3; - mfa
userVerificationRequirement: userVerificationRequirement,
},
}),
authRequestId,
const session = await updateSession({
loginName,
sessionId,
organization,
challenges: create(RequestChallengesSchema, {
webAuthN: {
domain: "",
userVerificationRequirement,
},
}),
authRequestId,
}).catch((error: Error) => {
setError(error.message);
});
setLoading(false);
if (!res.ok) {
const error = await res.json();
throw error.details.details;
}
return res.json();
return session;
}
async function submitLogin(data: any) {
setLoading(true);
const res = await fetch("/api/session", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
loginName,
sessionId,
organization,
checks: {
webAuthN: { credentialAssertionData: data },
} as Checks,
authRequestId,
}),
const response = await updateSession({
loginName,
sessionId,
organization,
checks: {
webAuthN: { credentialAssertionData: data },
} as Checks,
authRequestId,
}).catch((error: Error) => {
setError(error.message);
});
const response = await res.json();
setLoading(false);
if (!res.ok) {
setError(response.details);
return Promise.reject(response.details);
}
return response;
}
@@ -184,19 +170,16 @@ export default function LoginPasskey({
}),
);
} else {
return router.push(
`/signedin?` +
new URLSearchParams(
authRequestId
? {
loginName: resp.factors.user.loginName,
authRequestId,
}
: {
loginName: resp.factors.user.loginName,
},
),
);
const params = new URLSearchParams({});
if (authRequestId) {
params.set("authRequestId", authRequestId);
}
if (resp?.factors?.user?.loginName) {
params.set("loginName", resp.factors.user.loginName);
}
return router.push(`/signedin?` + params);
}
});
} else {

View File

@@ -9,8 +9,15 @@ 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,
ChecksSchema,
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { create } from "@zitadel/client";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { updateSession } from "@/lib/server/session";
import { resetPassword } from "@/lib/server/password";
type Inputs = {
password: string;
@@ -18,7 +25,7 @@ type Inputs = {
type Props = {
loginSettings: LoginSettings | undefined;
loginName?: string;
loginName: string;
organization?: string;
authRequestId?: string;
isAlternative?: boolean; // whether password was requested as alternative auth method
@@ -47,175 +54,167 @@ 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: create(ChecksSchema, {
password: { password: values.password },
}),
authRequestId,
}).catch((error: Error) => {
setError(error.message ?? "Could not verify password");
setLoading(false);
});
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;
}
async function resetPassword() {
async function resetPasswordAndContinue() {
setError("");
setLoading(true);
const res = await fetch("/api/resetpassword", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
loginName,
organization,
authRequestId,
}),
const response = await resetPassword({
loginName,
organization,
}).catch((error: Error) => {
setLoading(false);
setError(error.message ?? "Could not reset password");
});
const response = await res.json();
setLoading(false);
if (!res.ok) {
console.log(response.details.details);
setError(response.details?.details ?? "Could not verify password");
return Promise.reject(response.details);
}
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 ||
!submitted.factors?.user?.loginName
) {
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.webAuthN && // 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 (
@@ -230,7 +229,7 @@ export default function PasswordForm({
/>
<button
className="transition-all text-sm hover:text-primary-light-500 dark:hover:text-primary-dark-500"
onClick={() => resetPassword()}
onClick={() => resetPasswordAndContinue()}
type="button"
disabled={loading}
>

View File

@@ -13,7 +13,7 @@ import AuthenticationMethodRadio, {
import Alert from "./Alert";
import BackButton from "./BackButton";
import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb";
import { first } from "node_modules/cypress/types/lodash";
import { registerUser } from "@/lib/server/register";
type Inputs =
| {
@@ -57,24 +57,19 @@ export default function RegisterFormWithoutPassword({
async function submitAndRegister(values: Inputs) {
setLoading(true);
const res = await fetch("/api/registeruser", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: values.email,
firstName: values.firstname,
lastName: values.lastname,
organization: organization,
}),
const response = await registerUser({
email: values.email,
firstName: values.firstname,
lastName: values.lastname,
organization: organization,
}).catch((error) => {
setError(error.message ?? "Could not register user");
setLoading(false);
});
setLoading(false);
if (!res.ok) {
const error = await res.json();
throw new Error(error.details);
}
return res.json();
return response;
}
async function submitAndContinue(
@@ -91,28 +86,28 @@ export default function RegisterFormWithoutPassword({
registerParams.authRequestId = authRequestId;
}
return withPassword
? router.push(`/register?` + new URLSearchParams(registerParams))
: submitAndRegister(value)
.then((session) => {
setError("");
if (withPassword) {
return router.push(`/register?` + new URLSearchParams(registerParams));
} else {
const session = await submitAndRegister(value).catch((error) => {
setError(error.message ?? "Could not register user");
});
const params: any = { loginName: session.factors.user.loginName };
const params = new URLSearchParams({});
if (session?.factors?.user?.loginName) {
params.set("loginName", session.factors?.user?.loginName);
}
if (organization) {
params.organization = organization;
}
if (organization) {
params.set("organization", organization);
}
if (authRequestId) {
params.authRequestId = authRequestId;
}
if (authRequestId) {
params.set("authRequestId", authRequestId);
}
return router.push(`/passkey/add?` + new URLSearchParams(params));
})
.catch((errorDetails: Error) => {
setLoading(false);
setError(errorDetails.message);
});
return router.push(`/passkey/add?` + new URLSearchParams(params));
}
}
const { errors } = formState;

View File

@@ -9,6 +9,7 @@ import Alert from "./Alert";
import { coerceToArrayBuffer, coerceToBase64Url } from "@/utils/base64";
import BackButton from "./BackButton";
import { RegisterPasskeyResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { registerPasskeyLink, verifyPasskey } from "@/lib/server/passkeys";
type Inputs = {};
@@ -25,7 +26,7 @@ export default function RegisterPasskey({
organization,
authRequestId,
}: Props) {
const { register, handleSubmit, formState } = useForm<Inputs>({
const { handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur",
});
@@ -35,29 +36,6 @@ export default function RegisterPasskey({
const router = useRouter();
async function submitRegister() {
setError("");
setLoading(true);
const res = await fetch("/api/passkeys", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
sessionId,
}),
});
const response = await res.json();
setLoading(false);
if (!res.ok) {
setError(response.details);
return Promise.reject(response.details);
}
return response;
}
async function submitVerify(
passkeyId: string,
passkeyName: string,
@@ -65,121 +43,120 @@ export default function RegisterPasskey({
sessionId: string,
) {
setLoading(true);
const res = await fetch("/api/passkeys/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
passkeyId,
passkeyName,
publicKeyCredential,
sessionId,
}),
const response = await verifyPasskey({
passkeyId,
passkeyName,
publicKeyCredential,
sessionId,
}).catch((error: Error) => {
setError(error.message);
setLoading(false);
});
const response = await res.json();
setLoading(false);
if (!res.ok) {
setError(response.details);
return Promise.reject(response.details);
}
return response;
}
function submitRegisterAndContinue(value: Inputs): Promise<boolean | void> {
return submitRegister().then((resp: RegisterPasskeyResponse) => {
const passkeyId = resp.passkeyId;
const options: CredentialCreationOptions =
(resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ??
{};
if (options?.publicKey) {
options.publicKey.challenge = coerceToArrayBuffer(
options.publicKey.challenge,
"challenge",
);
options.publicKey.user.id = coerceToArrayBuffer(
options.publicKey.user.id,
"userid",
);
if (options.publicKey.excludeCredentials) {
options.publicKey.excludeCredentials.map((cred: any) => {
cred.id = coerceToArrayBuffer(
cred.id as string,
"excludeCredentials.id",
);
return cred;
});
}
navigator.credentials
.create(options)
.then((resp) => {
if (
resp &&
(resp as any).response.attestationObject &&
(resp as any).response.clientDataJSON &&
(resp as any).rawId
) {
const attestationObject = (resp as any).response
.attestationObject;
const clientDataJSON = (resp as any).response.clientDataJSON;
const rawId = (resp as any).rawId;
const data = {
id: resp.id,
rawId: coerceToBase64Url(rawId, "rawId"),
type: resp.type,
response: {
attestationObject: coerceToBase64Url(
attestationObject,
"attestationObject",
),
clientDataJSON: coerceToBase64Url(
clientDataJSON,
"clientDataJSON",
),
},
};
return submitVerify(passkeyId, "", data, sessionId).then(() => {
const params = new URLSearchParams();
if (organization) {
params.set("organization", organization);
}
if (authRequestId) {
params.set("authRequestId", authRequestId);
params.set("sessionId", sessionId);
// params.set("altPassword", ${false}); // without setting altPassword this does not allow password
// params.set("loginName", resp.loginName);
router.push("/passkey/login?" + params);
} else {
router.push("/accounts?" + params);
}
});
} else {
setLoading(false);
setError("An error on registering passkey");
return null;
}
})
.catch((error) => {
console.error(error);
setLoading(false);
setError(error);
return null;
});
}
async function submitRegisterAndContinue(): Promise<boolean | void> {
setLoading(true);
const resp = await registerPasskeyLink({
sessionId,
}).catch((error: Error) => {
setError(error.message ?? "Could not register passkey");
setLoading(false);
});
}
setLoading(false);
const { errors } = formState;
if (!resp) {
setError("An error on registering passkey");
return;
}
const passkeyId = resp.passkeyId;
const options: CredentialCreationOptions =
(resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ??
{};
if (!options.publicKey) {
setError("An error on registering passkey");
return;
}
options.publicKey.challenge = coerceToArrayBuffer(
options.publicKey.challenge,
"challenge",
);
options.publicKey.user.id = coerceToArrayBuffer(
options.publicKey.user.id,
"userid",
);
if (options.publicKey.excludeCredentials) {
options.publicKey.excludeCredentials.map((cred: any) => {
cred.id = coerceToArrayBuffer(
cred.id as string,
"excludeCredentials.id",
);
return cred;
});
}
const credentials = await navigator.credentials.create(options);
if (
!credentials ||
!(credentials as any).response?.attestationObject ||
!(credentials as any).response?.clientDataJSON ||
!(credentials as any).rawId
) {
setError("An error on registering passkey");
return;
}
const attestationObject = (credentials as any).response.attestationObject;
const clientDataJSON = (credentials as any).response.clientDataJSON;
const rawId = (credentials as any).rawId;
const data = {
id: credentials.id,
rawId: coerceToBase64Url(rawId, "rawId"),
type: credentials.type,
response: {
attestationObject: coerceToBase64Url(
attestationObject,
"attestationObject",
),
clientDataJSON: coerceToBase64Url(clientDataJSON, "clientDataJSON"),
},
};
const verificationResponse = await submitVerify(
passkeyId,
"",
data,
sessionId,
);
if (!verificationResponse) {
setError("Could not verify Passkey!");
return;
}
const params = new URLSearchParams();
if (organization) {
params.set("organization", organization);
}
if (authRequestId) {
params.set("authRequestId", authRequestId);
params.set("sessionId", sessionId);
// params.set("altPassword", ${false}); // without setting altPassword this does not allow password
// params.set("loginName", resp.loginName);
router.push("/passkey/login?" + params);
} else {
router.push("/accounts?" + params);
}
}
return (
<form className="w-full">

View File

@@ -9,6 +9,7 @@ import Alert from "./Alert";
import { coerceToArrayBuffer, coerceToBase64Url } from "@/utils/base64";
import BackButton from "./BackButton";
import { RegisterU2FResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { addU2F, verifyU2F } from "@/lib/server/u2f";
type Inputs = {};
@@ -23,39 +24,12 @@ export default function RegisterU2F({
organization,
authRequestId,
}: Props) {
const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur",
});
const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const router = useRouter();
async function submitRegister() {
setError("");
setLoading(true);
const res = await fetch("/api/u2f", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
sessionId,
}),
});
const response = await res.json();
setLoading(false);
if (!res.ok) {
setError(response.details);
return Promise.reject(response.details);
}
return response;
}
async function submitVerify(
u2fId: string,
passkeyName: string,
@@ -63,120 +37,119 @@ export default function RegisterU2F({
sessionId: string,
) {
setLoading(true);
const res = await fetch("/api/u2f/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
u2fId,
passkeyName,
publicKeyCredential,
sessionId,
}),
const response = await verifyU2F({
u2fId,
passkeyName,
publicKeyCredential,
sessionId,
}).catch((error: Error) => {
setLoading(false);
setError(error.message);
});
const response = await res.json();
setLoading(false);
if (!res.ok) {
setError(response.details);
return Promise.reject(response.details);
}
return response;
}
function submitRegisterAndContinue(value: Inputs): Promise<boolean | void> {
return submitRegister().then((resp: RegisterU2FResponse) => {
const u2fId = resp.u2fId;
const options: CredentialCreationOptions =
(resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ??
{};
if (options.publicKey) {
options.publicKey.challenge = coerceToArrayBuffer(
options.publicKey.challenge,
"challenge",
);
options.publicKey.user.id = coerceToArrayBuffer(
options.publicKey.user.id,
"userid",
);
if (options.publicKey.excludeCredentials) {
options.publicKey.excludeCredentials.map((cred: any) => {
cred.id = coerceToArrayBuffer(
cred.id as string,
"excludeCredentials.id",
);
return cred;
});
}
navigator.credentials
.create(options)
.then((resp) => {
if (
resp &&
(resp as any).response.attestationObject &&
(resp as any).response.clientDataJSON &&
(resp as any).rawId
) {
const attestationObject = (resp as any).response
.attestationObject;
const clientDataJSON = (resp as any).response.clientDataJSON;
const rawId = (resp as any).rawId;
const data = {
id: resp.id,
rawId: coerceToBase64Url(rawId, "rawId"),
type: resp.type,
response: {
attestationObject: coerceToBase64Url(
attestationObject,
"attestationObject",
),
clientDataJSON: coerceToBase64Url(
clientDataJSON,
"clientDataJSON",
),
},
};
return submitVerify(u2fId, "", data, sessionId).then(() => {
const params = new URLSearchParams();
if (organization) {
params.set("organization", organization);
}
if (authRequestId) {
params.set("authRequestId", authRequestId);
params.set("sessionId", sessionId);
// params.set("altPassword", ${false}); // without setting altPassword this does not allow password
// params.set("loginName", resp.loginName);
router.push("/u2f?" + params);
} else {
router.push("/accounts?" + params);
}
});
} else {
setLoading(false);
setError("An error on registering passkey");
return null;
}
})
.catch((error) => {
console.error(error);
setLoading(false);
setError(error);
return null;
});
}
async function submitRegisterAndContinue(): Promise<boolean | void> {
setError("");
setLoading(true);
const response = await addU2F({
sessionId,
}).catch((error) => {
setLoading(false);
setError(error.message);
});
}
const { errors } = formState;
if (!response) {
setLoading(false);
setError("An error on registering passkey");
return;
}
const u2fId = response?.u2fId;
const options: CredentialCreationOptions =
(response?.publicKeyCredentialCreationOptions as CredentialCreationOptions) ??
{};
if (options.publicKey) {
options.publicKey.challenge = coerceToArrayBuffer(
options.publicKey.challenge,
"challenge",
);
options.publicKey.user.id = coerceToArrayBuffer(
options.publicKey.user.id,
"userid",
);
if (options.publicKey.excludeCredentials) {
options.publicKey.excludeCredentials.map((cred: any) => {
cred.id = coerceToArrayBuffer(
cred.id as string,
"excludeCredentials.id",
);
return cred;
});
}
const resp = await navigator.credentials.create(options);
if (
!resp ||
!(resp as any).response.attestationObject ||
!(resp as any).response.clientDataJSON ||
!(resp as any).rawId
) {
setError("An error on registering passkey");
setLoading(false);
return;
}
const attestationObject = (resp as any).response.attestationObject;
const clientDataJSON = (resp as any).response.clientDataJSON;
const rawId = (resp as any).rawId;
const data = {
id: resp.id,
rawId: coerceToBase64Url(rawId, "rawId"),
type: resp.type,
response: {
attestationObject: coerceToBase64Url(
attestationObject,
"attestationObject",
),
clientDataJSON: coerceToBase64Url(clientDataJSON, "clientDataJSON"),
},
};
const submitResponse = await submitVerify(u2fId, "", data, sessionId);
if (!submitResponse) {
setLoading(false);
setError("An error on verifying passkey");
return;
}
const params = new URLSearchParams();
if (organization) {
params.set("organization", organization);
}
if (authRequestId) {
params.set("authRequestId", authRequestId);
params.set("sessionId", sessionId);
// params.set("altPassword", ${false}); // without setting altPassword this does not allow password
// params.set("loginName", resp.loginName);
router.push("/u2f?" + params);
} else {
router.push("/accounts?" + params);
}
}
setLoading(false);
}
return (
<form className="w-full">
@@ -194,8 +167,8 @@ export default function RegisterU2F({
type="submit"
className="self-end"
variant={ButtonVariants.Primary}
disabled={loading || !formState.isValid}
onClick={handleSubmit(submitRegisterAndContinue)}
disabled={loading}
onClick={submitRegisterAndContinue}
>
{loading && <Spinner className="h-5 w-5 mr-2" />}
continue

View File

@@ -6,6 +6,9 @@ import { Avatar } from "./Avatar";
import moment from "moment";
import { XCircleIcon } from "@heroicons/react/24/outline";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { timestampDate } from "@zitadel/client";
import { deleteSession } from "@/lib/zitadel";
import { cleanupSession } from "@/lib/server/session";
export default function SessionItem({
session,
@@ -16,42 +19,31 @@ export default function SessionItem({
reload: () => void;
authRequestId?: string;
}) {
// TODO: remove casting when bufbuild/protobuf-es@v2 is released
session = Session.fromJson(session as any);
const [loading, setLoading] = useState<boolean>(false);
async function clearSession(id: string) {
setLoading(true);
const res = await fetch("/api/session?" + new URLSearchParams({ id }), {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
id: id,
}),
const response = await cleanupSession({
sessionId: id,
}).catch((error) => {
setError(error.message);
});
const response = await res.json();
setLoading(false);
if (!res.ok) {
// setError(response.details);
return Promise.reject(response);
} else {
return response;
}
return response;
}
const validPassword = session?.factors?.password?.verifiedAt;
const validPasskey = session?.factors?.webAuthN?.verifiedAt;
const stillValid = session.expirationDate
? session.expirationDate.toDate() > new Date()
? timestampDate(session.expirationDate) > new Date()
: true;
const validDate = validPassword || validPasskey;
const validUser = (validPassword || validPasskey) && stillValid;
const [error, setError] = useState<string | null>(null);
return (
<Link
prefetch={false}
@@ -106,7 +98,7 @@ export default function SessionItem({
</span>
{validUser && (
<span className="text-xs opacity-80">
{validDate && moment(validDate.toDate()).fromNow()}
{validDate && moment(timestampDate(validDate)).fromNow()}
</span>
)}
</div>

View File

@@ -15,6 +15,7 @@ import { useRouter } from "next/navigation";
import { Spinner } from "./Spinner";
import Alert from "./Alert";
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
import { registerUser } from "@/lib/server/register";
type Inputs =
| {
@@ -56,52 +57,36 @@ export default function SetPasswordForm({
async function submitRegister(values: Inputs) {
setLoading(true);
const res = await fetch("/api/registeruser", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: email,
firstName: firstname,
lastName: lastname,
organization: organization,
authRequestId: authRequestId,
password: values.password,
}),
const response = await registerUser({
email: email,
firstName: firstname,
lastName: lastname,
organization: organization,
authRequestId: authRequestId,
password: values.password,
}).catch((error: Error) => {
setError(error.message ?? "Could not register user");
});
setLoading(false);
if (!res.ok) {
const error = await res.json();
throw new Error(error.details);
if (!response) {
setError("Could not register user");
return;
}
return res.json();
}
const params: any = { userId: response.userId };
function submitAndLink(value: Inputs): Promise<boolean | void> {
return submitRegister(value)
.then((registerResponse) => {
setError("");
if (authRequestId) {
params.authRequestId = authRequestId;
}
if (organization) {
params.organization = organization;
}
if (response && response.sessionId) {
params.sessionId = response.sessionId;
}
setLoading(false);
const params: any = { userId: registerResponse.userId };
if (authRequestId) {
params.authRequestId = authRequestId;
}
if (organization) {
params.organization = organization;
}
if (registerResponse && registerResponse.sessionId) {
params.sessionId = registerResponse.sessionId;
}
return router.push(`/verify?` + new URLSearchParams(params));
})
.catch((errorDetails: Error) => {
setLoading(false);
setError(errorDetails.message);
});
return router.push(`/verify?` + new URLSearchParams(params));
}
const { errors } = formState;
@@ -177,7 +162,7 @@ export default function SetPasswordForm({
!formState.isValid ||
watchPassword !== watchConfirmPassword
}
onClick={handleSubmit(submitAndLink)}
onClick={handleSubmit(submitRegister)}
>
{loading && <Spinner className="h-5 w-5 mr-2" />}
continue

View File

@@ -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;
@@ -27,11 +28,6 @@ export function SignInWithIDP({
authRequestId,
organization,
}: SignInWithIDPProps) {
// TODO: remove casting when bufbuild/protobuf-es@v2 is released
identityProviders = identityProviders.map((idp) =>
IdentityProvider.fromJson(idp as any),
);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const router = useRouter();
@@ -49,30 +45,32 @@ 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((error: Error) => {
setError(error.message ?? "Could not start IDP flow");
});
const response = await res.json();
setLoading(false);
if (!res.ok) {
setError(response.details);
return Promise.reject(response.details);
}
return response;
}
async function navigateToAuthUrl(id: string, type: IdentityProviderType) {
const startFlowResponse = await startFlow(id, idpTypeToSlug(type));
if (
startFlowResponse &&
startFlowResponse.nextStep.case === "authUrl" &&
startFlowResponse?.nextStep.value
) {
router.push(startFlowResponse.nextStep.value);
}
}
return (
<div className="flex flex-col w-full space-y-2 text-sm">
{identityProviders &&
@@ -83,12 +81,7 @@ export function SignInWithIDP({
<SignInWithGithub
key={`idp-${i}`}
onClick={() =>
startFlow(
idp.id,
idpTypeToSlug(IdentityProviderType.GITHUB),
).then(({ authUrl }) => {
router.push(authUrl);
})
navigateToAuthUrl(idp.id, IdentityProviderType.GITHUB)
}
></SignInWithGithub>
);
@@ -96,7 +89,9 @@ export function SignInWithIDP({
return (
<SignInWithGithub
key={`idp-${i}`}
onClick={() => alert("TODO: unimplemented")}
onClick={() =>
navigateToAuthUrl(idp.id, IdentityProviderType.GITHUB_ES)
}
></SignInWithGithub>
);
case IdentityProviderType.AZURE_AD:
@@ -104,12 +99,7 @@ export function SignInWithIDP({
<SignInWithAzureAD
key={`idp-${i}`}
onClick={() =>
startFlow(
idp.id,
idpTypeToSlug(IdentityProviderType.AZURE_AD),
).then(({ authUrl }) => {
router.push(authUrl);
})
navigateToAuthUrl(idp.id, IdentityProviderType.AZURE_AD)
}
></SignInWithAzureAD>
);
@@ -120,12 +110,7 @@ export function SignInWithIDP({
e2e="google"
name={idp.name}
onClick={() =>
startFlow(
idp.id,
idpTypeToSlug(IdentityProviderType.GOOGLE),
).then(({ authUrl }) => {
router.push(authUrl);
})
navigateToAuthUrl(idp.id, IdentityProviderType.GOOGLE)
}
></SignInWithGoogle>
);
@@ -133,14 +118,21 @@ export function SignInWithIDP({
return (
<SignInWithGitlab
key={`idp-${i}`}
onClick={() => alert("TODO: unimplemented")}
onClick={() =>
navigateToAuthUrl(idp.id, IdentityProviderType.GITLAB)
}
></SignInWithGitlab>
);
case IdentityProviderType.GITLAB_SELF_HOSTED:
return (
<SignInWithGitlab
key={`idp-${i}`}
onClick={() => alert("TODO: unimplemented")}
onClick={() =>
navigateToAuthUrl(
idp.id,
IdentityProviderType.GITLAB_SELF_HOSTED,
)
}
></SignInWithGitlab>
);
default:

View File

@@ -1,13 +1,12 @@
"use client";
import { setTheme } from "@/utils/colors";
import { useEffect } from "react";
import { ReactNode, useEffect } from "react";
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import { PartialMessage } from "@zitadel/client";
type Props = {
branding: PartialMessage<BrandingSettings> | undefined;
children: React.ReactNode;
branding: BrandingSettings | undefined;
children: ReactNode;
};
const ThemeWrapper = ({ children, branding }: Props) => {

View File

@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { ReactNode, useEffect, useState } from "react";
import { Button, ButtonVariants } from "./Button";
import { TextInput } from "./Input";
import { useForm } from "react-hook-form";
@@ -12,6 +12,8 @@ import {
PasskeysType,
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import BackButton from "./BackButton";
import { sendLoginname } from "@/lib/server/loginname";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
type Inputs = {
loginName: string;
@@ -24,7 +26,7 @@ type Props = {
organization?: string;
submit: boolean;
allowRegister: boolean;
children?: React.ReactNode;
children?: ReactNode;
};
export default function UsernameForm({
@@ -51,161 +53,140 @@ export default function UsernameForm({
async function submitLoginName(values: Inputs, organization?: string) {
setLoading(true);
let body: any = {
const res = await sendLoginname({
loginName: values.loginName,
};
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),
organization,
authRequestId,
}).catch((error: Error) => {
setError(error.message ?? "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) {

View File

@@ -7,6 +7,7 @@ import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
import { Spinner } from "./Spinner";
import Alert from "@/ui/Alert";
import { resendVerifyEmail, verifyUserByEmail } from "@/lib/server/email";
type Inputs = {
code: string;
@@ -50,71 +51,49 @@ export default function VerifyEmailForm({
const router = useRouter();
async function submitCode(values: Inputs) {
setLoading(true);
const res = await fetch("/api/verifyemail", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code: values.code,
userId,
organization,
}),
});
const response = await res.json();
setLoading(false);
if (!res.ok) {
setError(response.rawMessage);
return Promise.reject(response);
} else {
return response;
}
}
async function resendCode() {
setLoading(true);
const res = await fetch("/api/resendverifyemail", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId,
}),
const response = await resendVerifyEmail({
userId,
}).catch((error: Error) => {
setLoading(false);
setError(error.message);
});
const response = await res.json();
if (!res.ok) {
setLoading(false);
setError(response.details);
return Promise.reject(response);
} else {
setLoading(false);
return response;
}
setLoading(false);
return response;
}
function submitCodeAndContinue(value: Inputs): Promise<boolean | void> {
return submitCode(value).then((resp: any) => {
const params = new URLSearchParams({});
if (organization) {
params.set("organization", organization);
}
if (authRequestId && sessionId) {
params.set("authRequest", authRequestId);
params.set("sessionId", sessionId);
return router.push(`/login?` + params);
} else {
return router.push(`/loginname?` + params);
}
async function submitCodeAndContinue(value: Inputs): Promise<boolean | void> {
setLoading(true);
const verifyResponse = await verifyUserByEmail({
code: value.code,
userId,
}).catch((error: Error) => {
setLoading(false);
setError(error.message);
});
setLoading(false);
if (!verifyResponse) {
setError("Could not verify email");
return;
}
const params = new URLSearchParams({});
if (organization) {
params.set("organization", organization);
}
if (authRequestId && sessionId) {
params.set("authRequest", authRequestId);
params.set("sessionId", sessionId);
return router.push(`/login?` + params);
} else {
return router.push(`/loginname?` + params);
}
}
return (

View File

@@ -1,6 +1,5 @@
import tinycolor from "tinycolor2";
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import { PartialMessage } from "@zitadel/client";
export interface Color {
name: string;
@@ -69,10 +68,7 @@ type BrandingColors = {
};
};
export function setTheme(
document: any,
policy?: PartialMessage<BrandingSettings>,
) {
export function setTheme(document: any, policy?: BrandingSettings) {
const lP: BrandingColors = {
lightTheme: {
backgroundColor: policy?.lightTheme?.backgroundColor || BACKGROUND,

View File

@@ -12,8 +12,12 @@ import {
RequestChallenges,
} from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { PlainMessage } from "@zitadel/client";
import {
Checks,
ChecksSchema,
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { timestampDate, toDate } from "@zitadel/client";
import { create } from "@zitadel/client";
type CustomCookieData = {
id: string;
@@ -33,16 +37,18 @@ export async function createSessionAndUpdateCookie(
organization?: string,
authRequestId?: string,
) {
const createdSession = await createSessionFromChecks(
const checks = create(
ChecksSchema,
password
? {
user: { search: { case: "loginName", value: loginName } },
password: { password },
}
: { user: { search: { case: "loginName", value: loginName } } },
challenges,
);
const createdSession = await createSessionFromChecks(checks, challenges);
if (createdSession) {
return getSession(
createdSession.sessionId,
@@ -52,9 +58,9 @@ export async function createSessionAndUpdateCookie(
const sessionCookie: CustomCookieData = {
id: createdSession.sessionId,
token: createdSession.sessionToken,
creationDate: `${response.session.creationDate?.toDate().getTime() ?? ""}`,
expirationDate: `${response.session.expirationDate?.toDate().getTime() ?? ""}`,
changeDate: `${response.session.changeDate?.toDate().getTime() ?? ""}`,
creationDate: `${toDate(response.session.creationDate)?.getTime() ?? ""}`,
expirationDate: `${toDate(response.session.expirationDate)?.getTime() ?? ""}`,
changeDate: `${toDate(response.session.changeDate)?.getTime() ?? ""}`,
loginName: response.session.factors.user.loginName ?? "",
organization: response.session.factors.user.organizationId ?? "",
};
@@ -85,7 +91,8 @@ export async function createSessionForUserIdAndUpdateCookie(
challenges: RequestChallenges | undefined,
authRequestId: string | undefined,
): Promise<Session> {
const createdSession = await createSessionFromChecks(
const checks = create(
ChecksSchema,
password
? {
user: { search: { case: "userId", value: userId } },
@@ -93,8 +100,8 @@ export async function createSessionForUserIdAndUpdateCookie(
// totp: { code: totpCode },
}
: { user: { search: { case: "userId", value: userId } } },
challenges,
);
const createdSession = await createSessionFromChecks(checks, challenges);
if (createdSession) {
return getSession(
@@ -105,9 +112,15 @@ export async function createSessionForUserIdAndUpdateCookie(
const sessionCookie: CustomCookieData = {
id: createdSession.sessionId,
token: createdSession.sessionToken,
creationDate: `${response.session.creationDate?.toDate().getTime() ?? ""}`,
expirationDate: `${response.session.expirationDate?.toDate().getTime() ?? ""}`,
changeDate: `${response.session.changeDate?.toDate().getTime() ?? ""}`,
creationDate: response.session.creationDate
? `${timestampDate(response.session.creationDate).toDateString()}`
: "",
expirationDate: response.session.expirationDate
? `${timestampDate(response.session.expirationDate).toDateString()}`
: "",
changeDate: response.session.changeDate
? `${timestampDate(response.session.changeDate).toDateString()}`
: "",
loginName: response.session.factors.user.loginName ?? "",
};
@@ -155,9 +168,15 @@ export async function createSessionForIdpAndUpdateCookie(
const sessionCookie: CustomCookieData = {
id: createdSession.sessionId,
token: createdSession.sessionToken,
creationDate: `${response.session.creationDate?.toDate().getTime() ?? ""}`,
expirationDate: `${response.session.expirationDate?.toDate().getTime() ?? ""}`,
changeDate: `${response.session.changeDate?.toDate().getTime() ?? ""}`,
creationDate: response.session.creationDate
? `${timestampDate(response.session.creationDate).toDateString()}`
: "",
expirationDate: response.session.expirationDate
? `${timestampDate(response.session.expirationDate).toDateString()}`
: "",
changeDate: response.session.changeDate
? `${timestampDate(response.session.changeDate).toDateString()}`
: "",
loginName: response.session.factors.user.loginName ?? "",
organization: response.session.factors.user.organizationId ?? "",
};
@@ -188,9 +207,9 @@ export type SessionWithChallenges = Session & {
export async function setSessionAndUpdateCookie(
recentCookie: CustomCookieData,
checks: PlainMessage<Checks>,
challenges: RequestChallenges | undefined,
authRequestId: string | undefined,
checks?: Checks,
challenges?: RequestChallenges,
authRequestId?: string,
) {
return setSession(
recentCookie.id,
@@ -204,7 +223,10 @@ export async function setSessionAndUpdateCookie(
token: updatedSession.sessionToken,
creationDate: recentCookie.creationDate,
expirationDate: recentCookie.expirationDate,
changeDate: `${updatedSession.details?.changeDate?.toDate().getTime() ?? ""}`,
// just overwrite the changeDate with the new one
changeDate: updatedSession.details?.changeDate
? `${timestampDate(updatedSession.details.changeDate).toDateString()}`
: "",
loginName: recentCookie.loginName,
organization: recentCookie.organization,
};
@@ -222,7 +244,10 @@ export async function setSessionAndUpdateCookie(
token: updatedSession.sessionToken,
creationDate: sessionCookie.creationDate,
expirationDate: sessionCookie.expirationDate,
changeDate: `${session.changeDate?.toDate().getTime() ?? ""}`,
// just overwrite the changeDate with the new one
changeDate: updatedSession.details?.changeDate
? `${timestampDate(updatedSession.details.changeDate).toDateString()}`
: "",
loginName: session.factors?.user?.loginName ?? "",
organization: session.factors?.user?.organizationId ?? "",
};

View File

@@ -45,8 +45,8 @@
},
"dependencies": {
"@zitadel/proto": "workspace:*",
"@bufbuild/protobuf": "^1.10.0",
"@connectrpc/connect": "^1.4.0"
"@bufbuild/protobuf": "^2.0.0",
"@connectrpc/connect": "2.0.0-alpha.1"
},
"devDependencies": {
"@zitadel/tsconfig": "workspace:*",

View File

@@ -1,8 +1,11 @@
import type { DescService } from "@bufbuild/protobuf";
import { Timestamp, timestampDate } from "@bufbuild/protobuf/wkt";
import { createPromiseClient, Transport } from "@connectrpc/connect";
import type { ServiceType } from "@bufbuild/protobuf";
export function createClientFor<TService extends ServiceType>(
service: TService,
) {
export function createClientFor<TService extends DescService>(service: TService) {
return (transport: Transport) => createPromiseClient(service, transport);
}
export function toDate(timestamp: Timestamp | undefined): Date | undefined {
return timestamp ? timestampDate(timestamp) : undefined;
}

View File

@@ -1,2 +1,7 @@
export { toDate } from "./helpers";
export { NewAuthorizationBearerInterceptor } from "./interceptors";
export type { PartialMessage, PlainMessage } from "@bufbuild/protobuf";
// TODO: Move this to `./protobuf.ts` and export it from there
export { create, fromJson, toJson } from "@bufbuild/protobuf";
export { TimestampSchema, timestampDate } from "@bufbuild/protobuf/wkt";
export type { Timestamp } from "@bufbuild/protobuf/wkt";

View File

@@ -1,31 +1,28 @@
import { describe, expect, test, vitest } from "vitest";
import { Int32Value, MethodKind, StringValue } from "@bufbuild/protobuf";
import { Int32Value, Int32ValueSchema, StringValueSchema } from "@bufbuild/protobuf/wkt";
import { createRouterTransport, HandlerContext } from "@connectrpc/connect";
import { describe, expect, test, vitest } from "vitest";
import { NewAuthorizationBearerInterceptor } from "./interceptors";
const TestService = {
typeName: "handwritten.TestService",
methods: {
unary: {
name: "Unary",
I: Int32Value,
O: StringValue,
kind: MethodKind.Unary,
input: Int32ValueSchema,
output: StringValueSchema,
methodKind: "unary",
},
},
} as const;
describe("NewAuthorizationBearerInterceptor", () => {
describe.skip("NewAuthorizationBearerInterceptor", () => {
const transport = {
interceptors: [NewAuthorizationBearerInterceptor("mytoken")],
};
test("injects the authorization token", async () => {
const handler = vitest.fn(
(request: Int32Value, context: HandlerContext) => {
return { value: request.value.toString() };
},
);
const handler = vitest.fn((request: Int32Value, context: HandlerContext) => {
return { value: request.value.toString() };
});
const service = createRouterTransport(
({ service }) => {
@@ -34,27 +31,16 @@ describe("NewAuthorizationBearerInterceptor", () => {
{ transport },
);
await service.unary(
TestService,
TestService.methods.unary,
undefined,
undefined,
{},
{ value: 9001 },
);
await service.unary(TestService, TestService.methods.unary, undefined, undefined, {}, { value: 9001 });
expect(handler).toBeCalled();
expect(handler.mock.calls[0][1].requestHeader.get("Authorization")).toBe(
"Bearer mytoken",
);
expect(handler.mock.calls[0][1].requestHeader.get("Authorization")).toBe("Bearer mytoken");
});
test("do not overwrite the previous authorization token", async () => {
const handler = vitest.fn(
(request: Int32Value, context: HandlerContext) => {
return { value: request.value.toString() };
},
);
const handler = vitest.fn((request: Int32Value, context: HandlerContext) => {
return { value: request.value.toString() };
});
const service = createRouterTransport(
({ service }) => {
@@ -64,7 +50,6 @@ describe("NewAuthorizationBearerInterceptor", () => {
);
await service.unary(
TestService,
TestService.methods.unary,
undefined,
undefined,
@@ -73,8 +58,6 @@ describe("NewAuthorizationBearerInterceptor", () => {
);
expect(handler).toBeCalled();
expect(handler.mock.calls[0][1].requestHeader.get("Authorization")).toBe(
"Bearer somethingelse",
);
expect(handler.mock.calls[0][1].requestHeader.get("Authorization")).toBe("Bearer somethingelse");
});
});

View File

@@ -1,9 +1,9 @@
import { createClientFor } from "./helpers";
import { AdminService } from "@zitadel/proto/zitadel/admin_connect";
import { AuthService } from "@zitadel/proto/zitadel/auth_connect";
import { ManagementService } from "@zitadel/proto/zitadel/management_connect";
import { SystemService } from "@zitadel/proto/zitadel/system_connect";
import { AdminService } from "@zitadel/proto/zitadel/admin_pb";
import { AuthService } from "@zitadel/proto/zitadel/auth_pb";
import { ManagementService } from "@zitadel/proto/zitadel/management_pb";
import { SystemService } from "@zitadel/proto/zitadel/system_pb";
export const createAdminServiceClient = createClientFor(AdminService);
export const createAuthServiceClient = createClientFor(AuthService);

View File

@@ -1,11 +1,11 @@
import { FeatureService } from "@zitadel/proto/zitadel/feature/v2/feature_service_connect";
import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_connect";
import { FeatureService } from "@zitadel/proto/zitadel/feature/v2/feature_service_pb";
import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb";
import { RequestContext } from "@zitadel/proto/zitadel/object/v2/object_pb";
import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_connect";
import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_connect";
import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_connect";
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_connect";
import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_connect";
import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_pb";
import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { createClientFor } from "./helpers";

View File

@@ -1,5 +1,5 @@
import { ZITADELUsers } from "@zitadel/proto/zitadel/resources/user/v3alpha/user_service_connect";
import { ZITADELUserSchemas } from "@zitadel/proto/zitadel/resources/userschema/v3alpha/user_schema_service_connect.js";
import { ZITADELUsers } from "@zitadel/proto/zitadel/resources/user/v3alpha/user_service_pb";
import { ZITADELUserSchemas } from "@zitadel/proto/zitadel/resources/userschema/v3alpha/user_schema_service_pb";
import { createClientFor } from "./helpers";
export const createUserSchemaServiceClient = createClientFor(ZITADELUserSchemas);

View File

@@ -1,12 +1,15 @@
{
"name": "@zitadel/next",
"version": "0.0.0",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"sideEffects": false,
"license": "MIT",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist/**"
],

View File

@@ -4,7 +4,5 @@ export type ZitadelNextProps = {
};
export function ZitadelNextProvider({ dark, children }: ZitadelNextProps) {
return (
<div className={`${dark ? "ztdl-dark" : "ztdl-light"} `}>{children}</div>
);
return <div className={`${dark ? "ztdl-dark" : "ztdl-light"} `}>{children}</div>;
}

View File

@@ -31,8 +31,8 @@
"@zitadel/client": "workspace:*"
},
"dependencies": {
"@connectrpc/connect-node": "^1.4.0",
"@connectrpc/connect-web": "^1.4.0",
"@connectrpc/connect-node": "^2.0.0-alpha.1",
"@connectrpc/connect-web": "^2.0.0-alpha.1",
"jose": "^5.3.0"
},
"devDependencies": {

View File

@@ -1,8 +1,5 @@
import { createGrpcTransport, GrpcTransportOptions } from "@connectrpc/connect-node";
import { NewAuthorizationBearerInterceptor } from "@zitadel/client";
import {
createGrpcTransport,
GrpcTransportOptions,
} from "@connectrpc/connect-node";
import { importPKCS8, SignJWT } from "jose";
/**
@@ -10,16 +7,10 @@ import { importPKCS8, SignJWT } from "jose";
* @param token
* @param opts
*/
export function createServerTransport(
token: string,
opts: GrpcTransportOptions,
) {
export function createServerTransport(token: string, opts: GrpcTransportOptions) {
return createGrpcTransport({
...opts,
interceptors: [
...(opts.interceptors || []),
NewAuthorizationBearerInterceptor(token),
],
interceptors: [...(opts.interceptors || []), NewAuthorizationBearerInterceptor(token)],
});
}

View File

@@ -1 +1,4 @@
zitadel
zitadel
google
protoc-gen-openapiv2
validate

View File

@@ -2,7 +2,8 @@ version: v2
managed:
enabled: true
plugins:
- remote: buf.build/connectrpc/es:v1.4.0
out: .
- remote: buf.build/bufbuild/es:v1.7.2
- remote: buf.build/bufbuild/es:v2.0.0
out: .
include_imports: true
opt:
- json_types=true

View File

@@ -15,7 +15,7 @@
"clean": "rm -rf zitadel && rm -rf .turbo && rm -rf node_modules"
},
"dependencies": {
"@bufbuild/protobuf": "^1.10.0"
"@bufbuild/protobuf": "^2.0.0"
},
"devDependencies": {
"@bufbuild/buf": "^1.36.0"

View File

@@ -3,7 +3,7 @@
"tasks": {
"generate": {
"outputs": ["zitadel/**"],
"cache": true
"cache": false
}
}
}

View File

@@ -4,7 +4,5 @@ export type ZitadelReactProps = {
};
export function ZitadelReactProvider({ dark, children }: ZitadelReactProps) {
return (
<div className={`${dark ? "ztdl-dark" : "ztdl-light"} `}>{children}</div>
);
return <div className={`${dark ? "ztdl-dark" : "ztdl-light"} `}>{children}</div>;
}

View File

@@ -8,12 +8,6 @@ export { SignInWithAzureAD } from "./components/SignInWithAzureAD";
export { SignInWithGithub } from "./components/SignInWithGithub";
export {
ZitadelReactProvider,
type ZitadelReactProps,
} from "./components/ZitadelReactProvider";
export { ZitadelReactProvider, type ZitadelReactProps } from "./components/ZitadelReactProvider";
export {
SignInWithIDP,
type SignInWithIDPProps,
} from "./components/SignInWithIDP";
export { SignInWithIDP, type SignInWithIDPProps } from "./components/SignInWithIDP";

797
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff